From 08d52fa371d3f5a60462f72090113c70db8fb8bc Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Tue, 22 Dec 2020 01:01:36 +0100 Subject: [PATCH] Initial commit, pushing everything done so far --- .gitignore | 21 + LICENSE.md | 676 ++++++++++++++ qml/AppMenuBar.qml | 66 ++ qml/ComboBoxSetting.qml | 53 ++ qml/FileDialog.qml | 32 + qml/LogGraph.qml | 169 ++++ qml/LogGraphCanvas.qml | 173 ++++ qml/ObjectLists.qml | 290 ++++++ qml/Settings.qml | 209 +++++ qml/TextSetting.qml | 78 ++ qml/js/expr-eval.js | 1837 +++++++++++++++++++++++++++++++++++++++ qml/js/mathlib.js | 274 ++++++ qml/js/objects.js | 235 +++++ qml/js/utils.js | 118 +++ run.py | 94 ++ 15 files changed, 4325 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE.md create mode 100644 qml/AppMenuBar.qml create mode 100644 qml/ComboBoxSetting.qml create mode 100644 qml/FileDialog.qml create mode 100644 qml/LogGraph.qml create mode 100644 qml/LogGraphCanvas.qml create mode 100644 qml/ObjectLists.qml create mode 100644 qml/Settings.qml create mode 100644 qml/TextSetting.qml create mode 100644 qml/js/expr-eval.js create mode 100644 qml/js/mathlib.js create mode 100644 qml/js/objects.js create mode 100644 qml/js/utils.js create mode 100644 run.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8aefb4f --- /dev/null +++ b/.gitignore @@ -0,0 +1,21 @@ +build/ +dist/ +*.spec +*.zip +**/**.qmlc +**/**.jsc +*.jsc +*.qmlc +.DS_Store +**/.DS_Store +**/__pycache__/ +.ropeproject +.vscode +build +docs/html +.directory +loggraph.kdev4 +.kdev4 +AccountFree.pro +AccountFree.pro.user +AccountFree.egg-info/ diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..bd06286 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,676 @@ +### GNU 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. + + Logarithm Graph Creator - Create graphs with logarithm scales. + Copyright (C) 2020 Ad5001 + + 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: + + Logarithm Graph Creator Copyright (C) 2020 Ad5001 + 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 . + diff --git a/qml/AppMenuBar.qml b/qml/AppMenuBar.qml new file mode 100644 index 0000000..c5126ed --- /dev/null +++ b/qml/AppMenuBar.qml @@ -0,0 +1,66 @@ +/** + * Logarithm Graph Creator - Create graphs with logarithm scales. + * Copyright (C) 2020 Ad5001 + * + * 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 . + */ + +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +MenuBar { + Menu { + title: qsTr("&File") + Action { + text: qsTr("&Load...") + shortcut: StandardKey.Open + onTriggered: settings.load() + icon.name: 'fileopen' + + } + Action { + text: qsTr("&Save") + shortcut: StandardKey.Save + onTriggered: settings.save() + icon.name: 'filesave' + } + Action { + text: qsTr("Save &As...") + shortcut: StandardKey.SaveAs + onTriggered: settings.saveAs() + icon.name: 'filesaveas' + + } + MenuSeparator { } + Action { + text: qsTr("&Quit") + shortcut: StandardKey.Quit + onTriggered: Qt.quit() + icon.name: 'application-exit' + } + } + Menu { + title: qsTr("&Edit") + Action { + text: qsTr("&Copy diagram") + shortcut: StandardKey.Copy + onTriggered: root.copyDiagramToClipboard() + icon.name: 'editcopy' + } + } + Menu { + title: qsTr("&Help") + Action { text: qsTr("&About") } + } +} diff --git a/qml/ComboBoxSetting.qml b/qml/ComboBoxSetting.qml new file mode 100644 index 0000000..7d3a2c5 --- /dev/null +++ b/qml/ComboBoxSetting.qml @@ -0,0 +1,53 @@ +/** + * Logarithm Graph Creator - Create graphs with logarithm scales. + * Copyright (C) 2020 Ad5001 + * + * 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 . + */ + +import QtQuick 2.12 +import QtQuick.Controls 2.12 + +Item { + id: control + height: 30 + + signal activated(int newIndex) + + property var model: [] + property string label: '' + property alias currentIndex: combox.currentIndex + + Text { + id: labelItem + height: 30 + anchors.top: parent.top + verticalAlignment: TextInput.AlignVCenter + color: sysPalette.windowText + text: " "+ control.label +": " + } + + ComboBox { + id: combox + height: 30 + anchors.left: labelItem.right + anchors.leftMargin: 5 + width: control.width - labelItem.width + model: control.model + currentIndex: model.indexOf(defValue) + onActivated: function(newIndex) { + control.activated(newIndex) + } + } +} diff --git a/qml/FileDialog.qml b/qml/FileDialog.qml new file mode 100644 index 0000000..517cd27 --- /dev/null +++ b/qml/FileDialog.qml @@ -0,0 +1,32 @@ +/** + * Logarithm Graph Creator - Create graphs with logarithm scales. + * Copyright (C) 2020 Ad5001 + * + * 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 . + */ + +import QtQuick.Dialogs 1.3 as D + +D.FileDialog { + id: fileDialog + + property bool exportMode: false + + title: exportMode ? "Export Logarithmic Graph file" : "Import Logarithmic Graph file" + nameFilters: ["Logarithmic Graph JSON Data (*.json)", "All files (*)"] + + folder: shortcuts.documents + selectExisting: !exportMode + +} diff --git a/qml/LogGraph.qml b/qml/LogGraph.qml new file mode 100644 index 0000000..67dd27b --- /dev/null +++ b/qml/LogGraph.qml @@ -0,0 +1,169 @@ +/** + * Logarithm Graph Creator - Create graphs with logarithm scales. + * Copyright (C) 2020 Ad5001 + * + * 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 . + */ + +import QtQml 2.12 +import QtQuick.Controls 2.12 +import QtQuick.Layouts 1.15 +import QtQuick 2.12 +import "js/objects.js" as Objects + + +ApplicationWindow { + id: root + visible: true + width: 1000 + height: 500 + color: sysPalette.window + title: "Logarithmic Graph Creator " + (settings.saveFilename != "" ? " - " + settings.saveFilename : "") + + SystemPalette { id: sysPalette; colorGroup: SystemPalette.Active } + SystemPalette { id: sysPaletteIn; colorGroup: SystemPalette.Disabled } + + menuBar: AppMenuBar {} + + Drawer { + id: sidebar + width: 290 + height: parent.height + y: root.menuBar.height + readonly property bool inPortrait: root.width < root.height + modal: inPortrait + interactive: inPortrait + position: inPortrait ? 0 : 1 + visible: !inPortrait + + + Rectangle { + id: topSeparator + color: sysPaletteIn.dark + width: parent.width + height: 2 + } + + TabBar { + id: sidebarSelector + width: parent.width + anchors.top: topSeparator.bottom + TabButton { + text: qsTr("Settings") + } + TabButton { + text: qsTr("Objects") + } + } + + StackLayout { + width: parent.width + currentIndex: sidebarSelector.currentIndex + anchors.top: sidebarSelector.bottom + height: parent.height - sidebarSelector.height + + Settings { + id: settings + + onChanged: drawCanvas.requestPaint() + onCopyToClipboard: root.copyDiagramToClipboard() + onSaveDiagram: root.saveDiagram(filename) + onLoadDiagram: root.loadDiagram(filename) + } + + ObjectLists { + id: objectLists + onChanged: drawCanvas.requestPaint() + } + } + } + + LogGraphCanvas { + id: drawCanvas + anchors.top: parent.top + anchors.left: sidebar.right + height: parent.height + width: parent.width - sidebar.position*sidebar.width + x: sidebar.position*sidebar.width + + xmin: settings.xmin + ymax: settings.ymax + xzoom: settings.xzoom + yzoom: settings.yzoom + xlabel: settings.xaxislabel + ylabel: settings.yaxislabel + yaxisstep: settings.yaxisstep + + onPaint: { + var ctx = getContext("2d"); + } + } + + function saveDiagram(filename) { + var objs = {} + Object.keys(Objects.currentObjects).forEach(function(objType){ + objs[objType] = [] + Objects.currentObjects[objType].forEach(function(obj){ + objs[objType].push(obj.export()) + }) + }) + Helper.write(filename, JSON.stringify({ + "xzoom": settings.xzoom, + "yzoom": settings.yzoom, + "xmin": settings.xmin, + "ymax": settings.ymax, + "yaxisstep": settings.yaxisstep, + "xlabel": settings.xaxislabel, + "ylabel": settings.yaxislabel, + "width": root.width, + "height": root.height, + "objects": objs, + "type": "bodediagramv1" + })) + } + + function loadDiagram(filename) { + var data = JSON.parse(Helper.load(filename)) + if(Object.keys(data).indexOf("type") != -1 && data["type"] == "bodediagramv1") { + settings.xzoom = data["xzoom"] + settings.yzoom = data["yzoom"] + settings.xmin = data["xmin"] + settings.ymax = data["ymax"] + settings.yaxisstep = data["yaxisstep"] + settings.xaxislabel = data["xlabel"] + settings.yaxislabel = data["ylabel"] + root.height = data["height"] + root.width = data["width"] + + Object.keys(data['objects']).forEach(function(objType){ + Objects.currentObjects[objType] = [] + data['objects'][objType].forEach(function(objData){ + var obj = new Objects.drawableTypes[objType](...objData) + Objects.currentObjects[objType].push(obj) + }) + }) + // Refreshing sidebar + Object.keys(objectLists.listViews).forEach(function(type){ + objectLists.listViews[type].model = Objects.currentObjects[type] + }) + drawCanvas.requestPaint() + } + } + + function copyDiagramToClipboard() { + var file = Helper.gettmpfile() + drawCanvas.save(file) + Helper.copyImageToClipboard() + } +} diff --git a/qml/LogGraphCanvas.qml b/qml/LogGraphCanvas.qml new file mode 100644 index 0000000..78a47fa --- /dev/null +++ b/qml/LogGraphCanvas.qml @@ -0,0 +1,173 @@ +/** + * Logarithm Graph Creator - Create graphs with logarithm scales. + * Copyright (C) 2020 Ad5001 + * + * 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 . + */ + +import QtQuick 2.12 +import "js/objects.js" as Objects +import "js/utils.js" as Utils + + +Canvas { + id: canvas + anchors.top: separator.bottom + anchors.left: parent.left + height: parent.height - 90 + width: parent.width + + property double xmin: 0 + property double ymax: 0 + property int xzoom: 10 + property int yzoom: 10 + property double yaxisstep: 3 + property string xlabel: "" + property string ylabel: "" + + onPaint: { + //console.log('Redrawing') + var ctx = getContext("2d"); + reset(ctx) + drawGrille(ctx) + drawAxises(ctx) + Object.keys(Objects.currentObjects).forEach(function(objType){ + Objects.currentObjects[objType].forEach(function(obj){ + if(obj.visible) obj.draw(canvas, ctx) + }) + }) + drawLabels(ctx) + + } + + function reset(ctx){ + // Reset + ctx.fillStyle = "#FFFFFF" + ctx.strokeStyle = "#000000" + ctx.font = "12px sans-serif" + ctx.fillRect(0,0,width,height) + } + + // Drawing the log based graph + function drawGrille(ctx) { + ctx.strokeStyle = "#AAAAAA" + for(var xpow = -10; xpow <= 10; xpow++) { + for(var xmulti = 1; xmulti < 10; xmulti++) { + drawXLine(ctx, Math.pow(10, xpow)*xmulti) + } + } + for(var y = -Math.round(100/yaxisstep)*yaxisstep; y < canvas.ymax; y+=yaxisstep) { + drawYLine(ctx, y) + } + } + + function drawAxises(ctx) { + ctx.strokeStyle = "#000000" + drawXLine(ctx, 1) + drawYLine(ctx, 0) + var axisypx = x2px(1) // X coordinate of Y axis + var axisxpx = y2px(0) // Y coordinate of X axis + // Drawing arrows + drawLine(ctx, axisypx, 0, axisypx-10, 10) + drawLine(ctx, axisypx, 0, axisypx+10, 10) + drawLine(ctx, canvas.canvasSize.width, axisxpx, canvas.canvasSize.width-10, axisxpx-10) + drawLine(ctx, canvas.canvasSize.width, axisxpx, canvas.canvasSize.width-10, axisxpx+10) + } + + function drawLabels(ctx) { + var axisypx = x2px(1) // X coordinate of Y axis + var axisxpx = y2px(0) // Y coordinate of X axis + // Labels + ctx.fillStyle = "#000000" + ctx.font = "16px sans-serif" + ctx.fillText(canvas.ylabel, axisypx+5, 24) + var textSize = ctx.measureText(canvas.xlabel).width + ctx.fillText(canvas.xlabel, canvas.canvasSize.width-14-textSize, axisxpx-5) + // Axis graduation labels + ctx.font = "14px sans-serif" + + for(var xpow = -10; xpow <= 10; xpow+=1) { + var textSize = ctx.measureText("10"+Utils.textsup(xpow)).width + if(xpow != 0) + drawVisibleText(ctx, "10"+Utils.textsup(xpow), x2px(Math.pow(10,xpow))-textSize/2, axisxpx+12+(6*(y==0))) + } + for(var y = -Math.round(100/yaxisstep)*yaxisstep; y < canvas.ymax; y+=yaxisstep) { + var textSize = ctx.measureText(y).width + drawVisibleText(ctx, y, axisypx-3-textSize, y2px(y)+6+(6*(y==0))) + } + ctx.fillStyle = "#FFFFFF" + } + + function drawXLine(ctx, x) { + if(visible(x, canvas.ymax)) { + drawLine(ctx, x2px(x), 0, x2px(x), canvas.canvasSize.height) + } + } + + function drawYLine(ctx, y) { + if(visible(canvas.xmin, y)) { + drawLine(ctx, 0, y2px(y), canvas.canvasSize.width, y2px(y)) + } + } + + function drawVisibleText(ctx, text, x, y, lineHeight = 14) { + if(x > 0 && x < canvas.canvasSize.width && y > 0 && y < canvas.canvasSize.height) { + text.toString().split("\n").forEach(function(txt, i){ + ctx.fillText(txt, x, y+(lineHeight*i)) + }) + } + } + + // Method to calculate multiline string dimensions + function measureText(ctx, text, lineHeight=14) { + var theight = 0 + var twidth = 0 + text.split("\n").forEach(function(txt, i){ + theight += lineHeight + if(ctx.measureText(txt).width > twidth) twidth = ctx.measureText(txt).width + }) + return {'width': twidth, 'height': theight} + } + + // Converts x coordinate to it's relative position on the canvas. + function x2px(x) { + var logxmin = Math.log(canvas.xmin) + return (Math.log(x)-logxmin)*canvas.xzoom + } + // Converts y coordinate to it's relative position on the canvas. + // Y is NOT ln based. + function y2px(y) { + return (canvas.ymax-y)*canvas.yzoom + } + // Reverse functions + function px2x(px) { + return Math.exp(px/canvas.xzoom+Math.log(canvas.xmin)) + } + function px2y(px) { + return -(px/canvas.yzoom-canvas.ymax) + } + // Checks whether a point is visible or not. + function visible(x, y) { + return (x2px(x) >= 0 && x2px(x) <= canvas.canvasSize.width) && (y2px(y) >= 0 && y2px(y) <= canvas.canvasSize.height) + } + // Draws a line from a (x1, y1) to (x2, y2) + function drawLine(ctx, x1, y1, x2, y2) { + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); + } + + +} diff --git a/qml/ObjectLists.qml b/qml/ObjectLists.qml new file mode 100644 index 0000000..016888d --- /dev/null +++ b/qml/ObjectLists.qml @@ -0,0 +1,290 @@ +/** + * Logarithm Graph Creator - Create graphs with logarithm scales. + * Copyright (C) 2020 Ad5001 + * + * 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 . + */ + +import QtQuick 2.12 +import QtQuick.Dialogs 1.3 as D +import QtQuick.Controls 2.12 +import "js/objects.js" as Objects +import "js/mathlib.js" as MathLib + + +ListView { + id: objectListList + + signal changed() + + property var listViews: {'':''} // Needs to be initialized or will be undefined -_- + + model: Object.keys(Objects.drawableTypes) + implicitHeight: contentItem.childrenRect.height + + delegate: ListView { + id: objTypeList + property string objType: objectListList.model[index] + model: Objects.currentObjects[objType] + width: objectListList.width + implicitHeight: contentItem.childrenRect.height + visible: model.length > 0 + + Component.onCompleted: objectListList.listViews[objType] = objTypeList // Listing in order to be refreshed + + header: Text { + verticalAlignment: TextInput.AlignVCenter + color: sysPalette.windowText + text: objectListList.model[index] + "s:" + font.pixelSize: 20 + } + + delegate: Item { + id: controlRow + property var obj: Objects.currentObjects[objType][index] + height: 40 + width: objTypeList.width + + CheckBox { + id: visibilityCheckBox + checked: Objects.currentObjects[objType][index].visible + onClicked: { + Objects.currentObjects[objType][index].visible = !Objects.currentObjects[objType][index].visible + objectListList.changed() + controlRow.obj = Objects.currentObjects[objType][index] + } + + ToolTip.visible: hovered + ToolTip.text: checked ? `Hide ${objType} ${obj.name}` : `Show ${objType} ${obj.name}` + } + + Text { + id: objDescription + anchors.left: visibilityCheckBox.right + height: parent.height + verticalAlignment: TextInput.AlignVCenter + text: obj.getReadableString() + font.pixelSize: 16 + color: sysPalette.windowText + + MouseArea { + anchors.fill: parent + onClicked: { + console.log('Showing', objType, index, Objects.currentObjects[objType]) + objEditor.obj = Objects.currentObjects[objType][index] + objEditor.objType = objType + objEditor.objIndex = index + objEditor.editingRow = controlRow + objEditor.open() + } + } + } + + Rectangle { + anchors.right: parent.right + anchors.rightMargin: 5 + color: obj.color + width: parent.height - 10 + height: width + radius: Math.min(width, height) + border.width: 2 + border.color: sysPalette.windowText + + MouseArea { + anchors.fill: parent + onClicked: pickColor.open() + } + } + + D.ColorDialog { + id: pickColor + color: obj.color + title: `Pick new color for ${objType} ${obj.name}` + onAccepted: { + Objects.currentObjects[objType][index].color = color + objectListList.changed() + controlRow.obj = Objects.currentObjects[objType][index] + } + } + } + } + + // Object editor + D.Dialog { + id: objEditor + property string objType: 'Point' + property int objIndex: 0 + property var editingRow: QtObject{} + property var obj: Objects.currentObjects[objType][objIndex] + title: `Logarithmic Graph Creator` + width: 300 + height: 400 + + Text { + id: dlgTitle + anchors.left: parent.left + anchors.top: parent.top + verticalAlignment: TextInput.AlignVCenter + text: `Edit properties of ${objEditor.objType} ${objEditor.obj.name}` + font.pixelSize: 20 + color: sysPalette.windowText + } + + Column { + id: dlgProperties + anchors.top: dlgTitle.bottom + width: objEditor.width - 40 + //height: 30*Math.max(1, Math.ceil(7 / columns)) + //columns: Math.floor(width / settingWidth) + spacing: 10 + + TextSetting { + id: nameProperty + height: 30 + label: "Name" + min: 1 + width: dlgProperties.width + defValue: objEditor.obj.name + onChanged: function(newValue) { + Objects.currentObjects[objEditor.objType][objEditor.objIndex].name = newValue + // TODO Resolve dependencies + objEditor.obj = Objects.currentObjects[objEditor.objType][objEditor.objIndex] + objEditor.editingRow.obj = Objects.currentObjects[objEditor.objType][objEditor.objIndex] + objectListList.changed() + } + } + + ComboBoxSetting { + id: labelContentProperty + height: 30 + width: dlgProperties.width + label: "Label content" + model: ["null", "name", "name + value"] + currentIndex: model.indexOf(objEditor.obj.labelContent) + onActivated: function(newIndex) { + Objects.currentObjects[objEditor.objType][objEditor.objIndex].labelContent = model[newIndex] + objEditor.obj = Objects.currentObjects[objEditor.objType][objEditor.objIndex] + objEditor.editingRow.obj = Objects.currentObjects[objEditor.objType][objEditor.objIndex] + objectListList.changed() + } + } + + // Dynamic properties + Repeater { + property var objProps: Objects.drawableTypes[objEditor.objType].properties() + model: Array.from(Object.keys(objProps), prop => [prop, objProps[prop]]) // Converted to 2-dimentional array. + + Item { + height: 30 + width: dlgProperties.width + property string label: modelData[0].charAt(0).toUpperCase() + modelData[0].slice(1).replace(/([A-Z])/g," $1"); + + TextSetting { + id: customPropText + height: 30 + width: parent.width + label: parent.label + min: 1 + isDouble: modelData[1] == 'number' + visible: ['Expression', 'Domain', 'string', 'number'].indexOf(modelData[1]) >= 0 + defValue: visible ? { + 'Expression': function(){return objEditor.obj[modelData[0]].toEditableString()}, + 'Domain': function(){return objEditor.obj[modelData[0]].toString()}, + 'string': function(){return objEditor.obj[modelData[0]]}, + 'number': function(){return objEditor.obj[modelData[0]]} + }[modelData[1]]() : "" + onChanged: function(newValue) { + Objects.currentObjects[objEditor.objType][objEditor.objIndex][modelData[0]] = { + 'Expression': function(){return new MathLib.Expression(newValue)}, + 'Domain': function(){return MathLib.parseDomain(newValue)}, + 'string': function(){return newValue}, + 'number': function(){return parseFloat(newValue)} + }[modelData[1]]() + // TODO Resolve dependencies + objEditor.obj = Objects.currentObjects[objEditor.objType][objEditor.objIndex] + objEditor.editingRow.obj = Objects.currentObjects[objEditor.objType][objEditor.objIndex] + objectListList.changed() + } + Component.onCompleted: { + //console.log(modelData[0], objEditor.obj[modelData[0]],modelData[1], defValue) + } + } + + ComboBoxSetting { + id: customPropCombo + height: 30 + width: dlgProperties.width + label: parent.label + model: visible ? modelData[1] : [] + visible: Array.isArray(modelData[1]) + currentIndex: model.indexOf(objEditor.obj[modelData[0]]) + + onActivated: function(newIndex) { + // Setting object property. + Objects.currentObjects[objEditor.objType][objEditor.objIndex][modelData[0]] = model[newIndex] + // Refreshing + objEditor.obj = Objects.currentObjects[objEditor.objType][objEditor.objIndex] + objEditor.editingRow.obj = Objects.currentObjects[objEditor.objType][objEditor.objIndex] + objectListList.changed() + } + } + } + } + } + } + + footer: Column { + id: createRow + width: parent.width + + Text { + id: createTitle + verticalAlignment: TextInput.AlignVCenter + text: '+ Create new:' + font.pixelSize: 20 + color: sysPalette.windowText + } + + Repeater { + model: Object.keys(Objects.drawableTypes) + + Button { + id: createBtn + text: modelData + width: createRow.width + flat: false + + contentItem: Text { + + text: createBtn.text + font.pixelSize: 20 + opacity: enabled ? 1.0 : 0.3 + color: sysPalette.windowText + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + } + + onClicked: { + var newobj = new Objects.drawableTypes[modelData]() + if(Object.keys(Objects.currentObjects).indexOf(modelData) == -1) + Objects.currentObjects[modelData] = [] + Objects.currentObjects[modelData].push(newobj) + objectListList.changed() + console.log(objectListList, objectListList.listViews) + objectListList.listViews[modelData].model = Objects.currentObjects[modelData] + } + } + } + } +} diff --git a/qml/Settings.qml b/qml/Settings.qml new file mode 100644 index 0000000..7a8bc0a --- /dev/null +++ b/qml/Settings.qml @@ -0,0 +1,209 @@ +/** + * Logarithm Graph Creator - Create graphs with logarithm scales. + * Copyright (C) 2020 Ad5001 + * + * 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 . + */ + +import QtQuick.Controls 2.12 +import QtQuick 2.12 + +Grid { + id: root + height: 30*Math.max(1, Math.ceil(7 / columns)) + columns: Math.floor(width / settingWidth) + spacing: 10 + + signal changed() + signal copyToClipboard() + signal saveDiagram(string filename) + signal loadDiagram(string filename) + + property int settingWidth: 135 + + property int xzoom: 100 + property int yzoom: 10 + property double xmin: 5/10 + property double ymax: 25 + property int yaxisstep: 4 + property string xaxislabel: "ω (rad/s)" + property string yaxislabel: "Gain G (dB)" + property string saveFilename: "" + + FileDialog { + id: fdiag + onAccepted: { + var filePath = fileUrl.toString().substr(7) + root.saveFilename = filePath + console.log(filePath) + if(exportMode) { + root.saveDiagram(filePath) + } else { + root.loadDiagram(filePath) + } + } + } + + // Line 1 + // Zoom + TextSetting { + id: zoomX + height: 30 + isInt: true + label: "X Zoom" + min: 1 + width: root.settingWidth + defValue: root.xzoom + onChanged: function(newValue) { + root.xzoom = newValue + root.changed() + } + } + TextSetting { + id: zoomY + height: 30 + isInt: true + label: "Y Zoom" + width: root.settingWidth + defValue: root.yzoom + onChanged: function(newValue) { + root.yzoom = newValue + root.changed() + } + } + // Positioning the graph + TextSetting { + id: minX + height: 30 + isDouble: true + min: 0 + label: "Min X" + width: root.settingWidth + defValue: root.xmin + onChanged: function(newValue) { + root.xmin = newValue + root.changed() + } + } + TextSetting { + id: maxY + height: 30 + isDouble: true + label: "Max Y" + width: root.settingWidth + defValue: root.ymax + onChanged: function(newValue) { + root.ymax = newValue + root.changed() + } + } + TextSetting { + id: yAxisStep + height: 30 + isInt: true + label: "Y Axis Step" + width: root.settingWidth + defValue: root.yaxisstep + onChanged: function(newValue) { + root.yaxisstep = newValue + root.changed() + } + } + + Button { + id: copyToClipboard + height: 30 + width: root.settingWidth + text: "Copy to clipboard" + icon.name: 'editcopy' + onClicked: root.copyToClipboard() + } + + TextSetting { + id: xAxisLabel + height: 30 + label: "X Label" + width: root.settingWidth + defValue: root.xaxislabel + onChanged: function(newValue) { + root.xaxislabel = newValue + root.changed() + } + } + + TextSetting { + id: yAxisLabel + height: 30 + label: "Y Label" + width: root.settingWidth + defValue: root.yaxislabel + onChanged: function(newValue) { + root.yaxislabel = newValue + root.changed() + } + } + + Button { + id: saveDiagram + height: 30 + width: root.settingWidth + text: "Save diagram" + icon.name: 'filesave' + onClicked: save() + } + + Button { + id: saveDiagramAs + height: 30 + width: root.settingWidth + text: "Save diagram as" + icon.name: 'filesaveas' + onClicked: saveAs() + } + + Button { + id: loadDiagram + height: 30 + width: root.settingWidth + text: "Load diagram" + icon.name: 'fileopen' + onClicked: load() + } + CheckBox { + id: modePhaseCheck + height: 30 + width: root.settingWidth + text: "Mode phase" + property var refresh: checked ? root.changed() : root.changed() + } + + function save() { + if(root.saveFilename == "") { + fdiag.exportMode = true + fdiag.open() + } else { + root.saveDiagram(root.saveFilename) + } + } + + function saveAs() { + fdiag.exportMode = true + fdiag.open() + } + + function load() { + fdiag.exportMode = false + fdiag.open() + } +} diff --git a/qml/TextSetting.qml b/qml/TextSetting.qml new file mode 100644 index 0000000..d215a74 --- /dev/null +++ b/qml/TextSetting.qml @@ -0,0 +1,78 @@ +/** + * Logarithm Graph Creator - Create graphs with logarithm scales. + * Copyright (C) 2020 Ad5001 + * + * 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 . + */ + +import QtQuick.Controls 2.12 +import QtQuick 2.12 + +Item { + id: control + height: 30 + + signal changed(string newValue) + + property bool isInt: false + property bool isDouble: false + property double min: 1 + property string label + property string defValue + + Item { + anchors.centerIn: parent + width: labelItem.width + input.width + height: Math.max(labelItem.height, input.height) + + Text { + id: labelItem + height: 30 + anchors.top: parent.top + verticalAlignment: TextInput.AlignVCenter + color: sysPalette.windowText + text: " "+ control.label +": " + } + + + TextInput { + id: input + anchors.top: parent.top + anchors.left: labelItem.right + anchors.leftMargin: 5 + width: control.width - labelItem.width + height: 30 + verticalAlignment: TextInput.AlignVCenter + horizontalAlignment: TextInput.AlignHCenter + color: sysPalette.windowText + focus: true + text: control.defValue + selectByMouse: true + onEditingFinished: { + var value = text + if(control.isInt) value = Math.max(control.min,parseInt(value).toString()=="NaN"?control.min:parseInt(value)) + if(control.isDouble) value = Math.max(control.min,parseFloat(value).toString()=="NaN"?control.min:parseFloat(value)) + if(value != "") control.changed(value) + } + } + + Rectangle { + color: sysPalette.windowText + anchors.left: input.left + anchors.right: input.right + anchors.bottom: input.bottom + height: 2 + } + } +} diff --git a/qml/js/expr-eval.js b/qml/js/expr-eval.js new file mode 100644 index 0000000..5e602cd --- /dev/null +++ b/qml/js/expr-eval.js @@ -0,0 +1,1837 @@ +// https://silentmatt.com/javascript-expression-evaluator/ + +var INUMBER = 'INUMBER'; +var IOP1 = 'IOP1'; +var IOP2 = 'IOP2'; +var IOP3 = 'IOP3'; +var IVAR = 'IVAR'; +var IVARNAME = 'IVARNAME'; +var IFUNCALL = 'IFUNCALL'; +var IFUNDEF = 'IFUNDEF'; +var IEXPR = 'IEXPR'; +var IEXPREVAL = 'IEXPREVAL'; +var IMEMBER = 'IMEMBER'; +var IENDSTATEMENT = 'IENDSTATEMENT'; +var IARRAY = 'IARRAY'; + +function Instruction(type, value) { + this.type = type; + this.value = (value !== undefined && value !== null) ? value : 0; +} + +Instruction.prototype.toString = function () { + switch (this.type) { + case INUMBER: + case IOP1: + case IOP2: + case IOP3: + case IVAR: + case IVARNAME: + case IENDSTATEMENT: + return this.value; + case IFUNCALL: + return 'CALL ' + this.value; + case IFUNDEF: + return 'DEF ' + this.value; + case IARRAY: + return 'ARRAY ' + this.value; + case IMEMBER: + return '.' + this.value; + default: + return 'Invalid Instruction'; + } +}; + +function unaryInstruction(value) { + return new Instruction(IOP1, value); +} + +function binaryInstruction(value) { + return new Instruction(IOP2, value); +} + +function ternaryInstruction(value) { + return new Instruction(IOP3, value); +} + +function simplify(tokens, unaryOps, binaryOps, ternaryOps, values) { + var nstack = []; + var newexpression = []; + var n1, n2, n3; + var f; + for (var i = 0; i < tokens.length; i++) { + var item = tokens[i]; + var type = item.type; + if (type === INUMBER || type === IVARNAME) { + if (Array.isArray(item.value)) { + nstack.push.apply(nstack, simplify(item.value.map(function (x) { + return new Instruction(INUMBER, x); + }).concat(new Instruction(IARRAY, item.value.length)), unaryOps, binaryOps, ternaryOps, values)); + } else { + nstack.push(item); + } + } else if (type === IVAR && values.hasOwnProperty(item.value)) { + item = new Instruction(INUMBER, values[item.value]); + nstack.push(item); + } else if (type === IOP2 && nstack.length > 1) { + n2 = nstack.pop(); + n1 = nstack.pop(); + f = binaryOps[item.value]; + item = new Instruction(INUMBER, f(n1.value, n2.value)); + nstack.push(item); + } else if (type === IOP3 && nstack.length > 2) { + n3 = nstack.pop(); + n2 = nstack.pop(); + n1 = nstack.pop(); + if (item.value === '?') { + nstack.push(n1.value ? n2.value : n3.value); + } else { + f = ternaryOps[item.value]; + item = new Instruction(INUMBER, f(n1.value, n2.value, n3.value)); + nstack.push(item); + } + } else if (type === IOP1 && nstack.length > 0) { + n1 = nstack.pop(); + f = unaryOps[item.value]; + item = new Instruction(INUMBER, f(n1.value)); + nstack.push(item); + } else if (type === IEXPR) { + while (nstack.length > 0) { + newexpression.push(nstack.shift()); + } + newexpression.push(new Instruction(IEXPR, simplify(item.value, unaryOps, binaryOps, ternaryOps, values))); + } else if (type === IMEMBER && nstack.length > 0) { + n1 = nstack.pop(); + nstack.push(new Instruction(INUMBER, n1.value[item.value])); + } /* else if (type === IARRAY && nstack.length >= item.value) { + var length = item.value; + while (length-- > 0) { + newexpression.push(nstack.pop()); + } + newexpression.push(new Instruction(IARRAY, item.value)); + } */ else { + while (nstack.length > 0) { + newexpression.push(nstack.shift()); + } + newexpression.push(item); + } + } + while (nstack.length > 0) { + newexpression.push(nstack.shift()); + } + return newexpression; +} + +function substitute(tokens, variable, expr) { + var newexpression = []; + for (var i = 0; i < tokens.length; i++) { + var item = tokens[i]; + var type = item.type; + if (type === IVAR && item.value === variable) { + for (var j = 0; j < expr.tokens.length; j++) { + var expritem = expr.tokens[j]; + var replitem; + if (expritem.type === IOP1) { + replitem = unaryInstruction(expritem.value); + } else if (expritem.type === IOP2) { + replitem = binaryInstruction(expritem.value); + } else if (expritem.type === IOP3) { + replitem = ternaryInstruction(expritem.value); + } else { + replitem = new Instruction(expritem.type, expritem.value); + } + newexpression.push(replitem); + } + } else if (type === IEXPR) { + newexpression.push(new Instruction(IEXPR, substitute(item.value, variable, expr))); + } else { + newexpression.push(item); + } + } + return newexpression; +} + +function evaluate(tokens, expr, values) { + var nstack = []; + var n1, n2, n3; + var f, args, argCount; + + if (isExpressionEvaluator(tokens)) { + return resolveExpression(tokens, values); + } + + var numTokens = tokens.length; + + for (var i = 0; i < numTokens; i++) { + var item = tokens[i]; + var type = item.type; + if (type === INUMBER || type === IVARNAME) { + nstack.push(item.value); + } else if (type === IOP2) { + n2 = nstack.pop(); + n1 = nstack.pop(); + if (item.value === 'and') { + nstack.push(n1 ? !!evaluate(n2, expr, values) : false); + } else if (item.value === 'or') { + nstack.push(n1 ? true : !!evaluate(n2, expr, values)); + } else if (item.value === '=') { + f = expr.binaryOps[item.value]; + nstack.push(f(n1, evaluate(n2, expr, values), values)); + } else { + f = expr.binaryOps[item.value]; + nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values))); + } + } else if (type === IOP3) { + n3 = nstack.pop(); + n2 = nstack.pop(); + n1 = nstack.pop(); + if (item.value === '?') { + nstack.push(evaluate(n1 ? n2 : n3, expr, values)); + } else { + f = expr.ternaryOps[item.value]; + nstack.push(f(resolveExpression(n1, values), resolveExpression(n2, values), resolveExpression(n3, values))); + } + } else if (type === IVAR) { + if (item.value in expr.functions) { + nstack.push(expr.functions[item.value]); + } else if (item.value in expr.unaryOps && expr.parser.isOperatorEnabled(item.value)) { + nstack.push(expr.unaryOps[item.value]); + } else { + var v = values[item.value]; + if (v !== undefined) { + nstack.push(v); + } else { + throw new Error('undefined variable: ' + item.value); + } + } + } else if (type === IOP1) { + n1 = nstack.pop(); + f = expr.unaryOps[item.value]; + nstack.push(f(resolveExpression(n1, values))); + } else if (type === IFUNCALL) { + argCount = item.value; + args = []; + while (argCount-- > 0) { + args.unshift(resolveExpression(nstack.pop(), values)); + } + f = nstack.pop(); + if (f.apply && f.call) { + nstack.push(f.apply(undefined, args)); + } else { + throw new Error(f + ' is not a function'); + } + } else if (type === IFUNDEF) { + // Create closure to keep references to arguments and expression + nstack.push((function () { + var n2 = nstack.pop(); + var args = []; + var argCount = item.value; + while (argCount-- > 0) { + args.unshift(nstack.pop()); + } + var n1 = nstack.pop(); + var f = function () { + var scope = Object.assign({}, values); + for (var i = 0, len = args.length; i < len; i++) { + scope[args[i]] = arguments[i]; + } + return evaluate(n2, expr, scope); + }; + // f.name = n1 + Object.defineProperty(f, 'name', { + value: n1, + writable: false + }); + values[n1] = f; + return f; + })()); + } else if (type === IEXPR) { + nstack.push(createExpressionEvaluator(item, expr)); + } else if (type === IEXPREVAL) { + nstack.push(item); + } else if (type === IMEMBER) { + n1 = nstack.pop(); + nstack.push(n1[item.value]); + } else if (type === IENDSTATEMENT) { + nstack.pop(); + } else if (type === IARRAY) { + argCount = item.value; + args = []; + while (argCount-- > 0) { + args.unshift(nstack.pop()); + } + nstack.push(args); + } else { + throw new Error('invalid Expression'); + } + } + if (nstack.length > 1) { + throw new Error('invalid Expression (parity)'); + } + // Explicitly return zero to avoid test issues caused by -0 + return nstack[0] === 0 ? 0 : resolveExpression(nstack[0], values); +} + +function createExpressionEvaluator(token, expr, values) { + if (isExpressionEvaluator(token)) return token; + return { + type: IEXPREVAL, + value: function (scope) { + return evaluate(token.value, expr, scope); + } + }; +} + +function isExpressionEvaluator(n) { + return n && n.type === IEXPREVAL; +} + +function resolveExpression(n, values) { + return isExpressionEvaluator(n) ? n.value(values) : n; +} + +function expressionToString(tokens, toJS) { + var nstack = []; + var n1, n2, n3; + var f, args, argCount; + for (var i = 0; i < tokens.length; i++) { + var item = tokens[i]; + var type = item.type; + if (type === INUMBER) { + if (typeof item.value === 'number' && item.value < 0) { + nstack.push('(' + item.value + ')'); + } else if (Array.isArray(item.value)) { + nstack.push('[' + item.value.map(escapeValue).join(', ') + ']'); + } else { + nstack.push(escapeValue(item.value)); + } + } else if (type === IOP2) { + n2 = nstack.pop(); + n1 = nstack.pop(); + f = item.value; + if (toJS) { + if (f === '^') { + nstack.push('Math.pow(' + n1 + ', ' + n2 + ')'); + } else if (f === 'and') { + nstack.push('(!!' + n1 + ' && !!' + n2 + ')'); + } else if (f === 'or') { + nstack.push('(!!' + n1 + ' || !!' + n2 + ')'); + } else if (f === '||') { + nstack.push('(function(a,b){ return Array.isArray(a) && Array.isArray(b) ? a.concat(b) : String(a) + String(b); }((' + n1 + '),(' + n2 + ')))'); + } else if (f === '==') { + nstack.push('(' + n1 + ' === ' + n2 + ')'); + } else if (f === '!=') { + nstack.push('(' + n1 + ' !== ' + n2 + ')'); + } else if (f === '[') { + nstack.push(n1 + '[(' + n2 + ') | 0]'); + } else { + nstack.push('(' + n1 + ' ' + f + ' ' + n2 + ')'); + } + } else { + if (f === '[') { + nstack.push(n1 + '[' + n2 + ']'); + } else { + nstack.push('(' + n1 + ' ' + f + ' ' + n2 + ')'); + } + } + } else if (type === IOP3) { + n3 = nstack.pop(); + n2 = nstack.pop(); + n1 = nstack.pop(); + f = item.value; + if (f === '?') { + nstack.push('(' + n1 + ' ? ' + n2 + ' : ' + n3 + ')'); + } else { + throw new Error('invalid Expression'); + } + } else if (type === IVAR || type === IVARNAME) { + nstack.push(item.value); + } else if (type === IOP1) { + n1 = nstack.pop(); + f = item.value; + if (f === '-' || f === '+') { + nstack.push('(' + f + n1 + ')'); + } else if (toJS) { + if (f === 'not') { + nstack.push('(' + '!' + n1 + ')'); + } else if (f === '!') { + nstack.push('fac(' + n1 + ')'); + } else { + nstack.push(f + '(' + n1 + ')'); + } + } else if (f === '!') { + nstack.push('(' + n1 + '!)'); + } else { + nstack.push('(' + f + ' ' + n1 + ')'); + } + } else if (type === IFUNCALL) { + argCount = item.value; + args = []; + while (argCount-- > 0) { + args.unshift(nstack.pop()); + } + f = nstack.pop(); + nstack.push(f + '(' + args.join(', ') + ')'); + } else if (type === IFUNDEF) { + n2 = nstack.pop(); + argCount = item.value; + args = []; + while (argCount-- > 0) { + args.unshift(nstack.pop()); + } + n1 = nstack.pop(); + if (toJS) { + nstack.push('(' + n1 + ' = function(' + args.join(', ') + ') { return ' + n2 + ' })'); + } else { + nstack.push('(' + n1 + '(' + args.join(', ') + ') = ' + n2 + ')'); + } + } else if (type === IMEMBER) { + n1 = nstack.pop(); + nstack.push(n1 + '.' + item.value); + } else if (type === IARRAY) { + argCount = item.value; + args = []; + while (argCount-- > 0) { + args.unshift(nstack.pop()); + } + nstack.push('[' + args.join(', ') + ']'); + } else if (type === IEXPR) { + nstack.push('(' + expressionToString(item.value, toJS) + ')'); + } else if (type === IENDSTATEMENT) ; else { + throw new Error('invalid Expression'); + } + } + if (nstack.length > 1) { + if (toJS) { + nstack = [ nstack.join(',') ]; + } else { + nstack = [ nstack.join(';') ]; + } + } + return String(nstack[0]); +} + +function escapeValue(v) { + if (typeof v === 'string') { + return JSON.stringify(v).replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029'); + } + return v; +} + +function contains(array, obj) { + for (var i = 0; i < array.length; i++) { + if (array[i] === obj) { + return true; + } + } + return false; +} + +function getSymbols(tokens, symbols, options) { + options = options || {}; + var withMembers = !!options.withMembers; + var prevVar = null; + + for (var i = 0; i < tokens.length; i++) { + var item = tokens[i]; + if (item.type === IVAR || item.type === IVARNAME) { + if (!withMembers && !contains(symbols, item.value)) { + symbols.push(item.value); + } else if (prevVar !== null) { + if (!contains(symbols, prevVar)) { + symbols.push(prevVar); + } + prevVar = item.value; + } else { + prevVar = item.value; + } + } else if (item.type === IMEMBER && withMembers && prevVar !== null) { + prevVar += '.' + item.value; + } else if (item.type === IEXPR) { + getSymbols(item.value, symbols, options); + } else if (prevVar !== null) { + if (!contains(symbols, prevVar)) { + symbols.push(prevVar); + } + prevVar = null; + } + } + + if (prevVar !== null && !contains(symbols, prevVar)) { + symbols.push(prevVar); + } +} + +function Expression(tokens, parser) { + this.tokens = tokens; + this.parser = parser; + this.unaryOps = parser.unaryOps; + this.binaryOps = parser.binaryOps; + this.ternaryOps = parser.ternaryOps; + this.functions = parser.functions; +} + +Expression.prototype.simplify = function (values) { + values = values || {}; + return new Expression(simplify(this.tokens, this.unaryOps, this.binaryOps, this.ternaryOps, values), this.parser); +}; + +Expression.prototype.substitute = function (variable, expr) { + if (!(expr instanceof Expression)) { + expr = this.parser.parse(String(expr)); + } + + return new Expression(substitute(this.tokens, variable, expr), this.parser); +}; + +Expression.prototype.evaluate = function (values) { + values = values || {}; + return evaluate(this.tokens, this, values); +}; + +Expression.prototype.toString = function () { + return expressionToString(this.tokens, false); +}; + +Expression.prototype.symbols = function (options) { + options = options || {}; + var vars = []; + getSymbols(this.tokens, vars, options); + return vars; +}; + +Expression.prototype.variables = function (options) { + options = options || {}; + var vars = []; + getSymbols(this.tokens, vars, options); + var functions = this.functions; + return vars.filter(function (name) { + return !(name in functions); + }); +}; + +Expression.prototype.toJSFunction = function (param, variables) { + var expr = this; + var f = new Function(param, 'with(this.functions) with (this.ternaryOps) with (this.binaryOps) with (this.unaryOps) { return ' + expressionToString(this.simplify(variables).tokens, true) + '; }'); // eslint-disable-line no-new-func + return function () { + return f.apply(expr, arguments); + }; +}; + +var TEOF = 'TEOF'; +var TOP = 'TOP'; +var TNUMBER = 'TNUMBER'; +var TSTRING = 'TSTRING'; +var TPAREN = 'TPAREN'; +var TBRACKET = 'TBRACKET'; +var TCOMMA = 'TCOMMA'; +var TNAME = 'TNAME'; +var TSEMICOLON = 'TSEMICOLON'; + +function Token(type, value, index) { + this.type = type; + this.value = value; + this.index = index; +} + +Token.prototype.toString = function () { + return this.type + ': ' + this.value; +}; + +function TokenStream(parser, expression) { + this.pos = 0; + this.current = null; + this.unaryOps = parser.unaryOps; + this.binaryOps = parser.binaryOps; + this.ternaryOps = parser.ternaryOps; + this.consts = parser.consts; + this.expression = expression; + this.savedPosition = 0; + this.savedCurrent = null; + this.options = parser.options; + this.parser = parser; +} + +TokenStream.prototype.newToken = function (type, value, pos) { + return new Token(type, value, pos != null ? pos : this.pos); +}; + +TokenStream.prototype.save = function () { + this.savedPosition = this.pos; + this.savedCurrent = this.current; +}; + +TokenStream.prototype.restore = function () { + this.pos = this.savedPosition; + this.current = this.savedCurrent; +}; + +TokenStream.prototype.next = function () { + if (this.pos >= this.expression.length) { + return this.newToken(TEOF, 'EOF'); + } + + if (this.isWhitespace() || this.isComment()) { + return this.next(); + } else if (this.isRadixInteger() || + this.isNumber() || + this.isOperator() || + this.isString() || + this.isParen() || + this.isBracket() || + this.isComma() || + this.isSemicolon() || + this.isNamedOp() || + this.isConst() || + this.isName()) { + return this.current; + } else { + this.parseError('Unknown character "' + this.expression.charAt(this.pos) + '"'); + } +}; + +TokenStream.prototype.isString = function () { + var r = false; + var startPos = this.pos; + var quote = this.expression.charAt(startPos); + + if (quote === '\'' || quote === '"') { + var index = this.expression.indexOf(quote, startPos + 1); + while (index >= 0 && this.pos < this.expression.length) { + this.pos = index + 1; + if (this.expression.charAt(index - 1) !== '\\') { + var rawString = this.expression.substring(startPos + 1, index); + this.current = this.newToken(TSTRING, this.unescape(rawString), startPos); + r = true; + break; + } + index = this.expression.indexOf(quote, index + 1); + } + } + return r; +}; + +TokenStream.prototype.isParen = function () { + var c = this.expression.charAt(this.pos); + if (c === '(' || c === ')') { + this.current = this.newToken(TPAREN, c); + this.pos++; + return true; + } + return false; +}; + +TokenStream.prototype.isBracket = function () { + var c = this.expression.charAt(this.pos); + if ((c === '[' || c === ']') && this.isOperatorEnabled('[')) { + this.current = this.newToken(TBRACKET, c); + this.pos++; + return true; + } + return false; +}; + +TokenStream.prototype.isComma = function () { + var c = this.expression.charAt(this.pos); + if (c === ',') { + this.current = this.newToken(TCOMMA, ','); + this.pos++; + return true; + } + return false; +}; + +TokenStream.prototype.isSemicolon = function () { + var c = this.expression.charAt(this.pos); + if (c === ';') { + this.current = this.newToken(TSEMICOLON, ';'); + this.pos++; + return true; + } + return false; +}; + +TokenStream.prototype.isConst = function () { + var startPos = this.pos; + var i = startPos; + for (; i < this.expression.length; i++) { + var c = this.expression.charAt(i); + if (c.toUpperCase() === c.toLowerCase()) { + if (i === this.pos || (c !== '_' && c !== '.' && (c < '0' || c > '9'))) { + break; + } + } + } + if (i > startPos) { + var str = this.expression.substring(startPos, i); + if (str in this.consts) { + this.current = this.newToken(TNUMBER, this.consts[str]); + this.pos += str.length; + return true; + } + } + return false; +}; + +TokenStream.prototype.isNamedOp = function () { + var startPos = this.pos; + var i = startPos; + for (; i < this.expression.length; i++) { + var c = this.expression.charAt(i); + if (c.toUpperCase() === c.toLowerCase()) { + if (i === this.pos || (c !== '_' && (c < '0' || c > '9'))) { + break; + } + } + } + if (i > startPos) { + var str = this.expression.substring(startPos, i); + if (this.isOperatorEnabled(str) && (str in this.binaryOps || str in this.unaryOps || str in this.ternaryOps)) { + this.current = this.newToken(TOP, str); + this.pos += str.length; + return true; + } + } + return false; +}; + +TokenStream.prototype.isName = function () { + var startPos = this.pos; + var i = startPos; + var hasLetter = false; + for (; i < this.expression.length; i++) { + var c = this.expression.charAt(i); + if (c.toUpperCase() === c.toLowerCase()) { + if (i === this.pos && (c === '$' || c === '_')) { + if (c === '_') { + hasLetter = true; + } + continue; + } else if (i === this.pos || !hasLetter || (c !== '_' && (c < '0' || c > '9'))) { + break; + } + } else { + hasLetter = true; + } + } + if (hasLetter) { + var str = this.expression.substring(startPos, i); + this.current = this.newToken(TNAME, str); + this.pos += str.length; + return true; + } + return false; +}; + +TokenStream.prototype.isWhitespace = function () { + var r = false; + var c = this.expression.charAt(this.pos); + while (c === ' ' || c === '\t' || c === '\n' || c === '\r') { + r = true; + this.pos++; + if (this.pos >= this.expression.length) { + break; + } + c = this.expression.charAt(this.pos); + } + return r; +}; + +var codePointPattern = /^[0-9a-f]{4}$/i; + +TokenStream.prototype.unescape = function (v) { + var index = v.indexOf('\\'); + if (index < 0) { + return v; + } + + var buffer = v.substring(0, index); + while (index >= 0) { + var c = v.charAt(++index); + switch (c) { + case '\'': + buffer += '\''; + break; + case '"': + buffer += '"'; + break; + case '\\': + buffer += '\\'; + break; + case '/': + buffer += '/'; + break; + case 'b': + buffer += '\b'; + break; + case 'f': + buffer += '\f'; + break; + case 'n': + buffer += '\n'; + break; + case 'r': + buffer += '\r'; + break; + case 't': + buffer += '\t'; + break; + case 'u': + // interpret the following 4 characters as the hex of the unicode code point + var codePoint = v.substring(index + 1, index + 5); + if (!codePointPattern.test(codePoint)) { + this.parseError('Illegal escape sequence: \\u' + codePoint); + } + buffer += String.fromCharCode(parseInt(codePoint, 16)); + index += 4; + break; + default: + throw this.parseError('Illegal escape sequence: "\\' + c + '"'); + } + ++index; + var backslash = v.indexOf('\\', index); + buffer += v.substring(index, backslash < 0 ? v.length : backslash); + index = backslash; + } + + return buffer; +}; + +TokenStream.prototype.isComment = function () { + var c = this.expression.charAt(this.pos); + if (c === '/' && this.expression.charAt(this.pos + 1) === '*') { + this.pos = this.expression.indexOf('*/', this.pos) + 2; + if (this.pos === 1) { + this.pos = this.expression.length; + } + return true; + } + return false; +}; + +TokenStream.prototype.isRadixInteger = function () { + var pos = this.pos; + + if (pos >= this.expression.length - 2 || this.expression.charAt(pos) !== '0') { + return false; + } + ++pos; + + var radix; + var validDigit; + if (this.expression.charAt(pos) === 'x') { + radix = 16; + validDigit = /^[0-9a-f]$/i; + ++pos; + } else if (this.expression.charAt(pos) === 'b') { + radix = 2; + validDigit = /^[01]$/i; + ++pos; + } else { + return false; + } + + var valid = false; + var startPos = pos; + + while (pos < this.expression.length) { + var c = this.expression.charAt(pos); + if (validDigit.test(c)) { + pos++; + valid = true; + } else { + break; + } + } + + if (valid) { + this.current = this.newToken(TNUMBER, parseInt(this.expression.substring(startPos, pos), radix)); + this.pos = pos; + } + return valid; +}; + +TokenStream.prototype.isNumber = function () { + var valid = false; + var pos = this.pos; + var startPos = pos; + var resetPos = pos; + var foundDot = false; + var foundDigits = false; + var c; + + while (pos < this.expression.length) { + c = this.expression.charAt(pos); + if ((c >= '0' && c <= '9') || (!foundDot && c === '.')) { + if (c === '.') { + foundDot = true; + } else { + foundDigits = true; + } + pos++; + valid = foundDigits; + } else { + break; + } + } + + if (valid) { + resetPos = pos; + } + + if (c === 'e' || c === 'E') { + pos++; + var acceptSign = true; + var validExponent = false; + while (pos < this.expression.length) { + c = this.expression.charAt(pos); + if (acceptSign && (c === '+' || c === '-')) { + acceptSign = false; + } else if (c >= '0' && c <= '9') { + validExponent = true; + acceptSign = false; + } else { + break; + } + pos++; + } + + if (!validExponent) { + pos = resetPos; + } + } + + if (valid) { + this.current = this.newToken(TNUMBER, parseFloat(this.expression.substring(startPos, pos))); + this.pos = pos; + } else { + this.pos = resetPos; + } + return valid; +}; + +TokenStream.prototype.isOperator = function () { + var startPos = this.pos; + var c = this.expression.charAt(this.pos); + + if (c === '+' || c === '-' || c === '*' || c === '/' || c === '%' || c === '^' || c === '?' || c === ':' || c === '.') { + this.current = this.newToken(TOP, c); + } else if (c === '∙' || c === '•') { + this.current = this.newToken(TOP, '*'); + } else if (c === '>') { + if (this.expression.charAt(this.pos + 1) === '=') { + this.current = this.newToken(TOP, '>='); + this.pos++; + } else { + this.current = this.newToken(TOP, '>'); + } + } else if (c === '<') { + if (this.expression.charAt(this.pos + 1) === '=') { + this.current = this.newToken(TOP, '<='); + this.pos++; + } else { + this.current = this.newToken(TOP, '<'); + } + } else if (c === '|') { + if (this.expression.charAt(this.pos + 1) === '|') { + this.current = this.newToken(TOP, '||'); + this.pos++; + } else { + return false; + } + } else if (c === '=') { + if (this.expression.charAt(this.pos + 1) === '=') { + this.current = this.newToken(TOP, '=='); + this.pos++; + } else { + this.current = this.newToken(TOP, c); + } + } else if (c === '!') { + if (this.expression.charAt(this.pos + 1) === '=') { + this.current = this.newToken(TOP, '!='); + this.pos++; + } else { + this.current = this.newToken(TOP, c); + } + } else { + return false; + } + this.pos++; + + if (this.isOperatorEnabled(this.current.value)) { + return true; + } else { + this.pos = startPos; + return false; + } +}; + +TokenStream.prototype.isOperatorEnabled = function (op) { + return this.parser.isOperatorEnabled(op); +}; + +TokenStream.prototype.getCoordinates = function () { + var line = 0; + var column; + var newline = -1; + do { + line++; + column = this.pos - newline; + newline = this.expression.indexOf('\n', newline + 1); + } while (newline >= 0 && newline < this.pos); + + return { + line: line, + column: column + }; +}; + +TokenStream.prototype.parseError = function (msg) { + var coords = this.getCoordinates(); + throw new Error('parse error [' + coords.line + ':' + coords.column + ']: ' + msg); +}; + +function ParserState(parser, tokenStream, options) { + this.parser = parser; + this.tokens = tokenStream; + this.current = null; + this.nextToken = null; + this.next(); + this.savedCurrent = null; + this.savedNextToken = null; + this.allowMemberAccess = options.allowMemberAccess !== false; +} + +ParserState.prototype.next = function () { + this.current = this.nextToken; + return (this.nextToken = this.tokens.next()); +}; + +ParserState.prototype.tokenMatches = function (token, value) { + if (typeof value === 'undefined') { + return true; + } else if (Array.isArray(value)) { + return contains(value, token.value); + } else if (typeof value === 'function') { + return value(token); + } else { + return token.value === value; + } +}; + +ParserState.prototype.save = function () { + this.savedCurrent = this.current; + this.savedNextToken = this.nextToken; + this.tokens.save(); +}; + +ParserState.prototype.restore = function () { + this.tokens.restore(); + this.current = this.savedCurrent; + this.nextToken = this.savedNextToken; +}; + +ParserState.prototype.accept = function (type, value) { + if (this.nextToken.type === type && this.tokenMatches(this.nextToken, value)) { + this.next(); + return true; + } + return false; +}; + +ParserState.prototype.expect = function (type, value) { + if (!this.accept(type, value)) { + var coords = this.tokens.getCoordinates(); + throw new Error('parse error [' + coords.line + ':' + coords.column + ']: Expected ' + (value || type)); + } +}; + +ParserState.prototype.parseAtom = function (instr) { + var unaryOps = this.tokens.unaryOps; + function isPrefixOperator(token) { + return token.value in unaryOps; + } + + if (this.accept(TNAME) || this.accept(TOP, isPrefixOperator)) { + instr.push(new Instruction(IVAR, this.current.value)); + } else if (this.accept(TNUMBER)) { + instr.push(new Instruction(INUMBER, this.current.value)); + } else if (this.accept(TSTRING)) { + instr.push(new Instruction(INUMBER, this.current.value)); + } else if (this.accept(TPAREN, '(')) { + this.parseExpression(instr); + this.expect(TPAREN, ')'); + } else if (this.accept(TBRACKET, '[')) { + if (this.accept(TBRACKET, ']')) { + instr.push(new Instruction(IARRAY, 0)); + } else { + var argCount = this.parseArrayList(instr); + instr.push(new Instruction(IARRAY, argCount)); + } + } else { + throw new Error('unexpected ' + this.nextToken); + } +}; + +ParserState.prototype.parseExpression = function (instr) { + var exprInstr = []; + if (this.parseUntilEndStatement(instr, exprInstr)) { + return; + } + this.parseVariableAssignmentExpression(exprInstr); + if (this.parseUntilEndStatement(instr, exprInstr)) { + return; + } + this.pushExpression(instr, exprInstr); +}; + +ParserState.prototype.pushExpression = function (instr, exprInstr) { + for (var i = 0, len = exprInstr.length; i < len; i++) { + instr.push(exprInstr[i]); + } +}; + +ParserState.prototype.parseUntilEndStatement = function (instr, exprInstr) { + if (!this.accept(TSEMICOLON)) return false; + if (this.nextToken && this.nextToken.type !== TEOF && !(this.nextToken.type === TPAREN && this.nextToken.value === ')')) { + exprInstr.push(new Instruction(IENDSTATEMENT)); + } + if (this.nextToken.type !== TEOF) { + this.parseExpression(exprInstr); + } + instr.push(new Instruction(IEXPR, exprInstr)); + return true; +}; + +ParserState.prototype.parseArrayList = function (instr) { + var argCount = 0; + + while (!this.accept(TBRACKET, ']')) { + this.parseExpression(instr); + ++argCount; + while (this.accept(TCOMMA)) { + this.parseExpression(instr); + ++argCount; + } + } + + return argCount; +}; + +ParserState.prototype.parseVariableAssignmentExpression = function (instr) { + this.parseConditionalExpression(instr); + while (this.accept(TOP, '=')) { + var varName = instr.pop(); + var varValue = []; + var lastInstrIndex = instr.length - 1; + if (varName.type === IFUNCALL) { + if (!this.tokens.isOperatorEnabled('()=')) { + throw new Error('function definition is not permitted'); + } + for (var i = 0, len = varName.value + 1; i < len; i++) { + var index = lastInstrIndex - i; + if (instr[index].type === IVAR) { + instr[index] = new Instruction(IVARNAME, instr[index].value); + } + } + this.parseVariableAssignmentExpression(varValue); + instr.push(new Instruction(IEXPR, varValue)); + instr.push(new Instruction(IFUNDEF, varName.value)); + continue; + } + if (varName.type !== IVAR && varName.type !== IMEMBER) { + throw new Error('expected variable for assignment'); + } + this.parseVariableAssignmentExpression(varValue); + instr.push(new Instruction(IVARNAME, varName.value)); + instr.push(new Instruction(IEXPR, varValue)); + instr.push(binaryInstruction('=')); + } +}; + +ParserState.prototype.parseConditionalExpression = function (instr) { + this.parseOrExpression(instr); + while (this.accept(TOP, '?')) { + var trueBranch = []; + var falseBranch = []; + this.parseConditionalExpression(trueBranch); + this.expect(TOP, ':'); + this.parseConditionalExpression(falseBranch); + instr.push(new Instruction(IEXPR, trueBranch)); + instr.push(new Instruction(IEXPR, falseBranch)); + instr.push(ternaryInstruction('?')); + } +}; + +ParserState.prototype.parseOrExpression = function (instr) { + this.parseAndExpression(instr); + while (this.accept(TOP, 'or')) { + var falseBranch = []; + this.parseAndExpression(falseBranch); + instr.push(new Instruction(IEXPR, falseBranch)); + instr.push(binaryInstruction('or')); + } +}; + +ParserState.prototype.parseAndExpression = function (instr) { + this.parseComparison(instr); + while (this.accept(TOP, 'and')) { + var trueBranch = []; + this.parseComparison(trueBranch); + instr.push(new Instruction(IEXPR, trueBranch)); + instr.push(binaryInstruction('and')); + } +}; + +var COMPARISON_OPERATORS = ['==', '!=', '<', '<=', '>=', '>', 'in']; + +ParserState.prototype.parseComparison = function (instr) { + this.parseAddSub(instr); + while (this.accept(TOP, COMPARISON_OPERATORS)) { + var op = this.current; + this.parseAddSub(instr); + instr.push(binaryInstruction(op.value)); + } +}; + +var ADD_SUB_OPERATORS = ['+', '-', '||']; + +ParserState.prototype.parseAddSub = function (instr) { + this.parseTerm(instr); + while (this.accept(TOP, ADD_SUB_OPERATORS)) { + var op = this.current; + this.parseTerm(instr); + instr.push(binaryInstruction(op.value)); + } +}; + +var TERM_OPERATORS = ['*', '/', '%']; + +ParserState.prototype.parseTerm = function (instr) { + this.parseFactor(instr); + while (this.accept(TOP, TERM_OPERATORS)) { + var op = this.current; + this.parseFactor(instr); + instr.push(binaryInstruction(op.value)); + } +}; + +ParserState.prototype.parseFactor = function (instr) { + var unaryOps = this.tokens.unaryOps; + function isPrefixOperator(token) { + return token.value in unaryOps; + } + + this.save(); + if (this.accept(TOP, isPrefixOperator)) { + if (this.current.value !== '-' && this.current.value !== '+') { + if (this.nextToken.type === TPAREN && this.nextToken.value === '(') { + this.restore(); + this.parseExponential(instr); + return; + } else if (this.nextToken.type === TSEMICOLON || this.nextToken.type === TCOMMA || this.nextToken.type === TEOF || (this.nextToken.type === TPAREN && this.nextToken.value === ')')) { + this.restore(); + this.parseAtom(instr); + return; + } + } + + var op = this.current; + this.parseFactor(instr); + instr.push(unaryInstruction(op.value)); + } else { + this.parseExponential(instr); + } +}; + +ParserState.prototype.parseExponential = function (instr) { + this.parsePostfixExpression(instr); + while (this.accept(TOP, '^')) { + this.parseFactor(instr); + instr.push(binaryInstruction('^')); + } +}; + +ParserState.prototype.parsePostfixExpression = function (instr) { + this.parseFunctionCall(instr); + while (this.accept(TOP, '!')) { + instr.push(unaryInstruction('!')); + } +}; + +ParserState.prototype.parseFunctionCall = function (instr) { + var unaryOps = this.tokens.unaryOps; + function isPrefixOperator(token) { + return token.value in unaryOps; + } + + if (this.accept(TOP, isPrefixOperator)) { + var op = this.current; + this.parseAtom(instr); + instr.push(unaryInstruction(op.value)); + } else { + this.parseMemberExpression(instr); + while (this.accept(TPAREN, '(')) { + if (this.accept(TPAREN, ')')) { + instr.push(new Instruction(IFUNCALL, 0)); + } else { + var argCount = this.parseArgumentList(instr); + instr.push(new Instruction(IFUNCALL, argCount)); + } + } + } +}; + +ParserState.prototype.parseArgumentList = function (instr) { + var argCount = 0; + + while (!this.accept(TPAREN, ')')) { + this.parseExpression(instr); + ++argCount; + while (this.accept(TCOMMA)) { + this.parseExpression(instr); + ++argCount; + } + } + + return argCount; +}; + +ParserState.prototype.parseMemberExpression = function (instr) { + this.parseAtom(instr); + while (this.accept(TOP, '.') || this.accept(TBRACKET, '[')) { + var op = this.current; + + if (op.value === '.') { + if (!this.allowMemberAccess) { + throw new Error('unexpected ".", member access is not permitted'); + } + + this.expect(TNAME); + instr.push(new Instruction(IMEMBER, this.current.value)); + } else if (op.value === '[') { + if (!this.tokens.isOperatorEnabled('[')) { + throw new Error('unexpected "[]", arrays are disabled'); + } + + this.parseExpression(instr); + this.expect(TBRACKET, ']'); + instr.push(binaryInstruction('[')); + } else { + throw new Error('unexpected symbol: ' + op.value); + } + } +}; + +function add(a, b) { + return Number(a) + Number(b); +} + +function sub(a, b) { + return a - b; +} + +function mul(a, b) { + return a * b; +} + +function div(a, b) { + return a / b; +} + +function mod(a, b) { + return a % b; +} + +function concat(a, b) { + if (Array.isArray(a) && Array.isArray(b)) { + return a.concat(b); + } + return '' + a + b; +} + +function equal(a, b) { + return a === b; +} + +function notEqual(a, b) { + return a !== b; +} + +function greaterThan(a, b) { + return a > b; +} + +function lessThan(a, b) { + return a < b; +} + +function greaterThanEqual(a, b) { + return a >= b; +} + +function lessThanEqual(a, b) { + return a <= b; +} + +function andOperator(a, b) { + return Boolean(a && b); +} + +function orOperator(a, b) { + return Boolean(a || b); +} + +function inOperator(a, b) { + return contains(b, a); +} + +function sinh(a) { + return ((Math.exp(a) - Math.exp(-a)) / 2); +} + +function cosh(a) { + return ((Math.exp(a) + Math.exp(-a)) / 2); +} + +function tanh(a) { + if (a === Infinity) return 1; + if (a === -Infinity) return -1; + return (Math.exp(a) - Math.exp(-a)) / (Math.exp(a) + Math.exp(-a)); +} + +function asinh(a) { + if (a === -Infinity) return a; + return Math.log(a + Math.sqrt((a * a) + 1)); +} + +function acosh(a) { + return Math.log(a + Math.sqrt((a * a) - 1)); +} + +function atanh(a) { + return (Math.log((1 + a) / (1 - a)) / 2); +} + +function log10(a) { + return Math.log(a) * Math.LOG10E; +} + +function neg(a) { + return -a; +} + +function not(a) { + return !a; +} + +function trunc(a) { + return a < 0 ? Math.ceil(a) : Math.floor(a); +} + +function random(a) { + return Math.random() * (a || 1); +} + +function factorial(a) { // a! + return gamma(a + 1); +} + +function isInteger(value) { + return isFinite(value) && (value === Math.round(value)); +} + +var GAMMA_G = 4.7421875; +var GAMMA_P = [ + 0.99999999999999709182, + 57.156235665862923517, -59.597960355475491248, + 14.136097974741747174, -0.49191381609762019978, + 0.33994649984811888699e-4, + 0.46523628927048575665e-4, -0.98374475304879564677e-4, + 0.15808870322491248884e-3, -0.21026444172410488319e-3, + 0.21743961811521264320e-3, -0.16431810653676389022e-3, + 0.84418223983852743293e-4, -0.26190838401581408670e-4, + 0.36899182659531622704e-5 +]; + +// Gamma function from math.js +function gamma(n) { + var t, x; + + if (isInteger(n)) { + if (n <= 0) { + return isFinite(n) ? Infinity : NaN; + } + + if (n > 171) { + return Infinity; // Will overflow + } + + var value = n - 2; + var res = n - 1; + while (value > 1) { + res *= value; + value--; + } + + if (res === 0) { + res = 1; // 0! is per definition 1 + } + + return res; + } + + if (n < 0.5) { + return Math.PI / (Math.sin(Math.PI * n) * gamma(1 - n)); + } + + if (n >= 171.35) { + return Infinity; // will overflow + } + + if (n > 85.0) { // Extended Stirling Approx + var twoN = n * n; + var threeN = twoN * n; + var fourN = threeN * n; + var fiveN = fourN * n; + return Math.sqrt(2 * Math.PI / n) * Math.pow((n / Math.E), n) * + (1 + (1 / (12 * n)) + (1 / (288 * twoN)) - (139 / (51840 * threeN)) - + (571 / (2488320 * fourN)) + (163879 / (209018880 * fiveN)) + + (5246819 / (75246796800 * fiveN * n))); + } + + --n; + x = GAMMA_P[0]; + for (var i = 1; i < GAMMA_P.length; ++i) { + x += GAMMA_P[i] / (n + i); + } + + t = n + GAMMA_G + 0.5; + return Math.sqrt(2 * Math.PI) * Math.pow(t, n + 0.5) * Math.exp(-t) * x; +} + +function stringOrArrayLength(s) { + if (Array.isArray(s)) { + return s.length; + } + return String(s).length; +} + +function hypot() { + var sum = 0; + var larg = 0; + for (var i = 0; i < arguments.length; i++) { + var arg = Math.abs(arguments[i]); + var div; + if (larg < arg) { + div = larg / arg; + sum = (sum * div * div) + 1; + larg = arg; + } else if (arg > 0) { + div = arg / larg; + sum += div * div; + } else { + sum += arg; + } + } + return larg === Infinity ? Infinity : larg * Math.sqrt(sum); +} + +function condition(cond, yep, nope) { + return cond ? yep : nope; +} + +/** +* Decimal adjustment of a number. +* From @escopecz. +* +* @param {Number} value The number. +* @param {Integer} exp The exponent (the 10 logarithm of the adjustment base). +* @return {Number} The adjusted value. +*/ +function roundTo(value, exp) { + // If the exp is undefined or zero... + if (typeof exp === 'undefined' || +exp === 0) { + return Math.round(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.round(+(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)); +} + +function setVar(name, value, variables) { + if (variables) variables[name] = value; + return value; +} + +function arrayIndex(array, index) { + return array[index | 0]; +} + +function max(array) { + if (arguments.length === 1 && Array.isArray(array)) { + return Math.max.apply(Math, array); + } else { + return Math.max.apply(Math, arguments); + } +} + +function min(array) { + if (arguments.length === 1 && Array.isArray(array)) { + return Math.min.apply(Math, array); + } else { + return Math.min.apply(Math, arguments); + } +} + +function arrayMap(f, a) { + if (typeof f !== 'function') { + throw new Error('First argument to map is not a function'); + } + if (!Array.isArray(a)) { + throw new Error('Second argument to map is not an array'); + } + return a.map(function (x, i) { + return f(x, i); + }); +} + +function arrayFold(f, init, a) { + if (typeof f !== 'function') { + throw new Error('First argument to fold is not a function'); + } + if (!Array.isArray(a)) { + throw new Error('Second argument to fold is not an array'); + } + return a.reduce(function (acc, x, i) { + return f(acc, x, i); + }, init); +} + +function arrayFilter(f, a) { + if (typeof f !== 'function') { + throw new Error('First argument to filter is not a function'); + } + if (!Array.isArray(a)) { + throw new Error('Second argument to filter is not an array'); + } + return a.filter(function (x, i) { + return f(x, i); + }); +} + +function stringOrArrayIndexOf(target, s) { + if (!(Array.isArray(s) || typeof s === 'string')) { + throw new Error('Second argument to indexOf is not a string or array'); + } + + return s.indexOf(target); +} + +function arrayJoin(sep, a) { + if (!Array.isArray(a)) { + throw new Error('Second argument to join is not an array'); + } + + return a.join(sep); +} + +function sign(x) { + return ((x > 0) - (x < 0)) || +x; +} + +var ONE_THIRD = 1/3; +function cbrt(x) { + return x < 0 ? -Math.pow(-x, ONE_THIRD) : Math.pow(x, ONE_THIRD); +} + +function expm1(x) { + return Math.exp(x) - 1; +} + +function log1p(x) { + return Math.log(1 + x); +} + +function log2(x) { + return Math.log(x) / Math.LN2; +} + +class Parser { + constructor(options) { + this.options = options || {}; + this.unaryOps = { + sin: Math.sin, + cos: Math.cos, + tan: Math.tan, + asin: Math.asin, + acos: Math.acos, + atan: Math.atan, + sinh: Math.sinh || sinh, + cosh: Math.cosh || cosh, + tanh: Math.tanh || tanh, + asinh: Math.asinh || asinh, + acosh: Math.acosh || acosh, + atanh: Math.atanh || atanh, + sqrt: Math.sqrt, + cbrt: Math.cbrt || cbrt, + log: Math.log, + log2: Math.log2 || log2, + ln: Math.log, + lg: Math.log10 || log10, + log10: Math.log10 || log10, + expm1: Math.expm1 || expm1, + log1p: Math.log1p || log1p, + abs: Math.abs, + ceil: Math.ceil, + floor: Math.floor, + round: Math.round, + trunc: Math.trunc || trunc, + '-': neg, + '+': Number, + exp: Math.exp, + not: not, + length: stringOrArrayLength, + '!': factorial, + sign: Math.sign || sign + }; + + this.binaryOps = { + '+': add, + '-': sub, + '*': mul, + '/': div, + '%': mod, + '^': Math.pow, + '||': concat, + '==': equal, + '!=': notEqual, + '>': greaterThan, + '<': lessThan, + '>=': greaterThanEqual, + '<=': lessThanEqual, + and: andOperator, + or: orOperator, + 'in': inOperator, + '=': setVar, + '[': arrayIndex + }; + + this.ternaryOps = { + '?': condition + }; + + this.functions = { + random: random, + fac: factorial, + min: min, + max: max, + hypot: Math.hypot || hypot, + pyt: Math.hypot || hypot, // backward compat + pow: Math.pow, + atan2: Math.atan2, + 'if': condition, + gamma: gamma, + roundTo: roundTo, + map: arrayMap, + fold: arrayFold, + filter: arrayFilter, + indexOf: stringOrArrayIndexOf, + join: arrayJoin + }; + + this.consts = { + E: Math.E, + PI: Math.PI, + 'true': true, + 'false': false + }; + } + + parse(expr) { + var instr = []; + var parserState = new ParserState( + this, + new TokenStream(this, expr), + { allowMemberAccess: this.options.allowMemberAccess } + ); + + parserState.parseExpression(instr); + parserState.expect(TEOF, 'EOF'); + + return new Expression(instr, this); + } + + evaluate(expr, variables) { + return this.parse(expr).evaluate(variables); + } +}; + + +var sharedParser = new Parser(); + +Parser.parse = function (expr) { + return sharedParser.parse(expr); +}; + +Parser.evaluate = function (expr, variables) { + return sharedParser.parse(expr).evaluate(variables); +}; + +var optionNameMap = { + '+': 'add', + '-': 'subtract', + '*': 'multiply', + '/': 'divide', + '%': 'remainder', + '^': 'power', + '!': 'factorial', + '<': 'comparison', + '>': 'comparison', + '<=': 'comparison', + '>=': 'comparison', + '==': 'comparison', + '!=': 'comparison', + '||': 'concatenate', + 'and': 'logical', + 'or': 'logical', + 'not': 'logical', + '?': 'conditional', + ':': 'conditional', + '=': 'assignment', + '[': 'array', + '()=': 'fndef' +}; + +function getOptionName(op) { + return optionNameMap.hasOwnProperty(op) ? optionNameMap[op] : op; +} + +Parser.prototype.isOperatorEnabled = function (op) { + var optionName = getOptionName(op); + var operators = this.options.operators || {}; + + return !(optionName in operators) || !!operators[optionName]; +}; + +/*! + Based on ndef.parser, by Raphael Graf(r@undefined.ch) + http://www.undefined.ch/mparser/index.html + + Ported to JavaScript and modified by Matthew Crumley (email@matthewcrumley.com, http://silentmatt.com/) + + You are free to use and modify this code in anyway you find useful. Please leave this comment in the code + to acknowledge its original source. If you feel like it, I enjoy hearing about projects that use my code, + but don't feel like you have to let me know or ask permission. +*/ + + diff --git a/qml/js/mathlib.js b/qml/js/mathlib.js new file mode 100644 index 0000000..7bae694 --- /dev/null +++ b/qml/js/mathlib.js @@ -0,0 +1,274 @@ +/** + * Logarithm Graph Creator - Create graphs with logarithm scales. + * Copyright (C) 2020 Ad5001 + * + * 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 . + */ + +.pragma library + +.import "expr-eval.js" as ExprEval + +const parser = new ExprEval.Parser() + +class Expression { + constructor(expr) { + this.expr = expr + this.calc = parser.parse(expr).simplify() + this.replacements = [ + ['pi', 'π'], + ['inf', '∞'], + ['Infinity', '∞'], + [' * ', '×'], + ['0×', '0'], + ['1×', '1'], + ['2×', '2'], + ['3×', '3'], + ['4×', '4'], + ['5×', '5'], + ['6×', '6'], + ['7×', '7'], + ['8×', '8'], + ['9×', '9'], + [')×', ')'], + ['×(', '('], + ] + } + + isConstant() { + return this.expr.indexOf("x") == -1 + } + + evaluate(x = 0) { + return this.calc.evaluate({ + "x": x, + "pi": Math.PI, + "π": Math.PI, + "inf": Infinity, + "Infinity": Infinity, + "∞": Infinity, + "e": Math.E + }) + } + + toEditableString() { + return this.calc.toString() + } + + toString() { + var str = this.calc.toString() + if(str[0] == "(") str = str.substr(1) + if(str[str.length - 1] == ")") str = str.substr(0, str.length - 1) + this.replacements.forEach(function(replacement){ + str = str.replace(replacement[0], replacement[1]) + }) + return str + } +} + +// Domains + +class EmptySet { + constructor() {} + + includes(x) { return false } + + toString() { return "∅" } + + static import(frm) { return new EmptySet() } +} + +class Domain { + constructor(begin, end, openBegin, openEnd) { + if(typeof begin == 'number' || typeof begin == 'string') begin = new Expression(begin.toString()) + this.begin = begin + if(typeof end == 'number' || typeof end == 'string') end = new Expression(end.toString()) + this.end = end + this.openBegin = openBegin + this.openEnd = openEnd + this.displayName = (openBegin ? "]" : "[") + begin.toString() + ";" + end.toString() + (openEnd ? "[" : "]") + } + + includes(x) { + return ((this.openBegin && x > this.begin.evaluate()) || (!this.openBegin && x >= this.begin.evaluate())) && + ((this.openEnd && x < this.end.evaluate()) || (!this.openEnd && x <= this.end.evaluate())) + } + + toString() { + return this.displayName + } + + static importFrom(frm) { + switch(frm.trim().toUpperCase()) { + case "R": + case "ℝ": + return Domain.R + break; + case "RE": + case "R*": + case "ℝ*": + return Domain.RE + break; + case "RP": + case "R+": + case "ℝ⁺": + return Domain.RP + break; + case "RM": + case "R-": + case "ℝ⁻": + return Domain.RM + break; + case "RPE": + case "REP": + case "R+*": + case "R*+": + case "ℝ*⁺": + case "ℝ⁺*": + return Domain.RPE + break; + case "RME": + case "REM": + case "R-*": + case "R*-": + case "ℝ⁻*": + case "ℝ*⁻": + return Domain.RME + break; + default: + var openBegin = frm.trim().charAt(0) == "]" + var openEnd = frm.trim().charAt(frm.length -1) == "[" + var [begin, end] = frm.substr(1, frm.length-2).split(";") + console.log(frm, begin, end, openBegin, openEnd) + return new Domain(begin.trim(), end.trim(), openBegin, openEnd) + break; + } + } +} +Domain.R = new Domain(-Infinity,Infinity,true,true) +Domain.R.displayName = "ℝ" +Domain.RP = new Domain(0,Infinity,true,false) +Domain.RP.displayName = "ℝ⁺" +Domain.RM = new Domain(-Infinity,0,true,false) +Domain.RM.displayName = "ℝ⁻" +Domain.RPE = new Domain(0,Infinity,true,true) +Domain.RPE.displayName = "ℝ⁺*" +Domain.RME = new Domain(-Infinity,0,true,true) +Domain.RME.displayName = "ℝ⁻*" + +class DomainSet { + constructor(values) { + var newVals = [] + values.forEach(function(value){ + newVals.push(new Expression(value.toString())) + }) + this.values = newVals + } + + includes(x) { + var xcomputed = new Expression(x.toString()).evaluate() + var found = false + this.values.forEach(function(value){ + if(xcomputed == value.evaluate()) { + found = true + return + } + }) + return found + } + + toString() { + return "{" + this.values.join(";") + "}" + } + + static importFrom(frm) { + return new DomainSet(frm.substr(1, frm.length-2).split(";")) + } +} + +class UnionDomain { + constructor(dom1, dom2) { + this.dom1 = dom1 + this.dom2 = dom2 + } + + includes(x) { + return this.dom1.includes(x) || this.dom2.includes(x) + } + + toString() { + return this.dom1.toString() + " ∪ " + this.dom2.toString() + } + + static importFrom(frm) { + var domains = frm.trim().split("∪") + if(domains.length == 1) domains = frm.trim().split("U") // Fallback + return new UnionDomain(parseDomain(domains[0].trim()), parseDomain(domains[1].trim())) + } +} + +class IntersectionDomain { + constructor(dom1, dom2) { + this.dom1 = dom1 + this.dom2 = dom2 + } + + includes(x) { + return this.dom1.includes(x) && this.dom2.includes(x) + } + + toString() { + return this.dom1.toString() + " ∩ " + this.dom2.toString() + } + + static importFrom(frm) { + var domains = frm.trim().split("∩") + return new IntersectionDomain(parseDomain(domains[0].trim()), parseDomain(domains[1].trim())) + } +} + +class MinusDomain { + constructor(dom1, dom2) { + this.dom1 = dom1 + this.dom2 = dom2 + } + + includes(x) { + return this.dom1.includes(x) && !this.dom2.includes(x) + } + + toString() { + return this.dom1.toString() + "∖" + this.dom2.toString() + } + + static importFrom(frm) { + var domains = frm.trim().split("∖") + if(domains.length == 1) domains = frm.trim().split("\\") // Fallback + return new MinusDomain(parseDomain(domains[0].trim()), parseDomain(domains[1].trim())) + } +} + +Domain.RE = new MinusDomain("R", "{0}") +Domain.RE.displayName = "ℝ*" + + +function parseDomain(domain) { + if(domain.indexOf("U") >= 0 || domain.indexOf("∪") >= 0) return UnionDomain.importFrom(domain) + if(domain.indexOf("∩") >= 0) return IntersectionDomain.importFrom(domain) + if(domain.indexOf("∖") >= 0 || domain.indexOf("\\") >= 0) return MinusDomain.importFrom(domain) + if(domain.charAt(0) == "{" && domain.charAt(domain.length -1) == "}") return DomainSet.importFrom(domain) + if(domain.indexOf("]") >= 0 || domain.indexOf("]") >= 0) return Domain.importFrom(domain) + if(domain.toUpperCase().indexOf("R") >= 0 || domain.indexOf("ℝ") >= 0) return Domain.importFrom(domain) + return new EmptySet() +} diff --git a/qml/js/objects.js b/qml/js/objects.js new file mode 100644 index 0000000..ecb61c4 --- /dev/null +++ b/qml/js/objects.js @@ -0,0 +1,235 @@ +/** + * Logarithm Graph Creator - Create graphs with logarithm scales. + * Copyright (C) 2020 Ad5001 + * + * 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 . + */ + +.pragma library + +.import "utils.js" as Utils +.import "mathlib.js" as MathLib + + + +function getNewName(allowedLetters, category) { + if(Object.keys(currentObjects).indexOf(category) == -1) return allowedLetters[0] + var newid = currentObjects[category].length + var letter = allowedLetters[newid % allowedLetters.length] + var num = Math.round((newid - (newid % allowedLetters.length)) / allowedLetters.length) + return letter + (num > 0 ? Utils.textsup(num) : '') +} + +class DrawableObject { + static type(){return 'Unknown'} + static properties() {return {}} + + constructor(name, visible = true, color = null, labelContent = 'name + value') { + if(color == null) color = this.getRandomColor() + this.type = 'Unknown' + this.name = name + this.visible = visible + this.color = color + this.labelContent = labelContent // "null", "name", "name + value" + this.requiredBy = [] + } + + getRandomColor() { + var x = '0123456789ABCDEF'; + var color = '#'; + for (var i = 0; i < 6; i++) { + color += x[Math.floor(Math.random() * 16)]; + } + return color; + } + + getReadableString() { + return `${this.name} = Unknown` + } + + getLabel() { + switch(this.labelContent) { + case 'name': + return this.name + case 'name + value': + return this.getReadableString() + case 'null': + return '' + + } + } + + export() { + return [this.name, this.visible, this.color.toString(), this.labelContent] + } + + draw(canvas, ctx) {} +} + +class Point extends DrawableObject { + static type(){return 'Point'} + static properties() {return { + 'x': 'Expression', + 'y': 'Expression', + 'labelPos': ['top', 'bottom', 'left', 'right'], + 'pointStyle': ['dot', 'diagonal cross', 'vertical cross'], + }} + + constructor(name = null, visible = true, color = null, labelContent = 'name + value', + x = 1, y = 0, labelPos = 'top', pointStyle = 'dot') { + if(name == null) name = getNewName('ABCDEFJKLMNOPQRSTUVW', 'Point') + super(name, visible, color, labelContent) + this.type = 'Point' + if(typeof x == 'number' || typeof x == 'string') x = new MathLib.Expression(x.toString()) + this.x = x + if(typeof y == 'number' || typeof y == 'string') y = new MathLib.Expression(y.toString()) + this.y = y + this.labelPos = labelPos + this.pointStyle = pointStyle + } + + getReadableString() { + return `${this.name} = (${this.x}, ${this.y})` + } + + export() { + return [this.name, this.visible, this.color.toString(), this.labelContent, this.x.toEditableString(), this.y.toEditableString(), this.labelPos, this.pointStyle] + } + + draw(canvas, ctx) { + var [canvasX, canvasY] = [canvas.x2px(this.x.evaluate()), canvas.y2px(this.y.evaluate())] + var pointSize = 8 + switch(this.pointStyle) { + case 'dot': + ctx.fillStyle = this.color + ctx.beginPath(); + ctx.ellipse(canvasX-pointSize/2, canvasY-pointSize/2, pointSize, pointSize) + ctx.fill(); + break; + case 'diagonal cross': + ctx.strokeStyle = this.color + canvas.drawLine(ctx, canvasX-pointSize/2, canvasY-pointSize/2, canvasX+pointSize/2, canvasY+pointSize/2) + canvas.drawLine(ctx, canvasX-pointSize/2, canvasY+pointSize/2, canvasX-pointSize/2, canvasY+pointSize/2) + break; + case 'vertical cross': + ctx.strokeStyle = this.color + canvas.drawLine(ctx, canvasX, canvasY-pointSize/2, canvasX, canvasY+pointSize/2) + canvas.drawLine(ctx, canvasX-pointSize/2, canvasY, canvasX+pointSize/2, canvasY) + break; + } + var text = this.getLabel() + ctx.font = "14px sans-serif" + var textSize = ctx.measureText(text).width + ctx.fillStyle = this.color + switch(this.labelPos) { + case 'top': + canvas.drawVisibleText(ctx, text, canvasX-textSize/2, canvasY-16) + break; + case 'bottom': + canvas.drawVisibleText(ctx, text, canvasX-textSize/2, canvasY+16) + break; + case 'left': + canvas.drawVisibleText(ctx, text, canvasX-textSize-10, canvasY+4) + break; + case 'right': + canvas.drawVisibleText(ctx, text, canvasX+10, canvasY+4) + break; + + } + } +} + +class Function extends DrawableObject { + static type(){return 'Function'} + static properties() {return { + 'expression': 'Expression', + 'inDomain': 'Domain', + 'outDomain': 'Domain', + 'labelPos': ['above', 'below'], + 'displayMode': ['application', 'function'], + 'labelX': 'number' + }} + + constructor(name = null, visible = true, color = null, labelContent = 'name + value', + expression = 'x', inDomain = 'RPE', outDomain = 'R', displayMode = 'application', labelPos = 'above', labelX = 1) { + if(name == null) name = getNewName('fghjqlmnopqrstuvwabcde', 'Function') + super(name, visible, color, labelContent) + if(typeof expression == 'number' || typeof expression == 'string') expression = new MathLib.Expression(expression.toString()) + this.expression = expression + if(typeof inDomain == 'string') inDomain = MathLib.parseDomain(inDomain) + this.inDomain = inDomain + if(typeof outDomain == 'string') outDomain = MathLib.parseDomain(outDomain) + this.outDomain = outDomain + this.displayMode = displayMode + this.labelPos = labelPos + this.labelX = labelX + } + + getReadableString() { + if(this.displayMode == 'application') { + return `${this.name}: ${this.inDomain} ⸺˃ ${this.outDomain}\n x ⸺˃ ${this.expression.toString()}` + } else { + return `${this.name}(x) = ${this.expression.toString()}` + } + } + + export() { + return [this.name, this.visible, this.color.toString(), this.labelContent, + this.expression.toEditableString(), this.inDomain.toString(), this.outDomain.toString(), + this.displayMode, this.labelPos, this.labelX] + } + + draw(canvas, ctx) { + ctx.strokeStyle = this.color + ctx.fillStyle = this.color + // Drawing small traits every 2px + var pxprecision = 2 + var previousX = canvas.px2x(0) + var previousY = this.expression.evaluate(previousX) + for(var px = pxprecision; px < canvas.canvasSize.width; px += pxprecision) { + var currentX = canvas.px2x(px) + var currentY = this.expression.evaluate(currentX) + if(this.inDomain.includes(currentX) && this.inDomain.includes(previousX) && + this.outDomain.includes(currentY) && this.outDomain.includes(previousY) && + Math.abs(previousY-currentY)<100) { // 100 per 2px is a lot (probably inf to inf issue) + canvas.drawLine(ctx, canvas.x2px(previousX), canvas.y2px(previousY), canvas.x2px(currentX), canvas.y2px(currentY)) + } + previousX = currentX + previousY = currentY + } + // Label + var text = this.getLabel() + ctx.font = "14px sans-serif" + var textSize = canvas.measureText(ctx, text) + ctx.fillStyle = this.color + var posX = canvas.x2px(this.labelX) + var posY = canvas.y2px(this.expression.evaluate(this.labelX)) + switch(this.labelPos) { + case 'above': + canvas.drawVisibleText(ctx, text, posX-textSize.width/2, posY-textSize.height) + break; + case 'below': + canvas.drawVisibleText(ctx, text, posX-textSize.width/2, posY+textSize.height) + break; + + } + } +} + +const drawableTypes = { + 'Point': Point, + 'Function': Function +} + +var currentObjects = {} diff --git a/qml/js/utils.js b/qml/js/utils.js new file mode 100644 index 0000000..c463dfd --- /dev/null +++ b/qml/js/utils.js @@ -0,0 +1,118 @@ +/** + * Logarithm Graph Creator - Create graphs with logarithm scales. + * Copyright (C) 2020 Ad5001 + * + * 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 . + */ + +var powerpos = { + "-": "⁻", + "0": "⁰", + "1": "¹", + "2": "²", + "3": "³", + "4": "⁴", + "5": "⁵", + "6": "⁶", + "7": "⁷", + "8": "⁸", + "9": "⁹", + "+": "⁺", + "=": "⁼", + "a": "ᵃ", + "b": "ᵇ", + "c": "ᶜ", + "d": "ᵈ", + "e": "ᵉ", + "f": "ᶠ", + "g": "ᵍ", + "h": "ʰ", + "i": "ⁱ", + "j": "ʲ", + "k": "ᵏ", + "l": "ˡ", + "m": "ᵐ", + "n": "ⁿ", + "o": "ᵒ", + "p": "ᵖ", + "r": "ʳ", + "s": "ˢ", + "t": "ᵗ", + "u": "ᵘ", + "v": "ᵛ", + "w": "ʷ", + "x": "ˣ", + "y": "ʸ", + "z": "ᶻ", +} + +var indicepos = { + "-": "₋", + "0": "₀", + "1": "₁", + "2": "₂", + "3": "₃", + "4": "₄", + "5": "₅", + "6": "₆", + "7": "₇", + "8": "₈", + "9": "₉", + "+": "₊", + "=": "₌", + "a": "ₐ", + "e": "ₑ", + "h": "ₕ", + "i": "ᵢ", + "j": "ⱼ", + "k": "ₖ", + "l": "ₗ", + "m": "ₘ", + "n": "ₙ", + "o": "ₒ", + "p": "ₚ", + "r": "ᵣ", + "s": "ₛ", + "t": "ₜ", + "u": "ᵤ", + "v": "ᵥ", + "x": "ₓ", +} +// Put a text in sup position +function textsup(text) { + var ret = "" + text = text.toString() + for (var i = 0; i < text.length; i++) { + if(Object.keys(powerpos).indexOf(text[i]) >= 0) { + ret += powerpos[text[i]] + } else { + ret += text[i] + } + } + return ret +} + +// Put a text in sub position +function textsub(text) { + var ret = "" + text = text.toString() + for (var i = 0; i < text.length; i++) { + if(Object.keys(indicepos).indexOf(text[i]) >= 0) { + ret += indicepos[text[i]] + } else { + ret += text[i] + } + } + return ret +} diff --git a/run.py b/run.py new file mode 100644 index 0000000..a280138 --- /dev/null +++ b/run.py @@ -0,0 +1,94 @@ +""" + * Logarithm Graph Creator - Create graphs with logarithm scales. + * Copyright (C) 2020 Ad5001 + * + * 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 . +""" + +from PySide2.QtWidgets import QApplication, QFileDialog +from PySide2.QtQml import QQmlApplicationEngine, qmlRegisterType +from PySide2.QtCore import Qt, QObject, Signal, Slot, Property + +import os +import tempfile +from platform import release as os_release +from json import dumps +from sys import platform + +tempfile = tempfile.mkstemp(suffix = '.png')[1] + +def get_linux_theme(): + des = { + "KDE": "org.kde.desktop", # org.kde.desktop resolves to universal in PySide2. + "gnome": "default", + "lxqt": "fusion", + "mate": "fusion", + } + if "XDG_SESSION_DESKTOP" in os.environ: + return des[os.environ["XDG_SESSION_DESKTOP"]] if os.environ["XDG_SESSION_DESKTOP"] in des else "fusion" + else: + # Android + return "Material" +os.environ["QT_QUICK_CONTROLS_STYLE"] = { + "linux": get_linux_theme(), + "freebsd": get_linux_theme(), + "win32": "universal" if os_release == "10" else "fusion", + "cygwin": "fusion", + "darwin": "imagine" +}[platform] + +class Helper(QObject): + @Slot(str, str) + def write(self, filename, filedata): + if os.path.exists(os.path.dirname(os.path.realpath(filename))): + f = open(os.path.realpath(filename), 'w', -1, 'utf8') + f.write(filedata) + f.close() + + @Slot(str, result=str) + def load(self, filename): + if os.path.exists(os.path.realpath(filename)): + f = open(os.path.realpath(filename), 'r', -1, 'utf8') + data = f.read() + f.close() + return data + return '{}' + + @Slot(result=str) + def gettmpfile(self): + global tempfile + return tempfile + + @Slot() + def copyImageToClipboard(self): + global tempfile + # TODO: Better copy system + os.system("xclip -selection clipboard -t image/png -i " + tempfile) + +app = QApplication([]) +app.setApplicationName("Logarithmic Graph Creator") +app.setOrganizationName("Ad5001") +engine = QQmlApplicationEngine() +helper = Helper() +engine.rootContext().setContextProperty("Helper", helper) + +engine.addImportPath(os.path.realpath(os.path.join(os.getcwd(), "qml"))) +engine.load(os.path.realpath(os.path.join(os.getcwd(), "qml", "LogGraph.qml"))) + +if not engine.rootObjects(): + print("No root object") + exit(-1) +app.exec_() + +os.remove(tempfile)