diff --git a/.gitignore b/.gitignore index d1eec5a..0ccb7ea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ _site _sass .sass-cache +docs/build +node_modules/ +package-lock.json diff --git a/LICENCE b/LICENCE index ce6bd97..0a04128 100644 --- a/LICENCE +++ b/LICENCE @@ -1,688 +1,7 @@ - GNU GENERAL PUBLIC LICENSE + GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - 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 General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. - - - - - - - - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. + Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. @@ -843,4 +162,4 @@ General Public License ever published by the Free Software Foundation. whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the -Library. \ No newline at end of file +Library. diff --git a/README.md b/README.md index 1741a12..e05d54e 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,109 @@ # timingsrc -Web Documentation for timngsrc available at [http://webtiming.github.io/timingsrc/](http://webtiming.github.io/timingsrc/) +Web Documentation for timingsrc available at [http://webtiming.github.io/timingsrc/](http://webtiming.github.io/timingsrc/) -Timingsrc includes source code and documentation for timing related libraries managed by [Multi-Device Timing Community Group](https://www.w3.org/community/webtiming/) +Timingsrc includes source code and documentation for timing related libraries initially managed by the [Multi-Device Timing Community Group](https://www.w3.org/community/webtiming/) (closed early 2025). The timingsrc library is available under the LGPL licence. ### Timing Object -[timingobject](v2/timingobject) +[timingobject](v2/timingobject) -This implements the [Timing Object Draft Spec](https://github.com/webtiming/timingobject) as well as a set of Timing Converters. +This implements the [Timing Object Draft Spec](https://github.com/webtiming/timingobject) as well as a set of Timing Converters. ### Sequencing [sequencing](v2/sequencing) This implements tools for timed sequencing based on the Timing Object. + + +### Compile Timingsrc v3 + + +Install Node and NPM (Node Packet Manager) + +#### Ubuntu Instructions + +Update if necessary + +```sh +sudo apt update +sudo apt-get update +``` + +Add NodeSource repository for Nodejs and NPM (Node Packet Manager) + +[Install Nodejs on Ubuntu](https://github.com/nodesource/distributions) + +```sh +curl -sL https://deb.nodesource.com/setup_14.x | sudo -E bash - + +# script output +# Run `sudo apt-get install -y nodejs` to install Node.js 14.x and npm +# You may also need development tools to build native addons: +# sudo apt-get install gcc g++ make +# To install the Yarn package manager, run: +# curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - +# echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list +# sudo apt-get update && sudo apt-get install yarn + + +# install both node and npm +sudo apt-get install -y nodejs +``` + + +#### Install Package Dependencies + +- Bundler: [Rollup](https://rollupjs.org/guide/en/). +- Script Minifier: [Terser](https://terser.org/) +- Bundler Plugin: [Rollup-Plugin-Terser](https://www.npmjs.com/package/rollup-plugin-terser) + + +Rollup needs global install so that it may be used by the compile.py script. + +```sh +npm install --global rollup +``` +Then go ahead and install the rest from package.json + +```sh +cd ~/timingsrc +npm install +``` + +Alternatively, install manually + +```sh +sudo npm install --global rollup +npm install terser +npm install rollup-plugin-terser +``` + +### Build v3 + +```sh +python3 compile.py v3 +``` +Build puts new files in *docs/lib* + +After build - commit all build files + +```sh +git commit -am "build" +git push +``` + +### Deploy + +Switch to master and merge in new build files from develop. + +```sh +git checkout master +git merge develop +git push +git checkout develop +``` + diff --git a/build/almond-build-v2.1.js b/build/almond-build-v2.1.js deleted file mode 100644 index eae4f58..0000000 --- a/build/almond-build-v2.1.js +++ /dev/null @@ -1,10 +0,0 @@ -{ - baseUrl: '../v2.1', - name: '../build/almond', - include: ['timingsrc'], - //insertRequire: ['timingsrc'], - wrap: { - startFile: 'start.frag', - endFile: 'end.frag' - } -} \ No newline at end of file diff --git a/build/almond-build-v2.js b/build/almond-build-v2.js index 6874177..a067f19 100644 --- a/build/almond-build-v2.js +++ b/build/almond-build-v2.js @@ -7,4 +7,4 @@ startFile: 'start.frag', endFile: 'end.frag' } -} \ No newline at end of file +} diff --git a/build/build.py b/build/build.py deleted file mode 100755 index c4e294c..0000000 --- a/build/build.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/python3 - -import sys -import os -import subprocess - -versions = ["v1", "v2", "v2.1"] - -def build (version): - srcdir = os.path.join("../", version) - libdir = "../docs/lib" - - baseUrl = "baseUrl={}".format(srcdir) - almond = "almond-build-{}.js".format(version) - - # requirejs - unminified - out = "out={}/timingsrc-require-{}.js".format(libdir, version) - args = ["node", "r.js", "-o", baseUrl, "optimize=none", "name=timingsrc", out] - print(" ".join(args)) - subprocess.call(args) - - # requirejs - minified - out = "out={}/timingsrc-require-min-{}.js".format(libdir, version) - args = ["node", "r.js", "-o", baseUrl, "name=timingsrc", out] - print(" ".join(args)) - subprocess.call(args) - - # plain - unminified - out = "out={}/timingsrc-{}.js".format(libdir, version) - args = ["node", "r.js", "-o", almond, "optimize=none", out] - print(" ".join(args)) - subprocess.call(args) - - # plain - minified - out = "out={}/timingsrc-min-{}.js".format(libdir, version) - args = ["node", "r.js", "-o", almond, out] - print(" ".join(args)) - subprocess.call(args) - - -if __name__ == '__main__': - - if len(sys.argv) > 1: - _versions = [sys.argv[1]] - else: - _versions = versions - - for version in _versions: - build(version) - - diff --git a/compile.py b/compile.py new file mode 100755 index 0000000..671282e --- /dev/null +++ b/compile.py @@ -0,0 +1,98 @@ +#!/usr/bin/python3 + +import sys +import os +import subprocess + +versions = ["v1", "v2", "v3"] + + +def build(version): + srcdir = os.path.join(".", version) + libdir = "docs/lib" + + if version == "v3": + + entry_file = os.path.join(srcdir, "index.js") + rollup_config = os.path.join(".", "rollup.config.js") + + # classic - unminified + out = os.path.join(libdir, f'timingsrc-{version}.js') + args = ["rollup", "-m", "-f", "iife", "--name", "TIMINGSRC", "-o", out, entry_file] + print(" ".join(args)) + subprocess.call(args) + + # classic - minified + out = os.path.join(libdir, f'timingsrc-min-{version}.js') + args = ["rollup", + "-f", "iife", + "--name", "TIMINGSRC", + "-m", + "--environment", "BUILD:production", + "-c", rollup_config, + "-o", out, + entry_file] + print(" ".join(args)) + subprocess.call(args) + + # module export - unminified + out = os.path.join(libdir, f'timingsrc-esm-{version}.js') + args = ["rollup", "-m", "-f", "es", "-o", out, entry_file] + print(" ".join(args)) + subprocess.call(args) + + # module export - minified + out = os.path.join(libdir, f'timingsrc-min-esm-{version}.js') + args = ["rollup", + "-f", "es", + "-m", + "--environment", "BUILD:production", + "-c", rollup_config, + "-o", out, + entry_file] + print(" ".join(args)) + subprocess.call(args) + + else: + builddir = "build" + baseUrl = "baseUrl={}".format(srcdir) + almond = os.path.join(builddir, "almond-build-{}.js".format(version)) + rjs = os.path.join(builddir, "r.js") + + # requirejs - unminified + out = "out={}/timingsrc-require-{}.js".format(libdir, version) + args = [ + "node", rjs, "-o", baseUrl, + "optimize=none", "name=timingsrc", out + ] + print(" ".join(args)) + subprocess.call(args) + + # requirejs - minified + out = "out={}/timingsrc-require-min-{}.js".format(libdir, version) + args = ["node", rjs, "-o", baseUrl, "name=timingsrc", out] + print(" ".join(args)) + subprocess.call(args) + + # plain - unminified + out = "out={}/timingsrc-{}.js".format(libdir, version) + args = ["node", rjs, "-o", almond, "optimize=none", out] + print(" ".join(args)) + subprocess.call(args) + + # plain - minified + out = "out={}/timingsrc-min-{}.js".format(libdir, version) + args = ["node", rjs, "-o", almond, out] + print(" ".join(args)) + subprocess.call(args) + + +if __name__ == '__main__': + + if len(sys.argv) > 1: + _versions = [sys.argv[1]] + else: + _versions = versions + + for version in _versions: + build(version) diff --git a/docs/doc/api_timingprovider.md b/docs/doc/api_timingprovider.md index f8d7f98..9246531 100644 --- a/docs/doc/api_timingprovider.md +++ b/docs/doc/api_timingprovider.md @@ -5,11 +5,11 @@ title: Timing Provider API - [Timing Provider Background](background_timingprovider.html) - [Timing Provider API](api_timingprovider.html) -- [Timing Provider Example](exp_timingprovider.html) +- [Shared Motion Timing Provider](shared_motion.html) This describes the API of the Timing Provider, as required by the timingsrc library (i.e. Timing Object). -Timing Providers are implemented and instansiated by third party code. +Timing Providers are implemented and instansiated by third party code. Timing provider objects are given to the timing object as parameter in the constructor. @@ -17,8 +17,8 @@ Timing provider objects are given to the timing object as parameter in the const #### TimingProviderState -The following values are used as readystates for the timing provider. TimingProviderState is available in -timingsrc library. State *OPEN* indicates that properties *skew* and *vector* are available. +The following values are used as readystates for the timing provider. TimingProviderState is available in +timingsrc library. State *OPEN* indicates that properties *skew* and *vector* are available. ```javascript var TimingProviderState = Object.freeze({ @@ -33,9 +33,9 @@ var TimingProviderState = Object.freeze({ #### StateVector -State vectors are communicated between timing provider and timing object. -Timestamp defines a point in time when values for position, velocity and acceleration were|are|will-be valid. -Timestamps (in seconds) are from the *timing provider clock*, i.e. clock_timingprovider = performance.now() + skew. +State vectors are communicated between timing provider and timing object. +Timestamp defines a point in time when values for position, velocity and acceleration were|are|will-be valid. +Timestamps (in seconds) are from the *timing provider clock*, i.e. clock_timingprovider = clock_client + skew. ```javascript var vector = { @@ -61,8 +61,10 @@ var state = timingProvider.readyState; #### .skew -Getter property for current skew estimate of the timing provider. The skew estimate is defined as follows. -clock_timingprovider = performance.now() + skew +Getter property for current skew estimate of the timing provider. The skew is defined as the difference between the clock used by the timing +provider and the clock used by the client. The time unit used for the skew is *seconds*. + +clock_timingprovider = clock_client + skew ```javascript var skew = timingProvider.skew; @@ -73,7 +75,7 @@ var skew = timingProvider.skew; #### .vector -Getter property for current vector of the timing provider. +Getter property for current vector of the timing provider. ```javascript var vector = timingProvider.vector; @@ -93,7 +95,7 @@ var range = timingProvider.range; --- #### .update() -Update is used by the timing object to request modification to the current vector. +Update is used by the timing object to request modification to the current vector. For online timing providers, this request is likely forwarded to an online timing provider service for processing. @@ -101,7 +103,7 @@ For online timing providers, this request is likely forwarded to an online timin timingProvider.update(vector); ``` -- param:{[StateVector](#statevector)} [vector] request modification as specified by vector. +- param:{[StateVector](#statevector)} [vector] request modification as specified by vector. State vectors given to update operation may be only be partially complete. For instance, the below operation only requests the position of the vector to be changed (leaving velocity and acceleration unchanged). @@ -114,8 +116,8 @@ timingProvider.update({position:14.0}); #### Event types Timing provider objects supports three event types ["readystatechange", "skewchange", "vectorchange"]. -- Event type "skewchange" is emitted whenever the [skew](#skew) property takes a new value. -- Event type "vectorchange" is emitted whenever the [vector](#vector) property takes a new value. +- Event type "skewchange" is emitted whenever the [skew](#skew) property takes a new value. +- Event type "vectorchange" is emitted whenever the [vector](#vector) property takes a new value. - Event type "readystatechange" is emmitted whenever the [readyState](#readystate) of the timing provider changes. Event handlers do not provide event arguments. diff --git a/docs/doc/exp_mediasync.md b/docs/doc/exp_mediasync.md index 1445d69..982418e 100644 --- a/docs/doc/exp_mediasync.md +++ b/docs/doc/exp_mediasync.md @@ -17,8 +17,8 @@ Synchronization and control of two video elements using timing object. Both vide - +

@@ -29,14 +29,14 @@ Synchronization and control of two video elements using timing object. Both vide **Player 1**

-

**Player 2**

-

- + Timing Object position

@@ -48,7 +48,7 @@ Sequencer events is used to control styling (red color). var to = new TIMINGSRC.TimingObject({provider:timingProvider}); // Sequencer -var s = new TIMINGSRC.Sequencer(to); +var s = new TIMINGSRC.Sequencer(to); // Load data var r = s.request(); @@ -58,12 +58,12 @@ Object.keys(data).forEach(function (key) { r.submit(); // Register Handlers -s.on("enter", function (e) { +s.on("change", function (e) { var el = document.getElementById(e.key); el.classList.add("active"); }); -s.on("exit", function (e) { +s.on("remove", function (e) { var el = document.getElementById(e.key); el.classList.remove("active"); }); -``` +``` diff --git a/docs/doc/online_windowsequencer.md b/docs/doc/online_windowsequencer.md index 82c420a..3855d18 100644 --- a/docs/doc/online_windowsequencer.md +++ b/docs/doc/online_windowsequencer.md @@ -7,10 +7,10 @@ appidmcorp: 8456579076771837888 + - [Sequencer Background](background_sequencer.html) -- [Sequencer API](api_sequencer.html) +- [Sequencer API](api_sequencer.html) - [Sequencer Usage](usage_sequencer.html) The Sequencer may also provide enter and exit events based on a moving window. Window endpoints are implemented by two timing objects. @@ -26,7 +26,7 @@ The two endpoints may therefore be controlled independently, though in this demo #### Demo

- + Active Window : [ , ]

@@ -48,9 +48,9 @@ The two endpoints may therefore be controlled independently, though in this demo var to = new TIMINGSRC.TimingObject({provider:timingProvider}); var toA = new TIMINGSRC.SkewConverter(to, -5.0); var toB = new TIMINGSRC.SkewConverter(to, 4.0); - + // Sequencer -var s = new TIMINGSRC.Sequencer(toA, toB); +var s = new TIMINGSRC.Sequencer(toA, toB); // Load Data var r = s.request(); @@ -60,13 +60,13 @@ Object.keys(data).forEach(function (key) { r.submit(); // Register Handlers -s.on("enter", function (e) { +s.on("change", function (e) { var el = document.getElementById(e.key); el.classList.add("active"); }); -s.on("exit", function (e) { +s.on("remove", function (e) { var el = document.getElementById(e.key); el.classList.remove("active"); }); -``` +``` diff --git a/docs/examples/mediasync.html b/docs/examples/mediasync.html index 2fa75f0..8119e50 100644 --- a/docs/examples/mediasync.html +++ b/docs/examples/mediasync.html @@ -67,8 +67,8 @@

MediaSync

- +

@@ -77,13 +77,13 @@

MediaSync

-

-

Test Axis

- - diff --git a/v2.1/test/test_interval.html b/v2.1/test/test_interval.html deleted file mode 100644 index 56a788e..0000000 --- a/v2.1/test/test_interval.html +++ /dev/null @@ -1,356 +0,0 @@ - - - - - - - - - - - -

Test Interval

- - diff --git a/v2.1/test/test_iterable.html b/v2.1/test/test_iterable.html deleted file mode 100644 index d092c3b..0000000 --- a/v2.1/test/test_iterable.html +++ /dev/null @@ -1,144 +0,0 @@ - - - - - - - - - - - -

Test Iterable

- - \ No newline at end of file diff --git a/v2.1/test/test_sequencer.html b/v2.1/test/test_sequencer.html deleted file mode 100644 index 2aafffd..0000000 --- a/v2.1/test/test_sequencer.html +++ /dev/null @@ -1,116 +0,0 @@ - - - - - - - - - - - -

Test Sequencer

-

-

-

-

- - - - - - -

-

- - - - -

- - \ No newline at end of file diff --git a/v2.1/test/test_timeout.html b/v2.1/test/test_timeout.html deleted file mode 100644 index 447bb45..0000000 --- a/v2.1/test/test_timeout.html +++ /dev/null @@ -1,180 +0,0 @@ - - - - - - - - -

Test Timeouts

-

- -

Timeouts

-
-

Sequencer

-
-

- - \ No newline at end of file diff --git a/v2.1/test/test_timingcallbacks.html b/v2.1/test/test_timingcallbacks.html deleted file mode 100644 index 2c2a496..0000000 --- a/v2.1/test/test_timingcallbacks.html +++ /dev/null @@ -1,59 +0,0 @@ - - - - - - - - - - -

Test Timing Callbacks

-
- - - - - - - diff --git a/v2.1/test/test_timinginteger.html b/v2.1/test/test_timinginteger.html deleted file mode 100644 index d3a17fa..0000000 --- a/v2.1/test/test_timinginteger.html +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - - - - - -

Test Timing Integer

-
- - - - -
- - diff --git a/v2.1/test/test_timingobject.html b/v2.1/test/test_timingobject.html deleted file mode 100644 index 68781b3..0000000 --- a/v2.1/test/test_timingobject.html +++ /dev/null @@ -1,153 +0,0 @@ - - - - - - - - - - -

Test Timing Object

- -

Source

-
- - - - - -

Converter 1

-
- - - - - -

Converter 2

-
- - - - - - - diff --git a/v2.1/test/test_timingprovider.html b/v2.1/test/test_timingprovider.html deleted file mode 100644 index ace3855..0000000 --- a/v2.1/test/test_timingprovider.html +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - -

Test Timing Provider

- -

Source

-
- - - - - - - diff --git a/v2.1/test/test_windowsequencer.html b/v2.1/test/test_windowsequencer.html deleted file mode 100644 index 258e920..0000000 --- a/v2.1/test/test_windowsequencer.html +++ /dev/null @@ -1,147 +0,0 @@ - - - - - - - - - - - - -

Test window based sequencing

- -

- - Active Interval : [ , ] -

-

- - - - - -

-

-

-

- - - \ No newline at end of file diff --git a/v2.1/timingobject/README.md b/v2.1/timingobject/README.md deleted file mode 100644 index 2b2b8ae..0000000 --- a/v2.1/timingobject/README.md +++ /dev/null @@ -1,47 +0,0 @@ - -# Timing Object and Timing Converters - -This implements the *Timing Object* as well as a set of *Timing Converters*. - - -### Timing Object - -The [Timing Object](https://github.com/webtiming/timingobject) is defined by [Multi-Device Timing Community Group](https://www.w3.org/community/webtiming/). - -- [timingobject.js](timingobject.js) Timing Object - - -### Timing Converters - -Timing Converters are useful when you need an alternative representation for a Timing Object. For instance, -if different media components refer to different timelines, *skewed* representations of a common Timing Object could make up for this. -*Scaling* the timeline might also be useful in some circumstances. In video playback the position of a Timing Object typically represents media offset in seconds. -Alternatively, frame numbers could be reported as position, with standard playback velocity being 24 or 25 (fps), depending on the media format. -Or, when working with music it might be sensible to use beat number as position, and beats per second (bps) as velocity. -Again, different representations might be required to integrate different media components, or simply to suit the preferences of different programmers. - -A Timing Converter provides an alternative representation for a Timing Object. - -- a Timing Converter is a Timing Object that depends on another Timing Object. -- the *timingsrc* property of a Timing Converter identifies its source Timing Object. -- a Timing Converter implements some modification relative to *timingsrc*, but never affect the *timingsrc*. -- different Timing Converters can depend on the same *timingsrc*. -- a Timing Converter can itself be the *timingsrc* of another Timing Converter. - -So, a hierarchy/chain of Timing Converters can be created, where all Timing Converters ultimately depend on a common Timing Object as root. -Timing Converters provide a simple modification. More complex modifications can be achieved by combining multiple Timing Converters. - -Some Timing Converters of common utility are provided in this module: - -- [skewconverter.js](skewconverter.js) skews the timeline. -- [scaleconverter.js](scaleconverter.js) scales the timeline. -- [loopconverter.js](loopconverter.js) transformes infinite timeline into looped timeline. -- [delayconverter.js](delayconverter.js) delayed replay of *timingsrc*. -- [timeshiftconverter.js](timeshiftconverter.js) time-shift ahead or after timingsrc. -- [rangeconverter.js](rangeconverter.js) enforces a range on position. - - -### Module - -- [timingbase.js](timingbase.js) implements base classes *TimingBase* and *ConverterBase* used to implement Timing Object and Timing Converters. -- [main.js](main.js) module definition \ No newline at end of file diff --git a/v2.1/timingobject/delayconverter.js b/v2.1/timingobject/delayconverter.js deleted file mode 100644 index 4bd68cc..0000000 --- a/v2.1/timingobject/delayconverter.js +++ /dev/null @@ -1,98 +0,0 @@ -/* - Copyright 2015 Norut Northern Research Institute - Author : Ingar Mæhlum Arntzen - - This file is part of the Timingsrc module. - - Timingsrc is free software: you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Timingsrc 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 Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License - along with Timingsrc. If not, see . -*/ - - -/* - DELAY CONVERTER - - Delay Converter introduces a positive time delay on a source timing object. - - Generally - if the source timing object has some value at time t, - then the delayConverter will provide the same value at time t + delay. - - Since the delay Converter is effectively replaying past events after the fact, - it is not LIVE and not open to interactivity (i.e. update) - -*/ - - -define(['./timingobject'], function (timingobject) { - - 'use strict'; - - var TimingObjectBase = timingobject.TimingObjectBase; - var inherit = TimingObjectBase.inherit; - - var DelayConverter = function (timingObject, delay) { - if (!(this instanceof DelayConverter)) { - throw new Error("Contructor function called without new operation"); - } - if (delay < 0) {throw new Error ("negative delay not supported");} - if (delay === 0) {throw new Error ("zero delay makes delayconverter pointless");} - TimingObjectBase.call(this, timingObject); - // fixed delay - this._delay = delay; - }; - inherit(DelayConverter, TimingObjectBase); - - // overrides - DelayConverter.prototype.onVectorChange = function (vector) { - /* - Vector's timestamp always time-shifted (back-dated) by delay - - Normal operation is to delay every incoming vector update. - This implies returning null to abort further processing at this time, - and instead trigger a later continuation. - - However, delay is calculated based on the timestamp of the vector (age), not when the vector is - processed in this method. So, for small small delays the age of the vector could already be - greater than delay, indicating that the vector is immediately valid and do not require delayed processing. - - This is particularly true for the first vector, which may be old. - - So we generally check the age to figure out whether to apply the vector immediately or to delay it. - */ - - // age of incoming vector - var age = this.clock.now() - vector.timestamp; - - // time-shift vector timestamp - vector.timestamp += this._delay; - - if (age < this._delay) { - // apply vector later - abort processing now - var self = this; - var delayMillis = (this._delay - age) * 1000; - setTimeout(function () { - self._process(vector); - }, delayMillis); - return null; - } - // apply vector immediately - continue processing - return vector; - }; - - DelayConverter.prototype.update = function (vector) { - // Updates are prohibited on delayed timingobjects - throw new Error ("update is not legal on delayed (non-live) timingobject"); - }; - - return DelayConverter; -}); \ No newline at end of file diff --git a/v2.1/timingobject/derivativeconverter.js b/v2.1/timingobject/derivativeconverter.js deleted file mode 100644 index b0accfb..0000000 --- a/v2.1/timingobject/derivativeconverter.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - Copyright 2015 Norut Northern Research Institute - Author : Ingar Mæhlum Arntzen - - This file is part of the Timingsrc module. - - Timingsrc is free software: you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Timingsrc 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 Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License - along with Timingsrc. If not, see . -*/ - -/* - DERIVATIVE CONVERTER - - this Converter implements the derivative of it source timing object. - - The velocity of timingsrc becomes the position of the Converter. - - This means that the derivative Converter allows sequencing on velocity of a timing object, - by attatching a sequencer on the derivative Converter. -*/ - -define(['./timingobject'], function (timingobject) { - - 'use strict'; - - var TimingObjectBase = timingobject.TimingObjectBase; - var inherit = TimingObjectBase.inherit; - - var DerivativeConverter = function (timingsrc) { - if (!(this instanceof DerivativeConverter)) { - throw new Error("Contructor function called without new operation"); - } - TimingObjectBase.call(this, timingsrc); - }; - inherit(DerivativeConverter, TimingObjectBase); - - // overrides - DerivativeConverter.prototype.onRangeChange = function (range) { - return [-Infinity, Infinity]; - }; - - // overrides - DerivativeConverter.prototype.onVectorChange = function (vector) { - var newVector = { - position : vector.velocity, - velocity : vector.acceleration, - acceleration : 0, - timestamp : vector.timestamp - }; - return newVector; - }; - - DerivativeConverter.prototype.update = function (vector) { - throw new Error("updates illegal on derivative of timingobject"); - }; - - return DerivativeConverter; -}); \ No newline at end of file diff --git a/v2.1/timingobject/loopconverter.js b/v2.1/timingobject/loopconverter.js deleted file mode 100644 index 6fe38c1..0000000 --- a/v2.1/timingobject/loopconverter.js +++ /dev/null @@ -1,186 +0,0 @@ -/* - Copyright 2015 Norut Northern Research Institute - Author : Ingar Mæhlum Arntzen - - This file is part of the Timingsrc module. - - Timingsrc is free software: you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Timingsrc 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 Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License - along with Timingsrc. If not, see . -*/ - - -/* - LOOP CONVERTER - - This is a modulo type transformation where the converter will be looping within - a given range. Potentially one could create an associated timing object keeping track of the - loop number. -*/ - - -define(['../util/motionutils', './timingobject'], function (motionutils, timingobject) { - - 'use strict'; - - var TimingObjectBase = timingobject.TimingObjectBase; - var inherit = TimingObjectBase.inherit; - - /* - Coordinate system based on counting segments - skew + n*length + offset === x - skew : coordinate system is shifted by skew, so that segment 0 starts at offset. - n : segment counter - length : segment length - offset : offset of value x into the segment where it lies - x: float point value - */ - var SegmentCoords = function (skew, length) { - this.skew = skew; - this.length = length; - }; - - /* - Static method - ovverride modulo to behave better for negative numbers - */ - SegmentCoords.mod = function (n, m) { - return ((n % m) + m) % m; - }; - - // get point representation from float - SegmentCoords.prototype.getPoint = function (x) { - return { - n : Math.floor((x-this.skew)/this.length), - offset : SegmentCoords.mod(x-this.skew,this.length) - }; - }; - - // get float value from point representation - SegmentCoords.prototype.getFloat = function (p) { - return this.skew + (p.n * this.length) + p.offset; - }; - - // transform float x into segment defined by other float y - // if y isnt specified - transform into segment [skew, skew + length] - SegmentCoords.prototype.transformFloat = function (x, y) { - y = (y === undefined) ? this.skew : y; - var xPoint = this.getPoint(x); - var yPoint = this.getPoint(y); - return this.getFloat({n:yPoint.n, offset:xPoint.offset}); - }; - - - /* - LOOP CONVERTER - */ - - var LoopConverter = function (timingsrc, range) { - if (!(this instanceof LoopConverter)) { - throw new Error("Contructor function called without new operation"); - } - TimingObjectBase.call(this, timingsrc, {timeout:true}); - /* - note : - if a range point of the loop converter is the same as a range point of timingsrc, - then there will be duplicate events - */ - this._range = range; - this._coords = new SegmentCoords(range[0], range[1]-range[0]); - }; - inherit(LoopConverter, TimingObjectBase); - - // transform value from coordiantes X of timing source - // to looper coordinates Y - LoopConverter.prototype._transform = function (x) { - return this._coords.transformFloat(x); - }; - - // transform value from looper coordinates Y into - // coordinates X of timing object - maintain relative diff - LoopConverter.prototype._inverse = function (y) { - var current_y = this.query().position; - var current_x = this.timingsrc.query().position; - var diff = y - current_y; - var x = diff + current_x; - // verify that x is witin range - return x; - }; - - // overrides - LoopConverter.prototype.query = function () { - if (this.vector === null) return {position:undefined, velocity:undefined, acceleration:undefined}; - var vector = motionutils.calculateVector(this.vector, this.clock.now()); - // trigger state transition if range violation is detected - if (vector.position > this._range[1]) { - this._process(this._calculateInitialVector()); - } else if (vector.position < this._range[0]) { - this._process(this._calculateInitialVector()); - } else { - // no range violation - return vector; - } - // re-evaluate query after state transition - return motionutils.calculateVector(this.vector, this.clock.now()); - }; - - // overrides - LoopConverter.prototype.update = function (vector) { - if (vector.position !== undefined && vector.position !== null) { - vector.position = this._inverse(vector.position); - } - return this.timingsrc.update(vector); - }; - - // overrides - LoopConverter.prototype._calculateTimeoutVector = function () { - var freshVector = this.query(); - var res = motionutils.calculateDelta(freshVector, this.range); - var deltaSec = res[0]; - if (deltaSec === null) return null; - var position = res[1]; - var vector = motionutils.calculateVector(freshVector, freshVector.timestamp + deltaSec); - vector.position = position; // avoid rounding errors - return vector; - }; - - // overrides - LoopConverter.prototype.onRangeChange = function(range) { - return this._range; - }; - - // overrides - LoopConverter.prototype.onTimeout = function (vector) { - return this._calculateInitialVector(); - }; - - // overrides - LoopConverter.prototype.onVectorChange = function (vector) { - return this._calculateInitialVector(); - }; - - LoopConverter.prototype._calculateInitialVector = function () { - // parent snapshot - var parentVector = this.timingsrc.query(); - // find correct position for looper - var position = this._transform(parentVector.position); - // find looper vector - return { - position: position, - velocity: parentVector.velocity, - acceleration: parentVector.acceleration, - timestamp: parentVector.timestamp - }; - }; - - return LoopConverter; -}); \ No newline at end of file diff --git a/v2.1/timingobject/rangeconverter.js b/v2.1/timingobject/rangeconverter.js deleted file mode 100644 index 6de7d0a..0000000 --- a/v2.1/timingobject/rangeconverter.js +++ /dev/null @@ -1,195 +0,0 @@ -/* - Copyright 2015 Norut Northern Research Institute - Author : Ingar Mæhlum Arntzen - - This file is part of the Timingsrc module. - - Timingsrc is free software: you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Timingsrc 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 Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License - along with Timingsrc. If not, see . -*/ - -/* - - RANGE CONVERTER - - The converter enforce a range on position. - - It only has effect if given range is a restriction on the range of the timingsrc. - Range converter will pause on range endpoints if timingsrc leaves the range. - Range converters will continue mirroring timingsrc once it comes into the range. -*/ - -define(['../util/motionutils', './timingobject'], function (motionutils, timingobject) { - - 'use strict'; - - var TimingObjectBase = timingobject.TimingObjectBase; - var inherit = TimingObjectBase.inherit; - var RangeState = motionutils.RangeState; - - - var state = function () { - var _state = RangeState.INIT; - var _range = null; - var is_special_state_change = function (old_state, new_state) { - // only state changes between INSIDE and OUTSIDE* are special state changes. - if (old_state === RangeState.OUTSIDE_HIGH && new_state === RangeState.OUTSIDE_LOW) return false; - if (old_state === RangeState.OUTSIDE_LOW && new_state === RangeState.OUTSIDE_HIGH) return false; - if (old_state === RangeState.INIT) return false; - return true; - } - var get = function () {return _state;}; - var set = function (new_state, new_range) { - - var absolute = false; // absolute change - var special = false; // special change - - // check absolute change - if (new_state !== _state || new_range !== _range) { - absolute = true; - } - // check special change - if (new_state !== _state) { - special = is_special_state_change(_state, new_state); - } - // range change - if (new_range !== _range) { - _range = new_range; - } - // state change - if (new_state !== _state) { - _state = new_state; - } - return {special:special, absolute:absolute}; - - } - return {get: get, set:set}; - }; - - - /* - Range converter allows a new (smaller) range to be specified. - */ - - var RangeConverter = function (timingObject, range) { - if (!(this instanceof RangeConverter)) { - throw new Error("Contructor function called without new operation"); - } - TimingObjectBase.call(this, timingObject, {timeout:true}); - /* - note : - if a range point of the loop converter is the same as a range point of timingsrc, - then there will be duplicate events - */ - this._state = state(); - // todo - check range - this._range = range; - }; - inherit(RangeConverter, TimingObjectBase); - - // overrides - RangeConverter.prototype.query = function () { - if (this._ready.value === false) { - return {position:undefined, velocity:undefined, acceleration:undefined, timestamp:undefined}; - } - // reevaluate state to handle range violation - var vector = motionutils.calculateVector(this._timingsrc.vector, this.clock.now()); - var state = motionutils.getCorrectRangeState(vector, this._range); - // detect range violation - only if timeout is set - if (state !== motionutils.RangeState.INSIDE && this._timeout !== null) { - this._preProcess(vector); - } - // re-evaluate query after state transition - return motionutils.calculateVector(this._vector, this.clock.now()); - }; - - // overridden - RangeConverter.prototype._calculateTimeoutVector = function () { - var freshVector = this._timingsrc.query(); - var res = motionutils.calculateDelta(freshVector, this.range); - var deltaSec = res[0]; - if (deltaSec === null) return null; - var position = res[1]; - var vector = motionutils.calculateVector(freshVector, freshVector.timestamp + deltaSec); - vector.position = position; // avoid rounding errors - return vector; - }; - - // override range - Object.defineProperty(RangeConverter.prototype, 'range', { - get : function () { - return [this._range[0], this._range[1]]; - }, - set : function (range) { - this._range = range; - // use vector from timingsrc to emulate new event from timingsrc - this._preProcess(this.timingsrc.vector); - } - }); - - // overrides - RangeConverter.prototype.onRangeChange = function(range) { - return this._range; - }; - - // overrides - RangeConverter.prototype.onTimeout = function (vector) { - return this.onVectorChange(vector); - }; - - // overrides - RangeConverter.prototype.onVectorChange = function (vector) { - // console.log("onVectorChange vector", vector); - // console.log("onVectorChange range", this._range); - var new_state = motionutils.getCorrectRangeState(vector, this._range); - // console.log("onVectorChange state", new_state); - var state_changed = this._state.set(new_state, this._range); - if (state_changed.special) { - // state transition between INSIDE and OUTSIDE - if (this._state.get() === RangeState.INSIDE) { - // OUTSIDE -> INSIDE, generate fake start event - // vector delivered by timeout - // forward event unchanged - } else { - // INSIDE -> OUTSIDE, generate fake stop event - vector = motionutils.checkRange(vector, this._range); - } - } - else { - // no state transition between INSIDE and OUTSIDE - if (this._state.get() === RangeState.INSIDE) { - // stay inside or first event inside - // forward event unchanged - } else { - // stay outside or first event outside - // forward if - // - first event outside - // - skip from outside-high to outside-low - // - skip from outside-low to outside-high - // - range change - // else drop - // - outside-high to outside-high (no range change) - // - outside-low to outside-low (no range change) - if (state_changed.absolute) { - vector = motionutils.checkRange(vector, this._range); - } else { - // drop event - return null; - } - } - } - return vector; - }; - - return RangeConverter; -}); \ No newline at end of file diff --git a/v2.1/timingobject/scaleconverter.js b/v2.1/timingobject/scaleconverter.js deleted file mode 100644 index fcbaccf..0000000 --- a/v2.1/timingobject/scaleconverter.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - Copyright 2015 Norut Northern Research Institute - Author : Ingar Mæhlum Arntzen - - This file is part of the Timingsrc module. - - Timingsrc is free software: you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Timingsrc 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 Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License - along with Timingsrc. If not, see . -*/ - -/* - SCALE CONVERTER - - Scaling by a factor 2 means that values of the timing object (position, velocity and acceleration) are multiplied by two. - For example, if the timing object represents a media offset in seconds, scaling it to milliseconds implies a scaling factor of 1000. - -*/ - - -define(['./timingobject'], function (timingobject) { - - 'use strict'; - - var TimingObjectBase = timingobject.TimingObjectBase; - var inherit = TimingObjectBase.inherit; - - var ScaleConverter = function (timingsrc, factor) { - if (!(this instanceof ScaleConverter)) { - throw new Error("Contructor function called without new operation"); - } - this._factor = factor; - TimingObjectBase.call(this, timingsrc); - }; - inherit(ScaleConverter, TimingObjectBase); - - // overrides - ScaleConverter.prototype.onRangeChange = function (range) { - return [range[0]*this._factor, range[1]*this._factor]; - }; - - // overrides - ScaleConverter.prototype.onVectorChange = function (vector) { - vector.position = vector.position * this._factor; - vector.velocity = vector.velocity * this._factor; - vector.acceleration = vector.acceleration * this._factor; - return vector; - }; - - ScaleConverter.prototype.update = function (vector) { - if (vector.position !== undefined && vector.position !== null) vector.position = vector.position / this._factor; - if (vector.velocity !== undefined && vector.velocity !== null) vector.velocity = vector.velocity / this._factor; - if (vector.acceleration !== undefined && vector.acceleration !== null) vector.acceleration = vector.acceleration / this._factor; - return this.timingsrc.update(vector); - }; - - return ScaleConverter; -}); \ No newline at end of file diff --git a/v2.1/timingobject/skewconverter.js b/v2.1/timingobject/skewconverter.js deleted file mode 100644 index ac9a8c3..0000000 --- a/v2.1/timingobject/skewconverter.js +++ /dev/null @@ -1,81 +0,0 @@ -/* - Copyright 2015 Norut Northern Research Institute - Author : Ingar Mæhlum Arntzen - - This file is part of the Timingsrc module. - - Timingsrc is free software: you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Timingsrc 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 Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License - along with Timingsrc. If not, see . -*/ - - -/* - SKEW CONVERTER - - Skewing the timeline by 2 means that the timeline position 0 of the timingsrc becomes position 2 of Converter. - -*/ - -define(['./timingobject'], function (timingobject) { - - 'use strict'; - - var TimingObjectBase = timingobject.TimingObjectBase; - var inherit = TimingObjectBase.inherit; - - var SkewConverter = function (timingsrc, skew, options) { - if (!(this instanceof SkewConverter)) { - throw new Error("Contructor function called without new operation"); - } - this._skew = skew; - TimingObjectBase.call(this, timingsrc, options); - }; - inherit(SkewConverter, TimingObjectBase); - - // overrides - SkewConverter.prototype.onRangeChange = function (range) { - range[0] = (range[0] === -Infinity) ? range[0] : range[0] + this._skew; - range[1] = (range[1] === Infinity) ? range[1] : range[1] + this._skew; - return range; - }; - - // overrides - SkewConverter.prototype.onVectorChange = function (vector) { - vector.position += this._skew; - return vector; - }; - - SkewConverter.prototype.update = function (vector) { - if (vector.position !== undefined && vector.position !== null) { - vector.position = vector.position - this._skew; - } - return this.timingsrc.update(vector); - }; - - - Object.defineProperty(SkewConverter.prototype, 'skew', { - get : function () { - return this._skew; - }, - set : function (skew) { - this._skew = skew; - // pick up vector from timingsrc - var src_vector = this.timingsrc.vector; - // use this vector to emulate new event from timingsrc - this._preProcess(src_vector); - } - }); - - - return SkewConverter; -}); \ No newline at end of file diff --git a/v2.1/timingobject/timeshiftconverter.js b/v2.1/timingobject/timeshiftconverter.js deleted file mode 100644 index a8ce706..0000000 --- a/v2.1/timingobject/timeshiftconverter.js +++ /dev/null @@ -1,73 +0,0 @@ -/* - Copyright 2015 Norut Northern Research Institute - Author : Ingar Mæhlum Arntzen - - This file is part of the Timingsrc module. - - Timingsrc is free software: you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Timingsrc 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 Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License - along with Timingsrc. If not, see . -*/ - -/* - TIMESHIFT CONVERTER - - Timeshift Converter timeshifts a timing object by timeoffset. - Positive timeoffset means that the timeshift Converter will run ahead of the source timing object. - Negative timeoffset means that the timeshift Converter will run behind the source timing object. - - Updates affect the converter immediately. This means that update vector must be re-calculated - to the value it would have at time-shifted time. Timestamps are not time-shifted, since the motion is still live. - For instance, (0, 1, ts) becomes (0+(1*timeshift), 1, ts) - - However, this transformation may cause range violation - - this happens only when timing object is moving. - - implementation requires range converter logic - - - range is infinite -*/ - - -define(['../util/motionutils', './timingobject'], function (motionutils, timingobject) { - - 'use strict'; - - var TimingObjectBase = timingobject.TimingObjectBase; - var inherit = TimingObjectBase.inherit; - - - var TimeShiftConverter = function (timingsrc, timeOffset) { - if (!(this instanceof TimeShiftConverter)) { - throw new Error("Contructor function called without new operation"); - } - - TimingObjectBase.call(this, timingsrc); - this._timeOffset = timeOffset; - }; - inherit(TimeShiftConverter, TimingObjectBase); - - // overrides - TimeShiftConverter.prototype.onRangeChange = function (range) { - return [-Infinity, Infinity]; - }; - - - // overrides - TimeShiftConverter.prototype.onVectorChange = function (vector) { - // calculate timeshifted vector - var newVector = motionutils.calculateVector(vector, vector.timestamp + this._timeOffset); - newVector.timestamp = vector.timestamp; - return newVector; - }; - - return TimeShiftConverter; -}); \ No newline at end of file diff --git a/v2.1/timingobject/timingobject.js b/v2.1/timingobject/timingobject.js deleted file mode 100644 index adbc638..0000000 --- a/v2.1/timingobject/timingobject.js +++ /dev/null @@ -1,731 +0,0 @@ -/* - Copyright 2015 Norut Northern Research Institute - Author : Ingar Mæhlum Arntzen - - This file is part of the Timingsrc module. - - Timingsrc is free software: you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Timingsrc 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 Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License - along with Timingsrc. If not, see . -*/ - - - -define(['../util/eventify', '../util/motionutils', '../util/masterclock'], function (eventify, motionutils, MasterClock) { - - 'use strict'; - - // Utility inheritance function. - var inherit = function (Child, Parent) { - var F = function () {}; // empty object to break prototype chain - hinder child prototype changes to affect parent - F.prototype = Parent.prototype; - Child.prototype = new F(); // child gets parents prototypes via F - Child.uber = Parent.prototype; // reference in parent to superclass - Child.prototype.constructor = Child; // resetting constructor pointer - }; - - - // Polyfill for performance.now as Safari on ios doesn't have it... - (function(){ - if ("performance" in window === false) { - window.performance = {}; - window.performance.offset = new Date().getTime(); - } - if ("now" in window.performance === false){ - window.performance.now = function now(){ - return new Date().getTime() - window.performance.offset; - }; - } - })(); - - - /* - TIMING BASE - - abstract base class for objects that may be used as timingsrc - - essential internal state - - range, vector - - external methods - query, update - - events - on/off "change", "timeupdate" - - internal methods for range timeouts - - defines internal processing steps - - preProcess(vector) <- from external timingobject - - vector = onChange(vector) - - process(vector) <- from timeout or preProcess - - process (vector) - - set internal vector - - postProcess(vector) - - renew range timeout - - postprocess (vector) - - emit change event and timeupdate event - - turn periodic timeupdate on or off - - individual steps in this structure may be specialized - by subclasses (i.e. timing converters) - */ - - - var TimingBase = function (options) { - - this._options = options || {}; - - // cached vector - this._vector = { - position : undefined, - velocity : undefined, - acceleration : undefined, - timestamp : undefined - }; - - // cached range - this._range = [undefined,undefined]; - - // readiness - this._ready = new eventify.EventBoolean(false, {init:true}); - - // exported events - eventify.eventifyInstance(this); - this.eventifyDefineEvent("change", {init:true}); // define change event (supporting init-event) - this.eventifyDefineEvent("timeupdate", {init:true}); // define timeupdate event (supporting init-event) - - // timeout support - this._timeout = null; // timeoutid for range violation etc. - this._tid = null; // timeoutid for timeupdate - if (!this._options.hasOwnProperty("timeout")) { - // range timeouts off by default - this._options.timeout = false; - } - }; - eventify.eventifyPrototype(TimingBase.prototype); - - - /* - - EVENTS - - */ - - /* - overrides how immediate events are constructed - specific to eventutils - - overrides to add support for timeupdate events - */ - TimingBase.prototype.eventifyMakeInitEvents = function (type) { - if (type === "change") { - return (this._ready.value === true) ? [undefined] : []; - } else if (type === "timeupdate") { - return (this._ready.value === true) ? [undefined] : []; - } - return []; - }; - - - /* - - API - - */ - - // version - Object.defineProperty(TimingBase.prototype, 'version', { - get : function () { return this._version; } - }); - - // ready or not - TimingBase.prototype.isReady = function () { - return this._ready.value; - }; - - // ready promise - Object.defineProperty(TimingBase.prototype, 'ready', { - get : function () { - var self = this; - return new Promise (function (resolve, reject) { - if (self._ready.value === true) { - resolve(); - } else { - var onReady = function () { - if (self._ready.value === true) { - self._ready.off("change", onReady); - resolve(); - } - }; - self._ready.on("change", onReady); - } - }); - } - }); - - // range - - Object.defineProperty(TimingBase.prototype, 'range', { - get : function () { - // copy range - return [this._range[0], this._range[1]]; - } - }); - - - // internal vector - Object.defineProperty(TimingBase.prototype, 'vector', { - get : function () { - // copy vector - return { - position : this._vector.position, - velocity : this._vector.velocity, - acceleration : this._vector.acceleration, - timestamp : this._vector.timestamp - }; - } - }); - - // internal clock - Object.defineProperty(TimingBase.prototype, 'clock', { - get : function () { throw new Error ("not implemented") } - }); - - // query - TimingBase.prototype.query = function () { - if (this._ready.value === false) { - return {position:undefined, velocity:undefined, acceleration:undefined, timestamp:undefined}; - } - // reevaluate state to handle range violation - var vector = motionutils.calculateVector(this._vector, this.clock.now()); - var state = motionutils.getCorrectRangeState(vector, this._range); - // detect range violation - only if timeout is set - if (state !== motionutils.RangeState.INSIDE && this._timeout !== null) { - this._preProcess(vector); - } - // re-evaluate query after state transition - return motionutils.calculateVector(this._vector, this.clock.now()); - }; - - // update - to be ovverridden - TimingBase.prototype.update = function (vector) { - throw new Error ("not implemented"); - }; - - TimingBase.prototype.checkUpdateVector = function(vector) { - if (vector == undefined) { - throw new Error ("drop update, illegal updatevector"); - } - - // todo - check that vector properties are numbers - var pos = vector.position; - var vel = vector.velocity; - var acc = vector.acceleration; - - if (pos == undefined && vel == undefined && acc == undefined) { - throw new Error ("drop update, noop"); - } - - // default values - var p = 0, v = 0, a = 0; - var now = vector.timestamp || this.clock.now(); - if (this.isReady()) { - var nowVector = motionutils.calculateVector(this._vector, now); - nowVector = motionutils.checkRange(nowVector, this._range); - p = nowVector.position; - v = nowVector.velocity; - a = nowVector.acceleration; - } - - pos = (pos != undefined) ? pos : p; - vel = (vel != undefined) ? vel : v; - acc = (acc != undefined) ? acc : a; - return { - position : pos, - velocity : vel, - acceleration : acc, - timestamp : now - }; - } - - - // shorthand accessors - Object.defineProperty(TimingBase.prototype, 'pos', { - get : function () { - return this.query().position; - } - }); - - Object.defineProperty(TimingBase.prototype, 'vel', { - get : function () { - return this.query().velocity; - } - }); - - Object.defineProperty(TimingBase.prototype, 'acc', { - get : function () { - return this.query().acceleration; - } - }); - - - /* - - INTERNAL METHODS - - */ - - - /* - do not override - Handle incoming vector, from "change" from external object - or from an internal timeout. - - onVectorChange is invoked allowing subclasses to specify transformation - on the incoming vector before processing. - */ - TimingBase.prototype._preProcess = function (vector) { - vector = this.onVectorChange(vector); - this._process(vector); - }; - - - // may be overridden by subclsaa - TimingBase.prototype.onRangeChange = function (range) { - return range; - }; - - /* - specify transformation - on the incoming vector before processing. - useful for Converters that do mathematical transformations, - or as a way to enforse range restrictions. - invoming vectors from external change events or internal - timeout events - - returning null stops further processing, exept renewtimeout - */ - TimingBase.prototype.onVectorChange = function (vector) { - return motionutils.checkRange(vector, this._range); - }; - - /* - core processing step after change event or timeout - assignes the internal vector - */ - TimingBase.prototype._process = function (vector) { - if (vector !== null) { - var old_vector = this._vector; - // update internal vector - this._vector = vector; - // trigger events - this._ready.value = true; - this._postProcess(this._vector); - } - // renew timeout - this._renewTimeout(); - }; - - /* - process a new vector applied in order to trigger events - overriding this is only necessary if external change events - need to be suppressed, - */ - TimingBase.prototype._postProcess = function (vector) { - // trigger change events - this.eventifyTriggerEvent("change"); - // trigger timeupdate events - this.eventifyTriggerEvent("timeupdate"); - var moving = vector.velocity !== 0.0 || vector.acceleration !== 0.0; - if (moving && this._tid === null) { - var self = this; - this._tid = setInterval(function () { - self.eventifyTriggerEvent("timeupdate"); - }, 200); - } else if (!moving && this._tid !== null) { - clearTimeout(this._tid); - this._tid = null; - } - }; - - - /* - - TIMEOUTS - - */ - - /* - do not override - renew timeout is called during evenry processing step - in order to recalculate timeouts. - the calculation may be specialized in - _calculateTimeoutVector - */ - TimingBase.prototype._renewTimeout = function () { - if (this._options.timeout === true) { - this._clearTimeout(); - var vector = this._calculateTimeoutVector(); - if (vector === null) {return;} - var now = this.clock.now(); - var secDelay = vector.timestamp - now; - var self = this; - this._timeout = this.clock.setTimeout(function () { - self._process(self.onTimeout(vector)); - }, secDelay, {anchor: now, early: 0.005}); - } - }; - - /* - to be overridden - must be implemented by subclass if range timeouts are required - calculate a vector that will be delivered to _process(). - the timestamp in the vector determines when it is delivered. - */ - TimingBase.prototype._calculateTimeoutVector = function () { - var freshVector = this.query(); - var res = motionutils.calculateDelta(freshVector, this._range); - var deltaSec = res[0]; - if (deltaSec === null) return null; - if (deltaSec === Infinity) return null; - var position = res[1]; - var vector = motionutils.calculateVector(freshVector, freshVector.timestamp + deltaSec); - vector.position = position; // avoid rounding errors - return vector; - }; - - /* - do not override - internal utility function for clearing vector timeout - */ - TimingBase.prototype._clearTimeout = function () { - if (this._timeout !== null) { - this._timeout.cancel(); - this._timeout = null; - } - }; - - /* - to be overridden - subclass may implement transformation on timeout vector - before it is given to process. - returning null stops further processing, except renewtimeout - */ - TimingBase.prototype.onTimeout = function (vector) { - return motionutils.checkRange(vector, this._range); - }; - - - - - /* - INTERNAL PROVIDER - - Timing provider internal to the browser context - - Used by timing objects as timingsrc if no timingsrc is specified. - */ - - var InternalProvider = function (options) { - options = options || {}; - options.timeout = true; - TimingBase.call(this, options); - - // initialise internal state - this._clock = new MasterClock({skew:0}); - // range - this._range = this._options.range || [-Infinity, Infinity]; - // vector - var vector = this._options.vector || { - position : 0, - velocity : 0, - acceleration : 0 - }; - this.update(vector); - }; - inherit(InternalProvider, TimingBase); - - // internal clock - Object.defineProperty(InternalProvider.prototype, 'clock', { - get : function () { return this._clock; } - }); - - // update - InternalProvider.prototype.update = function (vector) { - var newVector = this.checkUpdateVector(vector); - this._preProcess(newVector); - return newVector; - }; - - - /* - EXTERNAL PROVIDER - - External Provider bridges the gap between the PROVIDER API (implemented by external timing providers) - and the TIMINGSRC API - - Objects implementing the TIMINGSRC API may be used as timingsrc (parent) for another timing object. - - - wraps a timing provider external - - handles some complexity that arises due to the very simple API of providers - - implements a clock for the provider - */ - - - // Need a polyfill for performance,now as Safari on ios doesn't have it... - - // local clock in seconds - var local_clock = { - now : function () {return performance.now()/1000.0;} - }; - - var ExternalProvider = function (provider, options) { - options = options || {}; - options.timeout = true; - TimingBase.call(this); - - this._provider = provider; - - this._provider_clock; // provider clock (may fluctuate based on live skew estimates) - /* - local clock - provider clock normalised to values of performance now - normalisation based on first skew measurement, so - */ - this._clock; - - - // register event handlers - var self = this; - this._provider.on("vectorchange", function () {self._onVectorChange();}); - this._provider.on("skewchange", function () {self._onSkewChange();}); - - // check if provider is ready - if (this._provider.skew != undefined) { - var self = this; - Promise.resolve(function () { - self._onSkewChange(); - }); - } - }; - inherit(ExternalProvider, TimingBase); - - // internal clock - Object.defineProperty(ExternalProvider.prototype, 'clock', { - get : function () { return this._clock; } - }); - - // internal provider object - Object.defineProperty(ExternalProvider.prototype, 'provider', { - get : function () { return this._provider; } - }); - - ExternalProvider.prototype._onSkewChange = function () { - if (!this._clock) { - this._provider_clock = new MasterClock({skew: this._provider.skew}); - this._clock = new MasterClock({skew:0}); - } else { - this._provider_clock.adjust({skew: this._provider.skew}); - // provider clock adjusted with new skew - correct local clock similarly - // current_skew = clock_provider - clock_local - var current_skew = this._provider_clock.now() - this._clock.now(); - // skew delta = new_skew - current_skew - var skew_delta = this._provider.skew - current_skew; - this._clock.adjust({skew: skew_delta}); - } - if (!this.isReady() && this._provider.vector != undefined) { - // just became ready (onVectorChange has fired earlier) - this._range = this._provider.range; - this._preProcess(this._provider.vector); - } - }; - - ExternalProvider.prototype._onVectorChange = function () { - if (this._clock) { - // is ready (onSkewChange has fired earlier) - if (!this._range) { - this._range = this._provider.range; - } - this._preProcess(this._provider.vector); - } - }; - - - /* - - local timestamp of vector is set for each new vector, using the skew available at that time - - the vector then remains unchanged - - skew changes affect local clock, thereby affecting the result of query operations - - - one could imagine reevaluating the vector as well when the skew changes, - but then this should be done without triggering change events - - - ideally the vector timestamp should be a function of the provider clock - - */ - - - - // override timing base to recalculate timestamp - ExternalProvider.prototype.onVectorChange = function (provider_vector) { - // local_ts = provider_ts - skew - var local_ts = provider_vector.timestamp - this._provider.skew; - return { - position : provider_vector.position, - velocity : provider_vector.velocity, - acceleration : provider_vector.acceleration, - timestamp : local_ts - } - }; - - - // update - ExternalProvider.prototype.update = function (vector) { - return this._provider.update(vector); - }; - - - - /* - - TIMING OBJECT BASE - - */ - - var TimingObjectBase = function (timingsrc, options) { - TimingBase.call(this, options); - this._version = 4; - /* - store a wrapper function used as a callback handler from timingsrc - (if this was a prototype function - it would be shared by multiple objects thus - prohibiting them from subscribing to the same timingsrc) - */ - var self = this; - this._internalOnChange = function () { - var vector = self._timingsrc.vector; - self._preProcess(vector); - }; - this._timingsrc = undefined; - this.timingsrc = timingsrc; - }; - inherit(TimingObjectBase, TimingBase); - - - // attach inheritance function on base constructor for convenience - TimingObjectBase.inherit = inherit; - - // internal clock - Object.defineProperty(TimingObjectBase.prototype, 'clock', { - get : function () { return this._timingsrc.clock; } - }); - - TimingObjectBase.prototype.onRangeChange = function (range) { - return range; - }; - - // invoked just after timingsrc switch - TimingObjectBase.prototype.onSwitch = function () { - }; - - - /* - - timingsrc property and switching on assignment - - */ - Object.defineProperty(TimingObjectBase.prototype, 'timingsrc', { - get : function () { - if (this._timingsrc instanceof InternalProvider) { - return undefined - } else if (this._timingsrc instanceof ExternalProvider) { - return this._timingsrc.provider; - } else { - return this._timingsrc; - } - }, - set : function (timingsrc) { - // new timingsrc undefined - if (!timingsrc) { - var options; - if (!this._timingsrc) { - // first time - use options - options = { - vector : this._options.vector, - range : this._options.range - } - } else { - // not first time - use current state - options = { - vector : this._vector, - range : this._range - } - } - timingsrc = new InternalProvider(options); - } - else if ((timingsrc instanceof TimingObjectBase) === false) { - // external provider - try to wrap it - try { - timingsrc = new ExternalProvider(timingsrc); - } catch (e) { - console.log(timingsrc); - throw new Error ("illegal timingsrc - not instance of timing object base and not timing provider"); - } - } - // transformation when new timingsrc is ready - var self = this; - var doSwitch = function () { - // disconnect and clean up timingsrc - if (self._timingsrc) { - self._timingsrc.off("change", self._internalOnChange); - } - self._timingsrc = timingsrc; - if (self._timingsrc.range !== self._range) { - self._range = self.onRangeChange(self._timingsrc.range); - } - self.onSwitch(); - self._timingsrc.on("change", self._internalOnChange); - }; - if (timingsrc.isReady()) { - doSwitch(); - } else { - timingsrc.ready.then(function (){ - doSwitch(); - }); - } - } - }); - - // update - TimingObjectBase.prototype.update = function (vector) { - return this._timingsrc.update(vector); - }; - - - /* - Timing Object - */ - var TimingObject = function (options) { - options = options || {}; - var timingsrc = options.timingsrc || options.provider; - TimingObjectBase.call(this, timingsrc, options); - }; - inherit(TimingObject, TimingObjectBase); - - // module - return { - InternalProvider : InternalProvider, - ExternalProvider : ExternalProvider, - TimingObjectBase : TimingObjectBase, - TimingObject : TimingObject - }; -}); - - diff --git a/v2.1/timingsrc.js b/v2.1/timingsrc.js deleted file mode 100644 index 40afd45..0000000 --- a/v2.1/timingsrc.js +++ /dev/null @@ -1,61 +0,0 @@ - -/* - Written by Ingar Arntzen, Norut -*/ - -define(function(require, exports, module) { - - const DefaultSequencer = require('sequencing/sequencer'); - const WindowSequencer = require('sequencing/windowsequencer'); - const timingobject = require('timingobject/timingobject'); - const timingcallbacks = require('sequencing/timingcallbacks'); - - /* - Common constructor DefaultSequencer and WindowSequencer - */ - const Sequencer = function (toA, toB, _axis) { - if (toB === undefined) { - return new DefaultSequencer(toA, _axis); - } else { - return new WindowSequencer(toA, toB, _axis); - } - }; - - // Add clone prototype to both Sequencer and WindowSequencer - DefaultSequencer.prototype.clone = function (toA, toB) { - return Sequencer(toA, toB, this._axis); - }; - WindowSequencer.prototype.clone = function (toA, toB) { - return Sequencer(toA, toB, this._axis); - }; - - return { - version : "v2.1", - - // util - Interval: require('util/interval'), - eventify: require('util/eventify'), - - // Timing Object - TimingObject : timingobject.TimingObject, - - // Timing Converters - ConverterBase : timingobject.ConverterBase, - SkewConverter : require('timingobject/skewconverter'), - DelayConverter : require('timingobject/delayconverter'), - ScaleConverter : require('timingobject/scaleconverter'), - LoopConverter : require('timingobject/loopconverter'), - RangeConverter : require('timingobject/rangeconverter'), - TimeShiftConverter : require('timingobject/timeshiftconverter'), - DerivativeConverter : require('timingobject/derivativeconverter'), - - // Sequencing - - Axis: require('sequencing/axis'), - Sequencer : Sequencer, - setPointCallback : timingcallbacks.setPointCallback, - setIntervalCallback : timingcallbacks.setIntervalCallback, - TimingInteger : require('sequencing/timinginteger'), - ActiveCue : require('sequencing/activecue') - }; -}); diff --git a/v2.1/util/eventify.js b/v2.1/util/eventify.js deleted file mode 100644 index a635e1e..0000000 --- a/v2.1/util/eventify.js +++ /dev/null @@ -1,665 +0,0 @@ - -/* - Copyright 2015 Norut Northern Research Institute - Author : Ingar Mæhlum Arntzen - - This file is part of the Timingsrc module. - - Timingsrc is free software: you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Timingsrc 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 Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License - along with Timingsrc. If not, see . -*/ - - -define(function () { - - 'use strict'; - - /* - UTILITY - */ - - // unique ID generator - var id = (function(length) { - var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - return function (len) { // key length - len = len || length; - var text = ""; - for( var i=0; i < len; i++ ) - text += possible.charAt(Math.floor(Math.random() * possible.length)); - return text; - }; - })(10); // default key length - - - // concatMap - var concatMap = function (array, projectionFunctionThatReturnsArray, ctx) { - var results = []; - array.forEach(function (item) { - results.push.apply(results, projectionFunctionThatReturnsArray.call(ctx, item)); - }, ctx); - return results; - }; - - // standard inheritance function - var inherit = function (Child, Parent) { - var F = function () {}; // empty object to break prototype chain - hinder child prototype changes to affect parent - F.prototype = Parent.prototype; - Child.prototype = new F(); // child gets parents prototypes via F - Child.uber = Parent.prototype; // reference in parent to superclass - Child.prototype.constructor = Child; // resetting constructor pointer - }; - - // equality function for object values - function areEqual(a, b) { - if (a === b) return true; - if (typeof a !== typeof b) return false; - - // disallow array comparison - if (Array.isArray(a)) throw new Error("illegal parameter a, array not supported", a); - if (Array.isArray(b)) throw new Error("illegal parameter b, array not supported", b); - - if (typeof a === 'object' && typeof b === 'object') { - // Create arrays of property names - var aProps = Object.getOwnPropertyNames(a); - var bProps = Object.getOwnPropertyNames(b); - - // If number of properties is different, - // objects are not equivalent - if (aProps.length != bProps.length) { - return false; - } - - for (var i = 0; i < aProps.length; i++) { - var propName = aProps[i]; - - // If values of same property are not equal, - // objects are not equivalent - if (a[propName] !== b[propName]) { - return false; - } - } - // If we made it this far, objects - // are considered equivalent - return true; - } else { - return false; - } - }; - - - - /* - HANDLER MAP - */ - - - // handler bookkeeping for one event type - var HandlerMap = function () { - this._id = 0; - this._map = {}; // ID -> {handler:, ctx:, pending:, count: } - }; - - HandlerMap.prototype._newID = function () { - this._id += 1; - return this._id; - }; - - HandlerMap.prototype._getID = function (handler) { - var item; - var res = Object.keys(this._map).filter(function (id) { - item = this._map[id]; - return (item.handler === handler); - }, this); - return (res.length > 0) ? res[0] : -1; - }; - - HandlerMap.prototype.getItem = function (id) { - return this._map[id]; - }; - - HandlerMap.prototype.register = function (handler, ctx) { - var ID = this._getID(handler); - if (ID > -1) { - throw new Error("handler already registered"); - } - ID = this._newID(); - this._map[ID] = { - ID : ID, - handler: handler, - ctx : ctx, - count : 0, - pending : false - }; - return ID; - }; - - HandlerMap.prototype.unregister = function (handler) { - var ID = this._getID(handler); - if (ID !== -1) { - delete this._map[ID]; - } - }; - - HandlerMap.prototype.getItems = function () { - return Object.keys(this._map).map(function (id) { - return this.getItem(id); - }, this); - }; - - - - - - /* - - EVENTIFY - - Eventify brings eventing capabilities to any object. - - In particular, eventify supports the initial-event pattern. - Opt-in for initial events per event type. - - A protected event type "events" provides a callback with a batch of events in a list, - instead as individual callbacks. - - if initial-events are used - eventified object must implement this._makeInitEvents(type) - - expect [{type:type, e:eArg}] - - */ - - var eventifyInstance = function (object, options) { - /* - Default event name "events" will fire a list of events - */ - object._ID = id(4); - object._callbacks = {}; // type -> HandlerMap - object._immediateCallbacks = []; - object._eBuffer = []; // buffering events before dispatch - - options = options || {} - // special event "events" - // init flag for builtin event type "events" - // default true - if (options.init == undefined) { - options.init = true; - } - object._callbacks["events"] = new HandlerMap(); - object._callbacks["events"]._options = {init:options.init}; - - return object; - }; - - - var eventifyPrototype = function (_prototype) { - /* - DEFINE EVENT TYPE - type is event type (string) - {init:true} specifies init-event semantics for this event type - */ - _prototype.eventifyDefineEvent = function (type, options) { - if (type === "events") throw new Error("Illegal event type : 'events' is protected"); - options = options || {}; - options.init = (options.init === undefined) ? false : options.init; - this._callbacks[type] = new HandlerMap(); - this._callbacks[type]._options = options; - }; - - /* - MAKE INIT EVENTS - - Produce init events for a specific callback handler - right after on("type", callback) - Return list consistent with input .eventifyTriggerEvents - [{type: "type", e: e}] - If [] list is returned there will be no init events. - - Protected event type 'events' is handled automatically - - Implement - .eventifyMakeInitEvents(type) - - */ - - _prototype._eventifyMakeEItemList = function (type) { - var makeInitEvents = this.eventifyMakeInitEvents || function (type) {return [];}; - return makeInitEvents.call(this, type) - .map(function(e){ - return {type:type, e:e}; - }); - }; - - _prototype._eventifyMakeInitEvents = function (type) { - if (type !== "events") { - return this._eventifyMakeEItemList(type); - } else { - // type === 'events' - var typeList = Object.keys(this._callbacks).filter(function (key) { - return (key !== "events" && this._callbacks[key]._options.init === true); - }, this); - return concatMap(typeList, function(_type){ - return this._eventifyMakeEItemList(_type); - }, this); - } - }; - - /* - EVENT FORMATTER - - Format the structure of EventArgs. - Parameter e is the object that was supplied to triggerEvent - Parameter type is the event type that was supplied to triggerEvent - Default is to use 'e' given in triggerEvent unchanged. - - Note, for protected event type 'events', eventFormatter is also applied recursively - within the list of events - ex: { type: "events", e: [{type:"change",e:e1},]) - - Implement - .eventifyEventFormatter(type, e) to override default - */ - _prototype._eventifyEventFormatter = function (type, e) { - var eventFormatter = this.eventifyEventFormatter || function (type, e) {return e;}; - if (type === "events") { - // e is really eList - eventformatter on every e in list - e = e.map(function(eItem){ - return {type: eItem.type, e: eventFormatter(eItem.type, eItem.e)}; - }); - } - return eventFormatter(type,e); - }; - - /* - CALLBACK FORMATTER - - Format which parameters are included in event callback. - Returns a list of parameters. - Default is to exclude type and eInfo and just deliver the event supplied to triggerEvent - - Implement - .eventifyCallbackForamtter(type, e, eInfo) to override default - */ - _prototype._eventifyCallbackFormatter = function (type, e, eInfo) { - var callbackFormatter = this.eventifyCallbackFormatter || function (type, e, eInfo) { return [e];}; - return callbackFormatter.call(this, type, e, eInfo); - }; - - /* - TRIGGER EVENTS - - This is the hub - all events go through here - Control flow is broken using Promise.resolve().then(...); - Parameter is a list of objects where 'type' specifies the event type and 'e' specifies the event object. - 'e' may be undefined - - [{type: "type", e: e}] - */ - _prototype.eventifyTriggerEvents = function (eItemList) { - // check list for illegal events - eItemList.forEach(function (eItem) { - if (eItem.type === undefined) throw new Error("Illegal event type; undefined"); - if (eItem.type === "events") throw new Error("Illegal event type; triggering of events on protocted event type 'events'" ); - }, this); - if (eItemList.length === 0) return this; - /* - Buffer list of eItems so that iterative calls to eventifyTriggerEvents - will be emitted in one batch - */ - this._eBuffer.push.apply(this._eBuffer, eItemList); - if (this._eBuffer.length === eItemList.length) { - // eBuffer just became non-empty - initiate triggering of events - var self = this; - Promise.resolve().then(function () { - // trigger events from eBuffer - self._eventifyTriggerProtectedEvents(self._eBuffer); - self._eventifyTriggerRegularEvents(self._eBuffer); - // empty eBuffer - self._eBuffer = []; - // flush immediate callbacks - self._eventifyFlushImmediateCallbacks(); - }); - } - return this; - }; - - /* - TRIGGER EVENT - Shorthand for triggering a single event - */ - _prototype.eventifyTriggerEvent = function (type, e) { - return this.eventifyTriggerEvents([{type:type, e:e}]); - }; - - /* - Internal method for triggering events - - distinguish "events" from other event names - */ - _prototype._eventifyTriggerProtectedEvents = function (eItemList, handlerID) { - // trigger event list on protected event type "events" - this._eventifyTriggerEvent("events", eItemList, handlerID); - }; - - _prototype._eventifyTriggerRegularEvents = function (eItemList, handlerID) { - // trigger events on individual event types - eItemList.forEach(function (eItem) { - this._eventifyTriggerEvent(eItem.type, eItem.e, handlerID); - }, this); - }; - - /* - Internal method for triggering a single event. - - if handler specificed - trigger only on given handler (for internal use only) - - awareness of init-events - */ - _prototype._eventifyTriggerEvent = function (type, e, handlerID) { - var argList, e, eInfo = {}; - if (!this._callbacks.hasOwnProperty(type)) throw new Error("Unsupported event type " + type); - var handlerMap = this._callbacks[type]; - var init = handlerMap._options.init; - handlerMap.getItems().forEach(function (handlerItem) { - if (handlerID === undefined) { - // all handlers to be invoked, except those with initial pending - if (handlerItem.pending) { - return false; - } - } else { - // only given handler to be called - ensuring that it is not removed - if (handlerItem.ID === handlerID) { - eInfo.init = true; - handlerItem.pending = false; - } else { - return false; - } - } - // eInfo - if (init) { - eInfo.init = (handlerItem.ID === handlerID) ? true : false; - } - eInfo.count = handlerItem.count; - eInfo.src = this; - // formatters - e = this._eventifyEventFormatter(type, e); - argList = this._eventifyCallbackFormatter(type, e, eInfo); - try { - handlerItem.handler.apply(handlerItem.ctx, argList); - handlerItem.count += 1; - return true; - } catch (err) { - console.log("Error in " + type + ": " + handlerItem.handler + " " + handlerItem.ctx + ": ", err); - } - }, this); - return false; - }; - - /* - ON - - register callback on event type. Available directly on object - optionally supply context object (this) used on callback invokation. - */ - _prototype.on = function (type, handler, ctx) { - if (!handler || typeof handler !== "function") throw new Error("Illegal handler"); - if (!this._callbacks.hasOwnProperty(type)) throw new Error("Unsupported event type " + type); - var handlerMap = this._callbacks[type]; - // register handler - ctx = ctx || this; - var handlerID = handlerMap.register(handler, ctx); - // do initial callback - if supported by source - if (handlerMap._options.init) { - // flag handler - var handlerItem = handlerMap.getItem(handlerID); - handlerItem.pending = true; - var self = this; - var immediateCallback = function () { - var eItemList = self._eventifyMakeInitEvents(type); - if (eItemList.length > 0) { - if (type === "events") { - self._eventifyTriggerProtectedEvents(eItemList, handlerID); - } else { - self._eventifyTriggerRegularEvents(eItemList, handlerID); - } - } else { - // initial callback is noop - handlerItem.pending = false; - } - }; - this._immediateCallbacks.push(immediateCallback); - Promise.resolve().then(function () { - self._eventifyFlushImmediateCallbacks(); - }); - } - return this; - }; - - _prototype._eventifyFlushImmediateCallbacks = function () { - if (this._eBuffer.length === 0) { - var callbacks = this._immediateCallbacks; - this._immediateCallbacks = []; - callbacks.forEach(function (callback) { - callback(); - }); - } - // if buffer is non-empty, immediate callbacks will be flushed after - // buffer is emptied - }; - - - /* - OFF - Available directly on object - Un-register a handler from a specfic event type - */ - - _prototype.off = function (type, handler) { - if (this._callbacks[type] !== undefined) { - var handlerMap = this._callbacks[type]; - handlerMap.unregister(handler); - - } - return this; - }; - - - }; - - /* - BASE EVENT OBJECT - - Convenience base class allowing eventified classes to be derived using (prototypal) inheritance. - This is alternative approach, hiding the use of eventifyInstance and eventifyPrototype. - - */ - var BaseEventObject = function () { - eventifyInstance(this); - }; - eventifyPrototype(BaseEventObject.prototype); - - // make standard inheritance function available as static method on constructor. - BaseEventObject.inherit = inherit; - - - /* - EVENT BOOLEAN - - Single boolean variable, its value accessible through get and toggle methods. - Defines an event 'change' whenever the value of the variable is changed. - - initialised to false if initValue is not specified - - Note : implementation uses falsiness of input parameter to constructor and set() operation, - so eventBoolean(-1) will actually set it to true because - (-1) ? true : false -> true ! - */ - - var EventBoolean = function (initValue, options) { - if (!(this instanceof EventBoolean)) { - throw new Error("Contructor function called without new operation"); - } - BaseEventObject.call(this); - this._value = (initValue) ? true : false; - // define change event (supporting init-event) - this.eventifyDefineEvent("change", options); - }; - BaseEventObject.inherit(EventBoolean, BaseEventObject); - - // ovverride to specify initialevents - EventBoolean.prototype.eventifyMakeInitEvents = function (type) { - if (type === "change") { - return [this._value]; - } - return []; - }; - - /* ACCESSOR PROPERTIES */ - Object.defineProperty(EventBoolean.prototype, "value", { - get: function () { - return this._value; - }, - set: function (newValue) { - return this.set(newValue); - } - }); - - EventBoolean.prototype.get = function () { return this._value;}; - EventBoolean.prototype.set = function (newValue) { - newValue = (newValue) ? true : false; - if (newValue !== this._value) { - this._value = newValue; - this.eventifyTriggerEvent("change", newValue); - return true; - } - return false; - }; - - EventBoolean.prototype.toggle = function () { - var newValue = !this._value; - this._value = newValue; - this.eventifyTriggerEvent("change", newValue); - return true; - }; - - - - - - - /* - EVENT VARIABLE - - Single variable, its value accessible through get and set methods. - Defines an event 'change' whenever the value of the variable is changed. - - Event variable may alternatively have a src eventVariable. - If it does, setting values will simply be forwarded to the source, - and value changes in src will be reflected. - This may be used to switch between event variables, simply by setting the - src property. - */ - - - var EventVariable = function (initValue, options) { - options = options || {}; - options.eqFunc = options.eqFunc || areEqual; - this._options = options; - - BaseEventObject.call(this); - this._value = initValue; - this._src; - // define change event (supporting init-event) - this.eventifyDefineEvent("change", options); - - // onSrcChange - var self = this; - this._onSrcChange = function (value) { - self.set(value); - }; - }; - BaseEventObject.inherit(EventVariable, BaseEventObject); - - // ovverride to specify initialevents - EventVariable.prototype.eventifyMakeInitEvents = function (type) { - if (type === "change") { - return [this._value]; - } - return []; - }; - - Object.defineProperty(EventVariable.prototype, "value", { - get: function () { - return this._value; - }, - set: function (newValue) { - // only if src is not set - if (this._src === undefined) { - this.set(newValue); - } else { - this._src.value = newValue; - } - } - }); - - Object.defineProperty(EventVariable.prototype, "src", { - get : function () { - return this._src; - }, - set: function (newSrc) { - if (this._src) { - // disconnect from old src - this._src.off("change", this._onSrcChange); - } - // connect to new src - this._src = newSrc; - this._src.on("change", this._onSrcChange); - } - }); - - EventVariable.prototype.get = function () { return this._value;}; - - EventVariable.prototype.set = function (newValue) { - var eqFunc = this._options.eqFunc; - if (!eqFunc(newValue,this._value)) { - this._value = newValue; - this.eventifyTriggerEvent("change", newValue); - return true; - } - return false; - }; - - - // utility function to make promise out of event variable - var makeEventPromise = function (ev, target) { - target = (target !== undefined) ? target : true; - return new Promise (function (resolve, reject) { - var callback = function (value) { - if (value === target) { - resolve(); - ev.off("change", callback); - } - }; - ev.on("change", callback); - }); - }; - - - - - - - // module api - return { - eventifyPrototype : eventifyPrototype, - eventifyInstance : eventifyInstance, - BaseEventObject : BaseEventObject, - EventVariable : EventVariable, - EventBoolean : EventBoolean, - makeEventPromise : makeEventPromise - }; -}); diff --git a/v2.1/util/interval.js b/v2.1/util/interval.js deleted file mode 100644 index d1cc1da..0000000 --- a/v2.1/util/interval.js +++ /dev/null @@ -1,569 +0,0 @@ -/* - Copyright 2015 Norut Northern Research Institute - Author : Ingar Mæhlum Arntzen - - This file is part of the Timingsrc module. - - Timingsrc is free software: you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Timingsrc 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 Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License - along with Timingsrc. If not, see . -*/ - -define(function () { - - 'use strict'; - - - const isNumber = function(n) { - let N = parseFloat(n); - return (n===N && !isNaN(N)); - }; - - - /********************************************************* - - ENDPOINT - - Utilities for interval endpoints comparisons - - **********************************************************/ - - const endpoint = function () { - - // bit flag - const CLOSED_BIT = 0 - const RIGHT_BIT = 1 - - // endpoint mode - const MODE_LEFT_OPEN = 0; //bx000 - const MODE_LEFT_CLOSED = 1; //bx001 - const MODE_RIGHT_OPEN = 2; //bx010 - const MODE_RIGHT_CLOSED = 3; //bx011 - const MODE_VALUE = 4 //bx100 - - /* - endpoint order - p), [p, p, p], (p - */ - const order = []; - - order[MODE_RIGHT_OPEN] = 0; - order[MODE_LEFT_CLOSED] = 1; - order[MODE_VALUE] = 2; - order[MODE_RIGHT_CLOSED] = 3; - order[MODE_LEFT_OPEN] = 4; - - /* - get order - - given two endpoints return - two numbers representing their order - also accepts regular numbers as endpoints - - for points or endpoint values that are not - equal, these values convey order directly, - otherwise the order numbers 0-4 are returned - based on endpoint inclusion and direction - - parameters are either - - point (number) - - endpoint [value (number), right (bool), closed (bool)] - */ - - function get_order(e1, e2) { - - // support plain numbers (not endpoints) - if (e1.length === undefined) { - if (!isNumber(e1)) { - throw new Error("e1 not a number", e1); - } - e1 = [e1, undefined, undefined]; - } - if (e2.length === undefined) { - if (!isNumber(e2)) { - throw new Error("e2 not a number", e2); - } - e2 = [e2, undefined, undefined]; - } - - let [e1_val, e1_right, e1_closed] = e1; - let [e2_val, e2_right, e2_closed] = e2; - let e1_mode, e2_mode; - - if (e1_val != e2_val) { - // different values - return [e1_val, e2_val]; - } else { - // equal values - if (e1_closed === undefined) { - e1_mode = MODE_VALUE; - } else { - e1_closed = Boolean(e1_closed); - e1_right = Boolean(e1_right); - e1_mode = (+e1_closed << CLOSED_BIT) | (+e1_right << RIGHT_BIT); - } - if (e2_closed === undefined) { - e2_mode = MODE_VALUE; - } else { - e2_closed = Boolean(e2_closed); - e2_right = Boolean(e2_right); - e2_mode = (+e2_closed << CLOSED_BIT) | (+e2_right << RIGHT_BIT); - } - return [order[e1_mode], order[e2_mode]]; - } - } - - /* - return true if e1 is ordered before e2 - false if equal - */ - - function leftof(e1, e2) { - let [order1, order2] = get_order(e1, e2); - return (order1 < order2); - } - - /* - return true if e1 is ordered after e2 - false is equal - */ - - function rightof(e1, e2) { - let [order1, order2] = get_order(e1, e2); - return (order1 > order2); - } - - /* - return true if e1 is ordered equal to e2 - */ - - function equal(e1, e2) { - let [order1, order2] = get_order(e1, e2); - return (order1 == order2); - } - - /* - return -1 if ordering e1, e2 is correct - return 0 if e1 and e2 is equal - return 1 if ordering e1, e2 is incorrect - */ - - function compare(e1, e2) { - let [order1, order2] = get_order(e1, e2); - let diff = order1 - order2; - if (diff == 0) return 0; - return (diff > 0) ? 1 : -1; - } - - /* - human friendly endpoint representation - */ - function toString(e) { - if (e.length === undefined) { - return e.toString(); - } else { - let [val, right, closed] = e; - let s = val.toString() - if (right && closed) { - return s + "]"; - } else if (right && !closed) { - return s + ")"; - } else if (!right && closed) { - return "[" + s; - } else { - return "(" + s; - } - } - } - - return { - compare: compare, - toString: toString, - equal: equal, - rightof: rightof, - leftof: leftof - } - - }(); - - - /********************************************************* - INSIDE INTERVAL - ********************************************************** - - inside (e, interval) - - return true if given point e is inside interval - - **********************************************************/ - - function inside (e, interval) { - let e_low = [interval.low, false, interval.lowInclude]; - let e_high = [interval.high, true, interval.highInclude]; - return !endpoint.leftof(e, e_low) && !endpoint.rightof(e, e_high); - } - - - /********************************************************* - COMPARE INTERVALS - ********************************************************** - - compare (a, b) - param a Interval - param b Interval - returns IntervalRelation - - compares interval b to interval a - e.g. return value COVERED reads b is covered by a. - - cmp_1 = endpoint_compare(b_low, a_low); - cmp_2 = endpoint_compare(b_high, a_high); - - key = 10*cmp_1 + cmp_2 - - cmp_1 cmp_2 key relation - ===== ===== === ============================ - -1 -1 -11 OUTSIDE_LEFT, PARTIAL_LEFT - -1 0 -10 COVERS - -1 1 -9 COVERS - 0 -1 -1 COVERED - 0 0 0 EQUAL - 0 1 1 COVERS - 1 -1 9 COVERED - 1 0 10 COVERED - 1 1 11 OUTSIDE_RIGHT, OVERLAP_RIGHT - ===== ===== === ============================ - - **********************************************************/ - - // Interval Relations - - const OUTSIDE_LEFT = 1; - const OVERLAP_LEFT = 2; - const COVERED = 3; - const EQUAL = 4; - const COVERS = 5; - const OVERLAP_RIGHT = 6; - const OUTSIDE_RIGHT = 7; - - - function compare(a, b) { - if (! a instanceof Interval) { - // could be a number - if (isNumber(a)) { - a = new Interval(a); - } else { - throw new Error("a not interval", a); - } - } - if (! b instanceof Interval) { - // could be a number - if (isNumber(b)) { - b = new Interval(b); - } else { - throw new Error("b not interval", b); - } - } - // interval endpoints - let a_low = [a.low, false, a.lowInclude]; - let a_high = [a.high, true, a.highInclude]; - let b_low = [b.low, false, b.lowInclude]; - let b_high = [b.high, true, b.highInclude]; - - let cmp_1 = endpoint.compare(a_low, b_low); - let cmp_2 = endpoint.compare(a_high, b_high); - let key = cmp_1*10 + cmp_2; - - if (key == 11) { - // OUTSIDE_LEFT or PARTIAL_LEFT - if (endpoint.leftof(b_high, a_low)) { - return OUTSIDE_RIGHT; - } else { - return OVERLAP_RIGHT; - } - } else if ([-1, 9, 10].includes(key)) { - return COVERED; - } else if ([1, -9, -10].includes(key)) { - return COVERS; - } else if (key == 0) { - return EQUAL; - } else { - // key == -11 - // OUTSIDE_RIGHT, PARTIAL_RIGHT - if (endpoint.rightof(b_low, a_high)) { - return OUTSIDE_LEFT; - } else { - return OVERLAP_LEFT; - } - } - } - - - - /********************************************************* - INTERVAL ERROR - **********************************************************/ - - var IntervalError = function (message) { - this.name = "IntervalError"; - this.message = (message||""); - }; - IntervalError.prototype = Error.prototype; - - - /********************************************************* - INTERVAL - **********************************************************/ - - class Interval { - - constructor (low, high, lowInclude, highInclude) { - if (!(this instanceof Interval)) { - throw new Error("Contructor function called without new operation"); - } - var lowIsNumber = isNumber(low); - var highIsNumber = isNumber(high); - // new Interval(3.0) defines singular - low === high - if (lowIsNumber && high === undefined) high = low; - if (!isNumber(low)) throw new IntervalError("low not a number"); - if (!isNumber(high)) throw new IntervalError("high not a number"); - if (low > high) throw new IntervalError("low > high"); - if (low === high) { - lowInclude = true; - highInclude = true; - } - if (low === -Infinity) lowInclude = true; - if (high === Infinity) highInclude = true; - if (lowInclude === undefined) lowInclude = true; - if (highInclude === undefined) highInclude = false; - if (typeof lowInclude !== "boolean") throw new IntervalError("lowInclude not boolean"); - if (typeof highInclude !== "boolean") throw new IntervalError("highInclude not boolean"); - this.low = low; - this.high = high; - this.lowInclude = lowInclude; - this.highInclude = highInclude; - this.length = this.high - this.low; - this.singular = (this.low === this.high); - this.finite = (isFinite(this.low) && isFinite(this.high)); - } - - toString () { - var lowBracket = (this.lowInclude) ? "[" : "<"; - var highBracket = (this.highInclude) ? "]" : ">"; - var low = (this.low === -Infinity) ? "<--" : this.low; //.toFixed(2); - var high = (this.high === Infinity) ? "-->" : this.high; //.toFixed(2); - if (this.singular) - return lowBracket + low + highBracket; - return lowBracket + low + ',' + high + highBracket; - }; - - - /* - coversPoint (x) { - if (this.low < x && x < this.high) return true; - if (this.lowInclude && x === this.low) return true; - if (this.highInclude && x === this.high) return true; - return false; - }; - - // overlap : it exists at least one point x covered by both interval - overlapsInterval(other) { - if (other instanceof Interval === false) throw new IntervalError("paramenter not instance of Interval"); - // singularities - if (this.singular && other.singular) - return (this.low === other.low); - if (this.singular) - return other.coversPoint(this.low); - if (other.singular) - return this.coversPoint(other.low); - // not overlap right - if (this.high < other.low) return false; - if (this.high === other.low) { - return this.coversPoint(other.low) && other.coversPoint(this.high); - } - // not overlap left - if (this.low > other.high) return false; - if (this.low === other.high) { - return (this.coversPoint(other.high) && other.coversPoint(this.low)); - } - return true; - }; - - // Interval fully covering other interval - coversInterval (other) { - if (other instanceof Interval === false) throw new IntervalError("paramenter not instance of Interval"); - if (other.low < this.low || this.high < other.high) return false; - if (this.low < other.low && other.high < this.high) return true; - // corner case - one or both endpoints are the same (the other endpoint is covered) - if (this.low === other.low && this.lowInclude === false && other.lowInclude === true) - return false; - if (this.high === other.high && this.highInclude === false && other.highInclude === true) - return false; - return true; - }; - equals (other) { - if (this.low !== other.low) return false; - if (this.high !== other.high) return false; - if (this.lowInclude !== other.lowInclude) return false; - if (this.highInclude !== other.highInclude) return false; - return true; - }; - */ - - compare (other) { - return compare(this, other); - } - - equals (other) { - return compare(this, other) == EQUALS; - } - - outside (other) { - return [ - OUTSIDE_LEFT, OUTSIDE_RIGHT - ].includes(compare(this, other)); - } - - overlap (other) { - return [ - OVERLAP_LEFT, - OVERLAP_RIGHT - ].includes(compare(this, other)); - } - - covered (other) { - return compare(this, other) == COVERED; - } - - covers (other) { - return compare(this, other) == COVERS; - } - - - /* - a.hasEndpointInside(b) - - returns true if interval a has at least one endpoint inside interval b - - This is easy for most intervals, but there are some subtleties - when when interval a and b have one or two endpoints in common - - 4 ways for intervals to share an endpoint - - - a.high == b.low : - >< (a.high outside b) - >[ (a.high outside b) - ]< (a.high outside b) - ][ (a.high inside b) - - a.high == b.high: - >> (a.high inside b) - >] (a.high inside b) - ]> (a.high outside b) - ]] (a.high inside b) - - a.low == b.low : - << (a.low inside b) - <[ (a.low inside b) - [< (a.low outside b) - [[ (a.low inside b) - - a.low == b.high : - <> (a.low outside b) - [> (a.low outside b) - <] (a.low outside b) - [] (a.low inside b) - - */ - - /* - hasEndpointInside (b) { - const a = this; - // check if a is to the right of b - if (b.high < a.low) return false; - // check if a is to the left of b - if (a.high < b.low) return false; - // check if a.low is inside b - if (b.low < a.low && a.low < b.high) return true; - // check if a.high is inside b - if (b.low < a.high && a.high < b.high) return true; - - // special consideration if a and b share endpoint(s) - - // a.high shared - if (a.high == b.low) { - if (a.highInclude && b.lowInclude) return true; - } - if (a.high == b.high) { - if (!(a.highInclude && !b.highInclude)) return true; - } - // a.low shared - if (a.low == b.low) { - if (!(a.lowInclude && !b.lowInclude)) return true; - } - if (a.low == b.high) { - if (a.lowInclude && b.highInclude) return true; - } - return false; - }; - - */ - - } - - - /********************************************************* - COMPARE BY INTERVAL ENDPOINTS - ********************************************************** - - cmp functions for sorting intervals (ascending) based on - endpoint low or high - - use with array.sort() - - **********************************************************/ - - function _make_interval_cmp(low) { - return function cmp (a, b) { - let e1, e2; - if (low) { - e1 = [a.low, false, a.lowInclude] - e2 = [b.low, false, b.lowInclude] - } else { - e1 = [a.high, true, a.highInclude] - e2 = [b.high, true, b.highInclude] - } - return endpoint.compare(e1, e2); - } - } - - - /* - Add static variables to Interval class. - */ - Interval.OUTSIDE_LEFT = OUTSIDE_LEFT; - Interval.OVERLAP_LEFT = OVERLAP_LEFT; - Interval.COVERED = COVERED; - Interval.EQUAL = EQUAL; - Interval.COVERS = COVERS; - Interval.OVERLAP_RIGHT = OVERLAP_RIGHT; - Interval.OUTSIDE_RIGHT = OUTSIDE_RIGHT; - Interval.cmpLow = _make_interval_cmp(true); - Interval.cmpHigh = _make_interval_cmp(false); - Interval.pointInside = inside; - // expose only for testing - Interval.endpoint = endpoint; - - /* - Possibility for more interval methods such as union, intersection, - */ - - return Interval; -}); - diff --git a/v2.1/util/iterable.js b/v2.1/util/iterable.js deleted file mode 100644 index cf16ee7..0000000 --- a/v2.1/util/iterable.js +++ /dev/null @@ -1,236 +0,0 @@ - -define(function () { - - 'use strict'; - - /* - empty iterable - */ - var empty = function () { - // return iterable - return [].values(); - }; - - - /* - make iterable for array or slice of array - - array is an iterable, but if one does not - want to expose the array object itself, one - may instead expose its contents through an iterable - */ - var slice = function (array, start, stop) { - start = (start == undefined) ? 0 : start; - stop = (stop == undefined) ? array.length : stop; - // check start and stop values - start = Math.max(start, 0); - start = Math.min(start, array.length); - stop = Math.max(start, stop); - stop = Math.min(stop, array.length); - let i = start; - let next = function next() { - if (i < stop) { - return {done:false, value: array[i++]}; - } - return {done:true}; - }; - // return iterable - return { - next: next, - [Symbol.iterator]: function () {return this;} - }; - }; - - - /* - chain multiple iterable into one iterable - */ - var chain = function (...iterables) { - // get iterators - const iterators = iterables.map(function (iterable) { - return iterable[Symbol.iterator](); - }); - let i = 0; - const next = function () { - let item = iterators[i].next(); - while (item.done) { - // current iterator exhausted - // go to next iterator if any - if (i < iterators.length-1) { - i++; - item = iterators[i].next(); - - continue; - } else { - // all iterators exhausted - return {done:true}; - } - } - // item ready - return item; - }; - // return iterable - return { - next: next, - [Symbol.iterator]: function () {return this;} - }; - }; - - /* - map iterable - */ - var map = function (iterable, mapFunc) { - let it = iterable[Symbol.iterator](); - let next = function () { - let item = it.next(); - if (!item.done) { - return {done:false, value: mapFunc(item.value)} - } - return {done:true}; - }; - // return iterable - return { - next: next, - [Symbol.iterator]: function () {return this;} - }; - }; - - /* - filter iterable - */ - var filter = function (iterable, predFunc) { - let it = iterable[Symbol.iterator](); - let next = function () { - let item = it.next(); - while (!item.done) { - if (predFunc(item.value)) { - return item; - } - item = it.next(); - } - return {done:true}; - } - // return iterable - return { - next: next, - [Symbol.iterator]: function () {return this;} - }; - }; - - - /* - unique iterable - */ - var unique = function (iterable, valueFunc) { - if (valueFunc == undefined) { - valueFunc = function (value) {return value;}; - } - const s = new Set(); - let it = iterable[Symbol.iterator](); - let next = function () { - let item = it.next(); - while (!item.done) { - let value = valueFunc(item.value); - if (!s.has(value)) { - s.add(value); - return item; - } - item = it.next(); - } - return {done:true}; - } - // return iterable - return { - next: next, - [Symbol.iterator]: function () {return this;} - }; - }; - - - /* - concatMap - mapFunc returns an iterable - */ - var concatMap = function (iterable, mapFunc) { - let mainIterator = iterable[Symbol.iterator](); - let subIterator = [].values(); - let mainItem, subItem; - let next = function () { - subItem = subIterator.next(); - while (subItem.done) { - // move main iterator - mainItem = mainIterator.next(); - if (mainItem.done) { - // main iterator exhausted - return {done:true}; - } else { - // fetch new subIterator - subIterator = mapFunc(mainItem.value)[Symbol.iterator](); - subItem = subIterator.next(); - // continue while loop to check subItem - } - } - // sub item ok - return subItem; - }; - // return iterable - return { - next: next, - [Symbol.iterator]: function () {return this;} - }; - }; - - - var Iter = function (iterable) { - this.iterable = iterable; - /* - for conveinience - make Iter objects into real iterables - by exposing the iterable it wraps - so that we dont have to access the - wrapped iterable - */ - this.iterator = iterable[Symbol.iterator](); - this[Symbol.iterator] = function () { - return this; - } - - }; - - Iter.prototype.next = function () { - return this.iterator.next(); - }; - - Iter.prototype.concatMap = function (mapFunc) { - return new Iter(concatMap(this.iterable, mapFunc)); - }; - - Iter.prototype.unique = function (valueFunc) { - return new Iter(unique(this.iterable, valueFunc)); - }; - - Iter.prototype.filter = function (predFunc) { - return new Iter(filter(this.iterable, predFunc)); - }; - - Iter.prototype.map = function (mapFunc) { - return new Iter(map(this.iterable, mapFunc)); - }; - - - var I = function (iterable) { - return new Iter(iterable); - }; - - - return { - empty: empty, - slice: slice, - filter: filter, - chain: chain, - map: map, - unique: unique, - concatMap: concatMap, - I: I - }; -}); \ No newline at end of file diff --git a/v2.1/util/masterclock.js b/v2.1/util/masterclock.js deleted file mode 100644 index ddaa48d..0000000 --- a/v2.1/util/masterclock.js +++ /dev/null @@ -1,146 +0,0 @@ -/* - Copyright 2015 Norut Northern Research Institute - Author : Ingar Mæhlum Arntzen - - This file is part of the Timingsrc module. - - Timingsrc is free software: you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Timingsrc 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 Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License - along with Timingsrc. If not, see . -*/ - - -/* - MASTER CLOCK - - - MasterClock is the reference clock used by TimingObjects. - - It is implemented using performance.now, - but is skewed and rate-adjusted relative to this local clock. - - This allows it to be used as a master clock in a distributed system, - where synchronization is generally relative to some other clock than the local clock. - - The master clock may need to be adjusted in time, for instance as a response to - varying estimation of clock skew or drift. The master clock supports an adjust primitive for this purpose. - - What policy is used for adjusting the master clock may depend on the circumstances - and is out of scope for the implementation of the MasterClock. - This policy is implemented by the timing object. This policy may or may not - provide monotonicity. - - A change event is emitted every time the masterclock is adjusted. - - Vector values define - - position : absolute value of the clock in seconds - - velocity : how many seconds added per second (1.0 exactly - or very close) - - timestamp : timstamp from local system clock (performance) in seconds. Defines point in time where position and velocity are valid. - - If initial vector is not provided, default value is - {position: now, velocity: 1.0, timestamp: now}; - implying that master clock is equal to local clock. -*/ - -define(['./eventify', './timeoututils'], function (eventify, timeoututils) { - - 'use strict'; - - // Need a polyfill for performance,now as Safari on ios doesn't have it... - (function(){ - if ("performance" in window === false) { - window.performance = {}; - window.performance.offset = new Date().getTime(); - } - if ("now" in window.performance === false){ - window.performance.now = function now(){ - return new Date().getTime() - window.performance.offset; - }; - } - })(); - - // local clock in seconds - var localClock = { - now : function () {return performance.now()/1000.0;} - }; - - var calculateVector = function (vector, tsSec) { - if (tsSec === undefined) tsSec = localClock.now(); - var deltaSec = tsSec - vector.timestamp; - return { - position : vector.position + vector.velocity*deltaSec, - velocity : vector.velocity, - timestamp : tsSec - }; - }; - - var MasterClock = function (options) { - var now = localClock.now(); - options = options || {}; - this._vector = {position: now, velocity: 1.0, timestamp: now}; - // event support - eventify.eventifyInstance(this); - this.eventifyDefineEvent("change"); // define change event (no init-event) - // adjust - this.adjust(options); - }; - eventify.eventifyPrototype(MasterClock.prototype); - - - /* - ADJUST - - could also accept timestamp for velocity if needed? - - given skew is relative to local clock - - given rate is relative to local clock - */ - MasterClock.prototype.adjust = function (options) { - options = options || {}; - var now = localClock.now(); - var nowVector = this.query(now); - if (options.skew === undefined && options.rate === undefined) { - return; - } - this._vector = { - position : (options.skew !== undefined) ? now + options.skew : nowVector.position, - velocity : (options.rate !== undefined) ? options.rate : nowVector.velocity, - timestamp : nowVector.timestamp - } - this.eventifyTriggerEvent("change"); - }; - - /* - NOW - - calculates the value of the clock right now - - shorthand for query - */ - MasterClock.prototype.now = function () { - return calculateVector(this._vector, localClock.now()).position; - }; - - /* - QUERY - - calculates the state of the clock right now - - result vector includes position and velocity - */ - MasterClock.prototype.query = function (now) { - return calculateVector(this._vector, now); - }; - - /* - Timeout support - */ - MasterClock.prototype.setTimeout = function (callback, delay, options) { - return timeoututils.setTimeout(this, callback, delay, options); - }; - - return MasterClock; -}); \ No newline at end of file diff --git a/v2.1/util/motionutils.js b/v2.1/util/motionutils.js deleted file mode 100644 index 6a52fbf..0000000 --- a/v2.1/util/motionutils.js +++ /dev/null @@ -1,433 +0,0 @@ -/* - Copyright 2015 Norut Northern Research Institute - Author : Ingar Mæhlum Arntzen - - This file is part of the Timingsrc module. - - Timingsrc is free software: you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Timingsrc 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 Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License - along with Timingsrc. If not, see . -*/ - - -define(function () { - - 'use strict'; - - - - // Closure - (function() { - /** - * Decimal adjustment of a number. - * - * @param {String} type The type of adjustment. - * @param {Number} value The number. - * @param {Integer} exp The exponent (the 10 logarithm of the adjustment base). - * @returns {Number} The adjusted value. - */ - function decimalAdjust(type, value, exp) { - // If the exp is undefined or zero... - if (typeof exp === 'undefined' || +exp === 0) { - return Math[type](value); - } - value = +value; - exp = +exp; - // If the value is not a number or the exp is not an integer... - if (isNaN(value) || !(typeof exp === 'number' && exp % 1 === 0)) { - return NaN; - } - // Shift - value = value.toString().split('e'); - value = Math[type](+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp))); - // Shift back - value = value.toString().split('e'); - return +(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp)); - } - - // Decimal round - if (!Math.round10) { - Math.round10 = function(value, exp) { - return decimalAdjust('round', value, exp); - }; - } - // Decimal floor - if (!Math.floor10) { - Math.floor10 = function(value, exp) { - return decimalAdjust('floor', value, exp); - }; - } - // Decimal ceil - if (!Math.ceil10) { - Math.ceil10 = function(value, exp) { - return decimalAdjust('ceil', value, exp); - }; - } - })(); - - - // Calculate a snapshot of the motion vector, - // given initials conditions vector: [p0,v0,a0,t0] and t (absolute - not relative to t0) - // if t is undefined - t is set to now - var calculateVector = function(vector, tsSec) { - if (tsSec === undefined) { - throw new Error ("no ts provided for calculateVector"); - } - var deltaSec = tsSec - vector.timestamp; - return { - position : vector.position + vector.velocity*deltaSec + 0.5*vector.acceleration*deltaSec*deltaSec, - velocity : vector.velocity + vector.acceleration*deltaSec, - acceleration : vector.acceleration, - timestamp : tsSec - }; - }; - - - // RANGE STATE is used for managing/detecting range violations. - var RangeState = Object.freeze({ - INIT : "init", - INSIDE: "inside", - OUTSIDE_LOW: "outsidelow", - OUTSIDE_HIGH: "outsidehigh" - }); - - /* - A snapshot vector is checked with respect to range, - calclulates correct RangeState (i.e. INSIDE|OUTSIDE) - */ - var getCorrectRangeState = function (vector, range) { - var p = vector.position, - v = vector.velocity, - a = vector.acceleration; - if (p > range[1]) return RangeState.OUTSIDE_HIGH; - if (p < range[0]) return RangeState.OUTSIDE_LOW; - // corner cases - if (p === range[1]) { - if (v > 0.0) return RangeState.OUTSIDE_HIGH; - if (v === 0.0 && a > 0.0) return RangeState.OUTSIDE_HIGH; - } else if (p === range[0]) { - if (v < 0.0) return RangeState.OUTSIDE_LOW; - if (v == 0.0 && a < 0.0) return RangeState.OUTSIDE_HIGH; - } - return RangeState.INSIDE; - }; - - /* - - A snapshot vector is checked with respect to range. - Returns vector corrected for range violations, or input vector unchanged. - */ - var checkRange = function (vector, range) { - var state = getCorrectRangeState(vector, range); - if (state !== RangeState.INSIDE) { - // protect from range violation - vector.velocity = 0.0; - vector.acceleration = 0.0; - if (state === RangeState.OUTSIDE_HIGH) { - vector.position = range[1]; - } else vector.position = range[0]; - } - return vector; - }; - - - - // Compare values - var cmp = function (a, b) { - if (a > b) {return 1;} - if (a === b) {return 0;} - if (a < b) {return -1;} - }; - - // Calculate direction of movement at time t. - // 1 : forwards, -1 : backwards: 0, no movement - var calculateDirection = function (vector, tsSec) { - /* - Given initial vector calculate direction of motion at time t - (Result is valid only if (t > vector[T])) - Return Forwards:1, Backwards -1 or No-direction (i.e. no-motion) 0. - If t is undefined - t is assumed to be now. - */ - var freshVector = calculateVector(vector, tsSec); - // check velocity - var direction = cmp(freshVector.velocity, 0.0); - if (direction === 0) { - // check acceleration - direction = cmp(vector.acceleration, 0.0); - } - return direction; - }; - - // Given motion determined from p,v,a,t. - // Determine if equation p(t) = p + vt + 0.5at^2 = x - // has solutions for some real number t. - var hasRealSolution = function (p,v,a,x) { - if ((Math.pow(v,2) - 2*a*(p-x)) >= 0.0) return true; - else return false; - }; - - // Given motion determined from p,v,a,t. - // Determine if equation p(t) = p + vt + 0.5at^2 = x - // has solutions for some real number t. - // Calculate and return real solutions, in ascending order. - var calculateRealSolutions = function (p,v,a,x) { - // Constant Position - if (a === 0.0 && v === 0.0) { - if (p != x) return []; - else return [0.0]; - } - // Constant non-zero Velocity - if (a === 0.0) return [(x-p)/v]; - // Constant Acceleration - if (hasRealSolution(p,v,a,x) === false) return []; - // Exactly one solution - var discriminant = v*v - 2*a*(p-x); - if (discriminant === 0.0) { - return [-v/a]; - } - var sqrt = Math.sqrt(Math.pow(v,2) - 2*a*(p-x)); - var d1 = (-v + sqrt)/a; - var d2 = (-v - sqrt)/a; - return [Math.min(d1,d2),Math.max(d1,d2)]; - }; - - // Given motion determined from p,v,a,t. - // Determine if equation p(t) = p + vt + 0.5at^2 = x - // has solutions for some real number t. - // Calculate and return positive real solutions, in ascending order. - var calculatePositiveRealSolutions = function (p,v,a,x) { - var res = calculateRealSolutions(p,v,a,x); - if (res.length === 0) return []; - else if (res.length == 1) { - if (res[0] > 0.0) { - return [res[0]]; - } - else return []; - } - else if (res.length == 2) { - if (res[1] < 0.0) return []; - if (res[0] > 0.0) return [res[0], res[1]]; - if (res[1] > 0.0) return [res[1]]; - return []; - } - else return []; - }; - - // Given motion determined from p,v,a,t. - // Determine if equation p(t) = p + vt + 0.5at^2 = x - // has solutions for some real number t. - // Calculate and return the least positive real solution. - var calculateMinPositiveRealSolution = function (vector,x) { - var p = vector.position; - var v = vector.velocity; - var a = vector.acceleration; - var res = calculatePositiveRealSolutions(p,v,a,x); - if (res.length === 0) return null; - else return res[0]; - }; - - // Given motion determined from p0,v0,a0 - // (initial conditions or snapshot) - // Supply two posisions, posBefore < p0 < posAfter. - // Calculate which of these positions will be reached first, - // if any, by the movement described by the vector. - // In addition, calculate when this position will be reached. - // Result will be expressed as time delta relative to t0, - // if solution exists, - // and a flag to indicate Before (false) or After (true) - // Note t1 == (delta + t0) is only guaranteed to be in the - // future as long as the function - // is evaluated at time t0 or immediately after. - var calculateDelta = function (vector, range) { - // Time delta to hit posBefore - var deltaBeforeSec = calculateMinPositiveRealSolution(vector, range[0]); - // Time delta to hit posAfter - var deltaAfterSec = calculateMinPositiveRealSolution(vector, range[1]); - // Pick the appropriate solution - if (deltaBeforeSec !== null && deltaAfterSec !== null) { - if (deltaBeforeSec < deltaAfterSec) - return [deltaBeforeSec, range[0]]; - else - return [deltaAfterSec, range[1]]; - } - else if (deltaBeforeSec !== null) - return [deltaBeforeSec, range[0]]; - else if (deltaAfterSec !== null) - return [deltaAfterSec, range[1]]; - else return [null,null]; - }; - - - /* - calculate_solutions_in_interval (vector, d, plist) - - Find all intersects in time between a motion and a the - positions given in plist, within a given time-interval d. A - single position may be intersected at 0,1 or 2 two different - times during the interval. - - - vector = (p0,v0,a0) describes the initial conditions of - (an ongoing) motion - - - relative time interval d is used rather than a tuple of - absolute values (t_start, t_stop). This essentially means - that (t_start, t_stop) === (now, now + d). As a consequence, - the result is independent of vector[T]. So, if the goal is - to find the intersects of an ongoing motion during the next - d seconds, be sure to give a fresh vector from msv.query() - (so that vector[T] actually corresponds to now). - - - - plist is an array of objects with .point property - returning a floating point. plist represents the points - where we investigate intersects in time. - - The following equation describes how position varies with time - p(t) = 0.5*a0*t*t + v0*t + p0 - - We solve this equation with respect to t, for all position - values given in plist. Only real solutions within the - considered interval 0<=t<=d are returned. Solutions are - returned sorted by time, thus in the order intersects will - occur. - - */ - var sortFunc = function (a,b){return a[0]-b[0];}; - var calculateSolutionsInInterval2 = function(vector, deltaSec, plist) { - var solutions = []; - var p0 = vector.position; - var v0 = vector.velocity; - var a0 = vector.acceleration; - for (var i=0; i 0 => p_turning minimum - // a0 < 0 => p_turning maximum - if (a0 > 0.0) { - return [p_turning, Math.max(p0, p1)]; - } - else { - return [Math.min(p0,p1), p_turning]; - } - } - } - // no turning point or turning point was not reached - return [Math.min(p0,p1), Math.max(p0,p1)]; - }; - - - // return module object - return { - calculateVector : calculateVector, - calculateDirection : calculateDirection, - calculateMinPositiveRealSolution : calculateMinPositiveRealSolution, - calculateDelta : calculateDelta, - calculateInterval : calculateInterval, - calculateSolutionsInInterval : calculateSolutionsInInterval, - calculateSolutionsInInterval2 : calculateSolutionsInInterval2, - getCorrectRangeState : getCorrectRangeState, - checkRange : checkRange, - RangeState : RangeState - }; -}); - diff --git a/v2.1/util/multimap.js b/v2.1/util/multimap.js deleted file mode 100644 index bb4333f..0000000 --- a/v2.1/util/multimap.js +++ /dev/null @@ -1,128 +0,0 @@ -/* - Copyright 2015 Norut Northern Research Institute - Author : Ingar Mæhlum Arntzen - - This file is part of the Timingsrc module. - - Timingsrc is free software: you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Timingsrc 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 Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License - along with Timingsrc. If not, see . -*/ - -define (function () { - - 'use strict'; - - /* - MULTI MAP - - MultiMap stores (key,value) tuples - - one key may be bound to multiple values - - protection from duplicate (key, value) bindings. - - values are not assumed to be unique, i.e., the same value may be - associated with multiple keys. - - MultiMap supports setting and removing of (key,value) bindings. - - - set (key, value) - - delete (key, value) - - Could have used Set instead of Array for values, - but the assumption is that there will be a big key set - (implying many Set objects), with few values in each. - Sets would have provided protection for duplicates, and - would likely also be faster on remove. - - Equality of values is regular object equality by default, - but may be relaxed to mean equality of object property by specifying a - propertyname. - - */ - - var MultiMap = function (options) { - this.options = options || {}; - // value - if (typeof this.options.value === "string") { - let propertyName = this.options.value; - - this.value = function (obj) {return obj[propertyName]}; - } else { - this.value = function (x) {return x;}; - } - this.map = new Map(); // key -> [obj0, obj1,...] - }; - - MultiMap.prototype.setAll = function (items) { - let len_items = items.length; - let values, key, value; - for (let i=0; i -1) { - values.splice(idx, 1); - // remove key if values is left empty - if (values.length === 0) { - this.map.delete(key); - } - } - } - } - }; - - MultiMap.prototype.delete = function (key, value) { - return this.deletAll([[key, value]]); - }; - - MultiMap.prototype.has = function (key) { - return this._map.has(key); - }; - - MultiMap.prototype.keys = function () { - return this._map.keys(); - }; - - MultiMap.prototype.get = function (key) { - return this._map.get(key); - }; - - return MultiMap; -}); - - diff --git a/v2.1/util/timeoututils.js b/v2.1/util/timeoututils.js deleted file mode 100644 index 56ec3ab..0000000 --- a/v2.1/util/timeoututils.js +++ /dev/null @@ -1,125 +0,0 @@ -/* - Copyright 2015 Norut Northern Research Institute - Author : Ingar Mæhlum Arntzen - - This file is part of the Timingsrc module. - - Timingsrc is free software: you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Timingsrc 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 Lesser General Public License for more details. - - You should have received a copy of the GNU Lesser General Public License - along with Timingsrc. If not, see . -*/ - - -define(function () { - - 'use strict'; - - /* - TIMEOUT - - Wraps setTimeout() to implement improved version - - guarantee that timeout does not wake up too early - - offers precise timeout by "busy"-looping just before timeout - - wraps a single timeout - - clock operates in seconds - - parameters expected in seconds - breaking conformance with setTimeout - - wakes up 3 seconds before on long timeouts to readjust - */ - - var Timeout = function (clock, callback, delay, options) { - // clock - this._clock = clock; // seconds - var now = this._clock.now(); // seconds - // timeout - this._tid = null; - this._callback = callback; - this._delay_counter = 0; - this._options = options || {}; - - // options - this._options.anchor = this._options.anchor || now; // seconds - this._options.early = Math.abs(this._options.early) || 0; // seconds - this._target = this._options.anchor + delay; // seconds - - // Initialise - var self = this; - window.addEventListener("message", this, true); // this.handleEvent - var time_left = this._target - this._clock.now(); // seconds - if (time_left > 10) { - // long timeout > 10s - wakeup 3 seconds earlier to readdjust - this._tid = setTimeout(function () {self._ontimeout();}, time_left - 3000); - } else { - // wake up just before - this._tid = setTimeout(function () {self._ontimeout();}, (time_left - self._options.early)*1000); - } - }; - - Object.defineProperty(Timeout.prototype, 'target', { - get : function () { - return this._target; - } - }); - - // Internal function - Timeout.prototype._ontimeout = function () { - if (this._tid !== null) { - var time_left = this._target - this._clock.now(); // seconds - if (time_left <= 0) { - // callback due - this.cancel(); - this._callback(); - } else if (time_left > this._options.early) { - // wakeup before target - options early sleep more - var self = this; - this._tid = setTimeout(function () {self._ontimeout();}, (time_left - this._options.early)*1000); - } else { - // wake up just before (options early) - event loop - this._smalldelay(); - } - } - }; - - // Internal function - handler for small delays - Timeout.prototype.handleEvent = function (event) { - if (event.source === window && event.data.indexOf("smalldelaymsg_") === 0) { - event.stopPropagation(); - // ignore if timeout has been canceled - var the_tid = parseInt(event.data.split("_")[1]); - if (this._tid !== null && this._tid === the_tid) { - this._ontimeout(); - } - } - }; - - Timeout.prototype._smalldelay = function () { - this._delay_counter ++; - var self = this; - window.postMessage("smalldelaymsg_" + self._tid, "*"); - }; - - Timeout.prototype.cancel = function () { - if (this._tid !== null) { - clearTimeout(this._tid); - this._tid = null; - var self = this; - window.removeEventListener("message", this, true); - } - }; - - // return module object - return { - setTimeout: function (clock, callback, delay, options) { - return new Timeout(clock, callback, delay, options); - } - }; -}); - diff --git a/v2/test/test_timeout.html b/v2/test/test_timeout.html index 447bb45..e3a425f 100644 --- a/v2/test/test_timeout.html +++ b/v2/test/test_timeout.html @@ -1,7 +1,7 @@ - + + + +

Test Dataset

+ + diff --git a/v3/test/dataset/test_dataset_lookup.html b/v3/test/dataset/test_dataset_lookup.html new file mode 100644 index 0000000..8d54d49 --- /dev/null +++ b/v3/test/dataset/test_dataset_lookup.html @@ -0,0 +1,313 @@ + + + + + + + +

Test Dataset

+ + diff --git a/v3/test/dataset/test_dataset_stress.html b/v3/test/dataset/test_dataset_stress.html new file mode 100644 index 0000000..c8d5827 --- /dev/null +++ b/v3/test/dataset/test_dataset_stress.html @@ -0,0 +1,317 @@ + + + + + + + + +

Test Dataset

+ + diff --git a/v3/test/dataset/test_dataset_update.html b/v3/test/dataset/test_dataset_update.html new file mode 100644 index 0000000..23e4474 --- /dev/null +++ b/v3/test/dataset/test_dataset_update.html @@ -0,0 +1,964 @@ + + + + + + + + +

Test Dataset

+ + diff --git a/v3/test/dataset/test_subset.html b/v3/test/dataset/test_subset.html new file mode 100644 index 0000000..83dd53b --- /dev/null +++ b/v3/test/dataset/test_subset.html @@ -0,0 +1,79 @@ + + + + + + + + +

Test Dataview

+ +

Dataset

+
+

Dataview

+
+ + diff --git a/v3/test/dist/test_timingsrc.html b/v3/test/dist/test_timingsrc.html new file mode 100644 index 0000000..ab07755 --- /dev/null +++ b/v3/test/dist/test_timingsrc.html @@ -0,0 +1,259 @@ + + + + + + + + + + + +

Test Sequencer

+
+

+ + + + + +

+

+ + + + + +

+

+ + + + + +

+

Update

+

+ + + + +

+

Timed Data

+

Active cues in red color

+

+

+

+ + + diff --git a/v3/test/dist/test_timingsrc_online.html b/v3/test/dist/test_timingsrc_online.html new file mode 100644 index 0000000..bc4f31b --- /dev/null +++ b/v3/test/dist/test_timingsrc_online.html @@ -0,0 +1,255 @@ + + + + + + + + + +

Test Sequencer

+
+

+ + + + + +

+

+ + + + + +

+

+ + + + + +

+

Update

+

+ + + + +

+

Timed Data

+

Active cues in red color

+

+

+

+ + + diff --git a/v3/test/mediasync/mediasync.js b/v3/test/mediasync/mediasync.js new file mode 100644 index 0000000..e08ca90 --- /dev/null +++ b/v3/test/mediasync/mediasync.js @@ -0,0 +1,913 @@ +/** + * MediaSync + * + * author: njaal.borch@motioncorporation.com + * + * Copyright 2015 + * License: LGPL + */ + +var mediascape = function(_MS_) { + + /** + * Detect if we need to kick the element + * If it returns true, you can re-run this function on + * a user interaction to actually perform the kick + */ + var _need_kick; + function needKick(elem, onerror) { + if (_need_kick === false) { + return false; + } + if (elem.canplay) { + _need_kick = false; + return false; + } + var vol = elem.volume; + // If muted, we won't detect MEI on Chrome but we want to be quiet + elem.volume = 0.01; + var p; + try { + p = elem.play(); + } catch (err) { + onerror(err); + } + if (p !== undefined && p.then) { + p.then(function() { + setTimeout(function() { + elem.pause(); + elem.volume = vol; + }, 0); + + }) + .catch(function(err) { + if (onerror) { + onerror(err); + } else { + console.log("Play failed, pass an error function to the needKick function to handle it (likely get the user to click a play button)"); + } + }); + } else { + _need_kick = elem.paused === true; + elem.pause(); + elem.volume = vol; + } + if (_need_kick && onerror) { + onerror("Play failed"); + } + return _need_kick; + } + + var setSync = function(func, target, msv) { + var state = msv.query(); + + if (state.vel === 0) { + // We stopped, trigger a single sync when the MSV changes + var handle_change = function() { + setSync(func, target, msv); + }; + msv.on("change", function() { + if (this.query().vel !== 0) { + msv.off("change", handle_change); + handle_change(); + } + }); + return this; + } + + var time_left = (target - state.pos)/state.vel; + if (time_left > 0.001) { + setTimeout(function() { + setSync(func, target, msv); + }, 1000 * ((time_left / 2.0) / state.vel)); + return this; + } + func.call(); + return {cancel:function() {}}; + }; + + + /** + * The mediaSync object will try to synchronize an HTML + * media element to a Shared Motion. It exploits + * playbackRate functionality if possible, but will fallback + * to only currentTime manipulation (skipping) if neccesariy. + * + * Options: + * * skew (default 0.0) + * how many seconds (float) should be added to the + * motion before synchronization. Calculate by + * start point of element - start point of motion + * * automute (default false) + * Mute the media element when playing too fast (or too slow) + * * mode (default "auto") + * "skip": Force "skip" mode - i.e. don't try using playbackRate. + * "vpbr": Force variable playback rate. Normally not a good idea + * "auto" (default): try playbackRate. If it's not supported, it will + * struggle for a while before reverting. If 'remember' is not set to + * false, this will only happen once after each browser update. + * * loop (default false) + * Loop the media + * * debug (default null) + * If debug is true, log to console, if a function, the function + * will be called with debug info + * * target (default 0.025 - 25ms ~ lipsync) + * What are we aiming for? Default is likely OK, if we can do + * better, we will. If the target is too narrow, you'll end up + * with a more skippy experience. When using variable playback + * rates, this parameter is ignored (target is always 0) + * * remember (default false) + * Remember the last experience on this device - stores support + * or lack of support for variable playback rate. Records in + * localStorage under key.mediasync_vpbr clear it to re-learn + */ + function mediaSync(elem, motion, options) { + var API; + options = options || {}; + options.skew = options.skew || 0.0; + options.target = options.target || 0.025; + options.original_target = options.target; + options.loop = options.loop || false; + options.target = options.target * 2; // Start out coarse + if (!options.mode) { + if (navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1) { + options.mode = "skip"; + } else { + options.mode = "auto"; + } + } + if (options.remember === undefined){ + options.remember = false; + } + if (options.debug || options.remember === false) { + localStorage.removeItem("mediasync_vpbr"); + options.remember = false; + } + if (options.automute === undefined) { + options.automute = false; + } + var _auto_muted = false; + + var play = function() { + try { + var p = elem.play(); + if (p) { + p.catch(function(err) { + _doCallbacks("error", {event:"error", op:"play", msg:err}); + }); + } + } catch (err) { + _doCallbacks("error", {event:"error", op:"play"}); + } + }; + + var onchange = function(e) { + _bad = 0; + _samples = []; + _last_skip = null; + + // If we're running but less than zero, we need to wake up when starting + if (motion.pos < -options.skew && motion.vel > 0) { + setTimeout(function() { setSync(onchange, -options.skew, motion) }, 100); + return; + } + // If we're paused, ignore + //if (_stopped || _paused) { + // console.log("Not active"); + // return; + // } + + if (_update_func !== undefined) { + _update_func(e); + } else { + console.log("WARNING: onchange but no update func yet"); + } + }; + + var setMotion = function(motion) { + _bad = 0; + if (_motion) { + /* CHANGE BEGIN + _motion.off("change", onchange); + */ + _motion.off("change", _sub); + _sub = undefined; + /* CHANGE END */ + } + _motion = motion; + + + /* CHANGE BEGIN + + // if motion is a timing object, we add some shortcuts + if (_motion.version >= 3) { + _motion.__defineGetter__("pos", function() {return _motion.query().position;}); + _motion.__defineGetter__("vel", function() {return _motion.query().velocity;}); + _motion.__defineGetter__("acc", function() {return _motion.query().acceleration;}); + } + needed only for direct access to motions + */ + + if (!("pos" in _motion)) { + _motion.__defineGetter__("pos", function() {return _motion.query().position;}); + } + if (!("vel" in _motion)) { + _motion.__defineGetter__("vel", function() {return _motion.query().velocity;}); + } + if (!("acc" in _motion)) { + _motion.__defineGetter__("acc", function() {return _motion.query().acceleration;}); + } + + /* CHANGE END */ + + /* CHANGE BEGIN + _motion.on("change", onchange); + */ + let ret = _motion.on("change", onchange); + if (ret === undefined || ret === _motion) { + // motion of timingsrc v2 + _sub = onchange; + } else { + // timingsrc v3 + _sub = ret; + } + /* CHANGE END */ + }; + + if (!motion) { + console.log("WARNING: No motion has been set"); + } else { + //setMotion(motion); + } + + + var _stopped = false; + var _paused = false; + var _motion; + + function onpaused() { + if (_motion.vel == 1) { + play(); + } + } + + function onplay() { + if (_motion.vel === 0) { + elem.pause(); + } + } + function onerror() { + console.log(err); // TODO: REPORT ERRORS + stop(); + } + + var pause = function(val) { + if (val === undefined) val = true; + _paused = val; + if (!_paused) { + onchange(); + } + }; + + var stop = function() { + _stopped = true; + elem.removeEventListener("paused", onpaused); + elem.removeEventListener("playing", onplay); + elem.removeEventListener("error", onerror); + }; + + var _update_func; + var _bad = 0; + var _amazing = 0; + var last_update; + var _samples = []; + var _vpbr; // Variable playback rate + var _last_bad = 0; + var _perfect = 5; + var _is_in_sync = false; + var _last_skip; + var _thrashing = 0; + + /* + CHANGE BEGIN + */ + var _sub; + /* + CHANGE END + */ + + var skip = function(pos) { + if (elem.readyState === 0) { + return; + } + if (_motion.vel != 1) { + // Just skip, don't do estimation + elem.currentTime = pos; + _last_skip = undefined; + _doCallbacks("skip", {event:"skip", pos:pos, target:_motion.pos, adjust:0}); + return; + } + + var adjust = 0; + var now = performance.now(); + if (_last_skip) { + if (now - _last_skip.ts < 1500) { + _thrashing += 1; + if (_thrashing > 3) { + // We skipped just a short time ago, we're thrashing + _dbg("Lost all confidence (thrashing)"); + options.target = Math.min(1, options.target*2); + _doCallbacks("target_change", { + event: "target_change", + target: options.target, + reason: "thrashing" + }); + _thrashing = 0; + } + } else { + _thrashing = 0; + } + var elapsed = (now - _last_skip.ts) / 1000; + var cur_pos = elem.currentTime; + var miss = (loop(_last_skip.pos + elapsed)) - cur_pos; + adjust = _last_skip.adjust + miss; + if (Math.abs(adjust) > 5) adjust = 0; // Too sluggish, likely unlucky + } + // Ensure that we're playing back at speed 1 + elem.playbackRate = 1.0; + _dbg({type:"skip", pos:pos + adjust, target:loop(_motion.pos), adjust:adjust}); + _perfect = Math.min(5, _perfect + 5); + if (_motion.vel != 1) { + elem.currentTime = pos; + } else { + elem.currentTime = pos + adjust; + _last_skip = { + ts: now, //performance.now(), + pos: pos, + adjust: adjust + }; + } + if (_is_in_sync) { + _is_in_sync = false; + _doCallbacks("sync", {event:"sync", sync:false}); + } + _doCallbacks("skip", {event:"skip", pos:pos + adjust, target:_motion.pos, adjust:adjust}); + }; + + + function loop(pos) { + if (options.loop) { + if (options.duration) { + return pos % options.duration; + } else { + return pos % elem.duration; + } + } + return pos; + } + + // onTimeChange handler for variable playback rate + var last_pbr_diff; + var update_func_playbackspeed = function(e) { + if (_stopped || _paused) { + return; + } + var snapshot = query(); + if (loop(snapshot.pos) == last_update) { + /* Figure out what this does - it makes it impossible to detect a reloaded video playing while we're not playing... + return; + */ + } + last_update = loop(snapshot.pos); + // If we're outside of the media range, don't stress the system + var p = loop(snapshot.pos + options.skew); + var duration = elem.duration; + if (duration) { + if (p < 0 || p > duration) { + if (!elem.paused) { + elem.pause(); + } + return; + } + } + // Force element to play/pause correctly + if (snapshot.vel !== 0) { + if (elem.paused) { + play(); + } + } else if (!elem.paused) { + elem.pause(); + } + + try { + if (!_vpbr && _bad > 40) { + if (_auto_muted) { + elem.muted = false; + _auto_muted = false; + } + _doCallbacks("muted", {event:"muted", muted:false}); + throw new Error("Variable playback rate seems broken - " + _bad + " bad"); + } + // If we're WAY OFF, jump + var ts = performance.now(); + var diff = p - elem.currentTime; + if ((diff < -1) || (snapshot.vel === 0 || Math.abs(diff) > 1)) { + _dbg({type:"jump", diff:diff}); + // Stationary, we need to just jump + var new_pos = loop(snapshot.pos + options.skew); + if (performance.now() - _last_bad > 150) { + //_bad += 10; + _last_bad = performance.now(); + skip(new_pos); + } + return; + } + + // If the diff is substantially larger than last time we updated it, trigger as broken + if (last_pbr_diff && Math.abs(diff - last_pbr_diff) > 0.500) { + //console.log("VPBR broken it seems", diff-last_pbr_diff); + _bad += 10; + //throw new Error("Variable playback rate seems broken"); + + } + + // Need to smooth diffs, many browsers are too inconsistent! + _samples.push({diff:diff, ts:ts, pos: p}); + var dp = _samples[_samples.length - 1].pos - _samples[0].pos; + var dt = _samples[_samples.length - 1].ts - _samples[0].ts; + if (_samples.length >= 3) { + var avg = 0; + for (var i = 0; i < _samples.length; i++) { + avg += _samples[i].diff; + } + diff = avg / _samples.length; + if (_samples.length > 3) { + _samples = _samples.splice(0, 1); + } + } else { + return; + } + + var pbr = 1000 * dp / dt; + //console.log("Playback rate was:", pbr, "reported", elem.playbackRate, elem.playbackRate - pbr); + // Actual sync + _dbg({type:"dbg", diff:diff, bad:_bad, vpbr:_vpbr}); + var getRate = function(limit, suggested) { + return Math.min(_motion.vel+limit, Math.max(_motion.vel-limit, _motion.vel + suggested)); + }; + + if (Math.abs(diff) > 1) { + _samples = []; + elem.playbackRate = getRate(1, diff*1.3); //Math.max(0, _motion.vel + (diff * 1.30)); + last_pbr_diff = diff; + _dbg({type:"vpbr", level:"coarse", rate:elem.playbackRate}); + _bad += 4; + } else if (Math.abs(diff) > 0.5) { + _samples = []; + elem.playbackRate = getRate(0.5, diff*0.75);//Math.min(1.10, _motion.vel + (diff * 0.75)); + last_pbr_diff = diff; + _dbg({type:"vpbr", level:"mid", rate:elem.playbackRate}); + _bad += 2; + } else if (Math.abs(diff) > 0.1) { + _samples = []; + elem.playbackRate = getRate(0.4, diff*0.75);//Math.min(1.10, _motion.vel + (diff * 0.75)); + last_pbr_diff = diff; + _dbg({type:"vpbr", level:"midfine", rate:elem.playbackRate}); + _bad += 1; + } else if (Math.abs(diff) > 0.025) { + _samples = []; + var newpbr = pbr - elem.playbackRate; + //console.log("New pbr", elem.playbackRate, "->", newpbr, getRate(0.30, diff*0.60)); + //elem.playbackRate = newpbr; + elem.playbackRate = getRate(0.30, diff*0.60); //Math.min(1.015, _motion.vel + (diff * 0.30)); + last_pbr_diff = diff; + _dbg({type:"vpbr", level:"fine", rate:elem.playbackRate}); + } else { + if (!_vpbr) { + _bad = Math.max(0, _bad-20); + _amazing++; + if (_amazing > 5) { + _vpbr = true; // Very unlikely to get here if we don't support it! + if (localStorage && options.remember) { + _dbg("Variable Playback Rate capability stored"); + localStorage.mediasync_vpbr = JSON.stringify({'appVersion':navigator.appVersion, "vpbr":true}); + } + } + } + if (!_is_in_sync) { + _is_in_sync = true; + _doCallbacks("sync", { + event: "sync", + sync: true + }); + } + //elem.playbackRate = getRate(0.02, diff * 0.07) + (pbr - 1); //_motion.vel + (diff * 0.1); + elem.playbackRate = getRate(0.02, diff * 0.07); //_motion.vel + (diff * 0.1); + last_pbr_diff = diff; + } + if (options.automute) { + if (!elem.muted && (elem.playbackRate > 1.05 || elem.playbackRate < 0.95)) { + _auto_muted = true; + elem.muted = true; + _doCallbacks("muted", {event:"muted", muted:true}); + _dbg({type:"mute", muted:true}); + } else if (elem.muted && _auto_muted) { + _auto_muted = false; + elem.muted = false; + _dbg({type:"mute", muted:false}); + _doCallbacks("muted", {event:"muted", muted:false}); + } + } + + } catch (err) { + // Not supported after all! + if (options.automute) { + elem.muted = false; + } + _last_skip = null; // Reset skip stuff + if (localStorage && options.remember) { + _dbg("Variable Playback Rate NOT SUPPORTED, remembering this "); + console.log("Variable playback speed not supported (remembered)"); + localStorage.mediasync_vpbr = JSON.stringify({'appVersion':navigator.appVersion, "vpbr":false}); + } + console.log("Error setting variable playback speed - seems broken", err); + _setUpdateFunc(update_func_skip); + } + }; + + var last_pos; + var last_diff; + // timeUpdate handler for skip based sync + var update_func_skip = function(ev) { + if (_stopped || _paused) { + return; + } + + var snapshot = query(); + var duration = elem.duration; + var new_pos; + if (duration) { + if (snapshot.pos < 0 || snapshot.pos > duration) { // Use snapshot, skew is not part of this + if (!elem.paused) { + elem.currentTime = duration - 0.03; + elem.pause(); + } + return; + } + } + + if (snapshot.vel > 0) { + if (elem.paused) { + play(); + } + } else if (!elem.paused) { + elem.pause(); + } + + + if (snapshot.vel != 1) { + if (loop(snapshot.pos) == last_pos) { + return; + } + last_pos = snapshot.pos; + _dbg("Jump, playback speed is not :", snapshot.vel); + // We need to just jump + new_pos = loop(snapshot.pos + options.skew); + if (elem.currentTime != new_pos) { + skip(new_pos, "jump"); + } + return; + } + + var p = snapshot.pos + options.skew; + var diff = p - elem.currentTime; + var ts = performance.now(); + + // If this was a Motion jump, skip immediately + if (ev !== undefined && ev.pos !== undefined) { + _dbg("MOTION JUMP"); + new_pos = snapshot.pos + options.skew; + skip(new_pos); + return; + } + + // Smooth diffs as currentTime is often inconsistent + _samples.push({diff:diff, ts:ts, pos: p}); + if (_samples.length >= 3) { + var avg = 0; + for (var i = 0; i < _samples.length; i++) { + avg += _samples[i].diff; + } + diff = avg / _samples.length; + _samples.splice(0, 1); + } else { + return; + } + + // We use the number of very good hits to build confidence + if (Math.abs(diff) < 0.001) { + _perfect = Math.max(5, _perfect); // Give us some breathing space! + } + + if (_perfect <= -2) { + // We are failing to meet the target, make target bigger + _dbg("Lost all confidence"); + options.target = Math.min(1, options.target*1.4); + _perfect = 0; + _doCallbacks("target_change", { + event: "target_change", + target: options.target, + reason: "unknown" + }); + } else if (_perfect > 15) { + // We are hitting the target, make target smaller if we're beyond the users preference + _dbg("Feels better"); + if (options.target == options.original_target) { + // We're improving yet 'perfect', trigger "good" sync event + if (!_is_in_sync) { + _is_in_sync = true; + _doCallbacks("sync", {event:"sync", sync:true}); + } + } + options.target = Math.max(Math.abs(diff) * 0.7, options.original_target); + _perfect -= 8; + _doCallbacks("target_change", { + event: "target_change", + target: options.target, + reason: "improving" + }); + } + + _dbg({type:"dbg", diff:diff, target:options.target, perfect:_perfect}); + + if (Math.abs(diff) > options.target) { + // Target miss - if we're still confident, don't do anything about it + _perfect -= 1; + if (_perfect > 0) { + return; + } + // We've had too many misses, skip + new_pos = _motion.pos + options.skew; + //_dbg("Adjusting time to " + new_pos); + _perfect += 8; // Give some breathing space + skip(new_pos); + } else { + // Target hit + if (Math.abs(diff - last_diff) < options.target / 2) { + _perfect++; + } + last_diff = diff; + } + }; + + var _initialized = false; + var init = function() { + if (_initialized) return; + _initialized = true; + if (_motion === undefined) { + setMotion(motion); + } + if (localStorage && options.remember) { + if (localStorage.mediasync_vpbr) { + var vpbr = JSON.parse(localStorage.mediasync_vpbr); + if (vpbr.appVersion === navigator.appVersion) { + _vpbr = vpbr.vpbr; + } + } + } + + if (options.mode === "vpbr") { + _vpbr = true; + } + if (options.mode === "skip" || _vpbr === false) { + elem.playbackRate = 1.0; + _update_func = update_func_skip; + } else { + if (options.automute) { + elem.muted = true; + _auto_muted = true; + _doCallbacks("muted", {event:"muted", muted:true}); + } + _update_func = update_func_playbackspeed; + } + elem.removeEventListener("canplay", init); + elem.removeEventListener("playing", init); + _setUpdateFunc(_update_func); + //_motion.on("change", onchange); + }; + + elem.addEventListener("canplay", init); + elem.addEventListener("playing", init); + + var _last_update_func; + var _poller; + var _setUpdateFunc = function(func) { + if (_last_update_func) { + clearInterval(_poller); + elem.removeEventListener("timeupdate", _last_update_func); + elem.removeEventListener("pause", _last_update_func); + elem.removeEventListener("ended", _last_update_func); + } + _last_update_func = func; + elem.playbackRate = 1.0; + elem.addEventListener("timeupdate", func); + elem.addEventListener("pause", func); + elem.addEventListener("ended", func); + + if (func === update_func_playbackspeed) { + _doCallbacks("mode_change", {event:"mode_change", mode:"vpbr"}); + } else { + _doCallbacks("mode_change", {event:"mode_change", mode:"skip"}); + } + }; + + var query = function() { + // Handle both msvs and timing objects + + /* + CHANGE BEGIN + + if (_motion.version >= 3) { + var q = _motion.query(); + return { + pos: q.position, + vel: q.velocity, + acc: q.acceleration + }; + } + + */ + if ("version" in _motion) { + if (_motion.version < 3) { + // motion + return _motion.query(); + } + } + // timing object + let {position:pos, velocity:vel, acceleration:acc} = _motion.query(); + return {pos, vel, acc}; + /* + CHANGE END + */ + }; + + + var setSkew = function(skew) { + options.skew = skew; + }; + + var getSkew = function() { + return options.skew; + }; + + var setOption = function(option, value) { + options[option] = value; + if (option === "target") { + options.original_target = value; + } + }; + + /* + * Return 'playbackRate' or 'skip' for play method + */ + var getMethod = function() { + if (_update_func === update_func_playbackspeed) { + return "playbackRate"; + } + return "skip"; + }; + + // As we are likely asynchronous, we don't really know if elem is already + // ready! If it has, it will not emit canplay. Also, canplay seems shady + // regardless + var beater = setInterval(function() { + if (elem.readyState >= 2) { + clearInterval(beater); + try { + var event = new Event("canplay"); + elem.dispatchEvent(event); + } catch (e) { + var event2 = document.createEvent("Event"); + event2.initEvent("canplay", true, false); + elem.dispatchEvent(event2); + } + } + }, 100); + + + // callbacks + var _callbacks = { + skip: [], + mode_change: [], + target_change: [], + muted: [], + sync: [], + error: [] + }; + var _doCallbacks = function(what, e) { + if (!_callbacks.hasOwnProperty(what)) { + throw "Unsupported event: " + what; + } + for (var i = 0; i < _callbacks[what].length; i++) { + h = _callbacks[what][i]; + try { + h.call(API, e); + } catch (e2) { + console.log("Error in " + what + ": " + h + ": " + e2); + } + } + }; + + // unregister callback + var off = function(what, handler) { + if (!_callbacks.hasOwnProperty(what)) throw "Unknown parameter " + what; + var index = _callbacks[what].indexOf(handler); + if (index > -1) { + _callbacks[what].splice(index, 1); + } + return API; + }; + + var on = function(what, handler, agentid) { + if (!_callbacks.hasOwnProperty(what)) { + throw new Error("Unsupported event: " + what); + } + if (!handler || typeof handler !== "function") throw "Illegal handler"; + var index = _callbacks[what].indexOf(handler); + if (index != -1) { + throw new Error("Already registered"); + } + + // register handler + _callbacks[what].push(handler); + + // do immediate callback? + setTimeout(function() { + if (what === "sync") { + _doCallbacks(what, { + event: what, + sync: _is_in_sync + }, handler); + } + if (what === "muted") { + _doCallbacks(what, { + event: what, + muted: _auto_muted + }, handler); + } + }, 0); + return API; + }; + + + function _dbg() { + if (!options.debug) { + return; + } + if (typeof(options.debug) === "function") { + //options.debug(arguments); + var args = arguments; + setTimeout(function() { + options.debug.apply(window, args); + }, 0); + } else { + var args2 = []; + for (var k in arguments) { + args2.push(arguments[k]); + } + console.log(JSON.stringify(args2)); + } + } + + + + + // Export the API + API = { + setSkew: setSkew, + getSkew: getSkew, + setOption: setOption, + getMethod: getMethod, + setMotion: setMotion, + stop: stop, + pause: pause, + on: on, + off: off, + init:init + }; + return API; + } + + _MS_.mediaSync = mediaSync; + _MS_.mediaNeedKick = needKick; + return _MS_; +} (mediascape || {}); + + +// Support mcorp integration too +if (!window.hasOwnProperty("MCorp")) { + MCorp = {}; +} + +MCorp.mediaSync = mediascape.mediaSync; +MCorp.mediaNeedKick = mediascape.mediaNeedKick; diff --git a/v3/test/mediasync/test_mediasync.html b/v3/test/mediasync/test_mediasync.html new file mode 100644 index 0000000..66fd1d0 --- /dev/null +++ b/v3/test/mediasync/test_mediasync.html @@ -0,0 +1,96 @@ + + + + + + + + +

+

+ Position: +
+
+ + + + +
+

+

+ +

+

+ + +

+ + diff --git a/v3/test/sequencing/bug.html b/v3/test/sequencing/bug.html new file mode 100644 index 0000000..f8b6293 --- /dev/null +++ b/v3/test/sequencing/bug.html @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/v3/test/sequencing/debug_windowsequencer.html b/v3/test/sequencing/debug_windowsequencer.html new file mode 100644 index 0000000..52cc693 --- /dev/null +++ b/v3/test/sequencing/debug_windowsequencer.html @@ -0,0 +1,109 @@ + + + + + + + + + + + + + +

Test Sequencer Interval Mode

+
+ + diff --git a/v3/test/sequencing/seqbug.html b/v3/test/sequencing/seqbug.html new file mode 100644 index 0000000..41c1d76 --- /dev/null +++ b/v3/test/sequencing/seqbug.html @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/v3/test/sequencing/test_schedule.html b/v3/test/sequencing/test_schedule.html new file mode 100644 index 0000000..eeb5c80 --- /dev/null +++ b/v3/test/sequencing/test_schedule.html @@ -0,0 +1,96 @@ + + + + + + + +

Test Schedule

+
+ + + + + + diff --git a/v3/test/sequencing/test_sequencer.html b/v3/test/sequencing/test_sequencer.html new file mode 100644 index 0000000..2481519 --- /dev/null +++ b/v3/test/sequencing/test_sequencer.html @@ -0,0 +1,151 @@ + + + + + + + +

Test Sequencer

+

Timing Object

+ + + +

+ Pos: +

+ + + + + + diff --git a/v3/test/sequencing/test_sequencer_intervalmode.html b/v3/test/sequencing/test_sequencer_intervalmode.html new file mode 100644 index 0000000..9131d59 --- /dev/null +++ b/v3/test/sequencing/test_sequencer_intervalmode.html @@ -0,0 +1,261 @@ + + + + + + + + + + +

Test Sequencer Interval Mode

+
+

+ + + + + +

+

+ + + + + +

+

+ + + + + +

+

Update

+

+ + + + +

+

Timed Data

+

Active cues in red color

+

+

+

+ + + diff --git a/v3/test/sequencing/test_sequencer_pointmode.html b/v3/test/sequencing/test_sequencer_pointmode.html new file mode 100644 index 0000000..760f1f4 --- /dev/null +++ b/v3/test/sequencing/test_sequencer_pointmode.html @@ -0,0 +1,291 @@ + + + + + + + + + + +

Test Sequencer Point Mode

+
+

+ + + + + +

+

+ + + + + +

+

+ + + + + +

+

Update

+

+ + + + +

+

Timed Data

+

Active cues in red color

+

+

+

+ + + diff --git a/v3/test/sequencing/test_sequencer_pointmode_update.html b/v3/test/sequencing/test_sequencer_pointmode_update.html new file mode 100644 index 0000000..40eb09c --- /dev/null +++ b/v3/test/sequencing/test_sequencer_pointmode_update.html @@ -0,0 +1,217 @@ + + + + + + + + + +

Test Sequencer Point Mode Dataset Update

+
+

+ + + + + +

+

+ + + + + +

+

+ + + + + +

+

Add cuesaround 0

+

+ + + + + + +

+

Add cues around 100

+

+ + + + + + +

+ + + + + + diff --git a/v3/test/sequencing/test_sequencer_subset.html b/v3/test/sequencing/test_sequencer_subset.html new file mode 100644 index 0000000..3d8f8ba --- /dev/null +++ b/v3/test/sequencing/test_sequencer_subset.html @@ -0,0 +1,288 @@ + + + + + + + + + + +

Test Sequencer Dataview

+
+

+ + + + + +

+

+ + + + + +

+

+ + + + + +

+

Update

+

+ + + + +

+

Timed Data

+

Active cues in red color

+

+

+

+ + + diff --git a/v3/test/timingobject/test_bug_loop.html b/v3/test/timingobject/test_bug_loop.html new file mode 100644 index 0000000..ca9ee9b --- /dev/null +++ b/v3/test/timingobject/test_bug_loop.html @@ -0,0 +1,38 @@ +< + + + + + + + + + + + + + + + diff --git a/v3/test/timingobject/test_converter_delay.html b/v3/test/timingobject/test_converter_delay.html new file mode 100644 index 0000000..a231d5a --- /dev/null +++ b/v3/test/timingobject/test_converter_delay.html @@ -0,0 +1,127 @@ + + + + + + + +

Test Delay Converter

+

+

Source

+

+ Pos : +

+ + + + + +

+

Set Range

+

+ Range : +

+ + +

+ +

+ + +

+

Delay Converter

+

+ Pos : +

+ + + + + +

+

Set Range

+

+ Range : +

+ + +

+

+

+

Set Delay

+

+ Delay : +

+ + +

+ + + diff --git a/v3/test/timingobject/test_converter_loop.html b/v3/test/timingobject/test_converter_loop.html new file mode 100644 index 0000000..c4c14c2 --- /dev/null +++ b/v3/test/timingobject/test_converter_loop.html @@ -0,0 +1,112 @@ + + + + + + + +

Test Loop Converter

+

+

Source

+

+ Pos : +

+ + + + + +

+

Set Range

+

+ Range : +

+

+ + +

+ +

+ + +

+

Loop Converter

+

+ Pos : +

+ + + + + +

+

Set Range

+

+ Range : +

+

+ + +

+ +

+ + + diff --git a/v3/test/timingobject/test_converter_range.html b/v3/test/timingobject/test_converter_range.html new file mode 100644 index 0000000..24b5558 --- /dev/null +++ b/v3/test/timingobject/test_converter_range.html @@ -0,0 +1,109 @@ + + + + + + + +

Test Range Converter

+

+

Source

+

+ Pos : +

+ + + + + +

+

Set Range

+

+ Range : +

+ + +

+ +

+ + +

+

Range Converter

+

+ Pos : +

+ + + + + +

+

Set Range

+

+ Range : +

+ + +

+ +

+ + + diff --git a/v3/test/timingobject/test_converter_scale.html b/v3/test/timingobject/test_converter_scale.html new file mode 100644 index 0000000..3cc5714 --- /dev/null +++ b/v3/test/timingobject/test_converter_scale.html @@ -0,0 +1,128 @@ + + + + + + + +

Test Scale Converter

+

+

Source

+

+ Pos : +

+ + + + + +

+

Set Range

+

+ Range : +

+ + +

+ +

+ + +

+

Scale Converter

+

+ Pos : +

+ + + + + +

+

Set Range

+

+ Range : +

+ + +

+ +

+

+

Set Scale

+

+ Scale : +

+ + +

+ + + diff --git a/v3/test/timingobject/test_converter_skew.html b/v3/test/timingobject/test_converter_skew.html new file mode 100644 index 0000000..3811231 --- /dev/null +++ b/v3/test/timingobject/test_converter_skew.html @@ -0,0 +1,134 @@ + + + + + + + + +

Test Skew Converter

+

+

Source

+

+ Pos : +

+ + + + + +

+

Set Range

+

+ Range : +

+ + +

+ +

+ + +

+

Skew Converter

+

+ Pos : +

+ + + + + +

+

Set Range

+

+ Range : +

+ + +

+ +

+

+

Set Skew

+

+ Skew : +

+ + +

+ + + diff --git a/v3/test/timingobject/test_converter_timeshift.html b/v3/test/timingobject/test_converter_timeshift.html new file mode 100644 index 0000000..800ec1a --- /dev/null +++ b/v3/test/timingobject/test_converter_timeshift.html @@ -0,0 +1,129 @@ + + + + + + + +

Test Timeshift Converter

+

+

Source

+

+ Pos : +

+ + + + + +

+

Set Range

+

+ Range : +

+ + +

+

+ + +

+

Timeshift Converter

+

+ Pos : +

+ + + + + +

+

Set Range

+

+ Range : +

+ + +

+ +

+

+

Set Offset

+

+ Time offset : +

+ + +

+ + + diff --git a/v2.1/test/test_masterclock.html b/v3/test/timingobject/test_masterclock.html similarity index 90% rename from v2.1/test/test_masterclock.html rename to v3/test/timingobject/test_masterclock.html index 7d1e400..0dd792c 100644 --- a/v2.1/test/test_masterclock.html +++ b/v3/test/timingobject/test_masterclock.html @@ -1,14 +1,9 @@ - - - - diff --git a/v3/test/timingobject/test_positioncallback.html b/v3/test/timingobject/test_positioncallback.html new file mode 100644 index 0000000..babfbfb --- /dev/null +++ b/v3/test/timingobject/test_positioncallback.html @@ -0,0 +1,51 @@ + + + + + + + +

Test Position Callback

+
+ + + + + + diff --git a/v3/test/timingobject/test_provider.html b/v3/test/timingobject/test_provider.html new file mode 100644 index 0000000..59d10ed --- /dev/null +++ b/v3/test/timingobject/test_provider.html @@ -0,0 +1,89 @@ + + + + + + + + +

Test Timing Provider

+ +

Source

+
+ + + + +

+ +

+ + diff --git a/v3/test/timingobject/test_timingobject.html b/v3/test/timingobject/test_timingobject.html new file mode 100644 index 0000000..45c2d62 --- /dev/null +++ b/v3/test/timingobject/test_timingobject.html @@ -0,0 +1,66 @@ + + + + + + + +

Test Timingobject Range Timeout

+ +

Source

+
+ + + + + + diff --git a/v3/test/timingobject/test_timingsrc_undefined.html b/v3/test/timingobject/test_timingsrc_undefined.html new file mode 100644 index 0000000..957ebfb --- /dev/null +++ b/v3/test/timingobject/test_timingsrc_undefined.html @@ -0,0 +1,86 @@ + + + + + + + + +

Test Timingsrc Undefined

+ +

Source

+
+ + + + +

+ + +

+ + diff --git a/v3/test/ui/test_timingprogress.html b/v3/test/ui/test_timingprogress.html new file mode 100644 index 0000000..f021837 --- /dev/null +++ b/v3/test/ui/test_timingprogress.html @@ -0,0 +1,154 @@ + + + + + + + + + + + + + +

Timing Progress

+

+ Pos : +

+
+ +
+ +

+ + + +

+ + diff --git a/v2.1/test/sort-splice-stats.json b/v3/test/util/sort-splice-stats.json similarity index 100% rename from v2.1/test/sort-splice-stats.json rename to v3/test/util/sort-splice-stats.json diff --git a/v2.1/test/test_binarysearch.html b/v3/test/util/test_binarysearch.html similarity index 95% rename from v2.1/test/test_binarysearch.html rename to v3/test/util/test_binarysearch.html index b6cd4ff..f84609d 100644 --- a/v2.1/test/test_binarysearch.html +++ b/v3/test/util/test_binarysearch.html @@ -1,18 +1,13 @@ - - - - - + +

Test Binary Search

- \ No newline at end of file + diff --git a/v3/test/util/test_concat.html b/v3/test/util/test_concat.html new file mode 100644 index 0000000..773aa32 --- /dev/null +++ b/v3/test/util/test_concat.html @@ -0,0 +1,100 @@ + + + + + + + + +

Test Concat

+ + diff --git a/v3/test/util/test_endpoint.html b/v3/test/util/test_endpoint.html new file mode 100644 index 0000000..375ac2b --- /dev/null +++ b/v3/test/util/test_endpoint.html @@ -0,0 +1,128 @@ + + + + + + +

Test Interval

+ + diff --git a/v3/test/util/test_eventify.html b/v3/test/util/test_eventify.html new file mode 100644 index 0000000..fe12cb0 --- /dev/null +++ b/v3/test/util/test_eventify.html @@ -0,0 +1,110 @@ + + + + + + + +

Test Eventify

+ + + + + + + diff --git a/v3/test/util/test_interval.html b/v3/test/util/test_interval.html new file mode 100644 index 0000000..b35181e --- /dev/null +++ b/v3/test/util/test_interval.html @@ -0,0 +1,281 @@ + + + + + + + +

Test Interval

+ + diff --git a/v3/timingobject/delayconverter.js b/v3/timingobject/delayconverter.js new file mode 100644 index 0000000..2a91b40 --- /dev/null +++ b/v3/timingobject/delayconverter.js @@ -0,0 +1,129 @@ +/* + Copyright 2015 Norut Northern Research Institute + Author : Ingar Mæhlum Arntzen + + This file is part of the Timingsrc module. + + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Timingsrc 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . +*/ + + +/* + DELAY CONVERTER + + Delay Converter introduces a positive time delay on a source timing object. + + Generally - if the source timing object has some value at time t, + then the delayConverter will provide the same value at time t + delay. + + Since the delay Converter is effectively replaying past events after the fact, + it is not LIVE and not open to interactivity (i.e. update) + +*/ + +import TimingObject from './timingobject.js'; +import Timeout from '../util/timeout.js'; + + +class DelayConverter extends TimingObject { + constructor (timingObject, delay) { + if (delay < 0) {throw new Error ("negative delay not supported");} + if (delay === 0) {throw new Error ("zero delay makes delayconverter pointless");} + super(timingObject); + // fixed delay + this._delay = delay; + // buffer + this._buffer = []; + // timeoutid + this._timeout = new Timeout(this, this.__handleDelayed.bind(this)); + this.eventifyDefine("delaychange", {init:true}); + }; + + // extend + eventifyInitEventArgs(name) { + if (name == "delaychange") { + return [this._delay]; + } else { + return super.eventifyInitEventArgs(name) + } + } + + // overrides + onUpdateStart(arg) { + /* + Vector's timestamp always time-shifted (back-dated) by delay + + Normal operation is to delay every incoming vector update. + This implies returning null to abort further processing at this time, + and instead trigger a later continuation. + + However, delay is calculated based on the timestamp of the vector (age), not when the vector is + processed in this method. So, for small small delays the age of the vector could already be + greater than delay, indicating that the vector is immediately valid and do not require delayed processing. + + This is particularly true for the first vector, which may be old. + + So we generally check the age to figure out whether to apply the vector immediately or to delay it. + */ + + this._buffer.push(arg); + // if timeout for next already defined, nothing to do + if (!this._timeout.isSet()) { + this.__handleDelayed(); + } + return; + }; + + __handleDelayed() { + // run through buffer and apply vectors that are due + let now = this.clock.now(); + let arg, due; + while (this._buffer.length > 0) { + due = this._buffer[0].timestamp + this._delay; + if (now < due) { + break; + } else { + arg = this._buffer.shift(); + // apply + arg.timestamp = due; + this.__process(arg); + } + } + // set new timeout + if (this._buffer.length > 0) { + due = this._buffer[0].timestamp + this._delay; + this._timeout.setTimeout(due); + } + }; + + update(arg) { + // Updates are prohibited on delayed timingobjects + throw new Error ("update is not legal on delayed (non-live) timingobject"); + }; + + get delay() {return this._delay;}; + + set delay(delay) { + if (delay != this._delay) { + // set delay and emulate new event from timingsrc + this._delay = delay; + this._timeout.clear(); + this.__handleDelayed(); + this.eventifyTrigger("delaychange", delay); + } + } +} + +export default DelayConverter; + diff --git a/v3/timingobject/externalprovider.js b/v3/timingobject/externalprovider.js new file mode 100644 index 0000000..856b52f --- /dev/null +++ b/v3/timingobject/externalprovider.js @@ -0,0 +1,186 @@ +/* + Copyright 2015 Norut Northern Research Institute + Author : Ingar Mæhlum Arntzen + + This file is part of the Timingsrc module. + + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Timingsrc 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . + +*/ + + + +import MasterClock from './masterclock.js'; + + +function checkTimingProvider(obj){ + let required = ["on", "skew", "vector", "range", "update"]; + for (let prop of required) { + if (!(prop in obj)) { + throw new Error(`TimingProvider ${obj} missing property ${prop}`); + } + } +} + + +/* + EXTERNAL PROVIDER + + External Provider bridges the gap between the PROVIDER API (implemented by external timing providers) + and the TIMINGSRC API + + Objects implementing the TIMINGSRC API may be used as timingsrc (parent) for another timing object. + + - wraps a timing provider external + - handles some complexity that arises due to the very simple API of providers + - implements a clock for the provider +*/ + +class ExternalProvider { + + constructor(provider, callback, options) { + checkTimingProvider(provider); + options = options || {}; + + this._provider = provider; + this._callback = callback; + this._range; + this._vector; + this._ready = false + + /* + provider clock (may fluctuate based on live skew estimates) + */ + this._provider_clock; + /* + local clock + provider clock normalised to values of performance now + normalisation based on first skew measurement, so + */ + this._clock; + + // register event handlers + this._provider.on("vectorchange", this._onVectorChange.bind(this)); + this._provider.on("skewchange", this._onSkewChange.bind(this)); + + // check if provider is ready + if (this._provider.skew != undefined) { + // initialise immediately - without a callback + this._onSkewChange(true); + } + }; + + isReady() {return this._ready;}; + + // internal clock + get clock() {return this._clock;}; + get range() {return this._range;}; + + + /* + - local timestamp of vector is set for each new vector, using the skew available at that time + - the vector then remains unchanged + - skew changes affect local clock, thereby affecting the result of query operations + + - one could imagine reevaluating the vector as well when the skew changes, + but then this should be done without triggering change events + + - ideally the vector timestamp should be a function of the provider clock + */ + + get vector() { + // local_ts = provider_ts - skew + let local_ts = this._vector.timestamp - this._provider.skew; + return { + position : this._vector.position, + velocity : this._vector.velocity, + acceleration : this._vector.acceleration, + timestamp : local_ts + } + } + + + // internal provider object + get provider() {return this._provider;}; + + + _onSkewChange(init=false) { + if (!this._clock) { + this._provider_clock = new MasterClock({skew: this._provider.skew}); + this._clock = new MasterClock({skew:0}); + } else { + this._provider_clock.adjust({skew: this._provider.skew}); + // provider clock adjusted with new skew - correct local clock similarly + // current_skew = clock_provider - clock_local + let current_skew = this._provider_clock.now() - this._clock.now(); + // skew delta = new_skew - current_skew + let skew_delta = this._provider.skew - current_skew; + this._clock.adjust({skew: skew_delta}); + } + // no upcalls on skew change + }; + + _onVectorChange() { + if (this._clock) { + // is ready (onSkewChange has fired earlier) + if (!this._ready && this._provider.vector != undefined) { + // become ready + this._ready = true; + } + if (this._ready) { + if (!this._range) { + this._range = this._provider.range; + } + this._vector = this._provider.vector; + let eArg = { + range: this.range, + ...this.vector + } + if (this._callback) { + this._callback(eArg); + } + } + } + }; + + // update + /* + TODO - support setting range on provider + TODO - suppport tunnel + TODO - support onRangeChange from provider + */ + update(arg) { + let vector = { + position: arg.position, + velocity: arg.velocity, + acceleration: arg.acceleration, + timestamp: arg.timestamp + }; + // calc back to provider ts + // local_ts = provider_ts - skew + vector.timestamp = vector.timestamp + this._provider.skew; + let res = this._provider.update(vector); + // return success + return true; + }; + + close() { + this._callback = undefined; + } + +} + +export default ExternalProvider; + + diff --git a/v3/timingobject/internalprovider.js b/v3/timingobject/internalprovider.js new file mode 100644 index 0000000..bb28919 --- /dev/null +++ b/v3/timingobject/internalprovider.js @@ -0,0 +1,116 @@ +/* + Copyright 2015 Norut Northern Research Institute + Author : Ingar Mæhlum Arntzen + + This file is part of the Timingsrc module. + + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Timingsrc 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . +*/ + + +import MasterClock from './masterclock.js'; +import {calculateVector, checkRange} from '../util/motionutils.js'; + + +/* + INTERNAL PROVIDER + + Timing provider internal to the browser context + + Used by timing objects as timingsrc if no timingsrc is specified. +*/ + +class InternalProvider { + + constructor (callback, options) { + options = options || {}; + // initialise internal state + this._clock = new MasterClock({skew:0}); + this._range = [-Infinity, Infinity]; + this._vector; + this._callback = callback; + // options + options.timestamp = options.timestamp || this._clock.now(); + this._process_update(options); + }; + + // internal clock + get clock() {return this._clock;}; + get range() {return this._range;}; + get vector() {return this._vector;}; + + isReady() {return true;}; + + // update + _process_update(arg) { + // process arg + let { + position: pos, + velocity: vel, + acceleration: acc, + timestamp: ts, + range: range, + ...rest + } = arg; + + // record state from current motion + let p = 0, v = 0, a = 0; + if (this._vector != undefined) { + let nowVector = calculateVector(this._vector, ts); + nowVector = checkRange(nowVector, this._range); + p = nowVector.position; + v = nowVector.velocity; + a = nowVector.acceleration; + } + + // fill in from current motion, for missing properties + let vector = { + position : (pos != undefined) ? pos : p, + velocity : (vel != undefined) ? vel : v, + acceleration : (acc != undefined) ? acc : a, + timestamp : ts + }; + + // update range + if (range != undefined) { + let [low, high] = range; + if (low < high) { + if (low != this._range[0] || high != this._range[1]) { + this._range = [low, high]; + } + } + } + + // check vector with respect to range + vector = checkRange(vector, this._range); + // save old vector + this._old_vector = this._vector; + // update vector + this._vector = vector; + return {range, ...vector, ...rest}; + }; + + // update + update(arg) { + arg = this._process_update(arg); + return this._callback(arg); + } + + close() { + this._callback = undefined; + } +} + +export default InternalProvider; + diff --git a/v3/timingobject/loopconverter.js b/v3/timingobject/loopconverter.js new file mode 100644 index 0000000..38c1cd6 --- /dev/null +++ b/v3/timingobject/loopconverter.js @@ -0,0 +1,121 @@ +/* + Copyright 2015 Norut Northern Research Institute + Author : Ingar Mæhlum Arntzen + + This file is part of the Timingsrc module. + + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Timingsrc 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . +*/ + + +/* + LOOP CONVERTER + + This is a modulo type transformation where the converter will be looping within + a given range. Potentially one could create an associated timing object keeping track of the + loop number. +*/ + + +import {calculateVector} from '../util/motionutils.js'; +import TimingObject from './timingobject.js'; + + +// ovverride modulo to behave better for negative numbers +function mod(n, m) { + return ((n % m) + m) % m; +}; + +function transform(x, range) { + let skew = range[0]; + let length = range[1] - range[0]; + return skew + mod(x-skew, length); +} + + +/* + LOOP CONVERTER +*/ + +class LoopConverter extends TimingObject { + + constructor(timingsrc, range) { + super(timingsrc, {timeout:true}); + + if (!Array.isArray(range) || range.length != 2) { + throw new Error(`range must be array [low, high], ${range}`); + } + this.__range = range; + }; + + update(arg) { + // range change - only a local operation + if (arg.range != undefined) { + // implement local range update + let [low, high] = arg.range; + if (low >= high) { + throw new Error("illegal range", arg.range) + } + if (low != this.__range[0] || high != this.__range[1]) { + this.__range = [low, high]; + let vector = this.__get_timingsrc().query(); + vector.position = transform(vector.position, this.__range); + this.__vector = vector; + // trigger vector change + let _arg = {range: this.__range, ...this.__vector, live:true}; + this.__dispatchEvents(_arg, true, true); + } + delete arg.range; + } + // vector change + if (arg.position != undefined) { + // inverse transformation of position, from looper + // coordinates to timingsrc coordinates + // preserve relative position diff + let now = this.clock.now(); + let now_vector = calculateVector(this.vector, now); + let diff = now_vector.position - arg.position; + let now_vector_src = calculateVector(this.__get_timingsrc().vector, now); + arg.position = now_vector_src.position - diff; + } + return super.update(arg); + }; + + // overrides + onRangeViolation(now_vector) { + now_vector.position = transform(now_vector.position, this.__range); + return now_vector; + }; + + // overrides + onUpdateStart(arg) { + if (arg.range != undefined) { + // ignore range change from timingsrc + // instead, insist that this._range is correct + arg.range = this.__range; + } + if (arg.position != undefined) { + // vector change + arg.position = transform(arg.position, this.__range); + /* + vector change must also apply to timestamp + this is handlet in onRangeViolation + */ + } + return arg; + }; + +} +export default LoopConverter; + diff --git a/v3/timingobject/masterclock.js b/v3/timingobject/masterclock.js new file mode 100644 index 0000000..d5f1695 --- /dev/null +++ b/v3/timingobject/masterclock.js @@ -0,0 +1,140 @@ +/* + Copyright 2015 Norut Northern Research Institute + Author : Ingar Mæhlum Arntzen + + This file is part of the Timingsrc module. + + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Timingsrc 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . +*/ + + +/* + MASTER CLOCK + + + MasterClock is the reference clock used by TimingObjects. + + It is implemented using performance.now, + but is skewed and rate-adjusted relative to this local clock. + + This allows it to be used as a master clock in a distributed system, + where synchronization is generally relative to some other clock than the local clock. + + The master clock may need to be adjusted in time, for instance as a response to + varying estimation of clock skew or drift. The master clock supports an adjust primitive for this purpose. + + What policy is used for adjusting the master clock may depend on the circumstances + and is out of scope for the implementation of the MasterClock. + This policy is implemented by the timing object. This policy may or may not + provide monotonicity. + + A change event is emitted every time the masterclock is adjusted. + + Vector values define + - position : absolute value of the clock in seconds + - velocity : how many seconds added per second (1.0 exactly - or very close) + - timestamp : timstamp from local system clock (performance) in seconds. Defines point in time where position and velocity are valid. + + If initial vector is not provided, default value is + {position: now, velocity: 1.0, timestamp: now}; + implying that master clock is equal to local clock. +*/ + +import eventify from '../util/eventify.js'; + + +// Need a polyfill for performance,now as Safari on ios doesn't have it... +(function(){ + if ("performance" in window === false) { + window.performance = {}; + window.performance.offset = new Date().getTime(); + } + if ("now" in window.performance === false){ + window.performance.now = function now(){ + return new Date().getTime() - window.performance.offset; + }; + } + })(); + +// local clock in seconds +const local_clock = { + now : function () {return performance.now()/1000.0;} +}; + +function calculateVector(vector, tsSec) { + if (tsSec === undefined) tsSec = local_clock.now(); + var deltaSec = tsSec - vector.timestamp; + return { + position : vector.position + vector.velocity*deltaSec, + velocity : vector.velocity, + timestamp : tsSec + }; +}; + +class MasterClock { + + constructor (options) { + var now = local_clock.now(); + options = options || {}; + this._vector = {position: now, velocity: 1.0, timestamp: now}; + // event support + eventify.eventifyInstance(this); + this.eventifyDefine("change", {init:false}); // define change event (no init-event) + // adjust + this.adjust(options); + }; + + /* + ADJUST + - could also accept timestamp for velocity if needed? + - given skew is relative to local clock + - given rate is relative to local clock + */ + adjust(options) { + options = options || {}; + var now = local_clock.now(); + var nowVector = this.query(now); + if (options.skew === undefined && options.rate === undefined) { + return; + } + this._vector = { + position : (options.skew !== undefined) ? now + options.skew : nowVector.position, + velocity : (options.rate !== undefined) ? options.rate : nowVector.velocity, + timestamp : nowVector.timestamp + } + this.eventifyTrigger("change"); + }; + + /* + NOW + - calculates the value of the clock right now + - shorthand for query + */ + now() { + return calculateVector(this._vector, local_clock.now()).position; + }; + + /* + QUERY + - calculates the state of the clock right now + - result vector includes position and velocity + */ + query(now) { + return calculateVector(this._vector, now); + }; + +} +eventify.eventifyPrototype(MasterClock.prototype); + +export default MasterClock; diff --git a/v3/timingobject/offsetcallback.js b/v3/timingobject/offsetcallback.js new file mode 100644 index 0000000..29b280c --- /dev/null +++ b/v3/timingobject/offsetcallback.js @@ -0,0 +1,96 @@ +import * as motionutils from '../util/motionutils.js'; + + +/* + Provide callback + + - the first time the timing object reaches or passes a given offset. + - if to is initially paused exactly at target offset, then nothing happens, until + next onchange event. + Analogy to setTimeout - except in position space. +*/ + +/* + determine if motion described by fresh vector is on + the left or right side of offset. +*/ + +function get_side(vector, offset) { + let {position, velocity, acceleration} = vector; + if (position < offset) return "left"; + if (offset < position) return "right"; + // position == offset + if (velocity < 0) return "left"; + if (0 < velocity) return "right"; + // velocity == 0 + if (acceleration < 0) return "left"; + if (0 < acceleration) return "right"; + return; +} + +export default class OffsetCallback { + + constructor (to, offset, callback) { + this._to = to; + this._offset = offset; + this._callback = callback; + this._tid; + // timing object timingsrc event + this._sub = this._to.on("timingsrc", this._onChange.bind(this)); + this._side; // left or right side of offset + this._terminited = false; + } + + _onChange(eArg, eInfo) { + this._clearTimeout(); + let vector = this._to.query(); + // try to initialise side if not already done + if (this._side == undefined) { + this._side = get_side(vector, this._offset); + if (this._side == undefined) { + return; + } + } else { + // check if we are still on same side + let side = get_side(vector, this._offset); + if (side != this._side) { + // terminate - by skip to other side + this._terminate(vector); + } + } + // register timeout for reacing offset if not paused + let delta = motionutils.calculateMinPositiveRealSolution(vector, this._offset); + if (delta != Infinity && delta != undefined) { + this._tid = setTimeout(this._handleTimeout.bind(this), delta*1000.0); + } + } + + _handleTimeout() { + if (this._tid != undefined) { + // terminate - by timeout during playback + this._terminate(this._to.query()); + } + } + + _clearTimeout() { + if (this._tid != undefined) { + clearTimeout(this._tid); + this._tid = undefined; + } + } + + _terminate(vector) { + if (!this._terminated) { + this._clearTimeout(); + this._terminated = true; + this._to.off(this._sub); + if (vector) { + this._callback(vector); + } + } + } + + cancel() { + this._terminate(); + } +} \ No newline at end of file diff --git a/v3/timingobject/positioncallback.js b/v3/timingobject/positioncallback.js new file mode 100644 index 0000000..684a725 --- /dev/null +++ b/v3/timingobject/positioncallback.js @@ -0,0 +1,137 @@ + + + +import Timeout from '../util/timeout.js'; +import * as motionutils from '../util/motionutils.js'; + +/* + modify modulo operation +*/ +function mod(n,m) { + return ((n % m) + m) % m; +} + +/* + divide n by m, + find q (integer) and r such that + n = q*m + r +*/ +function divmod(n, m) { + let q = Math.floor(n/m); + let r = mod(n, m); + return [q,r]; +} + +/** + * point n == offset + q*stride + r + - given stride, offset + represent point as [q, r] + */ + +function float2point(n, stride, offset) { + return divmod(n-offset, stride); +} + +function point2float(p, stride, offset) { + let [q, r] = p; + return offset + q*stride + r; +} + + +/* + Given stride and offset, calculate nearest + waypoints before and after given position. + If position is exact match with waypoint, + return [true, before, after] +*/ +function stride_points(position, stride, offset) { + let [q, r] = float2point(position, stride, offset); + let after = [q+1, 0]; + let before = (r == 0) ? [q-1, 0]: [q, 0]; + before = point2float(before, stride, offset); + after = point2float(after, stride, offset); + return [(r==0), before, after]; +}; + + + +/* + + Position callback + + - callback whenever the timing object position is x, + where (x - offset) % stride === 0 + + - analogy to setInterval - except callbacks are in position space, not + in time space + + options : { + stride - default 1 + offset - default 0 + } + + NOTE: pausing on x and later resuming from x triggers callback in both cases + +*/ + +class PositionCallback { + + constructor (timingObject, callback, options={}) { + this._to = timingObject; + let {stride=1, offset=0} = options; + this._offset = offset; + this._stride = stride; + this._callback = callback; + this._timeout = new Timeout(this._to, this._handleTimeout.bind(this)); + + // timing object timingsrc event + this._to.on("timingsrc", this._onChange.bind(this)); + } + + _onChange(eArg, eInfo) { + let pos = (eArg.live) ? eArg.position : this._to.pos; + this._renewTimeout(pos); + } + + _calculateTimeout(before, after) { + let vector = this._to.query(); + let [delta, pos] = motionutils.calculateDelta(vector, [before, after]); + if (delta == undefined) { + return; + } + // check range violation + let [rLow, rHigh] = this._to.range; + if (pos < rLow || rHigh < pos ) { + return [undefined, undefined]; + } + return [vector.timestamp + delta, pos]; + } + + _renewTimeout(pos) { + this._timeout.clear(); + // find candidate points - before and after + let [match, before, after] = stride_points(pos, + this._stride, + this._offset); + // callback + if (match) { + this._callback(pos); + } + // calculate timeout to next + let res = this._calculateTimeout(before, after); + if (res == undefined) { + return; + } + // set timeout + let ts = res[0]; + this._timeout.setTimeout(ts, res); + } + + _handleTimeout(now, arg) { + let pos = arg[1]; + this._renewTimeout(pos); + } +} + + +export default PositionCallback; \ No newline at end of file diff --git a/v3/timingobject/rangeconverter.js b/v3/timingobject/rangeconverter.js new file mode 100644 index 0000000..566c858 --- /dev/null +++ b/v3/timingobject/rangeconverter.js @@ -0,0 +1,208 @@ +/* + Copyright 2015 Norut Northern Research Institute + Author : Ingar Mæhlum Arntzen + + This file is part of the Timingsrc module. + + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Timingsrc 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . +*/ + +/* + + RANGE CONVERTER + + The converter enforce a range on position. + + It only has effect if given range is a restriction on the range of the timingsrc. + Range converter will pause on range endpoints if timingsrc leaves the range. + Range converters will continue mirroring timingsrc once it comes into the range. +*/ + + +import {RangeState, correctRangeState, checkRange} from '../util/motionutils.js'; +import TimingObject from './timingobject.js'; + + +function state() { + var _state = RangeState.INIT; + var _range = null; + var is_special_state_change = function (old_state, new_state) { + // only state changes between INSIDE and OUTSIDE* are special state changes. + if (old_state === RangeState.OUTSIDE_HIGH && new_state === RangeState.OUTSIDE_LOW) return false; + if (old_state === RangeState.OUTSIDE_LOW && new_state === RangeState.OUTSIDE_HIGH) return false; + if (old_state === RangeState.INIT) return false; + return true; + } + var get = function () {return _state;}; + var set = function (new_state, new_range) { + + var absolute = false; // absolute change + var special = false; // special change + + // check absolute change + if (new_state !== _state || new_range !== _range) { + absolute = true; + } + // check special change + if (new_state !== _state) { + special = is_special_state_change(_state, new_state); + } + // range change + if (new_range !== _range) { + _range = new_range; + } + // state change + if (new_state !== _state) { + _state = new_state; + } + return {special:special, absolute:absolute}; + + } + return {get: get, set:set}; +}; + + +/* + Range converter allows a new (smaller) range to be specified. + + - ignores the range of its timingsrc + - vector change from timingsrc + - outside own range - drop - set timeout to inside + - inside own range - normal processing + - extra vector changes (compared to timingsrc) + - enter inside + - range violation own range + - range updated locally + +*/ + +class RangeConverter extends TimingObject { + + constructor (timingObject, range) { + super(timingObject, {timeout:true}); + this.__state = state(); + this.__range = range; + }; + + + update(arg) { + throw Error("Not Implemented!"); + /* + range change - only a local operation + + - need to trigger local processing of new range, + so that range is changed and events triggered + - also need to trigger a reevaluation of + vector from timingsrc vector, for instance, if + range grows while timingsrc is outside, the + position of the vector needs to change + - cannot do both these things via emulation + of timingsrc event - because rangeconverter + is supposed to ignore range change from timingsrc + - could do both locally, but this would effectively + require reimplementation of logic in __process + - in addition, this could be a request to update + both range and vector at the same time, in which case + it would be good to do them both at the same time + + - possible solution - somehow let range converter + discriminate range changes based on origin? + + */ + if (arg.range != undefined) { + + // local processing of range change + // to trigger range change event + let _arg = {range: arg.range, ...this.__get_timingsrc().vector, live:true}; + this.__process(_arg); + // avoid that range change affects timingsrc + delete arg.range; + + } + return super.update(arg); + }; + + + + // overrides + onUpdateStart(arg) { + if (arg.range != undefined) { + // ignore range change from timingsrc + // delete causes update to be dropped + delete arg.range; + } + if (arg.position != undefined) { + // vector change from timingsrc + let {position, velocity, acceleration, timestamp} = arg; + let vector = {position, velocity, acceleration, timestamp}; + vector = this.onVectorChange(vector); + if (vector == undefined) { + // drop because motion is outside + // create new timeout for entering inside + this.__renewTimeout(this.__get_timingsrc().vector, this.__range); + return; + } else { + // regular + arg.position = vector.position; + arg.velocity = vector.velocity; + arg.acceleration = vector.acceleration; + arg.timestamp = vector.timestamp; + } + } + return arg; + }; + + + onVectorChange(vector) { + var new_state = correctRangeState(vector, this.__range); + var state_changed = this.__state.set(new_state, this.__range); + if (state_changed.special) { + // state transition between INSIDE and OUTSIDE + if (this.__state.get() === RangeState.INSIDE) { + // OUTSIDE -> INSIDE, generate fake start event + // vector delivered by timeout + // forward event unchanged + } else { + // INSIDE -> OUTSIDE, generate fake stop event + vector = checkRange(vector, this.__range); + } + } + else { + // no state transition between INSIDE and OUTSIDE + if (this.__state.get() === RangeState.INSIDE) { + // stay inside or first event inside + // forward event unchanged + } else { + // stay outside or first event outside + // forward if + // - first event outside + // - skip from outside-high to outside-low + // - skip from outside-low to outside-high + // - range change + // else drop + // - outside-high to outside-high (no range change) + // - outside-low to outside-low (no range change) + if (state_changed.absolute) { + vector = checkRange(vector, this.__range); + } else { + return; + } + } + } + return vector; + }; +} + +export default RangeConverter; + diff --git a/v3/timingobject/scaleconverter.js b/v3/timingobject/scaleconverter.js new file mode 100644 index 0000000..83c1608 --- /dev/null +++ b/v3/timingobject/scaleconverter.js @@ -0,0 +1,93 @@ +/* + Copyright 2015 Norut Northern Research Institute + Author : Ingar Mæhlum Arntzen + + This file is part of the Timingsrc module. + + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Timingsrc 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . +*/ + +/* + SCALE CONVERTER + + Scaling by a factor 2 means that values of the timing object (position, velocity and acceleration) are multiplied by two. + For example, if the timing object represents a media offset in seconds, scaling it to milliseconds implies a scaling factor of 1000. + +*/ + +import TimingObject from './timingobject.js'; + + +class ScaleConverter extends TimingObject { + constructor (timingsrc, factor) { + super(timingsrc); + this._factor = factor; + this.eventifyDefine("scalechange", {init:true}); + }; + + // extend + eventifyInitEventArgs(name) { + if (name == "scalechange") { + return [this._factor]; + } else { + return super.eventifyInitEventArgs(name) + } + } + + // overrides + onUpdateStart(arg) { + if (arg.range != undefined) { + arg.range = [arg.range[0]*this._factor, arg.range[1]*this._factor]; + } + if (arg.position != undefined) { + arg.position *= this._factor; + } + if (arg.velocity != undefined) { + arg.velocity *= this._factor; + } + if (arg.acceleration != undefined) { + arg.acceleration *= this._factor; + } + return arg; + } + + update(arg) { + if (arg.position != undefined) { + arg.position /= this._factor; + } + if (arg.velocity != undefined) { + arg.velocity /= this._factor; + } + if (arg.acceleration != undefined) { + arg.acceleration /= this._factor; + } + return super.update(arg); + }; + + get scale() {return this._factor;}; + + set scale(factor) { + if (factor != this._factor) { + // set scale and emulate new event from timingsrc + this._factor = factor; + this.__handleEvent({ + ...this.__get_timingsrc().vector, + range: this.__get_timingsrc().range + }); + this.eventifyTrigger("scalechange", factor); + } + } +} +export default ScaleConverter; + diff --git a/v3/timingobject/skewconverter.js b/v3/timingobject/skewconverter.js new file mode 100644 index 0000000..a7e26b6 --- /dev/null +++ b/v3/timingobject/skewconverter.js @@ -0,0 +1,89 @@ +/* + Copyright 2015 Norut Northern Research Institute + Author : Ingar Mæhlum Arntzen + + This file is part of the Timingsrc module. + + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Timingsrc 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . +*/ + + +/* + SKEW CONVERTER + + Skewing the timeline by 2 means that the timeline position 0 of the timingsrc becomes position 2 of Converter. + +*/ + + +import TimingObject from './timingobject.js'; + + +class SkewConverter extends TimingObject { + + constructor (timingsrc, skew, options) { + super(timingsrc, options); + this._skew = skew; + this.eventifyDefine("skewchange", {init:true}); + } + + // extend + eventifyInitEventArgs(name) { + if (name == "skewchange") { + return [this._skew]; + } else { + return super.eventifyInitEventArgs(name) + } + } + + // overrides + onUpdateStart(arg) { + if (arg.range != undefined) { + arg.range[0] += this._skew; + arg.range[1] += this._skew; + } + if (arg.position != undefined) { + arg.position += this._skew; + } + return arg; + }; + + // overrides + update(arg) { + if (arg.position != undefined) { + arg.position -= this._skew; + } + if (arg.range != undefined) { + let [low, high] = arg.range; + arg.range = [low - this._skew, high - this._skew]; + } + return super.update(arg); + }; + + get skew() {return this._skew;}; + + set skew(skew) { + if (skew != this._skew) { + // set skew and emulate new event from timingsrc + this._skew = skew; + this.__handleEvent({ + ...this.__get_timingsrc().vector, + range: this.__get_timingsrc().range + }); + this.eventifyTrigger("skewchange", skew); + } + } +}; + +export default SkewConverter; diff --git a/v3/timingobject/timeshiftconverter.js b/v3/timingobject/timeshiftconverter.js new file mode 100644 index 0000000..cfe3598 --- /dev/null +++ b/v3/timingobject/timeshiftconverter.js @@ -0,0 +1,95 @@ +/* + Copyright 2015 Norut Northern Research Institute + Author : Ingar Mæhlum Arntzen + + This file is part of the Timingsrc module. + + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Timingsrc 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . +*/ + +/* + TIMESHIFT CONVERTER + + Timeshift Converter timeshifts a timing object by timeoffset. + Positive timeoffset means that the converter will run ahead of the source timing object. + Negative timeoffset means that the converter will run behind the source timing object. + + Updates affect the converter immediately. + This means that update vector must be re-calculated + to the value it would have at time-shifted time. + Timestamps are not time-shifted, since the motion is still live. + For instance, (0, 1, ts) becomes (0+(1*timeshift), 1, ts) + + However, this transformation may cause range violation + - this happens only when timing object is moving. + - implementation requires range converter logic + + - range is infinite +*/ + +import TimingObject from './timingobject.js'; +import {calculateVector} from '../util/motionutils.js'; + + +class TimeshiftConverter extends TimingObject { + + constructor (timingsrc, offset) { + super(timingsrc); + this._offset = offset; + this.eventifyDefine("offsetchange", {init:true}); + }; + + // extend + eventifyInitEventArgs(name) { + if (name == "offsetchange") { + return [this._offset]; + } else { + return super.eventifyInitEventArgs(name) + } + } + + // overrides + onUpdateStart(arg) { + if (arg.range != undefined) { + arg.range = [-Infinity, Infinity]; + } + if (arg.position != undefined) { + // calculate timeshifted vector + let ts = arg.timestamp; + let new_vector = calculateVector(arg, ts + this._offset); + arg.position = new_vector.position; + arg.velocity = new_vector.velocity; + arg.acceleration = new_vector.acceleration; + arg.timestamp = ts; + } + return arg; + }; + + get offset() {return this._offset;}; + + set offset(offset) { + if (offset != this._offset) { + // set offset and emulate new event from timingsrc + this._offset = offset; + this.__handleEvent({ + ...this.__get_timingsrc().vector, + range: this.__get_timingsrc().range + }); + this.eventifyTrigger("offsetchange", offset); + } + } + +} + +export default TimeshiftConverter; diff --git a/v3/timingobject/timingobject.js b/v3/timingobject/timingobject.js new file mode 100644 index 0000000..17ab2e4 --- /dev/null +++ b/v3/timingobject/timingobject.js @@ -0,0 +1,639 @@ +/* + Copyright 2020 + Author : Ingar Mæhlum Arntzen + + This file is part of the Timingsrc module. + + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Timingsrc 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . +*/ + + +import eventify from '../util/eventify.js'; +import Timeout from '../util/timeout.js'; +import * as motionutils from '../util/motionutils.js'; +import InternalProvider from './internalprovider.js'; +import ExternalProvider from './externalprovider.js'; + +const MAX_NONCE = 10000; + +function getRandomInt() { + return Math.floor(Math.random() * MAX_NONCE); +}; + +function isTimingProvider(obj){ + let required = ["on", "skew", "vector", "range", "update"]; + for (let prop of required) { + if (!(prop in obj)) { + return false; + } + } + return true; +} + + +/* + TIMING BASE + + abstract base class for objects that may be used as timingsrc + + essential internal state + - range, vector + + external methods + query, update + + events + on/off "change", "timeupdate" + + internal methods for range timeouts + + defines internal processing steps + - handleEvent(arg) <- from external timingobject + - vector = onChange(vector) + - process(vector) <- from timeout or preProcess + - handleTimeout(arg) <- timeout on range restrictions + - process (arg) + - set internal vector, range + - dispatchEvents(arg) + - renew range timeout + - dispatchEvent (arg) + - emit change event and timeupdate event + - turn periodic timeupdate on or off + + individual steps in this structure may be specialized + by subclasses (i.e. timing converters) +*/ + + +class TimingObject { + + constructor (timingsrc, options) { + + // special support for options given as first and only argument + // equivalent to new TimingObject(undefined, options) + // in this case, timingsrc may be found in options + if (timingsrc != undefined && options == undefined) { + if (!(timingsrc instanceof TimingObject) && !isTimingProvider(timingsrc)) { + // timingsrc is neither timing object nor timingprovider + // assume timingsrc is options + options = timingsrc; + timingsrc = undefined; + if (options.provider) { + timingsrc = options.provider; + } else if (options.timingsrc) { + timingsrc = options.timingsrc; + } + }; + } + + // options + options = options || {}; + this.__options = options; + + + // default timeout option + if (options.timeout == undefined) { + options.timeout = true; + } + + // cached vectors and range + this.__old_vector; + this.__vector; + this.__range = [-Infinity, Infinity]; + + // range restriction timeout + this.__timeout = new Timeout(this, this.__handleTimeout.bind(this)); + + // timeoutid for timeupdate event + this.__tid = undefined; + + // timingsrc + this.__timingsrc; + this.__sub; + + // update promises + this.__update_events = new Map(); + + // readiness + this.__ready = new eventify.EventBoolean(); + + // exported events + eventify.eventifyInstance(this); + this.eventifyDefine("timingsrc", {init:true}); + this.eventifyDefine("change", {init:true}); + this.eventifyDefine("rangechange", {init:true}); + this.eventifyDefine("timeupdate", {init:true}); + + // initialise timingsrc + this.__set_timingsrc(timingsrc, options); + }; + + + /*************************************************************** + + EVENTS + + ***************************************************************/ + + /* + overrides how immediate events are constructed + specific to eventutils + - overrides to add support for timeupdate events + */ + eventifyInitEventArgs(name) { + if (this.__ready.value) { + if (name == "timingsrc") { + let eArg = { + ...this.__vector, + range: this.__range, + live:false + } + return [eArg]; + } else if (name == "timeupdate") { + return [undefined]; + } else if (name == "change") { + return [this.__vector]; + } else if (name == "rangechange") { + return [this.__range]; + } + } + }; + + + /*************************************************************** + + ACCESSORS + + ***************************************************************/ + + // ready or not + isReady() {return this.__ready.value;}; + + // ready promise + get ready() {return eventify.makePromise(this.__ready);}; + + // range + get range() { + // copy + return [this.__range[0], this.__range[1]]; + }; + + // vector + get vector() { + // copy + return { + position : this.__vector.position, + velocity : this.__vector.velocity, + acceleration : this.__vector.acceleration, + timestamp : this.__vector.timestamp + }; + }; + + // old vector + get old_vector() {return this.__old_vector;}; + + // delta + get delta() { + return new motionutils.MotionDelta(this.__old_vector, this.__vector); + } + + // clock - from timingsrc or provider + get clock() {return this.__timingsrc.clock}; + + get version() {return 5;} + + + /*************************************************************** + + QUERY + + ***************************************************************/ + + // query + query() { + if (this.__ready.value == false) { + throw new Error("query before timing object is ready"); + } + // reevaluate state to handle range violation + let vector = motionutils.calculateVector(this.__vector, this.clock.now()); + // detect range violation - only if timeout is set { + if (this.__timeout.isSet()) { + if (vector.position < this.__range[0] || this.__range[1] < vector.position) { + // emulate update event to trigger range restriction + this.__process({...vector}); + } + // re-evaluate query after state transition + return motionutils.calculateVector(this.__vector, this.clock.now()); + } + return vector; + }; + + // shorthand query + get pos() {return this.query().position;}; + get vel() {return this.query().velocity;}; + get acc() {return this.query().acceleration;}; + + + /*************************************************************** + + UPDATE + + ***************************************************************/ + + // internal update + __update(arg) { + if (this.__timingsrc instanceof TimingObject) { + return this.__timingsrc.__update(arg); + } else { + // provider + return this.__timingsrc.update(arg); + } + }; + + // external update + update(arg) { + // check if noop + let ok = (arg.range != undefined); + ok = ok || (arg.position != undefined); + ok = ok || (arg.velocity != undefined); + ok = ok || (arg.acceleration != undefined); + if (!ok) { + return Promise.resolve(arg); + } + arg.tunnel = getRandomInt(); + if (arg.timestamp == undefined) { + arg.timestamp = this.clock.now(); + } + let event = new eventify.EventVariable(); + this.__update_events.set(arg.tunnel, event); + let promise = eventify.makePromise(event, val => (val != undefined)); + this.__update(arg); + return promise; + } + + + /*************************************************************** + + CORE UPDATE PROCESSING + + ***************************************************************/ + + /* + do not override + handle incoming change event + eArg = {vector:vector, range:range, live:true} + + subclasses may specialise behaviour by overriding + onVectorChange + + */ + __handleEvent(arg) { + let { + range, + live = true, + ...rest + } = arg; + // copy range object + if (range != undefined) { + range = [range[0], range[1]]; + } + // new arg object + let _arg = { + range, + live, + ...rest, + }; + _arg = this.onUpdateStart(_arg); + if (_arg != undefined) { + return this.__process(_arg); + } + }; + + /* + do not override + handle timeout + */ + __handleTimeout(now, timeout_vector) { + this.__process({...timeout_vector}); + } + + /* + core processing step after change event or timeout + assignes the internal vector + */ + __process(arg) { + let { + range, + position, + velocity, + acceleration, + timestamp, + live=true, + ...rest + } = arg; + + + // update range + let range_change = false; + if (range != undefined) { + let [low, high] = range; + if (low < high) { + if (low != this.__range[0] || high != this.__range[1]) { + this.__range = [low, high]; + range = [low, high]; + range_change = true; + } + } + } + + /* + - vector change (all vector elements defined) + - range change (no vector elements defined) + - both (all vector elements and range defined) + */ + let vector_change = (position != undefined); + if (!vector_change && !range_change) { + console.log("__process: WARNING - no vector change and no range change") + } + + /* + check if vector is consistent with range + range violation may occur if + - vector change + - range change + - both + + - vector must be recalculated for present for detection + of range violation + */ + let vector; + if (vector_change) { + // vector change + vector = {position, velocity, acceleration, timestamp}; + } else { + vector = {...this.__vector}; + } + let now = this.clock.now(); + let now_vector = motionutils.calculateVector(vector, now); + let violation = motionutils.detectRangeViolation(now_vector, this.__range); + if (violation) { + vector = this.onRangeViolation(now_vector); + live = true; + } + + // reevaluate vector change and live + vector_change = vector_change || !motionutils.equalVectors(vector, this.__vector); + + // update vector + if (vector_change) { + // save old vector + this.__old_vector = this.__vector; + // update vector + this.__vector = vector; + } + + let _arg; + if (range_change && vector_change) { + _arg = {range, ...vector, live, ...rest}; + } else if (range_change) { + _arg = {range, live, ...rest}; + } else if (vector_change) { + _arg = {...vector, live, ...rest}; + } else { + _arg = {live, ...rest}; + } + + // trigger events + this.__ready.value = true; + this.__dispatchEvents(_arg, range_change, vector_change); + // renew timeout + if (this.__options.timeout) { + this.__renewTimeout(); + } + // release update promises + if (_arg.tunnel != undefined) { + let event = this.__update_events.get(_arg.tunnel); + if (event) { + this.__update_events.delete(_arg.tunnel); + delete _arg.tunnel; + event.value = _arg; + } + } + // TODO + // since externalprovider does not support tunnel yet + // free all remaining promises + for (let event of this.__update_events.values()) { + event.value = {}; + } + this.onUpdateDone(_arg); + return _arg; + }; + + /* + process a new vector applied in order to trigger events + overriding this is only necessary if external change events + need to be suppressed, + */ + __dispatchEvents(arg, range_change, vector_change) { + let { + range, + position, + velocity, + acceleration, + timestamp + } = arg; + // trigger timingsrc events + this.eventifyTrigger("timingsrc", arg); + // trigger public change events + if (vector_change) { + let vector = {position, velocity, acceleration, timestamp}; + this.eventifyTrigger("change", vector); + } + if (range_change) { + this.eventifyTrigger("rangechange", range); + } + // trigger timeupdate events + this.eventifyTrigger("timeupdate"); + let moving = motionutils.isMoving(this.__vector); + if (moving && this.__tid === undefined) { + let self = this; + this.__tid = setInterval(function () { + self.eventifyTrigger("timeupdate"); + }, 200); + } else if (!moving && this.__tid !== undefined) { + clearTimeout(this.__tid); + this.__tid = undefined; + } + }; + + + /*************************************************************** + + SUBCLASS MAY OVERRIDE + + ***************************************************************/ + + /* + may be overridden + */ + onRangeViolation(now_vector) { + return motionutils.checkRange(now_vector, this.__range); + }; + + /* + may be overridden + */ + onUpdateStart(arg) {return arg;}; + + /* + may be overridden + */ + onUpdateDone(arg) {}; + + + /*************************************************************** + + TIMEOUTS + + ***************************************************************/ + + /* + renew timeout is called during every processing step + in order to recalculate timeouts. + + - optional vector - default is own vector + - optional range - default is own range + */ + __renewTimeout(vector, range) { + this.__timeout.clear(); + let timeout_vector = this.__calculateTimeoutVector(vector, range); + if (timeout_vector == undefined) { + return; + } + this.__timeout.setTimeout(timeout_vector.timestamp, timeout_vector); + }; + + + /* + calculate a vector that will be delivered to _process(). + the timestamp in the vector determines when it is delivered. + + - optional vector - default is own vector + - optional range - default is own range + */ + __calculateTimeoutVector(vector, range) { + vector = vector || this.__vector; + range = range || this.__range; + let now = this.clock.now(); + let now_vector = motionutils.calculateVector(vector, now); + let [delta, pos] = motionutils.calculateDelta(now_vector, range); + if (delta == undefined) { + return; + } + // vector when range restriction will be reached + let timeout_vector = motionutils.calculateVector(vector, now + delta); + // possibly avoid rounding errors + timeout_vector.position = pos; + return timeout_vector; + }; + + + /*************************************************************** + + TIMINGSRC + + ***************************************************************/ + + /* + + timingsrc property and switching on assignment + + */ + __clear_timingsrc() { + // clear timingsrc + if (this.__timingsrc != undefined) { + if (this.__timingsrc instanceof TimingObject) { + this.__timingsrc.off(this.__sub); + this.__sub = undefined; + this.__timingsrc = undefined; + } else { + // provider + this.__timingsrc.close(); + this.__timingsrc = undefined; + } + } + } + + __set_timingsrc(timingsrc, options) { + // set timingsrc + let callback = this.__handleEvent.bind(this); + if (timingsrc instanceof TimingObject) { + // timingsrc + this.__timingsrc = timingsrc; + this.__sub = this.__timingsrc.on("timingsrc", callback); + } else { + // provider + if (timingsrc == undefined) { + // Internal Provider + this.__timingsrc = new InternalProvider(callback, options); + } else { + // External Provider + this.__timingsrc = new ExternalProvider(timingsrc, callback, options); + } + // emulating initial event from provider, causing + // this timingobject to initialise + if (this.__timingsrc.isReady()) { + let arg = { + range: this.__timingsrc.range, + ...this.__timingsrc.vector, + live: false + } + // generate initial event + callback(arg); + } + } + } + + __get_timingsrc() { + // returns InternalProvider, ExternalProvider or TimingObject + return this.__timingsrc; + } + + get timingsrc () { + // returns TimingObject, Provider or undefined + let timingsrc = this.__get_timingsrc(); + if (timingsrc instanceof TimingObject) { + return timingsrc; + } else if (timingsrc instanceof InternalProvider) { + return undefined; + } else if (timingsrc instanceof ExternalProvider) { + return timingsrc._provider; + } else { + throw new Error("illegal timingsrc") + } + } + + set timingsrc(timingsrc) { + this.__clear_timingsrc(); + this.__set_timingsrc(timingsrc); + } + +} + +eventify.eventifyPrototype(TimingObject.prototype); + +export default TimingObject; + + + diff --git a/v3/timingobject/timingsampler.js b/v3/timingobject/timingsampler.js new file mode 100644 index 0000000..25ceff0 --- /dev/null +++ b/v3/timingobject/timingsampler.js @@ -0,0 +1,122 @@ +/* + Copyright 2020 + Author : Ingar Mæhlum Arntzen + + This file is part of the Timingsrc module. + + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Timingsrc 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . +*/ + + +/** + * Sampler for Timing Object + * + * - samples timing object position and emits a change event at certain frequency + * - does not emit any events when timing object is paused + * - options + * - period (between samples) in ms + * - frequency (sample frequency) in hz + * if both given - period takes precedence + * if none given - default period = 200 ms + * + * TODO + - set refresh frequency to be sensitive + to velocity - adapted to a fixed rate + change in percent + calculate percent velocity + rate change in percent per second + + * + * + */ + +import eventify from '../util/eventify.js'; + +const DEFAULT_PERIOD = 200; + +class TimingSampler { + + constructor (timingObject, options = {}) { + this._to = timingObject; + // timeout id + this._tid; + // period + let {period, frequency} = options; + this._period = DEFAULT_PERIOD; + if (period != undefined) { + this._period = period; + } else if (frequency != undefined) { + this._period = 1.0/frequency; + } + // Events + eventify.eventifyInstance(this); + + this.eventifyDefine("change", {init:true}); + // Handle timing object change event + this._sub = this._to.on("change", this._onChange.bind(this)); + } + + /* + Eventify: immediate events + */ + eventifyInitEventArgs(name) { + if (name == "change" && this._to.isReady()) { + return [this._to.pos]; + } + } + + /** + * Start/stop sampling + */ + _onChange() { + let v = this._to.query(); + let moving = (v.velocity != 0.0 || v.acceleration != 0.0); + // start or stop sampling + if (moving && this._tid == undefined) { + this._tid = setInterval(function(){ + this._onSample(); + }.bind(this), this._period); + } + if (!moving && this._tid != undefined) { + clearTimeout(this._tid); + this._tid = undefined; + } + this._onSample(v.position); + } + + /** + * Sample timing object + */ + _onSample(position) { + position = (position != undefined) ? position : this._to.pos; + this.eventifyTrigger("change", position); + } + + /** + * Terminate sampler + */ + clear() { + // stop sampling + if (this._tid) { + clearTimeout(this._tid); + this._tid = undefined; + } + // disconnect handler + this._to.off(this._sub); + } +} + +eventify.eventifyPrototype(TimingSampler.prototype); + +export default TimingSampler; \ No newline at end of file diff --git a/v3/ui/datasetviewer.js b/v3/ui/datasetviewer.js new file mode 100644 index 0000000..464bb9d --- /dev/null +++ b/v3/ui/datasetviewer.js @@ -0,0 +1,64 @@ +/* + Copyright 2020 + Author : Ingar Arntzen + + This file is part of the Timingsrc module. + + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Timingsrc 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . +*/ + +import {random_string} from '../util/utils.js'; + +class DatasetViewer { + + constructor(ds, elem) { + this.ds = ds; + this.elem = elem; + this.nonce = random_string(4); + this.ds.on("change", this.onchange.bind(this)); + this.ds.on("remove", this.onremove.bind(this)); + } + + cue2string(cue) { + let itv = (cue.interval) ? cue.interval.toString() : "undefined"; + let data = JSON.stringify(cue.data); + return `${cue.key}, ${itv}, ${data}`; + } + + onchange(eItem) { + let _id = `${this.nonce}-${eItem.key}`; + let node = this.elem.querySelector(`#${_id}`); + if (node) { + // update existing node + node.innerHTML = this.cue2string(eItem.new); + } else { + // create new node + let node = document.createElement("div"); + node.innerHTML = this.cue2string(eItem.new); + node.setAttribute("id", _id); + this.elem.appendChild(node); + } + } + + onremove(eItem) { + // remove node + let _id = `${this.nonce}-${eItem.key}`; + let node = document.getElementById(_id); + if (node) { + node.parentNode.removeChild(node); + } + } +} + +export default DatasetViewer; \ No newline at end of file diff --git a/v3/ui/timingprogress.js b/v3/ui/timingprogress.js new file mode 100644 index 0000000..0adfd3a --- /dev/null +++ b/v3/ui/timingprogress.js @@ -0,0 +1,111 @@ +/* + Copyright 2020 + Author : Ingar Mæhlum Arntzen + + This file is part of the Timingsrc module. + + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Timingsrc 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . +*/ + +import TimingSampler from "../timingobject/timingsampler.js"; + +/* + TODO + + - treat progress change as a speculative + change, (with a timeout) + implementation - ideally as speculative converter + easy solution - just lock +*/ + +class TimingProgress { + + static position2percent(position, range) { + let [low, high] = range; + + let offset = position - low; + let length = high - low; + return 100.0*offset/length; + }; + + static percent2position(percent, range) { + let [low, high] = range; + // make sure percent is [0,100] + percent = Math.max(0, percent); + percent = Math.min(100, percent); + let length = high - low; + let offset = length*percent/100.0; + return low + offset; + }; + + constructor (timingObject, progress_elem, options={}) { + this._to = timingObject; + this._sampler = options.sampler; + this._progress_elem = progress_elem; + this._lock = false; + this._options = options; + this._range = options.range || this._to.range; + let [low, high] = this._range; + if (low == -Infinity || high == Infinity) { + throw new Error("illegal range", this._range); + } + + // subscribe to input event from progress elem + this._progress_elem.addEventListener("input", function() { + // set lock + // no updates on progress elem from timing object until lock is released + this._lock_value = true; + }.bind(this)); + + // subscribe to change event from progress elem + this._progress_elem.addEventListener("change", function () { + // clear lock + this._lock_value = false; + // update the timing object + let percent = parseInt(this._progress_elem.value); + let position = TimingProgress.percent2position(percent, this._range); + this._to.update({position: position}); + }.bind(this)); + + // sampler + if (this._sampler) { + this._sampler.on("change", this.refresh.bind(this)); + } + } + + refresh() { + let position = this._to.pos; + // update progress elem if unlocked + if (!this._lock_value) { + let percent = TimingProgress.position2percent(position, this._range); + if (this._options.thumb) { + // check if percent is legal + if (percent < 0.0 || 100.0 < percent) { + // hide + this._options.thumb.hide(); + return; + } + } else { + percent = (percent < 0.0) ? 0.0 : percent; + percent = (100.0 < percent) ? 100.0: percent; + } + this._progress_elem.value = `${percent}`; + if (this._options.thumb) { + this._options.thumb.show(); + } + } + } +} + +export default TimingProgress; \ No newline at end of file diff --git a/v2.1/util/binarysearch.js b/v3/util/binarysearch.js similarity index 68% rename from v2.1/util/binarysearch.js rename to v3/util/binarysearch.js index e2b7f9f..2dee81a 100644 --- a/v2.1/util/binarysearch.js +++ b/v3/util/binarysearch.js @@ -1,93 +1,106 @@ /* - Copyright 2015 Norut Northern Research Institute - Author : Ingar Mæhlum Arntzen + Copyright 2020 + Author : Ingar Arntzen - This file is part of the Timingsrc module. + This file is part of the Timingsrc module. - Timingsrc is free software: you can redistribute it and/or modify - it under the terms of the GNU Lesser General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. - Timingsrc 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 Lesser General Public License for more details. + Timingsrc 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 Lesser General Public License for more details. - You should have received a copy of the GNU Lesser General Public License - along with Timingsrc. If not, see . + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . */ +import Interval from './interval.js'; -define (['./interval'], function (Interval) { +// check if n is a number +function is_number(n) { + var N = parseFloat(n); + return (n==N && !isNaN(N)); +}; - 'use strict'; - // check if n is a number - var is_number = function(n) { - var N = parseFloat(n); - return (n==N && !isNaN(N)); - }; +/* + utility function for protecting against duplicates +*/ +function unique(A) { + return [...new Set(A)]; +}; - /* - batch inserts and removes have two strategies - 1) change-sort - 2) splice - - simple rule by measurement - splice is better for batchlength <= 100 for both insert and remove - */ - var resolve_approach = function (arrayLength, batchLength) { - if (arrayLength == 0) { - return "sort"; - } - return (batchLength <= 100) ? "splice" : "sort"; - }; +/* + batch inserts and removes have two strategies + 1) change-sort + 2) splice + + simple rule by measurement + splice is better for batchlength <= 100 for both insert and remove +*/ +function resolve_approach(arrayLength, batchLength) { + if (arrayLength == 0) { + return "sort"; + } + return (batchLength <= 100) ? "splice" : "sort"; +}; - var BinarySearchError = function (message) { + +class BinarySearchError extends Error { + + constructor(message) { + super(message); this.name = "BinarySearchError"; - this.message = (message||""); - }; - BinarySearchError.prototype = Error.prototype; + } - /* +} - BINARY SEARCH - - based on sorted list of unique elements - - implements protection against duplicates +/* +BINARY SEARCH - Public API - - update (remove_elements, insert_elements) - - lookup (interval) - returns list for all elements - - remove (interval) - removes elements within interval - - has (element) - returns true if element exists with value == element, else false - - get (element) - returns element with value if exists, else undefined - - values () - returns iterable for all elements - - indexOf(element) - returns index of element - - indexOfElements(elements) - - getByIndex(index) - returns element at given index +- based on sorted list of unique elements +- implements protection against duplicates - */ +Public API +- update (remove_elements, insert_elements) +- lookup (interval) - returns list for all elements +- remove (interval) - removes elements within interval +- has (element) - returns true if element exists with value == element, else false +- get (element) - returns element with value if exists, else undefined +- values () - returns iterable for all elements +- indexOf(element) - returns index of element +- indexOfElements(elements) +- getByIndex(index) - returns element at given index + + +*/ - var cmp = function (a, b) {return a-b;}; - +function cmp(a, b) {return a-b;}; - var BinarySearch = function (options) { + +class BinarySearch { + + constructor(options) { this.array = []; this.options = options || {}; - }; + } + /** * Binary search on sorted array * @param {*} searchElement The item to search for within the array. * @return {Number} The index of the element which defaults to -1 when not found. */ - BinarySearch.prototype.binaryIndexOf = function (searchElement) { + binaryIndexOf(searchElement) { let minIndex = 0; let maxIndex = this.array.length - 1; let currentIndex; @@ -106,30 +119,30 @@ define (['./interval'], function (Interval) { } // not found - indicate at what index the element should be inserted return ~maxIndex; - + // NOTE : ambiguity /* - search for for an element that is less than array[0] - should return a negative value indicating that the element + search for an element that is less than array[0] + should return a negative value indicating that the element was not found. Furthermore, as it escapes the while loop - the returned value should indicate the index that this element - would have had - had it been there - as is the idea of this bitwise + the returned value should indicate the index that this element + would have had - had it been there - as is the idea of this bitwise operator trick so, it follows that search for value of minimum element returns 0 if it exists, and 0 if it does not exists this ambiguity is compensated for in relevant methods */ }; - + /* utility function for resolving ambiguity */ - BinarySearch.prototype.isFound = function(index, x) { + isFound(index, x) { if (index > 0) { return true; - } + } if (index == 0 && this.array.length > 0 && this.array[0] == x) { return true; } @@ -139,12 +152,12 @@ define (['./interval'], function (Interval) { /* returns index of value or -1 */ - BinarySearch.prototype.indexOf = function (x) { + indexOf(x) { var index = this.binaryIndexOf(x); return (this.isFound(index, x)) ? index : -1; }; - BinarySearch.prototype.indexOfElements = function (elements) { + indexOfElements(elements) { let x, index; let indexes = []; for (let i=0; i -1) ? true : false; + has(x) { + return (this.indexOf(x) > -1) ? true : false; }; - BinarySearch.prototype.get = function (index) { + get(index) { return this.array[index]; }; - /* - utility function for protecting against duplicates - - removing duplicates using Set is natural, - but in objectModes Set equality will not work with the value callback function. - In this case use map instead - this is slower - due to repeated use of the custom value() function - - Note. If duplicates exists, this function preserves the last duplicate given - that both Map and Set replaces on insert, and that iteration is guaranteed to - be insert ordered. - */ - BinarySearch.prototype._unique = function (A) { - return [...new Set(A)]; - }; /* @@ -198,12 +196,12 @@ define (['./interval'], function (Interval) { WARNING - there should be no need to insert elements that are already present in the array. This function drops such duplicates */ - BinarySearch.prototype._update_splice = function (to_remove, to_insert, options) { + _update_splice(to_remove, to_insert, options) { // REMOVE - if (this.array.length > 0) { + if (this.array.length > 0) { let indexes = this.indexOfElements(to_remove); - /* + /* sort indexes to make sure we are removing elements in backwards order optimization @@ -242,9 +240,9 @@ define (['./interval'], function (Interval) { by doing both remove and insert in one operation, sorting can be done only once. */ - BinarySearch.prototype._update_sort = function (to_remove, to_insert, options) { + _update_sort(to_remove, to_insert, options) { // REMOVE - if (this.array.length > 0 && to_remove.length > 0) { + if (this.array.length > 0 && to_remove.length > 0) { // visit all elements and set their value to undefined // undefined values will be sorted to the end of the array let indexes = this.indexOfElements(to_remove); @@ -265,22 +263,22 @@ define (['./interval'], function (Interval) { } } // remove duplicates - this.array = this._unique(this.array); + this.array = unique(this.array); }; /* Update - removing and inserting elements in one operation - a single element should only be present once in the list, thus avoiding - multiple operations to one element. This is presumed solved externally. + a single element should only be present once in the list, thus avoiding + multiple operations to one element. This is presumed solved externally. - also objects must not be members of both lists. - internally selects the best method - searchsplice or concatsort - selection based on relative sizes of existing elements and new elements */ - BinarySearch.prototype.update = function (to_remove, to_insert, options) { + update(to_remove, to_insert, options) { let size = to_remove.length + to_insert.length; if (size == 0) { return; @@ -300,11 +298,11 @@ define (['./interval'], function (Interval) { Accessors */ - BinarySearch.prototype.getMinimum = function () { + getMinimum() { return (this.array.length > 0) ? this.array[0] : undefined; }; - BinarySearch.prototype.getMaximum = function () { + getMaximum = function () { return (this.array.length > 0) ? this.array[this.array.length - 1] : undefined; }; @@ -313,41 +311,41 @@ define (['./interval'], function (Interval) { Internal search functions */ - /* + /* Find index of largest value less than x Returns -1 if noe values exist that are less than x */ - BinarySearch.prototype.ltIndexOf = function(x) { + ltIndexOf(x) { var i = this.binaryIndexOf(x); if (this.isFound(i, x)) { - /* + /* found - x is found on index i consider element to the left - if we are at the left end of the array nothing is found + if we are at the left end of the array nothing is found return -1 - */ + */ if (i > 0) { return i-1; } else { return -1; } } else { - /* + /* not found - Math.abs(i) is index where x should be inserted => Math.abs(i) - 1 is the largest value less than x */ return Math.abs(i)-1; - } + } }; - /* - Find index of rightmost value less than x or equal to x + /* + Find index of rightmost value less than x or equal to x Returns -1 if noe values exist that are less than x or equal to x */ - BinarySearch.prototype.leIndexOf = function(x) { + leIndexOf(x) { var i = this.binaryIndexOf(x); if (this.isFound(i, x)) { - /* + /* element found */ return i; @@ -358,25 +356,25 @@ define (['./interval'], function (Interval) { } }; - /* + /* Find index of leftmost value greater than x Returns -1 if no values exist that are greater than x */ - BinarySearch.prototype.gtIndexOf = function (x) { + gtIndexOf(x) { var i = this.binaryIndexOf(x); if (this.isFound(i, x)) { /* found - x is found on index i if there are no elements to the right return -1 - */ + */ if (i < this.array.length -1) { return i+1; } else { return -1; } } else { - /* + /* not found - Math.abs(i) is index where x should be inserted => Math.abs(i) is the smallest value greater than x unless we hit the end of the array, in which cas no smalles value @@ -388,15 +386,15 @@ define (['./interval'], function (Interval) { }; - /* - Find index of leftmost value which is greater than x or equal to x + /* + Find index of leftmost value which is greater than x or equal to x Returns -1 if noe values exist that are greater than x or equal to x */ - BinarySearch.prototype.geIndexOf = function(x) { + geIndexOf(x) { var i = this.binaryIndexOf(x); if (this.isFound(i, x)) { - /* + /* found element */ return i; @@ -412,10 +410,10 @@ define (['./interval'], function (Interval) { for use with slice operation returns undefined if no elements are found */ - BinarySearch.prototype.lookupIndexes = function (interval) { - if (interval === undefined) + lookupIndexes(interval) { + if (interval === undefined) interval = new Interval(-Infinity, Infinity, true, true); - if (interval instanceof Interval === false) + if (interval instanceof Interval === false) throw new BinarySearchError("lookup requires Interval argument"); // interval represents a single point @@ -453,25 +451,25 @@ define (['./interval'], function (Interval) { /* lookup by interval */ - BinarySearch.prototype.lookup = function (interval) { + lookup(interval) { let [start, end] = this.lookupIndexes(interval); - return (start != undefined) ? this.array.slice(start, end) : []; + return (start != undefined) ? this.array.slice(start, end) : []; }; /* remove by interval */ - BinarySearch.prototype.remove = function (interval) { + remove(interval) { let [start, end] = this.lookupIndexes(interval); - return (start != undefined) ? this.array.splice(start, end-start) : []; + return (start != undefined) ? this.array.splice(start, end-start) : []; }; - BinarySearch.prototype.slice = function (start, end) { + slice(start, end) { return this.array.slice(start, end); }; - BinarySearch.prototype.splice = function (start, length) { + splice(start, length) { return this.array.splice(start, length); }; @@ -482,10 +480,10 @@ define (['./interval'], function (Interval) { - removeList is sorted - changes only affect the part of the index between first and last element - move remaining elements to the left, remove elements with a single splice - - efficent if removelist references elements that are close to eachother + - efficent if removelist references elements that are close to eachother */ - BinarySearch.prototype.removeInSlice = function (removeList) { + removeInSlice(removeList) { if (removeList.length == 0){ return; } @@ -519,23 +517,21 @@ define (['./interval'], function (Interval) { }; - - BinarySearch.prototype.values = function () { + values() { return this.array.values(); }; - BinarySearch.prototype.clear = function () { + clear() { this.array = []; }; - Object.defineProperty(BinarySearch.prototype, "length", { - get: function () { - return this.array.length; - } - }); + get length () { + return this.array.length; + } + +} - return BinarySearch; -}); +export default BinarySearch; diff --git a/v3/util/endpoint.js b/v3/util/endpoint.js new file mode 100644 index 0000000..1afff28 --- /dev/null +++ b/v3/util/endpoint.js @@ -0,0 +1,224 @@ +/* + Copyright 2020 + Author : Ingar Arntzen + + This file is part of the Timingsrc module. + + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Timingsrc 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . +*/ + + +const isNumber = function(n) { + let N = parseFloat(n); + return (n===N && !isNaN(N)); +}; + + +/********************************************************* + +ENDPOINT + +Utilities for interval endpoints comparison + +**********************************************************/ + +/* + endpoint modes - in endpoint order + endpoint order + p), [p, [p], p], (p +*/ +const MODE_RIGHT_OPEN = 0; +const MODE_LEFT_CLOSED = 1; +const MODE_SINGULAR = 2; +const MODE_RIGHT_CLOSED = 3; +const MODE_LEFT_OPEN = 4; + +// create endpoint +function create(val, right, closed, singular) { + // make sure infinity endpoints are legal + if (val == Infinity) { + if (right == false || closed == false) { + throw new Error("Infinity endpoint must be right-closed or singular"); + } + } + if (val == -Infinity) { + if (right == true || closed == false) { + throw new Error("-Infinity endpoint must be left-closed or singular") + } + } + return [val, right, closed, singular]; +} + + +/* + resolve endpoint mode +*/ +function get_mode(e) { + // if right, closed is given + // use that instead of singular + let [val, right, closed, singular] = e; + if (singular || right == undefined) { + return MODE_SINGULAR; + } else if (right) { + if (closed) { + return MODE_RIGHT_CLOSED; + } else { + return MODE_RIGHT_OPEN; + } + } else { + if (closed) { + return MODE_LEFT_CLOSED; + } else { + return MODE_LEFT_OPEN; + } + } +} + +/* + get order + + given two endpoints + return two numbers representing their order + + also accepts regular numbers as endpoints + regular number are represented as singular endpoints + + for endpoint values that are not + equal, these values convey order directly, + otherwise endpoint mode numbers 0-4 are returned + + parameters are either + - point: Number + or, + - endpoint: [ + value (number), + right (bool), + closed (bool), + singular (bool) + ] +*/ + +function get_order(e1, e2) { + // support plain numbers (not endpoints) + if (e1.length === undefined) { + if (!isNumber(e1)) { + throw new Error("e1 not a number", e1); + } + e1 = create(e1, undefined, undefined, true); + } + if (e2.length === undefined) { + if (!isNumber(e2)) { + throw new Error("e2 not a number", e2); + } + e2 = create(e2, undefined, undefined, true); + } + if (e1[0] != e2[0]) { + // different values + return [e1[0], e2[0]]; + } else { + // equal values + return [get_mode(e1), get_mode(e2)]; + } +} + +/* + return true if e1 is ordered before e2 + false if equal +*/ + +function leftof(e1, e2) { + let [order1, order2] = get_order(e1, e2); + return (order1 < order2); +} + +/* + return true if e1 is ordered after e2 + false is equal +*/ + +function rightof(e1, e2) { + let [order1, order2] = get_order(e1, e2); + return (order1 > order2); +} + +/* + return true if e1 is ordered equal to e2 +*/ + +function equals(e1, e2) { + let [order1, order2] = get_order(e1, e2); + return (order1 == order2); +} + +/* + return -1 if ordering e1, e2 is correct + return 0 if e1 and e2 is equal + return 1 if ordering e1, e2 is incorrect +*/ + +function cmp(e1, e2) { + let [order1, order2] = get_order(e1, e2); + let diff = order1 - order2; + if (diff == 0) return 0; + return (diff > 0) ? 1 : -1; +} + + +function min(e1, e2) { + return (cmp(e1, e2) <= 0) ? e1 : e2; +} + + +function max(e1, e2) { + return (cmp(e1, e2) <= 0) ? e2 : e1; +} + + +/* + human friendly endpoint representation +*/ +function toString(e) { + if (e.length === undefined) { + return e.toString(); + } else { + let mode = get_mode(e); + let val = e[0]; + if (val == Infinity || val == -Infinity) { + val = "--"; + } + if (mode == MODE_RIGHT_OPEN) { + return `${val})` + } else if (mode == MODE_LEFT_CLOSED) { + return `[${val}` + } else if (mode == MODE_SINGULAR){ + return `[${val}]` + } else if (mode == MODE_RIGHT_CLOSED) { + return `${val}]` + } else if (mode == MODE_LEFT_OPEN) { + return `(${val}` + } + } +} + + +export default { + cmp, + toString, + equals, + rightof, + leftof, + create, + min, + max +}; diff --git a/v3/util/eventify.js b/v3/util/eventify.js new file mode 100644 index 0000000..6bd9664 --- /dev/null +++ b/v3/util/eventify.js @@ -0,0 +1,366 @@ +/* + Copyright 2020 + Author : Ingar Arntzen + + This file is part of the Timingsrc module. + + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Timingsrc 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . +*/ + + + +/* + Event + - name: event name + - publisher: the object which defined the event + - init: true if the event suppports init events + - subscriptions: subscriptins to this event + +*/ + +class Event { + + constructor (publisher, name, options) { + options = options || {} + this.publisher = publisher; + this.name = name; + this.init = (options.init === undefined) ? false : options.init; + this.subscriptions = []; + } + + /* + subscribe to event + - subscriber: subscribing object + - callback: callback function to invoke + - options: + init: if true subscriber wants init events + */ + subscribe (callback, options) { + if (!callback || typeof callback !== "function") { + throw new Error("Callback not a function", callback); + } + const sub = new Subscription(this, callback, options); + this.subscriptions.push(sub); + // Initiate init callback for this subscription + if (this.init && sub.init) { + sub.init_pending = true; + let self = this; + Promise.resolve().then(function () { + const eArgs = self.publisher.eventifyInitEventArgs(self.name) || []; + sub.init_pending = false; + for (let eArg of eArgs) { + self.trigger(eArg, [sub], true); + } + }); + } + return sub + } + + /* + trigger event + + - if sub is undefined - publish to all subscriptions + - if sub is defined - publish only to given subscription + */ + trigger (eArg, subs, init) { + let eInfo, ctx; + for (const sub of subs) { + // ignore terminated subscriptions + if (sub.terminated) { + continue; + } + eInfo = { + src: this.publisher, + name: this.name, + sub: sub, + init: init + } + ctx = sub.ctx || this.publisher; + try { + sub.callback.call(ctx, eArg, eInfo); + } catch (err) { + console.log(`Error in ${this.name}: ${sub.callback} ${err}`); + } + } + } + + /* + unsubscribe from event + - use subscription returned by previous subscribe + */ + unsubscribe(sub) { + let idx = this.subscriptions.indexOf(sub); + if (idx > -1) { + this.subscriptions.splice(idx, 1); + sub.terminate(); + } + } +} + + +/* + Subscription class +*/ + +class Subscription { + + constructor(event, callback, options) { + options = options || {} + this.event = event; + this.name = event.name; + this.callback = callback + this.init = (options.init === undefined) ? this.event.init : options.init; + this.init_pending = false; + this.terminated = false; + this.ctx = options.ctx; + } + + terminate() { + this.terminated = true; + this.callback = undefined; + this.event.unsubscribe(this); + } +} + + +/* + + EVENTIFY INSTANCE + + Eventify brings eventing capabilities to any object. + + In particular, eventify supports the initial-event pattern. + Opt-in for initial events per event type. + + eventifyInitEventArgs(name) { + if (name == "change") { + return [this._value]; + } + } + +*/ + +export function eventifyInstance (object) { + object.__eventify_eventMap = new Map(); + object.__eventify_buffer = []; + return object; +}; + + +/* + EVENTIFY PROTOTYPE + + Add eventify functionality to prototype object +*/ + +export function eventifyPrototype(_prototype) { + + function eventifyGetEvent(object, name) { + const event = object.__eventify_eventMap.get(name); + if (event == undefined) { + throw new Error("Event undefined", name); + } + return event; + } + + /* + DEFINE EVENT + - used only by event source + - name: name of event + - options: {init:true} specifies init-event semantics for event + */ + function eventifyDefine(name, options) { + // check that event does not already exist + if (this.__eventify_eventMap.has(name)) { + throw new Error("Event already defined", name); + } + this.__eventify_eventMap.set(name, new Event(this, name, options)); + }; + + /* + ON + - used by subscriber + register callback on event. + */ + function on(name, callback, options) { + return eventifyGetEvent(this, name).subscribe(callback, options); + }; + + /* + OFF + - used by subscriber + Un-register a handler from a specfic event type + */ + function off(sub) { + return eventifyGetEvent(this, sub.name).unsubscribe(sub); + }; + + + function eventifySubscriptions(name) { + return eventifyGetEvent(this, name).subscriptions; + } + + + + /* + Trigger list of eventItems on object + + eventItem: {name:.., eArg:..} + + copy all eventItems into buffer. + request emptying the buffer, i.e. actually triggering events, + every time the buffer goes from empty to non-empty + */ + function eventifyTriggerAll(eventItems) { + if (eventItems.length == 0) { + return; + } + + // make trigger items + // resolve non-pending subscriptions now + // else subscriptions may change from pending to non-pending + // between here and actual triggering + // make list of [ev, eArg, subs] tuples + let triggerItems = eventItems.map((item) => { + let {name, eArg} = item; + let ev = eventifyGetEvent(this, name); + let subs = ev.subscriptions.filter(sub => sub.init_pending == false); + return [ev, eArg, subs]; + }, this); + + // append trigger Items to buffer + const len = triggerItems.length; + const buf = this.__eventify_buffer; + const buf_len = this.__eventify_buffer.length; + // reserve memory - set new length + this.__eventify_buffer.length = buf_len + len; + // copy triggerItems to buffer + for (let i=0; i { + return {name, eArg}; + })); + } + + /* + Trigger single event + */ + function eventifyTrigger(name, eArg) { + return this.eventifyTriggerAll([{name, eArg}]); + } + + _prototype.eventifyDefine = eventifyDefine; + _prototype.eventifyTrigger = eventifyTrigger; + _prototype.eventifyTriggerAlike = eventifyTriggerAlike; + _prototype.eventifyTriggerAll = eventifyTriggerAll; + _prototype.eventifySubscriptions = eventifySubscriptions; + _prototype.on = on; + _prototype.off = off; +}; + + +/* + Event Variable + + Objects with a single "change" event +*/ + +export class EventVariable { + + constructor (value) { + eventifyInstance(this); + this._value = value; + this.eventifyDefine("change", {init:true}); + } + + eventifyInitEventArgs(name) { + if (name == "change") { + return [this._value]; + } + } + + get value () {return this._value}; + set value (value) { + if (value != this._value) { + this._value = value; + this.eventifyTrigger("change", value); + } + } +} +eventifyPrototype(EventVariable.prototype); + +/* + Event Boolean + + + Note : implementation uses falsiness of input parameter to constructor and set() operation, + so eventBoolean(-1) will actually set it to true because + (-1) ? true : false -> true ! +*/ + +export class EventBoolean extends EventVariable { + constructor(value) { + super(Boolean(value)); + } + + set value (value) { + super.value = Boolean(value); + } + get value () {return super.value}; +} + + +/* + make a promise which is resolved when EventBoolean changes + value. +*/ +export function makePromise(eventObject, conditionFunc) { + conditionFunc = conditionFunc || function(val) {return val == true}; + return new Promise (function (resolve, reject) { + let sub = eventObject.on("change", function (value) { + if (conditionFunc(value)) { + resolve(value); + eventObject.off(sub); + } + }); + }); +}; + +// module api +export default { + eventifyPrototype, + eventifyInstance, + EventVariable, + EventBoolean, + makePromise +}; + diff --git a/v3/util/interval.js b/v3/util/interval.js new file mode 100644 index 0000000..f63b199 --- /dev/null +++ b/v3/util/interval.js @@ -0,0 +1,408 @@ +/* + Copyright 2020 + Author : Ingar Arntzen + + This file is part of the Timingsrc module. + + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Timingsrc 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . +*/ + + +import endpoint from './endpoint.js'; + + +const isNumber = function(n) { + let N = parseFloat(n); + return (n===N && !isNaN(N)); +}; + +/********************************************************* +INTERVAL ERROR +**********************************************************/ + +class IntervalError extends Error { + constructor(message) { + super(message); + this.name == "IntervalError"; + } +}; + + +/********************************************************* +INTERVAL +**********************************************************/ + +// Interval Relations +const Relation = Object.freeze({ + OUTSIDE_LEFT: 64, // 0b1000000 + OVERLAP_LEFT: 32, // 0b0100000 + COVERED: 16, // 0b0010000 + EQUALS: 8, // 0b0001000 + COVERS: 4, // 0b0000100 + OVERLAP_RIGHT: 2, // 0b0000010 + OUTSIDE_RIGHT: 1 // 0b0000001 +}); + +/* + Masks for Interval matching +*/ +const MATCH_OUTSIDE = Relation.OUTSIDE_LEFT + Relation.OUTSIDE_RIGHT; +const MATCH_INSIDE = Relation.EQUALS + Relation.COVERED; +const MATCH_OVERLAP = MATCH_INSIDE + + Relation.OVERLAP_LEFT + Relation.OVERLAP_RIGHT; +const MATCH_COVERS = MATCH_OVERLAP + Relation.COVERS; +const MATCH_ALL = MATCH_COVERS + MATCH_OUTSIDE; + +const Match = Object.freeze({ + OUTSIDE: MATCH_OUTSIDE, + INSIDE: MATCH_INSIDE, + OVERLAP: MATCH_OVERLAP, + COVERS: MATCH_COVERS, + ALL: MATCH_ALL +}); + + +/********************************************************* +COMPARE INTERVALS +********************************************************** + +compare (a, b) +param a Interval +param b Interval +returns IntervalRelation + +compares interval b to interval a +e.g. return value COVERED reads b is covered by a. + +cmp_1 = endpoint_compare(b_low, a_low); +cmp_2 = endpoint_compare(b_high, a_high); + +key = 10*cmp_1 + cmp_2 + +cmp_1 cmp_2 key relation +===== ===== === ============================ +-1 -1 -11 OUTSIDE_LEFT, PARTIAL_LEFT +-1 0 -10 COVERS +-1 1 -9 COVERS +0 -1 -1 COVERED +0 0 0 EQUAL +0 1 1 COVERS +1 -1 9 COVERED +1 0 10 COVERED +1 1 11 OUTSIDE_RIGHT, OVERLAP_RIGHT +===== ===== === ============================ + +**********************************************************/ + +function compare(a, b) { + if (! a instanceof Interval) { + // could be a number + if (isNumber(a)) { + a = new Interval(a); + } else { + throw new IntervalError("a not interval", a); + } + } + if (! b instanceof Interval) { + // could be a number + if (isNumber(b)) { + b = new Interval(b); + } else { + throw new IntervalError("b not interval", b); + } + } + + let cmp_1 = endpoint.cmp(a.endpointLow, b.endpointLow); + let cmp_2 = endpoint.cmp(a.endpointHigh, b.endpointHigh); + let key = cmp_1*10 + cmp_2; + + if (key == 11) { + // OUTSIDE_LEFT or PARTIAL_LEFT + if (endpoint.leftof(b.endpointHigh, a.endpointLow)) { + return Relation.OUTSIDE_RIGHT; + } else { + return Relation.OVERLAP_RIGHT; + } + } else if ([-1, 9, 10].includes(key)) { + return Relation.COVERED; + } else if ([1, -9, -10].includes(key)) { + return Relation.COVERS; + } else if (key == 0) { + return Relation.EQUALS; + } else { + // key == -11 + // OUTSIDE_RIGHT, PARTIAL_RIGHT + if (endpoint.rightof(b.endpointLow, a.endpointHigh)) { + return Relation.OUTSIDE_LEFT; + } else { + return Relation.OVERLAP_LEFT; + } + } +} + +/********************************************************* +COMPARE INTERVALS BY ENDPOINT +********************************************************** + +cmp functions for sorting intervals (ascending) based on +endpoint low or high + +use with array.sort() + +**********************************************************/ + +function _make_interval_cmp(low) { + return function cmp (a, b) { + let e1, e2; + if (low) { + e1 = [a.low, false, a.lowInclude, a.singular]; + e2 = [b.low, false, b.lowInclude, a.singular]; + } else { + e1 = [a.high, true, a.highInclude, a.singular]; + e2 = [b.high, true, b.highInclude, a.singular]; + } + return endpoint.cmp(e1, e2); + } +} + + + +/** + * Create interval from two endpoints + */ + +function fromEndpoints(endpointLow, endpointHigh) { + let [low, low_right, low_closed, low_singular] = endpointLow; + let [high, high_right, high_closed, high_singular] = endpointHigh; + if (low_right) { + throw new IntervalError("illegal endpointLow - bracket must be left"); + } + if (!high_right) { + throw new IntervalError("illegal endpointHigh - bracket must be right"); + } + return new Interval(low, high, low_closed, high_closed); +}; + + +// intersect two intervals +function intersect(a, b) { + let rel = compare(a, b); + if (rel == Relation.OUTSIDE_LEFT) { + return []; + } else if (rel == Relation.OVERLAP_LEFT) { + return [Interval.fromEndpoints(b.endpointLow, a.endpointHigh)]; + } else if (rel == Relation.COVERS) { + return [b]; + } else if (rel == Relation.EQUALS) { + return [a]; // or b + } else if (rel == Relation.COVERED) { + return [a]; + } else if (rel == Relation.OVERLAP_RIGHT) { + return [Interval.fromEndpoints(a.endpointLow, b.endpointHigh)]; + } else if (rel == Relation.OUTSIDE_RIGHT) { + return []; + } +} + +// union of two intervals +function union(a, b) { + let rel = compare(a, b); + if (rel == Relation.OUTSIDE_LEFT) { + // merge + // [aLow,aHigh)[bLow, bHigh] or [aLow,aHigh](bLow, bHigh] + if (a.high != b.low || (!a.highInclude && !b.lowInclude)) { + // no merge + return [a, b]; + } else { + // merge + return [Interval.fromEndpoints(a.endpointLow, b.endpointHigh)]; + } + } else if (rel == Relation.OVERLAP_LEFT) { + return [Interval.fromEndpoints(a.endpointLow, b.endpointHigh)]; + } else if (rel == Relation.COVERS) { + return [a]; + } else if (rel == Relation.EQUALS) { + return [a]; // or b + } else if (rel == Relation.COVERED) { + return [b]; + } else if (rel == Relation.OVERLAP_RIGHT) { + return [Interval.fromEndpoints(b.endpointLow, a.endpointHigh)]; + } else if (rel == Relation.OUTSIDE_RIGHT) { + // merge + // [bLow,bHigh)[aLow, aHigh] or [bLow,bHigh](aLow, aHigh] + if (b.high != a.low || (!b.highInclude && !a.lowInclude)) { + // no merge + return [b, a]; + } else { + // merge + return [Interval.fromEndpoints(b.endpointLow, a.endpointHigh)]; + } + } +} + +// intersection of multiple intervals +function intersectAll(intervals) { + intervals.sort(Interval.cmpLow); + if (intervals.length <= 1) { + return intervals; + } + const result = [intervals.shift()]; + while (intervals.length > 0) { + let prev = result.pop(); + let next = intervals.shift() + result.push(...Interval.intersect(prev, next)); + } + return result; +} + +// union of multiple interval +function unionAll(intervals) { + intervals.sort(Interval.cmpLow); + if (intervals.length <= 1) { + return intervals; + } + const result = [intervals.shift()]; + while (intervals.length > 0) { + let prev = result.pop(); + let next = intervals.shift() + result.push(...Interval.union(prev, next)); + } + return result; +} + + +/********************************************************* +INTERVAL CLASS +**********************************************************/ + +class Interval { + + + // private variables + + /* + Constructor + */ + constructor (low, high, lowInclude, highInclude) { + var lowIsNumber = isNumber(low); + // new Interval(3.0) defines singular - low === high + if (lowIsNumber && high === undefined) high = low; + if (!isNumber(low)) throw new IntervalError(`low not a number, ${low}`); + if (!isNumber(high)) throw new IntervalError(`high not a number, ${high}`); + if (low > high) throw new IntervalError(`low > high, ${low}, ${high}`); + if (low === high) { + lowInclude = true; + highInclude = true; + } + if (low === -Infinity) lowInclude = true; + if (high === Infinity) highInclude = true; + if (lowInclude === undefined) lowInclude = true; + if (highInclude === undefined) highInclude = false; + if (typeof lowInclude !== "boolean") { + throw new IntervalError(`lowInclude not boolean, ${lowInclude}`); + } + if (typeof highInclude !== "boolean") { + throw new IntervalError(`highInclude not boolean, ${highInclude}`); + } + this._low = low; + this._high = high; + this._lowInclude = lowInclude; + this._highInclude = highInclude; + this._length = this._high - this._low; + this._singular = (this._low === this._high); + this._finite = (isFinite(this._low) && isFinite(this._high)); + + /* + Accessors for full endpoint representationo + [value (number), right (bool), closed (bool)] + + - use with inside(endpoint, interval) + */ + this._endpointLow = endpoint.create(this._low, false, this._lowInclude, this._singular); + this._endpointHigh = endpoint.create(this._high, true, this._highInclude, this._singular); + } + + // accessors + get low () {return this._low;} + get high () {return this._high;} + get lowInclude () {return this._lowInclude;} + get highInclude () {return this._highInclude;} + get length () {return this._length;} + get singular () {return this._singular;} + get finite () {return this._finite;} + get endpointLow () {return this._endpointLow;} + get endpointHigh () {return this._endpointHigh;} + + /** + * Instance methods + */ + + toString () { + const toString = endpoint.toString; + if (this._singular) { + let p = this._endpointLow[0]; + return `[${p}]`; + } else { + let low = endpoint.toString(this._endpointLow); + let high = endpoint.toString(this._endpointHigh); + return `${low},${high}`; + } + }; + + + asArray() { + return [this._low, this._high, this._lowInclude, this._highInclude]; + } + + covers_endpoint (p) { + let leftof = endpoint.leftof(p, this._endpointLow); + let rightof = endpoint.rightof(p, this._endpointHigh); + return !leftof && !rightof; + } + + compare (other) { + return compare(this, other); + } + + equals (other) { + return compare(this, other) == Relation.EQUALS; + } + + /* + default mode - all except outside + 2+4+8+16+32 = 62 + */ + match (other, mask=MATCH_COVERS) { + let relation = compare(this, other); + return Boolean(mask & relation); + } +} + +/* + Add static properties to Interval class. +*/ + +Interval.Relation = Relation; +Interval.Match = Match; +Interval.cmpLow = _make_interval_cmp(true); +Interval.cmpHigh = _make_interval_cmp(false); +Interval.fromEndpoints = fromEndpoints; +Interval.intersect = intersect; +Interval.union = union; +Interval.intersectAll = intersectAll; +Interval.unionAll = unionAll; + + +export default Interval; + diff --git a/v3/util/motionutils.js b/v3/util/motionutils.js new file mode 100644 index 0000000..b062205 --- /dev/null +++ b/v3/util/motionutils.js @@ -0,0 +1,782 @@ +/* + Copyright 2020 + Author : Ingar Arntzen + + This file is part of the Timingsrc module. + + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Timingsrc 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . +*/ + +import endpoint from './endpoint.js'; +import Interval from './interval.js'; + +// Closure +(function() { + /** + * Decimal adjustment of a number. + * + * @param {String} type The type of adjustment. + * @param {Number} value The number. + * @param {Integer} exp The exponent (the 10 logarithm of the adjustment base). + * @returns {Number} The adjusted value. + */ + function decimalAdjust(type, value, exp) { + // If the exp is undefined or zero... + if (typeof exp === 'undefined' || +exp === 0) { + return Math[type](value); + } + value = +value; + exp = +exp; + // If the value is not a number or the exp is not an integer... + if (isNaN(value) || !(typeof exp === 'number' && exp % 1 === 0)) { + return NaN; + } + // Shift + value = value.toString().split('e'); + value = Math[type](+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp))); + // Shift back + value = value.toString().split('e'); + return +(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp)); + } + + // Decimal round + if (!Math.round10) { + Math.round10 = function(value, exp) { + return decimalAdjust('round', value, exp); + }; + } + // Decimal floor + if (!Math.floor10) { + Math.floor10 = function(value, exp) { + return decimalAdjust('floor', value, exp); + }; + } + // Decimal ceil + if (!Math.ceil10) { + Math.ceil10 = function(value, exp) { + return decimalAdjust('ceil', value, exp); + }; + } +})(); + + +// sort func +const cmp = function (a, b) {return a - b;}; + +/******************************************************************* + BASIC +*******************************************************************/ + +export function equalVectors(vector_a, vector_b) { + let pos = vector_a.position == vector_b.position; + let vel = vector_a.velocity == vector_b.velocity; + let acc = vector_a.acceleration == vector_b.acceleration; + let ts = vector_a.timestamp == vector_b.timestamp; + return pos && vel && acc && ts; +}; + + +export function copyVector(vector) { + return { + position: vector.position, + velocity: vector.velocity, + acceleration: vector.acceleration, + timestamp: vector.timestamp + } +}; + +/* + Calculate vector snapshot for motion defined by vector at time ts + + vector: [p0,v0,a0,t0] + t0 and ts are absolute time from same clock, in seconds +*/ + +export function calculateVector(vector, ts) { + if (ts === undefined) { + throw new Error ("no ts provided for calculateVector"); + } + const deltaSec = ts - vector.timestamp; + return { + position : vector.position + vector.velocity*deltaSec + 0.5*vector.acceleration*deltaSec*deltaSec, + velocity : vector.velocity + vector.acceleration*deltaSec, + acceleration : vector.acceleration, + timestamp : ts + }; +}; + + +/* + Calculate direction of motion at time ts + 1 : forwards, -1 : backwards: 0, no movement +*/ +export function calculateDirection(vector, ts) { + /* + Given initial vector calculate direction of motion at time t + (Result is valid only if (t > vector[T])) + Return Forwards:1, Backwards -1 or No-direction (i.e. no-motion) 0. + If t is undefined - t is assumed to be now. + */ + let freshVector; + if (ts == undefined) { + freshVector = vector; + } else { + freshVector = calculateVector(vector, ts); + } + // check velocity + let direction = cmp(freshVector.velocity, 0.0); + if (direction === 0) { + // check acceleration + direction = cmp(vector.acceleration, 0.0); + } + return direction; +}; + + +/* + isMoving + + returns true if motion is moving else false +*/ +export function isMoving(vector) { + return (vector.velocity !== 0.0 || vector.acceleration !== 0.0); +}; + + +/******************************************************************* + RANGE +*******************************************************************/ + +// RANGE STATE is used for managing/detecting range violations. +export const RangeState = Object.freeze({ + INIT : "init", + INSIDE: "inside", + OUTSIDE_LOW: "outsidelow", + OUTSIDE_HIGH: "outsidehigh" +}); + +/* + A snapshot vector is checked with respect to range, + calclulates correct RangeState (i.e. INSIDE|OUTSIDE) +*/ +export function correctRangeState(vector, range) { + const {position: p, velocity: v, acceleration: a} = vector; + if (p > range[1]) return RangeState.OUTSIDE_HIGH; + if (p < range[0]) return RangeState.OUTSIDE_LOW; + // corner cases + if (p === range[1]) { + if (v > 0.0) return RangeState.OUTSIDE_HIGH; + if (v === 0.0 && a > 0.0) return RangeState.OUTSIDE_HIGH; + } else if (p === range[0]) { + if (v < 0.0) return RangeState.OUTSIDE_LOW; + if (v == 0.0 && a < 0.0) return RangeState.OUTSIDE_HIGH; + } + return RangeState.INSIDE; +}; + + +/* + detect range violation + vector assumed to be valid now +*/ +export function detectRangeViolation(now_vector, range) { + return (correctRangeState(now_vector, range) != RangeState.INSIDE); +} + + +/* + A snapshot vector is checked with respect to range. + Returns vector corrected for range violations, or input vector unchanged. + + vector assumed to be valid now +*/ +export function checkRange(vector, range) { + const state = correctRangeState(vector, range); + if (state !== RangeState.INSIDE) { + // protect from range violation + vector.velocity = 0.0; + vector.acceleration = 0.0; + if (state === RangeState.OUTSIDE_HIGH) { + vector.position = range[1]; + } else vector.position = range[0]; + } + return vector; +}; + + +/* + Return tsEndpoint of (first) range intersect if any. +*/ +export function rangeIntersect(vector, range) { + let t0 = vector.timestamp; + // Time delta to hit rangeLeft + let deltaLeft = calculateMinPositiveRealSolution(vector, range[0]); + // Time delta to hit rangeRight + let deltaRight = calculateMinPositiveRealSolution(vector, range[1]); + // Pick the appropriate solution + if (deltaLeft !== undefined && deltaRight !== undefined) { + if (deltaLeft < deltaRight) { + return [t0 + deltaLeft, range[0]]; + } + else + return [t0 + deltaRight, range[1]]; + } + else if (deltaLeft !== undefined) + return [t0 + deltaLeft, range[0]]; + else if (deltaRight !== undefined) + return [t0 + deltaRight, range[1]]; + else return [undefined, undefined]; +} + + +/******************************************************************* + EQUATIONS +*******************************************************************/ + +/* + hasRealSolution + + Given motion determined from p,v,a,t. + Determine if equation p(t) = p + vt + 0.5at^2 = x + has solutions for some real number t. +*/ + +function hasRealSolution (p,v,a,x) { + if ((Math.pow(v,2) - 2*a*(p-x)) >= 0.0) return true; + else return false; +}; + + +/* + calculateRealSolution + + Given motion determined from p,v,a,t. + Determine if equation p(t) = p + vt + 0.5at^2 = x + has solutions for some real number t. + Calculate and return real solutions, in ascending order. +*/ + +function calculateRealSolutions(p,v,a,x) { + // Constant Position + if (a === 0.0 && v === 0.0) { + if (p != x) return []; + else return [0.0]; + } + // Constant non-zero Velocity + if (a === 0.0) return [(x-p)/v]; + // Constant Acceleration + if (hasRealSolution(p,v,a,x) === false) return []; + // Exactly one solution + const discriminant = v*v - 2*a*(p-x); + if (discriminant === 0.0) { + return [-v/a]; + } + const sqrt = Math.sqrt(Math.pow(v,2) - 2*a*(p-x)); + const d1 = (-v + sqrt)/a; + const d2 = (-v - sqrt)/a; + return [Math.min(d1,d2),Math.max(d1,d2)]; +}; + + +/* + calculatePositiveRealSolutions + + Given motion determined from p,v,a,t. + Determine if equation p(t) = p + vt + 0.5at^2 = x + has solutions for some real number t. + Calculate and return positive real solutions, in ascending order. +*/ + +function calculatePositiveRealSolutions(p,v,a,x) { + const res = calculateRealSolutions(p,v,a,x); + if (res.length === 0) return []; + else if (res.length == 1) { + if (res[0] > 0.0) { + return [res[0]]; + } + else return []; + } + else if (res.length == 2) { + if (res[1] < 0.0) return []; + if (res[0] > 0.0) return [res[0], res[1]]; + if (res[1] > 0.0) return [res[1]]; + return []; + } + else return []; +}; + + +/* + calculateMinPositiveRealSolution + + Given motion determined from p,v,a,t. + Determine if equation p(t) = p + vt + 0.5at^2 = x + has solutions for some real number t. + Calculate and return the least positive real solution. +*/ +export function calculateMinPositiveRealSolution(vector, x) { + const {position: p, velocity: v, acceleration: a} = vector; + const res = calculatePositiveRealSolutions(p,v,a,x); + if (res.length === 0) { + return; + } + else return res[0]; +}; + + +/* + calculateDelta + + + Given motion determined from p0,v0,a0 (initial conditions or snapshot), + Supply two posisions, posBefore < p0 < posAfter. + Calculate which of these positions will be reached first, + if any, by the movement described by the vector. + In addition, calculate when this position will be reached. + Result will be expressed as time delta relative to t0, if solution exists, + and a flag to indicate Before (false) or After (true) + Note: t1 == (delta + t0) is only guaranteed to be in the + future as long as the function + is evaluated at time t0 or immediately after. +*/ +export function calculateDelta(vector, range) { + // Time delta to hit posBefore + let deltaBeforeSec = calculateMinPositiveRealSolution(vector, range[0]); + // Time delta to hit posAfter + let deltaAfterSec = calculateMinPositiveRealSolution(vector, range[1]); + // Infinity is no good solution + if (deltaBeforeSec == Infinity) { + deltaBeforeSec = undefined; + } + if (deltaAfterSec == Infinity) { + deltaAfterSec = undefined; + } + // Pick the appropriate solution + if (deltaBeforeSec !== undefined && deltaAfterSec !== undefined) { + if (deltaBeforeSec < deltaAfterSec) + return [deltaBeforeSec, range[0]]; + else + return [deltaAfterSec, range[1]]; + } + else if (deltaBeforeSec !== undefined) + return [deltaBeforeSec, range[0]]; + else if (deltaAfterSec !== undefined) + return [deltaAfterSec, range[1]]; + else return [undefined, undefined]; +}; + + +/******************************************************************* + TIME_INTERVAL POS_INTERVAL +*******************************************************************/ + +/* + posInterval_from_timeInterval + + given + - a time interval + - a vector describing motion within the time interval + + figure out an interval (of positions) + which covers all possible positions during the time interval + + the interval may be a little bigger, so we will round down and up + to the nearest integer. Also, the interval will always be closed. + +*/ + +export function posInterval_from_timeInterval (timeInterval, vector) { + + /* + no motion or singular time interval + */ + if (!isMoving(vector) || timeInterval.singular) { + return new Interval(vector.position); + } + + let t0 = timeInterval.low; + let t1 = timeInterval.high; + let t0_closed = timeInterval.lowInclude; + let t1_closed = timeInterval.highInclude; + + let vector0 = calculateVector(vector, t0); + let p0 = vector0.position; + let v0 = vector0.velocity; + let a0 = vector0.acceleration; + let p1 = calculateVector(vector, t1).position; + + let low, high; + + if (a0 != 0) { + + /* + motion, with acceleration + + position over time is a parabola + figure out if extrema happens to occor within + timeInterval. If it does, extreme point is endpoint in + position Interval. p0 or p1 will be the other + interval endpoint. + + I extreme point is not occuring within timeInterval, + interval endpoint will be p0 and p1. + + general parabola + y = Ax*x + Bx + C + extrema (x,y) : x = - B/2A, y = -B*B/4A + C + + where t0 <= t <= t1 + p(t) = 0.5*a0*(t-t0)*(t-t0) + v0*(t-t0) + p0, + + A = a0/2, B = v0, C = p0 + + extrema (t_extrema, p_extrema): + t_extrem = -v0/a0 + t0 + p_extrem = -v0*v0/(2*a0) + p0 + + */ + let t_extrem = -v0/a0 + t0; + if (timeInterval.covers_endpoint(t_extrem)) { + let p_extrem = -v0*v0/(2.0*a0) + p0; + // maximal point reached in time interval + if (a0 > 0.0) { + // p_extrem is minimum + // figure out if p0 or p1 is maximum + if (p0 < p1) { + low = p_extrem; + high = p1; + } else { + low = p_extrem; + high = p0; + } + } else { + // p_extrem is maximum + // figure out if p0 or p1 is minimum + if (p0 < p1) { + low = p0; + high = p_extrem; + } else { + low = p1; + high = p_extrem; + } + } + } else { + // see below + } + } + + /* + Motion, with or without acceleration, + yet with no extreme points within interval + + positition monotonic increasing (forward velocity) + or decreasing (backward velocity) + + extrem positions are associated with p0 and p1. + */ + if (p0 < p1) { + // forward + low = p0; + high = p1; + } else { + // backward + low = p1; + high = p0; + } + + /* + round down and up - to the nearest decimal + + Math.floor10(4.999999, -1) -> 4.9 + Math.floor10(5, -1) -> 5 + Math.floor10(5.000001, -1) -> 5 + + Math.ceil10(4.999999, -1) -> 5 + Math.ceil10(5, -1) -> 5 + Math.ceil10(5.000001, -1) -> 5.1 + */ + low = Math.floor10(low, -1); + high = Math.ceil10(high, -1); + return new Interval(low, high, true, true); +} + + +/* + time endpoint and pos endpoints. + + time is always increasing even when position + is decreasing. When making a timeEndpoint from + a posEndpoin the right/left aspect of the endpoint + needs to be flipped. + + ts - the value of the timeEndpoint, ie. the time when + motion will pass over posEndpoing + direction - direction of motion at time ts +*/ + +export function timeEndpoint_from_posEndpoint(posEndpoint, ts, direction) { + let [pos, right, close, singular] = posEndpoint; + // flip right/left if direction is backwards + if (direction < 0 && right !== undefined) { + right = !right + } + return [ts, right, close, singular]; +} + + +/******************************************************************* + ENDPOINT EVENTS +*******************************************************************/ + +/* + endpointEvents + + Given a motion and a set of endpoints, calculate when + the motion will pass by each endpoint. + + Given + - timeInterval + - posInterval + - vector describing motion within timeInterval + - list of endpointItems + + endpointItem + { + endpoint: [value, high, closed, singular], + cue: { + key: "mykey", + interval: new Interval(...), + data: {...} + } + } + + Creates eventItem by adding to endpointItem + - tsEndpoint : timestamp endpoint (future) when motion will pass the endpoint + - direction: true if motion passes endpoint while moving forward + + EventItems will be sorted by ts + + Issue: + + timeInterval [t0, t1) + posinterval [p0, p1) + + Consider event at time t1 concerning endpoint p1) + This will be outside the timeInterval, but inside + the posInterval. + + Conversely, it will be inside the next timeInterval, + but not the next posInterval. + + This is a problem - like falling between chairs. + + Resolve this by representing timestamps as endpoints too + +*/ + +export function endpointEvents (timeInterval, posInterval, vector, endpointItems) { + + /* + no motion or singular time interval + */ + if (timeInterval.singular) { + throw new Error("endpointEvents: timeInterval is singular"); + } + if (!isMoving(vector)) { + throw new Error("endpointEvents: no motion") + } + + let p0 = vector.position; + let v0 = vector.velocity; + let a0 = vector.acceleration; + let t0 = vector.timestamp; + + let value, ts, deltas; + let tsEndpoint, direction; + let eventItems = []; + + endpointItems.forEach(function(item) { + // check that endpoint is inside given posInterval + if (!posInterval.covers_endpoint(item.endpoint)) { + console.log("fuck 1"); + return; + } + value = item.endpoint[0]; + // check if equation has any solutions + if (!hasRealSolution(p0, v0, a0, value)) { + console.log("fuck 2"); + return; + } + // find time when motion will pass value + // time delta is relative to t0 + // could be both in history or future + deltas = calculateRealSolutions(p0,v0,a0, value); + // include any timestamp within the timeinterval + deltas.forEach(function(delta) { + ts = t0 + delta; + direction = calculateDirection(vector, ts); + tsEndpoint = timeEndpoint_from_posEndpoint(item.endpoint, ts, direction); + if (timeInterval.covers_endpoint(tsEndpoint)){ + item.tsEndpoint = tsEndpoint; + item.direction = direction; + eventItems.push(item); + } + }); + }); + + // sort eventItems according to tsEndpoints + const cmp = function (a,b) { + return endpoint.cmp(a.tsEndpoint, b.tsEndpoint); + }; + eventItems.sort(cmp); + + /* + if (eventItems.length != endpointItems.length) { + console.log("BADNESS"); + console.log("timeInterval", timeInterval); + console.log("posInterval", posInterval); + console.log("vector", vector); + console.log("endpointItems", JSON.stringify(endpointItems)); + } + */ + + return eventItems; +}; + + +/******************************************************************* + MOTION TRANSITION +*******************************************************************/ + +/* + Figure the nature of the transition from one motion to another, + i.e. when old_vector is replaced by new_vector. + + The time when this transition occured is given bey + new_vector.timestamp, by definition. + + - was moving (boolean) - true if moving before change + - is moving (boolean) - true if moving after change + - pos changed (boolean) - true if position was changed instantaneously + - move changed (boolean) - true if movement was changed instantaneously + + report changed in two independent aspects + - change in position (i.e. discontinuity in position) + - change in movement (i.e. starting, stopping, changed) + + These are represented as + - PosDelta + - MoveDelta + + return [PosDelta, MoveDelta] +*/ + + +/* Static properties */ + +const PosDelta = Object.freeze({ + NOOP: 0, // no change in position + CHANGE: 1 // change in position +}); + + +const MoveDelta = Object.freeze({ + NOOP: 0, // no change in movement, not moving + NOOP_MOVING: 1, // no change in movement, moving + START: 2, // not moving -> moving + CHANGE: 3, // keep moving, movement changed + STOP: 4 // moving -> not moving +}); + + +export class MotionDelta { + + constructor (old_vector, new_vector) { + let ts = new_vector.timestamp; + let is_moving = isMoving(new_vector) + let init = (old_vector == undefined || old_vector.position == undefined); + + if (init) { + /* + Possible to introduce + PosDelta.INIT here instead of PosDelta.CHANGE + Not sure if this is needed. + */ + if (is_moving) { + this._mc = [PosDelta.CHANGE, MoveDelta.START]; + } else { + this._mc = [PosDelta.CHANGE, MoveDelta.NOOP]; + } + } else { + let was_moving = isMoving(old_vector); + let end_vector = calculateVector(old_vector, ts); + let start_vector = calculateVector(new_vector, ts); + + // position change + let pos_changed = (end_vector.position != start_vector.position); + let pct = (pos_changed) ? PosDelta.CHANGE : PosDelta.NOOP; + + // movement change + let mct; + if (was_moving && is_moving) { + let vel_changed = (end_vector.velocity != start_vector.velocity); + let acc_changed = (end_vector.acceleration != start_vector.acceleration); + let move_changed = (vel_changed || acc_changed); + if (move_changed) { + mct = MoveDelta.CHANGE; + } else { + mct = MoveDelta.NOOP_MOVING; + } + } else if (!was_moving && is_moving) { + mct = MoveDelta.START; + } else if (was_moving && !is_moving) { + mct = MoveDelta.STOP; + } else if (!was_moving && !is_moving) { + mct = MoveDelta.NOOP; + } + this._mc = [pct, mct]; + } + } + + get posDelta () { + return this._mc[0]; + } + + get moveDelta () { + return this._mc[1] + } + + toString() { + const PosDelta = MotionDelta.PosDelta; + const MoveDelta = MotionDelta.MoveDelta; + let str = (this.posDelta == PosDelta.CHANGE) ? "jump, " : ""; + if (this.moveDelta == MoveDelta.START) { + str += "movement started"; + } else if (this.moveDelta == MoveDelta.CHANGE) { + str += "movement changed"; + } else if (this.moveDelta == MoveDelta.STOP) { + str += "movement stopped"; + } else if (this.moveDelta == MoveDelta.NOOP_MOVING) { + str += "movement noop - moving"; + } else if (this.moveDelta == MoveDelta.NOOP) { + str += "movement noop - not moving"; + } + return str; + } +} + + +MotionDelta.PosDelta = PosDelta; +MotionDelta.MoveDelta = MoveDelta; + diff --git a/v3/util/observablemap.js b/v3/util/observablemap.js new file mode 100644 index 0000000..85b31f5 --- /dev/null +++ b/v3/util/observablemap.js @@ -0,0 +1,221 @@ +/* + Copyright 2020 + Author : Ingar Arntzen + + This file is part of the Timingsrc module. + + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Timingsrc 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . +*/ + +import eventify from './eventify.js'; + +/******************************************************************* + BASE OBSERVABLE MAP +*******************************************************************/ + +/* + This is a base class for observable map +*/ + +class ObservableMap { + + constructor (options={}) { + + this.options = options; + + // Events + eventify.eventifyInstance(this); + this.eventifyDefine("batch", {init:true}); + this.eventifyDefine("change", {init:true}); + this.eventifyDefine("remove", {init:false}); + } + + /** + * Abstract accessor to datasource backing implementation + * of observable map. Typically this is an instance of Map() class. + * + * Must be implemented by subclass. + */ + + get datasource () { + throw new Error("not implemented"); + } + + /*************************************************************** + ORDERING + ***************************************************************/ + + sortOrder(options={}) { + // sort options override constructor options + let {order=this.options.order} = options; + if (typeof order == "function") { + return order; + } + } + + /* + Sort values of Observable map + ordering can be overidden by specifying option + fallback to order from constructor + noop if no ordering is defined + */ + sortValues(iter, options={}) { + let order = this.sortOrder(options); + if (typeof order == "function") { + // sort + // if iterable not array - convert into array ahead of sorting + let arr = (Array.isArray(iter)) ? iter : [...iter]; + return arr.sort(order); + } else { + // noop + return iter; + } + } + + /* + Sort items (in-place) by value {new:value, old:value} using + ordering function for values + */ + sortItems(items) { + let order = this.sortOrder(); + if (typeof order == "function") { + items.sort(function(item_a, item_b) { + let cue_a = (item_a.new) ? item_a.new : item_a.old; + let cue_b = (item_b.new) ? item_b.new : item_b.old; + return order(cue_a, cue_b); + }); + } + } + + /*************************************************************** + EVENTS + ***************************************************************/ + + /* + Eventify: immediate events + */ + eventifyInitEventArgs(name) { + if (name == "batch" || name == "change") { + let items = [...this.datasource.entries()].map(([key, val]) => { + return {key:key, new:val, old:undefined}; + }); + // sort init items (if order defined) + this.sortItems(items); + return (name == "batch") ? [items] : items; + } + } + + /* + Event Notification + */ + _notifyEvents(items) { + // event notification + if (items.length == 0) { + return; + } + const has_update_subs = this.eventifySubscriptions("batch").length > 0; + const has_remove_subs = this.eventifySubscriptions("remove").length > 0; + const has_change_subs = this.eventifySubscriptions("change").length > 0; + // update + if (has_update_subs) { + this.eventifyTrigger("batch", items); + } + // change, remove + if (has_remove_subs || has_change_subs) { + for (let item of items) { + if (item.new == undefined && item.old != undefined) { + if (has_remove_subs) { + this.eventifyTrigger("remove", item); + } + } else { + if (has_change_subs) { + this.eventifyTrigger("change", item); + } + } + } + } + } + + + /*************************************************************** + ACCESSORS + ***************************************************************/ + + get size () { + return this.datasource.size; + } + + has(key) { + return this.datasource.has(key); + }; + + get(key) { + return this.datasource.get(key); + }; + + keys() { + return this.datasource.keys(); + }; + + values() { + return this.datasource.values(); + }; + + entries() { + return this.datasource.entries(); + } + + + /*************************************************************** + MODIFY + ***************************************************************/ + + set(key, value) { + let old = undefined; + if (this.datasource.has(key)) { + old = this.datasource.get(key); + } + this.datasource.set(key, value); + this._notifyEvents([{key: key, new:value, old: old}]); + return this; + } + + delete(key) { + let result = false; + let old = undefined; + if (this.datasource.has(key)) { + old = this.datasource.get(key); + this.datasource.delete(key); + result = true; + } + this._notifyEvents([{key: key, new:undefined, old: old}]); + return result; + } + + clear() { + // create change events for all cues + const items = [...this.datasource.entries()].map(([key, val]) => { + return {key: key, new: undefined, old: val}; + }) + // clear _map + this.datasource.clear(); + // event notification + this._notifyEvents(items); + } + +} + +eventify.eventifyPrototype(ObservableMap.prototype); + +export default ObservableMap; diff --git a/v3/util/timeout.js b/v3/util/timeout.js new file mode 100644 index 0000000..5021d1e --- /dev/null +++ b/v3/util/timeout.js @@ -0,0 +1,87 @@ +/* + Copyright 2020 + Author : Ingar Arntzen + + This file is part of the Timingsrc module. + + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Timingsrc 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . +*/ + + +/* + Wraps the built in setTimeout to provide a + Timeout that does not fire too early. + + Importantly, the Timeout object manages at most + one timeout. + + - Given clock.now() returns a value in seconds. + - The timeout is set with and absolute timestamp, + not a delay. +*/ + +class Timeout { + + constructor (timingObject, callback) { + this.tid = undefined; + this.to = timingObject; + this.callback = callback; + } + + isSet() { + return this.tid != undefined; + } + + /* + set timeout to point in time (seconds) + */ + setTimeout(target_ts, arg) { + if (this.tid != undefined) { + throw new Error("at most on timeout"); + } + let now = this.to.clock.now(); + let delay = Math.max(target_ts - now, 0) * 1000; + this.tid = setTimeout(this.onTimeout.bind(this), delay, target_ts, arg); + } + + /* + handle timeout intended for point in time (seconds) + */ + onTimeout(target_ts, arg) { + if (this.tid != undefined) { + this.tid = undefined; + // check if timeout was too early + let now = this.to.clock.now() + if (now < target_ts) { + // schedule new timeout + this.setTimeout(target_ts, arg); + } else { + // handle timeout + this.callback(now, arg); + } + } + } + + /* + cancel and clear timeout if active + */ + clear() { + if (this.tid != undefined) { + clearTimeout(this.tid); + this.tid = undefined; + } + } +} + +export default Timeout; diff --git a/v3/util/utils.js b/v3/util/utils.js new file mode 100644 index 0000000..58fe6ed --- /dev/null +++ b/v3/util/utils.js @@ -0,0 +1,236 @@ +/* + Copyright 2020 + Author : Ingar Arntzen + + This file is part of the Timingsrc module. + + Timingsrc is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Timingsrc 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with Timingsrc. If not, see . +*/ + + +export function random_string(length) { + var text = ""; + var possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + for(var i = 0; i < length; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} + + +/* Set Comparison */ +export function eqSet(as, bs) { + return as.size === bs.size && all(isIn(bs), as); +} + +export function all(pred, as) { + for (var a of as) if (!pred(a)) return false; + return true; +} + +export function isIn(as) { + return function (a) { + return as.has(a); + }; +} + +export function set_difference(as, bs) { + return new Set([...as].filter((e) => !bs.has(e))); +} + + + + + +/* + get the difference of two Maps + key in a but not in b +*/ +export const map_difference = function (a, b) { + if (a.size == 0) { + return new Map(); + } else if (b.size == 0) { + return a; + } else { + return new Map([...a].filter(function ([key, value]) { + return !b.has(key) + })); + } +}; + +/* + get the intersection of two Maps + key in a and b +*/ +export const map_intersect = function (a, b) { + [a, b] = (a.size <= b.size) ? [a,b] : [b,a]; + if (a.size == 0) { + // No intersect + return new Map(); + } + return new Map([...a].filter(function ([key, value]) { + return b.has(key) + })); +}; + +/* + +NOTE : just as good to do + let merged = new Map(...map0, ...map1, ...) + +effective concatenation of multiple arrays +- order - if true preserves ordering of input arrays + - else sorts input arrays (longest first) + - default false is more effective +- copy - if true leaves input arrays unchanged, copy + values into new array + - if false copies remainder arrays into the first + array + - default false is more effective +*/ +export function map_merge(array_of_maps, options={}) { + let {copy=false, order=false} = options; + // check input + if (array_of_maps instanceof Map) { + return array_of_maps; + } + if (!Array.isArray(array_of_maps)) { + throw new Error("illegal input array_of_maps", array_of_maps); + } + if (array_of_maps.length == 0) { + throw new Error("empty array_of_maps"); + } + let is_maps = array_of_maps.map((o) => { + return (o instanceof Map); + }); + if (!is_maps.every((e) => e == true)) { + throw new Error("some object in array_of_maps is not a Map", array_of_maps); + } + // order + if (!order) { + // sort array_of_maps according to size - longest first + array_of_maps.sort((a, b) => b.size - a.size); + } + // copy + let first = (copy) ? new Map() : array_of_maps.shift(); + // fill up first Map with entries from other Maps + for (let m of array_of_maps) { + for (let [key, val] of m.entries()) { + first.set(key, val); + } + } + return first; +} + + +export function divmod (n, d) { + let r = n % d; + let q = (n-r)/d; + return [q, r]; +} + + +export function isIterable(obj) { + // checks for null and undefined + if (obj == null) { + return false; + } + return typeof obj[Symbol.iterator] === 'function'; +} + +/* + effective concatenation of multiple arrays + - order - if true preserves ordering of input arrays + - else sorts input arrays (longest first) + - default false is more effective + - copy - if true leaves input arrays unchanged, copy + values into new array + - if false copies remainder arrays into the first + array + - default false is more effective +*/ +export function array_concat(arrays, options = {}) { + let {copy=false, order=false} = options; + if (arrays.length == 0) { + return []; + } + if (arrays.length == 1) { + return arrays[0]; + } + let total_len = arrays.reduce((acc, cur) => acc + cur.length, 0); + // order + if (!order) { + // sort arrays according to length - longest first + arrays.sort((a, b) => b.length - a.length); + } + // copy + let first = (copy) ? [] : arrays.shift(); + let start = first.length; + // reserve memory total length + first.length = total_len; + // fill up first with entries from other arrays + let end, len; + for (let arr of arrays) { + len = arr.length; + end = start + len; + for (let i=0; i not equal + if (aProps.length != bProps.length) { + return false; + } + for (let i=0; i not equal + if (a[propName] !== b[propName]) { + return false; + } + } + // equal + return true; +} + + +/* document readypromise */ +export const docready = new Promise(function(resolve) { + if (document.readyState === 'complete') { + resolve(); + } else { + let onReady = function () { + resolve(); + document.removeEventListener('DOMContentLoaded', onReady, true); + window.removeEventListener('load', onReady, true); + }; + document.addEventListener('DOMContentLoaded', onReady, true); + window.addEventListener('load', onReady, true); + } +}); +