--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<classpath>
+ <classpathentry kind="src" path="src"/>
+ <classpathentry kind="src" path="extra-src"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
+ <classpathentry kind="lib" path="/home/sampo/Projects/OpenRocket/lib/miglayout15-swing.jar"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/JCommon 1.0.16"/>
+ <classpathentry kind="con" path="org.eclipse.jdt.USER_LIBRARY/JFreeChart 1.0.13"/>
+ <classpathentry kind="lib" path="/home/sampo/Projects/OpenRocket/extra-lib/RXTXcomm.jar"/>
+ <classpathentry kind="output" path="bin"/>
+</classpath>
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<projectDescription>
+ <name>OpenRocket</name>
+ <comment></comment>
+ <projects>
+ </projects>
+ <buildSpec>
+ <buildCommand>
+ <name>org.eclipse.jdt.core.javabuilder</name>
+ <arguments>
+ </arguments>
+ </buildCommand>
+ </buildSpec>
+ <natures>
+ <nature>org.eclipse.jdt.core.javanature</nature>
+ </natures>
+</projectDescription>
--- /dev/null
+2009-05-24 Sampo Niskanen <sampo.niskanen@iki.fi>
+
+ * Initial release 0.9.0
+
--- /dev/null
+OpenRocket - A model rocket simulator
+
+Copyright (C) 2007-2009 Sampo Niskanen
+
+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 (below) for more details.
+
+
+Additional permission under GNU GPL version 3 section 7:
+
+The licensors grant additional permission to package this Program, or
+any covered work, along with any non-compilable data files (such as
+thrust curves or component databases) and convey the resulting work.
+
+
+------------------------------------------------------------------------
+
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ 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.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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 <http://www.gnu.org/licenses/>.
+
+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:
+
+ <program> Copyright (C) <year> <name of author>
+ 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
+<http://www.gnu.org/licenses/>.
+
+ 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
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+
--- /dev/null
+
+OpenRocket - an Open Source model rocket simulator
+--------------------------------------------------
+
+Copyright (C) 2007-2009 Sampo Niskanen
+
+
+For license information see the file LICENSE.TXT.
+
+For more information see http://openrocket.sourceforge.net/
+
+
+
+To start the software run the class
+
+ net.sf.openrocket.gui.main.BasicFrame
+
+or from the JAR file run
+
+ $ java -jar OpenRocket-<VERSION>.jar
--- /dev/null
+
+GUI:
+
+- Preferences dialog
+
+
+BUGS:
+
+
+COMPUTATION:
+
+
+FILE/STORAGE:
+
+
+OTHER:
+
+- web-sivut
+
+
+DIPPA:
+
+
+
+
+-------------------
+
+LATER:
+
+- Simulation delete/copy/paste hotkeys
+ (either component or simulation selected, but not both)
+- Add BodyComponent at end of rocket when no component is selected
+- Showing events in plot (maybe future)
+- Search field in motor selection dialog
+- Through-the-wall fins
+- Store materials
+
+- Streamer CD estimation
+
+- exporting (maybe later)
+
+- Make ThicknessRingComponent implement RadialParent and allow
+ attaching components to a TubeCoupler
+
+
+
+
+DONE:
+
+- Automatic diameters of body components
+- Copy/paste
+
+18.4.:
+- Esc, Ctrl-Z and Y etc.
+- Look and feel
+
+19.4.:
+- Nose cone and transition shoulders in GUI
+- zoom, cut/copy/paste etc. icons
+
+23.4.:
+- Figure or rocket not updating when using a new BasicFrame
+
+24.4.:
+- File save and load
+- Motor configuration editing (pre-alpha)
+- Save simulations
+
+25.4.:
+- Multi-stages simulation (pre-alpha)
+- Make sure simulations end
+- Mass and CG overrides (pre-alpha)
+- General loader
+
+26.4.:
+- Centering ring inner diameter automatics (pre-alpha)
+- Landing simulation (pre-alpha ??)
+- Parachute/Streamer editing in GUI (pre-alpha)
+- Launch lug editing in GUI (pre-alpha)
+
+29.4.:
+- Actual plotting done
+- Refactored source code packages
+
+2.5.:
+- Plotting (pre-alpha)
+- Gravity model
+- More units and specific custom units (angle, temperature, ...)
+- Transition/Nose cone description text wrapping
+- Fin set CP jumps at Mach 0.9
+
+- Error dialogs for load/save/etc
+
+3.5.:
+- More materials (pre-alpha)
+- File opening from command line
+
+9.5.:
+- Rocket configuration dialog
+- Warnings in poor conditions (transition supersonic)
+- New or old fin-body interference?
+- poista tiedot laminaarisesta vastuksesta
+- vertailuosio
+
+11.5.:
+- Better default values for components
+- Component analysis dialog show zero total mass and CG
+- Compression support in save
+- Simulation storage options
+
+12.5.:
+- Load simulations
+- Update file version to 1.0
+
+13.5.:
+- statistiikat softasta
+
+17.5.:
+- jonkin verran TODOja
+- conclusion
+- viitteet
+- Draw the component icons
+- splashscreen
+
+18.5.:
+- About dialog + version number
--- /dev/null
+<project name="OpenRocket" basedir=".">
+
+ <property name="src.dir" value="src"/> <!-- Source directory -->
+ <property name="build.dir" value="build"/> <!-- Build directory -->
+
+ <!-- Distribution directory, from which stuff is jar'ed -->
+ <property name="dist.dir" value="${build.dir}/dist"/>
+
+ <property name="classes.dir" value="${dist.dir}"/> <!-- Directory for classes -->
+ <property name="jar.dir" value="${build.dir}/jar"/> <!-- Directory for built jar's -->
+ <property name="lib.dir" value="lib"/> <!-- Library source directory -->
+
+
+ <!-- The main class of the application -->
+ <property name="main-class" value="net.sf.openrocket.gui.main.BasicFrame"/>
+
+
+ <!-- Classpath definition -->
+ <path id="classpath">
+ <fileset dir="${lib.dir}" includes="**/*.jar"/>
+ </path>
+
+
+ <!-- CLEAN -->
+ <target name="clean">
+ <delete dir="${build.dir}"/>
+ </target>
+
+ <!-- BUILD -->
+ <target name="build">
+ <mkdir dir="${classes.dir}"/>
+ <javac srcdir="${src.dir}" destdir="${classes.dir}" classpathref="classpath"/>
+ </target>
+
+ <!-- JAR -->
+ <target name="jar" depends="build">
+ <copy todir="${dist.dir}/">
+ <fileset dir="." includes="LICENSE.TXT" />
+ <fileset dir="." includes="README.TXT" />
+ <fileset dir="." includes="datafiles/**/* pix/**/*" />
+ </copy>
+ <mkdir dir="${jar.dir}"/>
+ <jar destfile="${jar.dir}/${ant.project.name}.jar" basedir="${dist.dir}">
+ <manifest>
+ <attribute name="Main-Class" value="${main-class}"/>
+ <attribute name="SplashScreen-Image" value="pix/splashscreen.png"/>
+ </manifest>
+ <zipfileset src="lib/miglayout15-swing.jar" />
+ <zipfileset src="lib/jcommon-1.0.16.jar" />
+ <zipfileset src="lib/jfreechart-1.0.13.jar" />
+ </jar>
+ </target>
+
+ <!-- RUN -->
+ <target name="run" depends="jar">
+ <java fork="true" classname="${main-class}">
+ <classpath>
+ <path location="${jar.dir}/${ant.project.name}.jar"/>
+ </classpath>
+ </java>
+ </target>
+
+</project>
\ No newline at end of file
--- /dev/null
+Rocket motor simulation data downloaded from ThrustCurve.org.\r
+This ZIP file contains 526 simulator data files.\r
+For more info, please see http://www.thrustcurve.org/\r
+\r
+AMW_I195.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: WW-38-390\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: John DeMar\r
+\r
+AMW_I220.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: SK-38-390\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: John DeMar\r
+\r
+AMW_I271.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: BB-38-390\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Greg Gardner\r
+\r
+AMW_I285.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: GG-38-390\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Greg Gardner\r
+\r
+AMW_I315.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: SK-38-640\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Koen Loeven\r
+\r
+AMW_I325.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: WW-38-640\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: John DeMar\r
+\r
+AMW_I375.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: GG-38-640\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Robert DeHate\r
+\r
+AMW_J357.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: WT-54-1050\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Carl Tulanko\r
+\r
+AMW_J365.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: SK-54-1400\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Robert DeHate\r
+\r
+AMW_J370.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: GG-54-1050\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_J400.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: RR-54-1050\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_J440.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: BB-38-640\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Robert DeHate\r
+\r
+AMW_J450.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: ST-54-1050\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Conway Stevens\r
+\r
+AMW_J450_1.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: ST-54-1050\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Carl Tulanko\r
+\r
+AMW_J480.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: BB-54-1050\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_J500.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: J500ST\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_K1000.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: SK-54-2550\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_K1075.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: GG-54-2550\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_K365.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: RR-75-1700\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_K450.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: BB-75-1700\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_K470.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: ST-75-1700\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_K475.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: WT-54-1400\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_K530.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: GG-54-1400\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_K555.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: SK-54-1750\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Koen Loeven\r
+\r
+AMW_K560.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: RR-54-1400\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_K570.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: WT-54-1750\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Carl Tulanko\r
+\r
+AMW_K600.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: WT-75-2500\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Conway Stevens\r
+\r
+AMW_K600_1.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: WT-75-2500\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Carl Tulanko\r
+\r
+AMW_K605.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: RR-75-2500\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_K650.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: RR-54-1750\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Koen Loeven\r
+\r
+AMW_K670.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: GG-54-1750\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Conway Stevens\r
+\r
+AMW_K670_1.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: GG-54-1750\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Carl Tulanko\r
+\r
+AMW_K700.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: BB-54-1400\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_K800.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: BB-54-1750\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Koen Loeven\r
+\r
+AMW_K950.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: ST-54-1750\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Conway Stevens\r
+\r
+AMW_K950_1.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: ST-54-1750\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Carl Tulanko\r
+\r
+AMW_K975.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: WT-54-2550\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Carl Tulanko\r
+\r
+AMW_L1060.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: GG-75-3500\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Conway Stevens\r
+\r
+AMW_L1060_1.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: GG-75-3500\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Carl Tulanko\r
+\r
+AMW_L1080.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: BB-75-3500\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_L1100.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: RR-54-2550\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_L1111.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: ST-75-3500\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_L1300.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: BB-54-2550\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_L1400.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: SK-75-6000\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: John DeMar\r
+\r
+AMW_L666.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: SK-75-3500\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Joel Rogers\r
+\r
+AMW_L700.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: BB-75-2500\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_L777.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: WT-75-3500\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Conway Stevens\r
+\r
+AMW_L777_1.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: WT-75-3500\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Carl Tulanko\r
+\r
+AMW_L900.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: RR-75-3500\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_M1350.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: WT-75-6000\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Carl Tulanko\r
+\r
+AMW_M1480.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: RR-75-6000\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_M1730.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: SK-98-11000\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Joel Rogers\r
+\r
+AMW_M1850.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: GG-75-6000\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Conway Stevens\r
+\r
+AMW_M1850_1.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: GG-75-6000\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Carl Tulanko\r
+\r
+AMW_M1900.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: BB-75-6000\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_M2500.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: GG-75-7600\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Carl Tulanko\r
+\r
+AMW_M3000.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: ST-75-7600\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Conway Stevens\r
+\r
+AMW_N2020.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: WT-98-11000\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Joel Rogers\r
+\r
+AMW_N2600.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: GG-98-11000\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_N2700.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: BB-98-11000\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AMW_N2800.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: WW-98-17500\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: John DeMar\r
+\r
+AMW_N4000.eng\r
+ Manufacturer: Animal Motor Works\r
+ Designation: BB-98-17500\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Robert DeHate\r
+\r
+AeroTech_D13.eng\r
+ Manufacturer: AeroTech\r
+ Designation: D13\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_D15.eng\r
+ Manufacturer: AeroTech\r
+ Designation: D15\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_D21.eng\r
+ Manufacturer: AeroTech\r
+ Designation: D21\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_D24.eng\r
+ Manufacturer: AeroTech\r
+ Designation: D24\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Stan Hemphill\r
+\r
+AeroTech_D7.eng\r
+ Manufacturer: AeroTech\r
+ Designation: D7\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_D9.eng\r
+ Manufacturer: AeroTech\r
+ Designation: D9\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_E11.eng\r
+ Manufacturer: AeroTech\r
+ Designation: E11J\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_E12.eng\r
+ Manufacturer: AeroTech\r
+ Designation: E12J\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_E15.eng\r
+ Manufacturer: AeroTech\r
+ Designation: E15\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+AeroTech_E15_1.eng\r
+ Manufacturer: AeroTech\r
+ Designation: E15\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_E16.eng\r
+ Manufacturer: AeroTech\r
+ Designation: E16\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_E18.eng\r
+ Manufacturer: AeroTech\r
+ Designation: E18\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_E23.eng\r
+ Manufacturer: AeroTech\r
+ Designation: E23\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_E28.eng\r
+ Manufacturer: AeroTech\r
+ Designation: E28\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_E30.eng\r
+ Manufacturer: AeroTech\r
+ Designation: E30\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_E6.eng\r
+ Manufacturer: AeroTech\r
+ Designation: E6\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+AeroTech_E7.eng\r
+ Manufacturer: AeroTech\r
+ Designation: E7\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_F10.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F10\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+AeroTech_F12.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F12\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_F13.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F13-RC\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_F16.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F16-RC\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_F20.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F20\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_F21.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F21W\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Stan Hemphill\r
+\r
+AeroTech_F22.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F22\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_F23.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F23FJ\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_F23_1.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F23-RC-SK\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_F24.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F24\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_F25.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F25W\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_F26.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F26FJ\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_F27.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F27R\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_F32.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F32\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_F35.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F35W\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_F37.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F37\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_F39.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F39\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_F40.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F40\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_F42.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F42T\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_F50.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F50\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_F52.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F52\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_F62.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F62T\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Stan Hemphill\r
+\r
+AeroTech_F72.eng\r
+ Manufacturer: AeroTech\r
+ Designation: F72\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_G101.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G101T\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+AeroTech_G104.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G104T\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Stan Hemphill\r
+\r
+AeroTech_G12.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G12-RC\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_G25.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G25\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_G33.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G33\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_G339.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G339N-P\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Bill Wagstaff\r
+\r
+AeroTech_G35.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G35\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_G38.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G38FJ\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_G40.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G40W\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_G53.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G53FJ\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_G54.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G54\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_G55.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G55\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_G61.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G61W\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_G64.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G64\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_G67.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G67R\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Stan Hemphill\r
+\r
+AeroTech_G69.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G69N\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_G71.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G71R\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_G71_1.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G71R\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Edward K. Chess\r
+\r
+AeroTech_G75.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G75J\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_G75_1.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G75J\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Stan Hemphill\r
+\r
+AeroTech_G76.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G76G\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_G76_1.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G76G\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John DeMar\r
+\r
+AeroTech_G77.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G77R\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Stan Hemphill\r
+\r
+AeroTech_G78.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G78G\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_G79.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G79W\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_G80.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G80\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John DeMar\r
+\r
+AeroTech_G80_1.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G80\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John DeMar\r
+\r
+AeroTech_G80_2.eng\r
+ Manufacturer: AeroTech\r
+ Designation: G80\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John DeMar\r
+\r
+AeroTech_H112.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H112J\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_H123.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H123W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_H125.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H125W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_H128.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H128W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_H148.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H148R\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_H165.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H165R\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_H180.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H180W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_H210.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H210R\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_H220.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H220T\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_H238.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H238T\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_H242.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H242T\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_H242_1.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H242T\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_H250.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H250G\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Jim Yehle\r
+\r
+AeroTech_H268.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H268R\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_H45.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H45W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_H55.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H55W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_H669.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H669N-P\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Greg Gardner\r
+\r
+AeroTech_H70.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H70W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_H73.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H73J\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_H97.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H97J\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_H999.eng\r
+ Manufacturer: AeroTech\r
+ Designation: H999\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Greg Gardner\r
+\r
+AeroTech_I115.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I115W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+AeroTech_I117.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I117FJ\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+AeroTech_I1299.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I1299N-P\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Jim Yehle\r
+\r
+AeroTech_I132.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I132W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_I154.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I154J\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_I161.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I161W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_I195.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I195J\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_I195_1.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I195J\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_I200.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I200W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_I211.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I211W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_I215.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I215R\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+AeroTech_I218.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I218R\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_I225.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I225FJ\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_I229.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I229T\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+AeroTech_I245.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I245G\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Jim Yehle\r
+\r
+AeroTech_I284.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I284W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_I284_1.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I284W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_I285.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I285R\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_I300.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I300T\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_I305.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I305FJ\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_I357.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I357T\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_I364.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I364FJ\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_I366.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I366R\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_I435.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I435T\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_I435_1.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I435T\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_I599.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I599N\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+AeroTech_I600.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I600R\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_I65.eng\r
+ Manufacturer: AeroTech\r
+ Designation: I65W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_J125.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J125W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_J1299.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J1299N-P\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Greg Gardner\r
+\r
+AeroTech_J135.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J135W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_J145.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J145H 2-jet std.\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_J180.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J180T\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_J1999.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J1999N-P\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Greg Gardner\r
+\r
+AeroTech_J210.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J210H 4-jet std.\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_J250.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J250FJ\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+AeroTech_J260.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J260HW 3-jet EFX\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_J275.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J275W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_J315.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J315R\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_J350.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J350W-L\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_J350_1.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J350W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_J390.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J390-turbo\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_J415.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J415W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_J420.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J420R\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_J460.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J460T\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_J500.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J500G\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Jim Yehle\r
+\r
+AeroTech_J540.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J540R\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_J570.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J570W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_J575.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J575FJ\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Simon Crafts\r
+\r
+AeroTech_J800.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J800T-PS\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_J825.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J825R\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Greg Gardner\r
+\r
+AeroTech_J90.eng\r
+ Manufacturer: AeroTech\r
+ Designation: J90W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_K1050.eng\r
+ Manufacturer: AeroTech\r
+ Designation: K1050W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_K1100.eng\r
+ Manufacturer: AeroTech\r
+ Designation: K1100T\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_K1275.eng\r
+ Manufacturer: AeroTech\r
+ Designation: K1275\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_K1499.eng\r
+ Manufacturer: AeroTech\r
+ Designation: K1499N-P\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Jim Yehle\r
+\r
+AeroTech_K185.eng\r
+ Manufacturer: AeroTech\r
+ Designation: K185W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_K1999.eng\r
+ Manufacturer: AeroTech\r
+ Designation: K1999N-P\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Christopher Kobel\r
+\r
+AeroTech_K250.eng\r
+ Manufacturer: AeroTech\r
+ Designation: K250W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_K270.eng\r
+ Manufacturer: AeroTech\r
+ Designation: K270W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+AeroTech_K458.eng\r
+ Manufacturer: AeroTech\r
+ Designation: K458W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_K485.eng\r
+ Manufacturer: AeroTech\r
+ Designation: K485H (3 jet)\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_K550.eng\r
+ Manufacturer: AeroTech\r
+ Designation: K550W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_K560.eng\r
+ Manufacturer: AeroTech\r
+ Designation: K560W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_K650.eng\r
+ Manufacturer: AeroTech\r
+ Designation: K650T\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_K680.eng\r
+ Manufacturer: AeroTech\r
+ Designation: K680R\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_K695.eng\r
+ Manufacturer: AeroTech\r
+ Designation: K695R\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_K700.eng\r
+ Manufacturer: AeroTech\r
+ Designation: K700W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_K780.eng\r
+ Manufacturer: AeroTech\r
+ Designation: K780R\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_K828.eng\r
+ Manufacturer: AeroTech\r
+ Designation: K828FJ\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+AeroTech_L1120.eng\r
+ Manufacturer: AeroTech\r
+ Designation: L1120W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_L1150.eng\r
+ Manufacturer: AeroTech\r
+ Designation: L1150R\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_L1300.eng\r
+ Manufacturer: AeroTech\r
+ Designation: L1300R\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_L1420.eng\r
+ Manufacturer: AeroTech\r
+ Designation: L1420R\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_L1500.eng\r
+ Manufacturer: AeroTech\r
+ Designation: L1500T\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_L850.eng\r
+ Manufacturer: AeroTech\r
+ Designation: L850W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_L952.eng\r
+ Manufacturer: AeroTech\r
+ Designation: L952W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_M1297.eng\r
+ Manufacturer: AeroTech\r
+ Designation: M1297W\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Greg Gardner\r
+\r
+AeroTech_M1315.eng\r
+ Manufacturer: AeroTech\r
+ Designation: M1315W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_M1419.eng\r
+ Manufacturer: AeroTech\r
+ Designation: M1419W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_M1550.eng\r
+ Manufacturer: AeroTech\r
+ Designation: M1550R\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_M1600.eng\r
+ Manufacturer: AeroTech\r
+ Designation: M1600R\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_M1850.eng\r
+ Manufacturer: AeroTech\r
+ Designation: M1850W-PS\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Greg Gardner\r
+\r
+AeroTech_M1939.eng\r
+ Manufacturer: AeroTech\r
+ Designation: M1939W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_M2000.eng\r
+ Manufacturer: AeroTech\r
+ Designation: M2000R\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_M2400.eng\r
+ Manufacturer: AeroTech\r
+ Designation: M2400T\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_M2500.eng\r
+ Manufacturer: AeroTech\r
+ Designation: M2500T\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_M650.eng\r
+ Manufacturer: AeroTech\r
+ Designation: M650W\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Greg Gardner\r
+\r
+AeroTech_M750.eng\r
+ Manufacturer: AeroTech\r
+ Designation: M750W\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Greg Gardner\r
+\r
+AeroTech_M845.eng\r
+ Manufacturer: AeroTech\r
+ Designation: M845\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+AeroTech_N2000.eng\r
+ Manufacturer: AeroTech\r
+ Designation: N2000W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+AeroTech_N4800.eng\r
+ Manufacturer: AeroTech\r
+ Designation: N4800T\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Apogee_1/2A2.eng\r
+ Manufacturer: Apogee Components\r
+ Designation: 1/2A2\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Apogee_1/4A2.eng\r
+ Manufacturer: Apogee Components\r
+ Designation: 1/4A2\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Apogee_A2.eng\r
+ Manufacturer: Apogee Components\r
+ Designation: A2\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Apogee_B2.eng\r
+ Manufacturer: Apogee Components\r
+ Designation: B2\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Apogee_B7.eng\r
+ Manufacturer: Apogee Components\r
+ Designation: B7\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Apogee_C10.eng\r
+ Manufacturer: Apogee Components\r
+ Designation: C10\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Apogee_C4.eng\r
+ Manufacturer: Apogee Components\r
+ Designation: C4\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Apogee_C6.eng\r
+ Manufacturer: Apogee Components\r
+ Designation: C6\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Apogee_D10.eng\r
+ Manufacturer: Apogee Components\r
+ Designation: D10\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Apogee_D3.eng\r
+ Manufacturer: Apogee Components\r
+ Designation: D3\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Apogee_E6.eng\r
+ Manufacturer: Apogee Components\r
+ Designation: E6\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Apogee_F10.eng\r
+ Manufacturer: Apogee Components\r
+ Designation: F10\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Cesaroni_G60.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 134 G60-14A\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Cesaroni_G69.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 121 G69-14A\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Pete Carr\r
+\r
+Cesaroni_G69_1.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 121 G69-14A\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_G79.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 129 G79SS-13A\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: Pete Carr\r
+\r
+Cesaroni_G79_1.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 129 G79SS-13A\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_H120.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 261 H120-14A\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Len Bryan\r
+\r
+Cesaroni_H143.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 247 H143SS-13A\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_H153.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: H153\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_H565.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: H565\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Cesaroni_I170.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 382 I170-14A\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Cesaroni_I205.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: I205\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_I212.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 364 I212SS-14A\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_I240.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: I240\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Cesaroni_I285.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: I285\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_I287.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 486 I287SS-15A\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_I350.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 601 I350SS-16A\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_I360.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: I360\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Cesaroni_I540.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 634I540WT\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Cesaroni_J210.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 836 J210-16A\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_J280.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: J280SS\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_J285.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 648 J285-15A\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_J295.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 1195 J295-15A\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_J300.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: J300\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Cesaroni_J330.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 765 J330-16A\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_J360.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: J360\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Cesaroni_J380.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 1043 J380SS-16A\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_J400.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: J400SS\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_J410.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 774 J410-16A\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Len Bryan\r
+\r
+Cesaroni_K445.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 1635 K445-A\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_K510.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 2486 K510-P-U\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Len Lekx\r
+\r
+Cesaroni_K510_1.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 2486 K510-P-U\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_K530.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 1412 K530SS-16A\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_K570.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 2060 K570-A\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_K575.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 2493 K575-P\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Cesaroni_K650.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 1750 K650SS-16A\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_K660.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 2437 K660-17A\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_L1090.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 4815 L1090-P\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Cesaroni_L1115.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 5015 L1115-P-U\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Len Lekx\r
+\r
+Cesaroni_L1115_1.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 5015 L1115-P-U\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_L610.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 4842 L610-P\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Cesaroni_L730.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 2765 L730-P\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_L800.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 3757 L800-P-U\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Len Lekx\r
+\r
+Cesaroni_L800_1.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 3757 L800-P-U\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_L890.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 3762 L890-P\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Cesaroni_M1060.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 7441 M1060-P\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Cesaroni_M1400.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 6251 M1400-P-U\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Len Lekx\r
+\r
+Cesaroni_M1400_1.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 6251 M1400-P-U\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_M1450.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 9955 M1450-P\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Cesaroni_M2505.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 7450 M2505-P\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_M520.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 7400 M520-P\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_M795.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 10133 M795-P\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Cesaroni_N1100.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 14005 N1100-P\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Cesaroni_N2500.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 13766 N2500-P\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Casey Hatch\r
+\r
+Cesaroni_O5100.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 29990 O5100-P\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Cesaroni_O5800.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 30605 O5800-P\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Len Bryan\r
+\r
+Cesaroni_O8000.eng\r
+ Manufacturer: Cesaroni Technology Inc.\r
+ Designation: 40960 O8000-P\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Len Bryan\r
+\r
+Contrail_G100.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: G100-PVC\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_G123.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: G123-HP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_G130.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: G130-PVC\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_G234.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: G234-HP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_G300.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: G300-PVC\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_H121.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: H121-HP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_H141.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: H141-PVC\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_H211.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: H211-PVC\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_H222.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: H222-HP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_H246.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: H246-HP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_H277.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: H277-HP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_H300.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: H300-HP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_H303.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: H303-PVC\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_H340.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: H340-SP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_I155.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: I155-HP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_I210.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: I210-PVC\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_I221.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: I221-PVC\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_I290.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: I290-SP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_I307.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: I307-HP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_I333.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: I333-PVC\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_I400.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: I400-HP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_I500.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: I500-HP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_I727.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: I727-HP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_I747.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: I747-HP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_J150.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: J150-HP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_J222.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: J222-HP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_J234.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: J234-BG\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_J242.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: J242-PVC\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_J245.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: J245-BG\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_J246.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: J246-HP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_J272.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: J272-SP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_J292.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: J292-SP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_J333.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: J333-HP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_J345.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: J345-PVC\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_J355.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: J355-BG\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_J358.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: J358-BG\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_J416.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: J416-SP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_J555.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: J555-HP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_J642.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: J642-BG\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_J800.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: J800-HP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_K234.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: K234-BG\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_K265.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: K265-SP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_K300.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: K300-BS\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_K321.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: K321-BG\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_K404.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: K404-SP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_K456.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: K456-BG\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_K630.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: K630-SP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_K678.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: K678-BG\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_K707.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: K707-BG\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_K777.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: K777-SP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_L1222.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: L1222-SM\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_L2525.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: L2525-GF\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_L369.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: L369-SP\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_L800.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: L800-BG\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_M1575.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: M1575-BG\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_M2700.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: M2700-BS\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_M2800.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: M2800-BG\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_M711.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: M711-BS\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Contrail_O6300.eng\r
+ Manufacturer: Contrail Rockets LLC\r
+ Designation: O6300-BS\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Ellis_G20.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: G20\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Ellis_G35.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: G35\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Ellis_G37.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: G37\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Ellis_H275.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: H275\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Ellis_H48.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: H48\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Ellis_H50.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: H50\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Ellis_I130.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: I130\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Ellis_I134.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: I134\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Ellis_I150.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: I150\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Ellis_I160.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: I160\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Ellis_I230.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: I230\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Ellis_I69.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: I69\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Ellis_J110.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: J110\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Ellis_J148.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: J148\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Ellis_J228.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: J228\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Ellis_J270.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: J270\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Ellis_J330.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: J330\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Ellis_K475.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: K475\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Ellis_L330.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: L330\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Ellis_L600.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: L600\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Ellis_M1000.eng\r
+ Manufacturer: Ellis Mountain Rocket Works\r
+ Designation: M1000\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Estes_1/2A3.eng\r
+ Manufacturer: Estes Industries\r
+ Designation: 1/2A3\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Estes_1/2A6.eng\r
+ Manufacturer: Estes Industries\r
+ Designation: 1/2A6\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Estes_1/4A3.eng\r
+ Manufacturer: Estes Industries\r
+ Designation: 1/4A3\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Estes_A10.eng\r
+ Manufacturer: Estes Industries\r
+ Designation: A10\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Estes_A3.eng\r
+ Manufacturer: Estes Industries\r
+ Designation: A3\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Estes_A8.eng\r
+ Manufacturer: Estes Industries\r
+ Designation: A8\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Estes_B4.eng\r
+ Manufacturer: Estes Industries\r
+ Designation: B4\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Estes_B6.eng\r
+ Manufacturer: Estes Industries\r
+ Designation: B6\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+Estes_C11.eng\r
+ Manufacturer: Estes Industries\r
+ Designation: C11\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Estes_C5.eng\r
+ Manufacturer: Estes Industries\r
+ Designation: C5\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Estes_C6.eng\r
+ Manufacturer: Estes Industries\r
+ Designation: C6\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Estes_D11.eng\r
+ Manufacturer: Estes Industries\r
+ Designation: D11\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Estes_D12.eng\r
+ Manufacturer: Estes Industries\r
+ Designation: D12\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Estes_E9.eng\r
+ Manufacturer: Estes Industries\r
+ Designation: E9\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+GR_K555.eng\r
+ Manufacturer: Gorilla Rocket Motors, Inc.\r
+ Designation: K555GT\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Mark Koelsch\r
+\r
+Hypertek_I130.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 300CC098J - I130\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Hypertek_I136.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 300CC098J2 - I136\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Hypertek_I145.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 300CC098JFX - I145FX\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Hypertek_I205.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 300CC125J - I205\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Hypertek_I222.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 300CC125J2 - I222\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Hypertek_I225.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 300CC125JFX - I225FX\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Hypertek_I260.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 440CC172J - I260\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Hypertek_I310.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 440CC172J - I310\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_J115.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 440CC076J - J115\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_J120.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 440CC076JFX - J120FX\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_J150.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 440CC086J - J150\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_J170.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 440CC098J - J170\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_J190.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 440CC098JFX - J190FX\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_J220.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 440CC110J - J220\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_J250.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 440CC125J - J250\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_J250_1.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 440CC125J - J250\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_J270.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 440CC125JFX - J270FX\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_J295.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 440CC172JFX - J295FX\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Hypertek_J317.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 835CC172J - J317\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_J330.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 835CC172JFX - J330FX\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_J330_1.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 835CC172JFX - J330FX\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_K240.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 835CC125J - K240\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_L200.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 1685CC098L - L200\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_L225.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 1685CC098LFX - L225FX\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_L350.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 1685CC125L - L350\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_L355.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 1685CC125LFX - L355FX\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_L475.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 1685CC172L - L475\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_L535.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 1685CC172LFX - L535FX\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_L540.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 2800CC172L - L540\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_L540_1.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 2800CC172L - L540\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_L550.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 1685CCRGL - L550\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_L570.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 2800CC172LFX - L570FX\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_L570_1.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 2800CC172LFX - L570FX\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_L575.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 2800CCRGL - L575\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_L575_1.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 2800CCRGL - L575\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_L610.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 1685CCRGLFX - L610FX\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_L625.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 2800CCRGLFX - L625FX\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_L625_1.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 2800CCRGLFX - L625FX\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_L740.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 2800CC200MFX - L740FX\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Hypertek_L970.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 2800CC300M - L970\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Hypertek_M1000.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 4630CCRGM - M1000\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_M1000_1.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 4630CCRGM - M1000\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_M1001.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 5478CCRGM - M1001\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Hypertek_M1010.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 4630CCRGMFX - M1010FX\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_M1010_1.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 4630CCRGMFX - M1010FX\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+Hypertek_M1015.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 3500CCRGMFX - M1015FX\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Hypertek_M1040.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 4630CCRGMFX - M1040FX\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Hypertek_M740.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 2800CC200M - M740\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Hypertek_M956.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 3500CCRGM - M956\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Hypertek_M960.eng\r
+ Manufacturer: Hypertek\r
+ Designation: 2800CC300MFX - M960FX\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+KBA_I170.eng\r
+ Manufacturer: Kosdon by AeroTech\r
+ Designation: I170S\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+KBA_I280.eng\r
+ Manufacturer: Kosdon by AeroTech\r
+ Designation: I280F\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+KBA_I301.eng\r
+ Manufacturer: Kosdon by AeroTech\r
+ Designation: I301W\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Mark Koelsch\r
+\r
+KBA_I310.eng\r
+ Manufacturer: Kosdon by AeroTech\r
+ Designation: I310S\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+KBA_I370.eng\r
+ Manufacturer: Kosdon by AeroTech\r
+ Designation: I370F\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+KBA_I450.eng\r
+ Manufacturer: Kosdon by AeroTech\r
+ Designation: I450F\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+KBA_I550.eng\r
+ Manufacturer: Kosdon by AeroTech\r
+ Designation: I550R\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Mark Koelsch\r
+\r
+KBA_J405.eng\r
+ Manufacturer: Kosdon by AeroTech\r
+ Designation: J405S\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+KBA_J605.eng\r
+ Manufacturer: Kosdon by AeroTech\r
+ Designation: J605F\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+KBA_K1750.eng\r
+ Manufacturer: Kosdon by AeroTech\r
+ Designation: K1750R\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Mark Koelsch\r
+\r
+KBA_K400.eng\r
+ Manufacturer: Kosdon by AeroTech\r
+ Designation: K400S\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+KBA_K600.eng\r
+ Manufacturer: Kosdon by AeroTech\r
+ Designation: K600F\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+KBA_K750.eng\r
+ Manufacturer: Kosdon by AeroTech\r
+ Designation: K750W\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Mark Koelsch\r
+\r
+KBA_L1000.eng\r
+ Manufacturer: Kosdon by AeroTech\r
+ Designation: L1000S\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+KBA_L1400.eng\r
+ Manufacturer: Kosdon by AeroTech\r
+ Designation: L1400F\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+KBA_M1450.eng\r
+ Manufacturer: Kosdon by AeroTech\r
+ Designation: M1450W\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Mark Koelsch\r
+\r
+Loki_H144.eng\r
+ Manufacturer: Loki Research\r
+ Designation: H144-LW\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: William Carney\r
+\r
+Loki_H500.eng\r
+ Manufacturer: Loki Research\r
+ Designation: H500\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Loki_I405.eng\r
+ Manufacturer: Loki Research\r
+ Designation: I405LW\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: William Carney\r
+\r
+Loki_J525.eng\r
+ Manufacturer: Loki Research\r
+ Designation: J525LW\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: William Carney\r
+\r
+Loki_J528.eng\r
+ Manufacturer: Loki Research\r
+ Designation: J528LW\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: William Carney\r
+\r
+Loki_K250.eng\r
+ Manufacturer: Loki Research\r
+ Designation: K250LWM\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: William Carney\r
+\r
+Loki_K350.eng\r
+ Manufacturer: Loki Research\r
+ Designation: K350LWM\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: William Carney\r
+\r
+Loki_K960.eng\r
+ Manufacturer: Loki Research\r
+ Designation: K960LWB\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: William Carney\r
+\r
+Loki_L1400.eng\r
+ Manufacturer: Loki Research\r
+ Designation: L1400LW\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: William Carney\r
+\r
+Loki_L930.eng\r
+ Manufacturer: Loki Research\r
+ Designation: L930LWB\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: William Carney\r
+\r
+Loki_M1882.eng\r
+ Manufacturer: Loki Research\r
+ Designation: M1882LW\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: William Carney\r
+\r
+PML_F50.eng\r
+ Manufacturer: Public Missiles, Ltd.\r
+ Designation: F50T\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+PML_G40.eng\r
+ Manufacturer: Public Missiles, Ltd.\r
+ Designation: G40W\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+PML_G80.eng\r
+ Manufacturer: Public Missiles, Ltd.\r
+ Designation: G80T\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+PP_H70.eng\r
+ Manufacturer: Propulsion Polymers\r
+ Designation: 240NS-H70\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+PP_I160.eng\r
+ Manufacturer: Propulsion Polymers\r
+ Designation: 484NS-I160\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+PP_I80.eng\r
+ Manufacturer: Propulsion Polymers\r
+ Designation: 460NS-I80\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+PP_J140.eng\r
+ Manufacturer: Propulsion Polymers\r
+ Designation: 664NS-J140\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Quest_A6.eng\r
+ Manufacturer: Quest Aerospace\r
+ Designation: A6\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Quest_B6.eng\r
+ Manufacturer: Quest Aerospace\r
+ Designation: B6\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+Quest_C6.eng\r
+ Manufacturer: Quest Aerospace\r
+ Designation: C6\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+Quest_D5.eng\r
+ Manufacturer: Quest Aerospace\r
+ Designation: D5-P\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+RATT_H70.eng\r
+ Manufacturer: R.A.T.T. Works Precision Rocket Motors\r
+ Designation: H70\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+RATT_I80.eng\r
+ Manufacturer: R.A.T.T. Works Precision Rocket Motors\r
+ Designation: I80\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+RATT_I90.eng\r
+ Manufacturer: R.A.T.T. Works Precision Rocket Motors\r
+ Designation: I90L\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+RATT_J160.eng\r
+ Manufacturer: R.A.T.T. Works Precision Rocket Motors\r
+ Designation: J160\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+RATT_K240.eng\r
+ Manufacturer: R.A.T.T. Works Precision Rocket Motors\r
+ Designation: K240H\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: John Coker\r
+\r
+RATT_L600.eng\r
+ Manufacturer: R.A.T.T. Works Precision Rocket Motors\r
+ Designation: L600\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+RATT_M900.eng\r
+ Manufacturer: R.A.T.T. Works Precision Rocket Motors\r
+ Designation: M900\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+RV_F32.eng\r
+ Manufacturer: Rocketvision Flight-Star\r
+ Designation: F32\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+RV_F72.eng\r
+ Manufacturer: Rocketvision Flight-Star\r
+ Designation: F72\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+RV_G55.eng\r
+ Manufacturer: Rocketvision Flight-Star\r
+ Designation: G55\r
+ Data Format: RASP\r
+ Data Source: cert\r
+ Contributor: Mark Koelsch\r
+\r
+Roadrunner_E25.eng\r
+ Manufacturer: Roadrunner Rocketry\r
+ Designation: E25\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Roadrunner Rocketry\r
+\r
+Roadrunner_F35.eng\r
+ Manufacturer: Roadrunner Rocketry\r
+ Designation: F35\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Roadrunner Rocketry\r
+\r
+Roadrunner_F45.eng\r
+ Manufacturer: Roadrunner Rocketry\r
+ Designation: F45\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Roadrunner Rocketry\r
+\r
+Roadrunner_F60.eng\r
+ Manufacturer: Roadrunner Rocketry\r
+ Designation: F60\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Roadrunner Rocketry\r
+\r
+Roadrunner_G80.eng\r
+ Manufacturer: Roadrunner Rocketry\r
+ Designation: G80\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Roadrunner Rocketry\r
+\r
+SkyR_G125.eng\r
+ Manufacturer: Sky Ripper Systems\r
+ Designation: G125\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+SkyR_G63.eng\r
+ Manufacturer: Sky Ripper Systems\r
+ Designation: G63\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+SkyR_G69.eng\r
+ Manufacturer: Sky Ripper Systems\r
+ Designation: G69\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+SkyR_H124.eng\r
+ Manufacturer: Sky Ripper Systems\r
+ Designation: H124\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Andrew MacMillen\r
+\r
+SkyR_H155.eng\r
+ Manufacturer: Sky Ripper Systems\r
+ Designation: H155\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Andrew MacMillen\r
+\r
+SkyR_H78.eng\r
+ Manufacturer: Sky Ripper Systems\r
+ Designation: H78\r
+ Data Format: RASP\r
+ Data Source: user\r
+ Contributor: John Coker\r
+\r
+SkyR_I117.eng\r
+ Manufacturer: Sky Ripper Systems\r
+ Designation: I117\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Andrew MacMillen\r
+\r
+SkyR_I119.eng\r
+ Manufacturer: Sky Ripper Systems\r
+ Designation: I119\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Andrew MacMillen\r
+\r
+SkyR_I147.eng\r
+ Manufacturer: Sky Ripper Systems\r
+ Designation: I147\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Andrew MacMillen\r
+\r
+SkyR_J144.eng\r
+ Manufacturer: Sky Ripper Systems\r
+ Designation: J144\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Andrew MacMillen\r
+\r
+SkyR_J261.eng\r
+ Manufacturer: Sky Ripper Systems\r
+ Designation: J261G\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: John Coker\r
+\r
+SkyR_J263.eng\r
+ Manufacturer: Sky Ripper Systems\r
+ Designation: J263G\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: John Coker\r
+\r
+SkyR_J337.eng\r
+ Manufacturer: Sky Ripper Systems\r
+ Designation: J337B\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: John Coker\r
+\r
+SkyR_J348.eng\r
+ Manufacturer: Sky Ripper Systems\r
+ Designation: J348B\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: John Coker\r
+\r
+SkyR_K257.eng\r
+ Manufacturer: Sky Ripper Systems\r
+ Designation: K257G\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: John Coker\r
+\r
+SkyR_K347.eng\r
+ Manufacturer: Sky Ripper Systems\r
+ Designation: K347B\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: John Coker\r
+\r
+WCH_I110.eng\r
+ Manufacturer: West Coast Hybrids\r
+ Designation: 499 I110-P\r
+ Data Format: RASP\r
+ Data Source: mfr\r
+ Contributor: Andrew MacMillen\r
--- /dev/null
+;Animal Motor Works 38-390\r
+I195WT 38 249 17 0.193 0.495 AMW\r
+ 0.0020 10.548\r
+ 0.018 42.653\r
+ 0.046 136.214\r
+ 0.064 179.784\r
+ 0.072 191.248\r
+ 0.078 197.211\r
+ 0.088 198.587\r
+ 0.126 198.587\r
+ 0.175 207.759\r
+ 0.217 211.887\r
+ 0.349 216.931\r
+ 0.401 221.059\r
+ 0.554 225.646\r
+ 0.586 228.856\r
+ 0.626 228.48\r
+ 0.65 230.232\r
+ 1.013 231.607\r
+ 1.105 230.691\r
+ 1.2 225.187\r
+ 1.356 210.511\r
+ 1.392 207.759\r
+ 1.441 206.842\r
+ 1.457 205.007\r
+ 1.519 181.159\r
+ 1.563 155.936\r
+ 1.693 49.992\r
+ 1.727 28.435\r
+ 1.756 16.512\r
+ 1.798 7.338\r
+ 1.86 1.376\r
+ 1.89 0.0\r
--- /dev/null
+;Animal Motor Works 38-390\r
+I220SK 38 249 20 0.202 0.495 AMW\r
+ 0.0050 12.747\r
+ 0.019 45.25\r
+ 0.036 79.666\r
+ 0.052 125.554\r
+ 0.069 162.519\r
+ 0.076 169.53\r
+ 0.095 174.629\r
+ 0.167 176.541\r
+ 0.229 191.199\r
+ 0.447 235.175\r
+ 0.602 260.668\r
+ 0.733 288.073\r
+ 0.85 302.095\r
+ 0.974 301.457\r
+ 1.094 289.985\r
+ 1.184 268.954\r
+ 1.268 240.273\r
+ 1.302 219.879\r
+ 1.388 177.178\r
+ 1.418 147.224\r
+ 1.435 127.467\r
+ 1.473 91.139\r
+ 1.504 65.645\r
+ 1.543 40.789\r
+ 1.593 19.12\r
+ 1.622 10.197\r
+ 1.65 0.0\r
--- /dev/null
+;\r
+; AMW 38-390\r
+I271BB 38 258 0 0.189 0.493 AMW\r
+0.011 119.530\r
+0.035 213.907\r
+0.050 245.903\r
+0.074 262.705\r
+0.115 269.446\r
+0.225 267.736\r
+0.346 282.929\r
+0.465 296.411\r
+0.584 303.152\r
+0.727 311.504\r
+0.916 318.245\r
+1.054 324.986\r
+1.162 331.400\r
+1.201 326.696\r
+1.225 313.214\r
+1.242 286.249\r
+1.268 240.990\r
+1.294 188.888\r
+1.323 136.833\r
+1.346 87.565\r
+1.368 45.467\r
+1.392 18.523\r
+1.430 0.000\r
+;\r
--- /dev/null
+;\r
+; AMW 38-390\r
+I285GG 38 258 0 0.206 0.515 AMW\r
+0.013 61.575\r
+0.032 119.327\r
+0.055 164.575\r
+0.076 191.004\r
+0.094 201.014\r
+0.139 212.326\r
+0.232 231.247\r
+0.357 258.876\r
+0.456 267.686\r
+0.592 278.998\r
+0.716 289.358\r
+0.841 291.200\r
+0.936 290.310\r
+1.051 285.204\r
+1.139 277.696\r
+1.204 280.199\r
+1.243 278.998\r
+1.265 268.887\r
+1.286 242.559\r
+1.319 187.200\r
+1.359 134.443\r
+1.387 86.702\r
+1.407 52.776\r
+1.428 31.413\r
+1.448 16.337\r
+1.465 5.026\r
+1.480 0.000\r
+;\r
--- /dev/null
+; This file my be used or given away. All I ask is that this header \r
+; is maintained to give credit to NAR S&T. Thank you, Jack Kane\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited \r
+; number of points (32) allowed with wRASP up to v1.6.\r
+;Animal Motor Works 38-640 \r
+I315SK 38 369 20 0.3829 0.7166 AMW\r
+0.011 314.573\r
+0.030 312.796\r
+0.066 300.786\r
+0.084 300.502\r
+0.120 304.087\r
+0.175 312.998\r
+0.266 324.086\r
+0.356 332.224\r
+0.447 347.855\r
+0.538 371.972\r
+0.629 382.833\r
+0.719 385.552\r
+0.810 385.586\r
+0.901 384.836\r
+0.992 382.296\r
+1.082 378.323\r
+1.173 370.837\r
+1.264 357.564\r
+1.355 347.122\r
+1.445 328.332\r
+1.536 202.733\r
+1.627 90.867\r
+1.718 35.427\r
+1.808 8.192\r
+1.815 0.000\r
--- /dev/null
+;Animal Motor Works 38-640 \r
+I325WT 38 370 17 0.317 0.712 AMW\r
+ 0.014 68.710\r
+ 0.022 113.038\r
+ 0.026 153.671\r
+ 0.037 244.545\r
+ 0.045 299.216\r
+ 0.055 330.246\r
+ 0.065 350.194\r
+ 0.079 365.709\r
+ 0.094 376.79\r
+ 0.124 381.963\r
+ 0.185 373.836\r
+ 0.252 373.836\r
+ 0.35 381.224\r
+ 0.47 382.701\r
+ 0.622 388.611\r
+ 1.102 384.179\r
+ 1.366 364.971\r
+ 1.379 360.537\r
+ 1.415 331.724\r
+ 1.49 223.119\r
+ 1.505 211.298\r
+ 1.551 187.657\r
+ 1.592 162.538\r
+ 1.688 80.529\r
+ 1.726 50.978\r
+ 1.775 27.336\r
+ 1.806 16.993\r
+ 1.834 9.605\r
+ 1.901 0.0\r
--- /dev/null
+;\r
+;Animal Motor Works 38-640 \r
+I375GG 38 369 20 0.3936 0.7338 AMW\r
+0.013 223.878\r
+0.045 273.929\r
+0.092 312.421\r
+0.140 334.383\r
+0.219 357.983\r
+0.298 381.992\r
+0.377 410.267\r
+0.457 431.141\r
+0.536 454.458\r
+0.615 476.825\r
+0.694 495.473\r
+0.773 504.665\r
+0.852 510.942\r
+0.931 511.972\r
+1.011 489.639\r
+1.090 441.350\r
+1.169 392.762\r
+1.248 354.753\r
+1.327 292.385\r
+1.406 177.309\r
+1.486 63.879\r
+1.565 14.901\r
+1.583 0.000\r
--- /dev/null
+; AMW Animal Motor Works fixed by dberez 12/08/03\r
+;\r
+;Animal Motor Works J357 White Wolf\r
+J357WW 54 326 0 0.5481 1.2101 AMW\r
+0.02 129.64\r
+0.03 205.95\r
+0.05 265.00\r
+0.06 316.51\r
+0.09 326.05\r
+0.13 314.60\r
+0.18 301.25\r
+0.24 299.34\r
+0.35 312.69\r
+0.50 326.05\r
+0.66 333.68\r
+0.87 345.13\r
+1.07 358.48\r
+1.46 383.18\r
+1.77 398.45\r
+1.86 400.36\r
+1.98 402.35\r
+2.18 398.45\r
+2.29 390.82\r
+2.41 369.93\r
+2.51 354.67\r
+2.55 352.76\r
+2.60 347.03\r
+2.65 335.59\r
+2.69 310.79\r
+2.75 249.73\r
+2.81 175.43\r
+2.84 108.65\r
+2.90 53.38\r
+2.92 20.98\r
+2.95 0.00\r
--- /dev/null
+;\r
+;Animal Motor Works 54-1400 \r
+J365SK 54 403 0 0.7571 1.4593 AMW\r
+0.029 389.731\r
+0.123 360.219\r
+0.218 334.200\r
+0.376 326.150\r
+0.534 334.217\r
+0.692 341.669\r
+0.850 347.676\r
+1.007 359.408\r
+1.165 370.043\r
+1.323 383.343\r
+1.481 399.248\r
+1.639 417.477\r
+1.797 443.735\r
+1.955 472.683\r
+2.112 501.668\r
+2.270 497.077\r
+2.428 425.371\r
+2.586 349.017\r
+2.744 262.068\r
+2.902 107.073\r
+3.060 41.821\r
+3.157 0.000\r
--- /dev/null
+;\r
+;Animal Motor Works 54-1050\r
+;AMW J370GG RASP.ENG file made from NAR data\r
+;File produced FEB 20, 2003\r
+;This file my be used or given away. All I ask is that this header\r
+;is maintained to give credit to NAR S&T. Thank you, Jack Kane\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+J370GG 54 326 100 0.5983 1.2491 Animal_Motor_Works \r
+0.008 185.496\r
+0.024 149.516\r
+0.063 225.273\r
+0.087 272.647\r
+0.122 304.829\r
+0.158 304.829\r
+0.273 335.212\r
+0.431 363.796\r
+0.573 390.381\r
+0.707 413.168\r
+0.877 428.459\r
+1.019 441.852\r
+1.126 441.852\r
+1.224 458.46\r
+1.284 443.951\r
+1.386 440.153\r
+1.572 438.454\r
+1.651 438.554\r
+1.813 417.765\r
+2.022 404.673\r
+2.141 385.883\r
+2.212 385.883\r
+2.255 374.59\r
+2.299 387.882\r
+2.362 357.599\r
+2.401 384.184\r
+2.421 348.204\r
+2.457 316.122\r
+2.559 251.758\r
+2.697 115.635\r
+2.753 45.624\r
+2.82 0\r
--- /dev/null
+;\r
+;AMW J400 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+J400RR 54 326 100 0.558 1.2314 Animal_Motor_Works \r
+0.043 246.55\r
+0.06 317.381\r
+0.081 344.709\r
+0.107 358.372\r
+0.15 356.06\r
+0.201 365.204\r
+0.308 392.532\r
+0.568 435.734\r
+0.863 458.339\r
+1.094 469.24\r
+1.209 467.18\r
+1.466 460.148\r
+1.705 443.972\r
+1.923 423.275\r
+2.132 409.411\r
+2.303 413.831\r
+2.402 420.563\r
+2.47 413.831\r
+2.517 395.445\r
+2.543 347.421\r
+2.568 265.238\r
+2.598 128.198\r
+2.615 68.801\r
+2.632 25.398\r
+2.66 0\r
--- /dev/null
+;Animal Motor Works 38-640 \r
+J440BB 38 369 20 0.3853 0.6985 AMW\r
+0.007 468.505\r
+0.022 509.996\r
+0.037 527.687\r
+0.052 532.792\r
+0.082 530.181\r
+0.127 525.586\r
+0.202 521.566\r
+0.277 519.840\r
+0.352 521.522\r
+0.426 525.414\r
+0.501 531.248\r
+0.576 538.724\r
+0.651 541.761\r
+0.726 538.508\r
+0.801 531.072\r
+0.876 516.175\r
+0.950 494.942\r
+1.025 477.251\r
+1.100 433.297\r
+1.175 313.900\r
+1.250 187.467\r
+1.325 101.546\r
+1.400 45.751\r
+1.474 22.083\r
+1.497 0.000\r
--- /dev/null
+;\r
+;AMW J450 RASP.ENG file made from NAR published data\r
+; File produced SEPT 4, 2002\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+J450 54 326 P .5331 1.1964 AMW\r
+ 0.009 251.586\r
+ 0.016 376.074\r
+ 0.030 413.450\r
+ 0.051 430.832\r
+ 0.094 423.296\r
+ 0.162 413.149\r
+ 0.262 395.566\r
+ 0.402 420.182\r
+ 0.495 444.898\r
+ 0.805 504.078\r
+ 1.048 536.028\r
+ 1.223 550.597\r
+ 1.299 563.180\r
+ 1.334 555.319\r
+ 1.470 560.042\r
+ 1.588 559.841\r
+ 1.764 546.980\r
+ 1.921 516.838\r
+ 1.993 496.743\r
+ 2.025 499.154\r
+ 2.047 479.160\r
+ 2.086 414.354\r
+ 2.115 344.525\r
+ 2.141 252.290\r
+ 2.177 140.161\r
+ 2.213 82.780\r
+ 2.239 50.347\r
+ 2.271 27.861\r
+ 2.296 12.860\r
+ 2.330 0.000\r
--- /dev/null
+;\r
+;Animal Motor Works J450 Super Tiger\r
+J450ST 54 326 0 0.5331 1.1964 AMW\r
+0.009 251.586\r
+0.016 376.074\r
+0.030 413.450\r
+0.051 430.832\r
+0.094 423.296\r
+0.162 413.149\r
+0.262 395.566\r
+0.402 420.182\r
+0.495 444.898\r
+0.805 504.078\r
+1.048 536.028\r
+1.223 550.597\r
+1.299 563.180\r
+1.334 555.319\r
+1.470 560.042\r
+1.588 559.841\r
+1.764 546.980\r
+1.921 516.838\r
+1.993 496.743\r
+2.025 499.154\r
+2.047 479.160\r
+2.086 414.354\r
+2.115 344.525\r
+2.141 252.290\r
+2.177 140.161\r
+2.213 82.780\r
+2.239 50.347\r
+2.271 27.861\r
+2.296 12.860\r
+2.330 0.000\r
--- /dev/null
+;\r
+;AMW J480 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+J480BB 54 326 100 0.556 1.2131 Animal_Motor_Works \r
+0.015 225.429\r
+0.041 348.18\r
+0.071 388.127\r
+0.194 422.453\r
+0.385 459.49\r
+0.699 502.347\r
+0.968 528.042\r
+1.2 536.573\r
+1.454 543.15\r
+1.674 533.763\r
+1.887 522.321\r
+2.044 519.41\r
+2.108 525.131\r
+2.164 528.042\r
+2.197 488.095\r
+2.25 419.543\r
+2.283 333.928\r
+2.328 231.15\r
+2.354 176.95\r
+2.392 111.309\r
+2.418 68.501\r
+2.436 37.106\r
+2.49 0\r
--- /dev/null
+;\r
+;J500ST entered by Tim Van Milligan\r
+;For RockSim - http://www.rocksim.com\r
+;Based on TRA Certification paperwork from 06-01-2002\r
+;Initial Mass from Jim Robinson at AMW\r
+;Not approved by TRA or AMW.\r
+J500ST 38 370 20 0.3265 0.744 Animal_Motor_Works \r
+0.006 444.822\r
+0.025 475.651\r
+0.04 418.397\r
+0.053 466.843\r
+0.059 409.589\r
+0.071 458.035\r
+0.077 409.589\r
+0.1 444.822\r
+0.127 506.481\r
+0.204 590.16\r
+0.25 644.992\r
+0.3 678.244\r
+0.34 709.073\r
+0.402 735.498\r
+0.445 766.327\r
+0.516 783.944\r
+0.6 787.335\r
+0.637 770.732\r
+0.68 744.306\r
+0.76 620.989\r
+0.859 475.651\r
+1.00464 303.888\r
+1.122 171.763\r
+1.227 52.8502\r
+1.3 0\r
--- /dev/null
+;\r
+;AMW K1000 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+K1000SK 54 728 100 1.297 2.556 Animal_Motor_Works \r
+0.019 1155.06\r
+0.045 1426.12\r
+0.094 1248.23\r
+0.161 1112.99\r
+0.239 1128.02\r
+0.343 1113.99\r
+0.377 1149.05\r
+0.44 1121\r
+0.544 1221.18\r
+0.633 1178.11\r
+0.674 1221.18\r
+0.737 1193.13\r
+0.883 1200.14\r
+1.009 1194.13\r
+1.057 1236.21\r
+1.188 1137.03\r
+1.299 1145.05\r
+1.396 1087.94\r
+1.516 954.104\r
+1.631 855.228\r
+1.717 827.077\r
+1.777 650.061\r
+1.848 465.932\r
+1.93 303.141\r
+2.023 147.463\r
+2.083 83.879\r
+2.132 41.484\r
+2.18 0\r
--- /dev/null
+;\r
+;Animal Motor Works K1075 RASP.ENG file made from NAR data\r
+;File produced Feb 22, 2003\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+K1075GG 54 726 100 1.3999 2.6658 Animal_Motor_Works \r
+0.009 672.664\r
+0.015 963.511\r
+0.022 860.518\r
+0.047 987.857\r
+0.075 975.835\r
+0.106 921.332\r
+0.215 958.001\r
+0.529 1092.05\r
+0.878 1220.29\r
+1.077 1269.39\r
+1.158 1311.47\r
+1.235 1293.43\r
+1.448 1330.5\r
+1.577 1318.48\r
+1.672 1319.48\r
+1.721 1337.52\r
+1.759 1337.52\r
+1.805 1337.52\r
+1.829 1331.5\r
+1.856 1384.67\r
+1.889 1277.4\r
+1.906 1216.29\r
+1.938 1052.98\r
+1.96 871.338\r
+1.988 659.239\r
+2.027 453.352\r
+2.062 301.967\r
+2.115 138.46\r
+2.168 41.608\r
+2.2 0\r
--- /dev/null
+;\r
+;AMW K365RR RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+K365RR 75 111 100 0.946 2.3456 Animal_Motor_Works \r
+0.049 138.157\r
+0.068 381.241\r
+0.084 454.75\r
+0.106 481.536\r
+0.164 488.182\r
+0.291 514.867\r
+0.435 545.982\r
+0.666 561.49\r
+0.868 565.73\r
+1.082 565.518\r
+1.296 550.111\r
+1.591 529.871\r
+1.805 509.731\r
+1.828 536.517\r
+1.886 498.554\r
+2.124 467.237\r
+2.501 411.35\r
+2.924 328.677\r
+3.296 241.573\r
+3.638 172.293\r
+3.969 100.798\r
+4.195 56.098\r
+4.265 51.607\r
+4.346 35.959\r
+4.433 15.859\r
+4.51 0\r
--- /dev/null
+;\r
+;AMW K450BB RASP.ENG file made from NAR published data\r
+;File produced Aug 19, 2003\r
+;This file my be used or given away. All I ask is that this header\r
+;is maintained to give credit to NAR S&T. Thank you, Jack Kane.\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+K450BB 75 302 100 0.8816 2.8349 Animal_Motor_Works \r
+0.03 78.903\r
+0.045 227.9\r
+0.064 449.955\r
+0.069 508.417\r
+0.094 555.187\r
+0.151 563.956\r
+0.362 625.442\r
+0.562 651.85\r
+0.825 660.62\r
+1.134 652.254\r
+1.453 626.147\r
+1.793 594.296\r
+2.113 538.958\r
+2.458 469.106\r
+2.798 384.538\r
+3.165 276.686\r
+3.201 279.609\r
+3.325 232.94\r
+3.51 171.757\r
+3.732 107.65\r
+3.861 58.018\r
+3.959 40.55\r
+4.036 20.149\r
+4.11 0\r
--- /dev/null
+;\r
+;AMW K470ST RASP.ENG file made from Tripoli published data\r
+;File produced May 15, 2004\r
+;This file my be used or given away. All I ask is that this header\r
+;is maintained to give credit to the people who produced the data.\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+K470ST 75 302 100 0.826 2.779 Animal_Motor_Works \r
+0.028 699.309\r
+0.039 799.337\r
+0.09 765.845\r
+0.157 770.311\r
+0.258 785.941\r
+0.41 804\r
+0.572 794.425\r
+0.707 794.425\r
+0.886 792.192\r
+0.998 783.261\r
+1.15 752.002\r
+1.318 709.579\r
+1.447 655.992\r
+1.593 595.707\r
+1.728 522.025\r
+1.885 444.101\r
+2.092 354.923\r
+2.356 270.167\r
+2.664 187.554\r
+2.945 131.734\r
+3.27 78.058\r
+3.433 55.686\r
+3.478 48.987\r
+3.556 28.909\r
+3.7 0\r
--- /dev/null
+;\r
+;Animal Motor Works K475 RASP.ENG file made from NAR data\r
+;File produced Feb 22, 2003\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+K475WW 54 403 100 0.7286 1.4925 Animal_Motor_Works \r
+0.022 127.831\r
+0.041 386.016\r
+0.063 548.326\r
+0.096 521.308\r
+0.134 499.129\r
+0.18 486.83\r
+0.285 486.83\r
+0.478 501.649\r
+0.731 523.727\r
+1.096 553.266\r
+1.433 577.962\r
+1.601 588.29\r
+1.756 582.704\r
+1.895 580.284\r
+1.958 575.344\r
+2.063 550.746\r
+2.209 518.788\r
+2.344 477.051\r
+2.495 417.974\r
+2.561 354.058\r
+2.582 334.399\r
+2.599 331.98\r
+2.62 297.501\r
+2.67 226.226\r
+2.707 157.37\r
+2.74 98.353\r
+2.799 49.176\r
+2.853 17.208\r
+2.94 0\r
--- /dev/null
+;\r
+;Animal Motor Works 54-1400\r
+;AMW K530GG RASP.ENG file made from NAR data\r
+;File produced Feb 25, 2003\r
+;This file my be used or given away. All I ask is that this header\r
+;is maintained to give credit to NAR S&T. Thank you, Jack Kane\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+K530GG 54 403 1000 0.7967 1.616 Animal_Motor_Works \r
+0.013 129.764\r
+0.054 171.852\r
+0.096 284.122\r
+0.138 392.892\r
+0.171 455.975\r
+0.217 501.662\r
+0.238 498.063\r
+0.326 508.66\r
+0.542 564.745\r
+0.755 613.831\r
+1.01 645.423\r
+1.17 657.23\r
+1.273 648.922\r
+1.51 638.425\r
+1.656 634.925\r
+1.702 606.833\r
+1.803 606.833\r
+1.857 585.839\r
+1.936 589.338\r
+1.974 575.242\r
+2.015 589.338\r
+2.04 564.745\r
+2.132 536.652\r
+2.207 540.251\r
+2.291 522.656\r
+2.357 487.566\r
+2.42 375.297\r
+2.478 242.033\r
+2.529 140.361\r
+2.583 66.651\r
+2.66 0\r
--- /dev/null
+;Animal Motor Works 54-1750 K555 skidmark\r
+;File provide by Joel Rogers of AMW\r
+K555SK 54 492 0 0.8707 1.7343 AMW\r
+0.063 507.328\r
+0.144 535.181\r
+0.226 559.826\r
+0.308 585.793\r
+0.389 607.239\r
+0.471 629.034\r
+0.553 664.586\r
+0.634 683.688\r
+0.716 697.625\r
+0.798 719.618\r
+0.879 756.521\r
+0.961 777.700\r
+1.043 789.004\r
+1.124 797.934\r
+1.206 801.689\r
+1.288 804.331\r
+1.369 799.414\r
+1.451 768.014\r
+1.533 704.469\r
+1.614 641.709\r
+1.696 568.727\r
+1.778 481.013\r
+1.859 401.614\r
+1.941 333.897\r
+2.023 277.226\r
+2.104 205.009\r
+2.186 129.425\r
+2.268 73.717\r
+2.349 22.380\r
+2.368 0.000\r
--- /dev/null
+;\r
+;AMW K560 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+K560RR 54 430 100 0.75 1.5866 Animal_Motor_Works \r
+0.023 229.13\r
+0.046 415.135\r
+0.059 485.264\r
+0.078 512.268\r
+0.106 525.67\r
+0.154 523.05\r
+0.211 528.39\r
+0.261 536.451\r
+0.369 560.734\r
+0.511 587.738\r
+0.657 603.86\r
+0.77 612.022\r
+1.096 625.75\r
+1.358 620.083\r
+1.627 612.022\r
+1.839 603.86\r
+2.057 590.459\r
+2.218 598.52\r
+2.335 609.301\r
+2.385 601.24\r
+2.407 585.018\r
+2.426 533.831\r
+2.467 385.511\r
+2.507 283.037\r
+2.542 164.441\r
+2.576 67.399\r
+2.595 29.653\r
+2.62 0\r
--- /dev/null
+;\r
+;Animal Motor Works K570 White Wolf\r
+K570WW 54 492 0 0.9146 1.8151 AMW\r
+0.020 364.42\r
+0.030 664.79\r
+0.051 751.47\r
+0.071 745.81\r
+0.096 705.25\r
+0.137 674.93\r
+0.284 661.38\r
+0.528 651.24\r
+0.913 644.51\r
+1.192 651.24\r
+1.430 651.24\r
+1.649 651.24\r
+1.872 644.51\r
+2.176 624.23\r
+2.318 600.64\r
+2.394 597.33\r
+2.455 546.63\r
+2.501 485.89\r
+2.562 421.84\r
+2.597 340.83\r
+2.638 266.54\r
+2.734 175.48\r
+2.836 97.86\r
+2.927 47.24\r
+3.040 0.00\r
--- /dev/null
+;\r
+; Animal Motor Works K600 RASP.ENG file made from NAR data\r
+; File produced August 22, 2002\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+K600 75 368 P 1.2233 2.9129 AMW\r
+ 0.010 412.229\r
+ 0.029 522.21\r
+ 0.059 547.215\r
+ 0.083 524.8\r
+ 0.122 497.305\r
+ 0.181 484.852\r
+ 0.333 495.113\r
+ 0.690 560.464\r
+ 1.195 643.548\r
+ 1.400 673.833\r
+ 1.420 708.799\r
+ 1.508 701.427\r
+ 1.591 721.551\r
+ 1.782 731.712\r
+ 2.017 752.035\r
+ 2.174 756.816\r
+ 2.257 765.2\r
+ 2.502 766.44\r
+ 2.727 752.931\r
+ 2.918 738.187\r
+ 3.143 705.91\r
+ 3.408 643.847\r
+ 3.603 569.131\r
+ 3.692 526.793\r
+ 3.745 439.426\r
+ 3.799 289.596\r
+ 3.883 112.272\r
+ 3.922 64.862\r
+ 3.971 37.437\r
+ 3.995 22.474\r
+ 4.070 0\r
--- /dev/null
+;\r
+;Animal Motor Works K600 White Wolf\r
+K600WW 75 368 0 1.2233 2.9129 AMW\r
+0.010 412.229\r
+0.029 522.21\r
+0.059 547.215\r
+0.083 524.8\r
+0.122 497.305\r
+0.181 484.852\r
+0.333 495.113\r
+0.690 560.464\r
+1.195 643.548\r
+1.400 673.833\r
+1.420 708.799\r
+1.508 701.427\r
+1.591 721.551\r
+1.782 731.712\r
+2.017 752.035\r
+2.174 756.816\r
+2.257 765.2\r
+2.502 766.44\r
+2.727 752.931\r
+2.918 738.187\r
+3.143 705.91\r
+3.408 643.847\r
+3.603 569.131\r
+3.692 526.793\r
+3.745 439.426\r
+3.799 289.596\r
+3.883 112.272\r
+3.922 64.862\r
+3.971 37.437\r
+3.995 22.474\r
+4.070 0\r
--- /dev/null
+;\r
+;AMW K605 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+K605RR 75 368 100 1.231 2.7688 Animal_Motor_Works \r
+0.03 165.845\r
+0.053 309.12\r
+0.077 361.916\r
+0.142 392.042\r
+0.527 501.412\r
+0.988 606.905\r
+1.515 682.37\r
+2.101 730.593\r
+2.355 737.58\r
+2.692 731.289\r
+3 712.497\r
+3.361 671.036\r
+3.503 663.479\r
+3.586 659.701\r
+3.645 633.353\r
+3.692 573\r
+3.734 444.838\r
+3.775 297.785\r
+3.828 162.066\r
+3.864 98.015\r
+3.905 41.471\r
+3.95 0\r
--- /dev/null
+; Animal Motor Works 54-1750\r
+; AMW K650RR RASP.ENG file made from NAR published data\r
+; File produced April 19, 2004\r
+; This file my be used or given away. All I ask is that this header \r
+; is maintained to give credit to NAR S&T. Thank you, Jack Kane\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited \r
+; number of points (32) allowed with wRASP up to v1.6.\r
+K650RR 54 492 0 0.931 1.8087 AMW\r
+0.022 308.257\r
+0.045 566.480\r
+0.058 620.440\r
+0.081 639.668\r
+0.135 639.668\r
+0.229 643.494\r
+0.351 662.823\r
+0.594 701.380\r
+0.810 724.434\r
+0.999 743.763\r
+1.151 751.220\r
+1.381 747.588\r
+1.610 736.001\r
+1.835 709.031\r
+2.073 685.876\r
+2.244 674.400\r
+2.334 682.051\r
+2.429 685.876\r
+2.469 666.648\r
+2.528 597.285\r
+2.573 481.714\r
+2.609 358.391\r
+2.631 250.471\r
+2.681 146.477\r
+2.721 65.507\r
+2.748 23.124\r
+2.770 0.000\r
--- /dev/null
+;\r
+; AMW K670 RASP.ENG file made from NAR published data\r
+; File produced SEPT 4, 2002\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+K670 54 492 P- 1.0140 1.9145 AMW\r
+ 0.016 294.05\r
+ 0.035 398.577\r
+ 0.086 506.292\r
+ 0.153 496.428\r
+ 0.264 506.093\r
+ 0.461 558.108\r
+ 0.722 629.553\r
+ 0.983 688.044\r
+ 1.116 714.051\r
+ 1.193 785.795\r
+ 1.409 788.784\r
+ 1.737 804.56\r
+ 2.074 781.41\r
+ 2.195 764.87\r
+ 2.226 781.211\r
+ 2.277 764.77\r
+ 2.398 751.517\r
+ 2.440 744.941\r
+ 2.468 718.834\r
+ 2.484 666.521\r
+ 2.525 418.107\r
+ 2.551 218.818\r
+ 2.573 120.768\r
+ 2.595 52.143\r
+ 2.620 0\r
--- /dev/null
+;\r
+;Animal Motor Works K670 Green Gorilla\r
+K670GG 54 492 0 1.0140 1.9145 AMW\r
+0.016 294.05\r
+0.035 398.577\r
+0.086 506.292\r
+0.153 496.428\r
+0.264 506.093\r
+0.461 558.108\r
+0.722 629.553\r
+0.983 688.044\r
+1.116 714.051\r
+1.193 785.795\r
+1.409 788.784\r
+1.737 804.56\r
+2.074 781.41\r
+2.195 764.87\r
+2.226 781.211\r
+2.277 764.77\r
+2.398 751.517\r
+2.440 744.941\r
+2.468 718.834\r
+2.484 666.521\r
+2.525 418.107\r
+2.551 218.818\r
+2.573 120.768\r
+2.595 52.143\r
+2.620 0\r
--- /dev/null
+;\r
+;AMW K700 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+K700BB 54 430 100 0.754 1.4831 Animal_Motor_Works \r
+0.014 359.559\r
+0.022 625.425\r
+0.03 737.756\r
+0.047 771.505\r
+0.082 786.516\r
+0.106 771.505\r
+0.144 775.233\r
+0.272 786.516\r
+0.477 812.71\r
+0.693 842.632\r
+0.97 847.06\r
+1.283 838.904\r
+1.516 816.438\r
+1.706 801.427\r
+1.779 793.972\r
+1.811 775.233\r
+1.841 726.573\r
+1.873 625.425\r
+1.909 509.367\r
+1.95 393.208\r
+1.982 337.093\r
+2.035 292.16\r
+2.073 228.489\r
+2.111 153.535\r
+2.155 86.137\r
+2.193 37.446\r
+2.24 0\r
--- /dev/null
+; This file my be used or given away. All I ask is that this header \r
+; is maintained to give credit to NAR S&T. Thank you, Jack Kane\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited \r
+; number of points (32) allowed with wRASP up to v1.6.\r
+K800BB 54 492 0 0.9140 1.7866 AMW\r
+0.017 516.316\r
+0.035 745.845\r
+0.046 817.592\r
+0.090 860.560\r
+0.191 889.338\r
+0.270 908.424\r
+0.438 918.017\r
+0.689 945.892\r
+0.996 955.090\r
+1.325 922.713\r
+1.557 894.035\r
+1.726 874.949\r
+1.849 884.542\r
+1.920 894.035\r
+1.954 894.035\r
+1.984 855.863\r
+2.011 741.048\r
+2.049 592.859\r
+2.079 492.433\r
+2.113 430.280\r
+2.154 377.719\r
+2.196 329.854\r
+2.237 243.818\r
+2.275 152.986\r
+2.309 71.716\r
+2.339 33.465\r
+2.380 0.000\r
--- /dev/null
+;\r
+; AMW K950 RASP.ENG file made from NAR published data\r
+; File produced SEPT 4, 2002\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+K950 54 492 P .8874 1.7949 AMW\r
+ 0.011 771.836\r
+ 0.025 1204.520\r
+ 0.039 1083.244\r
+ 0.053 1158.054\r
+ 0.067 1036.364\r
+ 0.085 1110.176\r
+ 0.099 1022.399\r
+ 0.135 982.102\r
+ 0.220 968.835\r
+ 0.404 1010.430\r
+ 0.566 1044.343\r
+ 0.701 1079.254\r
+ 0.867 1106.186\r
+ 0.995 1134.115\r
+ 1.211 1114.166\r
+ 1.313 1101.199\r
+ 1.430 1067.285\r
+ 1.529 1020.404\r
+ 1.579 993.772\r
+ 1.642 892.430\r
+ 1.674 818.119\r
+ 1.717 757.273\r
+ 1.738 621.918\r
+ 1.766 466.313\r
+ 1.791 351.306\r
+ 1.823 249.864\r
+ 1.865 175.553\r
+ 1.908 87.696\r
+ 1.943 33.654\r
+ 1.970 0.000\r
--- /dev/null
+;\r
+;Animal Motor Works K950 Super Tiger\r
+K950ST 54 492 0 .8874 1.7949 AMW\r
+0.011 771.836\r
+0.025 1204.520\r
+0.039 1083.244\r
+0.053 1158.054\r
+0.067 1036.364\r
+0.085 1110.176\r
+0.099 1022.399\r
+0.135 982.102\r
+0.220 968.835\r
+0.404 1010.430\r
+0.566 1044.343\r
+0.701 1079.254\r
+0.867 1106.186\r
+0.995 1134.115\r
+1.211 1114.166\r
+1.313 1101.199\r
+1.430 1067.285\r
+1.529 1020.404\r
+1.579 993.772\r
+1.642 892.430\r
+1.674 818.119\r
+1.717 757.273\r
+1.738 621.918\r
+1.766 466.313\r
+1.791 351.306\r
+1.823 249.864\r
+1.865 175.553\r
+1.908 87.696\r
+1.943 33.654\r
+1.970 0.000\r
--- /dev/null
+;\r
+;Animal Motor Works K975 White Wolf\r
+K975WW 54 728 0 1.357 2.5985 AMW\r
+0.017 526.644\r
+0.029 901.850\r
+0.038 1098.918\r
+0.046 1151.722\r
+0.076 1112.867\r
+0.130 1060.063\r
+0.219 1053.089\r
+0.336 1053.089\r
+0.479 1059.066\r
+0.609 1091.944\r
+0.866 1136.778\r
+1.046 1176.630\r
+1.164 1175.634\r
+1.202 1228.437\r
+1.239 1208.511\r
+1.315 1215.486\r
+1.353 1267.293\r
+1.387 1228.437\r
+1.487 1241.389\r
+1.538 1260.319\r
+1.634 1290.900\r
+1.723 1281.241\r
+1.794 1266.297\r
+1.836 1207.515\r
+1.933 1049.103\r
+1.992 851.437\r
+2.080 666.923\r
+2.118 640.521\r
+2.193 462.582\r
+2.269 212.311\r
+2.378 119.854\r
+2.510 0.000\r
--- /dev/null
+;\r
+; AMW L1060 RASP.ENG file made from NAR published data\r
+; File produced August 22, 2002\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+L1060 75 497 P- 1.9188 3.9388 AMW\r
+ 0.020 258.773\r
+ 0.024 368.235\r
+ 0.032 328.386\r
+ 0.076 427.96\r
+ 0.100 567.284\r
+ 0.116 751.352\r
+ 0.128 791.202\r
+ 0.169 816.071\r
+ 0.225 816.071\r
+ 0.309 875.795\r
+ 0.518 985.257\r
+ 0.763 1079.639\r
+ 1.024 1174.519\r
+ 1.308 1229.45\r
+ 1.606 1288.375\r
+ 1.782 1298.25\r
+ 1.983 1293.369\r
+ 2.256 1239.437\r
+ 2.525 1184.506\r
+ 2.822 1129.576\r
+ 3.038 1069.651\r
+ 3.111 1044.683\r
+ 3.135 995.145\r
+ 3.183 835.946\r
+ 3.239 552.303\r
+ 3.299 268.661\r
+ 3.327 164.193\r
+ 3.339 84.593\r
+ 3.360 44.783\r
+ 3.400 0\r
--- /dev/null
+;\r
+;Animal Motor Works L1060 Green Gorilla\r
+L1060GG 75 497 0 1.9188 3.9388 AMW\r
+0.020 258.773\r
+0.024 368.235\r
+0.032 328.386\r
+0.076 427.96\r
+0.100 567.284\r
+0.116 751.352\r
+0.128 791.202\r
+0.169 816.071\r
+0.225 816.071\r
+0.309 875.795\r
+0.518 985.257\r
+0.763 1079.639\r
+1.024 1174.519\r
+1.308 1229.45\r
+1.606 1288.375\r
+1.782 1298.25\r
+1.983 1293.369\r
+2.256 1239.437\r
+2.525 1184.506\r
+2.822 1129.576\r
+3.038 1069.651\r
+3.111 1044.683\r
+3.135 995.145\r
+3.183 835.946\r
+3.239 552.303\r
+3.299 268.661\r
+3.327 164.193\r
+3.339 84.593\r
+3.360 44.783\r
+3.400 0\r
--- /dev/null
+;\r
+;AMW L1080 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+L1080BB 75 497 100 1.717 3.5922 Animal_Motor_Works \r
+0.024 406.295\r
+0.043 812.489\r
+0.052 895.202\r
+0.088 929.641\r
+0.314 991.55\r
+0.626 1087.69\r
+0.988 1163.44\r
+1.346 1218.99\r
+1.638 1246.25\r
+1.864 1257.91\r
+2.247 1254.84\r
+2.6 1218.99\r
+2.766 1211.92\r
+2.851 1197.78\r
+2.942 1204.85\r
+3.002 1226.06\r
+3.033 1204.85\r
+3.089 1040.23\r
+3.124 874.499\r
+3.15 660.999\r
+3.191 461.336\r
+3.232 275.408\r
+3.268 144.622\r
+3.303 75.744\r
+3.339 41.316\r
+3.39 0\r
--- /dev/null
+;\r
+;AMW L1100 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+L1100RR 75 728 100 1.346 2.5881 Animal_Motor_Works \r
+0.013 681.489\r
+0.029 1116.88\r
+0.041 1196.3\r
+0.079 1210.38\r
+0.147 1203.34\r
+0.257 1218.42\r
+0.366 1225.45\r
+0.567 1254.61\r
+0.824 1282.76\r
+1.059 1311.91\r
+1.267 1340.23\r
+1.459 1311.91\r
+1.622 1297.84\r
+1.713 1290.8\r
+1.785 1268.68\r
+1.83 1218.42\r
+1.886 1080.69\r
+1.969 819.214\r
+2.048 558.24\r
+2.108 376.985\r
+2.156 246.498\r
+2.205 144.963\r
+2.269 72.501\r
+2.35 0\r
--- /dev/null
+;\r
+;L1111ST entered by Tim Van Milligan\r
+;For RockSim - http://www.rocksim.com\r
+;Based on TRA Certification paperwork from 06-01-2002\r
+;Initial Mass from Jim Robinson at AMW\r
+;Not approved by TRA or AMW.\r
+L1111ST 75 497 100 1.642 3.517 Animal_Motor_Works \r
+0.015 1023.97\r
+0.1 924.878\r
+0.147 902.857\r
+0.502 1034.98\r
+0.75 1156.1\r
+1.005 1266.2\r
+1.229 1354.29\r
+1.492 1398.33\r
+1.739 1398.33\r
+2.009 1354.29\r
+2.272 1244.18\r
+2.504 1123.07\r
+2.728 968.92\r
+2.782 902.857\r
+2.836 770.732\r
+2.98 363.345\r
+3.053 99.094\r
+3.083 22.021\r
+3.14 0\r
--- /dev/null
+;\r
+;AMW L1300 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+L1300BB 75 728 100 1.314 2.5454 Animal_Motor_Works \r
+0.014 710.467\r
+0.025 1247.64\r
+0.039 1384.13\r
+0.053 1447.83\r
+0.074 1420.53\r
+0.12 1447.83\r
+0.276 1474.12\r
+0.475 1519.61\r
+0.712 1555\r
+0.942 1586.74\r
+1.147 1562.08\r
+1.36 1534.78\r
+1.484 1551.97\r
+1.537 1551.97\r
+1.569 1497.37\r
+1.59 1406.38\r
+1.604 1451.87\r
+1.615 1333.58\r
+1.64 1168.78\r
+1.689 986.687\r
+1.753 767.749\r
+1.824 512.503\r
+1.891 275.512\r
+1.933 147.816\r
+1.987 74.737\r
+2.06 0\r
--- /dev/null
+; @File: SK-75-6000.txt, @Pts-I: 3609, @Pts-O: 31, @Sm: 6, @CO: 5%\r
+; @TI: 4740.56, @TIa: 4732.91, @TIe: 0.0%, @ThMax: 1908.398, @ThAvg: 1382.678, @Tb: 3.423\r
+; Exported using ThrustCurveTool, www.ThrustGear.com, by John DeMar\r
+L1400SK 75 785 P 2.8267 5.1985 AMW\r
+ 0.0 68.1234\r
+ 0.0040 193.7893\r
+ 0.016 690.259\r
+ 0.021 814.579\r
+ 0.027 900.741\r
+ 0.045 997.475\r
+ 0.076 1251.156\r
+ 0.092 1354.553\r
+ 0.107 1405.971\r
+ 0.132 1440.082\r
+ 0.169 1453.774\r
+ 0.368 1397.446\r
+ 0.525 1411.875\r
+ 0.705 1488.288\r
+ 1.082 1734.489\r
+ 1.414 1906.629\r
+ 1.556 1875.238\r
+ 1.766 1882.261\r
+ 1.899 1803.008\r
+ 2.142 1745.497\r
+ 2.34 1659.082\r
+ 2.504 1522.458\r
+ 2.58 1402.287\r
+ 2.819 844.839\r
+ 2.847 841.674\r
+ 2.893 730.795\r
+ 3.068 406.536\r
+ 3.176 265.8\r
+ 3.425 94.9644\r
+ 3.608 0.874524\r
+ 3.609 0\r
--- /dev/null
+;Animal Motor Works 75-3500\r
+L666SK 75 497 0 1.8877 3.5344 AMW\r
+0.096 105.880\r
+0.175 509.783\r
+0.312 549.481\r
+0.449 577.319\r
+0.586 602.900\r
+0.722 615.605\r
+0.859 632.540\r
+0.996 652.072\r
+1.133 671.418\r
+1.270 685.671\r
+1.407 701.286\r
+1.543 718.069\r
+1.680 734.116\r
+1.817 753.292\r
+1.954 771.589\r
+2.091 790.453\r
+2.228 819.222\r
+2.364 846.663\r
+2.501 874.629\r
+2.638 890.083\r
+2.775 898.271\r
+2.912 899.312\r
+3.049 881.683\r
+3.185 845.157\r
+3.322 768.451\r
+3.459 672.771\r
+3.596 525.466\r
+3.733 304.694\r
+3.870 86.663\r
+3.968 0.000\r
+;\r
+;\r
--- /dev/null
+;\r
+;\r
+L700BB 75.0 368.00 100 1.19310 2.73200 AMW\r
+ 0.02 221.87 \r
+ 0.03 399.33 \r
+ 0.05 467.56 \r
+ 0.08 494.89 \r
+ 0.13 498.41 \r
+ 0.24 535.99 \r
+ 0.48 614.67 \r
+ 0.77 683.20 \r
+ 1.23 755.25 \r
+ 1.62 789.72 \r
+ 1.92 810.42 \r
+ 2.26 821.14 \r
+ 2.58 817.85 \r
+ 2.91 801.07 \r
+ 3.14 773.94 \r
+ 3.25 750.13 \r
+ 3.32 743.39 \r
+ 3.37 729.83 \r
+ 3.42 688.83 \r
+ 3.46 593.37 \r
+ 3.50 484.14 \r
+ 3.53 368.18 \r
+ 3.57 248.80 \r
+ 3.62 149.82 \r
+ 3.66 61.13 \r
+ 3.72 0.00 \r
--- /dev/null
+;\r
+; AMW L777 RASP.ENG file made from NAR published data\r
+; File produced SEPT 4, 2002\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+L777 75 497 P 1.7623 3.6987 AMW\r
+ 0.025 140.882\r
+ 0.064 209.474\r
+ 0.108 360.055\r
+ 0.204 652.185\r
+ 0.360 641.518\r
+ 0.373 693.745\r
+ 0.418 683.28\r
+ 0.528 730.073\r
+ 0.670 761.268\r
+ 0.761 781.998\r
+ 0.787 802.828\r
+ 0.871 802.728\r
+ 1.065 854.754\r
+ 1.338 911.811\r
+ 1.668 963.636\r
+ 1.914 989.498\r
+ 2.115 1000.16\r
+ 2.368 962.831\r
+ 2.647 926\r
+ 2.985 878.603\r
+ 3.303 805.143\r
+ 3.472 752.815\r
+ 3.550 705.72\r
+ 3.602 648.26\r
+ 3.647 611.631\r
+ 3.693 512.409\r
+ 3.779 334.897\r
+ 3.857 178.216\r
+ 3.935 89.379\r
+ 3.981 26.687\r
+ 4.050 0\r
--- /dev/null
+;\r
+;Animal Motor Works L777 White Wolf\r
+L777WW 75 497 0 1.7623 3.6987 AMW\r
+0.025 140.882\r
+0.064 209.474\r
+0.108 360.055\r
+0.204 652.185\r
+0.360 641.518\r
+0.373 693.745\r
+0.418 683.28\r
+0.528 730.073\r
+0.670 761.268\r
+0.761 781.998\r
+0.787 802.828\r
+0.871 802.728\r
+1.065 854.754\r
+1.338 911.811\r
+1.668 963.636\r
+1.914 989.498\r
+2.115 1000.16\r
+2.368 962.831\r
+2.647 926\r
+2.985 878.603\r
+3.303 805.143\r
+3.472 752.815\r
+3.550 705.72\r
+3.602 648.26\r
+3.647 611.631\r
+3.693 512.409\r
+3.779 334.897\r
+3.857 178.216\r
+3.935 89.379\r
+3.981 26.687\r
+4.050 0\r
--- /dev/null
+;\r
+;AMW L900 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+L900RR 75 497 100 1.771 3.5888 Animal_Motor_Works \r
+0.029 464.292\r
+0.053 630.937\r
+0.059 684.506\r
+0.096 702.328\r
+0.133 696.387\r
+0.201 714.311\r
+0.486 803.524\r
+0.777 910.661\r
+1.099 988.093\r
+1.26 1041.16\r
+1.284 1071.37\r
+1.378 1053.24\r
+1.607 1101.57\r
+1.917 1142.86\r
+2.208 1173.56\r
+2.413 1160.98\r
+2.624 1107.62\r
+2.866 976.211\r
+3.053 886.897\r
+3.208 839.27\r
+3.314 827.388\r
+3.382 809.465\r
+3.432 720.252\r
+3.495 547.564\r
+3.57 345.273\r
+3.627 214.273\r
+3.714 77.382\r
+3.79 0\r
--- /dev/null
+;\r
+;Animal Motor Works M1350 White Wolf\r
+M1350WW 75 781 0 2.92700 5.40300 AMW\r
+0.03 1197.771588\r
+0.04 1465.181058\r
+0.07 1660.167131\r
+0.09 1665.738162\r
+0.16 1587.743733\r
+0.45 1587.743733\r
+0.61 1576.601671\r
+1.86 1649.02507\r
+2.27 1643.454039\r
+2.64 1598.885794\r
+3.18 1504.178273\r
+3.29 1353.760446\r
+3.41 991.643454\r
+3.49 841.2256267\r
+3.62 646.2395543\r
+3.74 428.9693593\r
+3.90 373.2590529\r
+4.22 0\r
--- /dev/null
+;\r
+;AMW M1480 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+M1480RR 75 785 100 3 5.5248 Animal_Motor_Works \r
+0.022 713.002\r
+0.032 1254.68\r
+0.055 1473.37\r
+0.078 1569.11\r
+0.156 1569.11\r
+0.352 1559.03\r
+0.642 1597.33\r
+0.974 1644.69\r
+1.289 1702.13\r
+1.52 1739.42\r
+1.918 1796.87\r
+2.279 1814.83\r
+2.481 1796.87\r
+2.707 1739.42\r
+2.968 1644.69\r
+3.058 1616.47\r
+3.135 1520.73\r
+3.218 1378.64\r
+3.284 1217.39\r
+3.332 1065.22\r
+3.344 1140.8\r
+3.368 1016.85\r
+3.41 741.522\r
+3.5 522.935\r
+3.613 275.727\r
+3.691 171.12\r
+3.768 66.553\r
+3.85 0\r
--- /dev/null
+;\r
+;Animal Motor Works 98-11000\r
+M1730SK 98 870 0 4.9452 9.8718 AMW\r
+0.040 682.642\r
+0.064 1153.387\r
+0.221 1354.665\r
+0.269 1414.771\r
+0.381 1458.026\r
+0.541 1526.924\r
+0.701 1589.200\r
+0.861 1675.203\r
+1.021 1732.669\r
+1.181 1802.227\r
+1.341 1886.644\r
+1.500 1973.713\r
+1.660 2070.514\r
+1.820 2183.822\r
+1.980 2299.313\r
+2.140 2433.862\r
+2.300 2568.119\r
+2.460 2679.423\r
+2.620 2638.376\r
+2.780 2484.185\r
+2.940 2306.038\r
+3.099 2173.849\r
+3.259 2074.688\r
+3.419 1961.303\r
+3.579 1807.810\r
+3.739 1640.258\r
+3.899 1303.035\r
+4.059 940.600\r
+4.219 567.152\r
+4.379 309.143\r
+4.539 188.981\r
+4.637 0.000\r
+;\r
--- /dev/null
+;\r
+; Animal Motor Works M1850GG\r
+; estimated from TRA graph by John DeMar jsdemar@syr.edu\r
+; motor mass is a guess based on similar types\r
+M1850GG 75 781 0 3.3750 4.5000 AMW\r
+ 0.08 979.00 \r
+ 0.13 1180.00 \r
+ 0.28 1290.00 \r
+ 0.33 1468.00 \r
+ 0.73 1936.00 \r
+ 1.33 2202.00 \r
+ 1.73 2279.00 \r
+ 2.58 2105.00 \r
+ 2.83 2007.00 \r
+ 2.88 1860.00 \r
+ 3.08 538.00 \r
+ 3.20 174.00 \r
+ 3.30 0.00 \r
--- /dev/null
+;\r
+;Animal Motor Works M1850 Green Gorilla\r
+M1850GG 75 781 0 3.37000 5.85100 AMW\r
+0.12 1201.01994\r
+0.25 1321.121934\r
+0.37 1579.11881\r
+0.50 1699.220804\r
+0.62 1846.01213\r
+0.75 1930.528348\r
+0.87 1997.251678\r
+1.00 2059.526786\r
+1.12 2126.250116\r
+1.25 2192.973446\r
+1.37 2224.111\r
+1.50 2246.35211\r
+1.62 2268.59322\r
+1.75 2277.489664\r
+1.87 2268.59322\r
+2.00 2246.35211\r
+2.12 2224.111\r
+2.25 2192.973446\r
+2.37 2166.284114\r
+2.50 2144.043004\r
+2.62 2099.560784\r
+2.75 2046.18212\r
+2.87 1912.73546\r
+3.00 831.817514\r
+3.12 311.37554\r
+3.25 84.516218\r
+3.3 0.000\r
--- /dev/null
+;\r
+;AMW M1900 RASP.ENG file made from NAR published data\r
+;File produced April 19, 2004\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+M1900BB 75 785 100 2.733 5.4225 Animal_Motor_Works \r
+0.018 1109.21\r
+0.044 1761.75\r
+0.061 1910.65\r
+0.085 1938.62\r
+0.159 1929.63\r
+0.29 1956.62\r
+0.409 2031.56\r
+0.438 1974.6\r
+0.569 2011.58\r
+0.815 2104.51\r
+1.073 2197.44\r
+1.401 2280.39\r
+1.688 2324.7\r
+1.905 2297.37\r
+2.073 2241.41\r
+2.254 2138.49\r
+2.397 2063.54\r
+2.479 2016.57\r
+2.54 2025.57\r
+2.581 2006.58\r
+2.63 1885.67\r
+2.716 1493.94\r
+2.805 1120.21\r
+2.887 840.605\r
+2.972 569.996\r
+3.046 299.488\r
+3.119 150.193\r
+3.168 56.829\r
+3.23 0\r
--- /dev/null
+;\r
+;Animal Motor Works M2500 Green Gorilla\r
+M2500GG 75 1039 0 4.248 7.5515 AMW\r
+0.026 1288.791\r
+0.053 2021.398\r
+0.079 2140.011\r
+0.123 2105.125\r
+0.207 2117.086\r
+0.540 2309.458\r
+0.971 2560.637\r
+1.265 2727.094\r
+1.480 2836.736\r
+1.678 2920.462\r
+1.757 2980.267\r
+1.946 2995.51\r
+2.047 2959.335\r
+2.240 2889.563\r
+2.310 2854.677\r
+2.486 2820.788\r
+2.526 2880.593\r
+2.592 2773.941\r
+2.653 2821.785\r
+2.706 2752.012\r
+2.758 2752.012\r
+2.807 2763.973\r
+2.842 2504.82\r
+2.886 2115.092\r
+2.930 1630.674\r
+2.987 1051.565\r
+3.040 437.571\r
+3.057 284.072\r
+3.079 142.434\r
+3.110 0\r
--- /dev/null
+;\r
+; Animal Motor Works M3000ST\r
+; estimated from TRA graph by Rob Bazinet rbazinet66@hotmail.com\r
+; motor mass is a guess based on similar types\r
+M3000ST 75 1038 0 3.8190 6.72 AMW\r
+ 0.032 2494.225\r
+ 0.113 2621.05 \r
+ 0.242 2705.6\r
+ 0.355 2811.288\r
+ 0.435 2895.838\r
+ 0.5 2959.25\r
+ 0.645 3128.35\r
+ 0.75 3297.45\r
+ 0.871 3382\r
+ 0.968 3551.1\r
+ 1.032 3656.788\r
+ 1.145 3804.75\r
+ 1.355 3973.85\r
+ 1.452 4037.263\r
+ 1.629 4079.538\r
+ 1.742 4142.95\r
+ 1.903 4185.225\r
+ 1.935 3847.025\r
+ 2.081 3424.275\r
+ 2.129 2959.25\r
+ 2.177 2536.5\r
+ 2.194 2113.75\r
+ 2.226 1691\r
+ 2.274 1268.25\r
+ 2.323 845.5\r
+ 2.403 422.75\r
+ 2.5 0\r
--- /dev/null
+;\r
+;Animal Motor Works 98-11000\r
+N2020WT 98 870 0 5.1609 9.9693 AMW\r
+.106 1941.344\r
+0.221 2151.149\r
+0.381 2253.406\r
+0.541 2340.792\r
+0.701 2400.847\r
+0.861 2453.821\r
+1.021 2506.314\r
+1.181 2556.306\r
+1.341 2607.251\r
+1.500 2652.790\r
+1.660 2688.660\r
+1.820 2710.675\r
+1.980 2729.797\r
+2.140 2733.895\r
+2.300 2704.255\r
+2.460 2634.582\r
+2.620 2532.160\r
+2.780 2433.380\r
+2.940 2329.740\r
+3.099 2234.246\r
+3.259 2165.804\r
+3.419 2099.684\r
+3.579 2028.350\r
+3.739 1951.013\r
+3.899 1871.316\r
+4.059 1558.113\r
+4.219 1053.376\r
+4.379 890.506\r
+4.539 636.689\r
+4.998 0.000\r
+\r
+;\r
--- /dev/null
+;\r
+;Animal Motor Works 98-11000\r
+N2600GG 98 870 1000 4.8812 10.4726 Animal_Motor_Works \r
+0.024 1674.37\r
+0.064 1949.62\r
+0.104 2039.52\r
+0.306 2189.98\r
+0.508 2334.45\r
+0.709 2491.23\r
+0.911 2668.93\r
+1.113 2874.7\r
+1.314 3038.83\r
+1.516 3191.29\r
+1.718 3266.01\r
+1.92 3318.98\r
+2.121 3336.18\r
+2.323 3229.26\r
+2.525 3089.68\r
+2.726 2943.98\r
+2.928 2847.69\r
+3.13 2751.68\r
+3.331 2682.22\r
+3.533 2463.48\r
+3.735 1339.63\r
+3.937 269.834\r
+4.034 0\r
--- /dev/null
+;\r
+;Animal Motor Works 98-11000\r
+N2700BB 98 870 1000 4.7837 9.9308 Animal_Motor_Works \r
+0.027 2229.53\r
+0.069 2476.18\r
+0.111 2539.74\r
+0.36 2723.21\r
+0.527 2863.83\r
+0.735 3016.48\r
+0.943 3141.25\r
+1.151 3241.72\r
+1.359 3335.56\r
+1.567 3519.92\r
+1.775 3425.88\r
+1.983 3420.56\r
+2.191 3356.08\r
+2.399 3270.48\r
+2.607 3182.6\r
+2.815 3098.31\r
+3.023 3002.95\r
+3.231 2888.73\r
+3.439 2266.61\r
+3.647 1498.26\r
+3.855 780.04\r
+4.063 233.545\r
+4.16 0\r
--- /dev/null
+; @File: N2800b.txt, @Pts-I: 5383, @Pts-O: 31, @Sm: 8, @CO: 5%\r
+; @TI: 14810.26, @TIa: 14792.71, @TIe: 0.0%, @ThMax: 3650.74, @ThAvg: 2770.17, @Tb: 5.34\r
+; Exported using ThrustCurveTool, www.ThrustGear.com, by John DeMar\r
+N2800 98 1213 100 7.6947 13.8 AMW\r
+ 0.0 93.0947\r
+ 0.0020 168.347\r
+ 0.0060 387.836\r
+ 0.019 1271.166\r
+ 0.029 1776.342\r
+ 0.043 2298.6\r
+ 0.062 2841.03\r
+ 0.072 3021.31\r
+ 0.084 3128.89\r
+ 0.14 3296.17\r
+ 0.277 3483.35\r
+ 0.293 3431.67\r
+ 0.369 3495.76\r
+ 0.978 3598.65\r
+ 1.973 3655.16\r
+ 2.977 3534.8\r
+ 3.3 3437.12\r
+ 3.497 3308.46\r
+ 3.583 3193.8\r
+ 3.651 3015.65\r
+ 3.748 2548.37\r
+ 3.836 2223.91\r
+ 4.109 1644.077\r
+ 4.245 1443.685\r
+ 4.272 1447.012\r
+ 4.397 1163.584\r
+ 4.489 1022.953\r
+ 4.516 1057.203\r
+ 4.574 883.885\r
+ 4.647 776.407\r
+ 5.569 0.0\r
--- /dev/null
+;\r
+;Animal Motor Works 98-17500\r
+N4000BB 98 1213 0 6.1026 13.6683 AMW\r
+0.029 4207.591\r
+0.071 4709.549\r
+0.113 4906.310\r
+0.155 5007.780\r
+0.239 5041.557\r
+0.323 4993.595\r
+0.534 5046.912\r
+0.744 5145.819\r
+0.954 5248.063\r
+1.165 5293.196\r
+1.375 5232.456\r
+1.585 5209.528\r
+1.796 5165.473\r
+2.006 5047.698\r
+2.216 4913.086\r
+2.427 4783.447\r
+2.637 4659.163\r
+2.847 4195.994\r
+3.058 2850.731\r
+3.268 1981.973\r
+3.478 1295.536\r
+3.689 907.699\r
+3.899 490.196\r
+4.110 316.338\r
+4.207 0.000\r
--- /dev/null
+; Aerotech D13 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (3/29/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+D13W 18 70 4-7-10 0.0098 0.0326 AT\r
+ 0.030 13.462\r
+ 0.061 21.171\r
+ 0.085 20.618\r
+ 0.127 21.605\r
+ 0.158 21.042\r
+ 0.182 22.306\r
+ 0.217 22.592\r
+ 0.227 23.610\r
+ 0.248 21.891\r
+ 0.279 23.155\r
+ 0.317 22.039\r
+ 0.366 21.338\r
+ 0.383 21.901\r
+ 0.449 20.648\r
+ 0.462 21.486\r
+ 0.480 19.947\r
+ 0.507 19.947\r
+ 0.521 20.509\r
+ 0.559 18.693\r
+ 0.580 19.118\r
+ 0.660 17.578\r
+ 0.743 15.337\r
+ 0.861 12.406\r
+ 0.947 9.329\r
+ 1.068 5.834\r
+ 1.155 4.158\r
+ 1.172 4.720\r
+ 1.231 2.762\r
+ 1.328 1.928\r
+ 1.404 1.093\r
+ 1.520 0.000\r
--- /dev/null
+; Aerotech D15 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (3/29/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+D15T 24 70 4-6-8 .0089 .0440 AT\r
+ 0.014 11.480\r
+ 0.049 26.272\r
+ 0.081 30.087\r
+ 0.107 31.261\r
+ 0.121 31.249\r
+ 0.159 31.360\r
+ 0.208 31.249\r
+ 0.283 29.583\r
+ 0.439 23.353\r
+ 0.551 18.484\r
+ 0.675 13.430\r
+ 0.863 6.422\r
+ 0.938 3.892\r
+ 1.010 2.335\r
+ 1.085 0.778\r
+ 1.142 0.389\r
+ 1.150 0.000\r
--- /dev/null
+;Aerotech D21 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;Submitted to ThrustCurve.org by Chris Kobel (3/29/07)\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+D21T 18 70 4-7 0.0096 0.025 AT \r
+ 0.01 1.367\r
+ 0.021 19.367\r
+ 0.029 32.12\r
+ 0.037 31.667\r
+ 0.051 30.528\r
+ 0.094 30.074\r
+ 0.115 31.213\r
+ 0.133 30.074\r
+ 0.177 30.76\r
+ 0.189 29.842\r
+ 0.203 30.528\r
+ 0.226 29.842\r
+ 0.275 28.935\r
+ 0.296 29.389\r
+ 0.331 28.027\r
+ 0.421 25.971\r
+ 0.478 24.146\r
+ 0.579 20.728\r
+ 0.659 17.774\r
+ 0.739 14.356\r
+ 0.799 9.569\r
+ 0.852 4.557\r
+ 0.899 1.139\r
+ 0.94 0\r
--- /dev/null
+; AeroTech D24T (Blue Thunder) RASP.ENG file made from\r
+; manufacturers published data.\r
+;\r
+; File was produced May 07, 2004 by Stanley_Hemphill@Hotmail.com.\r
+;\r
+; The motor is listed in the www.thrustcurve.org database as an\r
+; engine certified by NAR, but there is "no data" at the weblink\r
+; to the NAR file database.\r
+;\r
+; The author has created this file by extracting the manufacturers\r
+; Thrust-Time curve from The AeroTech-2002 Catalog, and then deploting\r
+; 32 points using the distance measuring tools in Paint Shop Pro 8.\r
+; The file was then created in RockSim 7 and the motor and static values\r
+; were read from the RockSim Engine Editor.\r
+;\r
+; Motor Dia Len Delay Propellant Total Manufacturer\r
+D24BT_CO_SU 18.00 70.00 4-7-10 0.00870 0.03200 AeroTech\r
+0.0380 39.6000\r
+0.0550 36.5000\r
+0.0760 34.4000\r
+0.1220 32.4000\r
+0.1640 31.1000\r
+0.2190 30.3000\r
+0.2610 29.3000\r
+0.3080 28.7000\r
+0.3290 27.9000\r
+0.3580 26.9000\r
+0.3920 25.8000\r
+0.4340 25.0000\r
+0.4850 24.0000\r
+0.5390 23.2000\r
+0.5770 22.3000\r
+0.6200 20.9000\r
+0.6660 19.6000\r
+0.6950 18.0000\r
+0.7210 16.5000\r
+0.7500 15.5000\r
+0.7540 14.3000\r
+0.7590 12.4000\r
+0.7600 11.0000\r
+0.7630 09.1000\r
+0.7634 07.5000\r
+0.7710 05.9000\r
+0.7920 04.0000\r
+0.8300 02.6000\r
+0.8680 01.8000\r
+0.9000 01.1000\r
+0.9400 00.0000\r
--- /dev/null
+;Aerotech D7 RASP.ENG file made from NAR published data\r
+D7 24 70 100 0.0105 0.0422 AT\r
+0.036 3.336\r
+0.084 9.326\r
+0.101 10.281\r
+0.143 10.827\r
+0.213 10.99\r
+0.271 10.887\r
+0.359 10.685\r
+0.471 10.13\r
+0.506 10.342\r
+0.535 9.929\r
+0.81 8.697\r
+1.226 6.713\r
+1.589 5.138\r
+1.8 4.861\r
+2.151 4.581\r
+2.649 4.57\r
+2.696 3.887\r
+2.748 2.388\r
+2.807 0.889\r
+2.842 0.207\r
+2.87 0\r
--- /dev/null
+; Aerotech D9 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (3/29/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+D9W 24 70 4-7 0.0101 0.045 AT \r
+ 0.1 13.7 \r
+ 0.15 15.4 \r
+ 0.2 16.3 \r
+ 0.25 16.8 \r
+ 0.35 17.2 \r
+ 0.40 17.2 \r
+ 0.50 16.8 \r
+ 0.65 15.9 \r
+ 0.80 14.5 \r
+ 1.10 9.2 \r
+ 1.25 7.0 \r
+ 1.40 4.8 \r
+ 1.60 2.5 \r
+ 1.90 0.0 \r
--- /dev/null
+;\r
+;Based On NAR Test Data\r
+;12/23/93\r
+E11J 24 70 4 0.025 0.0624 Aerotech\r
+0.0725446 14.3704\r
+0.16183 17.6296\r
+0.206473 18.3704\r
+0.418527 19.2593\r
+0.731027 18.3704\r
+1.31696 14.2222\r
+1.91964 9.03704\r
+2.51116 2.22222\r
+2.83 0\r
--- /dev/null
+;\r
+;\r
+;Aerotech E12JRC RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+E12JRC 24 70 100 0.0303 0.0594 AT\r
+0.054 16.764\r
+0.095 18.33\r
+0.197 16.545\r
+0.313 16.654\r
+0.36 17.211\r
+0.401 16.316\r
+0.442 17.55\r
+0.476 16.206\r
+0.578 16.316\r
+0.666 16.764\r
+0.7 15.649\r
+0.768 16.316\r
+0.89 16.097\r
+1.019 15.649\r
+1.162 14.983\r
+1.23 14.983\r
+1.25 13.968\r
+1.291 14.754\r
+1.332 13.749\r
+1.373 14.197\r
+1.434 13.53\r
+1.488 13.749\r
+1.597 12.635\r
+1.726 11.401\r
+1.828 10.615\r
+1.889 9.613\r
+1.957 9.613\r
+1.998 8.495\r
+2.093 8.607\r
+2.277 7.042\r
+2.487 5.813\r
+3.05 0\r
--- /dev/null
+; E15W-4,7,P from NAR data\r
+E15W 24 70 4-7-P 0.020100000000000003 0.0502 AT\r
+ 0.012 9.918\r
+ 0.018 20.205\r
+ 0.027 25.257\r
+ 0.039 28.152\r
+ 0.055 28.768\r
+ 0.088 27.29\r
+ 0.197 24.517\r
+ 0.297 22.977\r
+ 0.467 20.945\r
+ 0.561 19.959\r
+ 0.679 20.021\r
+ 0.722 19.22\r
+ 0.761 18.789\r
+ 0.807 20.021\r
+ 0.84 18.234\r
+ 0.904 18.727\r
+ 0.995 17.926\r
+ 1.034 18.172\r
+ 1.104 16.756\r
+ 1.147 17.248\r
+ 1.256 16.386\r
+ 1.377 15.77\r
+ 1.411 14.846\r
+ 1.426 16.324\r
+ 1.45 15.031\r
+ 1.547 14.353\r
+ 1.559 16.016\r
+ 1.589 13.86\r
+ 1.62 14.23\r
+ 1.693 13.121\r
+ 1.72 13.429\r
+ 1.829 12.936\r
+ 1.866 11.951\r
+ 1.944 11.951\r
+ 2.005 10.965\r
+ 2.093 10.472\r
+ 2.236 8.316\r
+ 2.26 9.055\r
+ 2.278 7.207\r
+ 2.378 4.99\r
+ 2.442 2.71\r
+ 2.499 1.602\r
+ 2.548 1.047\r
+ 2.618 0.0\r
--- /dev/null
+; Aerotech E15 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (3/30/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+E15W 24 70 4-7 .0201 .0501 AT\r
+ 0.020 23.330\r
+ 0.036 27.318\r
+ 0.058 28.840\r
+ 0.079 27.171\r
+ 0.139 25.638\r
+ 0.183 24.263\r
+ 0.237 24.106\r
+ 0.297 22.426\r
+ 0.373 21.964\r
+ 0.400 20.894\r
+ 0.443 21.355\r
+ 0.487 20.442\r
+ 0.617 19.833\r
+ 0.742 18.457\r
+ 0.812 20.000\r
+ 0.850 18.006\r
+ 0.899 18.467\r
+ 1.035 17.711\r
+ 1.100 16.945\r
+ 1.160 16.945\r
+ 1.377 15.736\r
+ 1.426 14.656\r
+ 1.436 16.198\r
+ 1.463 14.813\r
+ 1.550 14.361\r
+ 1.572 15.432\r
+ 1.610 13.752\r
+ 1.827 12.839\r
+ 2.126 10.098\r
+ 2.337 6.116\r
+ 2.538 1.369\r
+ 2.600 0.000\r
--- /dev/null
+; Aerotech E16 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (3/30/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+E16W 29 124 4-7-10 .0190 .107 AT \r
+ 0.132 32.223 \r
+ 0.221 37.200 \r
+ 0.255 36.699 \r
+ 0.306 36.699 \r
+ 0.371 35.357 \r
+ 0.414 33.785 \r
+ 0.437 34.906 \r
+ 0.472 33.785 \r
+ 0.530 32.894 \r
+ 0.553 31.772 \r
+ 0.576 32.443 \r
+ 0.638 29.309 \r
+ 0.720 27.296 \r
+ 0.867 23.942 \r
+ 1.083 19.245 \r
+ 1.273 14.319 \r
+ 1.458 9.397\r
+ 1.513 8.055 \r
+ 1.524 8.279 \r
+ 1.555 6.936 \r
+ 1.656 4.474 \r
+ 1.814 1.790 \r
+ 2.000 0.000 \r
--- /dev/null
+; Aerotech E18 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (3/29/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+E18W 24 70 4-8-10 .0207 .057 AT\r
+ 0.016 6.586\r
+ 0.042 18.004\r
+ 0.073 27.138\r
+ 0.098 29.815\r
+ 0.134 30.357\r
+ 0.170 30.347\r
+ 0.195 31.080\r
+ 0.236 30.347\r
+ 0.287 30.878\r
+ 0.338 30.337\r
+ 0.368 30.878\r
+ 0.404 29.795\r
+ 0.424 30.688\r
+ 0.465 29.976\r
+ 0.526 29.785\r
+ 0.592 29.063\r
+ 0.669 28.341\r
+ 0.786 26.908\r
+ 0.908 23.850\r
+ 1.025 21.163\r
+ 1.157 17.905\r
+ 1.284 14.857\r
+ 1.462 11.338\r
+ 1.660 7.106\r
+ 1.838 3.470\r
+ 2.006 1.309\r
+ 2.083 0.588\r
+ 2.140 0.000\r
--- /dev/null
+; Aerotech E23 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (3/30/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+E23T 29 124 5-8 .0174 .1039 AT\r
+ 0.024 16.299\r
+ 0.035 21.959\r
+ 0.067 30.785\r
+ 0.090 35.774\r
+ 0.153 37.577\r
+ 0.200 38.220\r
+ 0.240 37.357\r
+ 0.322 37.577\r
+ 0.393 35.093\r
+ 0.534 32.378\r
+ 0.727 27.168\r
+ 0.766 26.938\r
+ 0.798 25.125\r
+ 0.908 21.729\r
+ 1.057 16.980\r
+ 1.187 12.682\r
+ 1.336 7.471\r
+ 1.450 3.169\r
+ 1.497 1.584\r
+ 1.532 0.679\r
+ 1.570 0.000\r
--- /dev/null
+; Aerotech E28 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (3/29/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+E28T 24 70 2-5-8 .0184 .0545 A\r
+ 0.010 29.8620\r
+ 0.018 45.1390\r
+ 0.038 47.5620\r
+ 0.081 50.5200\r
+ 0.106 48.9530\r
+ 0.146 48.2630\r
+ 0.161 48.9530\r
+ 0.197 48.9530\r
+ 0.242 47.5620\r
+ 0.313 46.1800\r
+ 0.411 43.0570\r
+ 0.494 40.6240\r
+ 0.527 39.5830\r
+ 0.542 40.2740\r
+ 0.562 38.5420\r
+ 0.633 36.4600\r
+ 0.683 34.3770\r
+ 0.743 31.2440\r
+ 0.799 29.1620\r
+ 0.877 26.0380\r
+ 0.970 20.8320\r
+ 1.006 17.3590\r
+ 1.046 11.4620\r
+ 1.089 6.9430\r
+ 1.132 3.8190\r
+ 1.172 1.7350\r
+ 1.220 0.0000\r
--- /dev/null
+; Aerotech E30 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (3/30/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+E30T 24 70 4-7 .0193 .0433 AT\r
+ 0.013 38.8470\r
+ 0.020 45.6210\r
+ 0.041 48.2700\r
+ 0.059 46.5020\r
+ 0.110 46.5020\r
+ 0.166 45.9120\r
+ 0.184 46.7920\r
+ 0.217 45.9120\r
+ 0.265 45.9120\r
+ 0.319 45.0310\r
+ 0.383 44.1500\r
+ 0.482 42.0890\r
+ 0.594 38.8470\r
+ 0.615 39.4370\r
+ 0.628 37.3760\r
+ 0.684 35.3140\r
+ 0.742 33.2630\r
+ 0.804 30.0210\r
+ 0.880 25.6070\r
+ 0.962 20.0140\r
+ 1.038 12.9490\r
+ 1.089 7.3580\r
+ 1.151 3.2370\r
+ 1.186 1.1760\r
+ 1.200 0.0000\r
--- /dev/null
+; Aerotech E6T single use from NAR cert data\r
+E6T 24 70 2-4-8-P 0.021500000000000002 0.0463 AT\r
+ 0.011 18.085\r
+ 0.109 19.681\r
+ 0.217 16.312\r
+ 0.315 13.475\r
+ 0.457 11.348\r
+ 0.63 9.043\r
+ 0.804 7.801\r
+ 0.989 6.738\r
+ 1.272 6.028\r
+ 2.0 5.851\r
+ 3.0 5.496\r
+ 4.0 5.496\r
+ 4.446 4.965\r
+ 5.011 4.965\r
+ 5.533 4.787\r
+ 5.609 6.56\r
+ 5.707 4.255\r
+ 6.033 0.0\r
--- /dev/null
+;\r
+;Aerotech E7TRC RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+E7RC 24 70 100 0.0171 0.0484 AT\r
+0.038 6.636\r
+0.063 10.056\r
+0.087 11.019\r
+0.134 11.42\r
+0.206 11.58\r
+0.312 11.149\r
+0.466 10.738\r
+0.667 9.777\r
+0.94 8.132\r
+1.223 6.281\r
+1.484 5.182\r
+1.709 4.701\r
+2.112 4.423\r
+2.776 4.279\r
+3.31 4.205\r
+3.926 4.266\r
+4.401 4.192\r
+4.638 4.258\r
+4.744 4.119\r
+5.124 3.979\r
+5.219 3.977\r
+5.266 3.156\r
+5.313 1.992\r
+5.36 0.965\r
+5.43 0\r
--- /dev/null
+;\r
+F10 29.0 92.00 4-6-8 0.04000 0.08300 Aerotech\r
+ 0.01 16.81 \r
+ 0.03 22.34 \r
+ 0.11 22.23 \r
+ 0.26 21.49 \r
+ 0.37 20.00 \r
+ 0.47 20.21 \r
+ 0.67 18.09 \r
+ 0.99 15.74 \r
+ 1.31 13.40 \r
+ 1.81 10.85 \r
+ 2.49 10.21 \r
+ 3.13 8.94 \r
+ 3.60 8.83 \r
+ 4.11 8.62 \r
+ 4.95 8.62 \r
+ 5.45 8.62 \r
+ 5.58 8.51 \r
+ 5.88 8.72 \r
+ 6.22 8.51 \r
+ 6.46 8.51 \r
+ 6.60 7.77 \r
+ 6.71 7.02 \r
+ 6.79 5.64 \r
+ 6.91 3.83 \r
+ 6.95 2.23 \r
+ 7.00 0.96 \r
+ 7.05 0.00 \r
+;\r
--- /dev/null
+; Aerotech F12 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (4/6/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+F12J 24 70 2-5 .0303 .0667 AT\r
+ 0.037 20.894\r
+ 0.054 22.152\r
+ 0.101 22.152\r
+ 0.148 22.571\r
+ 0.165 23.409\r
+ 0.200 22.421\r
+ 0.281 22.142\r
+ 0.369 22.132\r
+ 0.474 22.271\r
+ 0.526 23.540\r
+ 0.549 21.982\r
+ 0.637 22.122\r
+ 0.724 21.842\r
+ 0.800 21.413\r
+ 0.823 22.251\r
+ 0.846 20.714\r
+ 0.881 21.553\r
+ 0.945 21.123\r
+ 1.021 20.704\r
+ 1.114 20.554\r
+ 1.213 19.296\r
+ 1.382 18.298\r
+ 1.481 18.019\r
+ 1.737 15.343\r
+ 1.790 17.300\r
+ 1.883 13.936\r
+ 2.051 11.260\r
+ 2.220 7.468\r
+ 2.447 3.671\r
+ 2.709 1.135\r
+ 2.930 0.000\r
--- /dev/null
+;\r
+;Aerotech F13RCJ RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+F13RCJ 32 107 100 0.0323 0.1105 AT\r
+0.048 15.309\r
+0.084 18.629\r
+0.143 19.98\r
+0.311 18.968\r
+0.538 18.172\r
+0.729 17.138\r
+0.992 15.428\r
+1.279 13.828\r
+1.673 12.456\r
+1.984 11.879\r
+2.044 12.227\r
+2.139 11.313\r
+2.378 11.193\r
+2.51 11.084\r
+2.558 12.108\r
+2.641 10.855\r
+2.976 10.736\r
+3.49 10.627\r
+3.873 10.507\r
+3.992 10.965\r
+4.028 10.627\r
+4.41 10.507\r
+4.625 10.736\r
+4.769 9.941\r
+4.829 8.684\r
+4.865 6.742\r
+4.96 3.199\r
+5.02 1.485\r
+5.1 0\r
--- /dev/null
+;\r
+;Aerotech F16RCJ RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+F16RCJ 32 107 100 0.0625 0.1404 AT\r
+0.046 26.35\r
+0.116 22.388\r
+0.139 21.374\r
+0.185 21.886\r
+0.22 20.54\r
+0.301 19.696\r
+0.498 18.35\r
+0.579 19.194\r
+0.637 16.492\r
+0.718 18.35\r
+0.834 18.35\r
+0.95 18.35\r
+1.054 19.194\r
+1.147 17.848\r
+1.181 18.853\r
+1.263 17.336\r
+1.436 18.009\r
+1.633 17.165\r
+1.784 17.336\r
+1.865 18.682\r
+1.934 16.834\r
+1.981 17.336\r
+2.178 16.332\r
+2.375 16.332\r
+2.502 18.18\r
+2.664 15.659\r
+2.896 15.488\r
+3.29 13.8\r
+3.718 11.611\r
+4.181 9.426\r
+4.888 5.891\r
+5.69 0\r
--- /dev/null
+;\r
+;\r
+F20EJ 29 83 4-7 0.03 0.0746 AeroTech\r
+0.01 52.08\r
+0.03 49.81\r
+0.06 46.98\r
+0.1 45.56\r
+0.15 44.49\r
+0.18 45.55\r
+0.21 43.42\r
+0.24 43.78\r
+0.32 43.77\r
+0.36 44.11\r
+0.44 43.04\r
+0.45 40.58\r
+0.53 39.86\r
+0.62 38.08\r
+0.76 36.3\r
+0.8 37.35\r
+0.84 34.88\r
+0.89 36.99\r
+0.9 33.46\r
+1.03 30.61\r
+1.06 32.02\r
+1.09 29.55\r
+1.23 26\r
+1.32 22.45\r
+1.35 23.16\r
+1.36 21.39\r
+1.58 16.42\r
+1.8 11.1\r
+2.01 6.48\r
+2.19 3.63\r
+2.39 1.13\r
+2.68 0\r
--- /dev/null
+; Aerotech F21W (White Lightning) RASP.ENG file.\r
+; File produced Jun 22 2004.\r
+; The file was produced by scaling data points off the\r
+; thrust curve in the Tripoli.org motor pdf file.\r
+;\r
+; The F21W cannot be found on thrustcurve.org.\r
+; Hence the amateur file production.\r
+; The file was created by Stan Hemphill\r
+; Contact at stanley_hemphill@hotmail.com\r
+;\r
+; Motor ## Dia Len Delays Prop Motor Company\r
+F21WL_CO_SU 24 96 6-8 0.0300 0.064 AeroTech\r
+0.0045 037.2266\r
+0.0090 042.1474\r
+0.0180 042.5040\r
+0.0270 040.5071\r
+0.0337 038.8669\r
+0.0427 038.2250\r
+0.0517 037.7258\r
+0.0607 036.8700\r
+0.0720 036.4422\r
+0.0877 036.4422\r
+0.1102 035.3724\r
+0.1350 035.8716\r
+0.1552 035.4437\r
+0.1732 036.2282\r
+0.2025 035.6577\r
+0.2452 037.2979\r
+0.2835 036.5848\r
+0.3195 038.0111\r
+0.3375 037.4406\r
+0.3757 038.5816\r
+0.3960 038.2250\r
+0.4297 039.3661\r
+0.4454 038.0111\r
+0.4747 038.7242\r
+0.4882 037.7971\r
+0.5084 038.1537\r
+0.5354 038.5103\r
+0.5647 037.9398\r
+0.5849 037.2266\r
+0.6007 037.8685\r
+0.6389 036.8700\r
+0.6704 037.1553\r
+0.7649 035.9429\r
+0.9201 032.9477\r
+1.0056 030.3090\r
+1.0709 029.7385\r
+1.2643 024.0333\r
+1.2868 024.0333\r
+1.3723 020.8241\r
+1.3926 020.8241\r
+1.5883 015.4754\r
+1.6108 015.4754\r
+2.0112 005.0634\r
+2.1192 003.4231\r
+2.2407 002.4247\r
+2.3780 001.4976\r
+2.4927 000.9271\r
+2.5152 000.0000\r
--- /dev/null
+;\r
+;Aerotech F22 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+F22 29 125 4-7 0.0463 0.1342 AT\r
+0.014 11.527\r
+0.075 20.126\r
+0.157 26.572\r
+0.293 29.113\r
+0.382 30.278\r
+0.45 29.69\r
+0.539 30.667\r
+0.614 30.089\r
+0.662 31.15\r
+0.771 30.478\r
+0.948 29.89\r
+0.996 28.714\r
+1.078 28.136\r
+1.187 27.738\r
+1.289 26.761\r
+1.337 26.96\r
+1.412 25.984\r
+1.474 25.008\r
+1.515 26.173\r
+1.542 24.808\r
+1.706 22.856\r
+1.938 20.903\r
+2.101 18.173\r
+2.129 19.338\r
+2.251 16.21\r
+2.402 13.48\r
+2.64 8.791\r
+2.961 3.32\r
+3.31 0\r
--- /dev/null
+;\r
+;F23FJ Motor Thrust Curve created by Tim Van Milligan\r
+;for RockSim Users - www.rocksim.com\r
+;file produced March 2, 2005\r
+;Based on data supplied by Aerotech for the newer molded case F23 econojet.\r
+F23FJ 29 83 4-7 0.033 0.0839 AeroTech\r
+0.03 48.7\r
+0.05 43.11\r
+0.08 41.41\r
+0.1 42.26\r
+0.13 40.84\r
+0.17 39.42\r
+0.23 38.85\r
+0.27 38.85\r
+0.3 37.44\r
+0.31 38.57\r
+0.36 37.72\r
+0.43 36.59\r
+0.5 36.02\r
+0.56 36.02\r
+0.59 34.6\r
+0.69 33.18\r
+0.77 32.61\r
+0.85 31.2\r
+0.94 29.5\r
+1.04 27.79\r
+1.18 24.39\r
+1.2 25.24\r
+1.25 22.97\r
+1.37 20.98\r
+1.53 16.73\r
+1.69 12.48\r
+1.83 9.07\r
+1.95 5.11\r
+2.07 2.27\r
+2.22 0\r
--- /dev/null
+;\r
+;Aerotech F23RCWSK RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+F23-RC-SK 32 107 100 0.0378 0.1287 AT\r
+0.042 22.644\r
+0.133 28.191\r
+0.161 27.261\r
+0.189 29.57\r
+0.252 31.419\r
+0.343 32.578\r
+0.399 32.348\r
+0.441 33.737\r
+0.476 30.729\r
+0.539 33.507\r
+0.609 34.197\r
+0.777 34.886\r
+0.826 34.656\r
+0.896 36\r
+0.938 34.656\r
+1.015 34.656\r
+1.071 34.197\r
+1.12 33.038\r
+1.218 32.578\r
+1.267 29.81\r
+1.351 29.34\r
+1.393 27.731\r
+1.54 26.802\r
+1.645 24.263\r
+1.799 21.255\r
+1.862 19.866\r
+2.051 15.479\r
+2.317 11.552\r
+2.618 6.7\r
+2.884 3.234\r
+3.185 1.386\r
+3.47 0\r
--- /dev/null
+; Aerotech F24 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (4/6/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+F24W 24 70 4-7-10 .0253 .062 AT\r
+ 0.033 16.442\r
+ 0.112 40.646\r
+ 0.125 41.450\r
+ 0.180 40.927\r
+ 0.245 40.626\r
+ 0.281 41.017\r
+ 0.355 40.024\r
+ 0.438 39.713\r
+ 0.543 38.227\r
+ 0.603 37.032\r
+ 0.658 33.779\r
+ 0.685 34.663\r
+ 0.726 29.934\r
+ 0.772 30.216\r
+ 0.951 26.953\r
+ 1.071 25.166\r
+ 1.107 23.088\r
+ 1.185 21.311\r
+ 1.383 17.144\r
+ 1.649 10.910\r
+ 1.828 5.869\r
+ 1.938 2.903\r
+ 1.988 2.306\r
+ 2.048 1.412\r
+ 2.130 0.000\r
--- /dev/null
+;\r
+;F25 Motor Thrust Curve created by Tim Van Milligan\r
+;for RockSim Users - www.rocksim.com\r
+;file produced March 2, 2005\r
+;Based on data supplied by Aerotech for the newer molded case F25.\r
+F25 29 98 4-6-9 0.0388 0.0972 Aerotech\r
+0.039 57.631\r
+0.187 53.491\r
+0.342 51.239\r
+0.5 47.86\r
+1 33.806\r
+1.5 22.94\r
+2 10.135\r
+2.207 4.504\r
+2.69 0\r
--- /dev/null
+;\r
+;F26FJ Motor Thrust Curve created by Tim Van Milligan\r
+;for RockSim Users - www.rocksim.com\r
+;File created March 2, 2005\r
+;Based on data supplied by Aerotech prior to NAR certification.\r
+F26FJ 29 98 6-9 0.0431 0.1007 Aerotech\r
+0.041 38.289\r
+0.114 36.318\r
+0.293 34.347\r
+0.497 32.939\r
+0.774 32.376\r
+1 31.25\r
+1.254 28.716\r
+1.498 25.338\r
+1.743 22.241\r
+2.003 17.737\r
+2.077 15.484\r
+2.304 5.349\r
+2.484 1.689\r
+2.61 0\r
--- /dev/null
+; Exported using ThrustCurveTool, www.ThrustGear.com \r
+; @File: 060603WF27Composite.txt, @Pts-I: 1001, @Pts-O: 32, @Sm: 5, @CO: 5% \r
+; @TI: 49.5446, @TIa: 49.299, @TIe: 0.0%, @ThMax: 36.2491, @ThAvg: 24.1799, @Tb: 2.049\r
+F27 29 83 4,8 0.0284 0.08 Aerotech \r
+ 0.0 4.84718 \r
+ 0.0125 15.5374 \r
+ 0.0175 18.81827 \r
+ 0.025 22.5311 \r
+ 0.0325 25.2547 \r
+ 0.04 27.3204 \r
+ 0.0575 30.3657 \r
+ 0.0725 31.4597 \r
+ 0.0975 32.2507 \r
+ 0.265 35.0238 \r
+ 0.295 35.9203 \r
+ 0.4675 35.9684 \r
+ 0.59 35.065 \r
+ 0.8 32.0145 \r
+ 0.825 31.2773 \r
+ 0.8575 31.1102 \r
+ 0.9025 29.9308 \r
+ 0.955 29.7244 \r
+ 1.045 27.2951 \r
+ 1.085 27.2663 \r
+ 1.1175 25.9881 \r
+ 1.1475 26.0014 \r
+ 1.235 23.2853 \r
+ 1.28 22.9001 \r
+ 1.3425 21.0771 \r
+ 1.52 17.256 \r
+ 1.8075 6.85478 \r
+ 1.9175 4.16387 \r
+ 2.05 1.783863 \r
+ 2.1775 0.488016 \r
+ 2.4225 0.44385 \r
+ 2.425 0.0\r
--- /dev/null
+;\r
+;Aerotech F32 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+F32 24 124 5-10-15 0.0377 0.0814 AeroTech\r
+0.025 46.699\r
+0.031 51.846\r
+0.061 55.64\r
+0.085 52.868\r
+0.126 47.37\r
+0.245 45.637\r
+0.34 44.946\r
+0.394 42.873\r
+0.447 42.873\r
+0.572 41.14\r
+0.72 39.408\r
+0.744 40.78\r
+0.786 38.026\r
+1.041 35.592\r
+1.136 33.179\r
+1.177 34.541\r
+1.225 32.818\r
+1.379 31.436\r
+1.474 30.394\r
+1.635 28.311\r
+1.676 27.28\r
+1.694 29.683\r
+1.712 26.929\r
+1.854 25.537\r
+1.943 23.815\r
+2.092 21.051\r
+2.187 18.287\r
+2.276 13.82\r
+2.382 7.281\r
+2.525 2.457\r
+2.72 0\r
--- /dev/null
+; Curve fit of AT Instruction Sheet by C. Kobel 7/29/08\r
+F35W 24 95 5-8-11 0.03 0.085 AT\r
+ 0.007 39.452\r
+ 0.012 51.842\r
+ 0.019 49.885\r
+ 0.034 57.873\r
+ 0.048 58.363\r
+ 0.058 57.221\r
+ 0.077 54.45\r
+ 0.098 54.939\r
+ 0.106 53.961\r
+ 0.201 53.472\r
+ 0.299 53.309\r
+ 0.398 52.005\r
+ 0.498 52.005\r
+ 0.549 49.559\r
+ 0.601 48.907\r
+ 0.653 47.277\r
+ 0.702 45.647\r
+ 0.752 44.669\r
+ 0.802 43.201\r
+ 0.898 39.778\r
+ 0.946 39.615\r
+ 0.984 36.843\r
+ 1.003 37.332\r
+ 1.102 33.583\r
+ 1.144 30.159\r
+ 1.200 22.334\r
+ 1.298 10.923\r
+ 1.346 6.521\r
+ 1.398 3.260\r
+ 1.448 1.793\r
+ 1.497 0.978\r
+ 1.600 0.0\r
--- /dev/null
+;\r
+;Aerotech F37 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+F37 29 99 6-10-14 0.0282 0.1086 AT\r
+0.018 7.251\r
+0.053 13.626\r
+0.088 22.331\r
+0.106 25.227\r
+0.141 26.385\r
+0.183 28.411\r
+0.26 37.685\r
+0.31 41.449\r
+0.422 44.035\r
+0.524 45.183\r
+0.59 46.47\r
+0.682 45.153\r
+0.864 43.386\r
+0.934 40.471\r
+1.042 35.23\r
+1.151 29.699\r
+1.246 25.037\r
+1.354 19.796\r
+1.445 13.397\r
+1.498 7.586\r
+1.54 3.226\r
+1.6 0\r
--- /dev/null
+; Aerotech F39 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Submitted to ThrustCurve.org by Chris Kobel (4/6/07)\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+F39T 24 70 3-6-9 .0227 .059 AT\r
+ 0.010 45.057\r
+ 0.016 54.131\r
+ 0.046 58.321\r
+ 0.079 59.470\r
+ 0.103 58.311\r
+ 0.130 57.253\r
+ 0.172 55.491\r
+ 0.235 53.738\r
+ 0.321 51.271\r
+ 0.363 50.566\r
+ 0.387 49.509\r
+ 0.408 50.203\r
+ 0.426 48.804\r
+ 0.453 47.746\r
+ 0.480 47.041\r
+ 0.680 41.059\r
+ 0.716 39.649\r
+ 0.752 38.944\r
+ 0.809 36.487\r
+ 0.860 34.382\r
+ 0.893 33.324\r
+ 0.917 32.619\r
+ 1.000 28.752\r
+ 1.075 25.247\r
+ 1.105 22.095\r
+ 1.126 17.201\r
+ 1.144 13.001\r
+ 1.174 8.109\r
+ 1.219 4.606\r
+ 1.261 2.500\r
+ 1.330 0.000\r
--- /dev/null
+;\r
+;Aerotech F40 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+F40 29 124 4-7-10 0.04 0.126 AT\r
+0.015 17.776\r
+0.049 41.016\r
+0.089 58.793\r
+0.124 62.9\r
+0.148 65.173\r
+0.183 62.442\r
+0.242 68.07\r
+0.292 60.617\r
+0.321 61.524\r
+0.415 60.617\r
+0.524 58.334\r
+0.741 52.412\r
+0.87 48.314\r
+0.889 49.221\r
+0.914 47.397\r
+1.102 40.109\r
+1.285 33.728\r
+1.492 25.064\r
+1.665 15.952\r
+1.808 8.659\r
+1.942 3.19\r
+2.06 0\r
--- /dev/null
+;\r
+;F42T Motor Thrust Curve created by Tim Van Milligan\r
+;for RockSim Users - www.rocksim.com\r
+;Based on data supplied by Aerotech prior to NAR certification.\r
+F42T 29 83 4-8 0.027 0.076 Aerotech\r
+0.01 68.694\r
+0.029 65.879\r
+0.202 62.5\r
+0.511 51.802\r
+0.739 43.356\r
+0.993 31.532\r
+1.02 29.279\r
+1.072 23.086\r
+1.199 9.572\r
+1.262 4.505\r
+1.319 2.815\r
+1.47 0\r
--- /dev/null
+;\r
+;Aerotech F50 RASP.ENG file made by Tim Van Milligan\r
+;For RockSim www.RockSim.com\r
+;File Created March 2, 2005\r
+;Thrust curve supplied by Aerotech for the molded case F50T motors.\r
+F50T 29 98 4-6-9 0.0336 0.0898 AeroTech\r
+0.013 73.762\r
+0.0326 70.383\r
+0.267 69.82\r
+0.518 67.005\r
+0.792 56.87\r
+0.906 50.676\r
+1 44.482\r
+1.036 39.978\r
+1.107 23.649\r
+1.199 6.194\r
+1.316 1.126\r
+1.43 0\r
--- /dev/null
+;\r
+;Aerotech F52 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+F52 29 124 5-8-11 0.0366 0.1214 AT\r
+0.012 46.899\r
+0.033 61.778\r
+0.056 69.441\r
+0.097 73.483\r
+0.115 76.636\r
+0.13 74.381\r
+0.153 74.82\r
+0.168 78.422\r
+0.182 78.95\r
+0.206 77.963\r
+0.238 77.504\r
+0.258 73.892\r
+0.314 72.974\r
+0.39 72.046\r
+0.428 70.679\r
+0.501 65.699\r
+0.565 62.975\r
+0.688 58.874\r
+0.749 56.15\r
+0.837 52.517\r
+0.901 49.793\r
+0.971 46.161\r
+1.088 39.365\r
+1.144 34.386\r
+1.173 29.417\r
+1.222 20.376\r
+1.275 13.151\r
+1.339 5.461\r
+1.389 1.838\r
+1.42 0\r
--- /dev/null
+; Aerotech F62T (Blue Thunder)\r
+;\r
+; AeroTech RMS-29/60 Easy Access Reloadable Motor Hardware.\r
+;\r
+; RASP.ENG file made from manufacturers catalog data.\r
+;\r
+; File produced May, 17 2004.\r
+;\r
+; The file was produced by scaling 16 data points off\r
+; the thrust curves in the manufacturers catalog.\r
+;\r
+; The F62T cannot be found on thrustcurve.org.\r
+; Hence the amateur file production.\r
+; The file was created by Stan Hemphill.\r
+; Contact at stanley_hemphill@hotmail.com.\r
+;\r
+; Motor Dia Len Delay Prop Gross Mfg\r
+F62T 29 99 6-8-9-10-11-13-14-16-18 0.025 0.109 AT\r
+0.0046 053.6364\r
+0.0416 055.2727\r
+0.0909 058.3636\r
+0.1356 061.6364\r
+0.1649 064.9091\r
+0.1864 067.6364\r
+0.5085 067.6364\r
+0.5701 064.7273\r
+0.6687 060.0000\r
+0.7427 055.0909\r
+0.7982 049.6364\r
+0.9029 048.7273\r
+0.9492 024.7273\r
+0.9661 020.1818\r
+0.9985 000.0000\r
--- /dev/null
+;\r
+;Aerotech F72 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+F72 24 124 5-10-15 0.0368 0.0742 AeroTech\r
+0.012 62.586\r
+0.017 84.986\r
+0.02 98.78\r
+0.03 94.748\r
+0.05 90.152\r
+0.069 82.688\r
+0.089 85.556\r
+0.104 80.39\r
+0.136 83.255\r
+0.146 80.96\r
+0.176 82.688\r
+0.198 78.672\r
+0.213 80.96\r
+0.253 80.39\r
+0.315 80.96\r
+0.38 79.821\r
+0.429 79.241\r
+0.489 78.092\r
+0.523 78.672\r
+0.536 75.225\r
+0.675 73.496\r
+0.699 67.182\r
+0.719 68.331\r
+0.747 64.884\r
+0.769 66.033\r
+0.858 60.867\r
+0.923 52.824\r
+0.98 40.195\r
+1.012 29.864\r
+1.034 20.092\r
+1.089 11.48\r
+1.21 0\r
--- /dev/null
+;\r
+G101T 29.0 114.30 5-8-12 0.04600 0.13600 AT\r
+ 0.01 35.29 \r
+ 0.02 63.97 \r
+ 0.02 78.09 \r
+ 0.03 84.71 \r
+ 0.04 89.96 \r
+ 0.06 93.96 \r
+ 0.12 97.26 \r
+ 0.17 99.14 \r
+ 0.21 101.73 \r
+ 0.27 104.09 \r
+ 0.31 104.56 \r
+ 0.36 103.62 \r
+ 0.40 103.38 \r
+ 0.45 101.03 \r
+ 0.51 99.27 \r
+ 0.53 94.41 \r
+ 0.56 93.09 \r
+ 0.64 87.35 \r
+ 0.76 78.53 \r
+ 0.80 75.88 \r
+ 0.88 68.82 \r
+ 0.89 65.73 \r
+ 0.90 61.32 \r
+ 0.91 55.15 \r
+ 0.93 35.73 \r
+ 0.94 32.65 \r
+ 0.95 22.50 \r
+ 0.98 10.59 \r
+ 1.00 5.29 \r
+ 1.05 0.00 \r
+;\r
--- /dev/null
+;\r
+; Aerotech G104T (Blue Thunder)\r
+;\r
+; AeroTech RMS-29/100 EZ Access Reloadable Motors.\r
+;\r
+; File produced 28 Feb 2005.\r
+;\r
+; The file was produced by scaling data points off the\r
+; thrust curve in the manufacturers catalog sheet.\r
+;\r
+; The motor is not yet on www.thrustcurve.org.\r
+; Hence the amateur file production.\r
+; The file was created by Stan Hemphill.\r
+; Contact at stanley_hemphill@hotmail.com.\r
+;\r
+; Motor Dia Len Delay Prop Gross Mfg\r
+G104T 29 124 6-8-9-10-11-13-14-16-18 0.0408 0.136 AT\r
+0.0067 125.3426\r
+0.0471 123.5424\r
+0.0856 121.9671\r
+0.1019 121.4046\r
+0.1462 121.1795\r
+0.1837 120.8420\r
+0.2029 120.5044\r
+0.2385 118.8167\r
+0.2644 117.2415\r
+0.2798 116.9039\r
+0.3279 116.6789\r
+0.3923 116.6789\r
+0.4298 116.1163\r
+0.4615 114.0910\r
+0.5067 110.1530\r
+0.5404 104.6397\r
+0.5760 096.2010\r
+0.6067 089.5626\r
+0.6817 078.8736\r
+0.7692 067.9595\r
+0.7865 064.9216\r
+0.7990 062.5588\r
+0.8058 058.0582\r
+0.8192 050.5196\r
+0.8385 039.0430\r
+0.8625 027.5664\r
+0.8769 016.6523\r
+0.9019 000.0000\r
--- /dev/null
+;\r
+;Aerotech G12RC RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+G12RC 32 107 100 0.0511 0.131 AT\r
+0.03 18.549\r
+0.117 19.96\r
+0.239 20.64\r
+0.362 20.111\r
+0.519 18.982\r
+0.694 17.138\r
+0.886 15.02\r
+1.131 13.186\r
+1.375 11.915\r
+1.689 11.069\r
+2.021 10.363\r
+2.422 10.232\r
+3.172 9.677\r
+4.114 9.267\r
+5.039 8.857\r
+6.137 8.733\r
+7.132 8.607\r
+7.795 8.335\r
+7.952 8.196\r
+8.074 8.055\r
+8.179 6.924\r
+8.319 4.661\r
+8.476 1.973\r
+8.55 0\r
--- /dev/null
+;\r
+;Aerotech G25 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+G25 29 124 5-10-15 0.0625 0.1197 AeroTech\r
+0.035 30.499\r
+0.047 36.712\r
+0.059 41.18\r
+0.13 40.669\r
+0.177 38.969\r
+0.295 38.969\r
+0.343 40.947\r
+0.413 40.38\r
+0.437 38.69\r
+0.484 39.824\r
+0.532 37.845\r
+0.65 37.557\r
+0.721 38.969\r
+0.803 38.69\r
+0.85 37.279\r
+0.98 39.535\r
+1.063 36.434\r
+1.098 38.124\r
+1.252 37.845\r
+1.37 37.279\r
+1.583 37\r
+1.819 35.3\r
+1.984 33.61\r
+2.185 31.344\r
+2.315 28.809\r
+2.622 24.286\r
+3.024 18.917\r
+3.39 13.838\r
+3.839 7.624\r
+4.323 4.518\r
+4.783 2.541\r
+5.3 0\r
--- /dev/null
+;\r
+;Aerotech G33 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+G33 29 124 5-7 0.0722 0.1593 AT\r
+0.027 22.642\r
+0.061 42.201\r
+0.117 47.354\r
+0.243 46.678\r
+0.34 46.339\r
+0.438 47.384\r
+0.48 50.92\r
+0.508 46.359\r
+0.543 47.732\r
+0.662 45.693\r
+0.851 42.28\r
+1.039 41.266\r
+1.116 42.987\r
+1.193 39.226\r
+1.221 42.31\r
+1.312 38.888\r
+1.326 40.609\r
+1.479 38.221\r
+1.675 35.157\r
+1.843 32.77\r
+1.878 36.888\r
+1.899 32.093\r
+1.997 30.382\r
+2.13 26.622\r
+2.263 23.547\r
+2.444 19.11\r
+2.591 13.977\r
+2.752 8.502\r
+2.892 4.743\r
+3.053 2.014\r
+3.27 0\r
--- /dev/null
+;\r
+; 38-120\r
+; Created from TRA Certification Record issued 23 Nov 2006\r
+; Bill Wagstaff - 04/30/07\r
+G339N 38 97 0 0.049 0.190 AT\r
+0.009 371\r
+0.05 375\r
+0.10 375\r
+0.15 364\r
+0.20 349\r
+0.25 310\r
+0.30 264\r
+0.324 257\r
+0.342 39\r
+0.359 0\r
+;\r
+;\r
--- /dev/null
+;\r
+;\r
+G35EJ 29 98 4-7 0.05 0.1005 AeroTech\r
+0.01 39.14\r
+0.02 76.22\r
+0.05 64.46\r
+0.13 57.54\r
+0.21 57.53\r
+0.24 64.43\r
+0.25 57.06\r
+0.35 56.12\r
+0.43 55.2\r
+0.48 57.49\r
+0.51 52.41\r
+0.55 53.33\r
+0.76 50.54\r
+0.91 50.06\r
+1.11 44.96\r
+1.32 41.24\r
+1.55 35.68\r
+1.6 36.13\r
+1.63 33.36\r
+1.67 34.28\r
+1.8 30.12\r
+2 25.02\r
+2.14 21.32\r
+2.23 19.46\r
+2.3 15.77\r
+2.41 9.76\r
+2.53 6.52\r
+2.65 3.74\r
+2.74 1.88\r
+2.91 0\r
--- /dev/null
+;\r
+;Aerotech G38FJ RASP.ENG file made by Tim Van Milligan\r
+;For RockSim www.RockSim.com\r
+;File Created March 2, 2005\r
+;Thrust curve supplied by Aerotech for the molded case G38FJ motors.\r
+G38FJ 29 124 4-7 0.0597 0.1264 Aerotech\r
+0.024 52.928\r
+0.171 48.424\r
+0.497 45.045\r
+1 42.23\r
+1.279 39.978\r
+1.498 36.599\r
+1.783 30.406\r
+2.011 23.086\r
+2.272 10.135\r
+2.467 3.941\r
+2.64 0\r
--- /dev/null
+;\r
+;Aerotech G40W RASP.ENG file made by Tim Van Milligan\r
+;For RockSim www.RockSim.com\r
+;File Created March 2, 2005\r
+;Thrust curve supplied by Aerotech for the molded case G40W motors.\r
+G40W 29 124 4-7-10 0.0538 0.123 AeroTech\r
+0.024 74.325\r
+0.057 67.005\r
+0.252 65.879\r
+0.5 63.063\r
+0.765 60.248\r
+1 54.054\r
+1.25 47.298\r
+1.502 36.599\r
+1.751 25.338\r
+1.999 12.951\r
+2.121 3.941\r
+2.3 0\r
--- /dev/null
+; G53FJ based on Aerotech instruction sheet by C. Kobel 12/9/07\r
+G53FJ 29 124 5-7-10 0.060 0.152 AT\r
+ 0.012 44.898\r
+ 0.031 71.504\r
+ 0.064 80.234\r
+ 0.081 83.976\r
+ 0.100 86.47\r
+ 0.150 84.599\r
+ 0.200 81.897\r
+ 0.300 78.571\r
+ 0.400 76.493\r
+ 0.500 73.583\r
+ 0.600 70.881\r
+ 0.700 67.347\r
+ 0.800 63.813\r
+ 0.900 60.072\r
+ 1.000 54.667\r
+ 1.100 47.392\r
+ 1.200 39.909\r
+ 1.300 32.426\r
+ 1.400 25.983\r
+ 1.500 20.578\r
+ 1.600 10.601\r
+ 1.700 3.949\r
+ 1.800 1.247\r
+ 1.850 0.0\r
--- /dev/null
+;\r
+;Aerotech G54 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+G54 29 124 6-10-14 0.046 0.1365 AT\r
+0.018 10.953\r
+0.042 39.215\r
+0.083 66.888\r
+0.14 72.075\r
+0.223 74.958\r
+0.25 76.694\r
+0.282 80.156\r
+0.315 79.577\r
+0.336 79.577\r
+0.354 81.64\r
+0.365 77.841\r
+0.374 80.724\r
+0.389 76.694\r
+0.455 76.116\r
+0.523 74.39\r
+0.639 70.928\r
+0.722 67.467\r
+0.82 64.005\r
+0.897 58.817\r
+0.992 51.894\r
+1.084 43.824\r
+1.197 34.017\r
+1.268 28.251\r
+1.283 29.987\r
+1.295 27.104\r
+1.328 23.642\r
+1.366 16.719\r
+1.399 9.803\r
+1.435 4.612\r
+1.51 0\r
--- /dev/null
+;\r
+;Aerotech G55 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+G55 24 117 5-10-15 0.0625 0.1148 AeroTech\r
+0.009 81.136\r
+0.014 84.65\r
+0.034 80.557\r
+0.084 77.064\r
+0.13 71.823\r
+0.18 72.422\r
+0.206 68.919\r
+0.342 69.538\r
+0.483 68.989\r
+0.513 66.663\r
+0.543 68.42\r
+0.664 66.114\r
+0.876 66.164\r
+0.901 64.418\r
+0.997 65.026\r
+1.062 66.793\r
+1.088 63.879\r
+1.148 63.889\r
+1.158 66.813\r
+1.173 63.31\r
+1.209 62.741\r
+1.325 61.593\r
+1.395 59.277\r
+1.456 57.541\r
+1.486 58.129\r
+1.587 52.32\r
+1.708 40.094\r
+1.824 26.11\r
+1.95 15.63\r
+2.112 7.498\r
+2.258 3.446\r
+2.44 0\r
--- /dev/null
+;\r
+;G61W Data Entered by Tim Van Milligan\r
+;For RockSim: www.RockSim.com\r
+;Based on TRA Certification Test date: June 13, 2004\r
+;Not Approved by TRA or Aerotech\r
+G61W 38 106.7 6-10-14 0.0613 0.1904 AT\r
+0.008 3.083\r
+0.054 71.348\r
+0.089 72.229\r
+0.174 75.312\r
+0.216 78.394\r
+0.247 79.716\r
+0.502 81.037\r
+0.753 77.073\r
+1.001 72.669\r
+1.132 66.944\r
+1.252 55.933\r
+1.503 38.316\r
+1.754 10.13\r
+1.905 3.523\r
+2.04 0\r
--- /dev/null
+;\r
+;Aerotech G64 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+G64 29 124 4-8-10 0.0625 0.1512 AT\r
+0.014 54.325\r
+0.032 81.488\r
+0.059 98.31\r
+0.101 85.021\r
+0.165 83.847\r
+0.274 85.614\r
+0.37 87.39\r
+0.476 86.798\r
+0.503 91.516\r
+0.517 85.614\r
+0.585 83.847\r
+0.723 80.896\r
+0.745 82.07\r
+0.773 77.945\r
+0.883 75.576\r
+0.988 74.401\r
+1.093 69.673\r
+1.262 61.412\r
+1.28 61.994\r
+1.326 58.451\r
+1.372 54.907\r
+1.422 47.238\r
+1.505 34.841\r
+1.591 23.027\r
+1.701 13.581\r
+1.829 7.085\r
+1.902 4.133\r
+1.966 1.771\r
+2.09 0\r
--- /dev/null
+;\r
+; Aerotech G67R (Redline)\r
+;\r
+; AeroTech RMS-38/120 EZ Access Reloadable Motors (New! Hardware).\r
+; New AeroTech Redline Motor. Just announced on AeroTech's Website!\r
+; File produced 28 Feb 2005.\r
+;\r
+; The file was produced by scaling data points off the\r
+; thrust curve in the manufacturers catalog sheet.\r
+;\r
+; The motor is not yet on www.thrustcurve.org.\r
+; Hence the amateur file production.\r
+; The file was created by Stan Hemphill.\r
+; Contact at stanley_hemphill@hotmail.com.\r
+;\r
+; Motor Dia Len Delay Prop Gross Mfg\r
+G67R 38 106 4-6-8-9-10-12-13-15-17 0.0576 0.191 AT\r
+0.0400 004.9200\r
+0.0500 006.5600\r
+0.0600 009.8400\r
+0.0700 016.4100\r
+0.0800 032.8100\r
+0.1000 049.2200\r
+0.1300 068.0800\r
+0.1500 076.2900\r
+0.1800 080.3900\r
+0.2400 082.8500\r
+0.2600 085.3100\r
+0.3100 087.7700\r
+0.5100 089.4100\r
+0.5300 091.0500\r
+0.5600 087.7700\r
+0.6000 086.9500\r
+0.6100 088.5900\r
+0.6700 086.9500\r
+0.6900 085.3100\r
+0.7100 086.9500\r
+0.7200 085.3100\r
+0.7400 086.1300\r
+0.7700 085.3100\r
+0.8100 082.0300\r
+0.9500 078.7500\r
+1.0500 073.8200\r
+1.4300 053.3200\r
+1.5000 052.5000\r
+1.5200 050.8600\r
+1.5400 045.9400\r
+1.6200 011.4800\r
+1.6400 000.0000\r
--- /dev/null
+; Submitted to ThrustCurve.org by Chris Kobel (4/13/07)\r
+; G69N based on Aerotech instruction sheet by C. Kobel 3/29/07\r
+G69N 38 106 0 0.0622 0.195 AT\r
+ 0.020 51.972\r
+ 0.050 75.574\r
+ 0.100 76.709\r
+ 0.200 77.617\r
+ 0.300 79.206\r
+ 0.400 81.475\r
+ 0.500 84.425\r
+ 0.600 86.922\r
+ 0.700 88.737\r
+ 0.800 89.645\r
+ 0.900 91.688\r
+ 1.000 93.503\r
+ 1.100 94.411\r
+ 1.200 94.638\r
+ 1.300 93.957\r
+ 1.350 93.05\r
+ 1.400 89.418\r
+ 1.500 62.865\r
+ 1.600 33.362\r
+ 1.650 19.518\r
+ 1.700 12.028\r
+ 1.750 7.489\r
+ 1.800 4.539\r
+ 1.900 1.816\r
+ 2.000 0.0\r
--- /dev/null
+; G71R based on Aerotech instruction sheet by C. Kobel 3/29/07\r
+G71R 29 124 4-7-10 0.0569 0.147 AT\r
+ 0.000 0.389\r
+ 0.050 109.714\r
+ 0.100 117.884\r
+ 0.200 113.216\r
+ 0.300 109.714\r
+ 0.400 105.045\r
+ 0.500 99.21\r
+ 0.600 92.207\r
+ 0.700 83.258\r
+ 0.800 75.477\r
+ 0.900 68.085\r
+ 1.000 57.97\r
+ 1.100 47.465\r
+ 1.200 33.848\r
+ 1.300 21.009\r
+ 1.400 11.283\r
+ 1.500 5.447\r
+ 1.600 2.334\r
+ 1.700 0.0\r
--- /dev/null
+; RMS-29/40-120 Reload, 2 grain design, G71-XR (Redline propellent), with 4, 7,\r
+; 10 second delays\r
+G71-R 29 120 7 0.0569 0.147 AT\r
+ 0.0080 46.656\r
+ 0.015 74.248\r
+ 0.023 85.787\r
+ 0.031 100.336\r
+ 0.05 110.871\r
+ 0.062 116.891\r
+ 0.085 120.403\r
+ 0.116 119.901\r
+ 0.139 118.898\r
+ 0.158 116.891\r
+ 0.181 115.386\r
+ 0.216 114.383\r
+ 0.251 113.379\r
+ 0.278 111.373\r
+ 0.297 111.373\r
+ 0.309 114.383\r
+ 0.328 112.376\r
+ 0.355 109.366\r
+ 0.39 107.359\r
+ 0.432 104.851\r
+ 0.463 103.848\r
+ 0.494 100.837\r
+ 0.525 98.831\r
+ 0.552 95.821\r
+ 0.583 94.316\r
+ 0.606 92.309\r
+ 0.633 89.299\r
+ 0.653 87.292\r
+ 0.676 85.787\r
+ 0.699 81.272\r
+ 0.714 84.282\r
+ 0.734 81.272\r
+ 0.749 88.797\r
+ 0.772 80.269\r
+ 0.799 76.255\r
+ 0.826 73.747\r
+ 0.861 70.737\r
+ 0.876 73.747\r
+ 0.892 69.232\r
+ 0.915 69.733\r
+ 0.923 65.72\r
+ 0.942 63.713\r
+ 0.977 60.703\r
+ 1.008 58.195\r
+ 1.039 54.181\r
+ 1.077 50.168\r
+ 1.108 46.154\r
+ 1.12 50.168\r
+ 1.127 46.154\r
+ 1.143 43.144\r
+ 1.178 37.626\r
+ 1.212 32.609\r
+ 1.232 30.101\r
+ 1.255 26.589\r
+ 1.274 23.579\r
+ 1.301 20.569\r
+ 1.317 18.06\r
+ 1.344 15.552\r
+ 1.382 11.539\r
+ 1.417 10.034\r
+ 1.448 7.024\r
+ 1.486 6.02\r
+ 1.517 3.512\r
+ 1.552 3.01\r
+ 1.587 2.508\r
+ 1.618 1.003\r
+ 1.668 0.502\r
+ 1.707 0.502\r
+ 1.734 0.0\r
--- /dev/null
+; AeroTech G75J\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+G75J 29 194 10 0.112 0.23296 AT\r
+ 0.047 65.701\r
+ 0.143 68.564\r
+ 0.239 72.143\r
+ 0.334 73.261\r
+ 0.430 73.960\r
+ 0.526 75.036\r
+ 0.622 75.705\r
+ 0.718 75.030\r
+ 0.814 77.886\r
+ 0.909 76.183\r
+ 1.005 76.852\r
+ 1.101 75.729\r
+ 1.197 78.854\r
+ 1.293 78.669\r
+ 1.389 76.464\r
+ 1.484 76.440\r
+ 1.580 74.976\r
+ 1.676 72.657\r
+ 1.772 69.460\r
+ 1.868 62.121\r
+ 1.964 39.090\r
+ 2.059 19.703\r
+ 2.155 7.554\r
+ 2.251 2.062\r
+ 2.347 0.382\r
+ 2.443 0.000\r
--- /dev/null
+;\r
+; Aerotech G75J (Black Jack)\r
+;\r
+; AeroTech RMS-29/180 Easy Access Reloadable Motor Hardware.\r
+;\r
+; RASP.ENG file made from made from NAR or TMT published data.\r
+;\r
+; File produced May, 17 2004.\r
+;\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data from NAR or TMT files.\r
+;\r
+; The curve drawn with these data points is as accurate as could\r
+; could be made scaling the data from the curve on the TMT html\r
+; page. The file is 63 data points. NOT wRASP v1.6 compatible.\r
+;\r
+; The file was created by Stan Hemphill.\r
+; Contact at stanley_hemphill@hotmail.com.\r
+;\r
+; Motor Dia Len Delay Prop Gross Mfg\r
+G75J 29 194 1-3--4-6-7-9-10 0.114 0.236 AT\r
+0.0281 068.8604\r
+0.0380 078.6517\r
+0.0561 075.9230\r
+0.0660 073.0337\r
+0.0776 070.4655\r
+0.1139 069.3419\r
+0.1403 068.6998\r
+0.1667 067.5762\r
+0.1881 070.3050\r
+0.2013 069.0209\r
+0.2294 072.5522\r
+0.2541 076.4045\r
+0.2723 071.4286\r
+0.3102 076.2440\r
+0.3350 071.9101\r
+0.4208 075.6019\r
+0.4604 072.3917\r
+0.5215 079.2937\r
+0.5941 073.3547\r
+0.6436 080.0963\r
+0.7013 073.8363\r
+0.7393 076.4045\r
+0.7541 074.6388\r
+0.7657 077.3676\r
+0.7937 078.6517\r
+0.8036 077.0465\r
+0.8168 080.2568\r
+0.8267 075.2809\r
+0.8383 081.5409\r
+0.8581 075.7624\r
+0.8795 077.8491\r
+0.9340 074.1573\r
+0.9868 079.9358\r
+1.0380 076.7255\r
+1.0561 072.2311\r
+1.0941 078.8122\r
+1.1221 075.6019\r
+1.1502 080.8989\r
+1.1617 076.8860\r
+1.1848 080.0963\r
+1.1997 076.7255\r
+1.2327 078.8122\r
+1.2508 076.4045\r
+1.2871 082.5040\r
+1.3102 077.5281\r
+1.3267 081.8620\r
+1.3564 075.2809\r
+1.3729 080.0963\r
+1.4076 075.7624\r
+1.4884 079.9358\r
+1.5116 073.6758\r
+1.5297 081.0594\r
+1.5512 074.6388\r
+1.5611 080.8989\r
+1.6040 072.8732\r
+1.7211 075.6019\r
+1.7607 068.6998\r
+1.7789 070.1445\r
+1.8119 066.9342\r
+1.8267 070.7865\r
+1.8482 070.7865\r
+2.3878 000.0000\r
--- /dev/null
+; Curve fit of AT Instruction sheet by C. Kobel 7/29/08\r
+G76G 29 124 4-7-10 0.06 0.147 AT\r
+ 0.025 89.368\r
+ 0.042 133.581\r
+ 0.052 144.87\r
+ 0.067 154.277\r
+ 0.098 144.399\r
+ 0.117 136.873\r
+ 0.150 132.64\r
+ 0.196 129.348\r
+ 0.255 123.233\r
+ 0.299 118.059\r
+ 0.349 112.885\r
+ 0.399 108.652\r
+ 0.449 101.126\r
+ 0.486 101.597\r
+ 0.511 105.36\r
+ 0.516 118.53\r
+ 0.543 100.186\r
+ 0.601 95.482\r
+ 0.656 88.897\r
+ 0.720 81.842\r
+ 0.737 93.601\r
+ 0.754 80.431\r
+ 0.797 70.553\r
+ 0.856 63.498\r
+ 0.898 58.794\r
+ 0.948 51.739\r
+ 1.000 47.976\r
+ 1.063 43.273\r
+ 1.102 41.391\r
+ 1.152 39.04\r
+ 1.200 36.688\r
+ 1.301 30.103\r
+ 1.347 25.399\r
+ 1.401 19.755\r
+ 1.499 12.229\r
+ 1.547 7.996\r
+ 1.599 5.644\r
+ 1.699 2.352\r
+ 1.750 0.0\r
--- /dev/null
+; Exported using ThrustCurveTool, www.ThrustGear.com\r
+; NAR S&T Data, contributed by John DeMar\r
+G72 29 124 4-7-10 0.06 0.144 Aerotech\r
+ 0.0040 4.2403\r
+ 0.0060 9.6831\r
+ 0.016 54.397\r
+ 0.03 98.17\r
+ 0.034 108.509\r
+ 0.04 121.159\r
+ 0.048 133.047\r
+ 0.058 142.348\r
+ 0.068 147.209\r
+ 0.082 149.393\r
+ 0.112 146.89\r
+ 0.15 138.783\r
+ 0.17 136.039\r
+ 0.376 112.222\r
+ 0.466 103.079\r
+ 0.482 104.438\r
+ 0.56 92.6523\r
+ 0.606 92.8669\r
+ 0.644 84.2058\r
+ 0.748 73.9389\r
+ 0.76 74.2841\r
+ 0.79 69.704\r
+ 0.804 70.6\r
+ 0.822 66.7591\r
+ 0.856 62.6853\r
+ 1.154 39.9972\r
+ 1.374 19.9105\r
+ 1.474 12.5198\r
+ 1.574 7.41771\r
+ 1.78 1.19026\r
+ 2.00 0.0\r
--- /dev/null
+;\r
+; Aerotech G77R (Redline)\r
+;\r
+; AeroTech RMS-29/120 EZ Access Reloadable Motors (New! Hardware).\r
+; New AeroTech Redline Motor. Just announced on AeroTech's Website!\r
+; File produced 28 Feb 2005.\r
+;\r
+; The file was produced by scaling data points off the\r
+; thrust curve in the manufacturers catalog sheet.\r
+;\r
+; The motor is not yet on www.thrustcurve.org.\r
+; Hence the amateur file production.\r
+; The file was created by Stan Hemphill.\r
+; Contact at stanley_hemphill@hotmail.com.\r
+;\r
+; Motor Dia Len Delay Prop Gross Mfg\r
+G77R 29 150 4-6-8-9-10-12-13-15-17 0.0554 0.155 AT\r
+0.0132 014.8333\r
+0.0243 032.4479\r
+0.0331 046.3542\r
+0.0375 052.8438\r
+0.0463 056.5521\r
+0.0617 059.3333\r
+0.2580 073.2396\r
+0.6548 087.1458\r
+0.8709 089.0000\r
+0.8885 085.2917\r
+1.0252 086.2188\r
+1.0472 084.3646\r
+1.0715 086.2188\r
+1.1002 084.3646\r
+1.1332 085.2917\r
+1.1950 076.9479\r
+1.2104 076.0208\r
+1.2369 065.8229\r
+1.2611 043.5729\r
+1.2898 027.8125\r
+1.3317 012.0521\r
+1.3625 004.6354\r
+1.4000 000.0000\r
--- /dev/null
+; @File: G78G_Typ.txt, @Pts-I: 1001, @Pts-O: 31, @Sm: 0, @CO: 5%\r
+; @TI: 109.776, @TIa: 109.5639, @TIe: 0.0%, @ThMax: 102.242, @ThAvg: 79.5671, @Tb: 1.377\r
+; Exported using ThrustCurveTool, www.ThrustGear.com\r
+G78G 29 146 4-7-10, 0.0597 0.125 AT/RCS\r
+0.0040 2.76203\r
+0.0060 34.7707\r
+0.0080 44.326\r
+0.01 41.2789\r
+0.012 28.5933\r
+0.014 27.3926\r
+0.016 38.0574\r
+0.02 47.7036\r
+0.022 48.2012\r
+0.03 56.4344\r
+0.042 61.4034\r
+0.06 65.883\r
+0.212 84.526\r
+0.266 88.9218\r
+0.404 95.5932\r
+0.43 98.9746\r
+0.466 100.5362\r
+0.58 102.242\r
+0.694 101.3187\r
+0.86 95.4526\r
+1.1 90.4186\r
+1.132 82.5618\r
+1.156 72.6383\r
+1.168 64.903\r
+1.194 42.3389\r
+1.216 28.3424\r
+1.226 24.9802\r
+1.2559 17.40051\r
+1.2859 13.04651\r
+1.3819 5.05295\r
+1.4719 0.0\r
--- /dev/null
+;\r
+;G79W Data Entered by Tim Van Milligan\r
+;For RockSim: www.RockSim.com\r
+;Based on TRA Certification Test date: June 13, 2004\r
+;Not Approved by TRA or Aerotech\r
+G79W 29 149.86 6-10-14 0.0609 0.154 AT\r
+0.015 7.157\r
+0.074 91.937\r
+0.09 91.387\r
+0.114 84.781\r
+0.145 84.23\r
+0.201 89.185\r
+0.291 94.69\r
+0.4 98.544\r
+0.6 99.645\r
+0.708 96.892\r
+0.8 93.038\r
+0.915 85.331\r
+1 77.624\r
+1.085 71.017\r
+1.175 68.265\r
+1.199 44.59\r
+1.28 22.021\r
+1.36 4.955\r
+1.42 0\r
--- /dev/null
+; 136 N-sec G80, Certified Nov. 2007. As published by NAR S&T.\r
+;\r
+; @File: NewATG80.txt, @Pts-I: 905, @Pts-O: 31, @Sm: 0, @CO: 5%\r
+; @TI: 133.2377, @TIa: 133.1309, @TIe: 0.0%, @ThMax: 102.2, @ThAvg: 77.9911, @Tb: 1.707\r
+; Exported using ThrustCurveTool, www.ThrustGear.com\r
+G78 29 128 7,10,13 0.0625 0.1282 RCS/Aerotech\r
+0.0060 1.158086\r
+0.0080 7.48984\r
+0.01 33.7575\r
+0.012 64.5955\r
+0.014 62.9316\r
+0.016 58.8272\r
+0.018 74.9118\r
+0.02 85.0062\r
+0.022 91.1072\r
+0.026 93.9913\r
+0.028 98.4284\r
+0.032 97.652\r
+0.038 102.2\r
+0.074 97.3192\r
+0.124 95.4334\r
+0.376 99.3159\r
+0.68 99.4268\r
+0.994 91.6619\r
+1.2459 83.0095\r
+1.2819 77.3522\r
+1.3159 61.9332\r
+1.3599 44.6285\r
+1.4239 29.0986\r
+1.5039 21.2227\r
+1.5979 19.33693\r
+1.6559 16.34188\r
+1.6759 13.90147\r
+1.6779 11.79384\r
+1.7139 5.0938\r
+1.7339 1.388816\r
+1.8079 0.0\r
--- /dev/null
+; AEROTECH G80 RASP.ENG FILE\r
+; Note: this is for the 94 N-sec G80T certified in Sept. 2006\r
+G80 29 124 4-7-10 0.0479 0.1129998 AERO\r
+ 0.0060 84.371\r
+ 0.018 118.23\r
+ 0.027 109.378\r
+ 0.037 101.35\r
+ 0.042 105.565\r
+ 0.059 98.37\r
+ 0.113 95.821\r
+ 0.185 96.252\r
+ 0.277 94.556\r
+ 0.404 94.978\r
+ 0.526 91.165\r
+ 0.671 87.1\r
+ 0.792 82.675\r
+ 0.885 77.166\r
+ 0.943 73.353\r
+ 0.971 68.266\r
+ 0.997 57.237\r
+ 1.03 48.337\r
+ 1.059 40.279\r
+ 1.085 27.986\r
+ 1.112 17.811\r
+ 1.144 10.175\r
+ 1.168 5.512\r
+ 1.182 2.968\r
+ 1.21 0.0\r
--- /dev/null
+; Aerotech G80 RASP.ENG file made from NAR published data\r
+; File produced July 4, 2000\r
+; Note: This is for the 116N-sec G80T produced before Sept. 2006\r
+G80 29 124 4-7-10 0.0574 0.1049 A\r
+ 0.0060 101.291\r
+ 0.013 105.18\r
+ 0.031 103.473\r
+ 0.038 104.069\r
+ 0.067 99.803\r
+ 0.103 96.906\r
+ 0.181 94.733\r
+ 0.271 94.039\r
+ 0.303 96.985\r
+ 0.367 95.547\r
+ 0.428 94.842\r
+ 0.456 97.055\r
+ 0.463 92.65\r
+ 0.51 94.872\r
+ 0.596 93.444\r
+ 0.606 95.646\r
+ 0.624 91.985\r
+ 0.635 95.656\r
+ 0.646 91.995\r
+ 0.696 90.547\r
+ 0.846 85.477\r
+ 0.96 80.388\r
+ 1.071 74.564\r
+ 1.207 62.878\r
+ 1.296 52.639\r
+ 1.35 37.252\r
+ 1.382 20.397\r
+ 1.418 10.139\r
+ 1.457 4.281\r
+ 1.5 0.0\r
--- /dev/null
+; AeroTech H112J\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H112J 38 202 0 0.187712 0.379456 AT\r
+ 0.064 85.431\r
+ 0.194 101.938\r
+ 0.324 101.897\r
+ 0.454 102.839\r
+ 0.584 104.479\r
+ 0.715 103.845\r
+ 0.845 103.439\r
+ 0.975 104.286\r
+ 1.106 104.922\r
+ 1.236 104.390\r
+ 1.367 102.768\r
+ 1.497 102.237\r
+ 1.627 100.032\r
+ 1.757 98.345\r
+ 1.888 94.560\r
+ 2.018 89.018\r
+ 2.148 82.857\r
+ 2.279 77.685\r
+ 2.409 72.373\r
+ 2.540 67.041\r
+ 2.670 59.764\r
+ 2.800 37.616\r
+ 2.930 14.457\r
+ 3.060 4.642\r
+ 3.192 1.818\r
+ 3.323 0.000\r
--- /dev/null
+; AeroTech H123W\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H123W 38 154 0 0.126336 0.278656 AT\r
+ 0.047 96.764\r
+ 0.143 146.256\r
+ 0.239 150.699\r
+ 0.334 152.496\r
+ 0.430 151.248\r
+ 0.526 149.875\r
+ 0.622 150.200\r
+ 0.718 149.176\r
+ 0.814 144.858\r
+ 0.909 143.536\r
+ 1.005 141.414\r
+ 1.101 135.125\r
+ 1.198 125.288\r
+ 1.295 114.035\r
+ 1.391 101.556\r
+ 1.486 90.175\r
+ 1.582 78.694\r
+ 1.678 66.364\r
+ 1.774 54.260\r
+ 1.870 46.872\r
+ 1.966 38.186\r
+ 2.061 22.737\r
+ 2.157 13.478\r
+ 2.253 7.587\r
+ 2.350 5.252\r
+ 2.447 0.000\r
--- /dev/null
+; AeroTech H125W\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H125W 29 330 14 0.18816 0.32256 AT\r
+ 0.053 275.994\r
+ 0.161 241.473\r
+ 0.270 216.283\r
+ 0.378 199.482\r
+ 0.488 188.758\r
+ 0.597 182.349\r
+ 0.705 175.862\r
+ 0.814 169.365\r
+ 0.922 162.281\r
+ 1.031 154.276\r
+ 1.141 143.915\r
+ 1.249 133.480\r
+ 1.357 123.007\r
+ 1.466 113.058\r
+ 1.575 101.801\r
+ 1.684 88.423\r
+ 1.793 73.530\r
+ 1.901 60.425\r
+ 2.009 46.643\r
+ 2.119 36.785\r
+ 2.228 29.546\r
+ 2.336 23.641\r
+ 2.445 18.794\r
+ 2.553 14.728\r
+ 2.663 10.970\r
+ 2.772 0.000\r
--- /dev/null
+; AeroTech H128W\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H128W 29 194 14 0.09408 0.2016 AT\r
+ 0.024 102.423\r
+ 0.074 175.203\r
+ 0.125 181.379\r
+ 0.176 179.281\r
+ 0.226 181.423\r
+ 0.277 186.074\r
+ 0.328 190.483\r
+ 0.378 189.509\r
+ 0.429 186.162\r
+ 0.480 184.114\r
+ 0.530 180.300\r
+ 0.581 174.144\r
+ 0.632 172.019\r
+ 0.683 169.360\r
+ 0.734 166.017\r
+ 0.784 161.882\r
+ 0.835 157.331\r
+ 0.886 153.073\r
+ 0.936 151.985\r
+ 0.988 139.902\r
+ 1.039 87.865\r
+ 1.089 40.857\r
+ 1.140 14.328\r
+ 1.191 4.330\r
+ 1.242 1.550\r
+ 1.293 0.000\r
--- /dev/null
+; AeroTech H148R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H148R 38 152 0 0.14784 0.30912 AT\r
+ 0.027 77.232\r
+ 0.088 174.296\r
+ 0.148 185.046\r
+ 0.208 190.458\r
+ 0.268 192.497\r
+ 0.327 191.996\r
+ 0.388 188.790\r
+ 0.448 187.548\r
+ 0.509 182.697\r
+ 0.570 178.151\r
+ 0.630 172.906\r
+ 0.690 169.607\r
+ 0.750 164.510\r
+ 0.810 158.375\r
+ 0.870 153.019\r
+ 0.930 146.810\r
+ 0.991 139.443\r
+ 1.053 132.001\r
+ 1.112 123.271\r
+ 1.173 112.559\r
+ 1.233 104.737\r
+ 1.292 97.657\r
+ 1.353 94.932\r
+ 1.413 60.644\r
+ 1.474 13.007\r
+ 1.535 0.000\r
--- /dev/null
+; AeroTech H165R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H165R 29 194 0 0.0896 0.2016 AT\r
+ 0.018 55.047\r
+ 0.059 157.258\r
+ 0.101 168.509\r
+ 0.144 173.219\r
+ 0.186 179.237\r
+ 0.229 183.947\r
+ 0.271 187.872\r
+ 0.314 188.134\r
+ 0.356 188.919\r
+ 0.399 190.488\r
+ 0.441 187.349\r
+ 0.484 189.180\r
+ 0.525 186.547\r
+ 0.566 185.517\r
+ 0.609 180.807\r
+ 0.651 177.667\r
+ 0.694 170.602\r
+ 0.736 167.201\r
+ 0.779 158.828\r
+ 0.821 155.688\r
+ 0.864 153.333\r
+ 0.906 136.325\r
+ 0.949 73.526\r
+ 0.991 20.671\r
+ 1.034 4.448\r
+ 1.076 0.000\r
--- /dev/null
+; AeroTech H180W\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H180W 29 238 6 0.12096 0.2464 AT\r
+ 0.024 149.374\r
+ 0.075 222.273\r
+ 0.127 222.339\r
+ 0.178 227.835\r
+ 0.229 234.963\r
+ 0.281 238.162\r
+ 0.333 240.252\r
+ 0.384 243.126\r
+ 0.435 240.757\r
+ 0.487 240.724\r
+ 0.539 236.311\r
+ 0.590 236.799\r
+ 0.642 234.897\r
+ 0.694 232.763\r
+ 0.745 229.198\r
+ 0.796 228.816\r
+ 0.848 231.906\r
+ 0.899 225.853\r
+ 0.950 188.285\r
+ 1.002 134.679\r
+ 1.054 78.940\r
+ 1.105 34.557\r
+ 1.156 15.482\r
+ 1.208 7.279\r
+ 1.260 3.585\r
+ 1.313 0.000\r
--- /dev/null
+; AeroTech H210R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H210R 29 238 0 0.12096 0.2464 AT\r
+ 0.019 105.923\r
+ 0.059 211.290\r
+ 0.099 219.770\r
+ 0.139 229.639\r
+ 0.179 235.082\r
+ 0.220 241.594\r
+ 0.260 242.706\r
+ 0.300 245.347\r
+ 0.341 249.100\r
+ 0.381 253.410\r
+ 0.421 258.553\r
+ 0.461 260.221\r
+ 0.502 257.997\r
+ 0.543 259.248\r
+ 0.583 256.607\r
+ 0.623 252.436\r
+ 0.663 245.056\r
+ 0.704 219.909\r
+ 0.744 209.344\r
+ 0.784 200.587\r
+ 0.824 193.565\r
+ 0.865 184.323\r
+ 0.905 153.881\r
+ 0.945 58.244\r
+ 0.986 13.210\r
+ 1.027 0.000\r
--- /dev/null
+;\r
+;\r
+H220T 29 239 6-10-14 0.1064 0.2386 AT\r
+0 314.1\r
+0.1 236.61\r
+0.2 269.23\r
+0.3 261.06\r
+0.4 252.9\r
+0.72 252.9\r
+0.8 112.58\r
+0.9 9.78\r
+0.96 0\r
--- /dev/null
+; AeroTech H238T\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H238T 29 194 6 0.08064 0.18816 AT\r
+ 0.019 173.100\r
+ 0.059 206.876\r
+ 0.100 211.036\r
+ 0.141 214.353\r
+ 0.181 217.113\r
+ 0.222 219.343\r
+ 0.263 221.034\r
+ 0.303 224.951\r
+ 0.344 229.324\r
+ 0.384 229.308\r
+ 0.425 228.558\r
+ 0.466 226.573\r
+ 0.506 222.365\r
+ 0.547 219.205\r
+ 0.588 217.750\r
+ 0.628 213.350\r
+ 0.669 208.070\r
+ 0.709 200.966\r
+ 0.750 196.988\r
+ 0.791 151.387\r
+ 0.831 96.273\r
+ 0.872 59.475\r
+ 0.912 18.621\r
+ 0.953 7.986\r
+ 0.995 3.697\r
+ 1.036 0.000\r
--- /dev/null
+; AeroTech H242T\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H242T 38 152 10 0.11648 0.2688 AT\r
+ 0.030 164.060\r
+ 0.093 197.516\r
+ 0.155 204.324\r
+ 0.218 208.970\r
+ 0.280 211.481\r
+ 0.343 211.261\r
+ 0.405 209.291\r
+ 0.468 208.438\r
+ 0.531 206.707\r
+ 0.595 203.967\r
+ 0.657 198.175\r
+ 0.720 192.137\r
+ 0.782 186.840\r
+ 0.845 180.802\r
+ 0.907 174.635\r
+ 0.970 165.581\r
+ 1.033 159.726\r
+ 1.097 151.690\r
+ 1.159 144.167\r
+ 1.222 138.550\r
+ 1.284 119.114\r
+ 1.347 69.055\r
+ 1.409 21.396\r
+ 1.472 3.473\r
+ 1.535 0.594\r
+ 1.599 0.000\r
--- /dev/null
+; AeroTech H242T\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H242T 38 154 0 0.114688 0.264768 AT\r
+ 0.025 207.058\r
+ 0.077 237.941\r
+ 0.129 240.171\r
+ 0.181 241.906\r
+ 0.234 246.425\r
+ 0.287 245.971\r
+ 0.340 247.210\r
+ 0.392 246.516\r
+ 0.445 245.710\r
+ 0.498 244.881\r
+ 0.550 242.997\r
+ 0.602 240.518\r
+ 0.655 235.271\r
+ 0.708 229.464\r
+ 0.760 222.871\r
+ 0.813 216.278\r
+ 0.866 206.959\r
+ 0.919 195.458\r
+ 0.971 184.255\r
+ 1.023 174.490\r
+ 1.076 170.067\r
+ 1.129 99.588\r
+ 1.181 25.281\r
+ 1.233 12.839\r
+ 1.286 7.769\r
+ 1.340 0.000\r
--- /dev/null
+;I don't know that \r
+;these ejection \r
+;delays are correct. \r
+;This was made \r
+;using the Aerotech \r
+;test thrust curves. \r
+;By Tobin Yehle, \r
+;11/11/07.\r
+H250G 29 228.93 0-6-10-14 0.1163 0.256 Aerotech \r
+0.00250627 88.6915\r
+0.0125313 177.383\r
+0.0300752 279.719\r
+0.0726817 311.103\r
+0.145363 320.654\r
+0.24812 311.103\r
+0.308271 297.458\r
+0.398496 282.448\r
+0.45614 270.168\r
+0.593985 238.785\r
+0.691729 221.047\r
+0.799499 218.318\r
+0.83208 210.131\r
+0.844612 189.663\r
+0.907268 13.6449\r
+0.92 0\r
+;\r
--- /dev/null
+; AeroTech H268R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H268R 29 333 0 0.18368 0.3584 AT\r
+ 0.022 268.095\r
+ 0.069 332.446\r
+ 0.116 312.429\r
+ 0.164 306.810\r
+ 0.211 305.757\r
+ 0.259 306.576\r
+ 0.306 312.546\r
+ 0.354 319.687\r
+ 0.401 321.234\r
+ 0.448 320.974\r
+ 0.495 321.208\r
+ 0.542 321.794\r
+ 0.590 323.315\r
+ 0.638 322.847\r
+ 0.685 307.044\r
+ 0.732 291.593\r
+ 0.779 277.713\r
+ 0.826 267.127\r
+ 0.874 257.529\r
+ 0.921 252.846\r
+ 0.969 222.645\r
+ 1.016 159.668\r
+ 1.064 108.747\r
+ 1.111 52.091\r
+ 1.159 15.569\r
+ 1.207 0.000\r
--- /dev/null
+; AeroTech H45W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H45W 38 194 0 0.193984 0.294784 AT\r
+ 0.141 62.554\r
+ 0.424 63.504\r
+ 0.707 65.913\r
+ 0.992 68.370\r
+ 1.276 69.315\r
+ 1.559 68.523\r
+ 1.843 67.231\r
+ 2.127 65.705\r
+ 2.411 63.154\r
+ 2.695 59.210\r
+ 2.979 55.600\r
+ 3.264 50.790\r
+ 3.547 45.237\r
+ 3.830 39.835\r
+ 4.115 34.562\r
+ 4.399 29.213\r
+ 4.682 24.720\r
+ 4.967 20.616\r
+ 5.251 17.475\r
+ 5.534 14.498\r
+ 5.818 12.697\r
+ 6.102 10.792\r
+ 6.386 9.229\r
+ 6.670 7.754\r
+ 6.954 6.075\r
+ 7.239 0.000\r
--- /dev/null
+; AeroTech H55W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H55W 29 191 0 0.09856 0.18816 AT\r
+ 0.052 92.752\r
+ 0.159 98.019\r
+ 0.268 95.821\r
+ 0.375 96.162\r
+ 0.482 97.146\r
+ 0.591 96.927\r
+ 0.699 95.915\r
+ 0.806 94.447\r
+ 0.914 92.001\r
+ 1.022 88.756\r
+ 1.129 86.970\r
+ 1.236 84.072\r
+ 1.345 80.172\r
+ 1.453 74.343\r
+ 1.560 64.990\r
+ 1.668 46.380\r
+ 1.776 32.835\r
+ 1.883 25.734\r
+ 1.991 19.920\r
+ 2.099 16.229\r
+ 2.207 13.059\r
+ 2.315 10.451\r
+ 2.422 7.700\r
+ 2.530 5.696\r
+ 2.639 3.979\r
+ 2.747 0.000\r
--- /dev/null
+;\r
+; 38-240\r
+; Greg Gardner - 09/15/06\r
+H669N 38 152 0 0.096 0.252 AT\r
+0.003 141\r
+0.006 523\r
+0.009 934\r
+0.012 1178\r
+0.016 926\r
+0.019 684\r
+0.022 487\r
+0.025 415\r
+0.028 622\r
+0.031 801\r
+0.0325 906\r
+0.034 866\r
+0.037 755\r
+0.04 737\r
+0.043 666\r
+0.047 737\r
+0.0485 802\r
+0.05 755\r
+0.053 791\r
+0.056 765\r
+0.059 755\r
+0.062 747\r
+0.069 737\r
+0.075 761\r
+0.082 755\r
+0.088 729\r
+0.093 741\r
+0.1 751\r
+0.2 703\r
+0.25 640\r
+0.3 586\r
+0.306 584\r
+0.309 576\r
+0.312 506\r
+0.318 292\r
+0.325 93\r
+0.329 0\r
+;\r
--- /dev/null
+; AeroTech H70W\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H70W 29 229 0 0.11648 0.224 AT\r
+ 0.055 114.847\r
+ 0.169 131.427\r
+ 0.283 126.879\r
+ 0.397 127.136\r
+ 0.510 127.254\r
+ 0.625 125.894\r
+ 0.739 124.917\r
+ 0.852 122.031\r
+ 0.967 119.032\r
+ 1.080 115.071\r
+ 1.194 108.446\r
+ 1.308 102.273\r
+ 1.422 96.098\r
+ 1.535 86.953\r
+ 1.650 75.702\r
+ 1.764 62.402\r
+ 1.877 48.132\r
+ 1.992 36.862\r
+ 2.105 28.065\r
+ 2.219 21.592\r
+ 2.333 16.894\r
+ 2.447 12.686\r
+ 2.560 9.681\r
+ 2.675 6.818\r
+ 2.790 4.488\r
+ 2.904 0.000\r
--- /dev/null
+; AeroTech H73J\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H73J 38 152 6 0.14784 0.30912 AT\r
+ 0.056 49.252\r
+ 0.172 82.004\r
+ 0.287 82.130\r
+ 0.403 84.596\r
+ 0.520 86.883\r
+ 0.635 88.888\r
+ 0.751 89.652\r
+ 0.867 91.342\r
+ 0.982 92.980\r
+ 1.099 94.571\r
+ 1.215 94.641\r
+ 1.330 93.549\r
+ 1.446 91.447\r
+ 1.561 88.189\r
+ 1.678 82.436\r
+ 1.794 77.397\r
+ 1.909 70.772\r
+ 2.025 61.173\r
+ 2.141 51.161\r
+ 2.257 38.540\r
+ 2.373 21.562\r
+ 2.489 12.213\r
+ 2.604 7.327\r
+ 2.720 3.706\r
+ 2.836 1.777\r
+ 2.953 0.000\r
--- /dev/null
+; AeroTech H97J\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H97J 29 238 6 0.1344 0.27776 AT\r
+ 0.045 89.405\r
+ 0.136 100.289\r
+ 0.228 100.463\r
+ 0.320 102.019\r
+ 0.411 102.813\r
+ 0.503 103.550\r
+ 0.595 101.701\r
+ 0.686 103.056\r
+ 0.778 103.331\r
+ 0.870 102.613\r
+ 0.961 103.394\r
+ 1.053 100.963\r
+ 1.145 101.226\r
+ 1.236 99.864\r
+ 1.328 98.420\r
+ 1.420 96.827\r
+ 1.511 95.034\r
+ 1.603 93.241\r
+ 1.695 93.485\r
+ 1.786 88.068\r
+ 1.878 64.358\r
+ 1.970 30.264\r
+ 2.061 8.691\r
+ 2.153 1.399\r
+ 2.245 0.525\r
+ 2.336 0.000\r
--- /dev/null
+;\r
+; 38-360\r
+; Greg Gardner - 09/15/06\r
+H999N 38 203 0 0.144 0.331 AT\r
+0.003 204\r
+0.006 757\r
+0.009 1357\r
+0.012 1710\r
+0.016 1345\r
+0.019 995\r
+0.022 710\r
+0.025 606\r
+0.028 905\r
+0.031 1165\r
+0.0325 1311\r
+0.034 1258\r
+0.037 1098\r
+0.04 1072\r
+0.043 969\r
+0.047 1072\r
+0.0485 1166\r
+0.05 1098\r
+0.053 1160\r
+0.056 1117\r
+0.059 1103\r
+0.062 1093\r
+0.069 1076\r
+0.075 1110\r
+0.082 1105\r
+0.088 1065\r
+0.093 1082\r
+0.1 1092\r
+0.2 1022\r
+0.25 931\r
+0.3 853\r
+0.306 850\r
+0.309 838\r
+0.312 735\r
+0.318 435\r
+0.325 161\r
+0.329 0\r
+;\r
--- /dev/null
+; Aerotech I115W from TRA Cert Data\r
+I115W 54 156 6-10-14-P 0.229 0.58 AT\r
+ 0.034 12.095\r
+ 0.177 105.225\r
+ 0.206 113.087\r
+ 1.017 163.281\r
+ 1.166 161.466\r
+ 1.257 162.676\r
+ 1.343 166.909\r
+ 1.417 160.862\r
+ 1.514 162.676\r
+ 1.617 163.885\r
+ 1.686 160.257\r
+ 1.977 142.719\r
+ 2.497 106.435\r
+ 2.68 91.316\r
+ 2.994 72.569\r
+ 3.103 67.126\r
+ 3.189 65.917\r
+ 3.24 59.265\r
+ 3.291 42.937\r
+ 3.331 30.237\r
+ 3.377 20.561\r
+ 3.429 12.7\r
+ 3.491 7.257\r
+ 3.514 0.0\r
--- /dev/null
+; Aerotech I117FJ from TRA Cert Data\r
+I117FJ 54 156 6-10-14-P 0.253 0.58 AT\r
+ 0.014 65.9\r
+ 0.021 94.456\r
+ 0.055 120.816\r
+ 0.089 107.636\r
+ 0.179 107.636\r
+ 0.344 124.111\r
+ 0.385 133.996\r
+ 0.447 123.013\r
+ 0.509 138.389\r
+ 0.564 131.799\r
+ 0.646 146.077\r
+ 0.708 144.979\r
+ 0.736 127.406\r
+ 0.75 149.372\r
+ 0.798 141.684\r
+ 0.825 164.749\r
+ 0.866 152.667\r
+ 1.004 158.159\r
+ 1.141 155.962\r
+ 1.224 154.864\r
+ 1.492 155.962\r
+ 2.304 121.914\r
+ 2.407 118.619\r
+ 2.482 117.521\r
+ 2.544 128.504\r
+ 2.599 112.029\r
+ 2.654 75.784\r
+ 2.695 45.031\r
+ 2.75 16.475\r
+ 2.771 8.787\r
+ 2.806 0.0\r
--- /dev/null
+;Entered by Jim Yehle\r
+;from TRA cert document\r
+I1299N 38 249 1000 0.192 0.422 AT-RMS \r
+0 15.7171\r
+0.00361 222.5\r
+0.0115 1112\r
+0.0134228 1237.11\r
+0.02 1287\r
+0.04 1359\r
+0.1 1451\r
+0.12 1470\r
+0.18 1491\r
+0.2 1483\r
+0.22 1462\r
+0.24 1399\r
+0.28 1208\r
+0.294743 1131.63\r
+0.3 1065\r
+0.304251 974.46\r
+0.32 305\r
+0.330537 55.0098\r
+0.333893 11.7878\r
+0.34 0\r
+;\r
--- /dev/null
+; AeroTech I132W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I132W 38 335 0 0.365568 0.512064 AT\r
+ 0.096 204.011\r
+ 0.290 174.236\r
+ 0.484 168.865\r
+ 0.679 170.783\r
+ 0.874 173.028\r
+ 1.069 174.287\r
+ 1.264 174.647\r
+ 1.458 174.364\r
+ 1.652 174.645\r
+ 1.847 173.002\r
+ 2.042 169.209\r
+ 2.236 164.309\r
+ 2.431 157.149\r
+ 2.626 149.580\r
+ 2.821 138.360\r
+ 3.016 124.171\r
+ 3.210 107.626\r
+ 3.404 89.785\r
+ 3.599 71.747\r
+ 3.794 55.124\r
+ 3.989 42.264\r
+ 4.183 31.373\r
+ 4.378 21.980\r
+ 4.573 14.389\r
+ 4.768 8.794\r
+ 4.962 0.000\r
--- /dev/null
+; AeroTech I154J\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I154J 38 250 0 0.25088 0.491904 AT\r
+ 0.066 120.409\r
+ 0.199 150.638\r
+ 0.332 151.666\r
+ 0.466 156.806\r
+ 0.599 150.331\r
+ 0.732 150.602\r
+ 0.866 145.101\r
+ 0.999 144.469\r
+ 1.133 145.159\r
+ 1.268 145.912\r
+ 1.401 141.710\r
+ 1.534 142.828\r
+ 1.668 141.187\r
+ 1.801 140.970\r
+ 1.934 137.832\r
+ 2.068 128.417\r
+ 2.202 122.339\r
+ 2.336 111.986\r
+ 2.470 105.295\r
+ 2.603 96.602\r
+ 2.736 90.469\r
+ 2.870 57.427\r
+ 3.003 20.489\r
+ 3.136 4.707\r
+ 3.271 2.966\r
+ 3.405 0.000\r
--- /dev/null
+; AeroTech I161W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I161W 38 191 0 0.189952 0.370048 AT\r
+ 0.043 178.900\r
+ 0.131 206.770\r
+ 0.221 206.101\r
+ 0.310 205.175\r
+ 0.400 206.924\r
+ 0.490 210.603\r
+ 0.579 210.475\r
+ 0.669 211.555\r
+ 0.758 212.379\r
+ 0.848 212.096\r
+ 0.938 209.060\r
+ 1.027 202.345\r
+ 1.116 192.439\r
+ 1.204 179.499\r
+ 1.294 162.159\r
+ 1.383 148.446\r
+ 1.473 135.222\r
+ 1.563 120.095\r
+ 1.652 104.041\r
+ 1.742 87.962\r
+ 1.831 74.789\r
+ 1.921 54.362\r
+ 2.010 23.386\r
+ 2.100 7.332\r
+ 2.190 5.171\r
+ 2.279 0.000\r
--- /dev/null
+; AeroTech I195J\r
+; Copyright Tripoli Motor Testing 1996 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I195J 38 298 10 0.3136 0.59136 AT\r
+ 0.050 258.670\r
+ 0.152 353.638\r
+ 0.254 300.655\r
+ 0.356 265.354\r
+ 0.458 266.338\r
+ 0.560 283.233\r
+ 0.662 332.442\r
+ 0.765 283.040\r
+ 0.867 230.795\r
+ 0.969 222.867\r
+ 1.071 217.091\r
+ 1.173 210.600\r
+ 1.275 202.722\r
+ 1.377 192.671\r
+ 1.479 182.571\r
+ 1.581 171.964\r
+ 1.683 162.238\r
+ 1.785 148.138\r
+ 1.888 130.259\r
+ 1.990 107.022\r
+ 2.092 80.230\r
+ 2.194 51.074\r
+ 2.296 26.313\r
+ 2.398 10.397\r
+ 2.500 3.977\r
+ 2.602 0.000\r
--- /dev/null
+; AeroTech I195J\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I195J 38 297 0 0.296576 0.563136 AT\r
+ 0.033 190.099\r
+ 0.103 354.046\r
+ 0.173 393.473\r
+ 0.243 414.842\r
+ 0.314 379.747\r
+ 0.383 364.640\r
+ 0.453 364.776\r
+ 0.524 357.242\r
+ 0.594 355.802\r
+ 0.664 355.644\r
+ 0.734 353.557\r
+ 0.804 339.941\r
+ 0.874 309.753\r
+ 0.944 275.017\r
+ 1.014 243.739\r
+ 1.084 218.135\r
+ 1.154 197.291\r
+ 1.224 173.680\r
+ 1.295 147.000\r
+ 1.365 116.506\r
+ 1.434 83.105\r
+ 1.505 51.011\r
+ 1.575 26.480\r
+ 1.645 13.927\r
+ 1.716 7.273\r
+ 1.786 0.000\r
--- /dev/null
+; AeroTech I200W\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I200W 29 333 0 0.181888 0.357504 AT\r
+ 0.033 303.951\r
+ 0.103 273.452\r
+ 0.174 276.061\r
+ 0.245 271.625\r
+ 0.316 268.233\r
+ 0.386 258.449\r
+ 0.457 252.480\r
+ 0.528 246.642\r
+ 0.599 242.304\r
+ 0.670 237.737\r
+ 0.741 234.769\r
+ 0.811 233.171\r
+ 0.882 230.660\r
+ 0.953 224.985\r
+ 1.024 221.658\r
+ 1.095 214.548\r
+ 1.166 177.365\r
+ 1.236 154.208\r
+ 1.307 119.146\r
+ 1.378 91.586\r
+ 1.449 65.330\r
+ 1.520 32.877\r
+ 1.591 28.702\r
+ 1.661 22.211\r
+ 1.732 15.558\r
+ 1.803 0.000\r
--- /dev/null
+; AeroTech I211W\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I211W 38 240 0 0.247296 0.466368 AT\r
+ 0.044 257.326\r
+ 0.134 295.533\r
+ 0.226 296.087\r
+ 0.318 298.204\r
+ 0.408 295.082\r
+ 0.499 287.669\r
+ 0.591 282.578\r
+ 0.682 272.875\r
+ 0.773 266.997\r
+ 0.864 257.602\r
+ 0.955 250.495\r
+ 1.047 238.574\r
+ 1.138 228.571\r
+ 1.228 215.135\r
+ 1.320 198.047\r
+ 1.411 180.631\r
+ 1.502 161.261\r
+ 1.593 146.708\r
+ 1.684 134.484\r
+ 1.776 101.241\r
+ 1.867 52.688\r
+ 1.957 35.461\r
+ 2.049 24.321\r
+ 2.141 11.165\r
+ 2.232 4.587\r
+ 2.324 0.000\r
--- /dev/null
+; Aerotech I215R from TRA Cert Data\r
+I215R 54 156 6-10-14-P 0.20800000000000002 0.527 AT\r
+ 0.049 88.39\r
+ 0.089 154.683\r
+ 0.094 206.914\r
+ 0.178 245.083\r
+ 0.251 255.127\r
+ 0.325 259.145\r
+ 0.404 249.1\r
+ 0.582 257.136\r
+ 0.631 253.118\r
+ 0.7 253.118\r
+ 0.774 245.083\r
+ 1.001 239.056\r
+ 1.509 192.852\r
+ 1.681 178.79\r
+ 1.716 180.799\r
+ 1.746 190.843\r
+ 1.775 178.79\r
+ 1.8 90.399\r
+ 1.82 34.151\r
+ 1.859 0.0\r
--- /dev/null
+; AeroTech I218R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I218R 38 191 0 0.19264 0.37184 AT\r
+ 0.027 136.078\r
+ 0.088 275.030\r
+ 0.148 280.998\r
+ 0.208 284.371\r
+ 0.268 284.037\r
+ 0.327 279.311\r
+ 0.388 277.791\r
+ 0.448 276.309\r
+ 0.509 269.384\r
+ 0.570 266.041\r
+ 0.630 261.907\r
+ 0.690 256.366\r
+ 0.750 250.565\r
+ 0.810 242.206\r
+ 0.870 234.607\r
+ 0.930 225.488\r
+ 0.991 216.166\r
+ 1.053 205.415\r
+ 1.112 193.238\r
+ 1.173 177.206\r
+ 1.233 161.304\r
+ 1.292 139.118\r
+ 1.353 96.082\r
+ 1.413 38.848\r
+ 1.474 5.978\r
+ 1.535 0.000\r
--- /dev/null
+; AeroTech I225FJ\r
+; Curvefit to instruction sheet on Aerotech website (12/27/06)\r
+; by Chris Kobel\r
+; burn time: 1.8 seconds\r
+; total impulse: 350.5 newton-seconds\r
+; average thrust: 43.8 pounds\r
+I225FJ 38 202 6-10-14 0.2417 0.486 AT\r
+ 0.04 213.6\r
+ 0.10 213.6\r
+ 0.20 218.1\r
+ 0.28 235.9\r
+ 0.30 249.2\r
+ 0.40 262.6\r
+ 0.50 267.0\r
+ 0.60 271.5\r
+ 0.70 275.9\r
+ 0.80 275.9\r
+ 0.87 271.5\r
+ 0.90 258.1\r
+ 1.00 240.3\r
+ 1.10 218.1\r
+ 1.20 200.3\r
+ 1.30 178.0\r
+ 1.40 160.2\r
+ 1.50 97.9\r
+ 1.60 40.1\r
+ 1.70 13.4\r
+ 1.80 0.0\r
--- /dev/null
+;\r
+I229T 54.0 156.00 6-10-14 0.20600 0.52000 AT\r
+ 0.01 44.73 \r
+ 0.02 216.65 \r
+ 0.19 244.72 \r
+ 0.40 266.64 \r
+ 0.49 266.64 \r
+ 0.52 273.66 \r
+ 0.59 271.90 \r
+ 0.75 272.78 \r
+ 0.84 268.40 \r
+ 0.88 271.90 \r
+ 0.97 262.26 \r
+ 1.00 265.76 \r
+ 1.07 255.24 \r
+ 1.21 249.10 \r
+ 1.51 219.28 \r
+ 1.60 230.68 \r
+ 1.63 191.21 \r
+ 1.64 132.44 \r
+ 1.66 86.83 \r
+ 1.70 44.73 \r
+ 1.71 21.05 \r
+ 1.73 0.00 \r
+;\r
--- /dev/null
+;Ejection delays may not be corrrect.\r
+;From Aerotech pre-cert data.\r
+;Created 11/11/07 by Jim Yehle.\r
+I245G 38 192.532 0-6-10-14 0.1813 0.365 Aerotech \r
+0.0244989 234.061\r
+0.0550162 257.888\r
+0.0868597 368.567\r
+0.106904 382.335\r
+0.13363 390.808\r
+0.200445 405.635\r
+0.262806 410.931\r
+0.302895 411.99\r
+0.363029 408.813\r
+0.401294 398.43\r
+0.501114 363.271\r
+0.594655 320.907\r
+0.68932 278.355\r
+0.797327 212.879\r
+0.893204 181.477\r
+1.00647 154.187\r
+1.09061 133.72\r
+1.16036 120.737\r
+1.1804 122.856\r
+1.23625 106.43\r
+1.30421 75.0467\r
+1.3608 36.0094\r
+1.40312 19.0638\r
+1.43875 5.2955\r
+1.46325 0\r
+;\r
--- /dev/null
+; AeroTech I284W\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I284W 38 298 10 0.3136 0.55552 AT\r
+ 0.033 370.682\r
+ 0.103 483.606\r
+ 0.174 483.282\r
+ 0.245 486.856\r
+ 0.316 490.842\r
+ 0.386 499.428\r
+ 0.457 508.800\r
+ 0.528 506.326\r
+ 0.599 485.287\r
+ 0.670 481.043\r
+ 0.741 455.776\r
+ 0.811 426.920\r
+ 0.882 393.422\r
+ 0.953 367.404\r
+ 1.024 347.490\r
+ 1.095 325.191\r
+ 1.166 304.064\r
+ 1.236 284.158\r
+ 1.307 271.165\r
+ 1.378 228.579\r
+ 1.449 130.521\r
+ 1.520 57.212\r
+ 1.591 29.552\r
+ 1.661 16.413\r
+ 1.732 10.365\r
+ 1.803 0.000\r
--- /dev/null
+; AeroTech I284W\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I284W 38 297 0 0.310016 0.555072 AT\r
+ 0.041 422.031\r
+ 0.125 448.597\r
+ 0.210 459.029\r
+ 0.295 451.940\r
+ 0.379 439.556\r
+ 0.465 427.370\r
+ 0.549 407.558\r
+ 0.633 399.734\r
+ 0.719 380.049\r
+ 0.803 368.042\r
+ 0.887 352.020\r
+ 0.973 342.102\r
+ 1.057 325.767\r
+ 1.142 306.936\r
+ 1.227 292.029\r
+ 1.311 267.283\r
+ 1.396 251.784\r
+ 1.481 227.534\r
+ 1.566 210.504\r
+ 1.650 168.299\r
+ 1.735 110.789\r
+ 1.820 71.036\r
+ 1.904 32.505\r
+ 1.990 17.537\r
+ 2.075 7.317\r
+ 2.160 0.000\r
--- /dev/null
+; AeroTech I285R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I285R 38 250 0 0.25088 0.4928 AT\r
+ 0.027 171.405\r
+ 0.088 325.573\r
+ 0.148 341.697\r
+ 0.208 358.916\r
+ 0.268 373.706\r
+ 0.327 373.966\r
+ 0.388 368.442\r
+ 0.448 367.497\r
+ 0.507 361.900\r
+ 0.568 351.928\r
+ 0.628 346.109\r
+ 0.687 340.993\r
+ 0.749 329.382\r
+ 0.810 321.625\r
+ 0.870 310.856\r
+ 0.930 295.955\r
+ 0.990 283.704\r
+ 1.050 269.655\r
+ 1.110 253.419\r
+ 1.170 240.222\r
+ 1.230 224.116\r
+ 1.290 204.118\r
+ 1.350 118.730\r
+ 1.410 23.483\r
+ 1.471 2.046\r
+ 1.532 0.000\r
--- /dev/null
+;\r
+;\r
+I300T 38 250 6-10-14 0.2216 0.4405 AT\r
+0 473.17\r
+0.1 395.68\r
+0.2 375.31\r
+0.3 367.14\r
+0.4 358.97\r
+0.5 346.72\r
+0.6 338.56\r
+0.7 318.19\r
+0.8 305.94\r
+0.9 295.35\r
+1.07 269.23\r
+1.1 258.01\r
+1.2 246.79\r
+1.3 179.49\r
+1.4 48.95\r
+1.5 13.91\r
+1.6 0\r
--- /dev/null
+; I305FJ based on Aerotech instruction sheet by C. Kobel 3/30/07\r
+I305FJ 38 298 6-10-14 0.302 0.581 AT\r
+ 0.020 341.398\r
+ 0.100 365.497\r
+ 0.200 383.571\r
+ 0.300 403.653\r
+ 0.400 405.662\r
+ 0.500 405.662\r
+ 0.600 404.657\r
+ 0.700 374.534\r
+ 0.800 342.402\r
+ 0.900 309.267\r
+ 1.000 272.115\r
+ 1.100 238.979\r
+ 1.150 224.921\r
+ 1.200 194.798\r
+ 1.300 119.489\r
+ 1.400 62.255\r
+ 1.450 33.136\r
+ 1.500 23.095\r
+ 1.600 0.0\r
--- /dev/null
+; AeroTech I357T\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I357T 38 203 14 0.1792 0.34944 AT\r
+ 0.028 311.629\r
+ 0.087 351.768\r
+ 0.147 349.074\r
+ 0.206 346.175\r
+ 0.266 341.229\r
+ 0.325 336.857\r
+ 0.384 333.748\r
+ 0.444 326.960\r
+ 0.503 319.679\r
+ 0.563 312.533\r
+ 0.622 300.790\r
+ 0.681 292.787\r
+ 0.741 283.766\r
+ 0.800 274.578\r
+ 0.859 264.915\r
+ 0.919 254.273\r
+ 0.978 241.755\r
+ 1.037 229.020\r
+ 1.097 216.238\r
+ 1.156 187.776\r
+ 1.216 109.940\r
+ 1.275 56.459\r
+ 1.334 24.476\r
+ 1.394 10.977\r
+ 1.454 3.450\r
+ 1.515 0.000\r
--- /dev/null
+; AeroTech I364FJ\r
+; Curvefit to instruction sheet on Aerotech website (12/27/06)\r
+; by Chris Kobel\r
+; burn time: 1.7 seconds\r
+; total impulse: 551.2 newton-seconds\r
+; average thrust: 72.9 pounds\r
+I364FJ 38 345 6-10-14 0.3625 0.678 AT\r
+ 0.02 356.0\r
+ 0.10 373.8\r
+ 0.20 387.2\r
+ 0.30 400.5\r
+ 0.40 400.5\r
+ 0.50 409.4\r
+ 0.60 413.9\r
+ 0.70 409.4\r
+ 0.80 382.7\r
+ 0.90 373.8\r
+ 1.00 351.6\r
+ 1.10 333.8\r
+ 1.20 320.4\r
+ 1.30 311.5\r
+ 1.40 244.8\r
+ 1.50 178.0\r
+ 1.60 80.1\r
+ 1.70 0.0\r
--- /dev/null
+; AeroTech I366R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I366R 38 298 0 0.3136 0.55552 AT\r
+ 0.027 323.256\r
+ 0.088 485.393\r
+ 0.148 483.744\r
+ 0.208 479.926\r
+ 0.268 473.365\r
+ 0.327 466.192\r
+ 0.388 457.444\r
+ 0.448 448.751\r
+ 0.509 441.477\r
+ 0.570 430.236\r
+ 0.630 421.524\r
+ 0.690 411.757\r
+ 0.750 398.876\r
+ 0.810 387.496\r
+ 0.870 375.430\r
+ 0.930 361.325\r
+ 0.991 345.057\r
+ 1.053 330.392\r
+ 1.112 312.636\r
+ 1.173 293.508\r
+ 1.233 275.085\r
+ 1.292 262.408\r
+ 1.353 230.881\r
+ 1.413 118.008\r
+ 1.474 23.611\r
+ 1.535 0.000\r
--- /dev/null
+; AeroTech I435T\r
+; Copyright Tripoli Motor Testing 1996 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I435T 38 298 6 0.28672 0.52864 AT\r
+ 0.026 684.626\r
+ 0.080 702.334\r
+ 0.134 655.130\r
+ 0.190 638.942\r
+ 0.245 624.098\r
+ 0.299 611.802\r
+ 0.354 602.601\r
+ 0.409 590.237\r
+ 0.464 575.712\r
+ 0.519 563.654\r
+ 0.574 548.912\r
+ 0.628 527.885\r
+ 0.683 504.211\r
+ 0.739 480.412\r
+ 0.793 459.219\r
+ 0.848 436.771\r
+ 0.903 414.493\r
+ 0.957 392.151\r
+ 1.012 366.634\r
+ 1.068 299.670\r
+ 1.122 182.639\r
+ 1.177 106.457\r
+ 1.232 55.447\r
+ 1.286 23.628\r
+ 1.342 11.052\r
+ 1.397 0.000\r
--- /dev/null
+; AeroTech I435T\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I435T 38 297 0 0.26656 0.513408 AT\r
+ 0.024 808.049\r
+ 0.074 749.691\r
+ 0.124 709.215\r
+ 0.174 656.216\r
+ 0.224 636.578\r
+ 0.274 621.839\r
+ 0.324 592.267\r
+ 0.374 584.551\r
+ 0.424 573.277\r
+ 0.474 547.725\r
+ 0.524 539.962\r
+ 0.574 525.268\r
+ 0.624 500.456\r
+ 0.674 484.978\r
+ 0.724 464.323\r
+ 0.774 442.837\r
+ 0.824 424.540\r
+ 0.874 405.872\r
+ 0.924 393.443\r
+ 0.974 317.157\r
+ 1.024 217.630\r
+ 1.074 126.188\r
+ 1.124 74.391\r
+ 1.174 30.034\r
+ 1.224 9.380\r
+ 1.274 0.000\r
--- /dev/null
+; Aerotech I599N from TRA Cert Data\r
+I599N 54 156 100 0.195 0.5200512 AT\r
+ 0.0070 179.424\r
+ 0.01 495.908\r
+ 0.012 578.144\r
+ 0.014 647.92\r
+ 0.017 797.44\r
+ 0.024 593.096\r
+ 0.032 647.92\r
+ 0.045 677.824\r
+ 0.051 702.744\r
+ 0.055 677.824\r
+ 0.062 720.188\r
+ 0.076 707.728\r
+ 0.12 752.584\r
+ 0.202 757.568\r
+ 0.225 755.076\r
+ 0.241 735.14\r
+ 0.25 720.188\r
+ 0.263 730.156\r
+ 0.329 722.68\r
+ 0.399 697.76\r
+ 0.45 667.856\r
+ 0.501 618.016\r
+ 0.536 585.62\r
+ 0.55 580.636\r
+ 0.564 585.62\r
+ 0.578 632.968\r
+ 0.581 555.716\r
+ 0.584 426.132\r
+ 0.589 299.04\r
+ 0.595 176.932\r
+ 0.598 112.14\r
+ 0.604 57.316\r
+ 0.608 24.92\r
+ 0.619 12.46\r
+ 0.623 0.0\r
--- /dev/null
+;\r
+;I600R Data Entered by Tim Van Milligan\r
+;For RockSim: www.RockSim.com\r
+;Based on Aerotech's Reload Kit Instruction Sheet.\r
+;Not Officially Approved by TRA or Aerotech\r
+I600R 38 344.68 6-10-14 0.3237 0.617 AT\r
+0.005 40.438\r
+0.046 817.754\r
+0.059 813.261\r
+0.1 772.822\r
+0.2 736.877\r
+0.4 696.439\r
+0.5 669.48\r
+0.6 620.055\r
+0.796 539.178\r
+0.894 485.261\r
+0.951 453.809\r
+0.964 435.836\r
+1 274.082\r
+1.052 152.767\r
+1.106 62.904\r
+1.144 13.48\r
+1.18 0\r
--- /dev/null
+; AeroTech I65W\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I65W 54 235 0 0.41216 0.7616 AT\r
+ 0.180 125.414\r
+ 0.544 139.304\r
+ 0.908 145.369\r
+ 1.273 148.283\r
+ 1.638 146.745\r
+ 2.002 139.049\r
+ 2.367 131.200\r
+ 2.731 123.276\r
+ 3.096 113.454\r
+ 3.460 102.368\r
+ 3.825 90.210\r
+ 4.190 78.084\r
+ 4.554 66.812\r
+ 4.919 55.780\r
+ 5.283 47.281\r
+ 5.648 39.154\r
+ 6.012 32.528\r
+ 6.377 27.069\r
+ 6.742 22.099\r
+ 7.106 18.095\r
+ 7.471 14.819\r
+ 7.835 12.097\r
+ 8.200 9.763\r
+ 8.565 7.875\r
+ 8.929 5.999\r
+ 9.294 0.000\r
--- /dev/null
+; AeroTech J125\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J125 54 368 0 0.63392 1.288 AT\r
+ 0.174 223.931\r
+ 0.525 254.842\r
+ 0.877 275.347\r
+ 1.229 285.163\r
+ 1.581 280.333\r
+ 1.933 264.476\r
+ 2.285 244.373\r
+ 2.638 223.774\r
+ 2.990 204.720\r
+ 3.342 185.434\r
+ 3.694 166.807\r
+ 4.046 147.653\r
+ 4.398 127.914\r
+ 4.750 108.483\r
+ 5.102 92.582\r
+ 5.454 77.817\r
+ 5.806 63.844\r
+ 6.158 53.017\r
+ 6.510 44.507\r
+ 6.862 37.543\r
+ 7.215 32.205\r
+ 7.567 27.212\r
+ 7.919 22.847\r
+ 8.271 18.596\r
+ 8.623 14.790\r
+ 8.975 0.000\r
--- /dev/null
+;\r
+; AT 54-852\r
+; Greg Gardner - 09/15/06\r
+J1299N 54 230 0 0.3716 0.834 AT\r
+0.01 548\r
+0.02 1152\r
+0.03 1232\r
+0.04 1277\r
+0.05 1272\r
+0.06 1288\r
+0.07 1333\r
+0.08 1347\r
+0.09 1378\r
+0.10 1383\r
+0.12 1405\r
+0.14 1410\r
+0.16 1440\r
+0.18 1444\r
+0.20 1446\r
+0.25 1449\r
+0.30 1452\r
+0.35 1448\r
+0.40 1440\r
+0.45 1405\r
+0.50 1320\r
+0.55 1248\r
+0.57 1224\r
+0.59 1210\r
+0.60 1180\r
+0.61 1188\r
+0.615 1195\r
+0.62 1188\r
+0.63 510\r
+0.64 220\r
+0.65 96\r
+0.66 46\r
+0.67 26\r
+0.678 0\r
+;\r
--- /dev/null
+; AeroTech J135W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J135W 54 368 0 0.62272 1.14106 AT\r
+ 0.147 226.295\r
+ 0.444 243.688\r
+ 0.742 250.916\r
+ 1.040 257.345\r
+ 1.338 259.308\r
+ 1.635 253.727\r
+ 1.933 246.071\r
+ 2.231 235.780\r
+ 2.529 221.775\r
+ 2.827 205.143\r
+ 3.125 183.570\r
+ 3.423 161.103\r
+ 3.720 140.983\r
+ 4.017 122.984\r
+ 4.315 106.605\r
+ 4.612 91.959\r
+ 4.910 77.693\r
+ 5.208 65.304\r
+ 5.506 54.347\r
+ 5.804 44.246\r
+ 6.102 35.395\r
+ 6.400 27.716\r
+ 6.698 21.121\r
+ 6.996 14.939\r
+ 7.294 9.737\r
+ 7.592 0.000\r
--- /dev/null
+; AeroTech J145H\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J145H 54 709 0 0.410816 1.79738 AT\r
+ 0.113 253.118\r
+ 0.340 293.672\r
+ 0.567 300.149\r
+ 0.794 289.519\r
+ 1.021 253.366\r
+ 1.248 251.809\r
+ 1.476 246.042\r
+ 1.704 236.553\r
+ 1.931 229.907\r
+ 2.158 222.550\r
+ 2.385 211.120\r
+ 2.612 201.066\r
+ 2.841 191.143\r
+ 3.069 139.197\r
+ 3.296 79.889\r
+ 3.523 63.900\r
+ 3.750 51.048\r
+ 3.977 40.565\r
+ 4.205 31.710\r
+ 4.433 24.429\r
+ 4.660 19.950\r
+ 4.887 15.256\r
+ 5.115 12.412\r
+ 5.342 10.212\r
+ 5.570 9.135\r
+ 5.798 0.000\r
--- /dev/null
+; AeroTech J180T\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J180T 54 230 0 0.429184 0.809088 AT\r
+ 0.093 301.634\r
+ 0.281 313.236\r
+ 0.470 313.710\r
+ 0.658 308.334\r
+ 0.847 300.100\r
+ 1.035 290.743\r
+ 1.224 278.867\r
+ 1.412 263.823\r
+ 1.601 245.974\r
+ 1.790 226.651\r
+ 1.978 207.345\r
+ 2.167 187.053\r
+ 2.355 168.339\r
+ 2.544 149.993\r
+ 2.732 133.094\r
+ 2.921 116.330\r
+ 3.109 100.088\r
+ 3.298 84.507\r
+ 3.486 70.453\r
+ 3.675 57.263\r
+ 3.864 44.453\r
+ 4.052 33.340\r
+ 4.241 24.654\r
+ 4.429 17.964\r
+ 4.619 12.391\r
+ 4.808 0.000\r
--- /dev/null
+;\r
+; AT 54-1280\r
+; Greg Gardner - 09/15/06\r
+J1999N 54 314 0 0.5574 1.111 AT\r
+0.01 830\r
+0.02 1716\r
+0.03 1787\r
+0.04 1873\r
+0.05 1896\r
+0.06 1918\r
+0.07 1984\r
+0.08 2007\r
+0.09 2051\r
+0.10 2058\r
+0.12 2090\r
+0.14 2098\r
+0.16 2135\r
+0.18 2138\r
+0.20 2142\r
+0.25 2146\r
+0.30 2150\r
+0.35 2146\r
+0.40 2138\r
+0.45 2096\r
+0.50 1974\r
+0.55 1864\r
+0.57 1829\r
+0.59 1815\r
+0.60 1762\r
+0.61 1673\r
+0.62 1085\r
+0.63 490\r
+0.64 190\r
+0.65 81\r
+0.66 31\r
+0.67 0\r
+;\r
--- /dev/null
+;\r
+;\r
+J210H 54 609.6 100 0.471 1.497 Aerotech\r
+0.00772798 651.819\r
+0.0695518 528.502\r
+0.200927 488.864\r
+0.502318 409.589\r
+0.996909 374.355\r
+1.4915 312.697\r
+1.59196 286.272\r
+2.00927 167.359\r
+2.43431 88.0836\r
+2.50386 101.296\r
+2.55023 74.8711\r
+3.02164 57.2543\r
+4 0\r
--- /dev/null
+; Aerotech J250FJ from TRA Cert Data\r
+J250FJ 54 241 6-10-14-18 0.511 0.92 AT\r
+ 0.011 132.176\r
+ 0.021 263.335\r
+ 0.084 236.899\r
+ 0.168 252.151\r
+ 0.294 238.933\r
+ 0.494 261.301\r
+ 0.715 285.703\r
+ 0.993 295.87\r
+ 1.177 306.038\r
+ 1.267 305.021\r
+ 1.498 301.971\r
+ 1.64 292.82\r
+ 2.002 253.167\r
+ 2.344 224.699\r
+ 2.391 225.715\r
+ 2.502 233.849\r
+ 2.57 185.046\r
+ 2.659 116.925\r
+ 2.685 76.255\r
+ 2.738 32.536\r
+ 2.77 17.285\r
+ 2.796 0.0\r
--- /dev/null
+;\r
+;\r
+J260HW 54 708.66 100 0.558 1.574 AT\r
+0.00772798 598.969\r
+0.0386399 475.651\r
+0.108192 506.481\r
+0.463679 493.268\r
+0.780526 475.651\r
+1.01236 427.205\r
+2.00155 330.314\r
+2.48841 193.784\r
+2.99073 114.509\r
+4.01082 57.2543\r
+4.5 0\r
--- /dev/null
+; AeroTech J275W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J275W 54 230 0 0.468608 0.864192 AT\r
+ 0.075 239.740\r
+ 0.227 289.133\r
+ 0.380 299.773\r
+ 0.533 312.721\r
+ 0.686 323.878\r
+ 0.840 332.165\r
+ 0.992 336.422\r
+ 1.145 335.110\r
+ 1.298 329.538\r
+ 1.451 325.343\r
+ 1.604 309.980\r
+ 1.756 292.901\r
+ 1.909 275.732\r
+ 2.063 257.341\r
+ 2.216 234.891\r
+ 2.369 213.102\r
+ 2.521 182.501\r
+ 2.674 167.853\r
+ 2.827 153.041\r
+ 2.980 138.115\r
+ 3.133 105.605\r
+ 3.285 67.369\r
+ 3.439 29.239\r
+ 3.592 14.599\r
+ 3.745 6.662\r
+ 3.898 0.000\r
--- /dev/null
+; AeroTech J315R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J315R 54 243 0 0.42112 0.8512 AT\r
+ 0.051 189.719\r
+ 0.154 337.529\r
+ 0.259 354.534\r
+ 0.363 364.111\r
+ 0.468 371.479\r
+ 0.572 373.222\r
+ 0.676 376.062\r
+ 0.780 372.962\r
+ 0.884 368.988\r
+ 0.989 366.978\r
+ 1.093 358.752\r
+ 1.197 351.302\r
+ 1.301 339.336\r
+ 1.406 325.202\r
+ 1.510 311.322\r
+ 1.614 300.496\r
+ 1.718 288.598\r
+ 1.822 278.279\r
+ 1.927 270.538\r
+ 2.031 262.127\r
+ 2.136 245.027\r
+ 2.239 236.238\r
+ 2.344 188.308\r
+ 2.448 63.668\r
+ 2.552 18.746\r
+ 2.657 0.000\r
--- /dev/null
+J350W-L 38 337 P 0.361 0.651 AT\r
+ 0.041 841.443\r
+ 0.051 767.077\r
+ 0.088 698.219\r
+ 0.173 644.51\r
+ 0.256 621.098\r
+ 0.298 564.635\r
+ 0.547 543.977\r
+ 0.783 487.514\r
+ 0.989 418.656\r
+ 1.16 359.438\r
+ 1.192 340.158\r
+ 1.213 320.878\r
+ 1.287 216.214\r
+ 1.319 179.031\r
+ 1.342 126.699\r
+ 1.386 84.007\r
+ 1.427 53.709\r
+ 1.48 45.446\r
+ 1.591 20.657\r
+ 1.695 0.0\r
--- /dev/null
+; AeroTech J350W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J350W 38 337 0 0.375872 0.650944 AT\r
+ 0.038 706.781\r
+ 0.115 669.055\r
+ 0.192 602.539\r
+ 0.270 565.084\r
+ 0.348 539.143\r
+ 0.425 514.910\r
+ 0.503 483.098\r
+ 0.581 449.128\r
+ 0.658 437.256\r
+ 0.736 424.199\r
+ 0.815 414.461\r
+ 0.892 402.956\r
+ 0.970 393.604\r
+ 1.048 377.837\r
+ 1.125 359.785\r
+ 1.203 341.916\r
+ 1.281 324.721\r
+ 1.358 305.935\r
+ 1.436 264.279\r
+ 1.515 175.471\r
+ 1.592 110.912\r
+ 1.670 77.100\r
+ 1.748 55.472\r
+ 1.825 39.990\r
+ 1.903 26.276\r
+ 1.981 0.000\r
+;\r
--- /dev/null
+;\r
+;\r
+J390HW-TURBO 54 708.66 100 0.69 1.74 AT\r
+0.015456 440.418\r
+0.100464 550.523\r
+0.193199 546.118\r
+0.301391 656.223\r
+0.502318 647.414\r
+0.973725 581.352\r
+1.48377 471.247\r
+1.98609 378.759\r
+2.17929 334.718\r
+2.30294 255.442\r
+2.49614 158.55\r
+3.01391 57.2543\r
+3.5 0\r
--- /dev/null
+; AeroTech J415W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J415W 54 314 0 0.686336 1.15718 AT\r
+ 0.065 431.300\r
+ 0.196 452.427\r
+ 0.327 489.904\r
+ 0.458 513.542\r
+ 0.591 523.192\r
+ 0.723 531.440\r
+ 0.854 542.165\r
+ 0.985 542.731\r
+ 1.118 549.788\r
+ 1.250 553.889\r
+ 1.381 537.331\r
+ 1.512 512.126\r
+ 1.645 517.338\r
+ 1.777 498.098\r
+ 1.908 473.365\r
+ 2.040 444.157\r
+ 2.172 413.187\r
+ 2.304 384.854\r
+ 2.435 360.556\r
+ 2.567 297.571\r
+ 2.699 178.288\r
+ 2.831 89.889\r
+ 2.962 43.066\r
+ 3.094 19.126\r
+ 3.226 8.995\r
+ 3.358 0.000\r
--- /dev/null
+; AeroTech J420R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J420R 38 337 0 0.37632 0.6496 AT\r
+ 0.031 61.083\r
+ 0.095 563.470\r
+ 0.160 525.283\r
+ 0.224 521.242\r
+ 0.288 527.371\r
+ 0.352 537.088\r
+ 0.418 535.138\r
+ 0.481 534.623\r
+ 0.545 530.245\r
+ 0.610 526.447\r
+ 0.674 517.203\r
+ 0.738 510.279\r
+ 0.802 500.887\r
+ 0.868 479.450\r
+ 0.931 460.675\r
+ 0.995 438.594\r
+ 1.060 409.647\r
+ 1.124 383.454\r
+ 1.188 361.024\r
+ 1.252 339.741\r
+ 1.318 319.194\r
+ 1.381 296.714\r
+ 1.445 195.191\r
+ 1.510 61.984\r
+ 1.575 7.220\r
+ 1.640 0.000\r
--- /dev/null
+; AeroTech J460T\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J460T 54 230 0 0.413504 0.801024 AT\r
+ 0.041 500.927\r
+ 0.125 509.423\r
+ 0.209 516.357\r
+ 0.294 527.752\r
+ 0.379 535.135\r
+ 0.464 541.858\r
+ 0.548 545.793\r
+ 0.633 545.678\r
+ 0.718 544.832\r
+ 0.802 540.278\r
+ 0.887 533.698\r
+ 0.972 526.340\r
+ 1.056 511.003\r
+ 1.141 492.475\r
+ 1.225 474.977\r
+ 1.310 457.021\r
+ 1.395 437.203\r
+ 1.479 418.093\r
+ 1.565 403.240\r
+ 1.649 339.173\r
+ 1.733 203.861\r
+ 1.819 102.620\r
+ 1.903 49.295\r
+ 1.987 9.538\r
+ 2.073 2.155\r
+ 2.158 0.000\r
--- /dev/null
+;Delays are speculation.\r
+;Taken from Aerotech curves, not cert docs.\r
+;Jim Yehle 15 Nov 07\r
+J500G 38 335.407 0-6-10-14 0.3626 0.654 Aerotech \r
+0.0134378 40.2458\r
+0.0335946 724.425\r
+0.0403135 781.616\r
+0.0604703 787.971\r
+0.0895857 711.716\r
+0.134378 686.297\r
+0.394177 637.578\r
+0.575588 588.86\r
+0.606943 622.751\r
+0.633819 620.633\r
+1.20045 360.094\r
+1.24076 345.267\r
+1.31019 182.165\r
+1.38186 65.6642\r
+1.43337 23.3002\r
+1.45 0\r
+;\r
--- /dev/null
+; AeroTech J540R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J540R 54 314 0 0.61376 1.08416 AT\r
+ 0.044 498.757\r
+ 0.134 639.617\r
+ 0.224 649.317\r
+ 0.314 657.966\r
+ 0.404 664.020\r
+ 0.494 666.924\r
+ 0.584 663.699\r
+ 0.675 658.398\r
+ 0.765 651.232\r
+ 0.855 638.505\r
+ 0.945 626.396\r
+ 1.035 612.557\r
+ 1.126 590.090\r
+ 1.216 562.391\r
+ 1.306 536.875\r
+ 1.396 511.607\r
+ 1.486 490.354\r
+ 1.576 468.978\r
+ 1.667 451.342\r
+ 1.758 430.180\r
+ 1.847 414.549\r
+ 1.937 398.116\r
+ 2.027 305.877\r
+ 2.118 55.541\r
+ 2.208 1.523\r
+ 2.299 0.000\r
--- /dev/null
+; AeroTech J570W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J570W 38 479 0 0.547904 0.886144 AT\r
+ 0.039 1149.795\r
+ 0.119 1042.846\r
+ 0.199 960.891\r
+ 0.279 900.020\r
+ 0.360 837.772\r
+ 0.441 792.834\r
+ 0.521 735.510\r
+ 0.602 685.857\r
+ 0.682 649.599\r
+ 0.762 608.757\r
+ 0.844 597.350\r
+ 0.924 568.934\r
+ 1.004 548.552\r
+ 1.084 505.080\r
+ 1.165 484.626\r
+ 1.246 452.328\r
+ 1.326 362.439\r
+ 1.406 297.973\r
+ 1.487 262.381\r
+ 1.568 195.696\r
+ 1.648 156.733\r
+ 1.729 124.649\r
+ 1.809 113.749\r
+ 1.890 69.812\r
+ 1.971 46.023\r
+ 2.052 0.000\r
--- /dev/null
+;\r
+J575FJ 38 478.79 6-10-14 0.576 0.91424 Aerotech \r
+0.0156556 656.682\r
+0.0195695 840.689\r
+0.037182 840.689\r
+0.0606654 839.001\r
+0.101761 839.001\r
+0.162427 839.001\r
+0.228963 839.001\r
+0.315068 839.001\r
+0.399217 837.312\r
+0.459883 837.312\r
+0.547945 822.119\r
+0.60274 801.862\r
+0.700587 742.777\r
+0.802348 685.381\r
+0.841487 646.554\r
+0.902153 573.964\r
+0.949791 483.69\r
+1 319.581\r
+1.05365 220.66\r
+1.12916 153.62\r
+1.19961 99.5997\r
+1.27593 43.8914\r
+1.34 0\r
+;\r
--- /dev/null
+; AeroTech J800T\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J800T 54 314 0 0.613312 1.08595 AT\r
+ 0.040 841.341\r
+ 0.121 818.497\r
+ 0.203 776.386\r
+ 0.285 784.308\r
+ 0.367 785.314\r
+ 0.449 783.315\r
+ 0.531 782.539\r
+ 0.612 779.977\r
+ 0.695 773.680\r
+ 0.777 765.307\r
+ 0.858 755.517\r
+ 0.941 744.777\r
+ 1.023 733.131\r
+ 1.105 719.947\r
+ 1.187 702.235\r
+ 1.269 685.369\r
+ 1.351 668.265\r
+ 1.433 650.327\r
+ 1.515 630.472\r
+ 1.597 615.483\r
+ 1.679 470.262\r
+ 1.760 256.617\r
+ 1.843 108.716\r
+ 1.925 15.005\r
+ 2.007 1.249\r
+ 2.090 0.000\r
--- /dev/null
+;\r
+;Aerotech J825R\r
+J825R 38 479 10 0.53 .88 AT\r
+0.0 11.504\r
+0.048913 1069.87\r
+0.100155 977.842\r
+0.118789 1035.36\r
+0.652174 897.314\r
+0.801242 839.794\r
+0.899068 782.274\r
+0.999224 586.705\r
+1.09938 103.536\r
+1.14363 23.008\r
+1.18 0.0\r
+;\r
--- /dev/null
+; AeroTech J90W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J90W 54 243 0 0.427392 0.852544 AT\r
+ 0.143 116.187\r
+ 0.430 165.444\r
+ 0.718 176.536\r
+ 1.005 184.645\r
+ 1.293 187.242\r
+ 1.580 183.651\r
+ 1.868 175.492\r
+ 2.155 167.687\r
+ 2.443 156.858\r
+ 2.730 143.514\r
+ 3.018 128.856\r
+ 3.305 110.879\r
+ 3.593 94.003\r
+ 3.880 79.657\r
+ 4.168 67.472\r
+ 4.455 57.268\r
+ 4.743 48.008\r
+ 5.030 40.523\r
+ 5.318 33.901\r
+ 5.605 28.248\r
+ 5.893 23.334\r
+ 6.180 19.275\r
+ 6.468 15.923\r
+ 6.755 12.727\r
+ 7.044 9.903\r
+ 7.332 0.000\r
--- /dev/null
+; AeroTech K1050W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K1050W 54 676 0 1.34714 2.12845 AT\r
+ 0.049 1305.649\r
+ 0.149 1270.386\r
+ 0.249 1288.922\r
+ 0.349 1327.059\r
+ 0.449 1345.719\r
+ 0.549 1359.794\r
+ 0.649 1364.452\r
+ 0.749 1365.493\r
+ 0.849 1377.189\r
+ 0.949 1379.519\r
+ 1.049 1346.586\r
+ 1.149 1286.742\r
+ 1.249 1232.101\r
+ 1.349 1186.480\r
+ 1.449 1156.521\r
+ 1.549 1120.045\r
+ 1.649 1098.708\r
+ 1.749 1070.186\r
+ 1.849 889.885\r
+ 1.949 646.691\r
+ 2.049 441.213\r
+ 2.149 302.245\r
+ 2.249 155.001\r
+ 2.349 52.187\r
+ 2.449 43.415\r
+ 2.549 0.000\r
--- /dev/null
+; AeroTech K1100T\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K1100T 54 398 0 0.7616 1.32518 AT\r
+ 0.034 1234.653\r
+ 0.105 1233.429\r
+ 0.176 1192.393\r
+ 0.247 1163.041\r
+ 0.318 1147.963\r
+ 0.389 1146.319\r
+ 0.460 1140.958\r
+ 0.532 1132.640\r
+ 0.603 1123.824\r
+ 0.674 1108.921\r
+ 0.745 1090.974\r
+ 0.816 1073.937\r
+ 0.887 1049.133\r
+ 0.959 1021.216\r
+ 1.030 994.559\r
+ 1.101 966.571\r
+ 1.172 940.194\r
+ 1.243 909.792\r
+ 1.315 880.264\r
+ 1.386 844.477\r
+ 1.457 643.599\r
+ 1.528 401.861\r
+ 1.599 145.498\r
+ 1.670 28.372\r
+ 1.742 0.000\r
--- /dev/null
+; AeroTech K1275R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K1275R 54 569 0 1.29024 2.03392 AT\r
+ 0.039 1282.616\r
+ 0.119 1557.989\r
+ 0.199 1540.196\r
+ 0.279 1526.782\r
+ 0.359 1500.693\r
+ 0.440 1456.584\r
+ 0.520 1425.794\r
+ 0.600 1390.416\r
+ 0.680 1355.109\r
+ 0.761 1323.311\r
+ 0.841 1282.825\r
+ 0.921 1247.795\r
+ 1.001 1194.417\r
+ 1.081 1150.977\r
+ 1.162 1108.223\r
+ 1.242 1068.754\r
+ 1.322 1036.922\r
+ 1.403 997.444\r
+ 1.482 964.569\r
+ 1.563 933.305\r
+ 1.644 889.992\r
+ 1.724 599.467\r
+ 1.804 134.559\r
+ 1.884 5.630\r
+ 1.964 0.205\r
+ 2.045 0.000\r
--- /dev/null
+;Entered by Jim Yehle\r
+;from TRA cert document\r
+K1499N 75 260 1000 0.604 1.741 AT-RMS \r
+0.01 1450\r
+0.2 1720.12\r
+0.35 1700\r
+0.5 1600\r
+0.6 1575\r
+0.7 1500\r
+0.82 1400\r
+0.84 250\r
+0.88 0\r
+;\r
--- /dev/null
+; AeroTech K185W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K185W 54 437 0 0.827008 1.43405 AT\r
+ 0.150 279.128\r
+ 0.452 308.220\r
+ 0.754 328.435\r
+ 1.056 338.929\r
+ 1.359 339.677\r
+ 1.663 333.166\r
+ 1.965 321.891\r
+ 2.267 309.687\r
+ 2.570 293.260\r
+ 2.873 271.536\r
+ 3.175 247.174\r
+ 3.477 216.883\r
+ 3.780 186.951\r
+ 4.083 161.096\r
+ 4.385 138.113\r
+ 4.688 117.749\r
+ 4.991 99.372\r
+ 5.294 82.759\r
+ 5.596 68.426\r
+ 5.898 55.126\r
+ 6.201 44.162\r
+ 6.504 34.209\r
+ 6.806 25.064\r
+ 7.108 16.880\r
+ 7.411 9.200\r
+ 7.715 0.000\r
--- /dev/null
+; AeroTech K1999N\r
+; Curvefit to instruction sheet on Aerotech website (1/29/07)\r
+; by Chris Kobel\r
+; burn time: 1.4 seconds\r
+; total impulse: 2522 newton-seconds\r
+; average thrust: 405 pounds\r
+K1999N 98 289 6-10-14-18 1.195 2.989 AT\r
+ 0.02 1557.5\r
+ 0.08 1780.0\r
+ 0.10 1913.5\r
+ 0.12 1869.0\r
+ 0.18 2002.5\r
+ 1.08 2002.5\r
+ 1.10 1958.0\r
+ 1.20 1780.0\r
+ 1.25 1557.5\r
+ 1.27 1335.0\r
+ 1.31 890.0\r
+ 1.33 667.5\r
+ 1.35 222.5\r
+ 1.40 0.0\r
--- /dev/null
+; AeroTech K250W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K250W 54 673 0 1.52902 2.21133 AT\r
+ 0.199 365.330\r
+ 0.599 403.324\r
+ 0.999 418.669\r
+ 1.400 409.813\r
+ 1.801 408.949\r
+ 2.201 412.146\r
+ 2.602 411.952\r
+ 3.003 409.488\r
+ 3.403 393.214\r
+ 3.804 373.599\r
+ 4.205 348.913\r
+ 4.605 328.463\r
+ 5.006 307.163\r
+ 5.407 281.467\r
+ 5.807 249.011\r
+ 6.208 217.159\r
+ 6.609 185.908\r
+ 7.009 149.190\r
+ 7.410 119.808\r
+ 7.811 92.096\r
+ 8.211 69.726\r
+ 8.613 52.613\r
+ 9.014 35.876\r
+ 9.414 16.727\r
+ 9.815 4.086\r
+ 10.216 0.000\r
--- /dev/null
+; Aerotech K270W-P Moon Burner from TRA Certification Data\r
+K270W 54 579 P 1.188 2.1 AT\r
+ 0.046 177.061\r
+ 0.062 292.932\r
+ 0.092 425.727\r
+ 0.154 414.01\r
+ 0.277 389.273\r
+ 0.446 377.556\r
+ 0.585 381.462\r
+ 0.738 372.349\r
+ 1.0 377.556\r
+ 1.154 376.254\r
+ 1.231 378.858\r
+ 1.308 395.783\r
+ 1.4 380.16\r
+ 1.569 399.689\r
+ 1.615 381.462\r
+ 1.846 381.462\r
+ 2.369 368.443\r
+ 2.415 381.462\r
+ 2.554 360.631\r
+ 3.015 350.216\r
+ 3.354 328.083\r
+ 3.723 300.743\r
+ 4.0 273.403\r
+ 4.6 225.232\r
+ 5.262 175.759\r
+ 5.677 144.513\r
+ 6.0 124.984\r
+ 6.538 89.832\r
+ 7.015 66.398\r
+ 8.0 22.133\r
+ 8.323 10.415\r
+ 8.508 5.208\r
+ 8.692 0.0\r
--- /dev/null
+; AeroTech K458W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K458W 98 275 0 1.42778 3.16378 AT\r
+ 0.133 294.911\r
+ 0.403 404.808\r
+ 0.674 462.021\r
+ 0.944 515.863\r
+ 1.214 555.072\r
+ 1.484 583.153\r
+ 1.755 600.299\r
+ 2.025 610.254\r
+ 2.295 618.543\r
+ 2.566 623.155\r
+ 2.835 618.885\r
+ 3.105 589.082\r
+ 3.376 546.307\r
+ 3.647 505.042\r
+ 3.917 451.412\r
+ 4.186 391.651\r
+ 4.457 338.409\r
+ 4.727 288.429\r
+ 4.997 245.814\r
+ 5.268 208.209\r
+ 5.539 178.153\r
+ 5.808 149.825\r
+ 6.078 62.931\r
+ 6.349 8.427\r
+ 6.620 2.562\r
+ 6.891 0.000\r
--- /dev/null
+; AeroTech K485HW\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K485HW 54 699 0 0.910784 2.22029 AT\r
+ 0.075 454.453\r
+ 0.227 568.735\r
+ 0.380 831.332\r
+ 0.533 825.584\r
+ 0.686 795.935\r
+ 0.840 759.473\r
+ 0.992 727.238\r
+ 1.145 680.051\r
+ 1.298 653.091\r
+ 1.451 627.316\r
+ 1.604 601.548\r
+ 1.756 576.270\r
+ 1.909 542.033\r
+ 2.063 479.078\r
+ 2.216 394.184\r
+ 2.369 346.719\r
+ 2.521 307.435\r
+ 2.674 276.291\r
+ 2.827 216.608\r
+ 2.980 146.021\r
+ 3.133 106.838\r
+ 3.285 81.226\r
+ 3.439 52.105\r
+ 3.592 37.385\r
+ 3.745 29.462\r
+ 3.898 0.000\r
--- /dev/null
+; AeroTech K550W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K550W 54 410 0 0.919744 1.48736 AT\r
+ 0.065 604.264\r
+ 0.196 642.625\r
+ 0.327 682.197\r
+ 0.458 732.995\r
+ 0.591 758.236\r
+ 0.723 780.289\r
+ 0.854 794.452\r
+ 0.985 797.939\r
+ 1.117 797.601\r
+ 1.249 773.842\r
+ 1.381 711.608\r
+ 1.512 646.522\r
+ 1.644 590.724\r
+ 1.775 537.505\r
+ 1.907 491.012\r
+ 2.040 445.836\r
+ 2.171 401.461\r
+ 2.302 364.291\r
+ 2.433 319.614\r
+ 2.566 255.577\r
+ 2.698 172.573\r
+ 2.829 103.501\r
+ 2.960 51.795\r
+ 3.092 26.814\r
+ 3.224 15.203\r
+ 3.356 0.000\r
--- /dev/null
+; AeroTech K560W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K560W 75 396 0 1.40806 2.71354 AT\r
+ 0.096 552.123\r
+ 0.290 645.403\r
+ 0.484 681.109\r
+ 0.679 716.167\r
+ 0.874 742.678\r
+ 1.069 764.778\r
+ 1.264 775.710\r
+ 1.458 785.859\r
+ 1.653 789.305\r
+ 1.848 789.077\r
+ 2.043 744.622\r
+ 2.237 676.886\r
+ 2.432 614.711\r
+ 2.627 557.908\r
+ 2.822 503.641\r
+ 3.017 455.504\r
+ 3.211 412.045\r
+ 3.406 372.963\r
+ 3.601 335.987\r
+ 3.796 307.346\r
+ 3.991 279.856\r
+ 4.185 223.491\r
+ 4.380 70.441\r
+ 4.575 10.028\r
+ 4.770 2.445\r
+ 4.965 0.000\r
--- /dev/null
+; AeroTech K650T\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K650T 98 289 0 1.27008 2.9353 AT\r
+ 0.079 514.338\r
+ 0.240 594.264\r
+ 0.401 618.849\r
+ 0.563 641.658\r
+ 0.723 665.057\r
+ 0.884 686.488\r
+ 1.046 704.685\r
+ 1.206 720.215\r
+ 1.368 730.072\r
+ 1.529 736.891\r
+ 1.690 743.109\r
+ 1.851 747.503\r
+ 2.013 747.557\r
+ 2.174 744.081\r
+ 2.335 732.294\r
+ 2.496 710.412\r
+ 2.657 682.670\r
+ 2.819 653.246\r
+ 2.979 627.020\r
+ 3.141 595.456\r
+ 3.302 563.844\r
+ 3.463 551.080\r
+ 3.624 236.059\r
+ 3.785 1.383\r
+ 3.947 1.234\r
+ 4.108 0.000\r
--- /dev/null
+;\r
+;Aerotech K680R RASP engine file\r
+;Data Entered by Tim Van Milligan\r
+;Source: TRA Certification paperwork, and\r
+;Aerotech's instruction sheet: RMS 98/2560-10240 REDLINE.\r
+K680R 98 289 100 1.316 3.035 AT\r
+0.085 629.798\r
+0.494 717.881\r
+0.996 797.157\r
+1.29 819.178\r
+1.506 819.178\r
+2.001 775.136\r
+2.519 673.84\r
+2.99 563.735\r
+3.137 541.714\r
+3.176 532.906\r
+3.238 563.735\r
+3.276 563.735\r
+3.408 52.85\r
+3.431 22.02\r
+3.49 0\r
--- /dev/null
+; AeroTech K695R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K695R 54 410 0 0.9184 1.48736 AT\r
+ 0.044 618.611\r
+ 0.134 727.840\r
+ 0.224 751.996\r
+ 0.314 812.480\r
+ 0.404 900.125\r
+ 0.495 884.763\r
+ 0.585 873.457\r
+ 0.675 864.561\r
+ 0.765 849.672\r
+ 0.856 838.886\r
+ 0.946 822.550\r
+ 1.036 806.240\r
+ 1.126 781.342\r
+ 1.216 753.973\r
+ 1.307 728.472\r
+ 1.398 697.629\r
+ 1.487 672.979\r
+ 1.578 646.660\r
+ 1.667 620.897\r
+ 1.758 595.574\r
+ 1.849 571.720\r
+ 1.939 546.822\r
+ 2.029 272.824\r
+ 2.119 57.950\r
+ 2.209 4.509\r
+ 2.300 0.000\r
--- /dev/null
+; AeroTech K700W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K700W 54 568 0 1.29158 2.03526 AT\r
+ 0.069 1005.472\r
+ 0.209 1018.916\r
+ 0.350 1026.610\r
+ 0.491 1028.637\r
+ 0.632 1029.337\r
+ 0.773 1004.203\r
+ 0.914 970.694\r
+ 1.055 946.516\r
+ 1.196 918.437\r
+ 1.336 873.783\r
+ 1.478 821.276\r
+ 1.619 773.270\r
+ 1.759 735.553\r
+ 1.900 692.732\r
+ 2.041 658.984\r
+ 2.182 626.737\r
+ 2.323 591.431\r
+ 2.464 508.666\r
+ 2.605 420.175\r
+ 2.746 328.309\r
+ 2.886 202.409\r
+ 3.028 121.672\r
+ 3.169 80.453\r
+ 3.309 50.873\r
+ 3.451 31.548\r
+ 3.593 0.000\r
--- /dev/null
+; AeroTech K780R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K780R 75 289 0 1.26784 2.9344 AT\r
+ 0.053 383.290\r
+ 0.173 718.241\r
+ 0.292 849.343\r
+ 0.413 885.503\r
+ 0.533 903.243\r
+ 0.652 924.403\r
+ 0.772 938.825\r
+ 0.892 938.623\r
+ 1.013 947.130\r
+ 1.133 953.578\r
+ 1.253 944.001\r
+ 1.373 935.448\r
+ 1.495 929.447\r
+ 1.617 920.379\r
+ 1.737 897.293\r
+ 1.857 888.917\r
+ 1.977 861.127\r
+ 2.098 840.971\r
+ 2.217 812.360\r
+ 2.337 779.614\r
+ 2.457 747.866\r
+ 2.578 726.819\r
+ 2.697 729.258\r
+ 2.817 279.891\r
+ 2.940 10.969\r
+ 3.063 0.000\r
--- /dev/null
+;\r
+K828FJ 54.0 579.00 6-10-14-18 1.45000 2.25500 AT\r
+ 0.01 1112.06 \r
+ 0.02 1238.60 \r
+ 0.04 1303.79 \r
+ 0.06 1135.06 \r
+ 0.08 1077.54 \r
+ 0.13 1031.53 \r
+ 0.20 1016.19 \r
+ 0.50 993.18 \r
+ 0.65 1004.68 \r
+ 1.00 985.51 \r
+ 1.08 974.01 \r
+ 1.19 974.01 \r
+ 1.42 954.83 \r
+ 1.51 935.66 \r
+ 1.69 912.65 \r
+ 1.75 885.81 \r
+ 1.83 893.48 \r
+ 1.89 843.63 \r
+ 1.95 774.60 \r
+ 2.00 667.23 \r
+ 2.15 444.82 \r
+ 2.20 364.29 \r
+ 2.23 260.76 \r
+ 2.27 184.06 \r
+ 2.33 111.21 \r
+ 2.39 49.85 \r
+ 2.50 0.00 \r
+;\r
--- /dev/null
+; AeroTech L1120W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L1120W 75 665 0 2.75699 4.65786 AT\r
+ 0.097 1377.215\r
+ 0.293 1442.670\r
+ 0.489 1496.986\r
+ 0.685 1537.057\r
+ 0.882 1554.962\r
+ 1.078 1554.131\r
+ 1.275 1547.973\r
+ 1.472 1533.465\r
+ 1.668 1510.342\r
+ 1.865 1472.279\r
+ 2.061 1362.534\r
+ 2.257 1245.425\r
+ 2.454 1148.864\r
+ 2.651 1062.680\r
+ 2.847 984.952\r
+ 3.044 916.169\r
+ 3.241 831.929\r
+ 3.436 766.450\r
+ 3.633 698.978\r
+ 3.830 562.966\r
+ 4.026 384.579\r
+ 4.223 227.654\r
+ 4.420 105.078\r
+ 4.616 56.339\r
+ 4.813 21.712\r
+ 5.009 0.000\r
--- /dev/null
+; AeroTech L1150\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L1150 75 531 0 2.06528 3.6736 AT\r
+ 0.053 935.855\r
+ 0.175 1292.642\r
+ 0.300 1260.926\r
+ 0.425 1241.482\r
+ 0.550 1257.058\r
+ 0.675 1272.287\r
+ 0.800 1287.605\r
+ 0.925 1301.012\r
+ 1.048 1309.708\r
+ 1.170 1308.417\r
+ 1.295 1304.830\r
+ 1.420 1285.265\r
+ 1.545 1267.657\r
+ 1.670 1255.624\r
+ 1.795 1227.212\r
+ 1.920 1202.443\r
+ 2.043 1182.617\r
+ 2.165 1150.712\r
+ 2.290 1117.909\r
+ 2.415 1081.739\r
+ 2.540 1037.547\r
+ 2.665 1007.091\r
+ 2.790 1008.911\r
+ 2.915 643.124\r
+ 3.040 64.371\r
+ 3.165 0.000\r
--- /dev/null
+;\r
+;\r
+L1300R 98 443 100 2.508 4.884 AT\r
+0.0231839 1299.23\r
+0.502318 1332.26\r
+0.996909 1497.42\r
+1.49923 1552.47\r
+1.99382 1508.43\r
+2.49614 1354.29\r
+2.99845 1101.05\r
+3.12983 1090.03\r
+3.21484 1145.09\r
+3.3694 176.167\r
+3.5 0\r
--- /dev/null
+;\r
+;\r
+L1420R 75 443 100 2.56 4.562 AT\r
+0.0386399 1332.26\r
+0.123648 1563.48\r
+0.502318 1519.44\r
+0.996909 1574.49\r
+1.49923 1662.58\r
+2.00155 1574.49\r
+2.48068 1409.34\r
+2.92117 1299.23\r
+2.99073 1167.11\r
+3.11437 187.178\r
+3.24 0\r
--- /dev/null
+; AeroTech L1500T\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L1500T 98 443 0 2.464 4.6592 AT\r
+ 0.073 1320.328\r
+ 0.222 1454.823\r
+ 0.372 1508.992\r
+ 0.522 1556.781\r
+ 0.672 1602.407\r
+ 0.822 1642.004\r
+ 0.971 1670.099\r
+ 1.120 1694.804\r
+ 1.270 1701.295\r
+ 1.420 1704.286\r
+ 1.570 1701.008\r
+ 1.720 1694.550\r
+ 1.869 1683.861\r
+ 2.018 1659.694\r
+ 2.168 1620.161\r
+ 2.318 1570.033\r
+ 2.468 1517.933\r
+ 2.618 1463.319\r
+ 2.767 1400.991\r
+ 2.916 1331.420\r
+ 3.066 1279.479\r
+ 3.216 1108.987\r
+ 3.366 217.788\r
+ 3.516 10.579\r
+ 3.666 3.245\r
+ 3.816 0.000\r
--- /dev/null
+; AeroTech L850W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L850W 75 531 0 2.06528 3.67315 AT\r
+ 0.091 1015.926\r
+ 0.274 1064.942\r
+ 0.458 1101.366\r
+ 0.643 1143.358\r
+ 0.827 1170.928\r
+ 1.011 1184.795\r
+ 1.196 1178.044\r
+ 1.380 1177.598\r
+ 1.564 1174.910\r
+ 1.748 1170.021\r
+ 1.932 1113.716\r
+ 2.117 1042.586\r
+ 2.301 972.795\r
+ 2.485 908.071\r
+ 2.670 844.471\r
+ 2.854 773.595\r
+ 3.039 714.046\r
+ 3.222 649.095\r
+ 3.406 597.341\r
+ 3.591 557.444\r
+ 3.775 422.233\r
+ 3.959 200.739\r
+ 4.144 79.411\r
+ 4.328 43.959\r
+ 4.513 14.862\r
+ 4.697 0.000\r
--- /dev/null
+; AeroTech L952W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L952W 98 427 0 2.73011 5.01222 AT\r
+ 0.141 679.073\r
+ 0.425 801.562\r
+ 0.709 848.474\r
+ 0.994 913.345\r
+ 1.278 981.614\r
+ 1.562 1043.690\r
+ 1.847 1088.114\r
+ 2.131 1112.556\r
+ 2.416 1121.541\r
+ 2.700 1118.573\r
+ 2.984 1100.665\r
+ 3.269 1039.140\r
+ 3.553 965.784\r
+ 3.837 876.793\r
+ 4.122 780.693\r
+ 4.406 693.903\r
+ 4.691 608.030\r
+ 4.975 528.335\r
+ 5.259 463.528\r
+ 5.544 405.769\r
+ 5.828 358.367\r
+ 6.112 279.009\r
+ 6.397 99.897\r
+ 6.681 20.108\r
+ 6.967 3.317\r
+ 7.252 0.000\r
--- /dev/null
+;\r
+; Aerotech M1297W\r
+; Greg Gardner - 12/20/04\r
+M1297W 75 665 0 2.722 4.637 AT\r
+0.10 1433.4\r
+0.15 1789.3\r
+0.20 1922.8\r
+0.25 1869.4\r
+0.30 1856.0\r
+0.35 1833.8\r
+0.40 1767.0\r
+0.50 1722.6\r
+0.60 1709.2\r
+0.90 1700.3\r
+1.00 1688.1\r
+1.50 1678.7\r
+1.75 1634.6\r
+1.85 1622.3\r
+1.95 1572.8\r
+2.00 1554.0\r
+2.50 1346.5\r
+3.00 1136.0\r
+3.20 1053.3\r
+3.25 1044.1\r
+3.35 1032.0\r
+3.38 1020.0\r
+3.40 937.0\r
+3.50 738.0\r
+3.60 545.0\r
+3.75 393.0\r
+4.00 226.0\r
+4.25 94.0\r
+4.35 45.0\r
+4.40 0.0\r
+;\r
--- /dev/null
+; AeroTech M1315W\r
+; Copyright Tripoli Motor Testing 1999 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M1315W 75 801 0 3.4496 5.6448 AT\r
+ 0.116 1728.683\r
+ 0.349 1673.336\r
+ 0.582 1686.810\r
+ 0.816 1696.068\r
+ 1.049 1663.167\r
+ 1.282 1631.243\r
+ 1.516 1620.471\r
+ 1.749 1619.702\r
+ 1.982 1621.042\r
+ 2.216 1615.320\r
+ 2.449 1567.089\r
+ 2.682 1493.722\r
+ 2.916 1420.079\r
+ 3.149 1358.660\r
+ 3.382 1292.507\r
+ 3.616 1224.806\r
+ 3.849 1171.995\r
+ 4.082 928.809\r
+ 4.316 577.949\r
+ 4.549 395.445\r
+ 4.782 314.006\r
+ 5.016 228.273\r
+ 5.249 159.803\r
+ 5.482 118.348\r
+ 5.716 109.782\r
+ 5.949 0.000\r
--- /dev/null
+; AeroTech M1419W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M1419W 98 579 0 4.032 6.91622 AT\r
+ 0.154 1154.896\r
+ 0.465 1241.151\r
+ 0.776 1300.224\r
+ 1.087 1358.364\r
+ 1.399 1411.033\r
+ 1.710 1461.033\r
+ 2.022 1485.747\r
+ 2.333 1503.653\r
+ 2.644 1513.113\r
+ 2.955 1511.947\r
+ 3.267 1492.438\r
+ 3.578 1418.368\r
+ 3.890 1326.608\r
+ 4.201 1219.222\r
+ 4.513 1087.648\r
+ 4.824 937.068\r
+ 5.135 810.066\r
+ 5.446 709.130\r
+ 5.757 624.701\r
+ 6.069 557.223\r
+ 6.380 437.806\r
+ 6.692 252.076\r
+ 7.003 107.741\r
+ 7.315 19.973\r
+ 7.626 0.515\r
+ 7.937 0.000\r
--- /dev/null
+; AeroTech M1550R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M1550R 75 800 0 3.4496 5.6448 AT\r
+ 0.069 1720.759\r
+ 0.212 2125.329\r
+ 0.358 1995.947\r
+ 0.501 1908.442\r
+ 0.645 1868.713\r
+ 0.790 1835.504\r
+ 0.935 1808.662\r
+ 1.079 1796.300\r
+ 1.222 1785.423\r
+ 1.368 1773.153\r
+ 1.511 1746.590\r
+ 1.655 1715.709\r
+ 1.800 1689.633\r
+ 1.945 1660.720\r
+ 2.089 1633.277\r
+ 2.232 1606.038\r
+ 2.378 1570.222\r
+ 2.521 1534.714\r
+ 2.665 1503.345\r
+ 2.810 1461.317\r
+ 2.955 1427.572\r
+ 3.099 1393.229\r
+ 3.242 939.955\r
+ 3.388 268.504\r
+ 3.532 4.985\r
+ 3.677 0.000\r
--- /dev/null
+; AeroTech M1600R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M1600R 98 579 0 4.032 6.91712 AT\r
+ 0.088 1370.361\r
+ 0.268 1626.628\r
+ 0.448 1672.654\r
+ 0.628 1720.596\r
+ 0.808 1763.287\r
+ 0.987 1801.282\r
+ 1.167 1829.825\r
+ 1.348 1845.146\r
+ 1.529 1856.370\r
+ 1.710 1850.089\r
+ 1.890 1847.370\r
+ 2.070 1829.454\r
+ 2.250 1810.982\r
+ 2.430 1784.910\r
+ 2.610 1754.267\r
+ 2.790 1726.898\r
+ 2.971 1689.288\r
+ 3.152 1641.579\r
+ 3.332 1581.589\r
+ 3.513 1511.036\r
+ 3.692 1431.400\r
+ 3.872 1361.032\r
+ 4.053 1234.566\r
+ 4.232 621.206\r
+ 4.414 42.471\r
+ 4.595 0.000\r
--- /dev/null
+;\r
+;75-7680 case\r
+; Greg Gardner - 10/25/07\r
+M1850W 75 935 0 3.979 6.871 AT\r
+0.1 2411\r
+0.2 2135\r
+0.3 2015\r
+0.4 2000\r
+0.5 2055\r
+1.0 2098\r
+1.5 1860\r
+2.0 1788\r
+2.5 1659\r
+3.0 1468\r
+3.25 1423\r
+3.35 1334\r
+3.5 1201\r
+3.75 934\r
+3.8 930\r
+4.0 881\r
+4.25 600\r
+4.5 468\r
+4.75 400\r
+5.0 290\r
+5.5 85\r
+6.0 23\r
+6.5 0\r
+;\r
--- /dev/null
+; AeroTech M1939W\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M1939W 98 732 0 5.656 8.98822 AT\r
+ 0.134 1905.185\r
+ 0.406 2021.155\r
+ 0.679 2095.900\r
+ 0.952 2158.087\r
+ 1.225 2198.211\r
+ 1.498 2219.694\r
+ 1.770 2228.643\r
+ 2.042 2229.881\r
+ 2.315 2225.641\r
+ 2.587 2211.713\r
+ 2.860 2164.724\r
+ 3.133 2047.014\r
+ 3.405 1916.238\r
+ 3.677 1805.664\r
+ 3.950 1658.489\r
+ 4.223 1497.704\r
+ 4.496 1339.452\r
+ 4.769 1213.061\r
+ 5.041 1102.130\r
+ 5.313 966.508\r
+ 5.585 670.253\r
+ 5.858 443.975\r
+ 6.131 155.355\r
+ 6.404 41.358\r
+ 6.677 5.775\r
+ 6.950 0.000\r
--- /dev/null
+; AeroTech M2000R\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M2000R 98 732 0 5.65824 8.98688 AT\r
+ 0.091 1530.959\r
+ 0.279 2186.270\r
+ 0.466 2166.698\r
+ 0.655 2187.237\r
+ 0.844 2219.069\r
+ 1.031 2248.071\r
+ 1.220 2273.743\r
+ 1.409 2298.306\r
+ 1.596 2309.753\r
+ 1.785 2315.708\r
+ 1.974 2316.158\r
+ 2.161 2306.313\r
+ 2.350 2282.230\r
+ 2.539 2252.104\r
+ 2.726 2209.638\r
+ 2.915 2168.800\r
+ 3.104 2117.175\r
+ 3.291 2067.533\r
+ 3.480 2004.508\r
+ 3.669 1934.442\r
+ 3.856 1831.480\r
+ 4.045 1745.634\r
+ 4.234 1504.269\r
+ 4.421 649.796\r
+ 4.610 58.178\r
+ 4.799 0.000\r
--- /dev/null
+; AeroTech M2400T\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M2400T 98 597 0 3.65254 6.4512 AT\r
+ 0.070 2441.945\r
+ 0.211 2495.460\r
+ 0.353 2556.133\r
+ 0.495 2601.596\r
+ 0.636 2637.660\r
+ 0.778 2660.804\r
+ 0.920 2676.486\r
+ 1.061 2687.081\r
+ 1.203 2695.807\r
+ 1.345 2694.493\r
+ 1.486 2684.268\r
+ 1.628 2667.289\r
+ 1.771 2629.961\r
+ 1.914 2578.923\r
+ 2.055 2522.074\r
+ 2.197 2461.704\r
+ 2.339 2393.518\r
+ 2.480 2303.939\r
+ 2.622 2201.610\r
+ 2.764 2097.461\r
+ 2.905 2010.409\r
+ 3.047 1275.776\r
+ 3.189 418.836\r
+ 3.330 17.586\r
+ 3.473 3.669\r
+ 3.616 0.000\r
--- /dev/null
+; AeroTech M2500T\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M2500T 98 751 0 4.6592 8.064 AT\r
+ 0.082 2651.855\r
+ 0.249 2780.285\r
+ 0.416 2820.733\r
+ 0.583 2843.010\r
+ 0.751 2847.765\r
+ 0.918 2851.215\r
+ 1.084 2854.737\r
+ 1.252 2861.690\r
+ 1.420 2858.088\r
+ 1.586 2851.086\r
+ 1.754 2844.622\r
+ 1.922 2830.855\r
+ 2.089 2804.711\r
+ 2.255 2765.796\r
+ 2.423 2710.509\r
+ 2.591 2648.262\r
+ 2.757 2586.910\r
+ 2.925 2520.794\r
+ 3.093 2462.217\r
+ 3.259 2419.937\r
+ 3.426 1894.936\r
+ 3.594 808.043\r
+ 3.761 282.403\r
+ 3.928 97.876\r
+ 4.096 24.492\r
+ 4.264 0.000\r
--- /dev/null
+;\r
+;75-6400 case\r
+; Greg Gardner - 10/25/07\r
+M650W 75 801 0 3.351 5.125 AT\r
+0.08 1240\r
+0.12 1328\r
+0.25 1230\r
+0.5 1142\r
+1.0 1071\r
+1.5 1048\r
+2.0 1018\r
+2.5 982\r
+3.0 950\r
+3.5 853\r
+4.0 781\r
+5.0 595\r
+6.0 443\r
+7.0 297\r
+8.0 155\r
+9.0 88\r
+10.0 32\r
+10.5 12\r
+11.0 4\r
+11.5 0\r
+;\r
--- /dev/null
+;\r
+;98-10240 case\r
+; Greg Gardner - 10/25/07\r
+M750W 98 732 0 5.3 8.776 AT\r
+0.1 1032\r
+0.2 992\r
+0.3 974\r
+0.48 966\r
+1.0 1055\r
+1.5 1152\r
+2.0 1192\r
+2.5 1218\r
+4.0 1103\r
+6.0 818\r
+8.0 561\r
+10.0 318\r
+11.0 216\r
+12.0 125\r
+13.0 76\r
+14.0 47\r
+15.0 23\r
+15.5 9\r
+16.0 0\r
+;\r
--- /dev/null
+;\r
+;\r
+M845HW 98 795.02 100 3.569 6.833 AT\r
+0.015456 1332.26\r
+0.0463679 1706.62\r
+0.0772798 1178.12\r
+0.185471 1310.24\r
+0.973725 1222.16\r
+1.51468 1200.14\r
+1.97836 1123.07\r
+3.97218 1057\r
+4.20402 880.836\r
+6.01236 627.596\r
+6.495 418.397\r
+7.017 99.0941\r
+7.5 0\r
--- /dev/null
+; AeroTech N2000W\r
+; Copyright Tripoli Motor Testing 1997 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+N2000W 98 1046 0 7.66707 12.2828 AT\r
+ 0.146 2775.075\r
+ 0.446 2831.810\r
+ 0.746 2834.354\r
+ 1.046 2829.564\r
+ 1.346 2777.650\r
+ 1.646 2688.252\r
+ 1.950 2597.973\r
+ 2.254 2501.043\r
+ 2.554 2415.747\r
+ 2.854 2343.624\r
+ 3.154 2262.579\r
+ 3.454 2178.182\r
+ 3.758 2104.164\r
+ 4.062 2024.475\r
+ 4.362 1935.616\r
+ 4.663 1839.781\r
+ 4.962 1756.910\r
+ 5.262 1351.806\r
+ 5.567 954.556\r
+ 5.871 681.831\r
+ 6.171 475.910\r
+ 6.471 361.124\r
+ 6.771 194.633\r
+ 7.071 44.938\r
+ 7.375 6.030\r
+ 7.679 0.000\r
--- /dev/null
+; AeroTech N4800T\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+N4800T 98 1194 0 9.7664 14.784 AT\r
+ 0.098 4752.717\r
+ 0.301 6007.533\r
+ 0.506 5594.225\r
+ 0.710 5270.361\r
+ 0.914 5150.120\r
+ 1.119 5108.054\r
+ 1.324 5086.206\r
+ 1.528 5031.651\r
+ 1.731 4941.811\r
+ 1.936 4800.400\r
+ 2.140 4664.876\r
+ 2.344 4527.840\r
+ 2.549 4401.003\r
+ 2.754 4263.565\r
+ 2.958 4120.406\r
+ 3.161 3971.136\r
+ 3.366 3876.421\r
+ 3.570 3916.232\r
+ 3.774 3913.510\r
+ 3.979 3312.758\r
+ 4.184 1649.267\r
+ 4.388 523.361\r
+ 4.591 327.209\r
+ 4.796 251.041\r
+ 5.001 128.177\r
+ 5.206 0.000\r
--- /dev/null
+;\r
+;Apogee 1/2A2 RASP.ENG file made from NAR published data\r
+;File produced September 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+1/2A2 11 57 2-4-6 0.0015 0.0044 Apogee \r
+0.007 0.19\r
+0.045 1.494\r
+0.078 3.152\r
+0.088 3.805\r
+0.093 3.805\r
+0.1 3.97\r
+0.105 3.696\r
+0.11 3.071\r
+0.117 2.554\r
+0.123 2.582\r
+0.132 2.31\r
+0.163 2.146\r
+0.2 1.984\r
+0.242 1.902\r
+0.253 2.01\r
+0.275 1.929\r
+0.342 1.929\r
+0.403 1.929\r
+0.41 1.848\r
+0.42 1.902\r
+0.467 1.902\r
+0.528 1.929\r
+0.565 1.929\r
+0.58 1.902\r
+0.593 1.848\r
+0.603 1.657\r
+0.61 1.141\r
+0.615 0.597\r
+0.622 0.244\r
+0.63 0\r
--- /dev/null
+;Apogee 1/4A2 RASP.ENG file made from NAR published data\r
+;File produced September 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+1/4A2 11 38 2-4 0.0008 0.0036 Apogee \r
+0.007 0.162\r
+0.023 0.65\r
+0.041 1.463\r
+0.058 2.519\r
+0.074 3.738\r
+0.079 3.9\r
+0.088 4.915\r
+0.097 5.119\r
+0.106 5.4\r
+0.11 5.119\r
+0.118 3.981\r
+0.125 3.656\r
+0.132 3.453\r
+0.136 3.209\r
+0.151 3.169\r
+0.156 2.966\r
+0.168 2.884\r
+0.18 2.397\r
+0.194 1.625\r
+0.207 1.056\r
+0.218 0.406\r
+0.23 0\r
--- /dev/null
+;\r
+;Apogee A2 RASP.ENG file made from NAR published data\r
+;File produced September 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+A2 11 58 0-3-5-7 0.003 0.0067 Apogee \r
+0.014 0.241\r
+0.036 0.895\r
+0.064 2.618\r
+0.1 4.82\r
+0.111 4.133\r
+0.125 2.687\r
+0.139 2.307\r
+0.185 2.031\r
+0.296 1.928\r
+0.481 1.825\r
+0.517 1.722\r
+0.538 1.791\r
+0.649 1.688\r
+0.748 1.757\r
+0.869 1.825\r
+1.04 1.894\r
+1.101 1.894\r
+1.119 1.825\r
+1.144 1.928\r
+1.229 1.859\r
+1.265 1.894\r
+1.283 1.757\r
+1.29 1.412\r
+1.293 0.688\r
+1.3 0.275\r
+1.31 0\r
--- /dev/null
+;\r
+;Apogee B2 RASP.ENG file made from NAR published data\r
+;File produced September 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+B2 11 88 0-3-5-7-9 0.006 0.0106 Apogee \r
+0.057 1.637\r
+0.093 4.091\r
+0.121 5.48\r
+0.143 4.787\r
+0.157 3.478\r
+0.207 2.578\r
+0.328 2.087\r
+0.371 2.087\r
+0.406 1.882\r
+0.641 1.841\r
+0.869 1.841\r
+1.283 1.882\r
+1.361 1.882\r
+1.397 1.718\r
+1.439 1.841\r
+1.532 1.718\r
+1.71 1.841\r
+1.888 1.882\r
+2.095 1.8\r
+2.23 1.8\r
+2.295 1.677\r
+2.423 1.759\r
+2.444 1.637\r
+2.466 0.982\r
+2.494 0.327\r
+2.53 0\r
--- /dev/null
+;\r
+;Apogee B7 RASP.ENG file made from NAR published data\r
+;File produced September 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+B7 13 50 4-6-8-10 0.0028 0.0091 Apogee \r
+0.007 5.708\r
+0.013 7.211\r
+0.032 6.111\r
+0.045 8.116\r
+0.056 7.717\r
+0.069 9.02\r
+0.078 12.122\r
+0.087 14.76\r
+0.106 13.832\r
+0.117 13.733\r
+0.125 12.636\r
+0.155 12.438\r
+0.168 11.836\r
+0.2 11.243\r
+0.209 11.737\r
+0.219 10.739\r
+0.266 9.846\r
+0.29 9.849\r
+0.299 8.949\r
+0.367 7.456\r
+0.393 7.159\r
+0.429 5.761\r
+0.487 4.567\r
+0.571 2.975\r
+0.607 2.178\r
+0.669 1.084\r
+0.708 0.489\r
+0.74 0\r
--- /dev/null
+;\r
+;Apogee C10 RASP.ENG file made from NAR published data\r
+;File produced September 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+C10 18 50 4-7-10 0.0049 0.0176 Apogee \r
+0.01 2.712\r
+0.019 5.842\r
+0.029 17.116\r
+0.037 25.72\r
+0.051 22.535\r
+0.07 20.446\r
+0.106 18.983\r
+0.164 17.085\r
+0.188 17.085\r
+0.2 15.824\r
+0.216 16.036\r
+0.255 15.602\r
+0.293 14.35\r
+0.343 13.503\r
+0.394 12.655\r
+0.41 11.605\r
+0.434 11.605\r
+0.521 9.287\r
+0.631 6.34\r
+0.741 4.021\r
+0.851 2.119\r
+0.911 1.48\r
+0.945 1.264\r
+0.96 0\r
--- /dev/null
+;\r
+;Apogee C4 RASP.ENG file made from NAR published data\r
+;File produced September 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+C4 18 50 3-5-7 0.0045 0.017 Apogee \r
+0.018 3.23\r
+0.041 6.874\r
+0.147 8.779\r
+0.294 10.683\r
+0.365 11.31\r
+0.388 10.521\r
+0.412 8.779\r
+0.441 7.04\r
+0.465 4.555\r
+0.529 3.479\r
+0.629 2.981\r
+0.653 3.23\r
+0.718 2.816\r
+0.853 2.733\r
+1.065 2.65\r
+1.253 2.567\r
+1.453 2.401\r
+1.694 2.484\r
+1.794 2.484\r
+1.812 2.733\r
+1.841 2.401\r
+1.947 2.401\r
+2.112 2.401\r
+2.235 2.401\r
+2.282 2.236\r
+2.312 1.656\r
+2.329 0.662\r
+2.35 0\r
--- /dev/null
+;\r
+;Apogee C6 RASP.ENG file made from NAR published data\r
+;File produced September 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+C6 13 83 4-7-10 0.007 0.0151 Apogee \r
+0.008 13.958\r
+0.016 21.1\r
+0.022 15.511\r
+0.03 12.831\r
+0.052 14.8\r
+0.081 15.927\r
+0.092 14.658\r
+0.114 16.069\r
+0.125 14.658\r
+0.136 15.369\r
+0.168 14.8\r
+0.214 13.816\r
+0.225 12.973\r
+0.247 13.958\r
+0.252 12.831\r
+0.285 12.547\r
+0.307 12.405\r
+0.317 12.831\r
+0.328 11.562\r
+0.347 11.988\r
+0.393 11.42\r
+0.442 10.719\r
+0.464 11.136\r
+0.488 9.164\r
+0.545 8.459\r
+0.624 7.754\r
+0.716 6.485\r
+0.838 5.075\r
+0.977 3.102\r
+1.096 1.833\r
+1.207 0.986\r
+1.32 0\r
--- /dev/null
+;\r
+;Apogee D10 RASP.ENG file made from NAR published data\r
+;File produced September 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+D10 18 70 3-5-7 0.0098 0.0259 Apogee \r
+0.011 14.506\r
+0.018 25.13\r
+0.032 20.938\r
+0.079 19.065\r
+0.122 21.139\r
+0.136 19.686\r
+0.169 21.139\r
+0.201 20.728\r
+0.223 21.76\r
+0.233 20.938\r
+0.255 21.97\r
+0.276 20.938\r
+0.352 20.728\r
+0.402 20.107\r
+0.42 20.728\r
+0.459 20.107\r
+0.488 20.517\r
+0.556 18.243\r
+0.671 15.959\r
+0.707 14.717\r
+0.729 15.127\r
+0.779 12.853\r
+0.793 13.474\r
+0.836 11.401\r
+0.904 10.158\r
+0.926 10.569\r
+0.99 8.083\r
+1.026 8.498\r
+1.123 6.011\r
+1.231 2.487\r
+1.342 0.829\r
+1.4 0\r
--- /dev/null
+;\r
+;Apogee D3 RASP.ENG file made from NAR published data\r
+;File produced September 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+D3 18 77 3-5-7 0.0098 0.0249 Apogee \r
+0.05 6.79\r
+0.168 8.788\r
+0.318 10.46\r
+0.385 10.07\r
+0.402 7.909\r
+0.469 5.432\r
+0.486 3.914\r
+0.687 3.115\r
+1.122 2.876\r
+2.06 2.636\r
+3.349 2.397\r
+4.639 2.156\r
+5.727 1.997\r
+6.163 1.837\r
+6.263 3.994\r
+6.347 2.317\r
+6.364 0.719\r
+6.39 0\r
--- /dev/null
+;\r
+;Aerotech E6 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+E6 24 70 2-4-6-8-100 0.0215 0.0463 Apogee \r
+0.056 18.59\r
+0.112 20.12\r
+0.168 17.575\r
+0.307 14.38\r
+0.531 10.45\r
+0.894 7.696\r
+1.146 6.244\r
+1.691 5.808\r
+2.836 5.663\r
+3.898 5.517\r
+4.275 5.227\r
+4.415 4.937\r
+5.058 5.082\r
+5.519 5.227\r
+5.603 6.679\r
+5.729 3.921\r
+5.882 2.323\r
+5.966 1.016\r
+6.06 0\r
--- /dev/null
+;\r
+;Aerotech F10 RASP.ENG file made from NAR published data\r
+;File produced July 4, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+F10 29 93 4-6-8 0.0407 0.0841 Apogee \r
+0.015 28.22\r
+0.077 26.082\r
+0.201 24.934\r
+0.31 22.806\r
+0.464 20.183\r
+0.573 17.886\r
+0.789 16.075\r
+1.068 13.946\r
+1.393 12.63\r
+1.718 11.155\r
+2.166 9.844\r
+2.677 9.515\r
+3.311 9.187\r
+3.683 8.859\r
+3.791 9.679\r
+4.101 9.679\r
+4.658 9.515\r
+5.168 9.023\r
+5.725 9.023\r
+6.112 8.531\r
+6.329 8.859\r
+6.499 7.546\r
+6.685 5.742\r
+6.778 4.921\r
+6.917 2.625\r
+7.025 1.312\r
+7.13 0\r
--- /dev/null
+; Cesaroni G60\r
+; Copyright Tripoli Motor Testing 2002 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+G60 38 125 0 0.077056 0.2016 CSR\r
+ 0.043 65.216\r
+ 0.130 74.906\r
+ 0.218 84.596\r
+ 0.305 83.963\r
+ 0.393 81.982\r
+ 0.480 81.956\r
+ 0.568 81.138\r
+ 0.655 80.530\r
+ 0.743 79.923\r
+ 0.830 78.867\r
+ 0.918 76.675\r
+ 1.005 75.118\r
+ 1.094 73.732\r
+ 1.182 71.315\r
+ 1.270 68.781\r
+ 1.357 66.853\r
+ 1.445 65.111\r
+ 1.532 63.526\r
+ 1.620 61.229\r
+ 1.707 59.249\r
+ 1.795 57.110\r
+ 1.882 51.671\r
+ 1.970 18.562\r
+ 2.057 2.667\r
+ 2.146 1.470\r
+ 2.234 0.000\r
--- /dev/null
+; Pro38 G69\r
+G69 38 127 5-7-9-12 0.066500 0.2045 Pro38\r
+ 0.079 79.935\r
+ 0.103 72.367\r
+ 0.136 82.989\r
+ 0.217 85.910\r
+ 0.247 82.193\r
+ 0.311 84.317\r
+ 0.352 80.201\r
+ 0.387 82.856\r
+ 0.840 72.499\r
+ 0.944 72.234\r
+ 0.978 69.047\r
+ 1.017 71.835\r
+ 1.082 67.852\r
+ 1.227 66.657\r
+ 1.237 65.329\r
+ 1.493 62.010\r
+ 1.530 59.354\r
+ 1.591 60.151\r
+ 1.714 56.167\r
+ 1.769 54.574\r
+ 1.848 46.607\r
+ 1.887 27.884\r
+ 1.958 00.000 \r
--- /dev/null
+;\r
+133G69 38.0 127.00 3-5-7-9-12 0.08400 0.20510 CTI\r
+ 0.06 80.77 \r
+ 0.20 84.50 \r
+ 0.41 82.74 \r
+ 0.59 80.11 \r
+ 0.81 76.82 \r
+ 1.00 72.87 \r
+ 1.20 68.92 \r
+ 1.40 64.31 \r
+ 1.61 59.48 \r
+ 1.82 50.92 \r
+ 1.90 21.40 \r
+ 1.93 0.00 \r
--- /dev/null
+;\r
+; Pro38 G79SS\r
+G79SS 38 127 6-8-10-13 0.069000 0.2070 Pro38\r
+0.042 67.279\r
+0.050 72.145\r
+0.065 76.176\r
+0.072 76.176\r
+0.082 74.647\r
+0.094 68.252\r
+0.109 66.167\r
+0.122 65.611\r
+0.433 81.041\r
+0.633 88.130\r
+0.643 87.574\r
+0.684 89.659\r
+0.723 89.798\r
+0.834 92.162\r
+0.939 93.135\r
+1.000 93.969\r
+1.151 91.884\r
+1.160 90.772\r
+1.185 91.189\r
+1.303 86.879\r
+1.499 77.149\r
+1.518 75.064\r
+1.540 66.584\r
+1.587 23.631\r
+1.607 10.982\r
+1.629 4.865\r
+1.631 0.000\r
--- /dev/null
+;\r
+;\r
+G79SS 38.0 127.00 13-10-8-6-4 0.08500 0.22600 CTI\r
+ 0.00 9.07 \r
+ 0.03 54.45 \r
+ 0.07 76.33 \r
+ 0.09 70.95 \r
+ 0.11 65.92 \r
+ 0.17 68.59 \r
+ 0.20 70.64 \r
+ 0.30 74.89 \r
+ 0.40 80.39 \r
+ 0.50 83.76 \r
+ 0.60 86.45 \r
+ 0.70 88.65 \r
+ 0.81 91.40 \r
+ 0.90 93.06 \r
+ 1.00 93.99 \r
+ 1.10 95.81 \r
+ 1.20 90.70 \r
+ 1.30 86.92 \r
+ 1.40 81.98 \r
+ 1.50 76.54 \r
+ 1.55 58.92 \r
+ 1.60 16.41 \r
+ 1.63 5.16 \r
+ 1.63 0.00 \r
--- /dev/null
+;Pro-38 Red Lightning 2 Grain reload\r
+H120-14A 38 186 14-11-9-7-5 0.1366 0.295 CTI \r
+0.016 53.023\r
+0.029 107.113\r
+0.036 124.55\r
+0.049 129.532\r
+0.062 117.789\r
+0.072 98.217\r
+0.131 123.838\r
+0.199 136.649\r
+0.258 144.122\r
+0.313 147.681\r
+0.369 146.257\r
+0.441 145.19\r
+0.558 143.411\r
+0.683 141.631\r
+0.777 140.92\r
+0.859 139.14\r
+0.98 136.293\r
+1.097 133.091\r
+1.251 128.82\r
+1.434 122.771\r
+1.558 118.856\r
+1.639 117.077\r
+1.731 117.077\r
+1.884 117.077\r
+1.927 105.334\r
+1.959 88.964\r
+1.995 68.325\r
+2.031 41.991\r
+2.083 18.505\r
+2.142 6.761\r
+2.181 2.135\r
+2.24 0\r
+;\r
--- /dev/null
+;\r
+;\r
+H143SS 38.0 186.00 4-6-8-10-13 0.16540 0.34700 CTI\r
+ 0.06 114.68 \r
+ 0.19 134.25 \r
+ 0.40 149.61 \r
+ 0.60 158.10 \r
+ 0.80 163.77 \r
+ 1.00 167.00 \r
+ 1.21 160.93 \r
+ 1.40 148.80 \r
+ 1.60 128.59 \r
+ 1.63 117.26 \r
+ 1.65 106.35 \r
+ 1.70 35.63 \r
+ 1.73 0.00 \r
--- /dev/null
+;\r
+;\r
+244H153 38.0 186.00 4-6-8-10-13 0.14390 0.30390 CTI\r
+ 0.13 149.36 \r
+ 0.17 173.70 \r
+ 0.23 171.77 \r
+ 0.39 179.91 \r
+ 0.60 188.30 \r
+ 0.81 180.40 \r
+ 1.01 168.25 \r
+ 1.18 160.91 \r
+ 1.29 149.64 \r
+ 1.41 136.95 \r
+ 1.60 105.37 \r
+ 1.69 23.58 \r
+ 1.75 0.00 \r
--- /dev/null
+;\r
+;\r
+H565 38 245 5-7-9-11-14 0.1742 0.3622 Cesaroni\r
+0.01 106.91\r
+0.02 479.76\r
+0.0347144 528.926\r
+0.0515118 553.719\r
+0.0761478 578.512\r
+0.100784 586.777\r
+0.150056 611.57\r
+0.2 607.1\r
+0.3 614.73\r
+0.4 616.58\r
+0.5 622.37\r
+0.51 645.28\r
+0.52 628.99\r
+0.53 535.19\r
+0.54 327.35\r
+0.55 147.98\r
+0.56 60.36\r
+0.57 0\r
--- /dev/null
+; Cesaroni I170\r
+; Copyright Tripoli Motor Testing 2002 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I170 38 242 0 0.209216 0.404992 CSR\r
+ 0.044 133.251\r
+ 0.134 230.114\r
+ 0.226 224.136\r
+ 0.318 223.695\r
+ 0.409 223.888\r
+ 0.501 225.265\r
+ 0.593 226.670\r
+ 0.684 229.397\r
+ 0.775 231.998\r
+ 0.866 233.034\r
+ 0.957 233.144\r
+ 1.049 232.703\r
+ 1.141 231.574\r
+ 1.232 227.745\r
+ 1.324 225.844\r
+ 1.416 221.160\r
+ 1.506 217.396\r
+ 1.597 211.711\r
+ 1.689 205.678\r
+ 1.780 198.020\r
+ 1.872 125.016\r
+ 1.964 61.847\r
+ 2.055 23.279\r
+ 2.147 2.066\r
+ 2.239 0.799\r
+ 2.330 0.000\r
--- /dev/null
+;\r
+;\r
+384I205 38.0 245.00 5-7-9-11-14 0.20610 0.40220 CTI\r
+ 0.10 181.50 \r
+ 0.13 213.30 \r
+ 0.20 210.10 \r
+ 0.40 226.61 \r
+ 0.60 235.80 \r
+ 0.80 234.00 \r
+ 1.00 232.80 \r
+ 1.20 227.70 \r
+ 1.40 216.80 \r
+ 1.60 197.21 \r
+ 1.74 183.13 \r
+ 1.80 87.30 \r
+ 1.88 0.00 \r
--- /dev/null
+;\r
+;\r
+I212SS 38.0 245.00 5-7-9-11-14 0.24810 0.47500 CTI\r
+ 0.04 189.66 \r
+ 0.20 207.11 \r
+ 0.40 222.41 \r
+ 0.60 236.62 \r
+ 0.80 249.60 \r
+ 0.95 255.15 \r
+ 1.01 250.22 \r
+ 1.21 233.54 \r
+ 1.40 208.99 \r
+ 1.55 183.99 \r
+ 1.60 168.08 \r
+ 1.63 134.62 \r
+ 1.69 25.86 \r
+ 1.71 0.00 \r
--- /dev/null
+; Cesaroni I240\r
+; Copyright Tripoli Motor Testing 2002 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I240 38 302 0 0.274624 0.503552 CSR\r
+ 0.043 265.317\r
+ 0.131 320.903\r
+ 0.221 314.148\r
+ 0.310 312.413\r
+ 0.399 313.564\r
+ 0.488 314.335\r
+ 0.577 321.117\r
+ 0.667 325.923\r
+ 0.755 327.040\r
+ 0.844 326.831\r
+ 0.933 324.348\r
+ 1.023 321.063\r
+ 1.111 317.446\r
+ 1.200 308.301\r
+ 1.290 300.612\r
+ 1.379 293.536\r
+ 1.468 283.358\r
+ 1.556 273.832\r
+ 1.646 259.708\r
+ 1.735 190.662\r
+ 1.824 124.130\r
+ 1.912 60.875\r
+ 2.002 26.967\r
+ 2.092 7.636\r
+ 2.181 2.296\r
+ 2.271 0.000\r
--- /dev/null
+;\r
+;\r
+512I285 38.0 303.00 6-8-10-12-15 0.27240 0.50590 CTI\r
+ 0.10 350.60 \r
+ 0.15 318.73 \r
+ 0.20 312.30 \r
+ 0.40 322.37 \r
+ 0.60 330.57 \r
+ 0.80 329.40 \r
+ 1.00 319.64 \r
+ 1.20 294.14 \r
+ 1.40 271.90 \r
+ 1.60 239.90 \r
+ 1.66 178.10 \r
+ 1.80 44.80 \r
+ 1.91 0.00 \r
--- /dev/null
+;\r
+;\r
+I287SS 38.0 303.00 6-8-10-12-15 0.33080 0.60500 CTI\r
+ 0.05 275.86 \r
+ 0.20 292.53 \r
+ 0.41 309.20 \r
+ 0.61 327.53 \r
+ 0.80 341.70 \r
+ 0.90 344.20 \r
+ 1.01 331.70 \r
+ 1.20 311.70 \r
+ 1.40 280.03 \r
+ 1.53 245.02 \r
+ 1.58 176.62 \r
+ 1.60 141.76 \r
+ 1.64 68.99 \r
+ 1.70 17.48 \r
+ 1.70 0.00 \r
--- /dev/null
+;\r
+;\r
+I350SS 38.0 367.00 7-9-11-13-16 0.41350 0.78200 CTI\r
+ 0.05 399.74 \r
+ 0.13 390.06 \r
+ 0.19 386.19 \r
+ 0.40 388.13 \r
+ 0.60 388.13 \r
+ 0.80 388.13 \r
+ 1.00 389.91 \r
+ 1.20 387.38 \r
+ 1.33 368.77 \r
+ 1.44 350.38 \r
+ 1.52 320.37 \r
+ 1.60 164.79 \r
+ 1.68 36.77 \r
+ 1.71 0.00 \r
--- /dev/null
+;\r
+;\r
+I360 38 367 15 0.3346 0.5963 Cesaroni\r
+0.08 555.5\r
+0.1 489.7\r
+0.13 448\r
+0.2 449\r
+0.4 483.7\r
+0.55 498\r
+0.6 494.9\r
+0.7 481.91\r
+0.8 457.9\r
+1 406.6\r
+1.2 344.4\r
+1.3 309.3\r
+1.4 182.2\r
+1.55 158.9\r
+1.6 101.8\r
+1.7 55.8\r
+1.77 0\r
--- /dev/null
+;\r
+;\r
+I540WT 38 367 7-9-11-13-16 0.3288 0.5982 CTI \r
+0.03 597.86\r
+0.04 611.31\r
+0.06 605.64\r
+0.12 612.36\r
+0.24 624.54\r
+0.36 626\r
+0.48 623.63\r
+0.6 616.42\r
+0.72 598.14\r
+0.84 583.16\r
+0.95 568.92\r
+0.96 558.53\r
+0.98 533.45\r
+1.02 436.53\r
+1.06 303.15\r
+1.09 184.92\r
+1.13 74.27\r
+1.18 0\r
--- /dev/null
+;\r
+;\r
+J210 54.0 236.00 6-16 0.08270 0.84200 CTI\r
+ 0.04 335.00 \r
+ 0.16 270.92 \r
+ 0.41 269.30 \r
+ 0.80 268.49 \r
+ 1.18 256.32 \r
+ 1.62 236.85 \r
+ 2.03 214.14 \r
+ 2.38 193.86 \r
+ 2.79 174.39 \r
+ 3.20 163.85 \r
+ 3.60 157.36 \r
+ 3.75 135.46 \r
+ 3.86 85.17 \r
+ 3.99 0.00 \r
--- /dev/null
+;\r
+;\r
+J280SS 54.0 236.00 6-16 0.51200 0.95400 CTI\r
+ 0.10 259.43 \r
+ 0.30 278.91 \r
+ 0.60 293.07 \r
+ 0.90 306.85 \r
+ 1.20 319.19 \r
+ 1.50 321.10 \r
+ 1.80 310.85 \r
+ 2.11 279.89 \r
+ 2.35 286.70 \r
+ 2.40 269.17 \r
+ 2.44 178.24 \r
+ 2.49 42.80 \r
+ 2.54 0.00 \r
--- /dev/null
+;\r
+;\r
+J285 38.0 367.00 6-8-10-12-15 0.31250 0.59500 CTI\r
+ 0.06 351.01 \r
+ 0.15 346.01 \r
+ 0.25 357.64 \r
+ 0.50 363.90 \r
+ 0.75 369.26 \r
+ 1.03 343.33 \r
+ 1.27 337.07 \r
+ 1.51 317.40 \r
+ 1.75 282.53 \r
+ 1.93 127.86 \r
+ 2.02 84.94 \r
+ 2.25 11.02 \r
+ 2.26 0.00 \r
--- /dev/null
+;\r
+;\r
+J295 54.0 329.00 6-16 0.59400 1.11900 CTI\r
+ 0.04 450.52 \r
+ 0.28 428.70 \r
+ 0.54 423.25 \r
+ 1.00 391.61 \r
+ 1.48 352.34 \r
+ 1.99 304.35 \r
+ 2.51 266.17 \r
+ 3.00 243.26 \r
+ 3.50 216.92 \r
+ 3.67 126.54 \r
+ 3.82 64.36 \r
+ 4.00 0.00 \r
--- /dev/null
+; Cesaroni J300\r
+; Copyright Tripoli Motor Testing 2002 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J300 38 360 0 0.340032 0.606592 CSR\r
+ 0.043 357.026\r
+ 0.131 436.586\r
+ 0.221 407.925\r
+ 0.310 399.528\r
+ 0.400 400.588\r
+ 0.490 406.733\r
+ 0.578 414.302\r
+ 0.667 417.117\r
+ 0.756 418.415\r
+ 0.846 421.302\r
+ 0.935 422.229\r
+ 1.025 415.951\r
+ 1.114 406.356\r
+ 1.202 395.237\r
+ 1.292 381.728\r
+ 1.381 369.861\r
+ 1.471 355.451\r
+ 1.560 331.691\r
+ 1.649 246.243\r
+ 1.738 161.766\r
+ 1.827 109.478\r
+ 1.917 71.413\r
+ 2.006 37.058\r
+ 2.096 13.880\r
+ 2.185 5.059\r
+ 2.275 0.000\r
--- /dev/null
+;\r
+;\r
+J330 38.0 421.00 7-9-11-13-16 0.37500 0.70200 CTI\r
+ 0.05 459.32 \r
+ 0.16 448.20 \r
+ 0.27 440.41 \r
+ 0.51 437.08 \r
+ 0.75 427.07 \r
+ 1.02 412.61 \r
+ 1.26 387.03 \r
+ 1.50 360.34 \r
+ 1.69 321.41 \r
+ 1.79 300.28 \r
+ 1.91 126.79 \r
+ 1.99 107.88 \r
+ 2.23 22.56 \r
+ 2.26 0.00 \r
--- /dev/null
+; Cesaroni J360\r
+; Copyright Tripoli Motor Testing 2002 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J360 38 419 0 0.409024 0.709184 CSR\r
+ 0.041 618.905\r
+ 0.124 616.584\r
+ 0.207 563.785\r
+ 0.291 557.730\r
+ 0.374 558.409\r
+ 0.457 562.088\r
+ 0.541 561.267\r
+ 0.624 563.219\r
+ 0.708 565.328\r
+ 0.793 566.558\r
+ 0.876 549.383\r
+ 0.959 529.633\r
+ 1.043 511.099\r
+ 1.126 483.285\r
+ 1.209 445.397\r
+ 1.293 421.658\r
+ 1.377 378.330\r
+ 1.461 261.647\r
+ 1.545 197.445\r
+ 1.628 146.570\r
+ 1.711 101.807\r
+ 1.795 78.039\r
+ 1.878 47.847\r
+ 1.961 31.861\r
+ 2.046 9.220\r
+ 2.130 0.000\r
--- /dev/null
+;\r
+;\r
+J380SS 54.0 320.00 6-16 0.76900 1.29330 CTI\r
+ 0.05 368.48 \r
+ 0.30 348.31 \r
+ 0.60 378.83 \r
+ 0.90 400.93 \r
+ 1.20 419.52 \r
+ 1.50 433.09 \r
+ 1.80 434.60 \r
+ 2.10 408.81 \r
+ 2.40 369.92 \r
+ 2.56 410.58 \r
+ 2.59 297.80 \r
+ 2.71 45.25 \r
+ 2.73 0.00 \r
--- /dev/null
+;\r
+;\r
+J400SS 38.0 421.00 7-9-11-13-16 0.48960 0.70200 CTI\r
+ 0.05 451.79 \r
+ 0.20 461.14 \r
+ 0.31 465.81 \r
+ 0.44 463.47 \r
+ 0.60 477.48 \r
+ 0.80 482.15 \r
+ 1.00 461.31 \r
+ 1.20 433.12 \r
+ 1.35 402.76 \r
+ 1.40 382.92 \r
+ 1.47 321.04 \r
+ 1.55 258.00 \r
+ 1.60 178.62 \r
+ 1.73 14.58 \r
+ 1.75 0.00 \r
--- /dev/null
+; Pro38 Red Lightning 6G.\r
+J410-16A 38 421 16 0.4098 0.735 CTI\r
+ 0.023 375.45\r
+ 0.029 446.221\r
+ 0.042 510.996\r
+ 0.11 499.0\r
+ 0.22 495.402\r
+ 0.442 491.803\r
+ 0.675 475.01\r
+ 0.901 464.214\r
+ 1.092 448.62\r
+ 1.221 437.825\r
+ 1.34 429.428\r
+ 1.492 419.832\r
+ 1.553 389.844\r
+ 1.592 349.06\r
+ 1.65 273.491\r
+ 1.689 196.721\r
+ 1.75 122.351\r
+ 1.809 64.774\r
+ 1.889 28.788\r
+ 1.941 13.195\r
+ 1.989 0.0\r
--- /dev/null
+;\r
+;\r
+K445 54.0 404.00 7-17 0.79200 1.39800 CTI\r
+ 0.05 664.83 \r
+ 0.19 640.68 \r
+ 0.48 622.98 \r
+ 1.00 576.29 \r
+ 1.51 515.12 \r
+ 2.00 442.68 \r
+ 2.50 392.26 \r
+ 3.02 350.93 \r
+ 3.13 339.66 \r
+ 3.31 210.88 \r
+ 3.47 78.88 \r
+ 3.67 0.00 \r
--- /dev/null
+;\r
+; Cesaroni Pro75 2486K510 \r
+; 'Classic Propellant'\r
+;\r
+; RockSim file by Kathy Miller\r
+; wRasp Adaptation by Len Lekx\r
+;\r
+K510 75 350 0 1.19 2.59 CTI\r
+0.10 645.25 \r
+0.30 689.75 \r
+0.50 658.60 \r
+1.00 636.35 \r
+1.60 600.75 \r
+2.00 565.15 \r
+2.40 534.00 \r
+2.50 525.10 \r
+3.00 471.70 \r
+3.50 422.75 \r
+3.70 400.50 \r
+4.00 391.60 \r
+4.40 382.70 \r
+4.50 378.25 \r
+4.60 333.75 \r
+4.70 66.75 \r
+4.84 0.00 \r
--- /dev/null
+;\r
+;\r
+K510 75.0 350.00 0 1.19700 2.59000 CTI\r
+ 0.04 394.38 \r
+ 0.07 617.68 \r
+ 0.10 645.17 \r
+ 0.21 658.16 \r
+ 0.35 669.23 \r
+ 0.53 667.72 \r
+ 0.82 661.58 \r
+ 1.18 626.92 \r
+ 1.72 588.46 \r
+ 2.15 557.69 \r
+ 2.39 542.31 \r
+ 2.90 492.86 \r
+ 3.07 470.31 \r
+ 3.56 426.81 \r
+ 3.98 398.96 \r
+ 4.32 393.98 \r
+ 4.48 380.63 \r
+ 4.60 364.22 \r
+ 4.65 290.91 \r
+ 4.80 91.23 \r
+ 4.84 45.82 \r
+ 4.84 0.00 \r
--- /dev/null
+;\r
+;\r
+K530SS 54.0 404.00 6-16 1.02500 1.63980 CTI\r
+ 0.05 533.59 \r
+ 0.09 503.98 \r
+ 0.29 514.09 \r
+ 0.60 534.31 \r
+ 0.90 557.41 \r
+ 1.20 577.63 \r
+ 1.50 587.74 \r
+ 1.80 596.40 \r
+ 2.10 535.44 \r
+ 2.31 502.54 \r
+ 2.47 551.63 \r
+ 2.56 393.94 \r
+ 2.60 274.37 \r
+ 2.64 137.19 \r
+ 2.67 0.00 \r
--- /dev/null
+;\r
+;\r
+K570 54.0 488.00 7-17 0.99000 1.68500 CTI\r
+ 0.04 892.67 \r
+ 0.50 797.99 \r
+ 1.00 738.68 \r
+ 1.50 659.37 \r
+ 2.00 585.96 \r
+ 2.50 512.88 \r
+ 2.97 417.16 \r
+ 3.20 224.79 \r
+ 3.47 67.00 \r
+ 3.59 0.00 \r
--- /dev/null
+;\r
+;\r
+K575SS 75 395 1000 1.803 3.143 Cesaroni\r
+0 16\r
+0.11 664.5\r
+0.43 620.2\r
+0.87 629\r
+1.3 637.92\r
+1.73 637.92\r
+2.17 629\r
+2.6 615.77\r
+3.03 553.75\r
+3.47 518.31\r
+3.9 438.57\r
+4.18 79.74\r
+4.33 0\r
--- /dev/null
+;\r
+;\r
+K650SS 54.0 488.00 6-16 1.28100 1.98990 CTI\r
+ 0.04 664.52 \r
+ 0.12 645.90 \r
+ 0.31 642.24 \r
+ 0.60 664.78 \r
+ 0.91 684.59 \r
+ 1.22 712.82 \r
+ 1.50 723.41 \r
+ 1.80 728.70 \r
+ 2.10 664.52 \r
+ 2.40 614.68 \r
+ 2.51 680.53 \r
+ 2.55 534.62 \r
+ 2.61 268.19 \r
+ 2.66 0.00 \r
--- /dev/null
+;\r
+;\r
+K660 54.0 572.00 7-17 1.17700 1.94900 CTI\r
+ 0.07 1078.90 \r
+ 0.23 1006.47 \r
+ 0.40 966.76 \r
+ 0.80 897.52 \r
+ 1.20 842.72 \r
+ 1.60 794.15 \r
+ 2.01 744.52 \r
+ 2.40 692.27 \r
+ 2.54 671.37 \r
+ 2.68 439.08 \r
+ 2.80 400.68 \r
+ 3.01 386.90 \r
+ 3.20 234.31 \r
+ 3.45 106.65 \r
+ 3.60 44.03 \r
+ 3.69 0.00 \r
--- /dev/null
+;\r
+;\r
+L1090SS 75 665 1000 3.491 5.461 Cesaroni\r
+0 487.3\r
+0.11 1639.1\r
+0.22 1484.05\r
+0.44 1417.6\r
+0.87 1373.3\r
+1.31 1329\r
+1.74 1306.85\r
+2.18 1262.55\r
+2.61 1218.25\r
+3.05 1151.8\r
+3.21 775.25\r
+3.48 598.05\r
+3.92 553.75\r
+4.13 221.5\r
+4.35 0\r
--- /dev/null
+;\r
+; Cesaroni Pro75 5015L1115\r
+; 'Classic Propellant'\r
+;\r
+; RockSim file by Kathy Miller\r
+; wRasp Adaptation by Len Lekx\r
+;\r
+L1115 75 621 0 2.39 4.40 CTI\r
+0.10 1468.85 \r
+0.30 1490.75 \r
+0.80 1401.75 \r
+1.00 1437.35 \r
+1.50 1335.00 \r
+2.00 1268.25 \r
+2.20 1246.00 \r
+2.50 1112.50 \r
+3.00 1090.25 \r
+3.30 979.00 \r
+3.80 979.00 \r
+4.00 623.00 \r
+4.20 311.50 \r
+4.40 35.00 \r
+4.48 0.00 \r
--- /dev/null
+;\r
+;\r
+L1115 75.0 621.00 0 2.39400 4.40400 CTI\r
+ 0.01 45.46 \r
+ 0.01 522.52 \r
+ 0.01 984.04 \r
+ 0.04 1256.10 \r
+ 0.05 1389.85 \r
+ 0.08 1713.25 \r
+ 0.24 1515.65 \r
+ 0.30 1474.74 \r
+ 0.40 1443.28 \r
+ 0.42 1446.25 \r
+ 0.50 1430.02 \r
+ 0.76 1392.85 \r
+ 1.00 1361.70 \r
+ 1.28 1339.45 \r
+ 1.84 1259.35 \r
+ 2.25 1201.50 \r
+ 3.00 1076.11 \r
+ 3.50 990.86 \r
+ 3.92 832.85 \r
+ 4.00 607.78 \r
+ 4.10 434.99 \r
+ 4.22 288.73 \r
+ 4.38 156.49 \r
+ 4.48 86.39 \r
+ 5.00 0.00 \r
--- /dev/null
+;\r
+;\r
+L610 98 394 0 2.415 4.975 CTI \r
+0.06 262.5\r
+0.12 667.2\r
+0.25 929.7\r
+0.39 871.21\r
+0.65 849.83\r
+1.05 823.1\r
+1.5 785.69\r
+2 747.3\r
+2.5 707.3\r
+3 667.2\r
+3.48 641.38\r
+4 593.28\r
+4.47 561.21\r
+5 523.79\r
+5.44 502.41\r
+5.68 491.72\r
+6 475.69\r
+6.5 459.66\r
+7.01 443.62\r
+7.5 413.7\r
+8 284.7\r
+8.12 53.3\r
+8.13 0\r
--- /dev/null
+;\r
+;\r
+L730 54.0 649.00 0 1.35100 2.24700 CTI\r
+ 0.00 81.36 \r
+ 0.01 1079.71 \r
+ 0.02 1216.59 \r
+ 0.04 1154.68 \r
+ 0.20 1127.51 \r
+ 0.45 1055.11 \r
+ 0.60 1028.17 \r
+ 0.75 995.24 \r
+ 1.00 959.33 \r
+ 1.50 898.71 \r
+ 2.00 830.70 \r
+ 2.50 730.76 \r
+ 2.60 592.55 \r
+ 2.70 510.96 \r
+ 2.90 487.88 \r
+ 3.00 405.72 \r
+ 3.10 299.80 \r
+ 3.20 296.09 \r
+ 3.30 251.85 \r
+ 3.40 171.70 \r
+ 3.50 165.26 \r
+ 3.60 139.38 \r
+ 3.65 117.77 \r
+ 3.77 45.38 \r
+ 3.77 0.00 \r
--- /dev/null
+;\r
+; Cesaroni Pro75 3757L800\r
+; 'Classic Propellant'\r
+;\r
+; RockSim file by Kathy Miller\r
+; wRasp Adaptation by Len Lekx\r
+;\r
+L800 75 486 0 1.79 3.51 CTI\r
+0.10 1023.50 \r
+0.20 1005.70 \r
+0.30 1023.50 \r
+0.50 1014.60 \r
+1.00 1010.15 \r
+1.50 1001.25 \r
+2.00 956.75 \r
+2.40 890.00 \r
+2.50 845.50 \r
+3.00 756.50 \r
+3.50 689.75 \r
+3.70 667.50 \r
+3.90 654.15 \r
+4.00 623.00 \r
+4.60 111.25 \r
+4.67 0.00 \r
--- /dev/null
+;\r
+;\r
+L800 75.0 486.00 0 1.79500 3.51100 CTI\r
+ 0.00 27.28 \r
+ 0.01 402.41 \r
+ 0.01 1285.54 \r
+ 0.12 1056.51 \r
+ 0.26 1041.73 \r
+ 0.71 1026.95 \r
+ 1.28 998.38 \r
+ 2.05 901.36 \r
+ 2.41 849.64 \r
+ 2.83 763.51 \r
+ 3.25 707.06 \r
+ 3.65 655.14 \r
+ 3.80 651.74 \r
+ 4.00 624.07 \r
+ 4.10 601.34 \r
+ 4.19 536.17 \r
+ 4.31 415.67 \r
+ 4.41 270.17 \r
+ 4.52 140.20 \r
+ 4.60 76.92 \r
+ 4.65 54.94 \r
+ 4.67 40.16 \r
+ 5.00 0.00 \r
--- /dev/null
+;\r
+;\r
+L890SS 75 530 1000 2.671 4.346 Cesaroni\r
+0 20\r
+0.05 1151.8\r
+0.41 1054.34\r
+0.83 1045.48\r
+1.24 1036.62\r
+1.65 1027.76\r
+2.07 1018.9\r
+2.89 886\r
+3.31 775.25\r
+3.72 664.5\r
+3.98 177.2\r
+4.13 0\r
--- /dev/null
+;\r
+;\r
+M1060 98 548 0 3.622 6.673 CTI \r
+0.07 131\r
+0.1 594\r
+0.2 1453\r
+0.238 1494\r
+0.378 1450\r
+0.378 1425\r
+0.5 1423\r
+1 1462\r
+1.5 1456\r
+2 1430\r
+2.5 1376\r
+3 1280\r
+3.5 1190\r
+4 1051\r
+4.5 976\r
+5 883\r
+5.5 835\r
+6 793\r
+6.5 321\r
+7 13\r
+7.229 7\r
+7.23 0\r
--- /dev/null
+;\r
+; Cesaroni Pro75 6251M1400\r
+; 'Classic Propellant'\r
+;\r
+; RockSim file by Kathy Miller\r
+; wRasp Adaptation by Len Lekx\r
+;\r
+M1400 75 7570 0 2.99 5.30 CTI\r
+0.10 1993.60 \r
+0.50 1891.25 \r
+1.10 1780.00 \r
+1.50 1691.00 \r
+2.00 1602.00 \r
+2.30 1557.50 \r
+2.50 1513.00 \r
+3.00 1335.00 \r
+3.50 1223.75 \r
+3.70 1112.00 \r
+3.90 667.50 \r
+4.00 534.00 \r
+4.40 222.50 \r
+4.47 0.00 \r
--- /dev/null
+;\r
+;\r
+M1400 75.0 757.00 0 2.99200 5.30200 CTI\r
+ 0.02 991.61 \r
+ 0.07 1939.66 \r
+ 0.11 2291.75 \r
+ 0.14 1976.39 \r
+ 0.19 1962.48 \r
+ 0.29 1936.13 \r
+ 0.52 1881.02 \r
+ 0.75 1833.40 \r
+ 1.00 1778.08 \r
+ 1.25 1738.57 \r
+ 1.70 1654.82 \r
+ 2.40 1502.39 \r
+ 2.85 1389.48 \r
+ 3.25 1283.00 \r
+ 3.40 1232.23 \r
+ 3.53 1199.64 \r
+ 3.65 1083.69 \r
+ 3.70 909.39 \r
+ 3.90 641.50 \r
+ 4.00 502.82 \r
+ 4.03 463.03 \r
+ 4.22 336.09 \r
+ 4.43 138.68 \r
+ 4.47 93.21 \r
+ 5.00 0.00 \r
--- /dev/null
+;\r
+;\r
+M1450 98 702 0 4.83 8.578 CTI \r
+0.01 60\r
+0.06 524\r
+0.1 2164\r
+0.151 2416\r
+0.25 2162\r
+0.5 2037\r
+0.75 2022\r
+1 2009\r
+1.5 2006\r
+2 1968\r
+2.5 1895\r
+3 1770\r
+3.5 1673\r
+4 1517\r
+4.5 1337\r
+5 1166\r
+5.5 954\r
+5.8 687\r
+6.2 360\r
+6.86 79\r
+6.87 0\r
--- /dev/null
+;\r
+;\r
+M2505 98.0 548.00 0 3.42300 6.25800 CTI\r
+ 0.12 2600.00 \r
+ 0.21 2482.00 \r
+ 0.60 2715.00 \r
+ 0.90 2876.00 \r
+ 1.20 2938.00 \r
+ 1.50 2889.00 \r
+ 1.80 2785.00 \r
+ 2.10 2573.00 \r
+ 2.40 2349.00 \r
+ 2.70 2182.00 \r
+ 3.00 85.00 \r
+ 3.00 0.00 \r
--- /dev/null
+;\r
+;\r
+M520 98.0 548.00 0 3.71300 6.69300 CTI\r
+ 0.01 1077.00 \r
+ 0.25 1062.83 \r
+ 0.38 1065.66 \r
+ 0.50 971.00 \r
+ 0.71 938.12 \r
+ 0.93 915.45 \r
+ 1.23 878.61 \r
+ 2.07 906.95 \r
+ 2.61 901.28 \r
+ 3.03 892.78 \r
+ 3.50 872.94 \r
+ 3.93 836.09 \r
+ 4.96 756.73 \r
+ 6.08 657.54 \r
+ 7.05 549.84 \r
+ 7.79 461.98 \r
+ 8.39 391.12 \r
+ 9.06 323.10 \r
+ 10.01 243.74 \r
+ 11.01 172.89 \r
+ 12.00 116.20 \r
+ 13.95 0.00 \r
--- /dev/null
+;\r
+;\r
+M795 98 702 0 4.892 8.492 CTI \r
+0.15 612.314\r
+0.21 1532.76\r
+0.245 1722\r
+0.43 1717.66\r
+0.5 1542.85\r
+0.62 1430.02\r
+0.8 1389.71\r
+1 1374.27\r
+1.5 1338.9\r
+2 1305.38\r
+3 1271.81\r
+4 1204\r
+5 1078\r
+6 928\r
+7 743\r
+8 563\r
+9 424.898\r
+10 299.697\r
+11 196.164\r
+12 116.759\r
+12.7 65.434\r
+12.76 0\r
--- /dev/null
+;\r
+;\r
+N1100 98 1010 0 4.517 11.644 CTI \r
+0.16 2624\r
+0.33 2708\r
+0.91 2055\r
+1.22 1896\r
+2.44 1793\r
+3.66 1625\r
+4.88 1402\r
+6.12 1158\r
+7.41 854\r
+9.77 494\r
+12.18 111.2\r
+12.19 0\r
--- /dev/null
+;\r
+;\r
+N2500 98.0 1010.00 0 6.77800 11.66800 CTI\r
+ 0.02 773.70 \r
+ 0.05 3356.60 \r
+ 0.06 3657.80 \r
+ 0.10 3546.80 \r
+ 0.25 3403.80 \r
+ 0.40 3309.20 \r
+ 0.80 3262.50 \r
+ 1.00 3206.10 \r
+ 1.50 3088.50 \r
+ 2.00 2940.40 \r
+ 2.50 2792.60 \r
+ 3.00 2598.40 \r
+ 3.50 2402.50 \r
+ 4.00 2227.00 \r
+ 4.25 2152.50 \r
+ 4.40 2102.50 \r
+ 4.50 2007.00 \r
+ 4.60 1683.80 \r
+ 4.75 1269.50 \r
+ 5.00 767.30 \r
+ 5.41 341.30 \r
+ 5.42 0.00 \r
--- /dev/null
+;\r
+;Cesaroni Technologies Inc Motor Data File\r
+;Composed by Carl Tulanko for 150mm "O" CAR Certed Motor\r
+;24-Jun-2003 using CTI Cert graph to chart points\r
+O5100 150 803 1000 13.245 23.577 Cesaroni\r
+0.01 815.07\r
+0.02 1407.85\r
+0.03 2334.11\r
+0.04 3260.42\r
+0.05 4001.47\r
+0.07 4927.78\r
+0.07 5483.57\r
+0.09 5817.04\r
+0.13 6057.88\r
+0.2 6206.09\r
+0.3 6298.72\r
+0.43 6280.19\r
+0.6 6261.67\r
+0.78 6298.72\r
+0.97 6354.3\r
+1.05 6428.4\r
+1.12 6391.35\r
+1.34 6465.46\r
+1.49 6502.51\r
+1.75 6539.56\r
+1.88 6558.09\r
+2.16 6521.03\r
+2.36 6465.46\r
+2.58 6372.82\r
+2.96 6113.46\r
+3.56 5557.67\r
+4.13 4909.25\r
+4.72 4260.83\r
+4.83 4149.68\r
+4.93 3038.1\r
+5 2612\r
+5.1 2111.79\r
+5.23 1741.29\r
+5.32 1537.53\r
+5.52 1222.61\r
+5.8 907.69\r
+5.85 666.88\r
+5.89 333.44\r
+5.9 0\r
--- /dev/null
+; Pro 150 O5800 White Thunder\r
+O5800 150 754 P 13.950000000000001 26.368000000000002 CTI\r
+ 0.069 6337.621\r
+ 0.103 5700.965\r
+ 0.218 5874.598\r
+ 0.378 6135.048\r
+ 0.561 6337.621\r
+ 0.745 6221.865\r
+ 0.985 6221.865\r
+ 1.18 6192.926\r
+ 1.455 6308.682\r
+ 1.753 6366.559\r
+ 1.994 6337.621\r
+ 2.269 6395.498\r
+ 2.509 6308.682\r
+ 2.83 6192.926\r
+ 3.14 6048.232\r
+ 3.426 5874.598\r
+ 3.69 5729.904\r
+ 3.965 5585.209\r
+ 4.263 5382.637\r
+ 4.572 5295.82\r
+ 4.939 5180.064\r
+ 5.053 5035.37\r
+ 5.11 4717.042\r
+ 5.133 4225.08\r
+ 5.145 3675.241\r
+ 5.156 3038.585\r
+ 5.179 2344.051\r
+ 5.214 1475.884\r
+ 5.259 607.717\r
+ 5.294 57.878\r
+ 5.295 0.0\r
--- /dev/null
+; Pro 150 O8000 White Thunder\r
+O8000 150 957 P 18.61 32.672000000000004 CTI\r
+ 0.045 3964.63\r
+ 0.046 6742.765\r
+ 0.047 8623.794\r
+ 0.125 7929.26\r
+ 0.239 8160.772\r
+ 0.364 8392.283\r
+ 0.489 8508.039\r
+ 0.614 8536.977\r
+ 0.773 8392.283\r
+ 0.989 8421.222\r
+ 1.273 8479.1\r
+ 1.602 8623.794\r
+ 2.011 8565.916\r
+ 2.33 8565.916\r
+ 2.682 8479.1\r
+ 3.102 8276.527\r
+ 3.568 8045.016\r
+ 3.886 7900.322\r
+ 4.239 7668.81\r
+ 4.591 7524.116\r
+ 4.739 7524.116\r
+ 4.909 7263.666\r
+ 4.955 7003.215\r
+ 4.977 6540.193\r
+ 4.989 5845.659\r
+ 5.0 5006.431\r
+ 5.023 4051.447\r
+ 5.034 3067.524\r
+ 5.045 1996.785\r
+ 5.08 1012.862\r
+ 5.114 318.328\r
+ 5.17 0.0\r
--- /dev/null
+;\r
+G100 38 406 0 0.093 0.511 Contrail_Rockets \r
+0 182.756\r
+0.199105 177.584\r
+0.606264 132.757\r
+0.986577 53.4476\r
+1.43 0\r
--- /dev/null
+;\r
+;\r
+G123 38 406 0 0.083 0.511 Contrail_Rockets \r
+0.00223714 217.239\r
+0.00671141 399.995\r
+0.0201342 220.687\r
+0.914989 72.4129\r
+0.955257 37.9306\r
+1.15 0\r
--- /dev/null
+;\r
+;\r
+G130 38 406 0 0.093 0.516 Contrail_Rockets \r
+0 662.061\r
+0.0145414 448.27\r
+0.0234899 241.376\r
+0.642058 41.3788\r
+0.86 0\r
--- /dev/null
+;\r
+;G-234-HP Reload\r
+;38mm/16 Inch Hardware\r
+;Fast Nozzle\r
+G234 38 406.4 0 0.498 0.544 Contrail_Rockets \r
+0.00169492 245.419\r
+0.0973154 540.63\r
+0.183445 526.943\r
+0.202461 191.616\r
+0.237136 143.712\r
+0.260626 136.868\r
+0.533 0\r
--- /dev/null
+;\r
+;G-300 PVC Motor for 38mm/16 Inch Case.\r
+;Motor uses Fast Nozzle\r
+;90cc's of Nitrous Oxide Used\r
+G300 38 406.4 0 0.023 0.544 Contrail_Rockets \r
+0.00111857 602.221\r
+0.0497763 814.367\r
+0.100671 670.655\r
+0.114094 266.893\r
+0.158837 239.52\r
+0.25 0\r
--- /dev/null
+;\r
+;\r
+H121 38 516 0 0.11 0.612 Contrail_Rockets \r
+0.00223714 251.721\r
+0.0402685 265.514\r
+0.0738255 203.446\r
+0.400447 179.308\r
+0.60179 134.481\r
+1.08949 127.585\r
+1.40268 93.1023\r
+1.61969 37.9306\r
+1.85 0\r
--- /dev/null
+;\r
+;\r
+H141 38 516 0 0.125 0.612 Contrail_Rockets \r
+0.00223714 265.514\r
+0.111857 262.066\r
+1.20134 106.895\r
+1.25951 55.1717\r
+1.3557 27.5859\r
+1.7 0\r
--- /dev/null
+;\r
+;\r
+H211 38 516 0 0.125 0.612 Contrail_Rockets \r
+0.00111857 531.028\r
+0.0190157 634.475\r
+0.0223714 593.096\r
+0.033557 544.821\r
+0.296421 317.238\r
+0.313199 186.205\r
+0.743848 96.5506\r
+0.97 0\r
--- /dev/null
+;\r
+;H-222-HP Reload\r
+;38mm/16 inch Case Used\r
+;Medium Nozzle Used For Reload\r
+;140cc of Nitrous Oxide Used\r
+H222 38 406.4 0 0.022 0.52 Contrail_Rockets \r
+0 684.342\r
+0.0302013 656.968\r
+0.0525727 574.847\r
+0.0581655 349.014\r
+0.346756 260.05\r
+0.364653 191.616\r
+0.7 0\r
--- /dev/null
+;\r
+;H-246 HP Reload\r
+;38mm/20 Inch Case Used\r
+;Medium Nozzle Used\r
+;185cc Nitrous Oxide Used\r
+H246 38 508 0 0.022 0.598 Contrail_Rockets \r
+0.00111857 609.064\r
+0.0123043 499.57\r
+0.502237 253.206\r
+0.514541 157.399\r
+0.9 0\r
--- /dev/null
+;\r
+;\r
+H277 38 719 0 0.11 0.71 Contrail_Rockets \r
+0 765.508\r
+0.0738255 703.44\r
+0.118568 337.927\r
+0.917226 179.308\r
+0.957494 75.8612\r
+0.995526 41.3788\r
+1.02908 48.2753\r
+1.15 0\r
--- /dev/null
+;\r
+;\r
+H300 38 516 0 0.11 0.612 Contrail_Rockets \r
+0 558.614\r
+0.115213 717.233\r
+0.12528 268.962\r
+0.214765 248.273\r
+0.286353 241.376\r
+0.334452 227.583\r
+0.62 0\r
--- /dev/null
+;\r
+;H-303-PVC Hybrid Motor\r
+;Uses Fast Nozzle\r
+;38mm/20 Inch Hardware\r
+;Uses 185cc Nitrous Oxide\r
+H303 38 508 0 0.023 0.589 Contrail_Rockets \r
+0 663.812\r
+0.0447427 780.15\r
+0.108501 704.872\r
+0.111857 342.171\r
+0.176734 328.484\r
+0.196868 307.954\r
+0.6 0\r
--- /dev/null
+;\r
+;\r
+H340 38 711.2 0 0.024 0.816 Contrail_Rockets \r
+0 920.322\r
+0.0847458 715.806\r
+0.101695 345.121\r
+0.683051 332.338\r
+0.740678 255.645\r
+0.766102 153.387\r
+0.95 0\r
--- /dev/null
+;\r
+;\r
+I155 38 711.2 0 0.045 0.725 Contrail_Rockets \r
+0.0111857 222.411\r
+2.71253 150.555\r
+2.82998 82.121\r
+2.96421 58.1691\r
+3.5 0\r
--- /dev/null
+;\r
+;\r
+I210 38 922 0 0.125 0.87 Contrail_Rockets \r
+0 468.96\r
+0.464206 386.202\r
+0.497763 206.894\r
+2.25391 110.343\r
+2.34899 41.3788\r
+2.40492 13.7929\r
+2.72 0\r
--- /dev/null
+;\r
+;\r
+I221 38 719 0 0.125 0.71 Contrail_Rockets \r
+0 482.753\r
+0.503356 358.616\r
+0.519016 179.308\r
+1.49217 103.447\r
+1.53691 27.5859\r
+1.74 0\r
--- /dev/null
+;\r
+;\r
+I290 38 914.4 0 0.068 0.884 Contrail_Rockets \r
+0 521.516\r
+0.0847458 337.451\r
+0.138983 357.903\r
+0.19661 398.806\r
+0.308475 490.838\r
+0.40339 449.935\r
+0.589831 357.903\r
+0.762712 419.258\r
+0.932203 265.871\r
+1.08814 163.613\r
+1.24068 81.8064\r
+1.5 0\r
--- /dev/null
+;\r
+;\r
+I307 38 922 0 0.11 0.81 Contrail_Rockets \r
+0.00223714 551.717\r
+0.199105 717.233\r
+0.210291 386.202\r
+0.756152 620.682\r
+0.834452 455.167\r
+0.941834 310.341\r
+1.09172 199.998\r
+1.22371 117.24\r
+1.85 0\r
--- /dev/null
+;\r
+;I-333-PVC Reload\r
+;38mm/36 Inch Hardware\r
+;Uses Fast Nozzle\r
+;460cc Nitrous Oxide\r
+I333 38 914.4 0 0.068 0.929 Contrail_Rockets \r
+0.00894855 855.427\r
+0.0290828 881.09\r
+0.0536913 504.702\r
+0.604027 342.171\r
+0.796421 461.931\r
+1.7 0\r
--- /dev/null
+;\r
+;I-400-HP\r
+;38mm/36 Inch Hardware\r
+;Uses Fast/X-Fast Nozzle\r
+;460cc Nitrous Oxide\r
+I400 38 914.4 0 0.086 0.925 Contrail_Rockets \r
+0.00447427 667.233\r
+0.0782998 898.199\r
+0.116331 598.799\r
+0.297539 521.811\r
+0.420582 410.605\r
+0.559284 487.594\r
+0.738255 367.834\r
+1 0\r
--- /dev/null
+;\r
+;\r
+I500 38 719 0 0.748 0.8 Contrail_Rockets \r
+0.00111857 1155.16\r
+0.0201342 706.888\r
+0.0313199 999.988\r
+0.574944 103.447\r
+0.623043 120.688\r
+0.7 0\r
--- /dev/null
+;\r
+;\r
+I727 38 914.4 0 0.022 0.929 Contrail_Rockets \r
+0.00847458 1278.22\r
+0.0355932 1661.69\r
+0.0983051 1508.31\r
+0.144068 1482.74\r
+0.171186 1175.97\r
+0.218644 1022.58\r
+0.422034 792.499\r
+0.75 0\r
--- /dev/null
+;\r
+;\r
+I747 38 711.2 0 0.068 0.839 Contrail_Rockets \r
+0 1917.34\r
+0.45 0\r
--- /dev/null
+;\r
+;J-150-HP\r
+;38mm/36 Inch\r
+;550cc\r
+;Slow Nozzle\r
+J150 38 914.4 0 0.091 0.839 Contrail_Rockets \r
+0 266.893\r
+2.00224 184.772\r
+2.75727 150.555\r
+3.00895 92.3861\r
+4.1 0\r
--- /dev/null
+;\r
+;J-222-HP Reload\r
+;Medium Nozzle\r
+;38mm/48 Inch Hardware\r
+;830cc\r
+J222 38 1219.2 0 0.091 1.043 Contrail_Rockets \r
+0.00559284 547.473\r
+0.167785 355.858\r
+2.86353 191.616\r
+2.95861 143.712\r
+3.08725 130.025\r
+3.46756 95.8079\r
+4.3 0\r
--- /dev/null
+;\r
+;J-234-BG Reload\r
+;Slow Nozzle\r
+;54mm/36 Inch Hardware\r
+J234 54 914.4 0 0.177 1.764 Contrail_Rockets \r
+0.00559284 229.255\r
+0.503356 349.014\r
+3.47875 208.724\r
+3.62416 116.338\r
+3.75839 78.6993\r
+4.3 0\r
--- /dev/null
+;\r
+;\r
+J242 38 1227 0 0.11 1.065 Contrail_Rockets \r
+0.0111857 448.27\r
+1.73937 268.962\r
+1.76174 165.515\r
+2.97539 48.2753\r
+3.1 0\r
--- /dev/null
+;\r
+;J-245-BG Reload\r
+;Slow Nozzle\r
+;54mm/28 Inch Hardware\r
+J245 54 711.2 0 0.1 1.55 Contrail_Rockets \r
+0 444.822\r
+0.139821 355.858\r
+1.05145 307.954\r
+2.06376 184.772\r
+2.15884 102.651\r
+2.28188 68.4342\r
+2.62 0\r
--- /dev/null
+;\r
+;J-246-HP Reload\r
+;38mm/36 Inch Hardware\r
+;550cc\r
+;Medium Nozzle\r
+J246 38 914.4 0 0.068 0.861 Contrail_Rockets \r
+0.0167785 492.726\r
+0.0279642 328.484\r
+0.134228 526.943\r
+0.341163 403.762\r
+0.520134 349.014\r
+2.00224 191.616\r
+2.12528 116.338\r
+2.8 0\r
--- /dev/null
+;\r
+;\r
+J272 54 914.4 0 0.114 1.746 Contrail_Rockets \r
+0.00847458 398.806\r
+0.169492 572.645\r
+0.533898 460.161\r
+0.872881 388.58\r
+1.05932 357.903\r
+2.91525 204.516\r
+3.19492 71.5806\r
+3.51695 40.9032\r
+3.63559 51.129\r
+3.86 0\r
--- /dev/null
+;\r
+;\r
+J292 54 711.2 0 0.136 1.542 Contrail_Rockets \r
+0.00847458 552.193\r
+0.262712 480.612\r
+0.423729 419.258\r
+0.762712 337.451\r
+1.97458 245.419\r
+2.07627 143.161\r
+2.53 0\r
--- /dev/null
+;\r
+;\r
+J333 38 1227 0 0.11 1.064 Contrail_Rockets \r
+0 717.233\r
+0.204139 799.99\r
+0.752237 448.27\r
+0.763423 268.962\r
+2.16443 62.0682\r
+2.23714 27.5859\r
+2.4 0\r
--- /dev/null
+;\r
+;J-345-PVC\r
+;38mm/48 Inch Hardware\r
+;735cc\r
+;Fast Nozzle\r
+J345 38 1219.2 0 0.098 1.118 Contrail_Rockets \r
+0.00559284 881.09\r
+0.0782998 667.233\r
+1.21924 376.388\r
+1.26398 359.279\r
+2.7 0\r
--- /dev/null
+;\r
+;\r
+J355 54 711.2 0 0.09 1.564 Contrail_Rockets \r
+0 562.419\r
+0.176271 501.064\r
+0.2 286.322\r
+0.433898 286.322\r
+0.688136 337.451\r
+0.701695 501.064\r
+0.80678 490.838\r
+1.00339 521.516\r
+1.21695 419.258\r
+1.31186 429.484\r
+1.37627 460.161\r
+1.49831 429.484\r
+1.54576 224.968\r
+1.64068 122.71\r
+1.91 0\r
--- /dev/null
+;\r
+;\r
+J358 54 914.4 0 0.111 1.743 Contrail_Rockets \r
+0.00847458 726.032\r
+0.0932203 726.032\r
+0.110169 501.064\r
+0.483051 480.612\r
+0.550847 398.806\r
+2.23729 286.322\r
+2.32203 153.387\r
+2.44915 112.484\r
+2.69 0\r
--- /dev/null
+;\r
+;\r
+J416 54 914.4 0 0.158 1.7 Contrail_Rockets \r
+0 787.386\r
+0.0762712 777.161\r
+0.211864 572.645\r
+0.432203 531.741\r
+0.864407 511.29\r
+1.26271 480.612\r
+1.82203 470.387\r
+2.00847 347.677\r
+2.13559 276.097\r
+2.24576 184.064\r
+2.40678 81.8064\r
+2.75 0\r
--- /dev/null
+;\r
+;\r
+J555 38 1227 0 0.166 1.132 Contrail_Rockets \r
+0 931.023\r
+0.0581655 1344.81\r
+0.277405 810.335\r
+1.17226 241.376\r
+1.2774 68.9647\r
+1.31767 51.7235\r
+1.6 0\r
--- /dev/null
+;\r
+;\r
+J642 54 914.4 0 0.159 1.791 Contrail_Rockets \r
+0.00677966 1482.74\r
+0.0779661 997.015\r
+0.471186 1303.79\r
+0.542373 818.064\r
+0.633898 741.37\r
+0.742373 587.983\r
+1.25085 485.725\r
+1.29831 332.338\r
+1.39661 178.951\r
+1.47458 51.129\r
+1.72 0\r
--- /dev/null
+;\r
+;J-800-HP\r
+;38mm/48 Inch\r
+;685cc\r
+;XXF Nozzle (Short Nozzle)\r
+J800 38 1219.2 0 0.105 1.148 Contrail_Rockets \r
+0.00223714 1830.61\r
+0.52349 889.644\r
+0.639821 650.125\r
+0.740492 444.822\r
+0.823266 273.737\r
+0.90604 153.977\r
+0.997763 136.868\r
+1.2 0\r
--- /dev/null
+;\r
+;K-234-BG Reload\r
+;Slow Nozzle\r
+;54mm/48 Inch Hardware\r
+K234 54 1219.2 0 0.385 2.063 Contrail_Rockets \r
+0 92.3861\r
+0.234899 396.918\r
+0.973154 338.749\r
+5.97315 171.085\r
+6.05145 106.073\r
+6.19687 78.6993\r
+6.37584 54.7473\r
+7.05 0\r
--- /dev/null
+;\r
+;\r
+K265 54 1219.2 0 0.271 2.085 Contrail_Rockets \r
+0 470.387\r
+2.44068 347.677\r
+3.91525 224.968\r
+4.77966 173.839\r
+5.13559 112.484\r
+5.33898 51.129\r
+6.26 0\r
--- /dev/null
+;\r
+;K-300-BS\r
+;75mm/40 Inch Hardware\r
+;2050cc\r
+;Slow Nozzle\r
+K300 75 1016 0 0.181 4.059 Contrail_Rockets \r
+0 431.135\r
+0.324385 526.943\r
+0.98434 479.039\r
+1.1745 369.545\r
+5 280.58\r
+5.19016 171.085\r
+5.35794 102.651\r
+5.6264 54.7473\r
+5.79418 27.3737\r
+6.5 0\r
--- /dev/null
+;\r
+;K-321-BG Reload\r
+;54mm/48 Inch Hardware\r
+;Medium Nozzle\r
+K321 54 1219.2 0 0.183 2.043 Contrail_Rockets \r
+0.00559284 218.989\r
+0.218121 410.605\r
+0.973154 718.559\r
+0.989933 732.246\r
+1.05705 444.822\r
+1.4877 403.762\r
+3.97092 232.676\r
+4.11633 88.9644\r
+4.23378 54.7473\r
+4.34564 54.7473\r
+4.9 0\r
--- /dev/null
+;\r
+;K-404-Sparky\r
+;75mm/40 Inch Hardware\r
+;2050cc\r
+;Slow Nozzle\r
+K404 75 1016 0 0.318 4.15 Contrail_Rockets \r
+0.0111857 670.655\r
+4.63087 335.328\r
+4.80984 205.303\r
+4.9217 130.025\r
+5.0783 82.121\r
+5.26846 41.0605\r
+6.4 0\r
--- /dev/null
+;\r
+;\r
+K456 75 813 0 0.58 3.704 Contrail_Rockets \r
+0.00559284 681.026\r
+0.212528 896.541\r
+0.503356 775.853\r
+1.36465 577.579\r
+1.52685 525.856\r
+2.51119 370.685\r
+2.66779 129.309\r
+3.7 0\r
--- /dev/null
+;\r
+;K-630-Sparky Reload\r
+;75mm/41 Inch Hardare\r
+;1400cc\r
+;Medium Nozzle\r
+K630 75 1041.4 0 0.075 3.55 Contrail_Rockets \r
+0.00559284 307.954\r
+0.0978747 573.136\r
+0.500559 889.644\r
+1.75336 667.233\r
+1.85403 410.605\r
+1.93792 239.52\r
+2.04978 128.314\r
+2.2 0\r
--- /dev/null
+;\r
+;K-678-Sparky\r
+;75mm/40 Inch Hardware\r
+;2050cc\r
+;Medium Nozzle\r
+K678 75 1016 0 0.827 4.05 Contrail_Rockets \r
+0.00559284 1163.38\r
+2.21477 444.822\r
+2.32103 256.628\r
+2.38814 102.651\r
+2.8 0\r
--- /dev/null
+;\r
+;\r
+K707 75 813 0 0.145 3.674 Contrail_Rockets \r
+0.0466102 281.209\r
+0.122881 1278.22\r
+0.165254 894.757\r
+0.495763 1431.61\r
+0.618644 1150.4\r
+0.694915 945.886\r
+0.834746 920.322\r
+1.01271 664.677\r
+1.50847 536.854\r
+1.62288 281.209\r
+1.72881 127.822\r
+2 0\r
--- /dev/null
+;\r
+;\r
+K777 75 1016 0 0.645 4.05 Contrail_Rockets \r
+0 931.023\r
+0.0950783 965.506\r
+0.111857 1793.08\r
+0.167785 1741.36\r
+0.206935 1344.81\r
+0.727069 1137.92\r
+1.00112 810.335\r
+1.97427 413.788\r
+2.04698 172.412\r
+2.6 0\r
--- /dev/null
+;\r
+;Contrail Rockets LLC Hybrid Rocket Motor (L1222)\r
+;75mm-3200cc Motor System\r
+;Sparky Hybrid Fuel\r
+;Data Input By Tom R. Sanders of Contrail Rockets\r
+L1222 75 1339.85 0 3.9 4.989 Contrail_Rockets \r
+0 455\r
+0.25 455\r
+0.5 2725\r
+0.75 1816\r
+2.75 680\r
+3.1 0\r
--- /dev/null
+;\r
+;\r
+L2525 75 1492.25 0 3.5 5.579 Contrail_Rockets \r
+0 4200\r
+0.754759 3294.57\r
+1.9 0\r
--- /dev/null
+;\r
+;L-369-Sparky\r
+;Slow nozzle\r
+;75mm/54 Inch Hardware\r
+;3200cc\r
+L369 75 1371.6 0 0.514 4.8 Contrail_Rockets \r
+0.0223714 540.63\r
+1.45414 533.787\r
+8.92617 260.05\r
+9.08277 130.025\r
+9.28412 68.4342\r
+10.6 0\r
--- /dev/null
+;\r
+;L-800-Sparky\r
+;75mm/54 Inch Hardware\r
+;3200cc\r
+;Medium Nozzle\r
+L800 75 1371.6 0 0.988 4.726 Contrail_Rockets \r
+0.00559284 1351.58\r
+0.167785 1129.16\r
+0.329978 1266.03\r
+0.553691 1248.92\r
+0.665548 1129.16\r
+3.48434 821.21\r
+3.5962 496.148\r
+3.69687 273.737\r
+3.83669 153.977\r
+4.6 0\r
--- /dev/null
+;\r
+;M-1575-Black Gold Reload\r
+;5300cc\r
+;98mm/60 Inch Hardware\r
+M1575 98 1524 0 0.726 10.863 Contrail_Rockets \r
+0.139821 2429.41\r
+0.503356 2976.89\r
+2.95302 1402.9\r
+3.06488 923.861\r
+3.21029 376.388\r
+3.31096 205.303\r
+4.2 0\r
--- /dev/null
+;\r
+;\r
+M2700 98 1524 0 0.412 10.432 Contrail_Rockets \r
+0.00847458 2965.48\r
+0.0508475 3272.26\r
+0.105932 5930.96\r
+0.347458 5828.7\r
+0.504237 6442.25\r
+0.512712 5726.45\r
+0.601695 5112.9\r
+0.745763 3681.29\r
+0.902542 3067.74\r
+1.06356 2454.19\r
+1.18644 1942.9\r
+1.34322 1738.39\r
+1.75 715.806\r
+1.95763 102.258\r
+2.3 0\r
--- /dev/null
+;\r
+;M-2800-Black Gold Reload\r
+;5300cc\r
+;98mm/60 inch Hardware\r
+M2800 98 1524 0 0.476 10.704 Contrail_Rockets \r
+0.00838926 2395.2\r
+0.251678 2805.8\r
+0.545302 3695.45\r
+0.75783 5611.6\r
+0.911633 4311.35\r
+1.1745 3558.58\r
+1.4094 2258.33\r
+1.70861 1505.55\r
+2.3 0\r
--- /dev/null
+;\r
+;Contrail Rockets LLC Hybrid Rocket Motor. (M-711)\r
+;75-3200cc Hardware Set\r
+;Black Smoke Fuel\r
+M711BS 75 1340 0 4.2 4.9 Contrail_Rockets \r
+0 1140\r
+1.46697 1069.77\r
+4 680\r
+6.47256 589.147\r
+6.67413 279.07\r
+7.22 0\r
--- /dev/null
+;\r
+;\r
+O6300 152 1828.8 0 3.175 28.576 Contrail_Rockets \r
+0.0338983 12271\r
+0.728814 9714.51\r
+1.65254 9203.22\r
+2.37288 8947.57\r
+2.51695 6646.77\r
+2.78814 4601.61\r
+2.99153 4857.25\r
+3.27966 3579.03\r
+3.61017 1278.22\r
+4.29 0\r
--- /dev/null
+;\r
+G20 29 149 3 0.0729 0.1179 Ellis_Mountain \r
+0.0463679 46.6843\r
+0.278207 30.3888\r
+0.479134 26.8655\r
+1.00464 24.6634\r
+3.47759 22.0209\r
+4.32767 13.653\r
+5.11592 3.08293\r
+5.47 0\r
--- /dev/null
+;\r
+;Ellis Mountain G35 Single Use Motor\r
+G35EM 29 165 6-10 0.082 0.135 Ellis_Mountain \r
+0.01 51.12\r
+0.04 57.55\r
+0.08 43.78\r
+2.73 28.16\r
+3.28 28.16\r
+3.78 6.73\r
+4 0\r
--- /dev/null
+;\r
+;\r
+G37 24 181 6-10-100 0.068 0.1133 Ellis_Mountain \r
+0.0231839 69.586\r
+0.162287 55.9331\r
+0.332303 48.0056\r
+0.502318 44.9226\r
+0.996909 40.9589\r
+1.49923 38.7568\r
+2.00155 34.3526\r
+2.49614 28.1868\r
+2.75116 18.4976\r
+2.99845 5.28502\r
+3.1 0\r
--- /dev/null
+;\r
+;\r
+H275 29 275 10 0.142 0.255 Ellis_Mountain \r
+0.0123648 792.752\r
+0.015456 356.739\r
+0.197836 312.697\r
+0.797527 268.655\r
+0.911901 255.442\r
+0.992272 123.317\r
+1.04173 39.6376\r
+1.1 0\r
--- /dev/null
+;\r
+;Ellis Mountain Rocket Works\r
+;H48 Single Use motor\r
+H48 38 200 8-100 0.154 0.292 Ellis_Mountain \r
+0.05 101.5\r
+0.1 101.5\r
+0.21 92.18\r
+0.46 86.48\r
+0.74 83.38\r
+1 80\r
+1.49 74.57\r
+1.99 68.36\r
+2.48 63.18\r
+2.99 56.45\r
+3.2 34.18\r
+3.5 18\r
+3.69 13.46\r
+4 11\r
+4.36 7.77\r
+4.4 0\r
--- /dev/null
+;\r
+;Ellis Mountain Rocket Works\r
+;H50 Single Use motor\r
+H50 29 279 6-10 0.163 0.3 Ellis_Mountain \r
+0.01 63.67\r
+0.17 108.9\r
+0.27 94.9\r
+0.47 81.43\r
+0.79 71.02\r
+1.27 64.9\r
+1.97 60.61\r
+2.56 56.94\r
+3.01 52.04\r
+3.52 45.31\r
+3.97 34.9\r
+4.49 18.37\r
+4.97 4.9\r
+5.28 0\r
--- /dev/null
+;\r
+;\r
+I130 38 330 100 0.308 0.625 Ellis_Mountain \r
+0.015456 266.453\r
+0.0540958 160.753\r
+0.502318 169.561\r
+2.23338 180.571\r
+2.48841 149.742\r
+2.99073 136.53\r
+3.49304 77.0732\r
+4.01082 26.4251\r
+4.43 0\r
--- /dev/null
+;\r
+;Ellis Mountain Rocket Works\r
+;I134 38mm Single Use motor\r
+I134 38 355 15 0.2807 0.5812 Ellis_Mountain \r
+0.1 268.8\r
+0.2 138\r
+1 116\r
+2 102\r
+3 85\r
+4 67\r
+4.65 16.46\r
+4.82 6.86\r
+5.07 6.86\r
+5.15 0\r
--- /dev/null
+; Ellis Mountain I150\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I150 38 229 0 0.172032 0.425152 EM\r
+ 0.050 101.298\r
+ 0.152 159.193\r
+ 0.255 169.686\r
+ 0.358 179.603\r
+ 0.460 188.152\r
+ 0.564 193.364\r
+ 0.667 204.520\r
+ 0.769 212.046\r
+ 0.872 212.937\r
+ 0.975 208.076\r
+ 1.077 196.555\r
+ 1.180 191.025\r
+ 1.283 186.106\r
+ 1.386 181.835\r
+ 1.490 177.947\r
+ 1.592 175.877\r
+ 1.695 173.744\r
+ 1.798 170.664\r
+ 1.900 161.823\r
+ 2.003 149.111\r
+ 2.106 124.923\r
+ 2.208 68.392\r
+ 2.311 20.122\r
+ 2.415 7.794\r
+ 2.518 4.464\r
+ 2.621 0.000\r
--- /dev/null
+; Ellis Mountain I160\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I160 38 280 0 0.235648 0.528192 EM\r
+ 0.068 169.405\r
+ 0.206 199.425\r
+ 0.346 205.072\r
+ 0.485 206.075\r
+ 0.624 205.840\r
+ 0.763 204.052\r
+ 0.902 200.850\r
+ 1.042 200.885\r
+ 1.180 203.053\r
+ 1.319 204.157\r
+ 1.458 206.392\r
+ 1.598 210.051\r
+ 1.736 212.769\r
+ 1.875 211.177\r
+ 2.015 207.500\r
+ 2.154 189.766\r
+ 2.293 136.149\r
+ 2.431 52.306\r
+ 2.571 42.841\r
+ 2.710 41.803\r
+ 2.849 33.042\r
+ 2.987 24.614\r
+ 3.127 17.154\r
+ 3.267 7.477\r
+ 3.406 1.777\r
+ 3.546 0.000\r
--- /dev/null
+; Ellis Mountain I230\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I230 38 331 0 0.282688 0.620928 EM\r
+ 0.058 292.627\r
+ 0.178 317.660\r
+ 0.298 309.874\r
+ 0.418 305.243\r
+ 0.537 299.679\r
+ 0.657 298.170\r
+ 0.777 294.591\r
+ 0.897 293.800\r
+ 1.018 289.736\r
+ 1.138 288.222\r
+ 1.257 284.614\r
+ 1.377 281.149\r
+ 1.497 274.879\r
+ 1.617 269.775\r
+ 1.736 258.925\r
+ 1.856 242.249\r
+ 1.976 207.607\r
+ 2.097 136.698\r
+ 2.217 86.506\r
+ 2.336 74.324\r
+ 2.456 51.246\r
+ 2.576 45.546\r
+ 2.696 27.050\r
+ 2.816 6.382\r
+ 2.936 1.423\r
+ 3.057 0.000\r
--- /dev/null
+;\r
+;Ellis Mountain Rocket Works\r
+;I69 38mm Single Use motor\r
+I69 29 406 10 0.236 0.4 Ellis_Mountain \r
+0.05 78.67\r
+0.1 149.7\r
+0.25 133.5\r
+0.49 111.51\r
+0.75 100\r
+1.07 93.18\r
+1.48 87.83\r
+2 82.49\r
+2.5 78\r
+2.99 73.32\r
+3.5 64.5\r
+3.99 48.88\r
+4.5 29.79\r
+4.99 9.17\r
+5.28 0\r
--- /dev/null
+;\r
+;\r
+J110 54 276.2 100 0.45359 0.8754 Ellis_Mountain \r
+0.108192 193.784\r
+0.386399 147.54\r
+1.00464 139.833\r
+4.034 116.711\r
+5.00773 94.6899\r
+6.01236 67.1637\r
+6.53787 37.4355\r
+6.8 0\r
--- /dev/null
+;\r
+;\r
+J148 54 355.6 14 0.67 1.179 Ellis_Mountain \r
+0.139104 218.007\r
+0.231839 183.875\r
+0.448223 171.763\r
+1.00464 170.662\r
+2.10201 170.662\r
+5.02318 147.54\r
+5.31685 133.226\r
+5.67233 49.547\r
+6.1 0\r
--- /dev/null
+;\r
+;\r
+J228 38 562 6 0.27 0.8391 Ellis_Mountain \r
+0.0309119 665.031\r
+0.0927357 444.822\r
+0.262751 356.739\r
+0.664606 343.526\r
+0.989181 317.101\r
+1.96291 259.847\r
+2.99845 193.784\r
+4.01855 118.913\r
+4.99227 35.2334\r
+5.2 0\r
--- /dev/null
+; Ellis Mountain J270\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J270 38 384 0 0.341824 0.711872 EM\r
+ 0.057 357.607\r
+ 0.175 386.516\r
+ 0.294 368.069\r
+ 0.412 360.627\r
+ 0.530 356.068\r
+ 0.648 353.900\r
+ 0.767 351.910\r
+ 0.885 349.900\r
+ 1.003 348.675\r
+ 1.121 347.552\r
+ 1.240 343.075\r
+ 1.358 338.000\r
+ 1.476 330.566\r
+ 1.594 315.474\r
+ 1.712 293.325\r
+ 1.831 266.102\r
+ 1.949 184.040\r
+ 2.067 131.638\r
+ 2.185 109.171\r
+ 2.304 89.570\r
+ 2.422 74.945\r
+ 2.540 55.700\r
+ 2.658 31.860\r
+ 2.777 17.751\r
+ 2.896 10.109\r
+ 3.015 0.000\r
--- /dev/null
+; Ellis Mountain J330\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J330 38 433 0 0.407232 0.820736 EM\r
+ 0.055 482.013\r
+ 0.169 515.156\r
+ 0.283 509.959\r
+ 0.398 511.485\r
+ 0.512 509.155\r
+ 0.626 503.627\r
+ 0.740 495.461\r
+ 0.854 486.118\r
+ 0.969 477.786\r
+ 1.083 472.073\r
+ 1.197 455.861\r
+ 1.310 433.714\r
+ 1.425 407.542\r
+ 1.540 367.945\r
+ 1.654 271.221\r
+ 1.768 203.711\r
+ 1.881 152.800\r
+ 1.996 106.108\r
+ 2.110 91.404\r
+ 2.225 72.286\r
+ 2.339 63.983\r
+ 2.452 61.809\r
+ 2.567 42.010\r
+ 2.681 16.437\r
+ 2.796 4.496\r
+ 2.910 0.000\r
--- /dev/null
+;\r
+;\r
+K475 54 663.6 14 1.035 2.168 Ellis_Mountain \r
+0.0463679 797.157\r
+0.15456 616.585\r
+0.278207 585.756\r
+0.479134 568.139\r
+2.92117 576.948\r
+3.29212 568.139\r
+4.00309 303.888\r
+4.51314 224.613\r
+5.02318 74.8711\r
+5.5 0\r
--- /dev/null
+; Ellis Mountain L330\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L330 76 381 0 1.46944 2.67008 EM\r
+ 0.194 298.963\r
+ 0.584 378.807\r
+ 0.975 376.204\r
+ 1.366 382.475\r
+ 1.757 391.163\r
+ 2.148 399.442\r
+ 2.539 406.048\r
+ 2.930 407.731\r
+ 3.321 405.666\r
+ 3.711 400.636\r
+ 4.103 393.384\r
+ 4.494 384.520\r
+ 4.884 377.009\r
+ 5.275 368.385\r
+ 5.666 359.041\r
+ 6.057 350.117\r
+ 6.448 341.587\r
+ 6.839 337.109\r
+ 7.230 300.039\r
+ 7.621 194.602\r
+ 8.011 123.445\r
+ 8.403 66.942\r
+ 8.794 32.233\r
+ 9.184 8.248\r
+ 9.576 1.563\r
+ 9.968 0.000\r
--- /dev/null
+; Ellis Mountain L600\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L600 76 584 0 2.4407 4.11981 EM\r
+ 0.186 829.668\r
+ 0.561 773.861\r
+ 0.936 767.837\r
+ 1.313 755.034\r
+ 1.689 736.454\r
+ 2.064 722.717\r
+ 2.440 706.215\r
+ 2.816 688.253\r
+ 3.191 673.457\r
+ 3.567 660.981\r
+ 3.943 648.124\r
+ 4.318 634.689\r
+ 4.694 622.058\r
+ 5.070 607.970\r
+ 5.445 594.926\r
+ 5.821 583.003\r
+ 6.197 573.084\r
+ 6.572 553.530\r
+ 6.948 399.379\r
+ 7.324 270.410\r
+ 7.699 211.401\r
+ 8.075 144.237\r
+ 8.451 74.227\r
+ 8.826 19.378\r
+ 9.202 4.274\r
+ 9.578 0.000\r
--- /dev/null
+; Ellis Mountain M1000\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M1000 76 787 0 3.47514 5.5776 EM\r
+ 0.159 1897.088\r
+ 0.481 1606.200\r
+ 0.803 1441.676\r
+ 1.125 1360.014\r
+ 1.447 1299.506\r
+ 1.769 1259.449\r
+ 2.091 1231.131\r
+ 2.413 1202.529\r
+ 2.735 1179.968\r
+ 3.057 1154.573\r
+ 3.379 1108.815\r
+ 3.701 1075.453\r
+ 4.023 1045.316\r
+ 4.345 1010.304\r
+ 4.667 951.184\r
+ 4.989 860.548\r
+ 5.310 727.369\r
+ 5.633 595.659\r
+ 5.955 518.911\r
+ 6.277 439.902\r
+ 6.599 347.743\r
+ 6.921 239.388\r
+ 7.243 144.608\r
+ 7.565 75.112\r
+ 7.887 33.539\r
+ 8.210 0.000\r
--- /dev/null
+;\r
+;Estes 1/2A3T RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+1/2A3T 13 45 2-4 0.002 0.0066 Estes \r
+0.024 0.501\r
+0.042 1.454\r
+0.064 3.009\r
+0.076 4.062\r
+0.088 4.914\r
+0.093 5.065\r
+0.103 6.068\r
+0.112 6.87\r
+0.117 7.021\r
+0.126 7.62\r
+0.137 7.472\r
+0.146 6.87\r
+0.153 6.118\r
+0.159 5.065\r
+0.166 4.363\r
+0.179 3.66\r
+0.197 2.908\r
+0.222 2.256\r
+0.25 2.156\r
+0.277 2.106\r
+0.294 2.056\r
+0.304 2.156\r
+0.316 1.955\r
+0.326 1.554\r
+0.339 1.053\r
+0.35 0.651\r
+0.36 0\r
--- /dev/null
+;\r
+;Estes 1/2A6 RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+1/2A6 18 70 2 0.0026 0.0138 Estes \r
+0.031 0.404\r
+0.064 1.258\r
+0.096 2.263\r
+0.124 3.467\r
+0.149 4.72\r
+0.172 6.023\r
+0.196 7.027\r
+0.21 7.528\r
+0.225 7.86\r
+0.235 7.482\r
+0.244 6.683\r
+0.254 5.685\r
+0.263 4.487\r
+0.269 4.087\r
+0.279 3.039\r
+0.29 1.79\r
+0.297 1.042\r
+0.306 0.593\r
+0.314 0.344\r
+0.33 0\r
--- /dev/null
+;Estes 1/4A3T RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+1/4A3T 13 45 3 0.00083 0.0061 Estes \r
+0.016 0.243\r
+0.044 1.164\r
+0.08 2.698\r
+0.088 2.851\r
+0.096 3.312\r
+0.105 3.804\r
+0.116 4.325\r
+0.129 4.754\r
+0.131 4.754\r
+0.135 4.95\r
+0.139 4.815\r
+0.143 4.814\r
+0.149 4.66\r
+0.157 4.289\r
+0.173 3.548\r
+0.187 2.808\r
+0.194 2.592\r
+0.197 2.13\r
+0.202 1.913\r
+0.206 1.512\r
+0.213 1.389\r
+0.218 1.112\r
+0.227 0.802\r
+0.236 0.493\r
+0.241 0.277\r
+0.25 0\r
--- /dev/null
+;\r
+;Estes A10T RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+A10T 13 45 3-100 0.0038 0.00525 Estes \r
+0.026 0.478\r
+0.055 1.919\r
+0.093 4.513\r
+0.124 8.165\r
+0.146 10.956\r
+0.166 12.64\r
+0.179 11.046\r
+0.194 7.966\r
+0.203 6.042\r
+0.209 3.154\r
+0.225 1.421\r
+0.26 1.225\r
+0.333 1.41\r
+0.456 1.206\r
+0.575 1.195\r
+0.663 1.282\r
+0.76 1.273\r
+0.811 1.268\r
+0.828 0.689\r
+0.85 0\r
--- /dev/null
+;\r
+;Estes A3T RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+A3T 13 45 4 0.0033 0.0085 Estes \r
+0.024 0.195\r
+0.048 0.899\r
+0.086 2.658\r
+0.11 4.183\r
+0.14 5.83\r
+0.159 5.395\r
+0.18 4.301\r
+0.199 3.635\r
+0.215 2.736\r
+0.234 2.267\r
+0.258 2.15\r
+0.315 2.072\r
+0.441 1.993\r
+0.554 2.033\r
+0.605 2.072\r
+0.673 1.954\r
+0.764 1.954\r
+0.874 2.072\r
+0.931 2.15\r
+0.953 2.072\r
+0.966 1.719\r
+0.977 1.173\r
+0.993 0.547\r
+1.01 0\r
--- /dev/null
+;\r
+;Estes A8 RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+A8 18 70 3-5 0.0033 0.01635 Estes \r
+0.041 0.512\r
+0.084 2.115\r
+0.127 4.358\r
+0.166 6.794\r
+0.192 8.588\r
+0.206 9.294\r
+0.226 9.73\r
+0.236 8.845\r
+0.247 7.179\r
+0.261 5.063\r
+0.277 3.717\r
+0.306 3.205\r
+0.351 2.884\r
+0.405 2.499\r
+0.467 2.371\r
+0.532 2.307\r
+0.589 2.371\r
+0.632 2.371\r
+0.652 2.243\r
+0.668 1.794\r
+0.684 1.153\r
+0.703 0.448\r
+0.73 0\r
--- /dev/null
+;\r
+;Estes B4 RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+B4 18 70 2-4 0.006 0.0189 Estes \r
+0.02 0.418\r
+0.04 1.673\r
+0.065 4.076\r
+0.085 6.69\r
+0.105 9.304\r
+0.119 11.496\r
+0.136 12.75\r
+0.153 11.916\r
+0.173 10.666\r
+0.187 9.304\r
+0.198 7.214\r
+0.207 5.645\r
+0.226 4.809\r
+0.258 4.182\r
+0.326 3.763\r
+0.422 3.554\r
+0.549 3.345\r
+0.665 3.345\r
+0.776 3.345\r
+0.863 3.345\r
+0.94 3.449\r
+0.991 3.449\r
+1.002 2.404\r
+1.01 1.254\r
+1.03 0\r
--- /dev/null
+; Estes B6-0 from NAR data by Mark Koelsch\r
+B6-0 18 70 0 0.0056 0.0156 E\r
+ 0.036 1.364\r
+ 0.064 2.727\r
+ 0.082 4.215\r
+ 0.111 6.694\r
+ 0.135 9.05\r
+ 0.146 9.545\r
+ 0.172 11.901\r
+ 0.181 12.149\r
+ 0.191 11.901\r
+ 0.211 9.174\r
+ 0.239 7.314\r
+ 0.264 6.074\r
+ 0.275 5.95\r
+ 0.333 5.207\r
+ 0.394 4.835\r
+ 0.445 4.835\r
+ 0.556 4.339\r
+ 0.667 4.587\r
+ 0.723 4.339\r
+ 0.78 4.339\r
+ 0.793 4.091\r
+ 0.812 2.603\r
+ 0.833 1.24\r
+ 0.857 0.0\r
--- /dev/null
+;\r
+;ESTES C11 RASP.ENG file made from NAR published data\r
+;File produced JANUARY 1, 2002\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+C11 24 70 0-3-5-7 0.012 0.0353 Estes \r
+0.034 1.692\r
+0.066 3.782\r
+0.107 7.566\r
+0.145 10.946\r
+0.183 14.832\r
+0.214 17.618\r
+0.226 18.213\r
+0.256 20.107\r
+0.281 21.208\r
+0.298 21.73\r
+0.306 20.206\r
+0.323 17.321\r
+0.337 14.931\r
+0.358 13.236\r
+0.385 11.947\r
+0.413 11.65\r
+0.468 10.946\r
+0.539 10.45\r
+0.619 10.648\r
+0.683 10.648\r
+0.715 10.648\r
+0.726 10.053\r
+0.74 8.163\r
+0.758 5.773\r
+0.778 3.185\r
+0.795 1.394\r
+0.81 0\r
--- /dev/null
+;\r
+;Estes C5 RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+C5 18 70 3 0.0113 0.0248 Estes \r
+0.042 2.195\r
+0.107 9.118\r
+0.159 16.213\r
+0.21 21.85\r
+0.233 18.407\r
+0.27 13.677\r
+0.289 9.793\r
+0.303 7.092\r
+0.326 5.065\r
+0.401 4.39\r
+0.55 3.883\r
+0.802 3.714\r
+1.026 3.883\r
+1.291 3.883\r
+1.524 4.221\r
+1.683 4.221\r
+1.702 2.195\r
+1.73 0\r
--- /dev/null
+;\r
+;Estes C6 RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+C6 18 70 0-3-5-7 0.0108 0.0231 Estes \r
+0.031 0.946\r
+0.092 4.826\r
+0.139 9.936\r
+0.192 14.09\r
+0.209 11.446\r
+0.231 7.381\r
+0.248 6.151\r
+0.292 5.489\r
+0.37 4.921\r
+0.475 4.448\r
+0.671 4.258\r
+0.702 4.542\r
+0.723 4.164\r
+0.85 4.448\r
+1.063 4.353\r
+1.211 4.353\r
+1.242 4.069\r
+1.303 4.258\r
+1.468 4.353\r
+1.656 4.448\r
+1.821 4.448\r
+1.834 2.933\r
+1.847 1.325\r
+1.86 0\r
--- /dev/null
+;\r
+;Estes D11 RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+D11 24 70 100 0.0245 0.0448 Estes \r
+0.033 2.393\r
+0.084 5.783\r
+0.144 12.17\r
+0.214 20.757\r
+0.261 24.35\r
+0.289 26.01\r
+0.311 23.334\r
+0.325 18.532\r
+0.338 14.536\r
+0.356 12.331\r
+0.398 10.72\r
+0.48 9.303\r
+0.618 8.676\r
+0.761 8.247\r
+0.955 8.209\r
+1.222 7.955\r
+1.402 8.319\r
+1.54 8.291\r
+1.701 8.459\r
+1.784 8.442\r
+1.803 6.239\r
+1.834 3.033\r
+1.86 0\r
--- /dev/null
+;\r
+;Estes D12 RASP.ENG file made from NAR published data\r
+;File produced October 3, 2000\r
+;The total impulse, peak thrust, average thrust and burn time are\r
+;the same as the averaged static test data on the NAR web site in\r
+;the certification file. The curve drawn with these data points is as\r
+;close to the certification curve as can be with such a limited\r
+;number of points (32) allowed with wRASP up to v1.6.\r
+D12 24 70 0-3-5-7 0.0211 0.0426 Estes \r
+0.049 2.569\r
+0.116 9.369\r
+0.184 17.275\r
+0.237 24.258\r
+0.282 29.73\r
+0.297 27.01\r
+0.311 22.589\r
+0.322 17.99\r
+0.348 14.126\r
+0.386 12.099\r
+0.442 10.808\r
+0.546 9.876\r
+0.718 9.306\r
+0.879 9.105\r
+1.066 8.901\r
+1.257 8.698\r
+1.436 8.31\r
+1.59 8.294\r
+1.612 4.613\r
+1.65 0\r
--- /dev/null
+; Estes E9-0 by Mark Koelsch from NAR data\r
+E9-0 24 95 0 0.0358 0.056799999999999996 E\r
+ 0.046 1.913\r
+ 0.235 16.696\r
+ 0.273 18.435\r
+ 0.326 14.957\r
+ 0.38 12.174\r
+ 0.44 10.435\r
+ 0.835 9.043\r
+ 1.093 8.87\r
+ 1.496 8.696\r
+ 1.997 8.696\r
+ 2.498 8.696\r
+ 3.014 9.217\r
+ 3.037 5.043\r
+ 3.067 1.217\r
+ 3.09 0.0\r
--- /dev/null
+; False data to test 1/2/4A-motors
+1/2A3 11 58 0-3-5-7 0.003 0.0067 Apogee
+0.014 0.241
+0.036 0.895
+0.064 2.618
+0.1 4.82
+0.111 4.133
+0.125 2.687
+0.139 2.307
+; More false data
+1/4A5 11 58 0-3-5-7 0.003 0.0067 Apogee
+0.014 0.241
+0.036 0.895
+0.064 2.618
+0.1 4.82
+0.111 4.133
+0.125 2.687
+0.139 2.307
--- /dev/null
+;The K555GT "Green Tornado" motor is a green flame, low smoke propellant.\r
+;This reload produces a 9% "K" motor with 1397 N-seconds of total impulse,\r
+;maximum thrust of 645.3 Newtons, and an average thrust of 556 Newtons,\r
+;for a 2.51 second burn time.\r
+K555GT 54 430 1000 0.78 1.52 Gorilla_Motors \r
+0.025 267\r
+0.05 338.2\r
+0.1 471.7\r
+0.12 498.4\r
+0.15 511.75\r
+0.18 522.875\r
+0.2 534\r
+0.7 631.9\r
+0.75 636.35\r
+0.9 645.25\r
+1.15 636.35\r
+1.57 623\r
+1.87 614.1\r
+2.17 600.75\r
+2.25 578.5\r
+2.27 480.6\r
+2.3 356\r
+2.35 178\r
+2.45 66.75\r
+2.51 0\r
+;\r
--- /dev/null
+;hand entered from Cesaroni (Mike Dennett) curve data\r
+;Andrew MacMillen NAR 77472 2/5/02\r
+;NOTE: NOT CTI OR TMT APPROVED\r
+;Hypertek 300CC098J\r
+I130 54 521 100 0.298 1.049 HyperTek \r
+0.05 200\r
+0.1 223\r
+0.5 205\r
+1 187\r
+1.5 169\r
+2 151\r
+2.25 143\r
+2.4 89\r
+2.5 71\r
+3 40\r
+3.5 18\r
+4 0\r
--- /dev/null
+;\r
+;Hypertek I136 Data entered by Tim Van Milligan\r
+;For RockSim www.RockSim.com\r
+;File Created March 2, 2005\r
+;Data from Mike Dennett at Hypertek\r
+I136 54 546 100 0.283 1.001 Hypertek \r
+0.155 256.236\r
+0.5 232.756\r
+1 212.85\r
+1.5 196.005\r
+2 174.976\r
+2.21 163.338\r
+2.4 100.912\r
+2.5 83.813\r
+3 42.468\r
+3.5 19.754\r
+3.7 15.262\r
+3.8 0\r
--- /dev/null
+;\r
+;Hypertek I145 Data entered by Tim Van Milligan\r
+;For RockSim www.RockSim.com\r
+;File Created March 2, 2005\r
+;Data from Tripoli Certification - test date 9/8/01\r
+;Not endorsed by TRA or Hypertek\r
+I145 54 546 100 0.311 1.068 Hypertek \r
+0.057 256.195\r
+0.204 253.38\r
+0.497 236.488\r
+1.002 208.334\r
+1.205 199.888\r
+1.376 211.15\r
+1.482 197.073\r
+2.003 177.366\r
+2.125 168.92\r
+2.5 90.091\r
+2.997 45.045\r
+3.282 16.892\r
+3.7 0\r
--- /dev/null
+;\r
+;hand entered from Cesaroni (Mike Dennett) curve data\r
+;Andrew MacMillen NAR 77472 2/5/02\r
+;NOTE: NOT CTI OR TMT APPROVED\r
+;Hypertek 300CC125J\r
+I205 54 521 100 0.298 1.049 HyperTek \r
+0.05 312\r
+0.1 347\r
+0.5 312\r
+1 258\r
+1.35 223\r
+1.6 125\r
+1.75 80\r
+2 45\r
+2.25 22\r
+2.75 0\r
--- /dev/null
+;\r
+;Hypertek I222 Data entered by Tim Van Milligan\r
+;For RockSim www.RockSim.com\r
+;File Created March 2, 2005\r
+;Data from Mike Dennett at Hypertek\r
+I222 54 546 100 0.28 1.013 Hypertek \r
+0.037 394.146\r
+0.065 439.192\r
+0.12 450.547\r
+0.24 436.734\r
+0.5 411.158\r
+0.66 392.487\r
+1 338.349\r
+1.348 292.639\r
+1.432 259.337\r
+1.5 193.668\r
+1.67 117.056\r
+2 57.357\r
+2.3 22.358\r
+2.4 0\r
--- /dev/null
+;\r
+;Hypertek I225 Data entered by Tim Van Milligan\r
+;For RockSim www.RockSim.com\r
+;File Created March 2, 2005\r
+;Data from Tripoli Certification - test date 9/8/01\r
+;Not endorsed by TRA or Hypertek\r
+I225 54 546 100 0.298 1.067 Hypertek \r
+0.012 309.686\r
+0.037 343.47\r
+0.106 351.916\r
+0.244 354.732\r
+0.497 337.84\r
+0.749 320.948\r
+0.998 298.425\r
+1.254 273.087\r
+1.433 239.303\r
+1.502 194.258\r
+1.755 101.352\r
+1.999 53.491\r
+2.211 11.261\r
+2.37 0\r
--- /dev/null
+;\r
+;Hypertek I260 Data entered by Tim Van Milligan\r
+;For RockSim www.RockSim.com\r
+;File Created March 2, 2005\r
+;Data from Mike Dennett at Hypertek\r
+I260 54 614 100 0.409 1.296 Hypertek \r
+0.03 339.01\r
+0.041 425.115\r
+0.12 413.854\r
+0.216 394.146\r
+0.354 391.331\r
+0.497 368.808\r
+0.749 346.286\r
+1.002 306.871\r
+1.36 264.641\r
+1.454 228.042\r
+1.502 180.181\r
+1.686 109.798\r
+2.003 50.676\r
+2.2 27.483\r
+2.3 0\r
--- /dev/null
+; HyperTek I310\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I310 54 645 0 0.40096 1.30502 HT\r
+ 0.042 465.886\r
+ 0.128 450.815\r
+ 0.216 438.421\r
+ 0.303 443.241\r
+ 0.391 433.808\r
+ 0.478 415.992\r
+ 0.566 406.746\r
+ 0.653 380.383\r
+ 0.741 385.170\r
+ 0.828 372.458\r
+ 0.916 358.282\r
+ 1.003 348.621\r
+ 1.091 337.887\r
+ 1.178 333.898\r
+ 1.266 303.469\r
+ 1.353 301.589\r
+ 1.441 268.788\r
+ 1.528 222.719\r
+ 1.616 155.314\r
+ 1.703 112.163\r
+ 1.791 80.510\r
+ 1.878 58.562\r
+ 1.966 41.774\r
+ 2.053 30.243\r
+ 2.141 21.270\r
+ 2.228 0.000\r
--- /dev/null
+; HyperTek J115 (440CC076J)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J115 54 614 0 0.411264 1.28218 HT\r
+ 0.129 218.303\r
+ 0.391 230.563\r
+ 0.653 216.171\r
+ 0.916 165.676\r
+ 1.178 158.834\r
+ 1.441 161.888\r
+ 1.703 157.955\r
+ 1.966 152.977\r
+ 2.228 148.337\r
+ 2.491 141.919\r
+ 2.753 136.970\r
+ 3.016 129.152\r
+ 3.278 121.815\r
+ 3.541 111.971\r
+ 3.803 79.163\r
+ 4.066 53.433\r
+ 4.328 42.975\r
+ 4.591 38.391\r
+ 4.853 33.418\r
+ 5.116 28.709\r
+ 5.378 23.886\r
+ 5.641 19.658\r
+ 5.903 15.894\r
+ 6.166 11.955\r
+ 6.428 9.151\r
+ 6.691 0.000\r
--- /dev/null
+; HyperTek J120 (440CC076JFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J120 54 614 0 0.442176 1.29338 HT\r
+ 0.134 232.707\r
+ 0.405 264.084\r
+ 0.676 230.699\r
+ 0.948 185.971\r
+ 1.220 174.226\r
+ 1.491 173.853\r
+ 1.763 165.828\r
+ 2.034 158.016\r
+ 2.305 152.389\r
+ 2.577 143.399\r
+ 2.849 135.969\r
+ 3.120 129.537\r
+ 3.392 124.822\r
+ 3.664 118.872\r
+ 3.934 109.922\r
+ 4.206 69.777\r
+ 4.478 47.837\r
+ 4.749 40.178\r
+ 5.021 35.768\r
+ 5.293 31.265\r
+ 5.564 26.359\r
+ 5.835 21.215\r
+ 6.107 17.175\r
+ 6.378 12.931\r
+ 6.650 9.463\r
+ 6.922 0.000\r
--- /dev/null
+; HyperTek J150\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J150 54 645 0 0.428288 1.30592 HT\r
+ 0.111 177.498\r
+ 0.336 193.696\r
+ 0.561 200.136\r
+ 0.786 204.034\r
+ 1.011 200.531\r
+ 1.236 197.233\r
+ 1.461 192.706\r
+ 1.686 189.854\r
+ 1.911 185.892\r
+ 2.136 183.117\r
+ 2.361 179.325\r
+ 2.586 174.178\r
+ 2.813 171.123\r
+ 3.039 164.933\r
+ 3.264 160.032\r
+ 3.489 154.604\r
+ 3.714 148.653\r
+ 3.939 92.092\r
+ 4.164 55.325\r
+ 4.389 42.913\r
+ 4.614 32.903\r
+ 4.839 24.742\r
+ 5.064 16.445\r
+ 5.289 8.527\r
+ 5.515 4.923\r
+ 5.741 0.000\r
--- /dev/null
+; HyperTek J170 (440CC098J)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J170 54 614 0 0.4032 1.28218 HT\r
+ 0.092 315.198\r
+ 0.278 351.486\r
+ 0.466 314.152\r
+ 0.653 255.278\r
+ 0.841 235.396\r
+ 1.027 234.785\r
+ 1.214 230.871\r
+ 1.401 223.051\r
+ 1.589 217.688\r
+ 1.776 209.940\r
+ 1.962 203.806\r
+ 2.149 197.520\r
+ 2.336 191.243\r
+ 2.524 178.598\r
+ 2.711 129.785\r
+ 2.898 82.459\r
+ 3.084 71.693\r
+ 3.272 64.633\r
+ 3.459 54.015\r
+ 3.647 45.022\r
+ 3.833 36.373\r
+ 4.020 28.397\r
+ 4.207 21.518\r
+ 4.395 16.072\r
+ 4.582 11.712\r
+ 4.770 0.000\r
--- /dev/null
+; HyperTek J190 (440CC098JFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J190 54 614 0 0.439488 1.29338 HT\r
+ 0.095 338.583\r
+ 0.287 384.416\r
+ 0.481 341.472\r
+ 0.674 279.175\r
+ 0.867 267.528\r
+ 1.060 256.325\r
+ 1.254 250.108\r
+ 1.447 244.404\r
+ 1.640 238.846\r
+ 1.833 236.505\r
+ 2.026 232.026\r
+ 2.219 223.962\r
+ 2.413 213.236\r
+ 2.606 201.661\r
+ 2.799 150.523\r
+ 2.992 101.327\r
+ 3.185 84.001\r
+ 3.378 73.902\r
+ 3.571 60.222\r
+ 3.765 49.208\r
+ 3.958 39.096\r
+ 4.151 29.873\r
+ 4.344 22.600\r
+ 4.538 16.842\r
+ 4.731 11.964\r
+ 4.925 0.000\r
--- /dev/null
+; HyperTek J220\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J220 54 645 0 0.417984 1.30502 HT\r
+ 0.071 277.975\r
+ 0.215 271.735\r
+ 0.358 289.851\r
+ 0.502 298.227\r
+ 0.647 293.803\r
+ 0.792 290.609\r
+ 0.935 283.211\r
+ 1.079 276.013\r
+ 1.223 271.808\r
+ 1.368 269.774\r
+ 1.513 262.986\r
+ 1.656 257.451\r
+ 1.800 253.286\r
+ 1.944 245.781\r
+ 2.089 239.739\r
+ 2.233 230.852\r
+ 2.377 220.234\r
+ 2.521 159.239\r
+ 2.665 97.180\r
+ 2.809 73.147\r
+ 2.954 58.766\r
+ 3.098 48.973\r
+ 3.242 37.549\r
+ 3.385 27.410\r
+ 3.530 19.267\r
+ 3.675 0.000\r
--- /dev/null
+; HyperTek J250 (440CC125J)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J250 54 614 0 0.40768 1.29248 HT\r
+ 0.064 409.711\r
+ 0.194 453.238\r
+ 0.324 416.509\r
+ 0.454 383.773\r
+ 0.584 359.202\r
+ 0.715 343.963\r
+ 0.845 336.331\r
+ 0.975 328.849\r
+ 1.105 318.614\r
+ 1.235 309.097\r
+ 1.366 306.155\r
+ 1.496 290.597\r
+ 1.626 283.180\r
+ 1.756 261.190\r
+ 1.886 200.168\r
+ 2.017 143.646\r
+ 2.147 126.521\r
+ 2.277 113.229\r
+ 2.407 91.310\r
+ 2.538 71.216\r
+ 2.668 54.183\r
+ 2.798 40.347\r
+ 2.928 29.057\r
+ 3.058 20.139\r
+ 3.190 13.793\r
+ 3.321 0.000\r
--- /dev/null
+; HyperTek J250\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J250 54 645 0 0.404992 1.30637 HT\r
+ 0.055 356.092\r
+ 0.168 316.638\r
+ 0.281 357.597\r
+ 0.395 351.765\r
+ 0.508 354.216\r
+ 0.622 354.162\r
+ 0.735 338.625\r
+ 0.849 332.051\r
+ 0.963 323.651\r
+ 1.076 315.678\r
+ 1.190 305.773\r
+ 1.303 298.769\r
+ 1.417 288.922\r
+ 1.530 293.337\r
+ 1.644 276.552\r
+ 1.757 269.543\r
+ 1.871 223.360\r
+ 1.984 131.511\r
+ 2.098 98.246\r
+ 2.211 76.331\r
+ 2.325 60.095\r
+ 2.439 47.691\r
+ 2.552 36.215\r
+ 2.666 26.693\r
+ 2.779 20.007\r
+ 2.893 0.000\r
--- /dev/null
+; HyperTek J270 (440CC125JFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J270 54 614 0 0.419776 1.29606 HT\r
+ 0.064 438.643\r
+ 0.193 498.603\r
+ 0.322 468.869\r
+ 0.451 438.261\r
+ 0.581 412.686\r
+ 0.711 390.684\r
+ 0.841 376.193\r
+ 0.970 362.205\r
+ 1.100 347.649\r
+ 1.230 333.459\r
+ 1.359 324.401\r
+ 1.489 311.483\r
+ 1.619 298.076\r
+ 1.749 278.397\r
+ 1.878 220.239\r
+ 2.007 150.276\r
+ 2.137 125.603\r
+ 2.268 121.989\r
+ 2.397 91.398\r
+ 2.526 71.671\r
+ 2.656 55.779\r
+ 2.786 41.822\r
+ 2.916 30.460\r
+ 3.045 22.243\r
+ 3.175 16.420\r
+ 3.305 0.000\r
--- /dev/null
+;\r
+;Hypertek J295 Data entered by Tim Van Milligan\r
+;For RockSim www.RockSim.com\r
+;File Created March 2, 2005\r
+;Data from Tripoli Certification - test date 9/8/01\r
+;Not endorsed by TRA or Hypertek\r
+J295 54 614 100 0.409 1.31 Hypertek \r
+0.004 467.345\r
+0.244 461.714\r
+0.501 416.669\r
+1.002 377.254\r
+1.254 343.47\r
+1.364 315.317\r
+1.502 219.596\r
+1.751 112.613\r
+2.003 50.676\r
+2.2 0\r
--- /dev/null
+; HyperTek J317O (835CC172J)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J317O 81 552 0 0.704256 1.7575 HT\r
+ 0.071 438.483\r
+ 0.215 471.024\r
+ 0.358 459.716\r
+ 0.502 447.348\r
+ 0.647 431.653\r
+ 0.792 418.545\r
+ 0.935 407.806\r
+ 1.079 400.212\r
+ 1.223 395.752\r
+ 1.368 382.516\r
+ 1.513 372.890\r
+ 1.656 368.033\r
+ 1.800 349.298\r
+ 1.944 336.071\r
+ 2.089 324.486\r
+ 2.233 301.205\r
+ 2.377 233.601\r
+ 2.521 176.972\r
+ 2.665 132.539\r
+ 2.809 96.229\r
+ 2.954 69.718\r
+ 3.098 49.457\r
+ 3.242 33.983\r
+ 3.385 23.063\r
+ 3.530 16.524\r
+ 3.675 0.000\r
--- /dev/null
+; HyperTek J330O (835CC172JFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J330O 81 552 0 0.727104 1.77722 HT\r
+ 0.068 453.500\r
+ 0.206 485.741\r
+ 0.345 476.873\r
+ 0.483 463.622\r
+ 0.623 439.951\r
+ 0.761 423.260\r
+ 0.900 426.386\r
+ 1.040 415.245\r
+ 1.178 443.371\r
+ 1.317 431.352\r
+ 1.456 407.015\r
+ 1.595 392.143\r
+ 1.733 390.332\r
+ 1.872 360.092\r
+ 2.010 334.240\r
+ 2.150 307.215\r
+ 2.289 225.611\r
+ 2.427 169.224\r
+ 2.567 126.562\r
+ 2.705 93.167\r
+ 2.844 68.293\r
+ 2.983 48.099\r
+ 3.122 32.856\r
+ 3.260 21.857\r
+ 3.400 15.193\r
+ 3.540 0.000\r
--- /dev/null
+; HyperTek J330 (835/54-172-J)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+J330 54 787 0 0.73024 1.59936 HT\r
+ 0.068 453.500\r
+ 0.206 485.741\r
+ 0.345 476.873\r
+ 0.483 463.622\r
+ 0.623 439.951\r
+ 0.761 423.260\r
+ 0.900 426.386\r
+ 1.040 415.245\r
+ 1.178 443.371\r
+ 1.317 431.352\r
+ 1.456 407.015\r
+ 1.595 392.143\r
+ 1.733 390.332\r
+ 1.872 360.092\r
+ 2.010 334.240\r
+ 2.150 307.215\r
+ 2.289 225.611\r
+ 2.427 169.224\r
+ 2.567 126.562\r
+ 2.705 93.167\r
+ 2.844 68.293\r
+ 2.983 48.099\r
+ 3.122 32.856\r
+ 3.260 21.857\r
+ 3.400 15.193\r
+ 3.540 0.000\r
--- /dev/null
+; HyperTek K240\r
+; Copyright Tripoli Motor Testing 1998 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K240 81 552 0 0.789376 1.80723 HT\r
+ 0.131 278.007\r
+ 0.396 329.552\r
+ 0.660 338.024\r
+ 0.925 334.092\r
+ 1.191 326.199\r
+ 1.456 319.745\r
+ 1.721 315.195\r
+ 1.985 311.182\r
+ 2.250 302.916\r
+ 2.516 305.943\r
+ 2.781 289.975\r
+ 3.046 281.781\r
+ 3.310 273.330\r
+ 3.575 268.852\r
+ 3.841 255.702\r
+ 4.106 251.068\r
+ 4.371 234.820\r
+ 4.635 159.972\r
+ 4.900 96.543\r
+ 5.166 73.367\r
+ 5.431 55.477\r
+ 5.696 40.928\r
+ 5.960 29.542\r
+ 6.225 21.250\r
+ 6.491 14.787\r
+ 6.756 0.000\r
--- /dev/null
+; HyperTek L200 (1685CC098L)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L200 111 724 0 1.59398 3.89491 HT\r
+ 0.292 310.935\r
+ 0.877 311.934\r
+ 1.464 284.574\r
+ 2.050 259.236\r
+ 2.636 245.149\r
+ 3.223 240.798\r
+ 3.809 246.021\r
+ 4.396 251.509\r
+ 4.981 255.559\r
+ 5.568 250.045\r
+ 6.154 242.343\r
+ 6.741 236.221\r
+ 7.327 230.527\r
+ 7.914 224.062\r
+ 8.500 218.240\r
+ 9.086 212.215\r
+ 9.673 189.706\r
+ 10.258 94.608\r
+ 10.845 67.128\r
+ 11.431 53.350\r
+ 12.018 41.550\r
+ 12.604 31.112\r
+ 13.191 22.445\r
+ 13.777 16.763\r
+ 14.364 10.892\r
+ 14.950 0.000\r
--- /dev/null
+; HyperTek L225 (1685CC098LFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L225 111 724 0 1.66118 3.94822 HT\r
+ 0.271 380.609\r
+ 0.815 364.078\r
+ 1.359 347.671\r
+ 1.904 311.980\r
+ 2.449 292.842\r
+ 2.994 284.620\r
+ 3.539 284.123\r
+ 4.083 295.754\r
+ 4.628 286.654\r
+ 5.173 271.822\r
+ 5.718 258.225\r
+ 6.263 249.357\r
+ 6.807 240.396\r
+ 7.352 232.666\r
+ 7.897 226.844\r
+ 8.442 217.601\r
+ 8.986 208.639\r
+ 9.531 122.739\r
+ 10.076 74.667\r
+ 10.621 60.930\r
+ 11.166 48.898\r
+ 11.710 38.996\r
+ 12.255 29.719\r
+ 12.800 21.925\r
+ 13.345 16.360\r
+ 13.890 0.000\r
--- /dev/null
+; HyperTek L350 (1685CC125L)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L350 111 724 0 1.6025 3.90342 HT\r
+ 0.188 592.182\r
+ 0.566 598.015\r
+ 0.945 479.774\r
+ 1.324 427.223\r
+ 1.702 406.561\r
+ 2.080 395.128\r
+ 2.459 393.279\r
+ 2.839 410.729\r
+ 3.217 517.335\r
+ 3.595 506.496\r
+ 3.974 439.490\r
+ 4.353 391.732\r
+ 4.731 463.388\r
+ 5.109 446.876\r
+ 5.489 436.237\r
+ 5.868 387.456\r
+ 6.246 247.342\r
+ 6.624 147.845\r
+ 7.003 117.459\r
+ 7.382 93.081\r
+ 7.760 73.034\r
+ 8.139 55.605\r
+ 8.518 40.854\r
+ 8.897 29.575\r
+ 9.276 21.627\r
+ 9.655 0.000\r
--- /dev/null
+; HyperTek L355 (1685CC125LFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L355 111 724 0 1.61952 3.95405 HT\r
+ 0.185 638.681\r
+ 0.559 623.792\r
+ 0.933 538.081\r
+ 1.307 493.173\r
+ 1.682 465.144\r
+ 2.056 432.702\r
+ 2.430 410.644\r
+ 2.804 388.862\r
+ 3.178 395.452\r
+ 3.553 384.851\r
+ 3.927 370.103\r
+ 4.301 356.484\r
+ 4.675 346.195\r
+ 5.049 342.684\r
+ 5.424 325.430\r
+ 5.798 317.798\r
+ 6.172 272.453\r
+ 6.546 156.214\r
+ 6.920 117.579\r
+ 7.295 93.203\r
+ 7.669 73.056\r
+ 8.043 56.997\r
+ 8.417 42.994\r
+ 8.791 30.338\r
+ 9.166 21.725\r
+ 9.541 0.000\r
--- /dev/null
+; HyperTek L475 (1685CC172L)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L475 111 724 0 1.52992 3.89805 HT\r
+ 0.129 640.755\r
+ 0.391 638.069\r
+ 0.652 619.018\r
+ 0.914 609.661\r
+ 1.176 597.937\r
+ 1.437 596.251\r
+ 1.699 594.039\r
+ 1.961 572.245\r
+ 2.223 578.304\r
+ 2.484 589.224\r
+ 2.747 578.752\r
+ 3.008 612.426\r
+ 3.270 646.188\r
+ 3.531 674.617\r
+ 3.793 652.574\r
+ 4.055 555.384\r
+ 4.317 299.749\r
+ 4.578 220.284\r
+ 4.841 170.007\r
+ 5.102 127.011\r
+ 5.364 94.998\r
+ 5.626 70.208\r
+ 5.888 49.458\r
+ 6.149 32.102\r
+ 6.411 18.382\r
+ 6.674 0.000\r
--- /dev/null
+; HyperTek L535 (1685CC172LFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L535 111 724 0 1.59667 3.94822 HT\r
+ 0.119 838.518\r
+ 0.358 768.667\r
+ 0.598 727.551\r
+ 0.838 745.172\r
+ 1.077 712.851\r
+ 1.317 694.613\r
+ 1.556 672.145\r
+ 1.796 656.726\r
+ 2.035 685.070\r
+ 2.275 808.735\r
+ 2.515 798.521\r
+ 2.754 755.277\r
+ 2.995 726.985\r
+ 3.235 699.331\r
+ 3.475 670.049\r
+ 3.715 515.315\r
+ 3.954 293.947\r
+ 4.194 227.905\r
+ 4.433 180.167\r
+ 4.673 140.714\r
+ 4.912 107.564\r
+ 5.152 80.907\r
+ 5.392 58.898\r
+ 5.631 39.782\r
+ 5.872 24.901\r
+ 6.113 0.000\r
--- /dev/null
+; HyperTek L540O (2800CC172L)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L540O 111 876 0 2.5303 5.656 HT\r
+ 0.191 685.548\r
+ 0.574 665.171\r
+ 0.957 635.467\r
+ 1.341 634.122\r
+ 1.725 656.225\r
+ 2.109 706.931\r
+ 2.493 696.526\r
+ 2.876 777.726\r
+ 3.260 775.919\r
+ 3.645 781.611\r
+ 4.028 712.736\r
+ 4.411 695.555\r
+ 4.796 701.251\r
+ 5.180 645.985\r
+ 5.564 607.757\r
+ 5.947 546.408\r
+ 6.331 387.372\r
+ 6.716 234.214\r
+ 7.099 181.086\r
+ 7.482 139.253\r
+ 7.867 106.109\r
+ 8.251 78.293\r
+ 8.634 54.851\r
+ 9.018 36.384\r
+ 9.402 20.375\r
+ 9.786 0.000\r
--- /dev/null
+; HyperTek L540 (2800/75-172-L)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L540 75 1387 0 2.52224 5.05792 HT\r
+ 0.191 685.548\r
+ 0.574 665.171\r
+ 0.957 635.467\r
+ 1.341 634.122\r
+ 1.725 656.225\r
+ 2.109 706.931\r
+ 2.493 696.526\r
+ 2.876 777.726\r
+ 3.260 775.919\r
+ 3.645 781.611\r
+ 4.028 712.736\r
+ 4.411 695.555\r
+ 4.796 701.251\r
+ 5.180 645.985\r
+ 5.564 607.757\r
+ 5.947 546.408\r
+ 6.331 387.372\r
+ 6.716 234.214\r
+ 7.099 181.086\r
+ 7.482 139.253\r
+ 7.867 106.109\r
+ 8.251 78.293\r
+ 8.634 54.851\r
+ 9.018 36.384\r
+ 9.402 20.375\r
+ 9.786 0.000\r
--- /dev/null
+; HyperTek L550 (1685CCRGL)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L550 111 724 0 1.53261 3.89805 HT\r
+ 0.124 816.849\r
+ 0.375 796.043\r
+ 0.626 781.861\r
+ 0.877 767.440\r
+ 1.129 759.627\r
+ 1.380 735.948\r
+ 1.631 714.454\r
+ 1.883 701.582\r
+ 2.134 674.667\r
+ 2.385 656.493\r
+ 2.637 636.076\r
+ 2.889 612.409\r
+ 3.140 587.801\r
+ 3.391 567.170\r
+ 3.642 559.971\r
+ 3.894 534.157\r
+ 4.145 444.562\r
+ 4.396 280.510\r
+ 4.648 216.702\r
+ 4.899 163.136\r
+ 5.150 120.571\r
+ 5.402 86.544\r
+ 5.653 59.990\r
+ 5.904 39.527\r
+ 6.156 25.914\r
+ 6.408 0.000\r
--- /dev/null
+; HyperTek L570O (2800CC172LFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L570O 111 876 0 2.57734 5.70618 HT\r
+ 0.181 793.916\r
+ 0.547 800.029\r
+ 0.914 811.765\r
+ 1.280 761.283\r
+ 1.647 725.674\r
+ 2.014 733.246\r
+ 2.380 783.159\r
+ 2.747 795.348\r
+ 3.112 823.178\r
+ 3.478 831.812\r
+ 3.845 805.614\r
+ 4.211 780.534\r
+ 4.578 741.917\r
+ 4.945 628.980\r
+ 5.311 547.886\r
+ 5.678 537.830\r
+ 6.044 330.850\r
+ 6.409 230.792\r
+ 6.776 180.510\r
+ 7.143 140.226\r
+ 7.509 108.348\r
+ 7.876 81.342\r
+ 8.243 59.608\r
+ 8.609 41.592\r
+ 8.976 25.536\r
+ 9.343 0.000\r
--- /dev/null
+; HyperTek L570 (2800/75-172-L-FX)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L570 75 1387 0 2.57152 5.10272 HT\r
+ 0.181 793.916\r
+ 0.547 800.029\r
+ 0.914 811.765\r
+ 1.280 761.283\r
+ 1.647 725.674\r
+ 2.014 733.246\r
+ 2.380 783.159\r
+ 2.747 795.348\r
+ 3.112 823.178\r
+ 3.478 831.812\r
+ 3.845 805.614\r
+ 4.211 780.534\r
+ 4.578 741.917\r
+ 4.945 628.980\r
+ 5.311 547.886\r
+ 5.678 537.830\r
+ 6.044 330.850\r
+ 6.409 230.792\r
+ 6.776 180.510\r
+ 7.143 140.226\r
+ 7.509 108.348\r
+ 7.876 81.342\r
+ 8.243 59.608\r
+ 8.609 41.592\r
+ 8.976 25.536\r
+ 9.343 0.000\r
--- /dev/null
+; HyperTek L575O (2800CCRGL)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L575O 111 876 0 2.52134 5.65286 HT\r
+ 0.190 705.343\r
+ 0.572 723.437\r
+ 0.955 738.414\r
+ 1.337 749.383\r
+ 1.720 735.169\r
+ 2.103 725.088\r
+ 2.486 733.751\r
+ 2.869 700.454\r
+ 3.251 690.771\r
+ 3.634 682.897\r
+ 4.017 674.825\r
+ 4.399 687.463\r
+ 4.782 675.411\r
+ 5.166 645.685\r
+ 5.548 643.612\r
+ 5.930 634.693\r
+ 6.314 559.731\r
+ 6.696 304.009\r
+ 7.078 229.423\r
+ 7.461 167.643\r
+ 7.845 121.036\r
+ 8.227 83.673\r
+ 8.609 54.311\r
+ 8.993 33.029\r
+ 9.376 19.886\r
+ 9.759 0.000\r
--- /dev/null
+; HyperTek L575 (2800/75-RG-L)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L575 75 1387 0 2.51328 5.06688 HT\r
+ 0.190 705.343\r
+ 0.572 723.437\r
+ 0.955 738.414\r
+ 1.337 749.383\r
+ 1.720 735.169\r
+ 2.103 725.088\r
+ 2.486 733.751\r
+ 2.869 700.454\r
+ 3.251 690.771\r
+ 3.634 682.897\r
+ 4.017 674.825\r
+ 4.399 687.463\r
+ 4.782 675.411\r
+ 5.166 645.685\r
+ 5.548 643.612\r
+ 5.930 634.693\r
+ 6.314 559.731\r
+ 6.696 304.009\r
+ 7.078 229.423\r
+ 7.461 167.643\r
+ 7.845 121.036\r
+ 8.227 83.673\r
+ 8.609 54.311\r
+ 8.993 33.029\r
+ 9.376 19.886\r
+ 9.759 0.000\r
--- /dev/null
+; HyperTek L610 (1685CCRGLFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L610 111 724 0 1.57696 3.95091 HT\r
+ 0.110 850.823\r
+ 0.333 837.700\r
+ 0.556 775.061\r
+ 0.779 739.656\r
+ 1.002 807.273\r
+ 1.225 809.462\r
+ 1.448 801.752\r
+ 1.671 789.534\r
+ 1.894 763.842\r
+ 2.117 858.087\r
+ 2.340 890.644\r
+ 2.563 837.593\r
+ 2.785 749.631\r
+ 3.008 648.961\r
+ 3.231 643.064\r
+ 3.454 637.413\r
+ 3.677 431.325\r
+ 3.900 276.205\r
+ 4.123 220.930\r
+ 4.346 166.632\r
+ 4.569 124.031\r
+ 4.792 89.721\r
+ 5.015 64.295\r
+ 5.237 45.360\r
+ 5.461 30.400\r
+ 5.685 0.000\r
--- /dev/null
+; HyperTek L625O (2800CCRGLFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L625O 111 876 0 2.56614 5.70618 HT\r
+ 0.169 855.024\r
+ 0.509 896.169\r
+ 0.851 905.926\r
+ 1.193 902.118\r
+ 1.534 853.016\r
+ 1.876 953.210\r
+ 2.218 904.246\r
+ 2.559 856.525\r
+ 2.901 743.522\r
+ 3.243 758.646\r
+ 3.584 751.619\r
+ 3.926 745.737\r
+ 4.267 751.907\r
+ 4.607 728.305\r
+ 4.949 691.703\r
+ 5.291 658.843\r
+ 5.632 504.450\r
+ 5.974 279.745\r
+ 6.316 216.661\r
+ 6.657 164.957\r
+ 6.999 123.426\r
+ 7.341 89.428\r
+ 7.682 62.955\r
+ 8.024 43.519\r
+ 8.366 27.956\r
+ 8.707 0.000\r
--- /dev/null
+; HyperTek L625 (2800/75-RG-L-FX)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L625 75 1387 0 2.56256 5.11616 HT\r
+ 0.169 855.024\r
+ 0.509 896.169\r
+ 0.851 905.926\r
+ 1.193 902.118\r
+ 1.534 853.016\r
+ 1.876 953.210\r
+ 2.218 904.246\r
+ 2.559 856.525\r
+ 2.901 743.522\r
+ 3.243 758.646\r
+ 3.584 751.619\r
+ 3.926 745.737\r
+ 4.267 751.907\r
+ 4.607 728.305\r
+ 4.949 691.703\r
+ 5.291 658.843\r
+ 5.632 504.450\r
+ 5.974 279.745\r
+ 6.316 216.661\r
+ 6.657 164.957\r
+ 6.999 123.426\r
+ 7.341 89.428\r
+ 7.682 62.955\r
+ 8.024 43.519\r
+ 8.366 27.956\r
+ 8.707 0.000\r
--- /dev/null
+;\r
+;\r
+L740 75 1422.4 100 2.667 6.416 HyperTek \r
+0.02 767.76\r
+0.05 1084.9\r
+0.07 1166.87\r
+0.09 1198.96\r
+0.12 1183.37\r
+0.38 1088.96\r
+0.63 1139.56\r
+0.89 1130.73\r
+1.15 1109\r
+1.4 1096.6\r
+1.66 1048.51\r
+1.92 1026.67\r
+2.18 980.7\r
+2.43 949.74\r
+2.69 909.63\r
+2.95 893.62\r
+3.21 866.64\r
+3.46 825.26\r
+3.72 820.05\r
+3.98 789.78\r
+4.24 874.08\r
+4.49 804.36\r
+4.75 738.04\r
+5.01 383.77\r
+5.38 255.83\r
+5.75 183.27\r
+6.13 130.48\r
+6.5 94.03\r
+6.8 0\r
--- /dev/null
+;\r
+;\r
+L970 75 1422.4 100 2.532 6.323 HyperTek \r
+0.01 534.31\r
+0.02 1007.71\r
+0.03 1320.67\r
+0.04 1463.43\r
+0.05 1482.44\r
+0.25 1315.87\r
+0.44 1362.79\r
+0.64 1441.44\r
+0.84 1452.58\r
+1.04 1418.39\r
+1.23 1403.65\r
+1.43 1337.52\r
+1.63 1311.22\r
+1.83 1257.09\r
+2.02 1279.26\r
+2.22 1229.81\r
+2.42 1174.23\r
+2.62 1162.77\r
+2.81 1122.04\r
+3.02 1108.55\r
+3.21 1058.31\r
+3.41 981.23\r
+3.61 959.35\r
+3.8 778.83\r
+4.09 437.55\r
+4.38 294.3\r
+4.66 194.71\r
+4.94 129.33\r
+5.23 86.31\r
+5.23 0\r
--- /dev/null
+; HyperTek M1000O (4630CCRGM)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M1000O 111 1147 0 4.17446 8.90714 HT\r
+ 0.197 1368.441\r
+ 0.593 1448.113\r
+ 0.989 1482.178\r
+ 1.384 1431.137\r
+ 1.780 1410.278\r
+ 2.176 1399.905\r
+ 2.573 1365.973\r
+ 2.970 1338.653\r
+ 3.366 1295.695\r
+ 3.761 1280.192\r
+ 4.157 1235.621\r
+ 4.553 1212.944\r
+ 4.950 1196.996\r
+ 5.347 1172.466\r
+ 5.743 1129.416\r
+ 6.139 1051.999\r
+ 6.534 635.308\r
+ 6.930 474.427\r
+ 7.327 359.958\r
+ 7.724 272.962\r
+ 8.120 205.206\r
+ 8.516 149.099\r
+ 8.911 103.639\r
+ 9.307 70.124\r
+ 9.704 48.706\r
+ 10.101 0.000\r
--- /dev/null
+; HyperTek M1000 (4630/98-RG-M)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M1000 98 1405 0 4.17536 8.72704 HT\r
+ 0.197 1368.441\r
+ 0.593 1448.113\r
+ 0.989 1482.178\r
+ 1.384 1431.137\r
+ 1.780 1410.278\r
+ 2.176 1399.905\r
+ 2.573 1365.973\r
+ 2.970 1338.653\r
+ 3.366 1295.695\r
+ 3.761 1280.192\r
+ 4.157 1235.621\r
+ 4.553 1212.944\r
+ 4.950 1196.996\r
+ 5.347 1172.466\r
+ 5.743 1129.416\r
+ 6.139 1051.999\r
+ 6.534 635.308\r
+ 6.930 474.427\r
+ 7.327 359.958\r
+ 7.724 272.962\r
+ 8.120 205.206\r
+ 8.516 149.099\r
+ 8.911 103.639\r
+ 9.307 70.124\r
+ 9.704 48.706\r
+ 10.101 0.000\r
--- /dev/null
+;\r
+;\r
+M1001 98 1493.5 100 5.161 10.092 HyperTek \r
+0.04 1394.15\r
+0.08 1440.23\r
+0.12 1322.3\r
+0.16 1328.89\r
+0.2 1340.76\r
+0.57 1411.23\r
+0.95 1420.87\r
+1.32 1415.98\r
+1.69 1404.61\r
+2.07 1384.44\r
+2.44 1370.95\r
+2.82 1354.19\r
+3.19 1318.56\r
+3.57 1326.82\r
+3.94 1338.4\r
+4.32 1247.32\r
+4.69 1287.12\r
+5.07 1220.48\r
+5.44 1123.54\r
+5.82 1075.29\r
+6.19 1078.11\r
+6.56 996.41\r
+6.94 953.17\r
+7.31 676.72\r
+7.83 419.5\r
+8.35 285.78\r
+8.87 192.18\r
+9.39 128.7\r
+9.87 0\r
--- /dev/null
+; HyperTek M1010O (4630CCRGMFX)\r
+; Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M1010O 111 1147 0 4.27571 8.99987 HT\r
+ 0.199 1473.475\r
+ 0.599 1472.868\r
+ 0.999 1463.740\r
+ 1.399 1380.769\r
+ 1.799 1408.210\r
+ 2.199 1383.970\r
+ 2.599 1332.771\r
+ 2.999 1356.808\r
+ 3.399 1339.075\r
+ 3.799 1306.425\r
+ 4.199 1266.222\r
+ 4.599 1223.656\r
+ 4.999 1190.978\r
+ 5.399 1145.190\r
+ 5.799 1103.440\r
+ 6.199 1060.790\r
+ 6.599 696.828\r
+ 6.999 487.469\r
+ 7.399 377.853\r
+ 7.799 288.215\r
+ 8.199 221.430\r
+ 8.599 166.674\r
+ 8.999 123.710\r
+ 9.399 90.168\r
+ 9.800 64.566\r
+ 10.201 0.000\r
--- /dev/null
+; HyperTek M1010 (4630/98-RG-M-FX)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+M1010 98 1405 0 4.23808 8.82112 HT\r
+ 0.199 1473.475\r
+ 0.599 1472.868\r
+ 0.999 1463.740\r
+ 1.399 1380.769\r
+ 1.799 1408.210\r
+ 2.199 1383.970\r
+ 2.599 1332.771\r
+ 2.999 1356.808\r
+ 3.399 1339.075\r
+ 3.799 1306.425\r
+ 4.199 1266.222\r
+ 4.599 1223.656\r
+ 4.999 1190.978\r
+ 5.399 1145.190\r
+ 5.799 1103.440\r
+ 6.199 1060.790\r
+ 6.599 696.828\r
+ 6.999 487.469\r
+ 7.399 377.853\r
+ 7.799 288.215\r
+ 8.199 221.430\r
+ 8.599 166.674\r
+ 8.999 123.710\r
+ 9.399 90.168\r
+ 9.800 64.566\r
+ 10.201 0.000\r
--- /dev/null
+;\r
+;\r
+M1015 98 1150.6 100 3.25 7.158 HyperTek \r
+0.04 1515.05\r
+0.08 1634.37\r
+0.12 1566.94\r
+0.16 1507.27\r
+0.2 1476.97\r
+0.41 1418.21\r
+0.63 1420.43\r
+0.85 1436.22\r
+1.06 1405.26\r
+1.28 1371.12\r
+1.49 1355\r
+1.71 1320.03\r
+1.93 1286.83\r
+2.14 1268.82\r
+2.36 1267.85\r
+2.58 1281.29\r
+2.79 1248.5\r
+3.01 1268.31\r
+3.23 1273.39\r
+3.44 1298.31\r
+3.66 1213.66\r
+3.87 1167.24\r
+4.09 1134.88\r
+4.31 1121.39\r
+4.69 544.64\r
+5.07 363.55\r
+5.46 234.8\r
+5.84 149.45\r
+6.22 94.73\r
+6.23 0\r
--- /dev/null
+;\r
+;\r
+M1040 98 1493.5 100 5.293 10.181 HyperTek \r
+0.02 1288.12\r
+0.05 1545.28\r
+0.07 1583.82\r
+0.09 1530.98\r
+0.12 1497.57\r
+0.53 1421.58\r
+0.95 1430.43\r
+1.37 1412.05\r
+1.78 1398.74\r
+2.2 1371.89\r
+2.61 1382.63\r
+3.03 1406.27\r
+3.44 1478.77\r
+3.86 1420.86\r
+4.27 1377.59\r
+4.69 1333.74\r
+5.1 1289.09\r
+5.52 1274.62\r
+5.93 1176.1\r
+6.35 1152.46\r
+6.77 891.06\r
+7.18 582.18\r
+7.6 429.01\r
+8.01 320.71\r
+8.43 238.67\r
+8.84 175.92\r
+9.25 128.91\r
+9.66 92.08\r
+9.7 0\r
--- /dev/null
+;\r
+;\r
+M740 75 1422.4 100 2.589 6.322 HyperTek \r
+0.04 979.34\r
+0.08 1135.85\r
+0.12 1065.56\r
+0.16 1026.83\r
+0.2 1022.88\r
+0.44 982.86\r
+0.68 1065.61\r
+0.91 1100.92\r
+1.15 1067.05\r
+1.39 1072.92\r
+1.63 1013.29\r
+1.87 1016.51\r
+2.11 1012.36\r
+2.35 1007.02\r
+2.58 968.14\r
+2.82 948.67\r
+3.06 944\r
+3.3 905.49\r
+3.54 899.55\r
+3.77 866.23\r
+4.02 847.5\r
+4.25 822.68\r
+4.49 813.98\r
+4.73 789.25\r
+4.97 752.47\r
+5.21 344.76\r
+5.45 271.93\r
+5.84 195.15\r
+6.46 108.81\r
+6.97 0\r
--- /dev/null
+;\r
+;\r
+M956 98 1150.6 100 3.162 7.061 HyperTek \r
+0.04 1325.32\r
+0.08 1335.19\r
+0.12 1273.52\r
+0.16 1245.8\r
+0.2 1261.01\r
+0.46 1283.75\r
+0.71 1339.94\r
+0.97 1333.16\r
+1.23 1322.79\r
+1.49 1330.11\r
+1.75 1294.72\r
+2.01 1271.81\r
+2.27 1246.37\r
+2.52 1233.02\r
+2.78 1214.19\r
+3.04 1199.4\r
+3.3 1152.16\r
+3.56 1128.04\r
+3.81 1119.3\r
+4.08 1098.79\r
+4.33 1054.12\r
+4.59 1031.85\r
+4.85 964.95\r
+5.11 548.37\r
+5.45 373.37\r
+5.79 248.05\r
+6.14 160.88\r
+6.48 109.1\r
+6.7 0\r
--- /dev/null
+;\r
+;\r
+M960 75 1422.4 100 2.629 6.414 HyperTek \r
+0.01 508.27\r
+0.02 1058.4\r
+0.03 1393.68\r
+0.04 1534.58\r
+0.05 1585.27\r
+0.25 1375.21\r
+0.44 1354.52\r
+0.64 1365.14\r
+0.84 1385.36\r
+1.04 1418.42\r
+1.23 1358.94\r
+1.43 1347.56\r
+1.63 1291.46\r
+1.83 1241.32\r
+2.02 1235.83\r
+2.22 1221.66\r
+2.42 1181.59\r
+2.62 1150.77\r
+2.81 1105.59\r
+3.02 1040.1\r
+3.21 982.54\r
+3.41 999.65\r
+3.61 943.72\r
+3.8 871.92\r
+4.11 526.67\r
+4.42 330.98\r
+4.72 212.71\r
+5.03 135.8\r
+5.33 0\r
--- /dev/null
+;Data entered by Tim Van Milligan\r
+;Based on TRA Certification 6-19-2002\r
+;And Instructions provided by Aerotech.\r
+I170S 38 258 14 0.1819 0.52 Kosdon-by-Aerotech \r
+0.019 194.885\r
+0.131 190.481\r
+0.255 191.582\r
+0.513 199.289\r
+0.641 204.794\r
+0.753 206.996\r
+0.88 209.199\r
+1 208.098\r
+1.051 208.098\r
+1.147 206.996\r
+1.24 201.491\r
+1.391 198.188\r
+1.537 190.481\r
+1.707 181.672\r
+1.746 178.369\r
+1.781 173.96\r
+1.808 168.46\r
+1.854 132.12\r
+1.939 53.951\r
+2.005 22.02\r
+2.059 9.909\r
+2.13 0\r
--- /dev/null
+;\r
+;\r
+I280F 38 258 14 100.182 0.52 Kosdon-by-AeroTech \r
+0.009 253.24\r
+0.055 255.442\r
+0.219 277.463\r
+0.482 301.686\r
+0.67 323.707\r
+0.735 330.314\r
+0.797 323.707\r
+1.001 297.282\r
+1.162 266.453\r
+1.205 259.847\r
+1.236 237.826\r
+1.363 50.6481\r
+1.428 26.4251\r
+1.5 0\r
--- /dev/null
+; KBA I301W\r
+I301W 38 369.6 18 0.295031 0.724 KBA\r
+ 0.0080 266.093\r
+ 0.014 327.114\r
+ 0.03 354.124\r
+ 0.058 350.122\r
+ 0.107 335.117\r
+ 0.133 326.114\r
+ 0.189 326.114\r
+ 0.217 333.116\r
+ 0.237 383.134\r
+ 0.253 402.14\r
+ 0.287 395.138\r
+ 0.33 381.133\r
+ 0.72 381.133\r
+ 1.035 341.119\r
+ 1.437 317.111\r
+ 1.57 262.092\r
+ 1.698 130.045\r
+ 1.789 83.029\r
+ 1.833 74.026\r
+ 1.867 53.019\r
+ 1.893 23.008\r
+ 1.916 13.005\r
+ 1.952 0.0\r
--- /dev/null
+;\r
+;Kosdon by AeroTech I310S\r
+;Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+;provided by ThrustCurve.org (www.thrustcurve.org)\r
+I310S 38 368 6-0 0.312256 0.713216 Kosdon-by-AeroTech \r
+0.045 334.66\r
+0.136 314.409\r
+0.228 322.556\r
+0.32 326.871\r
+0.411 331.851\r
+0.503 335.911\r
+0.595 336.933\r
+0.686 340.151\r
+0.778 342.066\r
+0.87 344.722\r
+0.961 348.578\r
+1.053 349.548\r
+1.146 351.943\r
+1.239 347.939\r
+1.33 345.079\r
+1.422 337.035\r
+1.514 333.332\r
+1.605 323.832\r
+1.697 289\r
+1.789 215.097\r
+1.88 136.596\r
+1.972 83.863\r
+2.064 37.922\r
+2.155 20.736\r
+2.248 5.943\r
+2.341 0\r
--- /dev/null
+;\r
+;Kosdon by AeroTech I370F\r
+;Copyright Tripoli Motor Testing 2001 (www.tripoli.org)\r
+;provided by ThrustCurve.org (www.thrustcurve.org)\r
+I370F 38 368 100 0.312256 0.705152 Kosdon-by-AeroTech \r
+0.035 373.074\r
+0.109 389.927\r
+0.184 401.07\r
+0.259 416.613\r
+0.334 429.598\r
+0.409 438.025\r
+0.484 443.83\r
+0.559 447.326\r
+0.634 446.764\r
+0.709 447.263\r
+0.784 444.735\r
+0.859 441.302\r
+0.933 435.676\r
+1.007 425.29\r
+1.082 414.897\r
+1.157 404.222\r
+1.232 395.358\r
+1.307 382.062\r
+1.382 334.152\r
+1.457 275.974\r
+1.532 179.654\r
+1.607 83.023\r
+1.682 39.608\r
+1.757 16.105\r
+1.832 4.151\r
+1.907 0\r
--- /dev/null
+;\r
+;\r
+I450F 38 370 14 0.3032 0.73 Kosdon-by-AeroTech \r
+0.012 634.202\r
+0.037 550.523\r
+0.108 519.693\r
+0.241 510.885\r
+0.639 550.523\r
+0.729 554.927\r
+0.809 546.118\r
+0.939 497.672\r
+1.072 471.247\r
+1.128 440.418\r
+1.165 387.568\r
+1.211 206.996\r
+1.295 88.0836\r
+1.36 26.4251\r
+1.41 0\r
--- /dev/null
+; KBA I550R\r
+I550R 38 369.6 20 0.295 0.713 KBA\r
+ 0.016 156.054\r
+ 0.028 278.097\r
+ 0.04 427.149\r
+ 0.054 550.192\r
+ 0.08 542.189\r
+ 0.245 588.205\r
+ 0.332 611.213\r
+ 0.424 631.22\r
+ 0.496 638.223\r
+ 0.613 644.225\r
+ 0.71 643.225\r
+ 0.758 631.22\r
+ 0.846 603.211\r
+ 0.894 613.214\r
+ 0.915 611.213\r
+ 0.939 586.205\r
+ 0.949 546.191\r
+ 0.959 505.176\r
+ 0.969 469.164\r
+ 0.983 381.133\r
+ 0.999 278.097\r
+ 1.011 200.07\r
+ 1.029 112.039\r
+ 1.053 42.015\r
+ 1.069 15.005\r
+ 1.089 0.0\r
--- /dev/null
+;\r
+;\r
+J405S 38 476 14 0.367 0.88 Kosdon-by-AeroTech \r
+0.009 528.502\r
+0.024 488.864\r
+0.046 462.439\r
+0.136 462.439\r
+0.268 458.035\r
+0.986 453.631\r
+1.421 444.822\r
+1.523 255.442\r
+1.697 92.4878\r
+1.93 0\r
--- /dev/null
+;\r
+;\r
+J605F 38 476 14 0.367 0.88 Kosdon-by-AeroTech \r
+0.024 886.341\r
+0.037 704.669\r
+0.077 660.627\r
+0.438 704.669\r
+0.506 715.679\r
+0.59 710.174\r
+0.853 655.122\r
+0.973 594.564\r
+1.041 412.892\r
+1.091 324.808\r
+1.177 132.125\r
+1.3 0\r
--- /dev/null
+;\r
+K1750R 54.0 728.00 0 1.25300 2.56000 KBA\r
+ 0.02 1309.09 \r
+ 0.03 1679.77 \r
+ 0.05 1736.54 \r
+ 0.11 1689.79 \r
+ 0.26 1799.99 \r
+ 0.40 1913.54 \r
+ 0.46 1896.84 \r
+ 0.68 2023.74 \r
+ 0.90 2133.94 \r
+ 0.95 2097.21 \r
+ 1.00 2050.46 \r
+ 1.05 1920.21 \r
+ 1.10 1793.31 \r
+ 1.16 1676.43 \r
+ 1.21 1719.85 \r
+ 1.25 1526.15 \r
+ 1.27 1302.41 \r
+ 1.32 874.95 \r
+ 1.35 454.17 \r
+ 1.36 317.25 \r
+ 1.37 200.37 \r
+ 1.40 90.17 \r
+ 1.46 0.00 \r
+;\r
--- /dev/null
+;\r
+;\r
+K400S 54 403 6-10-14 0.713216 1.50931 Kosdon-by-AeroTech \r
+0.074 465.928\r
+0.225 441.922\r
+0.377 442.414\r
+0.529 445.492\r
+0.681 449.048\r
+0.833 451.88\r
+0.985 454.481\r
+1.138 456.929\r
+1.29 458.237\r
+1.442 457.021\r
+1.594 455.62\r
+1.746 451.772\r
+1.897 446.421\r
+2.048 438.843\r
+2.2 429.377\r
+2.352 419.003\r
+2.504 408.274\r
+2.656 397.608\r
+2.808 388.018\r
+2.96 367.07\r
+3.113 263.666\r
+3.265 114.378\r
+3.417 46.238\r
+3.569 8.62\r
+3.721 2.401\r
+3.873 0\r
--- /dev/null
+; Kosdon by AeroTech K600F\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K600F 54 403 0 0.68096 1.41568 KBA\r
+ 0.045 639.654\r
+ 0.148 711.292\r
+ 0.252 695.617\r
+ 0.358 696.252\r
+ 0.462 701.336\r
+ 0.568 703.242\r
+ 0.670 705.265\r
+ 0.772 704.302\r
+ 0.878 702.819\r
+ 0.982 701.336\r
+ 1.088 696.888\r
+ 1.192 689.262\r
+ 1.295 681.245\r
+ 1.398 668.928\r
+ 1.502 653.465\r
+ 1.608 637.366\r
+ 1.712 619.785\r
+ 1.818 599.451\r
+ 1.920 586.275\r
+ 2.022 510.698\r
+ 2.128 334.676\r
+ 2.232 125.397\r
+ 2.338 37.916\r
+ 2.442 17.157\r
+ 2.548 4.025\r
+ 2.653 0.000\r
--- /dev/null
+; Kosdon by Aerotech K750 White Lightning.\r
+K750W 54 728 0 1.315 2.62 KBA\r
+ 0.0080 266.075\r
+ 0.012 457.102\r
+ 0.02 750.467\r
+ 0.032 999.485\r
+ 0.044 1112.055\r
+ 0.06 1180.279\r
+ 0.095 1098.41\r
+ 0.127 1057.476\r
+ 0.163 1040.42\r
+ 0.334 1050.653\r
+ 0.62 1054.064\r
+ 0.998 975.607\r
+ 1.324 907.382\r
+ 1.69 903.971\r
+ 2.06 886.915\r
+ 2.184 828.924\r
+ 2.299 757.289\r
+ 2.394 651.541\r
+ 2.502 556.028\r
+ 2.609 450.28\r
+ 2.784 327.476\r
+ 2.999 245.607\r
+ 3.039 201.261\r
+ 3.134 92.103\r
+ 3.206 40.935\r
+ 3.337 6.822\r
+ 3.468 0.0\r
--- /dev/null
+; Kosdon by AeroTech L1000S\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+L1000S 54 728 0 1.232 2.32512 KBA\r
+ 0.055 795.305\r
+ 0.175 981.574\r
+ 0.295 989.173\r
+ 0.415 1008.634\r
+ 0.535 1028.836\r
+ 0.655 1048.483\r
+ 0.775 1067.573\r
+ 0.895 1087.034\r
+ 1.015 1108.719\r
+ 1.135 1131.516\r
+ 1.255 1156.908\r
+ 1.375 1177.296\r
+ 1.498 1199.596\r
+ 1.620 1212.881\r
+ 1.740 1227.153\r
+ 1.860 1232.342\r
+ 1.980 1249.950\r
+ 2.100 1026.056\r
+ 2.220 737.107\r
+ 2.340 565.851\r
+ 2.460 313.414\r
+ 2.580 89.706\r
+ 2.700 20.758\r
+ 2.820 8.526\r
+ 2.942 5.338\r
+ 3.065 0.000\r
--- /dev/null
+;\r
+;\r
+L1400F 54 727 100 1.248 2.502 Kosdon-by-AeroTech \r
+0.037 1541.46\r
+0.061 1453.38\r
+0.166 1354.29\r
+1.001 1772.68\r
+1.279 1783.69\r
+1.329 1882.79\r
+1.387 1992.89\r
+1.486 1387.32\r
+1.604 869.826\r
+1.65 748.711\r
+1.666 726.69\r
+1.69 924.878\r
+1.697 594.564\r
+1.758 319.303\r
+1.88 0\r
--- /dev/null
+; KBA M1450W\r
+M1450W 75 1038.9 0 4.15 7.6000000000000005 KBA\r
+ 0.035 1842.929\r
+ 0.076 2287.088\r
+ 0.146 1968.884\r
+ 0.215 1882.704\r
+ 0.291 1836.299\r
+ 0.499 1862.816\r
+ 1.005 1935.738\r
+ 1.559 1889.333\r
+ 2.155 1816.412\r
+ 2.862 1750.119\r
+ 3.493 1663.939\r
+ 3.853 1358.994\r
+ 4.221 1060.678\r
+ 4.484 788.88\r
+ 4.761 523.71\r
+ 4.942 258.54\r
+ 5.323 258.54\r
+ 5.6 172.36\r
+ 5.801 119.326\r
+ 5.96 0.0\r
--- /dev/null
+;\r
+;\r
+H144 38 178 5-8-10-13-17 0.12 0.335 Loki \r
+0.02 209\r
+0.04 247.6\r
+0.05 241.2\r
+0.1 247.6\r
+0.15 244.4\r
+0.2 237.9\r
+0.25 231.54\r
+0.3 228.3\r
+0.4 215.32\r
+0.45 212.43\r
+0.5 204.48\r
+0.6 194.36\r
+0.7 189.7\r
+0.8 170.4\r
+0.9 154.3\r
+1 127.83\r
+1.1 109.3\r
+1.2 80.4\r
+1.3 64.6\r
+1.4 44.6\r
+1.5 32.1\r
+1.6 0\r
--- /dev/null
+;\r
+;\r
+H500 38 292 5-7-9-12-15 0.16 0.454 Loki \r
+0.001 189.286\r
+0.0116009 534.733\r
+0.099768 539.465\r
+0.199536 544.197\r
+0.302784 553.662\r
+0.402552 548.93\r
+0.50464 544.197\r
+0.584687 435.358\r
+0.61949 9.4643\r
+0.62 0\r
--- /dev/null
+;\r
+;\r
+I405 38 292 5-8-10-13-17 0.24 0.54 Loki \r
+0.01 151.1\r
+0.03 781.4\r
+0.05 800.7\r
+0.06 755.7\r
+0.09 724.3\r
+0.12 697.7\r
+0.15 701\r
+0.17 675.3\r
+0.2 643.1\r
+0.3 607.7\r
+0.4 569.2\r
+0.5 517.7\r
+0.6 472.7\r
+0.7 392.3\r
+0.8 318.3\r
+0.9 241.2\r
+1 151.1\r
+1.1 93.3\r
+1.15 40\r
+1.2 0\r
--- /dev/null
+;\r
+;\r
+J525 54 327 0 0.59 1.264 Loki \r
+0.01 210.9\r
+0.03 499.3\r
+0.05 628.5\r
+0.1 594\r
+0.13 568.2\r
+0.15 559.6\r
+0.2 555.3\r
+0.3 572.5\r
+0.4 589.7\r
+0.5 606.9\r
+0.6 624.2\r
+0.7 637.1\r
+0.8 645.7\r
+0.9 650\r
+1 658.6\r
+1.1 637.1\r
+1.2 628.5\r
+1.3 615.5\r
+1.41 586.27\r
+1.52 561.52\r
+1.67 536.78\r
+1.78 517.74\r
+1.85 485.38\r
+1.92 91.37\r
+2 0\r
--- /dev/null
+;\r
+J528 38 406 5-8-10-13-17 0.372 0.752 Loki \r
+0.01 704.2\r
+0.02 1019\r
+0.03 983.9\r
+0.05 881.1\r
+0.1 797.5\r
+0.15 771.7\r
+0.17 765.72\r
+0.21 765.72\r
+0.25 778.2\r
+0.42 789.28\r
+0.51 771.61\r
+0.6 756.89\r
+0.66 751\r
+0.71 762.78\r
+0.76 697.99\r
+0.8 665.59\r
+0.84 612.58\r
+0.92 488.88\r
+0.95 385.81\r
+1.02 282.73\r
+1.06 179.65\r
+1.14 53.01\r
+1.19 35.34\r
+1.23 32.2\r
+1.25 0\r
--- /dev/null
+;\r
+;\r
+K250 54 498 0 0.952544 1.79169 Loki \r
+0.03 800\r
+0.1 682\r
+0.125 574\r
+0.15 476\r
+0.175 447\r
+0.25 385\r
+0.45 340\r
+0.6 320\r
+1 313\r
+1.5 300\r
+2 297\r
+2.5 303\r
+3 294\r
+3.5 287\r
+4 248\r
+4.5 222\r
+5 187\r
+5.5 147\r
+6 114\r
+6.5 62\r
+7 0\r
--- /dev/null
+;\r
+;\r
+K350 54 702 0 1.4 2.54012 Loki\r
+0.025 1329\r
+0.0375 1061\r
+0.1 1006\r
+0.15 891\r
+0.2 768\r
+0.4 571\r
+0.5 542\r
+0.75 486\r
+1 486\r
+1.25 477\r
+1.5 481\r
+2.5058 460\r
+3.00464 427\r
+3.5 375\r
+4 333\r
+4.5 297\r
+5 249\r
+5.5 210\r
+6 164\r
+6.5 98\r
+7 0\r
--- /dev/null
+;\r
+;\r
+K960 54 498 0 0.929864 1.74633 Loki \r
+0.03 1210\r
+0.05 1512\r
+0.075 1535\r
+0.1 1502\r
+0.125 1437\r
+0.2 1237\r
+0.3 1175\r
+0.5 1139\r
+0.6 1130\r
+0.7 1156\r
+0.8 1182\r
+0.9 1192\r
+1 1166\r
+1.1 1139\r
+1.2 1101\r
+1.3 1091\r
+1.4 1026\r
+1.5 839\r
+1.6 790\r
+1.7 575\r
+1.8 284\r
+1.9 150\r
+2 0\r
--- /dev/null
+;\r
+;\r
+L1400 54 726 0 1.4 2.54 Loki \r
+0.00580046 1606.3\r
+0.11891 1535.7\r
+0.327726 1535.7\r
+0.49884 1588.65\r
+1.00058 1782.82\r
+1.40661 1906.38\r
+1.49942 1376.83\r
+1.60673 953.19\r
+1.74594 547.202\r
+1.90545 335.382\r
+1.99826 211.82\r
+2 0\r
--- /dev/null
+;\r
+;\r
+L930 76 498 0 1.81437 3.53802 Loki\r
+0.025 532\r
+0.05 1123\r
+0.075 1123\r
+0.125 1094\r
+0.2 930\r
+0.5 881\r
+0.6 878\r
+0.75 898\r
+1 921\r
+1.25 940\r
+1.5 1012\r
+1.75 1081\r
+2 1100\r
+2.25 1120\r
+2.5 1051\r
+2.75 980\r
+3 934\r
+3.25 826\r
+3.5 722\r
+3.75 280\r
+4 0\r
--- /dev/null
+;\r
+;\r
+M1882 75 785 0 3.12979 5.53383 Loki \r
+0.01 4.8\r
+0.0174014 2579.22\r
+0.0696056 2392.32\r
+0.232019 2298.87\r
+0.50464 2261.49\r
+0.771462 2298.87\r
+0.986079 2411.01\r
+1.1891 2579.22\r
+1.49652 2597.91\r
+1.72854 2485.77\r
+2.00116 2354.94\r
+2.5 1644.72\r
+2.99884 242.97\r
+3.25 0\r
--- /dev/null
+;\r
+F50T 29.0 98.00 4-6-9 0.03790 0.08490 AT\r
+ 0.01 37.97 \r
+ 0.02 56.27 \r
+ 0.03 65.08 \r
+ 0.12 71.86 \r
+ 0.23 75.25 \r
+ 0.33 77.29 \r
+ 0.35 77.70 \r
+ 0.45 75.25 \r
+ 0.59 71.86 \r
+ 0.72 65.76 \r
+ 0.83 58.98 \r
+ 1.01 43.39 \r
+ 1.19 25.76 \r
+ 1.25 15.59 \r
+ 1.30 8.81 \r
+ 1.36 4.75 \r
+ 1.42 0.00 \r
+;\r
--- /dev/null
+; PML G40W is the same as the Aerotech G40W.\r
+G40W 29 124 4-7-10 0.0624 0.115 PML\r
+ 0.015 40.0\r
+ 0.037 66.479\r
+ 0.066 60.845\r
+ 0.161 55.775\r
+ 0.308 55.775\r
+ 0.447 54.085\r
+ 0.549 52.394\r
+ 0.637 51.268\r
+ 0.857 52.958\r
+ 1.018 52.394\r
+ 1.172 50.141\r
+ 1.362 46.761\r
+ 1.611 41.69\r
+ 1.691 41.127\r
+ 1.845 34.366\r
+ 1.999 30.423\r
+ 2.372 23.099\r
+ 2.585 13.521\r
+ 2.738 6.761\r
+ 3.039 0.0\r
--- /dev/null
+; PML G80T is the same as the old Aerotech G480T.\r
+G80T 29 124 4-7-10 0.0574 0.106 PML\r
+ 0.0070 82.746\r
+ 0.018 104.754\r
+ 0.051 104.754\r
+ 0.095 97.711\r
+ 0.245 94.19\r
+ 0.458 95.07\r
+ 0.6 93.31\r
+ 0.86 84.507\r
+ 1.003 76.585\r
+ 1.139 69.542\r
+ 1.252 57.218\r
+ 1.303 51.056\r
+ 1.34 37.852\r
+ 1.38 19.366\r
+ 1.424 7.923\r
+ 1.461 2.641\r
+ 1.497 0.0\r
--- /dev/null
+;\r
+H70 38 462 100 0.318 0.627 Propulsion-Polymers \r
+0.05 170.9\r
+0.09 122.3\r
+0.37 106.8\r
+0.74 97.9\r
+1.11 93.4\r
+1.48 89\r
+1.84 84.5\r
+2.21 77.1\r
+2.33 74\r
+2.58 41.5\r
+2.95 23.7\r
+3.32 15.6\r
+3.69 0\r
--- /dev/null
+;\r
+;\r
+I160 38 646 20-100 0.31 0.856 Propulsion-Polymers \r
+0.04 298.9\r
+0.08 272.5\r
+0.32 264.7\r
+0.65 241.3\r
+0.97 218\r
+1.29 194.6\r
+1.61 179\r
+2 163.5\r
+2.26 93.4\r
+2.58 62.3\r
+2.9 31.1\r
+3.23 0\r
--- /dev/null
+;\r
+;\r
+I80 38 646 100 0.332 0.842 Propulsion-Polymers \r
+0.04 198.7\r
+0.08 158.4\r
+0.57 133.4\r
+0.9 114.5\r
+1.15 110\r
+1.73 108\r
+2.3 106\r
+2.88 104\r
+3.45 96.3\r
+3.75 66.7\r
+4.03 50\r
+4.6 35.6\r
+5.18 22.2\r
+5.75 0\r
--- /dev/null
+; Propulsion Polymers 664NS-J140\r
+; from CAR data sheet\r
+; created by John Coker 5/2006\r
+J140 38 881 P 0.626 1.166 PP\r
+ 0.02 249\r
+ 0.20 320\r
+ 0.25 240\r
+ 0.30 236\r
+ 0.35 191\r
+ 0.45 236\r
+ 1.15 223\r
+ 1.60 178\r
+ 3.05 165\r
+ 3.40 102\r
+ 4.25 45\r
+ 4.95 22\r
+ 5.00 0\r
--- /dev/null
+;\r
+;\r
+A6Q 18 70 4 0.0035 0.0153 Quest \r
+0.1 4.8\r
+0.2 11.82\r
+0.23 7.9\r
+0.3 4.8\r
+0.41 0\r
--- /dev/null
+;\r
+;\r
+B6Q 18 70 0-2-4 0.0065 0.0162 Quest \r
+0.1 7\r
+0.18 14.38\r
+0.2 10.2\r
+0.24 6.6\r
+0.3 6\r
+0.4 6.1\r
+0.5 6.2\r
+0.6 6.3\r
+0.65 6.6\r
+0.7 3\r
+0.75 0\r
--- /dev/null
+; Quest C6-0 from NAR data\r
+C6-0 18 70 0 0.0083 0.0216 Q\r
+ 0.02 0.497\r
+ 0.057 2.539\r
+ 0.089 5.132\r
+ 0.129 7.947\r
+ 0.159 9.437\r
+ 0.171 21.247\r
+ 0.181 23.234\r
+ 0.194 22.958\r
+ 0.204 22.185\r
+ 0.218 19.592\r
+ 0.233 17.881\r
+ 0.258 10.486\r
+ 0.308 2.428\r
+ 0.338 2.539\r
+ 0.385 2.98\r
+ 0.412 3.091\r
+ 0.442 3.422\r
+ 0.459 2.98\r
+ 0.536 3.256\r
+ 0.732 3.311\r
+ 0.747 2.483\r
+ 0.78 2.98\r
+ 1.323 3.587\r
+ 1.365 2.815\r
+ 1.887 3.808\r
+ 1.974 3.256\r
+ 2.1 3.532\r
+ 2.227 3.201\r
+ 2.247 0.0\r
--- /dev/null
+; Quest D5-0 by Mark Koelsch from NAR data\r
+D5-0 20 88 0 0.025 0.0384 Q\r
+ 0.096 1.241\r
+ 0.252 5.897\r
+ 0.304 8.586\r
+ 0.357 10.552\r
+ 0.391 11.483\r
+ 0.435 9.828\r
+ 0.557 6.103\r
+ 0.583 5.172\r
+ 0.67 5.172\r
+ 1.078 4.966\r
+ 1.2 4.345\r
+ 1.73 4.759\r
+ 1.8 4.759\r
+ 1.887 4.138\r
+ 2.391 5.069\r
+ 2.626 4.966\r
+ 3.009 5.379\r
+ 3.357 5.276\r
+ 3.661 5.69\r
+ 3.835 3.103\r
+ 3.887 1.655\r
+ 3.983 0.0\r
--- /dev/null
+; RATT Works H70H\r
+; Copyright Tripoli Motor Testing 2002 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+H70H 29 457 0 0.106176 0.348992 RTW\r
+ 0.051 124.137\r
+ 0.156 134.902\r
+ 0.261 127.047\r
+ 0.367 115.870\r
+ 0.473 110.286\r
+ 0.578 108.630\r
+ 0.683 105.741\r
+ 0.789 104.108\r
+ 0.894 101.515\r
+ 1.000 98.471\r
+ 1.105 93.809\r
+ 1.210 90.543\r
+ 1.316 85.219\r
+ 1.421 74.353\r
+ 1.527 57.739\r
+ 1.632 44.019\r
+ 1.738 34.554\r
+ 1.843 27.992\r
+ 1.948 22.691\r
+ 2.054 19.064\r
+ 2.159 15.665\r
+ 2.265 12.897\r
+ 2.370 11.332\r
+ 2.475 10.107\r
+ 2.581 8.591\r
+ 2.688 0.000\r
--- /dev/null
+; RATT Works I80H\r
+; Copyright Tripoli Motor Testing 2002 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I80H 29 730 0 0.21952 0.549248 RTW\r
+ 0.093 84.101\r
+ 0.280 102.075\r
+ 0.469 118.708\r
+ 0.657 117.130\r
+ 0.846 114.829\r
+ 1.034 112.225\r
+ 1.222 109.818\r
+ 1.410 107.622\r
+ 1.599 105.945\r
+ 1.787 102.168\r
+ 1.976 100.449\r
+ 2.164 99.265\r
+ 2.352 96.272\r
+ 2.541 91.951\r
+ 2.729 89.380\r
+ 2.918 86.430\r
+ 3.105 54.544\r
+ 3.294 41.902\r
+ 3.482 33.368\r
+ 3.671 26.516\r
+ 3.859 21.324\r
+ 4.047 16.951\r
+ 4.235 14.387\r
+ 4.424 13.456\r
+ 4.612 12.777\r
+ 4.801 0.000\r
--- /dev/null
+; RATT Works I90LH\r
+; Copyright Tripoli Motor Testing 2002 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+I90LH 29 921 0 0.285376 0.684544 RTW\r
+ 0.127 137.737\r
+ 0.383 121.431\r
+ 0.640 117.048\r
+ 0.897 121.354\r
+ 1.154 118.436\r
+ 1.410 116.538\r
+ 1.668 114.826\r
+ 1.925 112.358\r
+ 2.181 110.303\r
+ 2.439 107.768\r
+ 2.696 105.749\r
+ 2.952 104.733\r
+ 3.209 102.802\r
+ 3.467 95.410\r
+ 3.723 68.281\r
+ 3.980 55.000\r
+ 4.237 43.620\r
+ 4.494 33.784\r
+ 4.751 31.704\r
+ 5.008 27.714\r
+ 5.265 24.875\r
+ 5.522 22.930\r
+ 5.779 21.379\r
+ 6.035 20.884\r
+ 6.293 18.637\r
+ 6.550 0.000\r
--- /dev/null
+;\r
+;J160 data entered by Tim Van Milligan\r
+;Thrust Curve based on TRA certification dated 10/24/2003.\r
+;Propellant weight based on 80F degree day, 490cc Oxidizer + 4.25g AP weight.\r
+J160 38 1219 100 0.327926 0.544 RATT_Works \r
+0.015456 262.049\r
+0.185471 255.442\r
+0.278207 240.028\r
+0.278207 229.017\r
+0.386399 251.038\r
+0.510046 242.23\r
+0.64915 213.603\r
+0.788253 189.38\r
+1.00464 178.369\r
+1.45286 160.753\r
+2.00927 147.54\r
+2.44204 129.923\r
+2.90572 121.115\r
+2.96754 162.955\r
+3.15301 149.742\r
+3.32303 125.519\r
+3.67852 94.6899\r
+3.97218 74.8711\r
+4.32767 46.2439\r
+4.69861 19.8188\r
+5.1 0\r
--- /dev/null
+; RATT Works K240H\r
+; Copyright Tripoli Motor Testing 2002 (www.tripoli.org)\r
+; provided by ThrustCurve.org (www.thrustcurve.org)\r
+K240H 64 908 0 1.29338 2.81434 RTW\r
+ 0.164 413.978\r
+ 0.493 392.975\r
+ 0.822 370.841\r
+ 1.151 362.058\r
+ 1.480 337.839\r
+ 1.809 321.401\r
+ 2.139 285.118\r
+ 2.468 280.031\r
+ 2.798 275.060\r
+ 3.128 278.392\r
+ 3.457 274.394\r
+ 3.786 252.302\r
+ 4.116 272.858\r
+ 4.445 264.671\r
+ 4.774 255.572\r
+ 5.103 210.314\r
+ 5.433 163.955\r
+ 5.764 126.239\r
+ 6.093 98.015\r
+ 6.422 74.491\r
+ 6.751 55.617\r
+ 7.080 40.778\r
+ 7.409 29.618\r
+ 7.739 21.915\r
+ 8.069 17.214\r
+ 8.399 0.000\r
--- /dev/null
+;\r
+;Rattworks L600 Hybrid\r
+;prepared by Andrew MacMillen NAR 77472 2/28/04\r
+;based on TMT thrust data from Paul Holmes & TMT cert doc\r
+;NOX weight calc'd at .7048gm/cc, 75 deg F @ 812 psi\r
+;compiled with RockSim7 EngEdit\r
+;within 3 percent for total impulse\r
+;peak and average thrust are off due to data spike smoothing\r
+;accurate thrust profile\r
+;NOTE: NOT RATT, CAR, TMT OR NAR APPROVED\r
+;Rattworks L600\r
+L600 64 1066 100 1.863 2.25 RATT_Works \r
+0 88.62\r
+0.02 153.82\r
+0.19 158.02\r
+0.35 169.94\r
+0.4 1127.04\r
+0.78 1001.2\r
+2.23 778.11\r
+2.84 736.24\r
+3.07 704\r
+3.62 673.43\r
+3.81 435.29\r
+3.89 350.43\r
+4.14 240.05\r
+4.31 197.19\r
+4.49 165.4\r
+4.66 139.26\r
+4.82 119.21\r
+5.01 99.01\r
+5.34 70.99\r
+5.93 49.74\r
+6.49 38.96\r
+6.98 36.42\r
+7.1 0\r
--- /dev/null
+;\r
+;Rattworks M900 Hybrid\r
+;prepared by Andrew MacMillen NAR 77472 12/23/04\r
+;based on TMT thrust data from Paul Holmes & TMT cert doc\r
+;NOX weight calc'd at .7048gm/cc, 75 deg F @ 812 psi\r
+;compiled with RockSim6 EngEdit\r
+;within 2 percent for total impulse\r
+;peak and average thrust are off due to data spike smoothing\r
+;accurate thrust profile\r
+;NOTE: NOT RATT, CAR, TMT OR NAR APPROVED\r
+;Rattworks M900\r
+M900 69 1828 100 3.288 5.956 RATT_Works \r
+0.04 151.9\r
+0.43 241.76\r
+0.5 1054.61\r
+7.04 718.06\r
+7.22 346.42\r
+7.26 297.14\r
+7.35 241.24\r
+7.48 200.65\r
+7.66 164.8\r
+12.28 49.36\r
+12.3 0\r
--- /dev/null
+; Rocketvision F32\r
+; from NAR data sheet updated 11/2000\r
+; created by John Coker 5/2006\r
+F32 24 124 5-10-15 .0377 .0695 RV\r
+ 0.01 50\r
+ 0.05 56\r
+ 0.10 48\r
+ 2.00 24\r
+ 2.20 19\r
+ 2.45 5\r
+ 2.72 0\r
--- /dev/null
+; Same motor as the Aerotech F72T single use from NAR cert data\r
+F72T 24 124 5-10-15 0.0368 0.0746 Rocketvision\r
+ 0.0040 37.671\r
+ 0.01 78.082\r
+ 0.017 97.26\r
+ 0.027 91.781\r
+ 0.043 89.726\r
+ 0.06 80.822\r
+ 0.087 84.932\r
+ 0.101 78.767\r
+ 0.132 81.507\r
+ 0.143 78.767\r
+ 0.171 81.507\r
+ 0.192 78.082\r
+ 0.215 80.822\r
+ 0.24 78.082\r
+ 0.264 81.507\r
+ 0.279 78.767\r
+ 0.298 80.137\r
+ 0.517 76.027\r
+ 0.68 70.548\r
+ 0.855 58.219\r
+ 0.934 49.315\r
+ 0.961 43.151\r
+ 0.996 31.507\r
+ 1.025 21.233\r
+ 1.054 14.384\r
+ 1.103 7.534\r
+ 1.147 3.425\r
+ 1.196 0.0\r
--- /dev/null
+; Same motor as the Aerotech G55W single use from NAR cert data\r
+G55W 24 177 5-10-15 0.0625 0.115 Rocketvision\r
+ 0.0040 74.648\r
+ 0.012 85.211\r
+ 0.054 81.69\r
+ 0.128 73.239\r
+ 0.182 73.944\r
+ 0.231 69.718\r
+ 0.508 69.014\r
+ 0.868 67.606\r
+ 1.037 65.493\r
+ 1.07 67.606\r
+ 1.091 64.085\r
+ 1.14 63.38\r
+ 1.161 66.901\r
+ 1.19 62.676\r
+ 1.269 62.676\r
+ 1.397 59.155\r
+ 1.496 57.042\r
+ 1.583 52.113\r
+ 1.653 45.07\r
+ 1.719 38.028\r
+ 1.76 31.69\r
+ 1.831 24.648\r
+ 1.901 19.014\r
+ 1.988 12.676\r
+ 2.083 9.155\r
+ 2.169 5.634\r
+ 2.252 3.521\r
+ 2.36 0.0\r
--- /dev/null
+;ROADRUNNER E25R WRASP FILE\r
+E25R 29 76 4-7 0.02 0.078 RR\r
+ 0.0 1.15995\r
+ 0.0 4.0904\r
+ 0.01 13.9194\r
+ 0.02 24.481\r
+ 0.025 28.327\r
+ 0.04 33.028\r
+ 0.045 33.639\r
+ 0.07 33.516\r
+ 0.12 35.287\r
+ 0.195 36.569\r
+ 0.245 38.278\r
+ 0.28 37.668\r
+ 0.315 38.657\r
+ 0.35 37.729\r
+ 0.385 37.973\r
+ 0.45 36.691\r
+ 0.56 36.569\r
+ 0.73 32.295\r
+ 0.82 29.487\r
+ 0.9 26.068\r
+ 0.92 25.824\r
+ 0.945 24.176\r
+ 0.995 22.772\r
+ 1.035 20.024\r
+ 1.08 18.5592\r
+ 1.165 14.2247\r
+ 1.19 13.6142\r
+ 1.31 7.2039\r
+ 1.395 4.2735\r
+ 1.445 3.602\r
+ 1.49 2.1978\r
+ 1.505 0.79365\r
+ 1.506 0\r
--- /dev/null
+; ROADRUNNER F35 RASP.ENG FILE\r
+; File produced April 5, 2006\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+F35 29 112 6-10 0.040 0.111 RR\r
+0.023 33.700\r
+0.040 44.462\r
+0.081 47.206\r
+0.121 48.579\r
+0.166 49.270\r
+0.242 49.550\r
+0.315 51.010\r
+0.411 50.111\r
+0.528 49.710\r
+0.664 48.208\r
+0.791 47.256\r
+0.896 46.986\r
+1.000 45.484\r
+1.097 44.943\r
+1.194 42.338\r
+1.277 40.400\r
+1.323 40.706\r
+1.356 37.691\r
+1.402 35.637\r
+1.451 32.753\r
+1.505 29.467\r
+1.578 25.491\r
+1.675 20.833\r
+1.750 17.137\r
+1.828 13.021\r
+1.907 8.638\r
+1.984 5.075\r
+2.049 2.610\r
+2.130 0.000\r
--- /dev/null
+; ROADRUNNER F45R RASP ENG FILE\r
+F45R 29 93 5-8-14 0.03 0.093 RR\r
+ 0.0 4.1971\r
+ 0.019 45.500\r
+ 0.038 53.070\r
+ 0.057 52.402\r
+ 0.095 54.741\r
+ 0.113 55.298\r
+ 0.132 56.744\r
+ 0.151 57.190\r
+ 0.227 60.419\r
+ 0.284 61.754\r
+ 0.416 62.422\r
+ 0.491 60.753\r
+ 0.510 61.420\r
+ 0.567 60.307\r
+ 0.624 58.414\r
+ 0.662 58.080\r
+ 0.737 55.298\r
+ 0.775 52.959\r
+ 0.813 51.513\r
+ 0.888 46.169\r
+ 0.983 34.701\r
+ 1.002 31.807\r
+ 1.096 23.902\r
+ 1.134 21.453\r
+ 1.210 14.106\r
+ 1.229 12.992\r
+ 1.285 8.4282\r
+ 1.342 5.5328\r
+ 1.361 5.4218\r
+ 1.418 2.9723\r
+ 1.420 0.0\r
--- /dev/null
+;\r
+; ROADRUNNER F60 RASP.ENG FILE\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+F60R 29 112 4-7-10 0.038 0.109 RR\r
+0.013 45.860\r
+0.021 63.937\r
+0.029 72.291\r
+0.041 75.214\r
+0.061 74.374\r
+0.087 76.872\r
+0.155 83.122\r
+0.231 86.440\r
+0.309 88.088\r
+0.329 90.070\r
+0.345 88.33\r
+0.395 87.90\r
+0.454 87.208\r
+0.514 87.188\r
+0.616 82.141\r
+0.699 77.105\r
+0.765 70.400\r
+0.807 61.611\r
+0.859 51.983\r
+0.926 42.355\r
+0.978 33.556\r
+1.022 21.430\r
+1.061 13.056\r
+1.101 6.776\r
+1.133 3.423\r
+1.190 0.000\r
--- /dev/null
+;\r
+; ROADRUNNER G80 RASP.ENG FILE\r
+; The total impulse, peak thrust, average thrust and burn time are\r
+; the same as the averaged static test data on the NAR web site in\r
+; the certification file. The curve drawn with these data points is as\r
+; close to the certification curve as can be with such a limited\r
+; number of points (32) allowed with wRASP up to v1.6.\r
+G80R 29 140 4-7-10 0.055 0.133 RR\r
+0.012 63.563\r
+0.028 84.077\r
+0.057 89.563\r
+0.119 96.03\r
+0.206 102.518\r
+0.242 104.42\r
+0.297 106.923\r
+0.356 109.826\r
+0.422 111.829\r
+0.483 111.328\r
+0.558 112.632\r
+0.622 112.750\r
+0.683 112.129\r
+0.739 109.125\r
+0.796 102.017\r
+0.863 90.494\r
+0.901 82.750\r
+0.935 72.41\r
+0.976 59.869\r
+1.018 49.826\r
+1.028 44.321\r
+1.042 39.805\r
+1.073 28.272\r
+1.113 18.231\r
+1.170 11.176\r
+1.218 4.636\r
+1.310 0.000\r
--- /dev/null
+; Sachsen Feuerwerk / WECO Feuerwerk A8-3
+; Created by Sampo Niskanen
+; Data taken from:
+; http://www.raketenmodellbautechnik.de/produkte/Motoren/SF-Motoren.pdf
+A8 18 70 3 0.00312 0.0153 SF
+ 0.065 0.44
+ 0.11 1.832
+ 0.199 6.412
+ 0.298 12.0
+ 0.332 7.805
+ 0.363 5.716
+ 0.438 4.397
+ 0.462 4.379
+ 0.555 1.227
+ 0.62 0.0
--- /dev/null
+; Sachsen Feuerwerk / WECO Feuerwerk B4-0, B4-4
+; Created by Sampo Niskanen
+; Data taken from:
+; http://www.raketenmodellbautechnik.de/produkte/Motoren/SF-Motoren.pdf
+B4 18 70 0-4 0.00833 0.0195 SF
+ 0.088 0.542
+ 0.167 3.007
+ 0.319 11.678
+ 0.373 5.297
+ 0.525 4.091
+ 0.648 3.461
+ 0.8 3.321
+ 1.305 3.321
+ 1.389 0.455
+ 1.443 0.0
--- /dev/null
+; Sachsen Feuerwerk / WECO Feuerwerk Held 1000
+; Created by Sampo Niskanen
+; True propellant weight unknown
+; Data taken from:
+; http://www.raketenmodellbautechnik.de/produkte/Motoren/SF-Motoren.pdf
+C2 15 95 P 0.012 0.024 SF
+ 0.075 3.543
+ 0.16 8.231
+ 0.184 8.007
+ 0.255 3.134
+ 0.316 1.765
+ 0.444 1.304
+ 0.821 1.251
+ 0.963 1.04
+ 1.538 1.277
+ 1.736 1.133
+ 2.491 1.317
+ 2.704 1.264
+ 3.397 1.436
+ 3.907 1.436
+ 4.157 1.277
+ 4.459 1.449
+ 4.469 2.292
+ 4.53 1.436
+ 4.917 1.449
+ 4.931 2.292
+ 4.969 1.422
+ 5.002 1.436
+ 5.068 0.0
--- /dev/null
+; Sachsen Feuerwerk / WECO Feuerwerk C6-0, C6-3, C6-5
+; Created by Sampo Niskanen
+; Data taken from:
+; http://www.raketenmodellbautechnik.de/produkte/Motoren/SF-Motoren.pdf
+C6 18 70 0-3-5 0.01248 0.022 SF
+ 0.096 0.579
+ 0.152 2.441
+ 0.184 4.372
+ 0.312 11.642
+ 0.354 11.589
+ 0.395 6.269
+ 0.441 5.127
+ 0.537 4.091
+ 0.643 3.529
+ 0.983 3.301
+ 1.162 3.249
+ 1.217 3.02
+ 1.882 3.652
+ 1.919 1.141
+ 1.997 0.0
--- /dev/null
+; Sachsen Feuerwerk / WECO Feuerwerk D7-0, D7-3
+; Created by Sampo Niskanen
+; Data taken from:
+; http://www.raketenmodellbautechnik.de/produkte/Motoren/SF-Motoren.pdf
+D7 25 70 0-3 0.019 0.043 SF
+ 0.079 1.625
+ 0.179 6.979
+ 0.326 18.992
+ 0.355 19.407
+ 0.372 20.426
+ 0.422 20.331
+ 0.48 14.085
+ 0.538 11.536
+ 0.68 9.815
+ 0.96 7.839
+ 1.34 8.253
+ 1.461 2.167
+ 1.582 0.0
--- /dev/null
+;\r
+;\r
+;\r
+G125 38.0 408.00 1 0.15800 0.53700 SRS\r
+ 0.01 346.87 \r
+ 0.04 325.72 \r
+ 0.07 324.57 \r
+ 0.08 415.57 \r
+ 0.09 219.61 \r
+ 0.13 194.84 \r
+ 0.20 177.77 \r
+ 0.40 157.13 \r
+ 0.60 131.89 \r
+ 0.80 88.31 \r
+ 1.00 42.44 \r
+ 1.09 14.64 \r
+ 1.20 0.00 \r
--- /dev/null
+;\r
+;Sky Ripper Systems 29/75 G63\r
+G63 29 304.80 0 .06500 .23600 SRS\r
+ 0.01 121.91 \r
+ 0.03 142.66 \r
+ 0.07 156.57 \r
+ 0.09 133.60 \r
+ 0.13 100.62 \r
+ 0.18 114.82 \r
+ 0.20 105.21 \r
+ 0.27 104.91 \r
+ 0.35 93.38 \r
+ 0.41 83.85 \r
+ 0.50 67.95 \r
+ 0.65 61.99 \r
+ 0.83 61.99 \r
+ 0.93 40.14 \r
+ 1.09 17.09 \r
+ 1.18 13.78 \r
+ 1.29 5.56 \r
+ 1.30 0.00 \r
--- /dev/null
+;\r
+;\r
+;Sky Ripper Systems 29/125 G69\r
+G69 29 406.40 0 .10700 .33300 SRS\r
+ 0.01 99.80 \r
+ 0.04 137.51 \r
+ 0.07 103.01 \r
+ 0.13 94.13 \r
+ 0.30 80.09 \r
+ 0.49 72.20 \r
+ 0.57 70.72 \r
+ 0.65 71.96 \r
+ 0.74 80.83 \r
+ 0.81 81.81 \r
+ 0.94 73.43 \r
+ 1.01 74.42 \r
+ 1.06 85.02 \r
+ 1.11 83.78 \r
+ 1.19 62.10 \r
+ 1.24 60.62 \r
+ 1.32 65.30 \r
+ 1.37 64.81 \r
+ 1.43 52.98 \r
+ 1.49 48.55 \r
+ 1.55 44.85 \r
+ 1.64 28.59 \r
+ 1.85 17.74 \r
+ 1.99 14.29 \r
+ 2.00 0.00 \r
--- /dev/null
+;\r
+; Sky Ripper Systems 38mm hybrid motors\r
+; prepared for SRS by Andrew MacMillen NAR 77472 8/10/04\r
+;\r
+; based on TMT thrust data & cert docs\r
+; NOX weight calc'd at 90 deg. F; 0.5469 gm/cc; 984 psi\r
+; compiled with RockSim6 EngEdit\r
+;\r
+;\r
+; SRS H124 38/220 PVC\r
+H124_pvc 38.0 508.00 0 0.14200 0.66400 SRS\r
+ 0.01 198.24 \r
+ 0.02 300.84 \r
+ 0.03 314.34 \r
+ 0.03 316.00 \r
+ 0.04 305.39 \r
+ 0.08 280.35 \r
+ 0.11 221.43 \r
+ 0.69 168.38 \r
+ 0.78 180.85 \r
+ 0.81 158.64 \r
+ 0.84 167.59 \r
+ 0.89 152.32 \r
+ 0.92 120.70 \r
+ 0.95 123.55 \r
+ 1.06 82.23 \r
+ 1.35 48.36 \r
+ 1.55 25.63 \r
+ 1.60 21.88 \r
+ 1.62 23.66 \r
+ 1.66 18.58 \r
+ 1.67 12.46 \r
+ 1.68 0.00 \r
+;\r
--- /dev/null
+;\r
+; Sky Ripper Systems 38mm hybrid motors\r
+; prepared by Andrew MacMillen NAR 77472 8/10/04\r
+;\r
+; based on TMT thrust data & cert docs\r
+; NOX weight calc'd at 90 deg. F; 0.5469 gm/cc; 984 psi\r
+; compiled with RockSim6 EngEdit\r
+;\r
+;\r
+; SRS H155 38/220 PP\r
+H155_pp 38.0 508.00 0 0.14200 0.66400 SRS\r
+ 0.03 308.75 \r
+ 0.05 362.22 \r
+ 0.09 252.41 \r
+ 1.02 102.11 \r
+ 1.08 119.98 \r
+ 1.45 33.45 \r
+ 1.66 19.06 \r
+ 1.68 12.59 \r
+ 1.69 0.00 \r
+;\r
--- /dev/null
+;\r
+;Sky Ripper Systems 29/185 H78\r
+H78 29 520.70 0 .15800 .41800 SRS\r
+ 0.01 138.21 \r
+ 0.08 150.10 \r
+ 0.12 142.67 \r
+ 0.15 132.27 \r
+ 0.22 130.49 \r
+ 0.29 90.30 \r
+ 0.34 88.27 \r
+ 0.40 86.81 \r
+ 0.61 86.52 \r
+ 0.72 81.59 \r
+ 0.76 71.72 \r
+ 0.86 64.46 \r
+ 1.01 63.30 \r
+ 1.18 62.42 \r
+ 1.28 60.39 \r
+ 1.50 57.49 \r
+ 1.80 58.36 \r
+ 1.89 59.23 \r
+ 2.01 55.46 \r
+ 2.21 36.29 \r
+ 2.36 22.94 \r
+ 2.54 13.36 \r
+ 2.72 9.58 \r
+ 2.75 0.00 \r
--- /dev/null
+;\r
+; Sky Ripper Systems 38mm hybrid motors\r
+; prepared by Andrew MacMillen NAR 77472 8/10/04\r
+;\r
+; based on TMT thrust data & cert docs\r
+; NOX weight calc'd at 90 deg. F; 0.5469 gm/cc; 984 psi\r
+; compiled with RockSim6 EngEdit\r
+;\r
+;\r
+; SRS I117 38/580 PVC\r
+I117_pvc 38.0 914.00 0 0.37000 1.13300 SRS\r
+ 0.01 203.36 \r
+ 0.02 339.95 \r
+ 0.02 358.77 \r
+ 0.03 367.94 \r
+ 0.03 361.47 \r
+ 0.06 277.40 \r
+ 0.08 277.11 \r
+ 0.15 250.99 \r
+ 0.27 256.14 \r
+ 0.30 248.56 \r
+ 0.42 194.78 \r
+ 0.47 222.59 \r
+ 1.35 174.82 \r
+ 1.39 184.03 \r
+ 1.79 150.12 \r
+ 2.12 156.59 \r
+ 3.42 54.16 \r
+ 3.80 40.00 \r
+ 4.20 17.22 \r
+ 4.23 0.00 \r
+;\r
--- /dev/null
+;\r
+; Sky Ripper Systems 38mm hybrid motors\r
+; prepared by Andrew MacMillen NAR 77472 8/10/04\r
+;\r
+; based on TMT thrust data & cert docs\r
+; NOX weight calc'd at 90 deg. F; 0.5469 gm/cc; 984 psi\r
+; compiled with RockSim6 EngEdit\r
+;\r
+;\r
+; SRS I119 38/400 PVC\r
+I119_pvc 38.0 711.00 0 0.26400 0.96700 SRS\r
+ 0.00 192.14 \r
+ 0.01 262.20 \r
+ 0.02 341.53 \r
+ 0.03 338.42 \r
+ 0.06 221.25 \r
+ 0.07 195.93 \r
+ 0.11 189.23 \r
+ 0.15 202.20 \r
+ 0.18 233.36 \r
+ 0.47 230.26 \r
+ 0.99 200.90 \r
+ 1.18 165.36 \r
+ 1.27 172.11 \r
+ 1.48 162.61 \r
+ 2.18 74.18 \r
+ 2.92 24.69 \r
+ 2.97 10.98 \r
+ 3.10 10.06 \r
+ 3.12 0.00 \r
+;\r
--- /dev/null
+;\r
+; Sky Ripper Systems 38mm hybrid motors\r
+; prepared by Andrew MacMillen NAR 77472 8/10/04\r
+;\r
+; based on TMT thrust data & cert docs\r
+; NOX weight calc'd at 90 deg. F; 0.5469 gm/cc; 984 psi\r
+; compiled with RockSim6 EngEdit\r
+;\r
+;\r
+; SRS I147 38/400 PP\r
+I147_pp 38.0 711.00 0 0.26400 0.96700 SRS\r
+ 0.00 194.47 \r
+ 0.02 366.71 \r
+ 0.03 390.09 \r
+ 0.03 392.30 \r
+ 0.05 359.02 \r
+ 0.07 217.40 \r
+ 0.10 207.68 \r
+ 0.38 241.52 \r
+ 1.07 183.80 \r
+ 2.33 141.34 \r
+ 2.51 100.04 \r
+ 2.75 71.51 \r
+ 3.32 34.49 \r
+ 3.89 17.18 \r
+ 4.43 7.17 \r
+ 4.45 0.00 \r
+;\r
--- /dev/null
+;\r
+; Sky Ripper Systems 38mm hybrid motors\r
+; prepared by Andrew MacMillen NAR 77472 8/10/04\r
+;\r
+; based on TMT thrust data & cert docs\r
+; NOX weight calc'd at 90 deg. F; 0.5469 gm/cc; 984 psi\r
+; compiled with RockSim6 EngEdit\r
+;\r
+;\r
+; SRS J144 38/580 PP\r
+J144_pp 38.0 914.00 0 0.37000 1.13300 SRS\r
+ 0.01 301.25 \r
+ 0.03 376.44 \r
+ 0.03 376.86 \r
+ 0.08 247.31 \r
+ 0.22 273.45 \r
+ 0.45 263.59 \r
+ 0.79 231.99 \r
+ 1.09 175.69 \r
+ 1.21 185.68 \r
+ 2.05 149.60 \r
+ 2.86 127.64 \r
+ 3.42 69.07 \r
+ 3.88 42.69 \r
+ 4.59 20.49 \r
+ 5.20 10.00 \r
+ 5.80 6.12 \r
+ 5.80 0.00 \r
+;\r
--- /dev/null
+;Sky Ripper Systems 54/830 J261 Gold Insert\r
+J261 54.0 731.50 0 0.56470 1.90250 SRS\r
+ 0.02 1303.39 \r
+ 0.09 903.88 \r
+ 0.21 591.22 \r
+ 0.42 349.57 \r
+ 0.61 304.62 \r
+ 0.81 329.59 \r
+ 1.01 277.16 \r
+ 1.21 294.64 \r
+ 1.39 232.21 \r
+ 1.61 197.26 \r
+ 1.82 255.99 \r
+ 2.00 274.66 \r
+ 2.20 269.67 \r
+ 2.41 189.77 \r
+ 2.61 294.64 \r
+ 2.82 212.24 \r
+ 3.01 254.69 \r
+ 3.21 225.29 \r
+ 3.41 194.76 \r
+ 3.59 156.32 \r
+ 3.74 229.72 \r
+ 3.85 237.71 \r
+ 3.90 156.32 \r
+ 4.00 112.76 \r
+ 4.20 140.19 \r
+ 4.40 192.26 \r
+ 4.48 106.66 \r
+ 4.61 54.86 \r
+ 4.76 33.52 \r
+ 4.95 0.00 \r
+;\r
--- /dev/null
+;Sky Ripper Systems 54/550 J263 Gold Insert\r
+J263 54.0 606.50 0 0.37420 1.59830 SRS\r
+ 0.01 1359.46 \r
+ 0.09 794.32 \r
+ 0.15 559.93 \r
+ 0.21 419.30 \r
+ 0.40 296.89 \r
+ 0.61 286.48 \r
+ 0.81 265.64 \r
+ 1.01 244.81 \r
+ 1.20 247.41 \r
+ 1.40 244.81 \r
+ 1.60 231.79 \r
+ 1.81 200.53 \r
+ 2.01 205.74 \r
+ 2.21 205.74 \r
+ 2.41 195.32 \r
+ 2.60 174.49 \r
+ 2.80 166.68 \r
+ 2.91 161.47 \r
+ 3.00 138.03 \r
+ 3.08 98.96 \r
+ 3.21 62.50 \r
+ 3.41 28.74 \r
+ 3.60 0.00 \r
+;\r
--- /dev/null
+;Sky Ripper Systems 54/830 J337 Black Insert\r
+J337 54.0 731.50 0 0.56470 1.90250 SRS\r
+ 0.00 8.88 \r
+ 0.01 1521.03 \r
+ 0.10 585.29 \r
+ 0.20 651.28 \r
+ 0.41 589.45 \r
+ 0.50 548.23 \r
+ 0.60 539.99 \r
+ 0.99 461.67 \r
+ 1.40 416.33 \r
+ 1.60 391.59 \r
+ 1.80 387.47 \r
+ 1.91 364.90 \r
+ 2.01 239.43 \r
+ 2.20 171.02 \r
+ 2.40 119.72 \r
+ 2.60 72.68 \r
+ 2.80 42.76 \r
+ 3.00 21.68 \r
+ 3.10 0.00 \r
+;\r
--- /dev/null
+;Sky Ripper Systems 54/550 J348 Black Insert\r
+J348 54.0 606.50 0 0.37420 1.59830 SRS\r
+ 0.00 8.88 \r
+ 0.02 451.80 \r
+ 0.13 557.60 \r
+ 0.14 1203.84 \r
+ 0.20 617.65 \r
+ 0.40 549.02 \r
+ 0.60 494.69 \r
+ 0.79 406.05 \r
+ 1.01 354.57 \r
+ 1.20 334.56 \r
+ 1.31 303.10 \r
+ 1.40 237.34 \r
+ 1.60 148.69 \r
+ 1.80 82.92 \r
+ 2.00 37.17 \r
+ 2.20 0.00 \r
+;\r
--- /dev/null
+;Sky Ripper Systems 54/1130 K257 Gold Insert\r
+K257 54.0 911.40 0 0.76880 2.31070 SRS\r
+ 0.07 1133.08 \r
+ 0.20 529.98 \r
+ 0.40 333.69 \r
+ 0.59 363.13 \r
+ 0.80 347.18 \r
+ 0.98 366.40 \r
+ 1.20 356.59 \r
+ 1.40 310.79 \r
+ 1.60 274.80 \r
+ 1.81 337.71 \r
+ 2.00 356.59 \r
+ 2.20 294.43 \r
+ 2.40 314.06 \r
+ 2.60 294.43 \r
+ 2.81 333.69 \r
+ 3.00 340.23 \r
+ 3.22 319.78 \r
+ 3.41 292.68 \r
+ 3.59 249.32 \r
+ 3.79 278.07 \r
+ 4.00 243.90 \r
+ 4.21 281.35 \r
+ 4.41 235.54 \r
+ 4.60 251.90 \r
+ 4.81 211.38 \r
+ 5.01 222.22 \r
+ 5.20 189.70 \r
+ 5.41 153.23 \r
+ 5.60 81.40 \r
+ 5.80 23.94 \r
+ 6.10 0.00 \r
+;\r
--- /dev/null
+;Sky Ripper Systems 54/1130 K347 Black Insert\r
+K347 54.0 911.40 0 0.76880 2.31070 SRS\r
+ 0.00 8.88 \r
+ 0.03 1474.51 \r
+ 0.11 972.20 \r
+ 0.19 737.25 \r
+ 0.40 697.22 \r
+ 0.61 673.87 \r
+ 0.81 653.85 \r
+ 1.01 600.40 \r
+ 1.20 550.44 \r
+ 1.40 530.42 \r
+ 1.60 533.76 \r
+ 1.80 517.08 \r
+ 1.89 396.98 \r
+ 1.95 493.19 \r
+ 2.06 393.65 \r
+ 2.13 443.69 \r
+ 2.30 365.38 \r
+ 2.30 292.31 \r
+ 2.40 257.92 \r
+ 2.60 184.84 \r
+ 2.79 124.66 \r
+ 3.00 81.67 \r
+ 3.21 51.58 \r
+ 3.41 21.49 \r
+ 3.58 0.00 \r
+;\r
--- /dev/null
+;\r
+; West Coast Hybrid motor\r
+; prepared for WestCoast/Scott Harrison by\r
+; Andrew MacMillen NAR 77472 9/13/02\r
+;\r
+; based on CAR thrust curves & cert letters\r
+; since the cert letters and test curves don't agree\r
+; the data is hand entered data points from CAR thrust curves\r
+; NOX weight calc'd at 90 deg. F; 0.5469 gm/cc; 984 psi\r
+; compiled with RockSim5 EngEdit\r
+;\r
+; within 2-3 percent for total impulse, peak and average thrust\r
+; accurate thrust profile\r
+;\r
+; NOTE: NOT WCH, CAR, TMT OR NAR APPROVED\r
+;\r
+; West Coast Hybrids I110\r
+;\r
+I110H 38.0 606.00 0 0.32700 0.82400 WCH\r
+ 0.05 240.00 \r
+ 0.38 200.00 \r
+ 0.76 182.00 \r
+ 1.14 160.00 \r
+ 1.52 142.00 \r
+ 1.90 125.00 \r
+ 2.23 113.00 \r
+ 2.66 71.00 \r
+ 3.04 44.00 \r
+ 3.42 29.00 \r
+ 3.78 22.00 \r
+ 4.00 0.00 \r
+;\r
--- /dev/null
+package altimeter;
+
+import gnu.io.CommPortIdentifier;
+import gnu.io.PortInUseException;
+import gnu.io.SerialPort;
+import gnu.io.UnsupportedCommOperationException;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PrintStream;
+import java.nio.charset.Charset;
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.Enumeration;
+import java.util.TimeZone;
+
+/**
+ * Class to interface the PerfectFlite Alt15K/WD altimeter.
+ *
+ * Also includes a main method that retrieves all flight profiles and saves them to files.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class Alt15K {
+ public static final int TIMEOUT = 500;
+ public static final int RWDELAY = 5;
+
+ private static final boolean DEBUG = false;
+
+ private static final Charset CHARSET = Charset.forName("ISO-8859-1");
+
+ private final CommPortIdentifier portID;
+ private SerialPort port = null;
+ private InputStream is = null;
+ private OutputStream os = null;
+
+
+
+ @SuppressWarnings("unchecked")
+ public static String[] getNames() {
+ ArrayList<String> list = new ArrayList<String>();;
+
+ Enumeration pids = CommPortIdentifier.getPortIdentifiers();
+
+ while (pids.hasMoreElements()) {
+ CommPortIdentifier pid = (CommPortIdentifier) pids.nextElement();
+
+ if (pid.getPortType() == CommPortIdentifier.PORT_SERIAL)
+ list.add(pid.getName());
+ }
+ return list.toArray(new String[0]);
+ }
+
+
+
+ @SuppressWarnings("unchecked")
+ public Alt15K(String name) throws IOException {
+ CommPortIdentifier pID = null;
+
+ Enumeration portIdentifiers = CommPortIdentifier.getPortIdentifiers();
+ while (portIdentifiers.hasMoreElements()) {
+ CommPortIdentifier pid = (CommPortIdentifier) portIdentifiers.nextElement();
+
+ if(pid.getPortType() == CommPortIdentifier.PORT_SERIAL &&
+ pid.getName().equals(name)) {
+ pID = pid;
+ break;
+ }
+ }
+
+ if (pID==null) {
+ throw new IOException("Port '"+name+"' not found.");
+ }
+ this.portID = pID;
+ }
+
+
+ /**
+ * Get altimeter flight data. The flight profile is chosen by the parameter n,
+ * 0 = latest flight, 1 = second latest, etc.
+ *
+ * @param n Which flight profile to use (0=newest, 1=second newest, etc)
+ * @return The altimeter flight data
+ * @throws IOException in case of IOException
+ * @throws PortInUseException in case of PortInUseException
+ */
+ public AltData getData(int n) throws IOException, PortInUseException {
+ AltData alt = new AltData();
+ ArrayList<Integer> data = new ArrayList<Integer>();
+ byte[] buf;
+ byte[] buf2 = new byte[0];
+ boolean identical = false; // Whether identical lines have been read
+
+ if (DEBUG)
+ System.out.println(" Retrieving altimeter data n="+n);
+
+ try {
+ open();
+
+ // Get version and position data
+ byte[] ver = getVersionData();
+ alt.setVersion(new byte[] { ver[0],ver[1] });
+
+ // Calculate the position requested
+ if (n > 2)
+ n = 2;
+ int position = ver[2] - n;
+ while (position < 0)
+ position += 3;
+
+ if (DEBUG)
+ System.out.println(" Requesting data from position "+position);
+
+ // Request the data
+ write("D");
+ write((byte)position);
+ write("PS");
+
+ sleep();
+
+ // Read preliminary data
+ buf = read(4);
+ int msl_level = combine(buf[0],buf[1]);
+ int datacount = combine(buf[2],buf[3]);
+
+ if (DEBUG)
+ System.out.println(" Preliminary data msl="+msl_level+" count="+datacount);
+
+ alt.setMslLevel(msl_level-6000);
+ alt.setDataSamples(datacount);
+
+ if (DEBUG)
+ System.out.println(" Retrieving "+datacount+" samples");
+
+ long t = System.currentTimeMillis();
+
+ int count = 0;
+ while (count < datacount) {
+ sleep();
+ write("G");
+ sleep();
+ buf = read(17);
+
+ if (buf.length == 17) {
+ // Checksum = sum of all bytes + 1
+ // (signedness does not change the result)
+ byte checksum = 1;
+ for (int i=0; i<16; i++)
+ checksum += buf[i];
+ if (checksum != buf[16]) {
+ printBytes("ERROR: Checksum fail on data (computed="+checksum+
+ " orig="+buf[16]+")",buf);
+ System.out.println("Ignoring error");
+ }
+ } else {
+ System.err.println("ERROR: Only "+buf.length+" bytes read, should be 17");
+ }
+
+ for (int i=0; i<buf.length-1; i+=2) {
+ data.add(combine(buf[i],buf[i+1]));
+ count++;
+ }
+
+ /*
+ * Check whether the data is identical to the previous data batch. If reading
+ * too fast, the data seems to become duplicated in the transfer. We need to check
+ * whether this has happened by attempting to read more data than is normally
+ * available.
+ */
+ int c, l=Math.min(buf.length, buf2.length);
+ for (c=0; c<l; c++) {
+ if (buf[c] != buf2[c])
+ break;
+ }
+ if (c==l && buf.length == buf2.length)
+ identical = true;
+ buf2 = buf.clone();
+ }
+
+ if (DEBUG)
+ System.out.println(" Retrieved "+data.size()+" samples in "+
+ (System.currentTimeMillis()-t)+" ms");
+
+
+ // In case of identical lines, check for more data. This would mean that the
+ // transfer was corrupted.
+ if (identical) {
+ System.err.println("WARNING: Duplicate data detected, possible error");
+ }
+
+ // Test for more data
+ if (DEBUG)
+ System.out.println(" Testing for more data");
+ sleep();
+ write("G");
+ sleep();
+ buf = read(17);
+ if (buf.length > 0) {
+ System.err.println("ERROR: Data available after transfer! (length="+buf.length+")");
+ }
+
+
+
+
+
+
+ // Create an int[] array and set it
+ int[] d = new int[data.size()];
+ for (int i=0; i<d.length; i++)
+ d[i] = data.get(i);
+ alt.setData(d);
+
+ // Catch all exceptions, close the port and re-throw the exception
+ } catch (PortInUseException e) {
+ close();
+ throw e;
+ } catch (IOException e) {
+ close();
+ throw e;
+ } catch (UnsupportedCommOperationException e) {
+ close();
+ throw new RuntimeException("Required function of RxTx library not supported",e);
+ } catch (RuntimeException e) {
+ // Catch-all for all other types of exceptions
+ close();
+ throw e;
+ }
+
+ close();
+ return alt;
+ }
+
+
+
+
+ private byte[] getVersionData() throws PortInUseException, IOException,
+ UnsupportedCommOperationException {
+ byte[] ver = new byte[3];
+ byte[] buf;
+
+ if (DEBUG)
+ System.out.println(" Retrieving altimeter version information");
+
+ // Signal to altimeter we are here
+ write((byte)0);
+ sleep(15); // Sleep for 15ms, data is incoming at 10 samples/sec
+
+ // Get altimeter version, skip zeros
+ write("PV");
+ sleep();
+ buf = readSkipZero(2);
+ sleep();
+ if (buf.length != 2) {
+ close();
+ throw new IOException("Communication with altimeter failed.");
+ }
+ ver[0] = buf[0];
+ ver[1] = buf[1];
+
+ // Get position of newest data
+ write("M");
+ sleep();
+ buf = read(1);
+ if (buf.length != 1) {
+ close();
+ throw new IOException("Communication with altimeter failed.");
+ }
+ ver[2] = buf[0];
+
+ if (DEBUG)
+ System.out.println(" Received version info "+ver[0]+"."+ver[1]+", position "+ver[2]);
+
+ return ver;
+ }
+
+
+ /**
+ * Delay the communication by a small delay (RWDELAY ms).
+ */
+ private void sleep() {
+ sleep(RWDELAY);
+ }
+
+ /**
+ * Sleep for the given amount of milliseconds.
+ */
+ private void sleep(int n) {
+ try {
+ Thread.sleep(n);
+ } catch (InterruptedException ignore) { }
+ }
+
+
+ private void open()
+ throws PortInUseException, IOException, UnsupportedCommOperationException {
+ if (port != null) {
+ System.err.println("ERROR: open() called with port="+port);
+ Thread.dumpStack();
+ close();
+ }
+
+ if (DEBUG) {
+ System.out.println(" Opening port...");
+ }
+
+ port = (SerialPort)portID.open("OpenRocket",1000);
+
+ port.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
+ SerialPort.PARITY_NONE);
+
+ port.setInputBufferSize(1);
+ port.setOutputBufferSize(1);
+
+ port.enableReceiveTimeout(TIMEOUT);
+
+ is = port.getInputStream();
+ os = port.getOutputStream();
+ }
+
+
+ private byte[] readSkipZero(int n) throws IOException, UnsupportedCommOperationException {
+ long t = System.currentTimeMillis() + TIMEOUT*2;
+
+ if (DEBUG) {
+ System.out.println(" readSkipZero "+n+" bytes");
+ }
+
+ while (System.currentTimeMillis() < t) {
+ byte[] buf = read(n);
+ if (DEBUG)
+ printBytes(" Received",buf);
+
+ if (buf.length == 0) // No data available
+ return buf;
+
+ // Skip zeros
+ int i;
+ for (i=0; i<buf.length; i++)
+ if (buf[i] != 0)
+ break;
+
+ if (i==0) // No zeros to skip
+ return buf;
+
+ if (i < buf.length) {
+ // Partially read
+ int count = buf.length-i; // No. of data bytes
+ byte[] array = new byte[n];
+ System.arraycopy(buf, i, array, 0, count);
+ buf = read(n-count);
+ if (DEBUG)
+ printBytes(" Received (partial)",buf);
+ System.arraycopy(buf, 0, array, count, buf.length);
+
+ if (DEBUG)
+ printBytes(" Returning",array);
+ return array;
+ }
+ }
+
+ if (DEBUG)
+ System.out.println(" No data read, returning empty");
+ return new byte[0]; // no data, only zeros
+ }
+
+
+ private byte[] read(int n) throws IOException, UnsupportedCommOperationException {
+ byte[] bytes = new byte[n];
+
+ port.enableReceiveThreshold(n);
+
+ long t = System.currentTimeMillis() + TIMEOUT;
+ int count = 0;
+
+ if (DEBUG)
+ System.out.println(" Reading "+n+" bytes");
+
+ while (count < n && System.currentTimeMillis() < t) {
+ byte[] buf = new byte[n-count];
+ int c = is.read(buf);
+ System.arraycopy(buf, 0, bytes, count, c);
+ count += c;
+ }
+
+ byte[] array = new byte[count];
+ System.arraycopy(bytes, 0, array, 0, count);
+
+ if (DEBUG)
+ printBytes(" Returning",array);
+
+ return array;
+ }
+
+ private void write(String s) throws IOException {
+ write(s.getBytes(CHARSET));
+ }
+
+ private void write(byte ... bytes) throws IOException {
+ if (DEBUG)
+ printBytes(" Writing",bytes);
+ os.write(bytes);
+ }
+
+ private void close() {
+ if (DEBUG)
+ System.out.println(" Closing port");
+
+ SerialPort p = port;
+ port = null;
+ is = null;
+ os = null;
+ if (p != null)
+ p.close();
+ }
+
+
+
+
+
+ public static void main(String[] arg) {
+
+ if (arg.length != 1) {
+ System.err.println("Usage: java Alt15K <basename>");
+ System.err.println("Files will be saved <basename>-old.log, -med and -new");
+ return;
+ }
+
+
+ String device = null;
+ String[] devices = Alt15K.getNames();
+ for (int i=0; i<devices.length; i++) {
+ if (devices[i].matches(".*USB.*")) {
+ device = devices[i];
+ break;
+ }
+ }
+ if (device == null) {
+ System.out.println("Device not found.");
+ return;
+ }
+
+
+ System.out.println("Selected device "+device);
+
+ AltData alt = null;
+ String file;
+ try {
+ Alt15K p = new Alt15K(device);
+
+ System.out.println("Retrieving newest data...");
+ alt = p.getData(0);
+ System.out.println("Apogee at "+alt.getApogee()+" feet");
+
+ file = arg[0]+"-new.log";
+ System.out.println("Saving data to "+file+"...");
+ savefile(file,alt);
+
+
+ System.out.println("Retrieving medium data...");
+ alt = p.getData(1);
+ System.out.println("Apogee at "+alt.getApogee()+" feet");
+
+ file = arg[0]+"-med.log";
+ System.out.println("Saving data to "+file+"...");
+ savefile(file,alt);
+
+
+ System.out.println("Retrieving oldest data...");
+ alt = p.getData(2);
+ System.out.println("Apogee at "+alt.getApogee()+" feet");
+
+ file = arg[0]+"-old.log";
+ System.out.println("Saving data to "+file+"...");
+ savefile(file,alt);
+
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (PortInUseException e) {
+ e.printStackTrace();
+ }
+
+// System.out.println(alt);
+// alt.printData();
+
+ }
+
+
+ static private void savefile(String file, AltData data) throws FileNotFoundException {
+
+ PrintStream output = new PrintStream(file);
+
+ // WTF is this so difficult?!?
+ DateFormat fmt = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.MEDIUM);
+ TimeZone tz=TimeZone.getTimeZone("GMT+3");
+ fmt.setTimeZone(tz);
+
+ output.println("# Alt15K data, file "+file);
+ output.println("# Data retrieved at: "+fmt.format(new Date()));
+ output.println("# Values are in feet above launch level");
+ output.println("# ");
+ output.println("# Apogee = "+data.getApogee());
+ output.println("# MSL level = "+data.getMslLevel());
+ output.println("# Data count = "+data.getDataSamples());
+
+ byte[] b = data.getVersion();
+ String s="";
+ for (int i=0; i<b.length; i++) {
+ if (s.equals(""))
+ s = ""+((int)b[i]);
+ else
+ s = s+"."+((int)b[i]);
+ }
+ output.println("# Altimeter version = " + s);
+
+ int[] values = data.getData();
+ for (int i=0; i < values.length; i++) {
+ output.println(""+values[i]);
+ }
+
+ output.close();
+ }
+
+
+ static private void printBytes(String str, byte[] b) {
+ printBytes(str, b,b.length);
+ }
+
+ static private void printBytes(String str, byte[] b, int n) {
+ String s;
+ s = str+" "+n+" bytes:";
+ for (int i=0; i<n; i++) {
+ s += " "+unsign(b[i]);
+ }
+ System.out.println(s);
+ }
+
+ static private int unsign(byte b) {
+ if (b >= 0)
+ return b;
+ else
+ return 256 + b;
+ }
+
+ @SuppressWarnings("unused")
+ static private int combine(int a, int b) {
+ return 256*a + b;
+ }
+
+ static private int combine(byte a, byte b) {
+ int val = 256*unsign(a)+unsign(b);
+ if (val <= 32767)
+ return val;
+ else
+ return val-65536;
+
+ }
+
+}
--- /dev/null
+package altimeter;
+
+public class AltData {
+
+ private int mslLevel = 0;
+ private int samples = 0;
+ private int[] data = null;
+ private byte[] version = null;
+
+
+ public void setMslLevel(int msl) {
+ mslLevel = msl;
+ }
+ public int getMslLevel() {
+ return mslLevel;
+ }
+
+ public void setDataSamples(int s) {
+ samples = s;
+ }
+ public int getDataSamples() {
+ return samples;
+ }
+
+ public void setVersion(byte[] v) {
+ if (v==null)
+ version = null;
+ else
+ version = v.clone();
+ }
+ public byte[] getVersion() {
+ if (version == null)
+ return null;
+ return version.clone();
+ }
+
+ public void setData(int[] data) {
+ if (data==null)
+ this.data = null;
+ else
+ this.data = data.clone();
+ }
+ public int[] getData() {
+ if (data == null)
+ return null;
+ return data.clone();
+ }
+
+ public int getApogee() {
+ if (data == null || data.length==0)
+ return 0;
+ int max = Integer.MIN_VALUE;
+ for (int i=0; i<data.length; i++) {
+ if (data[i] > max)
+ max = data[i];
+ }
+ return max;
+ }
+
+ @Override
+ public String toString() {
+ String s = "AltData(";
+ s += "MSL:"+getMslLevel()+",";
+ s += "Apogee:"+getApogee()+",";
+ s += "Samples:"+getDataSamples();
+ s += ")";
+ return s;
+ }
+
+ public void printData() {
+ System.out.println(toString()+":");
+ for (int i=0; i<data.length; i+=8) {
+ String s = " "+i+":";
+ for (int j=0; j<8 && (i+j)<data.length; j++) {
+ s += " "+data[i+j];
+ }
+ System.out.println(s);
+ }
+ }
+
+}
--- /dev/null
+package altimeter;
+
+import gnu.io.CommPortIdentifier;
+import gnu.io.PortInUseException;
+import gnu.io.SerialPort;
+import gnu.io.UnsupportedCommOperationException;
+
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Enumeration;
+
+/**
+ * Class to interface the PerfectFlite Alt15K/WD altimeter.
+ *
+ * Also includes a main method that retrieves all flight profiles and saves them to files.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class RotationLogger {
+ private static final boolean DEBUG = false;
+
+ private static final int BYTES = 65536;
+
+
+ private final CommPortIdentifier portID;
+ private SerialPort port = null;
+ private InputStream is = null;
+ private OutputStream os = null;
+
+
+
+ @SuppressWarnings("unchecked")
+ public static String[] getNames() {
+ ArrayList<String> list = new ArrayList<String>();;
+
+ Enumeration pids = CommPortIdentifier.getPortIdentifiers();
+
+ while (pids.hasMoreElements()) {
+ CommPortIdentifier pid = (CommPortIdentifier) pids.nextElement();
+
+ if (pid.getPortType() == CommPortIdentifier.PORT_SERIAL)
+ list.add(pid.getName());
+ }
+ return list.toArray(new String[0]);
+ }
+
+
+
+
+
+ @SuppressWarnings("unchecked")
+ public RotationLogger(String name) throws IOException {
+ CommPortIdentifier portID = null;
+
+ Enumeration portIdentifiers = CommPortIdentifier.getPortIdentifiers();
+ while (portIdentifiers.hasMoreElements()) {
+ CommPortIdentifier pid = (CommPortIdentifier) portIdentifiers.nextElement();
+
+ if(pid.getPortType() == CommPortIdentifier.PORT_SERIAL &&
+ pid.getName().equals(name)) {
+ portID = pid;
+ break;
+ }
+ }
+
+ if (portID==null) {
+ throw new IOException("Port '"+name+"' not found.");
+ }
+ this.portID = portID;
+ }
+
+
+
+
+
+
+ public void readData() throws IOException, PortInUseException {
+ int c;
+
+ int[] data = new int[BYTES];
+
+ FileOutputStream rawdump = null;
+
+
+ try {
+ open();
+
+ System.err.println("Sending dump mode command...");
+
+ for (int i=0; i<16; i++) {
+ os.write('D');
+ try {
+ Thread.sleep(10);
+ } catch (InterruptedException ignore) { }
+ }
+
+ System.err.println("Waiting for response...");
+ while (true) {
+ c = is.read();
+ if (c == 'K') {
+ break;
+ } else {
+ System.err.printf("Received spurious c=%d\n",c);
+ }
+ }
+
+ System.err.println("Received response.");
+
+
+
+ System.err.println("Opening 'rawdump'...");
+ rawdump = new FileOutputStream("rawdump");
+
+
+
+ System.err.println("Performing dump...");
+
+ os.write('A');
+
+ byte[] buffer = new byte[1024];
+ int printCount = 0;
+ for (int count=0; count < BYTES; ) {
+ if ((BYTES-count) < buffer.length) {
+ buffer = new byte[BYTES-count];
+ }
+
+ int n = is.read(buffer);
+ if (n < 0) {
+ System.err.println("Error condition, n="+n);
+ return;
+ }
+
+ rawdump.write(buffer, 0, n);
+
+ for (int i=0; i<n; i++) {
+ data[count+i] = unsign(buffer[i]);
+ }
+ count += n;
+ if (count - printCount > 1024) {
+ System.err.println("Read "+count+" bytes...");
+ printCount = count;
+ }
+ }
+
+
+ System.err.println("Verifying checksum...");
+ int reported = is.read();
+
+ byte computed = 0;
+ for (int i=0; i < data.length; i++) {
+ computed += data[i];
+ }
+ if (computed == reported) {
+ System.err.println("Checksum ok ("+computed+")");
+ } else {
+ System.err.println("Error in checksum, computed="+computed+
+ " reported="+reported);
+ }
+
+ System.err.println("Communication done.");
+
+ } catch (UnsupportedCommOperationException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } finally {
+ close();
+ if (rawdump != null)
+ rawdump.close();
+ }
+
+ convertData(data);
+
+ }
+
+
+
+ //////////// Data interpretation //////////////
+
+
+ private static void convertData(int[] data) {
+
+ System.err.println("Converting data...");
+
+ int lastBuffer = data[0xffff];
+ if (lastBuffer < 0 || lastBuffer > 3) {
+ System.err.println("Illegal last accessed buffer: "+lastBuffer);
+ return;
+ }
+ System.err.println("Last used buffer: "+lastBuffer);
+
+ for (int i=4; i>0; i--) {
+ int n = (lastBuffer + i) % 4;
+ int bufNumber = 4-i;
+
+ convertBuffer(data, n * (BYTES/4), bufNumber);
+ }
+
+ }
+
+
+ private static void convertBuffer(int[] data, int position, int bufNumber) {
+ int startPosition;
+
+ startPosition = data[position + 0xfd] << 8 + data[position+0xfe];
+
+ // 50 samples per 128 bytes
+ int startTime = (startPosition -position) * 50 / 128;
+
+ System.err.println(" Buffer "+ bufNumber + " (at position "+position+")...");
+ System.err.println(" Start position "+startPosition+" time "+startTime);
+
+ System.out.println("# Buffer "+bufNumber);
+ System.out.println("# Start position t="+startTime);
+
+
+ int t = 0;
+ for (int page = 0; page < 128; page++) {
+ int pageStart = position + page * 128;
+
+ if (pageStart == startPosition) {
+ System.out.println("# ---clip---");
+ }
+
+ for (int i=0; i<125; i += 5) {
+ int sample1, sample2;
+
+ int start = pageStart + i;
+// System.err.println("page="+page+" i="+i+
+// " position="+position+" pageStart="+pageStart+" start="+start);
+
+ sample1 = (data[start] << 2) + (data[start+1] >> 6);
+ sample2 = ((data[start+1] & 0x3f) << 4) + (data[start+2] >> 4);
+ System.out.printf("%d %4d %4d %4d\n", bufNumber, t, sample1, sample2);
+ t++;
+
+ sample1 = ((data[start+2] & 0x0f) << 6) + (data[start+3] >> 2);
+ sample2 = ((data[start+3] & 3) << 8) + data[start+4];
+ System.out.printf("%d %4d %4d %4d\n", bufNumber, t, sample1, sample2);
+ t++;
+ }
+ }
+ }
+
+
+
+ private void open() throws PortInUseException, IOException,
+ UnsupportedCommOperationException {
+
+ if (port != null) {
+ System.err.println("ERROR: open() called with port="+port);
+ Thread.dumpStack();
+ close();
+ }
+
+ if (DEBUG) {
+ System.err.println(" Opening port...");
+ }
+
+ port = (SerialPort)portID.open("OpenRocket",1000);
+
+ port.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
+ SerialPort.PARITY_NONE);
+
+ port.setInputBufferSize(1);
+ port.setOutputBufferSize(1);
+
+ is = port.getInputStream();
+ os = port.getOutputStream();
+ }
+
+
+ private void close() {
+ if (DEBUG)
+ System.err.println(" Closing port");
+
+ SerialPort p = port;
+ port = null;
+ is = null;
+ if (p != null)
+ p.close();
+ }
+
+
+
+ private static int unsign(byte b) {
+ if (b >= 0)
+ return b;
+ else
+ return 256 + b;
+ }
+
+
+
+
+ public static void main(String[] arg) throws Exception {
+
+ if (arg.length > 2) {
+ System.err.println("Illegal arguments.");
+ return;
+ }
+ if (arg.length == 1) {
+ FileInputStream is = new FileInputStream(arg[0]);
+ byte[] buffer = new byte[BYTES];
+ int n = is.read(buffer);
+ if (n != BYTES) {
+ System.err.println("Could read only "+n+" bytes");
+ return;
+ }
+
+ int[] data = new int[BYTES];
+ for (int i=0; i<BYTES; i++) {
+ data[i] = unsign(buffer[i]);
+ }
+
+ int checksum=0;
+ for (int i=0; i<BYTES; i++) {
+ checksum += data[i];
+ }
+ checksum = checksum%256;
+ System.err.println("Checksum: "+checksum);
+
+ convertData(data);
+ return;
+ }
+
+
+ String device = null;
+ String[] devices = RotationLogger.getNames();
+ for (int i=0; i<devices.length; i++) {
+ if (devices[i].matches(".*USB.*")) {
+ device = devices[i];
+ break;
+ }
+ }
+ if (device == null) {
+ System.err.println("Device not found.");
+ return;
+ }
+
+
+ System.err.println("Selected device "+device);
+
+
+ RotationLogger p = new RotationLogger(device);
+
+ p.readData();
+
+ }
+
+
+}
--- /dev/null
+package altimeter;
+
+import gnu.io.CommPortIdentifier;
+import gnu.io.PortInUseException;
+import gnu.io.SerialPort;
+import gnu.io.UnsupportedCommOperationException;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Enumeration;
+
+/**
+ * Class to interface the PerfectFlite Alt15K/WD altimeter.
+ *
+ * Also includes a main method that retrieves all flight profiles and saves them to files.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class SerialDownload {
+ private static final boolean DEBUG = false;
+
+ private static final int MAGIC = 666;
+
+ private final CommPortIdentifier portID;
+ private SerialPort port = null;
+ private InputStream is = null;
+
+
+
+ @SuppressWarnings("unchecked")
+ public static String[] getNames() {
+ ArrayList<String> list = new ArrayList<String>();;
+
+ Enumeration pids = CommPortIdentifier.getPortIdentifiers();
+
+ while (pids.hasMoreElements()) {
+ CommPortIdentifier pid = (CommPortIdentifier) pids.nextElement();
+
+ if (pid.getPortType() == CommPortIdentifier.PORT_SERIAL)
+ list.add(pid.getName());
+ }
+ return list.toArray(new String[0]);
+ }
+
+
+
+
+
+ @SuppressWarnings("unchecked")
+ public SerialDownload(String name) throws IOException {
+ CommPortIdentifier portID = null;
+
+ Enumeration portIdentifiers = CommPortIdentifier.getPortIdentifiers();
+ while (portIdentifiers.hasMoreElements()) {
+ CommPortIdentifier pid = (CommPortIdentifier) portIdentifiers.nextElement();
+
+ if(pid.getPortType() == CommPortIdentifier.PORT_SERIAL &&
+ pid.getName().equals(name)) {
+ portID = pid;
+ break;
+ }
+ }
+
+ if (portID==null) {
+ throw new IOException("Port '"+name+"' not found.");
+ }
+ this.portID = portID;
+ }
+
+
+
+
+
+
+ public void readData() throws IOException, PortInUseException {
+ long t0 = -1;
+ long t;
+
+ int previous = MAGIC;
+
+
+ try {
+ open();
+
+ System.err.println("Ready to read...");
+ while (true) {
+ int c = is.read();
+ t = System.nanoTime();
+ if (t0 < 0)
+ t0 = t;
+
+ System.out.printf("%10.6f %d\n", ((double)t-t0)/1000000000.0, c);
+
+ if (previous == MAGIC) {
+ previous = c;
+ } else {
+ System.out.printf("# Altitude: %5d\n", previous*256 + c);
+ previous = MAGIC;
+ }
+
+ if (c < 0)
+ break;
+ }
+
+
+ } catch (UnsupportedCommOperationException e) {
+ // TODO Auto-generated catch block
+ e.printStackTrace();
+ } finally {
+ close();
+ }
+ }
+
+
+
+ private void open() throws PortInUseException, IOException,
+ UnsupportedCommOperationException {
+
+ if (port != null) {
+ System.err.println("ERROR: open() called with port="+port);
+ Thread.dumpStack();
+ close();
+ }
+
+ if (DEBUG) {
+ System.err.println(" Opening port...");
+ }
+
+ port = (SerialPort)portID.open("OpenRocket",1000);
+
+ port.setSerialPortParams(9600, SerialPort.DATABITS_8, SerialPort.STOPBITS_1,
+ SerialPort.PARITY_NONE);
+
+ port.setInputBufferSize(1);
+ port.setOutputBufferSize(1);
+
+ is = port.getInputStream();
+ }
+
+
+ private void close() {
+ if (DEBUG)
+ System.err.println(" Closing port");
+
+ SerialPort p = port;
+ port = null;
+ is = null;
+ if (p != null)
+ p.close();
+ }
+
+
+
+
+
+ public static void main(String[] arg) throws Exception {
+
+ String device = null;
+ String[] devices = SerialDownload.getNames();
+ for (int i=0; i<devices.length; i++) {
+ if (devices[i].matches(".*USB.*")) {
+ device = devices[i];
+ break;
+ }
+ }
+ if (device == null) {
+ System.err.println("Device not found.");
+ return;
+ }
+
+
+ System.err.println("Selected device "+device);
+
+
+ SerialDownload p = new SerialDownload(device);
+
+ p.readData();
+
+ }
+
+
+}
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>OpenRocket—Support and contact information</title>
+ <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"/>
+ <meta name="keywords" content="OpenRocket, model rocket, simulator, simulation, rocketry, support, bug report, feature request, contact information"/>
+ <link rel="stylesheet" type="text/css" href="layout.css"/>
+</head>
+
+<body class="page_contact">
+ <!--[if lte IE 6]>
+ <div id="iewarn">
+ You are using a browser that is <strong>8 years old!</strong>
+
+ In Internet-years that is <em>prehistoric!</em><br/>
+ For the sanity of all webmasterkind,
+ <em>please <a href="http://www.mozilla.com/">upgrade</a></em>. It's easy!
+ </div>
+ <![endif]-->
+
+ <h1>Support and contact information for OpenRocket</h1>
+
+ <div class="menucontainer">
+ <div class="menu">
+ <ul>
+ <li>OpenRocket</li>
+ <li><a href="index.html">Home</a></li>
+ <li><a href="features.html">Features</a></li>
+ <li><a href="screenshots.html">Screenshots</a></li>
+ <li><a href="download.html">Download</a></li>
+ <li><a href="documentation.html">Documentation</a></li>
+ <li><a href="contact.html">
+ Mailing lists<br/>
+ Support forums<br/>
+ Contact info</a></li>
+ <li><a href="report.html">
+ Report a bug<br/>
+ Request a feature</a></li>
+ <li><a href="license.html">License</a></li>
+ </ul>
+ <div class="logo">
+ <a href="http://sourceforge.net/projects/openrocket"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=260357&type=12" width="120" height="30" alt="Get OpenRocket at SourceForge.net. Fast, secure and Free Open Source software downloads" /></a>
+ </div>
+ </div>
+ </div>
+
+ <div class="content">
+
+ <h2>Mailing lists</h2>
+
+ <p>If you would like to be notified when new versions of
+ OpenRocket are released, you can join the
+ <a href="https://lists.sourceforge.net/lists/listinfo/openrocket-announce"><tt>OpenRocket-announce</tt> mailing list</a>. The
+ list is moderated and meant only for OpenRocket related
+ announcements.</p>
+
+ <p>When more developers join the project, a development mailing
+ list will be created as well.</p>
+
+ <p><strong>Unsubscribing</strong> from the lists can be performed
+ in the above links as well. <em>Please do not send unsubscription
+ requests to the list.</em></p>
+
+
+ <h2>Support forums</h2>
+
+ <p>The main support channel for the usage of OpenRocket is the
+ support forums. This way everybody can benefit from the answers
+ provided.</p>
+
+ <p><em><a href="http://apps.sourceforge.net/phpbb/openrocket/">Go
+ to the support forums →</a></em></p>
+
+
+ <h2 id="contact">Contact information</h2>
+
+ <p>OpenRocket is developed by Sampo Niskanen. His contact
+ information can be found below.</p>
+
+ <p><em>If you would like to contribute something to OpenRocket, please
+ contact me!</em></p>
+
+ <p><strong><em>Support requests</em></strong> should be sent to
+ the <a href="http://apps.sourceforge.net/phpbb/openrocket/">support
+ forums</a>.<br/>
+ <strong><em>Bug reports and feature requests</em></strong> should
+ be <a href="report.html">reported separately</a>.</p>
+
+ <p><strong>Email:</strong>
+ <em>sam<span>po</span>.<span>niskanen</span><span>@i</span>ki.fi</em></p>
+
+ <p><strong>WWW:</strong>
+ <a href="http://www.iki.fi/sampo.niskanen/"
+ title="Home page of Sampo Niskanen"><em>http://www.iki.fi/sampo.niskanen/</em></a></p>
+
+ </div>
+
+ <div class="valid">
+ <p><a href="http://validator.w3.org/check/referer"><img src="valid-xhtml10.png" alt="Valid XHTML 1.0!"/></a>
+ <a href="http://jigsaw.w3.org/css-validator/check/referer"><img src="vcss.gif" alt="Valid CSS!"/></a>
+ </p>
+ </div>
+
+</body>
+</html>
+
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>OpenRocket—Documentation</title>
+ <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"/>
+ <meta name="keywords" content="OpenRocket, model rocket, simulator, simulation, rocketry, documentation"/>
+ <link rel="stylesheet" type="text/css" href="layout.css"/>
+</head>
+
+<body class="page_documentation">
+ <!--[if lte IE 6]>
+ <div id="iewarn">
+ You are using a browser that is <strong>8 years old!</strong>
+
+ In Internet-years that is <em>prehistoric!</em><br/>
+ For the sanity of all webmasterkind,
+ <em>please <a href="http://www.mozilla.com/">upgrade</a></em>. It's easy!
+ </div>
+ <![endif]-->
+
+ <h1>Documentation for OpenRocket</h1>
+
+ <div class="menucontainer">
+ <div class="menu">
+ <ul>
+ <li>OpenRocket</li>
+ <li><a href="index.html">Home</a></li>
+ <li><a href="features.html">Features</a></li>
+ <li><a href="screenshots.html">Screenshots</a></li>
+ <li><a href="download.html">Download</a></li>
+ <li><a href="documentation.html">Documentation</a></li>
+ <li><a href="contact.html">
+ Mailing lists<br/>
+ Support forums<br/>
+ Contact info</a></li>
+ <li><a href="report.html">
+ Report a bug<br/>
+ Request a feature</a></li>
+ <li><a href="license.html">License</a></li>
+ </ul>
+ <div class="logo">
+ <a href="http://sourceforge.net/projects/openrocket"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=260357&type=12" width="120" height="30" alt="Get OpenRocket at SourceForge.net. Fast, secure and Free Open Source software downloads" /></a>
+ </div>
+ </div>
+ </div>
+
+ <div class="content">
+
+ <h2>User documentation</h2>
+
+ <p>No user's guide currently exists for OpenRocket. There is a
+ <a href="http://apps.sourceforge.net/mediawiki/openrocket/index.php?title=User%27s_Guide">page on the wiki</a> for creating a User's guide.</p>
+ <p>If you would like to help, please extend it!</p>
+
+
+ <h2>Technical documentation</h2>
+
+ <p>Coming within the next few weeks.</p>
+ <!--
+ <p>OpenRocket was originally written as the Master's thesis of
+ Sampo Niskanen at Helsinki University of Technology. The thesis
+ currently functions as the technical documentation, and will be
+ updated in the future to account for further enhancements.
+ The thesis is licensed under a
+ <a rel="license" href="http://creativecommons.org/licenses/by-nd-nc/1.0/fi/deed.en">Creative Commons BY-NC-ND License</a>.</p>
+
+ <p class="right"><a rel="license" href="http://creativecommons.org/licenses/by-nd-nc/1.0/fi/deed.en"><img alt="Creative Commons BY-NC-ND License" src="http://i.creativecommons.org/l/by-nd-nc/1.0/fi/88x31.png" /></a></p>
+
+
+
+ <p><a href="thesis.pdf">Development of an Open Source model
+ rocket simulation software</a> (PDF, 1.3MB)</p>
+
+
+ <p class="quote"><strong>Table of contents:</strong></p>
+ <ol class="toc">
+ <li>1. Introduction</li>
+ <li>2. Basics of model rocket flight</li>
+ <li>3. Aerodynamic properties of model rockets</li>
+ <li>4. Flight simulation</li>
+ <li>5. The OpenRocket simulation software</li>
+ <li>6. Comparison with experimental data</li>
+ <li>7. Conclusion</li>
+ </ol>
+ <ol class="toc">
+ <li>A. Nose cone and transition geometries</li>
+ <li>B. Transonic wave drag of nose cones</li>
+ <li>C. Streamer drag coefficient estimation</li>
+ </ol>
+ -->
+
+ <h2>Resources</h2>
+
+ <p>Below are resources that have been found useful in the analysis
+ of model rockets. Many useful scientific aerodynamic articles and
+ documents are available at the invaluable
+ <a href="http://ntrs.nasa.gov/">NASA Technical Resources Server
+ (NTRS)</a>.</p>
+
+ <dl>
+ <dt><em>
+ <a href="http://www.apogeerockets.com/Education/downloads/barrowman_report.pdf">The Theoretical Prediction of the Center of
+ Pressure</a></em>, James and Judith Barrowman, 1966.</dt>
+ <dd>The original NARAM R&D report explaining how to
+ calculate the CP position of a rocket.</dd>
+
+ <dt><em>
+ <a href="http://ntrs.nasa.gov/search.jsp?Ntk=all&Ntx=mode+matchall&Ntt=Barrowman+Practical+Calculation+of+the+Aerodynamic+Characteristics+of+Slender+Finned+Vehicles">The Practical Calculation of the Aerodynamic
+ Characteristics of Slender Finned Vehicles</a></em>, James
+ Barrowman, 1967.</dt>
+ <dd>The more in-depth and technical thesis, where Barrowman
+ presents methods for calculating the CP position of a rocket at
+ both subsonic and supersonic velocities and its other
+ aerodynamic properties. Available on
+ <a href="http://ntrs.nasa.gov/">NTRS</a>.</dd>
+
+ <dt><em>
+ <a href="http://projetosulfos.if.sc.usp.br/artigos/sentinel39-galejs.pdf">Wind instability—What Barrowman left out</a></em>,
+ Robert Galejs.</dt>
+ <dd>An extension to the Barrowman method to account for body
+ lift at large angles of attack.</dd>
+
+ <dt><em>Topics in Advanced Model Rocketry</em>, Mandell,
+ Caporaso, Bengen, MIT Press, 1973.</dt>
+ <dd>An excellent theoretical study on the flight of model
+ rockets. Available as a reprint edition.</dd>
+
+ <dt><em>Fluid-dynamic drag</em>, Sighard Hoerner,
+ published by the author, 1965.</dt>
+ <dd>An excellent resource for all kinds of experimental data
+ regarding drag. Available as a reprint edition.</dd>
+
+ <dt><em>Tactical missile design</em>, 2nd edition, Eugene
+ L. Fleeman, AIAA, 2006.</dt>
+ <dd>Useful approximation methods for estimating the aerodynamic
+ properties of rockets.</dd>
+
+ <dt><em><a href="http://www.aoe.vt.edu/~mason/Mason_f/CAtxtTop.html">Applied
+ Computational Aerodynamics</a></em>, William Mason.</dt>
+ <dd>An online textbook on computational aerodynamics.</dd>
+
+ <dt><em><a href="http://www.combatindex.com/mil_docs/pdf/hdbk/0700/MIL-HDBK-762.pdf">Design of aerodynamically stabilized free rockets</a></em>,
+ MIL-HDBK-762, US Army Missile Command, 1990.</dt>
+ <dd>Military handbook on the design of rockets, a good resource
+ for aerodynamic estimation methods.</dd>
+
+ <dt><em>
+ <a href="http://www.thrustcurve.org/">ThrustCurve.org</a></em>,
+ John Coker.</dt>
+ <dd>An excellent resource for model rocket motor thrust curves.</dd>
+
+ <dt><em>
+ <a href="http://ntrs.nasa.gov/search.jsp?Ntk=all&Ntx=mode+matchall&Ntt=NASA-TN-D-4013">Static stability investigation of a single-stage
+ sounding rocket at Mach numbers from 0.60 to 1.20</a></em>, James
+ Ferris, NASA-TN-D-4013, 1967.</dt>
+ <dt><em>
+ <a href="http://ntrs.nasa.gov/search.jsp?Ntk=all&Ntx=mode+matchall&Ntt=NASA-TN-D-4014">Static stability investigation of a sounding-rocket
+ vehicle at Mach numbers from 1.50 to 4.63</a></em>, Donald Babb and
+ Dennis Fuller, NASA-TN-D-4014, 1967.</dt>
+ <dd>Experimental data of a wind tunnel investigation of a
+ sounding rocket at subsonic, transonic and supersonic
+ velocities. Available on
+ <a href="http://ntrs.nasa.gov/">NTRS</a>.</dd>
+
+
+
+ </dl>
+
+
+ </div>
+
+ <div class="valid">
+ <p><a href="http://validator.w3.org/check/referer"><img src="valid-xhtml10.png" alt="Valid XHTML 1.0!"/></a>
+ <a href="http://jigsaw.w3.org/css-validator/check/referer"><img src="vcss.gif" alt="Valid CSS!"/></a>
+ </p>
+ </div>
+
+</body>
+</html>
+
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>OpenRocket—Download</title>
+ <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"/>
+ <meta name="keywords" content="OpenRocket, model rocket, simulator, simulation, rocketry"/>
+ <link rel="stylesheet" type="text/css" href="layout.css"/>
+</head>
+
+<body class="page_download">
+ <!--[if lte IE 6]>
+ <div id="iewarn">
+ You are using a browser that is <strong>8 years old!</strong>
+
+ In Internet-years that is <em>prehistoric!</em><br/>
+ For the sanity of all webmasterkind,
+ <em>please <a href="http://www.mozilla.com/">upgrade</a></em>. It's easy!
+ </div>
+ <![endif]-->
+
+ <h1>Download OpenRocket</h1>
+
+ <div class="menucontainer">
+ <div class="menu">
+ <ul>
+ <li>OpenRocket</li>
+ <li><a href="index.html">Home</a></li>
+ <li><a href="features.html">Features</a></li>
+ <li><a href="screenshots.html">Screenshots</a></li>
+ <li><a href="download.html">Download</a></li>
+ <li><a href="documentation.html">Documentation</a></li>
+ <li><a href="contact.html">
+ Mailing lists<br/>
+ Support forums<br/>
+ Contact info</a></li>
+ <li><a href="report.html">
+ Report a bug<br/>
+ Request a feature</a></li>
+ <li><a href="license.html">License</a></li>
+ </ul>
+ <div class="logo">
+ <a href="http://sourceforge.net/projects/openrocket"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=260357&type=12" width="120" height="30" alt="Get OpenRocket at SourceForge.net. Fast, secure and Free Open Source software downloads" /></a>
+ </div>
+ </div>
+ </div>
+
+ <div class="content">
+
+ <h2>Binary download</h2>
+
+ <p>The binary download below is the recommended package for general
+ use. It is pre-packaged with motor thrust curves from
+ <a href="http://www.thrustcurve.org/">thrustcurve.org</a>.</p>
+
+ <p><em>OpenRocket requires <strong>Java version 6</strong> or
+ later. The Sun JRE is recommended.</em></p>
+
+ <p class="download">
+ <a href="https://sourceforge.net/project/downloading.php?group_id=260357&filename=OpenRocket-0.9.0.jar">Download OpenRocket 0.9.0</a></p>
+
+ <p>OpenRocket is still considered <strong>beta software</strong>.
+ If you encounter any problems, please
+ <a href="report.html">report them</a> so they can be fixed!</p>
+
+ <p>OpenRocket can be started in graphical environments (such as
+ Windows) by double-clicking the package icon. No installation is
+ required. From the command line it can be started by</p>
+ <pre class="quote">$ java -jar OpenRocket-0.9.0.jar</pre>
+
+ <p>Older packages are available from the
+ <a href="https://sourceforge.net/project/showfiles.php?group_id=260357">SourceForge repository</a>.</p>
+
+
+ <h2>Source code</h2>
+
+ <p>The source code for OpenRocket is available from the
+ <a href="http://openrocket.svn.sourceforge.net/viewvc/openrocket/">SourceForge SVN repository</a>.
+ It can be retrieved simply using the command</p>
+ <pre class="quote">$ svn co https://openrocket.svn.sourceforge.net/svnroot/openrocket openrocket</pre>
+ <p>The above URL may be used to connect to the repository with
+ other Subversion clients as well.</p>
+
+
+
+ </div>
+
+ <div class="valid">
+ <p><a href="http://validator.w3.org/check/referer"><img src="valid-xhtml10.png" alt="Valid XHTML 1.0!"/></a>
+ <a href="http://jigsaw.w3.org/css-validator/check/referer"><img src="vcss.gif" alt="Valid CSS!"/></a>
+ </p>
+ </div>
+
+</body>
+</html>
+
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>OpenRocket—Features</title>
+ <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"/>
+ <meta name="keywords" content="OpenRocket, model rocket, simulator, simulation, rocketry, features"/>
+ <link rel="stylesheet" type="text/css" href="layout.css"/>
+</head>
+
+<body class="page_features">
+ <!--[if lte IE 6]>
+ <div id="iewarn">
+ You are using a browser that is <strong>8 years old!</strong>
+
+ In Internet-years that is <em>prehistoric!</em><br/>
+ For the sanity of all webmasterkind,
+ <em>please <a href="http://www.mozilla.com/">upgrade</a></em>. It's easy!
+ </div>
+ <![endif]-->
+
+ <h1>Features of OpenRocket</h1>
+
+ <div class="menucontainer">
+ <div class="menu">
+ <ul>
+ <li>OpenRocket</li>
+ <li><a href="index.html">Home</a></li>
+ <li><a href="features.html">Features</a></li>
+ <li><a href="screenshots.html">Screenshots</a></li>
+ <li><a href="download.html">Download</a></li>
+ <li><a href="documentation.html">Documentation</a></li>
+ <li><a href="contact.html">
+ Mailing lists<br/>
+ Support forums<br/>
+ Contact info</a></li>
+ <li><a href="report.html">
+ Report a bug<br/>
+ Request a feature</a></li>
+ <li><a href="license.html">License</a></li>
+ </ul>
+ <div class="logo">
+ <a href="http://sourceforge.net/projects/openrocket"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=260357&type=12" width="120" height="30" alt="Get OpenRocket at SourceForge.net. Fast, secure and Free Open Source software downloads" /></a>
+ </div>
+ </div>
+ </div>
+
+ <div class="content">
+
+ <h2>Current features</h2>
+
+ <h3>General</h3>
+
+ <ul>
+ <li><strong>Fully cross-platform</strong>, written in Java</li>
+ <li><strong>Fully documented</strong> <a href="documentation.html">simulation
+ methods</a></li>
+ <li><strong>Open Source</strong>, source code available under the
+ <a href="license.html">GNU GPL</a></li>
+ </ul>
+
+ <h3>User interface</h3>
+ <ul>
+ <li><a href="screenshots.html">Easy-to-use user interface</a> for
+ rocket design</li>
+ <li><strong>Zoomable schematic view</strong> of rocket from the side or rear</li>
+ <li>Rocket rotation around center axis</li>
+ <li><strong>Real-time view of CG and CP</strong> position</li>
+ <li><strong>Real-time flight altitude, velocity and
+ acceleration</strong> information from a continuous simulation
+ performed in the background</li>
+ </ul>
+
+ <h3>Design</h3>
+
+ <ul>
+ <li>A multitude of available components to
+ choose from</li>
+ <li><strong>Trapezoidal</strong>, <strong>elliptical</strong>
+ and <strong>free-form fins</strong> supported</li>
+ <li>Support for <strong>canted fins</strong> (roll
+ stabilization)</li>
+ <li><strong>Staging</strong> and <strong>clustering</strong> support</li>
+ <li>Automatic calculation of component mass and CG based on
+ shape and density</li>
+ <li>Ability to <strong>override mass and CG</strong> of
+ components or stages separately</li>
+ </ul>
+
+ <h3>Simulation and analysis</h3>
+
+ <ul>
+ <li>Full <strong>six degree of freedom</strong> simulation</li>
+ <li>Rocket stability computed using <strong>extended Barrowman
+ method</strong></li>
+ <li>Realistic wind modeling</li>
+ <li>Analysis of the <strong>effect of separate
+ components</strong> on the stability, drag and roll
+ characteristics of the rocket</li>
+ <li><strong>Fully configurable plotting</strong>, with
+ various preset configurations</li>
+ <li><strong>Simulation listeners</strong> allowing custom-made
+ code to interact with the rocket during flight simulation</li>
+ </ul>
+
+
+ <h2 id="future">Planned future features</h2>
+
+ <p>OpenRocket is under constant work, and anybody can help make
+ OpenRocket an even better simulator! Here are a few features that
+ have been planned...</p>
+
+ <ul>
+ <li>Aerodynamic computation using
+ <acronym title="Computational Fluid Dynamics">CFD</acronym>
+ <a href="contact.html" class="help">(help needed!)</a></li>
+ <li>Better support for supersonic simulation
+ <a href="contact.html" class="help">(help needed!)</a></li>
+ <li>3D view of the rocket design
+ <a href="contact.html" class="help">(help needed!)</a></li>
+ <li>Saving figures and exporting simulation data</li>
+ <li>Importing and plotting actual flight data from altimeters</li>
+ <li>Importing new motor thrust curves</li>
+ <li>Support for ready-made component databases</li>
+ <li>Customized support for hybrid rocket motors and water
+ rockets</li>
+ <li>Rocket flight animation</li>
+ <li>A "wizard" for creating new rocket designs</li>
+ <li>. . .</li>
+ </ul>
+
+ <p>If you want to help make OpenRocket the best rocket simulator,
+ don't hesitate to <a href="contact.html">contact us</a>!</p>
+
+ </div>
+
+ <div class="valid">
+ <p><a href="http://validator.w3.org/check/referer"><img src="valid-xhtml10.png" alt="Valid XHTML 1.0!"/></a>
+ <a href="http://jigsaw.w3.org/css-validator/check/referer"><img src="vcss.gif" alt="Valid CSS!"/></a>
+ </p>
+ </div>
+
+</body>
+</html>
+
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>OpenRocket</title>
+ <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"/>
+ <meta name="keywords" content="OpenRocket, model rocket, simulator, simulation, rocketry"/>
+ <link rel="stylesheet" type="text/css" href="layout.css"/>
+</head>
+
+<body class="page_index">
+ <!--[if lte IE 6]>
+ <div id="iewarn">
+ You are using a browser that is <strong>8 years old!</strong>
+
+ In Internet-years that is <em>prehistoric!</em><br/>
+ For the sanity of all webmasterkind,
+ <em>please <a href="http://www.mozilla.com/">upgrade</a></em>. It's easy!
+ </div>
+ <![endif]-->
+
+ <h1>OpenRocket — an Open Source model rocket simulator</h1>
+
+ <div class="menucontainer">
+ <div class="menu">
+ <ul>
+ <li>OpenRocket</li>
+ <li><a href="index.html">Home</a></li>
+ <li><a href="features.html">Features</a></li>
+ <li><a href="screenshots.html">Screenshots</a></li>
+ <li><a href="download.html">Download</a></li>
+ <li><a href="documentation.html">Documentation</a></li>
+ <li><a href="contact.html">
+ Mailing lists<br/>
+ Support forums<br/>
+ Contact info</a></li>
+ <li><a href="report.html">
+ Report a bug<br/>
+ Request a feature</a></li>
+ <li><a href="license.html">License</a></li>
+ </ul>
+ <div class="logo">
+ <a href="http://sourceforge.net/projects/openrocket"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=260357&type=12" width="120" height="30" alt="Get OpenRocket at SourceForge.net. Fast, secure and Free Open Source software downloads" /></a>
+ </div>
+ </div>
+ </div>
+
+ <div class="content">
+
+ <h2>Introduction</h2>
+
+ <p><strong>OpenRocket</strong> is a Free, fully featured model
+ rocket simulator written in Java. It can be used to design and
+ simulate rockets before actually building and flying them.
+ </p>
+ <p>OpenRocket features a full six-degree-of-freedom simulation,
+ realistic wind modeling, a multitude of different components
+ including free-form fins and canted fins, clustering and
+ staging. Read more about its <a href="features.html">features</a>.
+ </p>
+ <p>Best of all, OpenRocket is Open Source—its source code is
+ freely available to study and extend. Anybody wishing to
+ contribute to the project can do so according to the
+ <a href="license.html">GNU GPL</a>. Simply
+ <a href="download.html">download</a> the source code
+ and start hacking, or <a href="download.html">get the ready
+ package</a> to begin designing and simulating.
+ </p>
+ <p>OpenRocket is still considered to be <strong>beta
+ software</strong>—there will still be bugs and occasional
+ problems. If you encounter problems, please
+ <a href="contact.html">report them</a> so they can be fixed.
+ </p>
+
+ <div>
+ <div class="smallshot"><a href="screenshots.html">
+ <img src="shots-small/main.jpg" alt="Main window"/><br/>
+ Main window
+ </a></div>
+ <div class="smallshot"><a href="screenshots.html">
+ <img src="shots-small/dialog-analysis.jpg" alt="Analysis dialog"/><br/>
+ Analysis dialog
+ </a></div>
+ <div class="smallshot last"><a href="screenshots.html">
+ <img src="shots-small/dialog-plot.jpg" alt="Simulation plot"/><br/>
+ Simulation plot
+ </a></div>
+ </div>
+ <div class="clear"></div>
+
+
+ <h2>News</h2>
+
+ <p><strong>24.5.2009:</strong> First version 0.9.0
+ <a href="download.html">released</a>!</p>
+
+ </div>
+
+ <div class="valid">
+ <p><a href="http://validator.w3.org/check/referer"><img src="valid-xhtml10.png" alt="Valid XHTML 1.0!"/></a>
+ <a href="http://jigsaw.w3.org/css-validator/check/referer"><img src="vcss.gif" alt="Valid CSS!"/></a>
+ </p>
+ </div>
+
+</body>
+</html>
+
--- /dev/null
+
+body {
+ margin: 0;
+ padding: 0;
+}
+
+#iewarn {
+ width: 100%;
+ background-color: #fa0;
+ text-align: center;
+ padding: 1em 2em;
+ border-top: solid 1px black;
+ border-bottom: solid 1px black;
+}
+
+
+h1 {
+ margin: 0.75em 2em 1.25em 2em;
+}
+
+h2 {
+ margin-top: 1.5em;
+ border-bottom: dotted 2px #f99;
+}
+
+a {
+ text-decoration: none;
+ color: #00F;
+}
+a:hover {
+ color: #55c;
+}
+
+
+div.menucontainer {
+ position: relative;
+}
+
+div.menu {
+ position: absolute;
+ left: 1.5em;
+ width: 12em;
+ margin: 0;
+ padding: 0 0;
+ background-color: #ccc;
+}
+div.menu ul {
+ position: relative;
+ left: -2px;
+ top: -2px;
+ right: 2px;
+ bottom: 2px;
+ background-color: #89cbe0;
+ border: solid 1px black;
+ list-style: none;
+ margin: 0;
+ padding: 0 0;
+}
+
+div.menu li {
+ display: block;
+ left: 0;
+ right: 0;
+ margin: 0;
+ text-align: center;
+}
+
+div.menu li:first-child {
+ padding: 0.5em 0;
+ font-size: 160%;
+}
+
+div.menu li+li {
+ border-top: dashed 1px black;
+}
+
+div.menu li a {
+ display: block;
+ left: 0;
+ right: 0;
+ font-style: normal;
+ text-decoration: none;
+ color: #00d;
+ padding: 0.75em 1em;
+ outline: none;
+}
+div.menu li a:focus {
+ background-color: #8fd5eb;
+}
+
+div.menu li a:hover {
+ background-color: #ee9494;
+}
+
+div.menu div.logo {
+ position: absolute;
+ top: 100%;
+ left: -2px;
+ margin-top: 15px;
+ width: 100%;
+}
+
+div.menu div.logo img {
+ display: block;
+ margin: 0 auto;
+}
+
+
+.page_index div.menu a[href="index.html"],
+.page_features div.menu a[href="features.html"],
+.page_screenshots div.menu a[href="screenshots.html"],
+.page_download div.menu a[href="download.html"],
+.page_documentation div.menu a[href="documentation.html"],
+.page_contact div.menu a[href="contact.html"],
+.page_report div.menu a[href="report.html"],
+.page_license div.menu a[href="license.html"] {
+ font-weight: bold;
+ font-size: 110%;
+}
+
+
+.content {
+ margin: 0em 2em 2em 15.5em;
+ min-height: 27em;
+}
+
+img {
+ border: 0px;
+ outline: none;
+ font-size: 70%;
+}
+
+.smallshot {
+ float: left;
+ margin-top:2em;
+ text-align: center;
+ font-style: italic;
+ margin-right: 2em;
+}
+.smallshot.last {
+ margin-right: 0;
+}
+.clear {
+ clear:both;
+}
+
+
+.smallshotconst {
+ float: left;
+ width: 270px;
+ height: 220px;
+ margin: 1em 1em;
+ text-align: center;
+ font-style: italic;
+}
+.smallshotconst em {
+ font-style: normal;
+}
+
+
+
+a.help {
+ margin-left: 1em;
+ font-size: smaller;
+ font-style: italic;
+}
+
+
+pre.quote {
+ margin: 2em;
+ padding: 1em;
+ border: dashed 1px #888;
+ background-color: #ddd;
+}
+
+p.quote {
+ margin: 2em;
+}
+
+hr {
+ margin: 2em 0em;
+}
+
+.right {
+ float: right;
+ margin: 0;
+}
+
+li {
+ margin-top: 0.5em;
+}
+
+
+p.download {
+ margin: 2em;
+}
+p.download a {
+ font-size: 140%;
+ font-style: italic;
+ padding: 0.5em;
+ border: dashed 1px red;
+ background-color: #89cbe0;
+ outline: none;
+}
+p.download a:hover {
+ color: #00F;
+ background-color: #ee9494;
+}
+p.download a:focus {
+ background-color: #8fd5eb;
+}
+
+div.valid {
+ float: right;
+ margin-right: 2em;
+}
+
+
+
+ol.toc {
+ list-style-type: none;
+}
+
+dt+dt {
+ margin-top: 0.5em;
+}
+dd {
+ margin-top: 0.2em;
+ margin-bottom: 1.4em;
+}
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>OpenRocket—License</title>
+ <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"/>
+ <meta name="keywords" content="OpenRocket, model rocket, simulator, simulation, rocketry, license"/>
+ <link rel="stylesheet" type="text/css" href="layout.css"/>
+</head>
+
+<body class="page_license">
+ <!--[if lte IE 6]>
+ <div id="iewarn">
+ You are using a browser that is <strong>8 years old!</strong>
+
+ In Internet-years that is <em>prehistoric!</em><br/>
+ For the sanity of all webmasterkind,
+ <em>please <a href="http://www.mozilla.com/">upgrade</a></em>. It's easy!
+ </div>
+ <![endif]-->
+
+ <h1>OpenRocket license</h1>
+
+ <div class="menucontainer">
+ <div class="menu">
+ <ul>
+ <li>OpenRocket</li>
+ <li><a href="index.html">Home</a></li>
+ <li><a href="features.html">Features</a></li>
+ <li><a href="screenshots.html">Screenshots</a></li>
+ <li><a href="download.html">Download</a></li>
+ <li><a href="documentation.html">Documentation</a></li>
+ <li><a href="contact.html">
+ Mailing lists<br/>
+ Support forums<br/>
+ Contact info</a></li>
+ <li><a href="report.html">
+ Report a bug<br/>
+ Request a feature</a></li>
+ <li><a href="license.html">License</a></li>
+ </ul>
+ <div class="logo">
+ <a href="http://sourceforge.net/projects/openrocket"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=260357&type=12" width="120" height="30" alt="Get OpenRocket at SourceForge.net. Fast, secure and Free Open Source software downloads" /></a>
+ </div>
+ </div>
+ </div>
+
+ <div class="content">
+
+ <p><em>The license text is available also in the simulator under
+ <strong>Help → License</strong> and in the file
+ <tt>LICENSE.TXT</tt>.</em></p>
+
+ <hr/>
+
+ <pre>
+OpenRocket - A model rocket simulator
+
+Copyright (C) 2007-2009 Sampo Niskanen
+
+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 (below) for more details.
+
+
+Additional permission under GNU GPL version 3 section 7:
+
+The licensors grant additional permission to package this Program, or
+any covered work, along with any non-compilable data files (such as
+thrust curves or component databases) and convey the resulting work.
+
+
+------------------------------------------------------------------------
+
+
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ 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.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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 <http://www.gnu.org/licenses/>.
+
+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:
+
+ <program> Copyright (C) <year> <name of author>
+ 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
+<http://www.gnu.org/licenses/>.
+
+ 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
+<http://www.gnu.org/philosophy/why-not-lgpl.html>.
+ </pre>
+
+ <hr/>
+
+ </div>
+
+ <div class="valid">
+ <p><a href="http://validator.w3.org/check/referer"><img src="valid-xhtml10.png" alt="Valid XHTML 1.0!"/></a>
+ <a href="http://jigsaw.w3.org/css-validator/check/referer"><img src="vcss.gif" alt="Valid CSS!"/></a>
+ </p>
+ </div>
+
+</body>
+</html>
+
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>OpenRocket—Support and contact information</title>
+ <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"/>
+ <meta name="keywords" content="OpenRocket, model rocket, simulator, simulation, rocketry, support, bug report, feature request, contact information"/>
+ <link rel="stylesheet" type="text/css" href="layout.css"/>
+</head>
+
+<body class="page_report">
+ <!--[if lte IE 6]>
+ <div id="iewarn">
+ You are using a browser that is <strong>8 years old!</strong>
+
+ In Internet-years that is <em>prehistoric!</em><br/>
+ For the sanity of all webmasterkind,
+ <em>please <a href="http://www.mozilla.com/">upgrade</a></em>. It's easy!
+ </div>
+ <![endif]-->
+
+ <h1>Support and contact information for OpenRocket</h1>
+
+ <div class="menucontainer">
+ <div class="menu">
+ <ul>
+ <li>OpenRocket</li>
+ <li><a href="index.html">Home</a></li>
+ <li><a href="features.html">Features</a></li>
+ <li><a href="screenshots.html">Screenshots</a></li>
+ <li><a href="download.html">Download</a></li>
+ <li><a href="documentation.html">Documentation</a></li>
+ <li><a href="contact.html">
+ Mailing lists<br/>
+ Support forums<br/>
+ Contact info</a></li>
+ <li><a href="report.html">
+ Report a bug<br/>
+ Request a feature</a></li>
+ <li><a href="license.html">License</a></li>
+ </ul>
+ <div class="logo">
+ <a href="http://sourceforge.net/projects/openrocket"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=260357&type=12" width="120" height="30" alt="Get OpenRocket at SourceForge.net. Fast, secure and Free Open Source software downloads" /></a>
+ </div>
+ </div>
+ </div>
+
+ <div class="content">
+
+ <h2>Bug reports</h2>
+
+ <p>If you encounter problems with OpenRocket, please report them
+ so they can be fixed in future versions. Please follow the
+ instructions below to report a bug:</p>
+
+ <ul>
+ <li>Search the bug repository to see if the bug has already been
+ reported. If it is, please add extra information to that bug
+ report:<br/>
+ <form action="https://sourceforge.net/search/index.php" method="get">
+ <input type="hidden" name="group_id" value="260357" />
+ <input type="hidden" name="type_of_search" value="artifact"/>
+<!-- <input type="hidden" name="group_artifact_id" value="1127606" /> -->
+<!-- <input type="hidden" name="artifact_group" value="Bug" /> -->
+ <input type="hidden" name="search_summary" value="1" />
+ <input type="hidden" name="search_details" value="1" />
+ <input type="hidden" name="search_comments" value="1" />
+
+ <input type="text" name="all_words" value="" />
+ <input type="submit" name="form_submit" value="Search" />
+ </form>
+ </li>
+
+ <li>Report the bug using the
+ <a href="https://sourceforge.net/tracker/?func=add&group_id=260357&atid=1127606">bug
+ tracker</a>. Follow the instructions provided to fill in the
+ report.</li>
+
+ <li>If you are unsure about some issue, you can discuss it in
+ the appropriate
+ <a href="http://apps.sourceforge.net/phpbb/openrocket/">support
+ forum</a>.</li>
+ </ul>
+
+ <h2>Feature requests</h2>
+
+ <p>Good ideas on how to make OpenRocket better are always welcome!
+ The features will be implemented as there is time. However, no
+ promises are made of when or whether some feature will be
+ provided.</p>
+
+ <p>If you would like to implement some feature yourself, patches
+ are sincerely welcome. Please <a href="contact.html#contact">contact
+ me</a> in order to coordinate our efforts.</p>
+
+ <p>When requesting a feature:</p>
+
+ <ul>
+ <li>Check that the feature is not already in the
+ <a href="features.html#future">planned future features</a> or
+ the <a href="https://sourceforge.net/tracker/?group_id=260357&atid=1127606&artgroup=899287">enhancement requests</a>.</li>
+ <li>Send the request to the <a href="https://sourceforge.net/tracker/?func=add&group_id=260357&atid=1127606">bug tracker</a> as an
+ enhancement request. Please send multiple enhancements as
+ individual items.</li>
+ </ul>
+
+ </div>
+
+ <div class="valid">
+ <p><a href="http://validator.w3.org/check/referer"><img src="valid-xhtml10.png" alt="Valid XHTML 1.0!"/></a>
+ <a href="http://jigsaw.w3.org/css-validator/check/referer"><img src="vcss.gif" alt="Valid CSS!"/></a>
+ </p>
+ </div>
+
+</body>
+</html>
+
--- /dev/null
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <title>OpenRocket—Screenshots</title>
+ <meta http-equiv="content-type" content="text/html; charset=ISO-8859-1"/>
+ <meta name="keywords" content="OpenRocket, model rocket, simulator, simulation, rocketry, screenshots"/>
+ <link rel="stylesheet" type="text/css" href="layout.css"/>
+</head>
+
+<body class="page_screenshots">
+ <!--[if lte IE 6]>
+ <div id="iewarn">
+ You are using a browser that is <strong>8 years old!</strong>
+
+ In Internet-years that is <em>prehistoric!</em><br/>
+ For the sanity of all webmasterkind,
+ <em>please <a href="http://www.mozilla.com/">upgrade</a></em>. It's easy!
+ </div>
+ <![endif]-->
+
+ <h1>Screenshots of OpenRocket</h1>
+
+ <div class="menucontainer">
+ <div class="menu">
+ <ul>
+ <li>OpenRocket</li>
+ <li><a href="index.html">Home</a></li>
+ <li><a href="features.html">Features</a></li>
+ <li><a href="screenshots.html">Screenshots</a></li>
+ <li><a href="download.html">Download</a></li>
+ <li><a href="documentation.html">Documentation</a></li>
+ <li><a href="contact.html">
+ Mailing lists<br/>
+ Support forums<br/>
+ Contact info</a></li>
+ <li><a href="report.html">
+ Report a bug<br/>
+ Request a feature</a></li>
+ <li><a href="license.html">License</a></li>
+ </ul>
+ <div class="logo">
+ <a href="http://sourceforge.net/projects/openrocket"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=260357&type=12" width="120" height="30" alt="Get OpenRocket at SourceForge.net. Fast, secure and Free Open Source software downloads" /></a>
+ </div>
+ </div>
+ </div>
+
+ <div class="content">
+
+ <p>Below are screenshots of the <strong>OpenRocket</strong> model
+ rocket simulator. Click on the images for a full view. You can
+ also <a href="download.html">download the program</a> and start
+ experimenting yourself!</p>
+
+
+ <div class="smallshotconst"><a href="shots/main.png">
+ <img src="shots-small/main.jpg" alt="Main window"/><br/>
+ The main rocket design window is used to design the rocket and
+ it also provides information about a flight simulation in
+ real-time.
+ </a></div>
+ <div class="smallshotconst"><a href="shots/dialog-edit.png">
+ <img src="shots-small/dialog-edit.jpg" alt="Component edit dialog"/><br/>
+ The component shape and properties are defined in their own
+ dialog.
+ </a></div>
+ <div class="smallshotconst"><a href="shots/dialog-analysis.png">
+ <img src="shots-small/dialog-analysis.jpg" alt="Analysis dialog"/><br/>
+ You can analyze the effect of individual components on the
+ stability, drag and roll characteristics of the rocket.
+ </a></div>
+ <div class="smallshotconst"><a href="shots/dialog-plot-options.png">
+ <img src="shots-small/dialog-plot-options.jpg"
+ alt="Simulation plot options"/><br/>
+ The simulation results can be plotted in a multitude
+ of ways. You can either use the predefined plot
+ configurations or define your own.<br/>
+ </a></div>
+ <div class="smallshotconst"><a href="shots/dialog-plot.png">
+ <img src="shots-small/dialog-plot.jpg" alt="Simulation plot"/><br/>
+ The simulations are plotted using the
+ <em>JFreeChart</em> plotting library.
+ </a></div>
+ <div class="clear"></div>
+
+ </div>
+
+ <div class="valid">
+ <p><a href="http://validator.w3.org/check/referer"><img src="valid-xhtml10.png" alt="Valid XHTML 1.0!"/></a>
+ <a href="http://jigsaw.w3.org/css-validator/check/referer"><img src="vcss.gif" alt="Valid CSS!"/></a>
+ </p>
+ </div>
+
+</body>
+</html>
+
--- /dev/null
+#FIG 3.2 Produced by xfig version 3.2.5
+Landscape
+Center
+Metric
+A4
+100.00
+Single
+-2
+1200 2
+2 1 0 1 0 7 50 -1 -1 0.000 0 0 -1 0 0 4
+ 2520 4050 3780 2970 5580 2970 5130 4050
+2 1 0 1 0 7 50 -1 -1 0.000 0 0 -1 1 0 2
+ 0 0 1.00 60.00 60.00
+ 4185 2790 3780 2790
+2 1 0 1 0 7 50 -1 -1 0.000 0 0 -1 1 0 2
+ 0 0 1.00 60.00 60.00
+ 5220 2790 5580 2790
+2 1 0 1 0 7 50 -1 -1 0.000 0 0 -1 1 1 2
+ 0 0 1.00 60.00 60.00
+ 0 0 1.00 60.00 60.00
+ 5715 2970 5715 4050
+2 1 1 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+ 2520 2880 2520 3870
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 1 0 2
+ 0 0 1.00 60.00 60.00
+ 3465 2790 3780 2790
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 1 0 2
+ 0 0 1.00 60.00 60.00
+ 2745 2790 2520 2790
+2 1 0 1 0 7 50 -1 -1 0.000 0 0 -1 0 0 2
+ 1665 4050 6705 4050
+2 1 0 1 0 7 50 -1 -1 0.000 0 0 -1 1 0 2
+ 0 0 1.00 60.00 60.00
+ 3195 4230 2520 4230
+2 1 0 1 0 7 50 -1 -1 0.000 0 0 -1 1 0 2
+ 0 0 1.00 60.00 60.00
+ 4500 4230 5130 4230
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+ 3780 2745 3780 2835
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+ 2025 4050 1800 4275
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+ 2250 4050 2025 4275
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+ 2475 4050 2250 4275
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+ 2520 2745 2520 2835
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+ 5580 2745 5580 2835
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+ 5130 4185 5130 4275
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+ 2520 4185 2520 4275
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+ 5670 2970 5760 2970
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+ 5670 4050 5445 4275
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+ 5445 4050 5220 4275
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+ 5895 4050 5670 4275
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+ 6120 4050 5895 4275
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+ 6345 4050 6120 4275
+2 1 0 1 0 7 50 -1 -1 4.000 0 0 -1 0 0 2
+ 6570 4050 6345 4275
+4 0 0 50 -1 0 11 0.0000 4 135 960 4185 2835 TIP CHORD\001
+4 0 0 50 -1 0 11 0.0000 4 135 630 2790 2835 SWEEP\001
+4 0 0 50 -1 0 11 0.0000 4 135 1185 3240 4275 ROOT CHORD\001
+4 0 0 50 -1 0 11 0.0000 4 135 690 5805 3555 HEIGHT\001
--- /dev/null
+<?xml version="1.0" standalone="no"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
+"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<!-- Creator: fig2dev Version 3.2 Patchlevel 5 -->
+<!-- CreationDate: Wed Jan 9 01:26:26 2008 -->
+<!-- Magnification: 1.050 -->
+<svg xmlns="http://www.w3.org/2000/svg" width="4.4in" height="1.4in" viewBox="1735 2818 5316 1685">
+<g style="stroke-width:.025in; stroke:black; fill:none">
+<!-- Line -->
+<polyline points="2645,4251
+3968,3118
+5858,3118
+5385,4251
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="4393,2929
+3985,2929
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Arrowhead on XXXpoint 4393 2929 - 3952 2929-->
+<polyline points="4025 2897
+3962 2929
+4025 2960
+" style="stroke:#000000;stroke-width:8;
+"/>
+<!-- Line -->
+<polyline points="5480,2929
+5840,2929
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Arrowhead on XXXpoint 5480 2929 - 5874 2929-->
+<polyline points="5801 2960
+5864 2929
+5801 2897
+" style="stroke:#000000;stroke-width:8;
+"/>
+<!-- Line -->
+<polyline points="5999,3135
+5999,4234
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Arrowhead on XXXpoint 5999 3118 - 5999 4267-->
+<polyline points="5968 4195
+5999 4258
+6031 4195
+" style="stroke:#000000;stroke-width:8;
+"/>
+<!-- Arrowhead on XXXpoint 5999 4251 - 5999 3102-->
+<polyline points="6031 3174
+5999 3111
+5968 3174
+" style="stroke:#000000;stroke-width:8;
+"/>
+<!-- Line -->
+<polyline points="2645,3023
+2645,4062
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+stroke-dasharray:41 41;"/>
+<!-- Line -->
+<polyline points="3637,2929
+3950,2929
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Arrowhead on XXXpoint 3637 2929 - 3984 2929-->
+<polyline points="3911 2960
+3974 2929
+3911 2897
+" style="stroke:#000000;stroke-width:8;
+"/>
+<!-- Line -->
+<polyline points="2881,2929
+2662,2929
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Arrowhead on XXXpoint 2881 2929 - 2629 2929-->
+<polyline points="2702 2897
+2639 2929
+2702 2960
+" style="stroke:#000000;stroke-width:8;
+"/>
+<!-- Line -->
+<polyline points="1748,4251
+7039,4251
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="3354,4440
+2662,4440
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Arrowhead on XXXpoint 3354 4440 - 2629 4440-->
+<polyline points="2702 4409
+2639 4440
+2702 4472
+" style="stroke:#000000;stroke-width:8;
+"/>
+<!-- Line -->
+<polyline points="4724,4440
+5367,4440
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Arrowhead on XXXpoint 4724 4440 - 5401 4440-->
+<polyline points="5329 4472
+5392 4440
+5329 4409
+" style="stroke:#000000;stroke-width:8;
+"/>
+<!-- Line -->
+<polyline points="3968,2881
+3968,2976
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="2125,4251
+1889,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="2362,4251
+2125,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="2598,4251
+2362,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="2645,2881
+2645,2976
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="5858,2881
+5858,2976
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="5385,4393
+5385,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="2645,4393
+2645,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="5952,3118
+6047,3118
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="5952,4251
+5716,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="5716,4251
+5480,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="6188,4251
+5952,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="6425,4251
+6188,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="6661,4251
+6425,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Line -->
+<polyline points="6897,4251
+6661,4488
+" style="stroke:#000000;stroke-width:8;
+stroke-linejoin:miter; stroke-linecap:butt;
+"/>
+<!-- Text -->
+<text xml:space="preserve" x="4393" y="2976" stroke="#000000" fill="#000000" font-family="Times" font-style="normal" font-weight="normal" font-size="139" text-anchor="start">TIP CHORD</text>
+<!-- Text -->
+<text xml:space="preserve" x="2929" y="2976" stroke="#000000" fill="#000000" font-family="Times" font-style="normal" font-weight="normal" font-size="139" text-anchor="start">SWEEP</text>
+<!-- Text -->
+<text xml:space="preserve" x="3551" y="4488" stroke="#000000" fill="#000000" font-family="Times" font-style="normal" font-weight="normal" font-size="139" text-anchor="start">ROOT CHORD</text>
+<!-- Text -->
+<text xml:space="preserve" x="6094" y="3732" stroke="#000000" fill="#000000" font-family="Times" font-style="normal" font-weight="normal" font-size="139" text-anchor="start">HEIGHT</text>
+</g>
+</svg>
--- /dev/null
+Originally from http://flyosity.com/tutorial/how-to-draw-a-mac-internet-globe-icon.php
--- /dev/null
+
+Copyright of the icons:
+-----------------------
+
+
+From the "Nuvola" package by David Vignoni:
+http://www.icon-king.com/projects/nuvola/
+
+application-exit.png
+document-close.png
+document-new.png
+document-open.png
+document-save-as.png
+document-save.png
+edit-copy.png
+edit-cut.png
+edit-delete.png
+edit-paste.png
+edit-redo.png
+edit-undo.png
+
+
+From the "Crystal" project:
+http://www.everaldo.com/crystal/
+
+preferences.png
+zoom-in.png
+zoom-out.png
+
--- /dev/null
+package net.sf.openrocket.aerodynamics;
+
+import static net.sf.openrocket.util.MathUtil.pow2;
+
+import java.util.Iterator;
+import java.util.Map;
+
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+/**
+ * A class that is the base of all aerodynamical calculations.
+ * <p>
+ * A {@link Configuration} object must be assigned to this class before any
+ * operations are allowed. This can be done using the constructor or using
+ * the {@link #setConfiguration(Configuration)} method. The default is a
+ * <code>null</code> configuration, in which case the calculation
+ * methods throw {@link NullPointerException}.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public abstract class AerodynamicCalculator {
+
+ private static final double MIN_MASS = 0.001 * MathUtil.EPSILON;
+
+ /** Number of divisions used when calculating worst CP. */
+ public static final int DIVISIONS = 360;
+
+ /**
+ * A <code>WarningSet</code> that can be used if <code>null</code> is passed
+ * to a calculation method.
+ */
+ protected WarningSet ignoreWarningSet = new WarningSet();
+
+ /**
+ * The <code>Rocket</code> currently being calculated.
+ */
+ protected Rocket rocket = null;
+
+ protected Configuration configuration = null;
+
+
+ /*
+ * Cached data. All CG data is in absolute coordinates. All moments of inertia
+ * are relative to their respective CG.
+ */
+ private Coordinate[] cgCache = null;
+ private Coordinate[] origCG = null; // CG of non-overridden stage
+ private double longitudalInertiaCache[] = null;
+ private double rotationalInertiaCache[] = null;
+
+
+ // TODO: LOW: Do not void unnecessary data (mass/aero separately)
+ private int rocketModID = -1;
+// private int aeroModID = -1;
+// private int massModID = -1;
+
+ /**
+ * No-options constructor. The rocket is left as <code>null</code>.
+ */
+ public AerodynamicCalculator() {
+
+ }
+
+
+
+ /**
+ * A constructor that sets the Configuration to be used.
+ *
+ * @param config the configuration to use
+ */
+ public AerodynamicCalculator(Configuration config) {
+ setConfiguration(config);
+ }
+
+
+
+ public Configuration getConfiguration() {
+ return configuration;
+ }
+
+ public void setConfiguration(Configuration config) {
+ this.configuration = config;
+ this.rocket = config.getRocket();
+ }
+
+
+ public abstract AerodynamicCalculator newInstance();
+
+
+ ////////////////// Mass property calculations ///////////////////
+
+
+ /**
+ * Get the CG and mass of the current configuration with motors at the specified
+ * time. The motor ignition times are taken from the configuration.
+ */
+ public Coordinate getCG(double time) {
+ Coordinate totalCG;
+
+ totalCG = getEmptyCG();
+
+ Iterator<MotorMount> iterator = configuration.motorIterator();
+ while (iterator.hasNext()) {
+ MotorMount mount = iterator.next();
+ double ignition = configuration.getIgnitionTime(mount);
+ Motor motor = mount.getMotor(configuration.getMotorConfigurationID());
+ RocketComponent component = (RocketComponent) mount;
+
+ double position = (component.getLength() - motor.getLength()
+ + mount.getMotorOverhang());
+
+ for (Coordinate c: component.toAbsolute(motor.getCG(time-ignition).
+ add(position,0,0))) {
+ totalCG = totalCG.average(c);
+ }
+ }
+
+ return totalCG;
+ }
+
+
+ /**
+ * Get the CG and mass of the current configuration without motors.
+ *
+ * @return the CG of the configuration
+ */
+ public Coordinate getEmptyCG() {
+ checkCache();
+
+ if (cgCache == null) {
+ calculateStageCache();
+ }
+
+ Coordinate totalCG = null;
+ for (int stage: configuration.getActiveStages()) {
+ totalCG = cgCache[stage].average(totalCG);
+ }
+
+ if (totalCG == null)
+ totalCG = Coordinate.NUL;
+
+ return totalCG;
+ }
+
+
+ /**
+ * Return the longitudal inertia of the current configuration with motors at
+ * the specified time. The motor ignition times are taken from the configuration.
+ *
+ * @param time the time.
+ * @return the longitudal moment of inertia of the configuration.
+ */
+ public double getLongitudalInertia(double time) {
+ checkCache();
+
+ if (cgCache == null) {
+ calculateStageCache();
+ }
+
+ final Coordinate totalCG = getCG(time);
+ double totalInertia = 0;
+
+ // Stages
+ for (int stage: configuration.getActiveStages()) {
+ Coordinate stageCG = cgCache[stage];
+
+ totalInertia += (longitudalInertiaCache[stage] +
+ stageCG.weight * MathUtil.pow2(stageCG.x - totalCG.x));
+ }
+
+
+ // Motors
+ Iterator<MotorMount> iterator = configuration.motorIterator();
+ while (iterator.hasNext()) {
+ MotorMount mount = iterator.next();
+ double ignition = configuration.getIgnitionTime(mount);
+ Motor motor = mount.getMotor(configuration.getMotorConfigurationID());
+ RocketComponent component = (RocketComponent) mount;
+
+ double position = (component.getLength() - motor.getLength()
+ + mount.getMotorOverhang());
+
+ double inertia = motor.getLongitudalInertia(time - ignition);
+ for (Coordinate c: component.toAbsolute(motor.getCG(time-ignition).
+ add(position,0,0))) {
+ totalInertia += inertia + c.weight * MathUtil.pow2(c.x - totalCG.x);
+ }
+ }
+
+ return totalInertia;
+ }
+
+
+ /**
+ * Return the rotational inertia of the configuration with motors at the specified time.
+ * The motor ignition times are taken from the configuration.
+ *
+ * @param time the time.
+ * @return the rotational moment of inertia of the configuration.
+ */
+ public double getRotationalInertia(double time) {
+ checkCache();
+
+ if (cgCache == null) {
+ calculateStageCache();
+ }
+
+ final Coordinate totalCG = getCG(time);
+ double totalInertia = 0;
+
+ // Stages
+ for (int stage: configuration.getActiveStages()) {
+ Coordinate stageCG = cgCache[stage];
+
+ totalInertia += rotationalInertiaCache[stage] + stageCG.weight * (
+ MathUtil.pow2(stageCG.y-totalCG.y) + MathUtil.pow2(stageCG.z-totalCG.z)
+ );
+ }
+
+
+ // Motors
+ Iterator<MotorMount> iterator = configuration.motorIterator();
+ while (iterator.hasNext()) {
+ MotorMount mount = iterator.next();
+ double ignition = configuration.getIgnitionTime(mount);
+ Motor motor = mount.getMotor(configuration.getMotorConfigurationID());
+ RocketComponent component = (RocketComponent) mount;
+
+ double position = (component.getLength() - motor.getLength()
+ + mount.getMotorOverhang());
+
+ double inertia = motor.getRotationalInertia(time - ignition);
+ for (Coordinate c: component.toAbsolute(motor.getCG(time-ignition).
+ add(position,0,0))) {
+ totalInertia += inertia + c.weight * (
+ MathUtil.pow2(c.y - totalCG.y) + MathUtil.pow2(c.z - totalCG.z)
+ );
+ }
+ }
+
+ return totalInertia;
+ }
+
+
+
+ private void calculateStageCache() {
+ int stages = rocket.getStageCount();
+
+ cgCache = new Coordinate[stages];
+ longitudalInertiaCache = new double[stages];
+ rotationalInertiaCache = new double[stages];
+
+ for (int i=0; i < stages; i++) {
+ RocketComponent stage = rocket.getChild(i);
+ MassData data = calculateAssemblyMassData(stage);
+ cgCache[i] = stage.toAbsolute(data.cg)[0];
+ longitudalInertiaCache[i] = data.longitudalInertia;
+ rotationalInertiaCache[i] = data.rotationalInetria;
+ }
+ }
+
+
+
+// /**
+// * Updates the stage CGs.
+// */
+// private void calculateStageCGs() {
+// int stages = rocket.getStageCount();
+//
+// cgCache = new Coordinate[stages];
+// origCG = new Coordinate[stages];
+//
+// for (int i=0; i < stages; i++) {
+// Stage stage = (Stage) rocket.getChild(i);
+// Coordinate stageCG = null;
+//
+// Iterator<RocketComponent> iterator = stage.deepIterator();
+// while (iterator.hasNext()) {
+// RocketComponent component = iterator.next();
+//
+// for (Coordinate c: component.toAbsolute(component.getCG())) {
+// stageCG = c.average(stageCG);
+// }
+// }
+//
+// if (stageCG == null)
+// stageCG = Coordinate.NUL;
+//
+// origCG[i] = stageCG;
+//
+// if (stage.isMassOverridden()) {
+// stageCG = stageCG.setWeight(stage.getOverrideMass());
+// }
+// if (stage.isCGOverridden()) {
+// stageCG = stageCG.setXYZ(stage.getOverrideCG());
+// }
+//
+//// System.out.println("Stage "+i+" CG:"+stageCG);
+//
+// cgCache[i] = stageCG;
+// }
+// }
+//
+//
+// private Coordinate calculateCG(RocketComponent component) {
+// Coordinate componentCG = Coordinate.NUL;
+//
+// // Compute CG of this component
+// Coordinate cg = component.getCG();
+// if (cg.weight < MIN_MASS)
+// cg = cg.setWeight(MIN_MASS);
+//
+// for (Coordinate c: component.toAbsolute(cg)) {
+// componentCG = componentCG.average(c);
+// }
+//
+// // Compute CG with subcomponents
+// for (RocketComponent sibling: component.getChildren()) {
+// componentCG = componentCG.average(calculateCG(sibling));
+// }
+//
+// // Override mass/CG if subcomponents are also overridden
+// if (component.getOverrideSubcomponents()) {
+// if (component.isMassOverridden()) {
+// componentCG = componentCG.setWeight(
+// MathUtil.max(component.getOverrideMass(), MIN_MASS));
+// }
+// if (component.isCGOverridden()) {
+// componentCG = componentCG.setXYZ(component.getOverrideCG());
+// }
+// }
+//
+// return componentCG;
+// }
+//
+//
+//
+// private void calculateStageInertias() {
+// int stages = rocket.getStageCount();
+//
+// if (cgCache == null)
+// calculateStageCGs();
+//
+// longitudalInertiaCache = new double[stages];
+// rotationalInertiaCache = new double[stages];
+//
+// for (int i=0; i < stages; i++) {
+// Coordinate stageCG = cgCache[i];
+// double stageLongitudalInertia = 0;
+// double stageRotationalInertia = 0;
+//
+// Iterator<RocketComponent> iterator = rocket.getChild(i).deepIterator();
+// while (iterator.hasNext()) {
+// RocketComponent component = iterator.next();
+// double li = component.getLongitudalInertia();
+// double ri = component.getRotationalInertia();
+// double mass = component.getMass();
+//
+// for (Coordinate c: component.toAbsolute(component.getCG())) {
+// stageLongitudalInertia += li + mass * MathUtil.pow2(c.x - stageCG.x);
+// stageRotationalInertia += ri + mass * (MathUtil.pow2(c.y - stageCG.y) +
+// MathUtil.pow2(c.z - stageCG.z));
+// }
+// }
+//
+// // Check for mass override of complete stage
+// if ((origCG[i].weight != cgCache[i].weight) && origCG[i].weight > 0.0000001) {
+// stageLongitudalInertia = (stageLongitudalInertia * cgCache[i].weight /
+// origCG[i].weight);
+// stageRotationalInertia = (stageRotationalInertia * cgCache[i].weight /
+// origCG[i].weight);
+// }
+//
+// longitudalInertiaCache[i] = stageLongitudalInertia;
+// rotationalInertiaCache[i] = stageRotationalInertia;
+// }
+// }
+//
+
+
+ /**
+ * Returns the mass and inertia data for this component and all subcomponents.
+ * The inertia is returned relative to the CG, and the CG is in the coordinates
+ * of the specified component, not global coordinates.
+ */
+ private MassData calculateAssemblyMassData(RocketComponent parent) {
+ MassData parentData = new MassData();
+
+ // Calculate data for this component
+ parentData.cg = parent.getComponentCG();
+ if (parentData.cg.weight < MIN_MASS)
+ parentData.cg = parentData.cg.setWeight(MIN_MASS);
+
+
+ // Override only this component's data
+ if (!parent.getOverrideSubcomponents()) {
+ if (parent.isMassOverridden())
+ parentData.cg = parentData.cg.setWeight(MathUtil.max(parent.getOverrideMass(),MIN_MASS));
+ if (parent.isCGOverridden())
+ parentData.cg = parentData.cg.setXYZ(parent.getOverrideCG());
+ }
+
+ parentData.longitudalInertia = parent.getLongitudalUnitInertia() * parentData.cg.weight;
+ parentData.rotationalInetria = parent.getRotationalUnitInertia() * parentData.cg.weight;
+
+
+ // Combine data for subcomponents
+ for (RocketComponent sibling: parent.getChildren()) {
+ Coordinate combinedCG;
+ double dx2, dr2;
+
+ // Compute data of sibling
+ MassData siblingData = calculateAssemblyMassData(sibling);
+ Coordinate[] siblingCGs = sibling.toRelative(siblingData.cg, parent);
+
+ for (Coordinate siblingCG: siblingCGs) {
+
+ // Compute CG of this + sibling
+ combinedCG = parentData.cg.average(siblingCG);
+
+ // Add effect of this CG change to parent inertia
+ dx2 = pow2(parentData.cg.x - combinedCG.x);
+ parentData.longitudalInertia += parentData.cg.weight * dx2;
+
+ dr2 = pow2(parentData.cg.y - combinedCG.y) + pow2(parentData.cg.z - combinedCG.z);
+ parentData.rotationalInetria += parentData.cg.weight * dr2;
+
+
+ // Add inertia of sibling
+ parentData.longitudalInertia += siblingData.longitudalInertia;
+ parentData.rotationalInetria += siblingData.rotationalInetria;
+
+ // Add effect of sibling CG change
+ dx2 = pow2(siblingData.cg.x - combinedCG.x);
+ parentData.longitudalInertia += siblingData.cg.weight * dx2;
+
+ dr2 = pow2(siblingData.cg.y - combinedCG.y) + pow2(siblingData.cg.z - combinedCG.z);
+ parentData.rotationalInetria += siblingData.cg.weight * dr2;
+
+ // Set combined CG
+ parentData.cg = combinedCG;
+ }
+ }
+
+ // Override total data
+ if (parent.getOverrideSubcomponents()) {
+ if (parent.isMassOverridden()) {
+ double oldMass = parentData.cg.weight;
+ double newMass = MathUtil.max(parent.getOverrideMass(), MIN_MASS);
+ parentData.longitudalInertia = parentData.longitudalInertia * newMass / oldMass;
+ parentData.rotationalInetria = parentData.rotationalInetria * newMass / oldMass;
+ parentData.cg = parentData.cg.setWeight(newMass);
+ }
+ if (parent.isCGOverridden()) {
+ double oldx = parentData.cg.x;
+ double newx = parent.getOverrideCGX();
+ parentData.longitudalInertia += parentData.cg.weight * pow2(oldx - newx);
+ parentData.cg = parentData.cg.setX(newx);
+ }
+ }
+
+ return parentData;
+ }
+
+
+ private static class MassData {
+ public Coordinate cg = Coordinate.NUL;
+ public double longitudalInertia = 0;
+ public double rotationalInetria = 0;
+ }
+
+
+
+
+
+ //////////////// Aerodynamic calculators ////////////////
+
+ public abstract Coordinate getCP(FlightConditions conditions, WarningSet warnings);
+
+ /*
+ public abstract List<AerodynamicForces> getCPAnalysis(FlightConditions conditions,
+ WarningSet warnings);
+ */
+
+ public abstract Map<RocketComponent, AerodynamicForces>
+ getForceAnalysis(FlightConditions conditions, WarningSet warnings);
+
+ public abstract AerodynamicForces getAerodynamicForces(double time,
+ FlightConditions conditions, WarningSet warnings);
+
+
+ /* Calculate only axial forces (and do not warn about insane AOA etc) */
+ public abstract AerodynamicForces getAxialForces(double time,
+ FlightConditions conditions, WarningSet warnings);
+
+
+
+ public Coordinate getWorstCP() {
+ return getWorstCP(new FlightConditions(configuration), ignoreWarningSet);
+ }
+
+ /*
+ * The worst theta angle is stored in conditions.
+ */
+ public Coordinate getWorstCP(FlightConditions conditions, WarningSet warnings) {
+ FlightConditions cond = conditions.clone();
+ Coordinate worst = new Coordinate(Double.MAX_VALUE);
+ Coordinate cp;
+ double theta = 0;
+
+ for (int i=0; i < DIVISIONS; i++) {
+ cond.setTheta(2*Math.PI*i/DIVISIONS);
+ cp = getCP(cond, warnings);
+ if (cp.x < worst.x) {
+ worst = cp;
+ theta = cond.getTheta();
+ }
+ }
+
+ conditions.setTheta(theta);
+
+ return worst;
+ }
+
+
+
+
+
+ /**
+ * Check the current cache consistency. This method must be called by all
+ * methods that may use any cached data before any other operations are
+ * performed. If the rocket has changed since the previous call to
+ * <code>checkCache()</code>, then either {@link #voidAerodynamicCache()} or
+ * {@link #voidMassCache()} (or both) are called.
+ * <p>
+ * This method performs the checking based on the rocket's modification IDs,
+ * so that these method may be called from listeners of the rocket itself.
+ */
+ protected final void checkCache() {
+ if (rocketModID != rocket.getModID()) {
+ rocketModID = rocket.getModID();
+ voidMassCache();
+ voidAerodynamicCache();
+ }
+ }
+
+
+ /**
+ * Void cached mass data. This method is called whenever a change occurs in
+ * the rocket structure that affects the mass of the rocket and when a new
+ * Rocket is set. This method must be overridden to void any cached data
+ * necessary. The method must call <code>super.voidMassCache()</code> during its
+ * execution.
+ */
+ protected void voidMassCache() {
+ cgCache = null;
+ longitudalInertiaCache = null;
+ rotationalInertiaCache = null;
+ }
+
+ /**
+ * Void cached aerodynamic data. This method is called whenever a change occurs in
+ * the rocket structure that affects the aerodynamics of the rocket and when a new
+ * Rocket is set. This method must be overridden to void any cached data
+ * necessary. The method must call <code>super.voidAerodynamicCache()</code> during
+ * its execution.
+ */
+ protected void voidAerodynamicCache() {
+ // No-op
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.aerodynamics;
+
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.util.Coordinate;
+
+public class AerodynamicForces implements Cloneable {
+
+ /**
+ * The component this data is referring to. Used in analysis methods.
+ * A total value is indicated by the component being the Rocket component.
+ */
+ public RocketComponent component = null;
+
+
+ /** CG and mass */
+ public Coordinate cg = null;
+
+ /** Longitudal moment of inertia with reference to the CG. */
+ public double longitudalInertia = Double.NaN;
+
+ /** Rotational moment of inertia with reference to the CG. */
+ public double rotationalInertia = Double.NaN;
+
+
+
+ /** CP and CNa. */
+ public Coordinate cp = null;
+
+
+ /**
+ * Normal force coefficient derivative. At values close to zero angle of attack
+ * this value may be poorly defined or NaN if the calculation method does not
+ * compute CNa directly.
+ */
+ public double CNa = Double.NaN;
+
+
+ /** Normal force coefficient. */
+ public double CN = Double.NaN;
+
+ /** Pitching moment coefficient, relative to the coordinate origin. */
+ public double Cm = Double.NaN;
+
+ /** Side force coefficient, Cy */
+ public double Cside = Double.NaN;
+
+ /** Yaw moment coefficient, Cn, relative to the coordinate origin. */
+ public double Cyaw = Double.NaN;
+
+ /** Roll moment coefficient, Cl, relative to the coordinate origin. */
+ public double Croll = Double.NaN;
+
+ /** Roll moment damping coefficient */
+ public double CrollDamp = Double.NaN;
+
+ /** Roll moment forcing coefficient */
+ public double CrollForce = Double.NaN;
+
+
+
+ /** Axial drag coefficient, CA */
+ public double Caxial = Double.NaN;
+
+ /** Total drag force coefficient, parallel to the airflow. */
+ public double CD = Double.NaN;
+
+ /** Drag coefficient due to fore pressure drag. */
+ public double pressureCD = Double.NaN;
+
+ /** Drag coefficient due to base drag. */
+ public double baseCD = Double.NaN;
+
+ /** Drag coefficient due to friction drag. */
+ public double frictionCD = Double.NaN;
+
+
+ public double pitchDampingMoment = Double.NaN;
+ public double yawDampingMoment = Double.NaN;
+
+
+ /**
+ * Reset all values to null/NaN.
+ */
+ public void reset() {
+ component = null;
+ cg = null;
+ longitudalInertia = Double.NaN;
+ rotationalInertia = Double.NaN;
+
+ cp = null;
+ CNa = Double.NaN;
+ CN = Double.NaN;
+ Cm = Double.NaN;
+ Cside = Double.NaN;
+ Cyaw = Double.NaN;
+ Croll = Double.NaN;
+ CrollDamp = Double.NaN;
+ CrollForce = Double.NaN;
+ Caxial = Double.NaN;
+ CD = Double.NaN;
+ pitchDampingMoment = Double.NaN;
+ yawDampingMoment = Double.NaN;
+ }
+
+ /**
+ * Zero all values to 0 / Coordinate.NUL. Component is left as it was.
+ */
+ public void zero() {
+ // component untouched
+ cg = Coordinate.NUL;
+ longitudalInertia = 0;
+ rotationalInertia = 0;
+
+ cp = Coordinate.NUL;
+ CNa = 0;
+ CN = 0;
+ Cm = 0;
+ Cside = 0;
+ Cyaw = 0;
+ Croll = 0;
+ CrollDamp = 0;
+ CrollForce = 0;
+ Caxial = 0;
+ CD = 0;
+ pitchDampingMoment = 0;
+ yawDampingMoment = 0;
+ }
+
+
+ @Override
+ public AerodynamicForces clone() {
+ try {
+ return (AerodynamicForces)super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException("CloneNotSupportedException?!?");
+ }
+ }
+
+ @Override
+ public String toString() {
+ String text="AerodynamicForces[";
+
+ if (component != null)
+ text += "component:" + component + ",";
+ if (cg != null)
+ text += "cg:" + cg + ",";
+ if (cp != null)
+ text += "cp:" + cp + ",";
+ if (!Double.isNaN(longitudalInertia))
+ text += "longIn:" + longitudalInertia + ",";
+ if (!Double.isNaN(rotationalInertia))
+ text += "rotIn:" + rotationalInertia + ",";
+
+ if (!Double.isNaN(CNa))
+ text += "CNa:" + CNa + ",";
+ if (!Double.isNaN(CN))
+ text += "CN:" + CN + ",";
+ if (!Double.isNaN(Cm))
+ text += "Cm:" + Cm + ",";
+
+ if (!Double.isNaN(Cside))
+ text += "Cside:" + Cside + ",";
+ if (!Double.isNaN(Cyaw))
+ text += "Cyaw:" + Cyaw + ",";
+
+ if (!Double.isNaN(Croll))
+ text += "Croll:" + Croll + ",";
+ if (!Double.isNaN(Caxial))
+ text += "Caxial:" + Caxial + ",";
+
+ if (!Double.isNaN(CD))
+ text += "CD:" + CD + ",";
+
+ if (text.charAt(text.length()-1) == ',')
+ text = text.substring(0, text.length()-1);
+
+ text += "]";
+ return text;
+ }
+}
--- /dev/null
+package net.sf.openrocket.aerodynamics;
+
+public class AtmosphericConditions implements Cloneable {
+
+ /** Specific gas constant of dry air. */
+ public static final double R = 287.053;
+
+ /** Specific heat ratio of air. */
+ public static final double GAMMA = 1.4;
+
+ /** The standard air pressure (1.01325 bar). */
+ public static final double STANDARD_PRESSURE = 101325.0;
+
+ /** The standard air temperature (20 degrees Celcius). */
+ public static final double STANDARD_TEMPERATURE = 293.15;
+
+
+
+ /** Air pressure, in Pascals. */
+ public double pressure = STANDARD_PRESSURE;
+
+ /** Air temperature, in Kelvins. */
+ public double temperature = STANDARD_TEMPERATURE;
+
+
+ /**
+ * Construct standard atmospheric conditions.
+ */
+ public AtmosphericConditions() {
+
+ }
+
+ /**
+ * Construct specified atmospheric conditions.
+ *
+ * @param temperature the temperature in Kelvins.
+ * @param pressure the pressure in Pascals.
+ */
+ public AtmosphericConditions(double temperature, double pressure) {
+ this.temperature = temperature;
+ this.pressure = pressure;
+ }
+
+
+
+ /**
+ * Return the current density of air for dry air.
+ *
+ * @return the current density of air.
+ */
+ public double getDensity() {
+ return pressure / (R*temperature);
+ }
+
+
+ /**
+ * Return the current speed of sound for dry air.
+ * <p>
+ * The speed of sound is calculated using the expansion around the temperature 0 C
+ * <code> c = 331.3 + 0.606*T </code> where T is in Celcius. The result is accurate
+ * to about 0.5 m/s for temperatures between -30 and 30 C, and within 2 m/s
+ * for temperatures between -55 and 30 C.
+ *
+ * @return the current speed of sound.
+ */
+ public double getMachSpeed() {
+ return 165.77 + 0.606 * temperature;
+ }
+
+
+ /**
+ * Return the current kinematic viscosity of the air.
+ * <p>
+ * The effect of temperature on the viscosity of a gas can be computed using
+ * Sutherland's formula. In the region of -40 ... 40 degrees Celcius the effect
+ * is highly linear, and thus a linear approximation is used in its stead.
+ * This is divided by the result of {@link #getDensity()} to achieve the
+ * kinematic viscosity.
+ *
+ * @return the current kinematic viscosity.
+ */
+ public double getKinematicViscosity() {
+ double v = 3.7291e-06 + 4.9944e-08 * temperature;
+ return v / getDensity();
+ }
+
+ /**
+ * Return a copy of the atmospheric conditions.
+ */
+ @Override
+ public AtmosphericConditions clone() {
+ try {
+ return (AtmosphericConditions) super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException("BUG: CloneNotSupportedException encountered!");
+ }
+ }
+
+
+ @Override
+ public String toString() {
+ return String.format("AtmosphericConditions[T=%.2f,P=%.2f]", temperature, pressure);
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.aerodynamics;
+
+public abstract class AtmosphericModel {
+ /** Layer thickness of interpolated altitude. */
+ private static final double DELTA = 500;
+
+ private AtmosphericConditions[] levels = null;
+
+
+ public AtmosphericConditions getConditions(double altitude) {
+ if (levels == null)
+ computeLayers();
+
+ if (altitude <= 0)
+ return levels[0];
+ if (altitude >= DELTA*(levels.length-1))
+ return levels[levels.length-1];
+
+ int n = (int)(altitude/DELTA);
+ double d = (altitude - n*DELTA)/DELTA;
+ AtmosphericConditions c = new AtmosphericConditions();
+ c.temperature = levels[n].temperature * (1-d) + levels[n+1].temperature * d;
+ c.pressure = levels[n].pressure * (1-d) + levels[n+1].pressure * d;
+
+ return c;
+ }
+
+
+ private void computeLayers() {
+ double max = getMaxAltitude();
+ int n = (int)(max/DELTA) + 1;
+ levels = new AtmosphericConditions[n];
+ for (int i=0; i < n; i++) {
+ levels[i] = getExactConditions(i*DELTA);
+ }
+ }
+
+
+ public abstract double getMaxAltitude();
+ public abstract AtmosphericConditions getExactConditions(double altitude);
+}
--- /dev/null
+package net.sf.openrocket.aerodynamics;
+
+import static net.sf.openrocket.util.MathUtil.pow2;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+import net.sf.openrocket.aerodynamics.barrowman.FinSetCalc;
+import net.sf.openrocket.aerodynamics.barrowman.RocketComponentCalc;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.ExternalComponent;
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.SymmetricComponent;
+import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.PolyInterpolator;
+import net.sf.openrocket.util.Reflection;
+import net.sf.openrocket.util.Test;
+
+/**
+ * An aerodynamic calculator that uses the extended Barrowman method to
+ * calculate the CP of a rocket.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class BarrowmanCalculator extends AerodynamicCalculator {
+
+ private static final String BARROWMAN_PACKAGE = "net.sf.openrocket.aerodynamics.barrowman";
+ private static final String BARROWMAN_SUFFIX = "Calc";
+
+
+ private Map<RocketComponent, RocketComponentCalc> calcMap = null;
+
+ private double cacheDiameter = -1;
+ private double cacheLength = -1;
+
+
+
+ public BarrowmanCalculator() {
+
+ }
+
+ public BarrowmanCalculator(Configuration config) {
+ super(config);
+ }
+
+
+ @Override
+ public BarrowmanCalculator newInstance() {
+ return new BarrowmanCalculator();
+ }
+
+
+ /**
+ * Calculate the CP according to the extended Barrowman method.
+ */
+ @Override
+ public Coordinate getCP(FlightConditions conditions, WarningSet warnings) {
+ AerodynamicForces forces = getNonAxial(conditions, null, warnings);
+ return forces.cp;
+ }
+
+
+
+ @Override
+ public Map<RocketComponent, AerodynamicForces> getForceAnalysis(FlightConditions conditions,
+ WarningSet warnings) {
+
+ AerodynamicForces f;
+ Map<RocketComponent, AerodynamicForces> map =
+ new LinkedHashMap<RocketComponent, AerodynamicForces>();
+
+ // Add all components to the map
+ for (RocketComponent c: configuration) {
+ f = new AerodynamicForces();
+ f.component = c;
+
+ // Calculate CG
+ f.cg = Coordinate.NUL;
+ for (Coordinate coord: c.toAbsolute(c.getCG())) {
+ f.cg = f.cg.average(coord);
+ }
+
+ map.put(c, f);
+ }
+
+
+ // Calculate non-axial force data
+ AerodynamicForces total = getNonAxial(conditions, map, warnings);
+
+
+ // Calculate friction data
+ total.frictionCD = calculateFrictionDrag(conditions, map, warnings);
+ total.pressureCD = calculatePressureDrag(conditions, map, warnings);
+ total.baseCD = calculateBaseDrag(conditions, map, warnings);
+ total.cg = getCG(0);
+
+ total.component = rocket;
+ map.put(rocket, total);
+
+
+ for (RocketComponent c: map.keySet()) {
+ f = map.get(c);
+ if (Double.isNaN(f.baseCD) && Double.isNaN(f.pressureCD) &&
+ Double.isNaN(f.frictionCD))
+ continue;
+ if (Double.isNaN(f.baseCD))
+ f.baseCD = 0;
+ if (Double.isNaN(f.pressureCD))
+ f.pressureCD = 0;
+ if (Double.isNaN(f.frictionCD))
+ f.frictionCD = 0;
+ f.CD = f.baseCD + f.pressureCD + f.frictionCD;
+ f.Caxial = calculateAxialDrag(conditions, f.CD);
+ }
+
+ return map;
+ }
+
+
+
+ @Override
+ public AerodynamicForces getAerodynamicForces(double time, FlightConditions conditions,
+ WarningSet warnings) {
+
+ if (warnings == null)
+ warnings = ignoreWarningSet;
+
+ // Calculate non-axial force data
+ AerodynamicForces total = getNonAxial(conditions, null, warnings);
+
+ // Calculate friction data
+ total.frictionCD = calculateFrictionDrag(conditions, null, warnings);
+ total.pressureCD = calculatePressureDrag(conditions, null, warnings);
+ total.baseCD = calculateBaseDrag(conditions, null, warnings);
+
+ total.CD = total.frictionCD + total.pressureCD + total.baseCD;
+
+ total.Caxial = calculateAxialDrag(conditions, total.CD);
+
+
+ // Calculate CG and moments of inertia
+ total.cg = this.getCG(time);
+ total.longitudalInertia = this.getLongitudalInertia(time);
+ total.rotationalInertia = this.getRotationalInertia(time);
+
+
+ // Calculate pitch and yaw damping moments
+ calculateDampingMoments(conditions, total);
+ total.Cm -= total.pitchDampingMoment;
+ total.Cyaw -= total.yawDampingMoment;
+
+
+// System.out.println("Conditions are "+conditions + "
+// pitch rate="+conditions.getPitchRate());
+// System.out.println("Total Cm="+total.Cm+" damping effect="+
+// (12 * Math.signum(conditions.getPitchRate()) *
+// MathUtil.pow2(conditions.getPitchRate()) /
+// MathUtil.pow2(conditions.getVelocity())));
+
+// double ef = Math.abs(12 *
+// MathUtil.pow2(conditions.getPitchRate()) /
+// MathUtil.pow2(conditions.getVelocity()));
+//
+//// System.out.println("maxEffect="+maxEffect);
+// total.Cm -= 12 * Math.signum(conditions.getPitchRate()) *
+// MathUtil.pow2(conditions.getPitchRate()) /
+// MathUtil.pow2(conditions.getVelocity());
+//
+// total.Cyaw -= 0.06 * Math.signum(conditions.getYawRate()) *
+// MathUtil.pow2(conditions.getYawRate()) /
+// MathUtil.pow2(conditions.getVelocity());
+
+ return total;
+ }
+
+
+
+ @Override
+ public AerodynamicForces getAxialForces(double time,
+ FlightConditions conditions, WarningSet warnings) {
+
+ if (warnings == null)
+ warnings = ignoreWarningSet;
+
+ AerodynamicForces total = new AerodynamicForces();
+ total.zero();
+
+ // Calculate friction data
+ total.frictionCD = calculateFrictionDrag(conditions, null, warnings);
+ total.pressureCD = calculatePressureDrag(conditions, null, warnings);
+ total.baseCD = calculateBaseDrag(conditions, null, warnings);
+
+ total.CD = total.frictionCD + total.pressureCD + total.baseCD;
+
+ total.Caxial = calculateAxialDrag(conditions, total.CD);
+
+ // Calculate CG and moments of inertia
+ total.cg = this.getCG(time);
+ total.longitudalInertia = this.getLongitudalInertia(time);
+ total.rotationalInertia = this.getRotationalInertia(time);
+
+ return total;
+ }
+
+
+
+
+
+ /*
+ * Perform the actual CP calculation.
+ */
+ private AerodynamicForces getNonAxial(FlightConditions conditions,
+ Map<RocketComponent, AerodynamicForces> map, WarningSet warnings) {
+
+ AerodynamicForces total = new AerodynamicForces();
+ total.zero();
+
+ double radius = 0; // aft radius of previous component
+ double componentX = 0; // aft coordinate of previous component
+ AerodynamicForces forces = new AerodynamicForces();
+
+ if (warnings == null)
+ warnings = ignoreWarningSet;
+
+ if (conditions.getAOA() > 17.5*Math.PI/180)
+ warnings.add(new Warning.LargeAOA(conditions.getAOA()));
+
+ checkCache();
+
+ if (calcMap == null)
+ buildCalcMap();
+
+ for (RocketComponent component: configuration) {
+
+ // Skip non-aerodynamic components
+ if (!component.isAerodynamic())
+ continue;
+
+ // Check for discontinuities
+ if (component instanceof SymmetricComponent) {
+ SymmetricComponent sym = (SymmetricComponent) component;
+ // TODO:LOW: Ignores other cluster components (not clusterable)
+ double x = component.toAbsolute(Coordinate.NUL)[0].x;
+
+ // Check for lengthwise discontinuity
+ if (x > componentX + 0.0001){
+ if (!MathUtil.equals(radius, 0)) {
+ warnings.add(Warning.DISCONTINUITY);
+ radius = 0;
+ }
+ }
+ componentX = component.toAbsolute(new Coordinate(component.getLength()))[0].x;
+
+ // Check for radius discontinuity
+ if (!MathUtil.equals(sym.getForeRadius(), radius)) {
+ warnings.add(Warning.DISCONTINUITY);
+ // TODO: MEDIUM: Apply correction to values to cp and to map
+ }
+ radius = sym.getAftRadius();
+ }
+
+ // Call calculation method
+ forces.zero();
+ calcMap.get(component).calculateNonaxialForces(conditions, forces, warnings);
+ forces.cp = component.toAbsolute(forces.cp)[0];
+ forces.Cm = forces.CN * forces.cp.x / conditions.getRefLength();
+// System.out.println(" CN="+forces.CN+" cp.x="+forces.cp.x+" Cm="+forces.Cm);
+
+ if (map != null) {
+ AerodynamicForces f = map.get(component);
+
+ f.cp = forces.cp;
+ f.CNa = forces.CNa;
+ f.CN = forces.CN;
+ f.Cm = forces.Cm;
+ f.Cside = forces.Cside;
+ f.Cyaw = forces.Cyaw;
+ f.Croll = forces.Croll;
+ f.CrollDamp = forces.CrollDamp;
+ f.CrollForce = forces.CrollForce;
+ }
+
+ total.cp = total.cp.average(forces.cp);
+ total.CNa += forces.CNa;
+ total.CN += forces.CN;
+ total.Cm += forces.Cm;
+ total.Cside += forces.Cside;
+ total.Cyaw += forces.Cyaw;
+ total.Croll += forces.Croll;
+ total.CrollDamp += forces.CrollDamp;
+ total.CrollForce += forces.CrollForce;
+ }
+
+ return total;
+ }
+
+
+
+
+ //////////////// DRAG CALCULATIONS ////////////////
+
+
+ private double calculateFrictionDrag(FlightConditions conditions,
+ Map<RocketComponent, AerodynamicForces> map, WarningSet set) {
+ double c1=1.0, c2=1.0;
+
+ double mach = conditions.getMach();
+ double Re;
+ double Cf;
+
+ if (calcMap == null)
+ buildCalcMap();
+
+ Re = conditions.getVelocity() * configuration.getLength() /
+ conditions.getAtmosphericConditions().getKinematicViscosity();
+
+// System.out.printf("Re=%.3e ", Re);
+
+ // Calculate the skin friction coefficient (assume non-roughness limited)
+ if (configuration.getRocket().isPerfectFinish()) {
+
+// System.out.printf("Perfect finish: Re=%f ",Re);
+ // Assume partial laminar layer. Roughness-limitation is checked later.
+ if (Re < 1e4) {
+ // Too low, constant
+ Cf = 1.33e-2;
+// System.out.printf("constant Cf=%f ",Cf);
+ } else if (Re < 5.39e5) {
+ // Fully laminar
+ Cf = 1.328 / Math.sqrt(Re);
+// System.out.printf("basic Cf=%f ",Cf);
+ } else {
+ // Transitional
+ Cf = 1.0/pow2(1.50 * Math.log(Re) - 5.6) - 1700/Re;
+// System.out.printf("transitional Cf=%f ",Cf);
+ }
+
+ // Compressibility correction
+
+ if (mach < 1.1) {
+ // Below Re=1e6 no correction
+ if (Re > 1e6) {
+ if (Re < 3e6) {
+ c1 = 1 - 0.1*pow2(mach)*(Re-1e6)/2e6; // transition to turbulent
+ } else {
+ c1 = 1 - 0.1*pow2(mach);
+ }
+ }
+ }
+ if (mach > 0.9) {
+ if (Re > 1e6) {
+ if (Re < 3e6) {
+ c2 = 1 + (1.0 / Math.pow(1+0.045*pow2(mach), 0.25) -1) * (Re-1e6)/2e6;
+ } else {
+ c2 = 1.0 / Math.pow(1+0.045*pow2(mach), 0.25);
+ }
+ }
+ }
+
+// System.out.printf("c1=%f c2=%f\n", c1,c2);
+ // Applying continuously around Mach 1
+ if (mach < 0.9) {
+ Cf *= c1;
+ } else if (mach < 1.1) {
+ Cf *= (c2 * (mach-0.9)/0.2 + c1 * (1.1-mach)/0.2);
+ } else {
+ Cf *= c2;
+ }
+
+// System.out.printf("M=%f Cf=%f (smooth)\n",mach,Cf);
+
+ } else {
+
+ // Assume fully turbulent. Roughness-limitation is checked later.
+ if (Re < 1e4) {
+ // Too low, constant
+ Cf = 1.48e-2;
+// System.out.printf("LOW-TURB ");
+ } else {
+ // Turbulent
+ Cf = 1.0/pow2(1.50 * Math.log(Re) - 5.6);
+// System.out.printf("NORMAL-TURB ");
+ }
+
+ // Compressibility correction
+
+ if (mach < 1.1) {
+ c1 = 1 - 0.1*pow2(mach);
+ }
+ if (mach > 0.9) {
+ c2 = 1/Math.pow(1 + 0.15*pow2(mach), 0.58);
+ }
+ // Applying continuously around Mach 1
+ if (mach < 0.9) {
+ Cf *= c1;
+ } else if (mach < 1.1) {
+ Cf *= c2 * (mach-0.9)/0.2 + c1 * (1.1-mach)/0.2;
+ } else {
+ Cf *= c2;
+ }
+
+// System.out.printf("M=%f, Cd=%f (turbulent)\n", mach,Cf);
+
+ }
+
+ // Roughness-limited value correction term
+ double roughnessCorrection;
+ if (mach < 0.9) {
+ roughnessCorrection = 1 - 0.1*pow2(mach);
+ } else if (mach > 1.1) {
+ roughnessCorrection = 1/(1 + 0.18*pow2(mach));
+ } else {
+ c1 = 1 - 0.1*pow2(0.9);
+ c2 = 1.0/(1+0.18 * pow2(1.1));
+ roughnessCorrection = c2 * (mach-0.9)/0.2 + c1 * (1.1-mach)/0.2;
+ }
+
+// System.out.printf("Cf=%.3f ", Cf);
+
+
+ /*
+ * Calculate the friction drag coefficient.
+ *
+ * The body wetted area is summed up and finally corrected with the rocket
+ * fineness ratio (calculated in the same iteration). The fins are corrected
+ * for thickness as we go on.
+ */
+
+ double finFriction = 0;
+ double bodyFriction = 0;
+ double maxR=0, len=0;
+
+ double[] roughnessLimited = new double[Finish.values().length];
+ Arrays.fill(roughnessLimited, Double.NaN);
+
+ for (RocketComponent c: configuration) {
+
+ // Consider only SymmetricComponents and FinSets:
+ if (!(c instanceof SymmetricComponent) &&
+ !(c instanceof FinSet))
+ continue;
+
+ // Calculate the roughness-limited friction coefficient
+ Finish finish = ((ExternalComponent)c).getFinish();
+ if (Double.isNaN(roughnessLimited[finish.ordinal()])) {
+ roughnessLimited[finish.ordinal()] =
+ 0.032 * Math.pow(finish.getRoughnessSize()/configuration.getLength(), 0.2) *
+ roughnessCorrection;
+
+// System.out.printf("roughness["+finish+"]=%.3f ",
+// roughnessLimited[finish.ordinal()]);
+ }
+
+ /*
+ * Actual Cf is maximum of Cf and the roughness-limited value.
+ * For perfect finish require additionally that Re > 1e6
+ */
+ double componentCf;
+ if (configuration.getRocket().isPerfectFinish()) {
+
+ // For perfect finish require Re > 1e6
+ if ((Re > 1.0e6) && (roughnessLimited[finish.ordinal()] > Cf)) {
+ componentCf = roughnessLimited[finish.ordinal()];
+// System.out.printf(" rl=%f Cf=%f (perfect=%b)\n",
+// roughnessLimited[finish.ordinal()],
+// Cf,rocket.isPerfectFinish());
+
+// System.out.printf("LIMITED ");
+ } else {
+ componentCf = Cf;
+// System.out.printf("NORMAL ");
+ }
+
+ } else {
+
+ // For fully turbulent use simple max
+ componentCf = Math.max(Cf, roughnessLimited[finish.ordinal()]);
+
+ }
+
+// System.out.printf("compCf=%.3f ", componentCf);
+
+
+
+
+ // Calculate the friction drag:
+ if (c instanceof SymmetricComponent) {
+
+ SymmetricComponent s = (SymmetricComponent)c;
+
+ bodyFriction += componentCf * s.getComponentWetArea();
+
+ if (map != null) {
+ // Corrected later
+ map.get(c).frictionCD = componentCf * s.getComponentWetArea()
+ / conditions.getRefArea();
+ }
+
+ double r = Math.max(s.getForeRadius(), s.getAftRadius());
+ if (r > maxR)
+ maxR = r;
+ len += c.getLength();
+
+ } else if (c instanceof FinSet) {
+
+ FinSet f = (FinSet)c;
+ double mac = ((FinSetCalc)calcMap.get(c)).getMACLength();
+ double cd = componentCf * (1 + 2*f.getThickness()/mac) *
+ 2*f.getFinCount() * f.getFinArea();
+ finFriction += cd;
+
+ if (map != null) {
+ map.get(c).frictionCD = cd / conditions.getRefArea();
+ }
+
+ }
+
+ }
+ // fB may be POSITIVE_INFINITY, but that's ok for us
+ double fB = (len+0.0001) / maxR;
+ double correction = (1 + 1.0/(2*fB));
+
+ // Correct body data in map
+ if (map != null) {
+ for (RocketComponent c: map.keySet()) {
+ if (c instanceof SymmetricComponent) {
+ map.get(c).frictionCD *= correction;
+ }
+ }
+ }
+
+// System.out.printf("\n");
+ return (finFriction + correction*bodyFriction) / conditions.getRefArea();
+ }
+
+
+
+ private double calculatePressureDrag(FlightConditions conditions,
+ Map<RocketComponent, AerodynamicForces> map, WarningSet warnings) {
+
+ double stagnation, base, total;
+ double radius = 0;
+
+ if (calcMap == null)
+ buildCalcMap();
+
+ stagnation = calculateStagnationCD(conditions.getMach());
+ base = calculateBaseCD(conditions.getMach());
+
+ total = 0;
+ for (RocketComponent c: configuration) {
+ if (!c.isAerodynamic())
+ continue;
+
+ // Pressure fore drag
+ double cd = calcMap.get(c).calculatePressureDragForce(conditions, stagnation, base,
+ warnings);
+ total += cd;
+
+ if (map != null) {
+ map.get(c).pressureCD = cd;
+ }
+
+
+ // Stagnation drag
+ if (c instanceof SymmetricComponent) {
+ SymmetricComponent s = (SymmetricComponent)c;
+
+ if (radius < s.getForeRadius()) {
+ double area = Math.PI*(pow2(s.getForeRadius()) - pow2(radius));
+ cd = stagnation * area / conditions.getRefArea();
+ total += cd;
+ if (map != null) {
+ map.get(c).pressureCD += cd;
+ }
+ }
+
+ radius = s.getAftRadius();
+ }
+ }
+
+ return total;
+ }
+
+
+ private double calculateBaseDrag(FlightConditions conditions,
+ Map<RocketComponent, AerodynamicForces> map, WarningSet warnings) {
+
+ double base, total;
+ double radius = 0;
+ RocketComponent prevComponent = null;
+
+ if (calcMap == null)
+ buildCalcMap();
+
+ base = calculateBaseCD(conditions.getMach());
+ total = 0;
+
+ for (RocketComponent c: configuration) {
+ if (!(c instanceof SymmetricComponent))
+ continue;
+
+ SymmetricComponent s = (SymmetricComponent)c;
+
+ if (radius > s.getForeRadius()) {
+ double area = Math.PI*(pow2(radius) - pow2(s.getForeRadius()));
+ double cd = base * area / conditions.getRefArea();
+ total += cd;
+ if (map != null) {
+ map.get(prevComponent).baseCD = cd;
+ }
+ }
+
+ radius = s.getAftRadius();
+ prevComponent = c;
+ }
+
+ if (radius > 0) {
+ double area = Math.PI*pow2(radius);
+ double cd = base * area / conditions.getRefArea();
+ total += cd;
+ if (map != null) {
+ map.get(prevComponent).baseCD = cd;
+ }
+ }
+
+ return total;
+ }
+
+
+
+ public static double calculateStagnationCD(double m) {
+ double pressure;
+ if (m <=1) {
+ pressure = 1 + pow2(m)/4 + pow2(pow2(m))/40;
+ } else {
+ pressure = 1.84 - 0.76/pow2(m) + 0.166/pow2(pow2(m)) + 0.035/pow2(m*m*m);
+ }
+ return 0.85 * pressure;
+ }
+
+
+ public static double calculateBaseCD(double m) {
+ if (m <= 1) {
+ return 0.12 + 0.13 * m*m;
+ } else {
+ return 0.25 / m;
+ }
+ }
+
+
+
+ private static final double[] axialDragPoly1, axialDragPoly2;
+ static {
+ PolyInterpolator interpolator;
+ interpolator = new PolyInterpolator(
+ new double[] { 0, 17*Math.PI/180 },
+ new double[] { 0, 17*Math.PI/180 }
+ );
+ axialDragPoly1 = interpolator.interpolator(1, 1.3, 0, 0);
+
+ interpolator = new PolyInterpolator(
+ new double[] { 17*Math.PI/180, Math.PI/2 },
+ new double[] { 17*Math.PI/180, Math.PI/2 },
+ new double[] { Math.PI/2 }
+ );
+ axialDragPoly2 = interpolator.interpolator(1.3, 0, 0, 0, 0);
+ }
+
+
+ /**
+ * Calculate the axial drag from the total drag coefficient.
+ *
+ * @param conditions
+ * @param cd
+ * @return
+ */
+ private double calculateAxialDrag(FlightConditions conditions, double cd) {
+ double aoa = MathUtil.clamp(conditions.getAOA(), 0, Math.PI);
+ double mul;
+
+// double sinaoa = conditions.getSinAOA();
+// return cd * (1 + Math.min(sinaoa, 0.25));
+
+
+ if (aoa > Math.PI/2)
+ aoa = Math.PI - aoa;
+ if (aoa < 17*Math.PI/180)
+ mul = PolyInterpolator.eval(aoa, axialDragPoly1);
+ else
+ mul = PolyInterpolator.eval(aoa, axialDragPoly2);
+
+ if (conditions.getAOA() < Math.PI/2)
+ return mul * cd;
+ else
+ return -mul * cd;
+ }
+
+
+ private void calculateDampingMoments(FlightConditions conditions,
+ AerodynamicForces total) {
+
+ // Calculate pitch and yaw damping moments
+ if (conditions.getPitchRate() > 0.1 || conditions.getYawRate() > 0.1 || true) {
+ double mul = getDampingMultiplier(conditions, total.cg.x);
+ double pitch = conditions.getPitchRate();
+ double yaw = conditions.getYawRate();
+ double vel = conditions.getVelocity();
+
+// double Cm = total.Cm - total.CN * total.cg.x / conditions.getRefLength();
+// System.out.printf("Damping pitch/yaw, mul=%.4f pitch rate=%.4f "+
+// "Cm=%.4f / %.4f effect=%.4f aoa=%.4f\n", mul, pitch, total.Cm, Cm,
+// -(mul * MathUtil.sign(pitch) * pow2(pitch/vel)),
+// conditions.getAOA()*180/Math.PI);
+
+ mul *= 3; // TODO: Higher damping yields much more realistic apogee turn
+
+// total.Cm -= mul * pitch / pow2(vel);
+// total.Cyaw -= mul * yaw / pow2(vel);
+ total.pitchDampingMoment = mul * MathUtil.sign(pitch) * pow2(pitch/vel);
+ total.yawDampingMoment = mul * MathUtil.sign(yaw) * pow2(yaw/vel);
+ } else {
+ total.pitchDampingMoment = 0;
+ total.yawDampingMoment = 0;
+ }
+
+ }
+
+ // TODO: MEDIUM: Are the rotation etc. being added correctly? sin/cos theta?
+
+
+ private double getDampingMultiplier(FlightConditions conditions, double cgx) {
+ if (cacheDiameter < 0) {
+ double area = 0;
+ cacheLength = 0;
+ cacheDiameter = 0;
+
+ for (RocketComponent c: configuration) {
+ if (c instanceof SymmetricComponent) {
+ SymmetricComponent s = (SymmetricComponent)c;
+ area += s.getComponentPlanformArea();
+ cacheLength += s.getLength();
+ }
+ }
+ if (cacheLength > 0)
+ cacheDiameter = area / cacheLength;
+ }
+
+ double mul;
+
+ // Body
+ mul = 0.275 * cacheDiameter / (conditions.getRefArea() * conditions.getRefLength());
+ mul *= (MathUtil.pow4(cgx) + MathUtil.pow4(cacheLength - cgx));
+
+ // Fins
+ // TODO: LOW: This could be optimized a lot...
+ for (RocketComponent c: configuration) {
+ if (c instanceof FinSet) {
+ FinSet f = (FinSet)c;
+ mul += 0.6 * Math.min(f.getFinCount(), 4) * f.getFinArea() *
+ MathUtil.pow3(Math.abs(f.toAbsolute(new Coordinate(
+ ((FinSetCalc)calcMap.get(f)).getMidchordPos()))[0].x
+ - cgx)) /
+ (conditions.getRefArea() * conditions.getRefLength());
+ }
+ }
+
+ return mul;
+ }
+
+
+
+ //////// The calculator map
+
+ @Override
+ protected void voidAerodynamicCache() {
+ super.voidAerodynamicCache();
+
+ calcMap = null;
+ cacheDiameter = -1;
+ cacheLength = -1;
+ }
+
+
+ private void buildCalcMap() {
+ Iterator<RocketComponent> iterator;
+
+ calcMap = new HashMap<RocketComponent, RocketComponentCalc>();
+
+ iterator = rocket.deepIterator();
+ while (iterator.hasNext()) {
+ RocketComponent c = iterator.next();
+
+ if (!c.isAerodynamic())
+ continue;
+
+ calcMap.put(c, (RocketComponentCalc) Reflection.construct(BARROWMAN_PACKAGE,
+ c, BARROWMAN_SUFFIX, c));
+ }
+ }
+
+
+
+
+ public static void main(String[] arg) {
+
+ PolyInterpolator interpolator;
+
+ interpolator = new PolyInterpolator(
+ new double[] { 0, 17*Math.PI/180 },
+ new double[] { 0, 17*Math.PI/180 }
+ );
+ double[] poly1 = interpolator.interpolator(1, 1.3, 0, 0);
+
+ interpolator = new PolyInterpolator(
+ new double[] { 17*Math.PI/180, Math.PI/2 },
+ new double[] { 17*Math.PI/180, Math.PI/2 },
+ new double[] { Math.PI/2 }
+ );
+ double[] poly2 = interpolator.interpolator(1.3, 0, 0, 0, 0);
+
+
+ for (double a=0; a<=180.1; a++) {
+ double r = a*Math.PI/180;
+ if (r > Math.PI/2)
+ r = Math.PI - r;
+
+ double value;
+ if (r < 18*Math.PI/180)
+ value = PolyInterpolator.eval(r, poly1);
+ else
+ value = PolyInterpolator.eval(r, poly2);
+
+ System.out.println(""+a+" "+value);
+ }
+
+ System.exit(0);
+
+
+ Rocket normal = Test.makeRocket();
+ Rocket perfect = Test.makeRocket();
+ normal.setPerfectFinish(false);
+ perfect.setPerfectFinish(true);
+
+ Configuration confNormal = new Configuration(normal);
+ Configuration confPerfect = new Configuration(perfect);
+
+ for (RocketComponent c: confNormal) {
+ if (c instanceof ExternalComponent) {
+ ((ExternalComponent)c).setFinish(Finish.NORMAL);
+ }
+ }
+ for (RocketComponent c: confPerfect) {
+ if (c instanceof ExternalComponent) {
+ ((ExternalComponent)c).setFinish(Finish.NORMAL);
+ }
+ }
+
+
+ confNormal.setToStage(0);
+ confPerfect.setToStage(0);
+
+
+
+ BarrowmanCalculator calcNormal = new BarrowmanCalculator(confNormal);
+ BarrowmanCalculator calcPerfect = new BarrowmanCalculator(confPerfect);
+
+ FlightConditions conditions = new FlightConditions(confNormal);
+
+ for (double mach=0; mach < 3; mach += 0.1) {
+ conditions.setMach(mach);
+
+ Map<RocketComponent, AerodynamicForces> data =
+ calcNormal.getForceAnalysis(conditions, null);
+ AerodynamicForces forcesNormal = data.get(normal);
+
+ data = calcPerfect.getForceAnalysis(conditions, null);
+ AerodynamicForces forcesPerfect = data.get(perfect);
+
+ System.out.printf("%f %f %f %f %f %f %f\n",mach,
+ forcesNormal.pressureCD, forcesPerfect.pressureCD,
+ forcesNormal.frictionCD, forcesPerfect.frictionCD,
+ forcesNormal.CD, forcesPerfect.CD);
+ }
+
+
+
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.aerodynamics;
+
+import net.sf.openrocket.util.PolyInterpolator;
+
+public class ConeDragTest {
+
+ private static final double DELTA = 0.01;
+ private static final double SUBSONIC = 0.0;
+ private static final double SUPERSONIC = 1.3;
+
+
+ private static final PolyInterpolator polyInt2 = new PolyInterpolator(
+ new double[] {1.0, SUPERSONIC}
+ ,new double[] {1.0, SUPERSONIC}
+ ,new double[] {SUPERSONIC}
+ );
+
+ private final double angle;
+ private final double sin;
+ private final double ratio;
+
+ private final double[] int2;
+
+ // Coefficients for subsonic interpolation a * M^b + c
+ private final double a, b, c;
+
+
+
+ public ConeDragTest(double angle) {
+ this.angle = angle;
+ this.sin = Math.sin(angle);
+ this.ratio = 1.0 / (2*Math.tan(angle));
+
+ double dsuper = (supersonic(SUPERSONIC+DELTA) - supersonic(SUPERSONIC))/DELTA;
+
+
+ c = subsonic(0);
+ a = sonic() - c;
+ b = sonicDerivative() / a;
+
+
+ int2 = polyInt2.interpolator(
+ sonic(), supersonic(SUPERSONIC)
+ , sonicDerivative(), dsuper
+ ,0
+ );
+
+ System.err.println("At mach1: CD="+sin+" dCD/dM="+(4.0/2.4*(1-0.5*sin)));
+
+ }
+
+ private double subsonic(double m) {
+ return 0.8*sin*sin/Math.sqrt(1-m*m);
+ }
+
+ private double sonic() {
+ return sin;
+ }
+
+ private double sonicDerivative() {
+ return 4.0/2.4*(1-0.5*sin);
+ }
+
+ private double supersonic(double m) {
+ return 2.1 * sin*sin + 0.5*sin/Math.sqrt(m*m-1);
+ }
+
+
+
+
+ public double getCD(double m) {
+ if (m >= SUPERSONIC)
+ return supersonic(m);
+
+ if (m <= 1.0)
+ return a * Math.pow(m,b) + c;
+ return PolyInterpolator.eval(m, int2);
+
+// return PolyInterpolator.eval(m, interpolator);
+ }
+
+
+
+ public static void main(String[] arg) {
+
+ ConeDragTest cone10 = new ConeDragTest(10.0*Math.PI/180);
+ ConeDragTest cone20 = new ConeDragTest(20.0*Math.PI/180);
+ ConeDragTest cone30 = new ConeDragTest(30.0*Math.PI/180);
+
+ ConeDragTest coneX = new ConeDragTest(5.0*Math.PI/180);
+
+ for (double m=0; m < 4.0001; m+=0.02) {
+ System.out.println(m + " "
+ + cone10.getCD(m) + " "
+ + cone20.getCD(m) + " "
+ + cone30.getCD(m) + " "
+// + coneX.getCD(m)
+ );
+ }
+
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.aerodynamics;
+
+/**
+ * A class containing more accurate methods for computing the atmospheric properties.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class ExactAtmosphericConditions extends AtmosphericConditions {
+
+ @Override
+ public double getDensity() {
+ // TODO Auto-generated method stub
+ return super.getDensity();
+ }
+
+ @Override
+ public double getKinematicViscosity() {
+ // TODO Auto-generated method stub
+ return super.getKinematicViscosity();
+ }
+
+ @Override
+ public double getMachSpeed() {
+ return 331.3 * Math.sqrt(1 + (temperature - 273.15)/273.15);
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.aerodynamics;
+
+import static net.sf.openrocket.aerodynamics.AtmosphericConditions.R;
+import net.sf.openrocket.util.MathUtil;
+
+
+/**
+ * An atmospheric temperature/pressure model based on the International Standard Atmosphere
+ * (ISA). The no-argument constructor creates an {@link AtmosphericModel} that corresponds
+ * to the ISA model. It is extended by the other constructors to allow defining a custom
+ * first layer. The base temperature and pressure are as given, and all other values
+ * are calculated based on these.
+ * <p>
+ * TODO: LOW: Values at altitudes over 32km differ from standard results by ~5%.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class ExtendedISAModel extends AtmosphericModel {
+
+ public static final double STANDARD_TEMPERATURE = 288.15;
+ public static final double STANDARD_PRESSURE = 101325;
+
+ private static final double G = 9.80665;
+
+ private final double[] layer = {0, 11000, 20000, 32000, 47000, 51000, 71000, 84852};
+ private final double[] baseTemperature = {
+ 288.15, 216.65, 216.65, 228.65, 270.65, 270.65, 214.65, 186.95
+ };
+ private final double[] basePressure = new double[layer.length];
+
+
+ /**
+ * Construct the standard ISA model.
+ */
+ public ExtendedISAModel() {
+ this(STANDARD_TEMPERATURE, STANDARD_PRESSURE);
+ }
+
+ /**
+ * Construct an extended model with the given temperature and pressure at MSL.
+ *
+ * @param temperature the temperature at MSL.
+ * @param pressure the pressure at MSL.
+ */
+ public ExtendedISAModel(double temperature, double pressure) {
+ this(0, temperature, pressure);
+ }
+
+
+ /**
+ * Construct an extended model with the given temperature and pressure at the
+ * specified altitude. Conditions below the given altitude cannot be calculated,
+ * and the values at the specified altitude will be returned instead. The altitude
+ * must be lower than the altitude of the next ISA standard layer (below 11km).
+ *
+ * @param altitude the altitude of the measurements.
+ * @param temperature the temperature.
+ * @param pressure the pressure.
+ * @throws IllegalArgumentException if the altitude exceeds the second layer boundary
+ * of the ISA model (over 11km).
+ */
+ public ExtendedISAModel(double altitude, double temperature, double pressure) {
+ if (altitude >= layer[1]) {
+ throw new IllegalArgumentException("Too high first altitude: "+altitude);
+ }
+
+ layer[0] = altitude;
+ baseTemperature[0] = temperature;
+ basePressure[0] = pressure;
+
+ for (int i=1; i < basePressure.length; i++) {
+ basePressure[i] = getExactConditions(layer[i]-1).pressure;
+ }
+ }
+
+
+ @Override
+ public AtmosphericConditions getExactConditions(double altitude) {
+ altitude = MathUtil.clamp(altitude, layer[0], layer[layer.length-1]);
+ int n;
+ for (n=0; n < layer.length-1; n++) {
+ if (layer[n+1] > altitude)
+ break;
+ }
+
+ double rate = (baseTemperature[n+1] - baseTemperature[n]) / (layer[n+1] - layer[n]);
+
+ double t = baseTemperature[n] + (altitude - layer[n]) * rate;
+ double p;
+ if (Math.abs(rate) > 0.001) {
+ p = basePressure[n] *
+ Math.pow(1 + (altitude-layer[n])*rate/baseTemperature[n], -G/(rate*R));
+ } else {
+ p = basePressure[n] *
+ Math.exp(-(altitude-layer[n])*G/(R*baseTemperature[n]));
+ }
+
+ return new AtmosphericConditions(t,p);
+ }
+
+ @Override
+ public double getMaxAltitude() {
+ return layer[layer.length-1];
+ }
+
+
+ public static void main(String foo[]) {
+ ExtendedISAModel model1 = new ExtendedISAModel();
+ ExtendedISAModel model2 = new ExtendedISAModel(278.15,100000);
+
+ for (double alt=0; alt < 80000; alt += 500) {
+ AtmosphericConditions cond1 = model1.getConditions(alt);
+ AtmosphericConditions cond2 = model2.getConditions(alt);
+
+ AtmosphericConditions diff = new AtmosphericConditions();
+ diff.pressure = (cond2.pressure - cond1.pressure)/cond1.pressure*100;
+ diff.temperature = (cond2.temperature - cond1.temperature)/cond1.temperature*100;
+ System.out.println("alt=" + alt +
+ ": std:" + cond1 + " mod:" + cond2 + " diff:" + diff);
+ }
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.aerodynamics;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.util.ChangeSource;
+import net.sf.openrocket.util.MathUtil;
+
+
+public class FlightConditions implements Cloneable, ChangeSource {
+
+ private List<ChangeListener> listenerList = new ArrayList<ChangeListener>();
+ private ChangeEvent event = new ChangeEvent(this);
+
+ /** Modification count */
+ private int modCount = 0;
+
+ /** Reference length used in calculations. */
+ private double refLength = 1.0;
+
+ /** Reference area used in calculations. */
+ private double refArea = Math.PI * 0.25;
+
+
+ /** Angle of attack. */
+ private double aoa = 0;
+
+ /** Sine of the angle of attack. */
+ private double sinAOA = 0;
+
+ /**
+ * The fraction <code>sin(aoa) / aoa</code>. At an AOA of zero this value
+ * must be one. This value may be used in many cases to avoid checking for
+ * division by zero.
+ */
+ private double sincAOA = 1.0;
+
+ /** Lateral wind direction. */
+ private double theta = 0;
+
+ /** Current Mach speed. */
+ private double mach = 0.3;
+
+ /**
+ * Sqrt(1 - M^2) for M<1
+ * Sqrt(M^2 - 1) for M>1
+ */
+ private double beta = Math.sqrt(1 - mach*mach);
+
+
+ /** Current roll rate. */
+ private double rollRate = 0;
+
+ private double pitchRate = 0;
+ private double yawRate = 0;
+
+
+ private AtmosphericConditions atmosphericConditions = new AtmosphericConditions();
+
+
+
+ /**
+ * Sole constructor. The reference length is initialized to the reference length
+ * of the <code>Configuration</code>, and the reference area accordingly.
+ * If <code>config</code> is <code>null</code>, then the reference length is set
+ * to 1 meter.
+ *
+ * @param config the configuration of which the reference length is taken.
+ */
+ public FlightConditions(Configuration config) {
+ if (config != null)
+ setRefLength(config.getReferenceLength());
+ }
+
+
+ /**
+ * Set the reference length from the given configuration.
+ * @param config the configuration from which to get the reference length.
+ */
+ public void setReference(Configuration config) {
+ setRefLength(config.getReferenceLength());
+ }
+
+
+ /**
+ * Set the reference length and area.
+ */
+ public void setRefLength(double length) {
+ refLength = length;
+
+ refArea = Math.PI * MathUtil.pow2(length/2);
+ fireChangeEvent();
+ }
+
+ /**
+ * Return the reference length.
+ */
+ public double getRefLength() {
+ return refLength;
+ }
+
+ /**
+ * Set the reference area and length.
+ */
+ public void setRefArea(double area) {
+ refArea = area;
+ refLength = Math.sqrt(area / Math.PI)*2;
+ fireChangeEvent();
+ }
+
+ /**
+ * Return the reference area.
+ */
+ public double getRefArea() {
+ return refArea;
+ }
+
+
+ /**
+ * Sets the angle of attack. It calculates values also for the methods
+ * {@link #getSinAOA()} and {@link #getSincAOA()}.
+ *
+ * @param aoa the angle of attack.
+ */
+ public void setAOA(double aoa) {
+ aoa = MathUtil.clamp(aoa, 0, Math.PI);
+ if (MathUtil.equals(this.aoa, aoa))
+ return;
+
+ this.aoa = aoa;
+ if (aoa < 0.001) {
+ this.sinAOA = aoa;
+ this.sincAOA = 1.0;
+ } else {
+ this.sinAOA = Math.sin(aoa);
+ this.sincAOA = sinAOA / aoa;
+ }
+ fireChangeEvent();
+ }
+
+
+ /**
+ * Sets the angle of attack with the sine. The value <code>sinAOA</code> is assumed
+ * to be the sine of <code>aoa</code> for cases in which this value is known.
+ * The AOA must still be specified, as the sine is not unique in the range
+ * of 0..180 degrees.
+ *
+ * @param aoa the angle of attack in radians.
+ * @param sinAOA the sine of the angle of attack.
+ */
+ public void setAOA(double aoa, double sinAOA) {
+ aoa = MathUtil.clamp(aoa, 0, Math.PI);
+ sinAOA = MathUtil.clamp(sinAOA, 0, 1);
+ if (MathUtil.equals(this.aoa, aoa))
+ return;
+
+ assert(Math.abs(Math.sin(aoa) - sinAOA) < 0.0001) :
+ "Illegal sine: aoa="+aoa+" sinAOA="+sinAOA;
+
+ this.aoa = aoa;
+ this.sinAOA = sinAOA;
+ if (aoa < 0.001) {
+ this.sincAOA = 1.0;
+ } else {
+ this.sincAOA = sinAOA / aoa;
+ }
+ fireChangeEvent();
+ }
+
+
+ /**
+ * Return the angle of attack.
+ */
+ public double getAOA() {
+ return aoa;
+ }
+
+ /**
+ * Return the sine of the angle of attack.
+ */
+ public double getSinAOA() {
+ return sinAOA;
+ }
+
+ /**
+ * Return the sinc of the angle of attack (sin(AOA) / AOA). This method returns
+ * one if the angle of attack is zero.
+ */
+ public double getSincAOA() {
+ return sincAOA;
+ }
+
+
+ /**
+ * Set the direction of the lateral airflow.
+ */
+ public void setTheta(double theta) {
+ if (MathUtil.equals(this.theta, theta))
+ return;
+ this.theta = theta;
+ fireChangeEvent();
+ }
+
+ /**
+ * Return the direction of the lateral airflow.
+ */
+ public double getTheta() {
+ return theta;
+ }
+
+
+ /**
+ * Set the current Mach speed. This should be (but is not required to be) in
+ * reference to the speed of sound of the atmospheric conditions.
+ */
+ public void setMach(double mach) {
+ mach = Math.max(mach, 0);
+ if (MathUtil.equals(this.mach, mach))
+ return;
+
+ this.mach = mach;
+ if (mach < 1)
+ this.beta = Math.sqrt(1 - mach*mach);
+ else
+ this.beta = Math.sqrt(mach*mach - 1);
+ fireChangeEvent();
+ }
+
+ /**
+ * Return the current Mach speed.
+ */
+ public double getMach() {
+ return mach;
+ }
+
+ /**
+ * Returns the current rocket velocity, calculated from the Mach number and the
+ * speed of sound. If either of these parameters are changed, the velocity changes
+ * as well.
+ *
+ * @return the velocity of the rocket.
+ */
+ public double getVelocity() {
+ return mach * atmosphericConditions.getMachSpeed();
+ }
+
+ /**
+ * Sets the Mach speed according to the given velocity and the current speed of sound.
+ *
+ * @param velocity the current velocity.
+ */
+ public void setVelocity(double velocity) {
+ setMach(velocity / atmosphericConditions.getMachSpeed());
+ }
+
+
+ /**
+ * Return sqrt(abs(1 - Mach^2)). This is calculated in the setting call and is
+ * therefore fast.
+ */
+ public double getBeta() {
+ return beta;
+ }
+
+
+ /**
+ * Return the current roll rate.
+ */
+ public double getRollRate() {
+ return rollRate;
+ }
+
+
+ /**
+ * Set the current roll rate.
+ */
+ public void setRollRate(double rate) {
+ if (MathUtil.equals(this.rollRate, rate))
+ return;
+
+ this.rollRate = rate;
+ fireChangeEvent();
+ }
+
+
+ public double getPitchRate() {
+ return pitchRate;
+ }
+
+
+ public void setPitchRate(double pitchRate) {
+ if (MathUtil.equals(this.pitchRate, pitchRate))
+ return;
+ this.pitchRate = pitchRate;
+ fireChangeEvent();
+ }
+
+
+ public double getYawRate() {
+ return yawRate;
+ }
+
+
+ public void setYawRate(double yawRate) {
+ if (MathUtil.equals(this.yawRate, yawRate))
+ return;
+ this.yawRate = yawRate;
+ fireChangeEvent();
+ }
+
+
+ /**
+ * Return the current atmospheric conditions. Note that this method returns a
+ * reference to the {@link AtmosphericConditions} object used by this object.
+ * Changes made to the object will modify the encapsulated object, but will NOT
+ * generate change events.
+ *
+ * @return the current atmospheric conditions.
+ */
+ public AtmosphericConditions getAtmosphericConditions() {
+ return atmosphericConditions;
+ }
+
+ /**
+ * Set the current atmospheric conditions. This method will fire a change event
+ * if a change occurs.
+ */
+ public void setAtmosphericConditions(AtmosphericConditions cond) {
+ if (atmosphericConditions == cond)
+ return;
+ atmosphericConditions = cond;
+ fireChangeEvent();
+ }
+
+
+ /**
+ * Retrieve the modification count of this object. Each time it is modified
+ * the modification count is increased by one.
+ *
+ * @return the number of times this object has been modified since instantiation.
+ */
+ public int getModCount() {
+ return modCount;
+ }
+
+
+ @Override
+ public String toString() {
+ return String.format("FlightConditions[aoa=%.2f\u00b0,theta=%.2f\u00b0,"+
+ "mach=%.2f,rollRate=%.2f]",
+ aoa*180/Math.PI, theta*180/Math.PI, mach, rollRate);
+ }
+
+
+ /**
+ * Return a copy of the flight conditions. The copy has no listeners. The
+ * atmospheric conditions is also cloned.
+ */
+ @Override
+ public FlightConditions clone() {
+ try {
+ FlightConditions cond = (FlightConditions) super.clone();
+ cond.listenerList = new ArrayList<ChangeListener>();
+ cond.event = new ChangeEvent(cond);
+ cond.atmosphericConditions = atmosphericConditions.clone();
+ return cond;
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException("BUG: clone not supported!",e);
+ }
+ }
+
+
+
+ @Override
+ public void addChangeListener(ChangeListener listener) {
+ listenerList.add(0,listener);
+ }
+
+ @Override
+ public void removeChangeListener(ChangeListener listener) {
+ listenerList.remove(listener);
+ }
+
+ protected void fireChangeEvent() {
+ modCount++;
+ ChangeListener[] listeners = listenerList.toArray(new ChangeListener[0]);
+ for (ChangeListener l: listeners) {
+ l.stateChanged(event);
+ }
+ }
+}
--- /dev/null
+package net.sf.openrocket.aerodynamics;
+
+/**
+ * A gravity model based on the International Gravity Formula of 1967. The gravity
+ * value is computed when the object is constructed and later returned as a static
+ * value.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class GravityModel {
+
+ private final double g;
+
+ /**
+ * Construct the static gravity model at the specific latitude (in degrees).
+ * @param latitude the latitude in degrees (-90 ... 90)
+ */
+ public GravityModel(double latitude) {
+ double sin = Math.sin(latitude * Math.PI/180);
+ double sin2 = Math.sin(2 * latitude * Math.PI/180);
+ g = 9.780327 * (1 + 0.0053024 * sin - 0.0000058 * sin2);
+ }
+
+ public double getGravity() {
+ return g;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.aerodynamics;
+
+import net.sf.openrocket.unit.UnitGroup;
+
+public abstract class Warning {
+
+
+ /**
+ * Return a Warning with the specific text.
+ */
+ public static Warning fromString(String text) {
+ return new Warning.Other(text);
+ }
+
+
+ /**
+ * Return <code>true</code> if the <code>other</code> warning should replace
+ * this warning. The method should return <code>true</code> if the other
+ * warning indicates a "worse" condition than the current warning.
+ *
+ * @param other the warning to compare to
+ * @return whether this warning should be replaced
+ */
+ public abstract boolean replaceBy(Warning other);
+
+
+ /**
+ * Two <code>Warning</code>s are by default considered equal if they are of
+ * the same class. Therefore only one instance of a particular warning type
+ * is stored in a {@link WarningSet}. Subclasses may override this method for
+ * more specific functionality.
+ */
+ @Override
+ public boolean equals(Object o) {
+ return (o.getClass() == this.getClass());
+ }
+
+ /**
+ * A <code>hashCode</code> method compatible with the <code>equals</code> method.
+ */
+ @Override
+ public int hashCode() {
+ return this.getClass().hashCode();
+ }
+
+
+
+
+ ///////////// Specific warning classes /////////////
+
+
+ /**
+ * A <code>Warning</code> indicating a large angle of attack was encountered.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+ public static class LargeAOA extends Warning {
+ private double aoa;
+
+ /**
+ * Sole constructor. The argument is the AOA that caused this warning.
+ *
+ * @param aoa the angle of attack that caused this warning
+ */
+ public LargeAOA(double aoa) {
+ this.aoa = aoa;
+ }
+
+ @Override
+ public String toString() {
+ if (Double.isNaN(aoa))
+ return "Large angle of attack encountered.";
+ return ("Large angle of attack encountered (" +
+ UnitGroup.UNITS_ANGLE.getDefaultUnit().toString(aoa) + ").");
+ }
+
+ @Override
+ public boolean replaceBy(Warning other) {
+ if (!(other instanceof LargeAOA))
+ return false;
+
+ LargeAOA o = (LargeAOA)other;
+ if (Double.isNaN(this.aoa)) // If this has value NaN then replace
+ return true;
+ return (o.aoa > this.aoa);
+ }
+ }
+
+
+
+ /**
+ * An unspecified warning type. This warning type holds a <code>String</code>
+ * describing it. Two warnings of this type are considered equal if the strings
+ * are identical.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+ public static class Other extends Warning {
+ private String description;
+
+ public Other(String description) {
+ this.description = description;
+ }
+
+ @Override
+ public String toString() {
+ return description;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof Other))
+ return false;
+
+ Other o = (Other)other;
+ return (o.description.equals(this.description));
+ }
+
+ @Override
+ public int hashCode() {
+ return description.hashCode();
+ }
+
+ @Override
+ public boolean replaceBy(Warning other) {
+ return false;
+ }
+ }
+
+
+ /** A <code>Warning</code> that the body diameter is discontinuous. */
+ public static final Warning DISCONTINUITY =
+ new Other("Discontinuity in rocket body diameter.");
+
+ /** A <code>Warning</code> that the fins are thick compared to the rocket body. */
+ public static final Warning THICK_FIN =
+ new Other("Thick fins may not be modeled accurately.");
+
+ /** A <code>Warning</code> that the fins have jagged edges. */
+ public static final Warning JAGGED_EDGED_FIN =
+ new Other("Jagged-edged fin predictions may be inaccurate.");
+
+ /** A <code>Warning</code> that simulation listeners have affected the simulation */
+ public static final Warning LISTENERS_AFFECTED =
+ new Other("Listeners modified the flight simulation");
+
+ public static final Warning RECOVERY_DEPLOYMENT_WHILE_BURNING =
+ new Other("Recovery device opened while motor still burning.");
+
+
+
+ public static final Warning FILE_INVALID_PARAMETER =
+ new Other("Invalid parameter encountered, ignoring.");
+}
--- /dev/null
+package net.sf.openrocket.aerodynamics;
+
+import java.util.AbstractSet;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+/**
+ * A set that contains multiple <code>Warning</code>s. When adding a
+ * {@link Warning} to this set, the contents is checked for a warning of the
+ * same type. If one is found, then the warning left in the set is determined
+ * by the method {@link #Warning.replaceBy(Warning)}.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class WarningSet extends AbstractSet<Warning> implements Cloneable {
+
+ private ArrayList<Warning> warnings = new ArrayList<Warning>();
+
+
+ /**
+ * Add a <code>Warning</code> to the set. If a warning of the same type
+ * exists in the set, the warning that is left in the set is defined by the
+ * method {@link Warning#replaceBy(Warning)}.
+ */
+ @Override
+ public boolean add(Warning w) {
+ int index = warnings.indexOf(w);
+
+ if (index < 0) {
+ warnings.add(w);
+ return false;
+ }
+
+ Warning old = warnings.get(index);
+ if (old.replaceBy(w)) {
+ warnings.set(index, w);
+ }
+
+ return true;
+ }
+
+ /**
+ * Add a <code>Warning</code> with the specified text to the set. The Warning object
+ * is created using the {@link Warning#fromString(String)} method. If a warning of the
+ * same type exists in the set, the warning that is left in the set is defined by the
+ * method {@link Warning#replaceBy(Warning)}.
+ *
+ * @param s the warning text.
+ */
+ public boolean add(String s) {
+ return add(Warning.fromString(s));
+ }
+
+
+ @Override
+ public Iterator<Warning> iterator() {
+ return warnings.iterator();
+ }
+
+ @Override
+ public int size() {
+ return warnings.size();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public WarningSet clone() {
+ try {
+
+ WarningSet newSet = (WarningSet) super.clone();
+ newSet.warnings = (ArrayList<Warning>) this.warnings.clone();
+ return newSet;
+
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException("CloneNotSupportedException occurred, report bug!",e);
+ }
+ }
+
+
+ @Override
+ public String toString() {
+ String s = "";
+
+ for (Warning w: warnings) {
+ if (s.length() > 0)
+ s = s+",";
+ s += w.toString();
+ }
+ return "WarningSet[" + s + "]";
+ }
+}
--- /dev/null
+package net.sf.openrocket.aerodynamics;\r
+\r
+import java.util.Random;\r
+\r
+import net.sf.openrocket.util.MathUtil;\r
+import net.sf.openrocket.util.PinkNoise;\r
+\r
+\r
+public class WindSimulator {\r
+ \r
+ /** Source for seed numbers. */\r
+ private static final Random seedSource = new Random();\r
+\r
+ /** Pink noise alpha parameter. */\r
+ private static final double ALPHA = 5.0/3.0;\r
+ \r
+ /** Number of poles to use in the pink noise IIR filter. */\r
+ private static final int POLES = 2;\r
+ \r
+ /** The standard deviation of the generated pink noise with the specified number of poles. */\r
+ private static final double STDDEV = 2.252;\r
+ \r
+ /** Time difference between random samples. */\r
+ private static final double DELTA_T = 0.05;\r
+\r
+ \r
+ private double average = 0;\r
+ private double standardDeviation = 0;\r
+ \r
+ private int seed;\r
+ \r
+ private PinkNoise randomSource = null;\r
+ private double time1;\r
+ private double value1, value2;\r
+ \r
+\r
+ /**\r
+ * Construct a new wind simulator with a random starting seed value.\r
+ */\r
+ public WindSimulator() {\r
+ synchronized(seedSource) {\r
+ seed = seedSource.nextInt();\r
+ }\r
+ }\r
+ \r
+ \r
+ \r
+ /**\r
+ * Return the average wind speed.\r
+ * \r
+ * @return the average wind speed.\r
+ */\r
+ public double getAverage() {\r
+ return average;\r
+ }\r
+ /**\r
+ * Set the average wind speed. This method will also modify the\r
+ * standard deviation such that the turbulence intensity remains constant.\r
+ * \r
+ * @param average the average wind speed to set\r
+ */\r
+ public void setAverage(double average) {\r
+ double intensity = getTurbulenceIntensity();\r
+ this.average = Math.max(average, 0);\r
+ setTurbulenceIntensity(intensity);\r
+ }\r
+ \r
+ \r
+ \r
+ /**\r
+ * Return the standard deviation from the average wind speed.\r
+ * \r
+ * @return the standard deviation of the wind speed\r
+ */\r
+ public double getStandardDeviation() {\r
+ return standardDeviation;\r
+ }\r
+ \r
+ /**\r
+ * Set the standard deviation of the average wind speed.\r
+ * \r
+ * @param standardDeviation the standardDeviation to set\r
+ */\r
+ public void setStandardDeviation(double standardDeviation) {\r
+ this.standardDeviation = Math.max(standardDeviation, 0);\r
+ }\r
+ \r
+ \r
+ /**\r
+ * Return the turbulence intensity (standard deviation / average).\r
+ * \r
+ * @return the turbulence intensity\r
+ */\r
+ public double getTurbulenceIntensity() {\r
+ if (MathUtil.equals(average, 0)) {\r
+ if (MathUtil.equals(standardDeviation, 0))\r
+ return 0;\r
+ else\r
+ return 1000;\r
+ }\r
+ return standardDeviation / average;\r
+ }\r
+\r
+ /**\r
+ * Set the standard deviation to match the turbulence intensity.\r
+ * \r
+ * @param intensity the turbulence intensity\r
+ */\r
+ public void setTurbulenceIntensity(double intensity) {\r
+ setStandardDeviation(intensity * average);\r
+ }\r
+ \r
+ \r
+ \r
+ \r
+ \r
+ public int getSeed() {\r
+ return seed;\r
+ }\r
+ \r
+ public void setSeed(int seed) {\r
+ if (this.seed == seed)\r
+ return;\r
+ this.seed = seed;\r
+ }\r
+ \r
+ \r
+ \r
+ public double getWindSpeed(double time) {\r
+ if (time < 0) {\r
+ throw new IllegalArgumentException("Requesting wind speed at t="+time);\r
+ }\r
+ \r
+ if (randomSource == null) {\r
+ randomSource = new PinkNoise(ALPHA, POLES, new Random(seed));\r
+ time1 = 0;\r
+ value1 = randomSource.nextValue();\r
+ value2 = randomSource.nextValue();\r
+ }\r
+ \r
+ if (time < time1) {\r
+ reset();\r
+ return getWindSpeed(time);\r
+ }\r
+ \r
+ while (time1 + DELTA_T < time) {\r
+ value1 = value2;\r
+ value2 = randomSource.nextValue();\r
+ time1 += DELTA_T;\r
+ }\r
+ \r
+ double a = (time - time1)/DELTA_T;\r
+\r
+ \r
+ return average + (value1 * (1-a) + value2 * a) * standardDeviation / STDDEV;\r
+ }\r
+\r
+ \r
+ private void reset() {\r
+ randomSource = null;\r
+ }\r
+ \r
+ \r
+ public static void main(String[] str) {\r
+ \r
+ WindSimulator sim = new WindSimulator();\r
+ \r
+ sim.setAverage(2);\r
+ sim.setStandardDeviation(0.5);\r
+ \r
+ for (int i=0; i < 10000; i++) {\r
+ double t = 0.01*i;\r
+ double v = sim.getWindSpeed(t);\r
+ System.out.printf("%d.%03d %d.%03d\n", (int)t,((int)(t*1000))%1000, (int)v, ((int)(v*1000))%1000);\r
+// if ((i % 5) == 0)\r
+// System.out.println(" ***");\r
+// else\r
+// System.out.println("");\r
+ }\r
+ \r
+ }\r
+ \r
+}\r
--- /dev/null
+package net.sf.openrocket.aerodynamics.barrowman;
+
+import static java.lang.Math.pow;
+import static java.lang.Math.sqrt;
+import static net.sf.openrocket.util.MathUtil.pow2;
+
+import java.util.Arrays;
+import java.util.Iterator;
+
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.aerodynamics.Warning;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.LinearInterpolator;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.PolyInterpolator;
+import net.sf.openrocket.util.Test;
+
+
+public class FinSetCalc extends RocketComponentCalc {
+ private static final double STALL_ANGLE = (20 * Math.PI/180);
+
+ /** Number of divisions in the fin chords. */
+ protected static final int DIVISIONS = 48;
+
+
+
+
+ private FinSet component;
+
+
+ protected double macLength = Double.NaN; // MAC length
+ protected double macLead = Double.NaN; // MAC leading edge position
+ protected double macSpan = Double.NaN; // MAC spanwise position
+ protected double finArea = Double.NaN; // Fin area
+ protected double ar = Double.NaN; // Fin aspect ratio
+ protected double span = Double.NaN; // Fin span
+ protected double cosGamma = Double.NaN; // Cosine of midchord sweep angle
+ protected double cosGammaLead = Double.NaN; // Cosine of leading edge sweep angle
+ protected double rollSum = Double.NaN; // Roll damping sum term
+
+ protected double[] chordLead = new double[DIVISIONS];
+ protected double[] chordTrail = new double[DIVISIONS];
+ protected double[] chordLength = new double[DIVISIONS];
+
+ protected final WarningSet geometryWarnings = new WarningSet();
+
+ private double[] poly = new double[6];
+
+
+ public FinSetCalc(RocketComponent component) {
+ super(component);
+ if (!(component instanceof FinSet)) {
+ throw new IllegalArgumentException("Illegal component type "+component);
+ }
+ this.component = (FinSet) component;
+ }
+
+
+ /*
+ * Calculates the non-axial forces produced by the fins (normal and side forces,
+ * pitch, yaw and roll moments, CP position, CNa).
+ */
+ @Override
+ public void calculateNonaxialForces(FlightConditions conditions,
+ AerodynamicForces forces, WarningSet warnings) {
+
+ // Compute and cache the fin geometry
+ if (Double.isNaN(macLength)) {
+ calculateFinGeometry();
+ calculatePoly();
+ }
+
+ if (span < 0.001) {
+ forces.Cm = 0;
+ forces.CN = 0;
+ forces.CNa = 0;
+ forces.cp = Coordinate.NUL;
+ forces.Croll = 0;
+ forces.CrollDamp = 0;
+ forces.CrollForce = 0;
+ forces.Cside = 0;
+ forces.Cyaw = 0;
+ return;
+ }
+
+
+ // Add warnings (radius/2 == diameter/4)
+ if (component.getThickness() > component.getBodyRadius()/2) {
+ warnings.add(Warning.THICK_FIN);
+ }
+ warnings.addAll(geometryWarnings);
+
+
+
+ //////// Calculate CNa. /////////
+
+ // One fin without interference (both sub- and supersonic):
+ double cna1 = calculateFinCNa1(conditions);
+
+
+
+ // Multiple fins with fin-fin interference
+ double cna;
+
+ // TODO: MEDIUM: Take into account multiple fin sets
+ int fins = component.getFinCount();
+ double theta = conditions.getTheta();
+ double angle = component.getBaseRotation();
+
+ switch (fins) {
+ case 1:
+ case 2:
+ // from geometry
+ double mul = 0;
+ for (int i=0; i < fins; i++) {
+ mul += MathUtil.pow2(Math.sin(theta - angle));
+ angle += 2 * Math.PI / fins;
+ }
+ cna = cna1*mul;
+ break;
+
+ case 3:
+ // multiplier 1.5, sinusoidal reduction of 15%
+ cna = cna1 * 1.5 * (1 - 0.15*pow2(Math.cos(1.5 * (theta-angle))));
+ break;
+
+ case 4:
+ // multiplier 2.0, sinusoidal reduction of 6%
+ cna = cna1 * 2.0 * (1 - 0.06*pow2(Math.sin(2 * (theta-angle))));
+ break;
+
+ case 5:
+ cna = 2.37 * cna1;
+ break;
+
+ case 6:
+ cna = 2.74 * cna1;
+ break;
+
+ case 7:
+ cna = 2.99 * cna1;
+ break;
+
+ case 8:
+ cna = 3.24 * cna1;
+ break;
+
+ default:
+ // Assume N/2 * 3/4 efficiency for more fins
+ cna = cna1 * fins * 3.0/8.0;
+ break;
+ }
+
+
+ // Body-fin interference effect
+ double r = component.getBodyRadius();
+ double tau = r / (span+r);
+ if (Double.isNaN(tau) || Double.isInfinite(tau))
+ tau = 0;
+ cna *= 1 + tau; // Classical Barrowman
+// cna *= pow2(1 + tau); // Barrowman thesis (too optimistic??)
+
+
+
+ // TODO: LOW: check for fin tip mach cone interference
+ // (Barrowman thesis pdf-page 40)
+
+ // TODO: LOW: fin-fin mach cone effect, MIL-HDBK page 5-25
+
+
+
+ // Calculate CP position
+ double x = macLead + calculateCPPos(conditions) * macLength;
+
+
+
+ // Calculate roll forces, reduce forcing above stall angle
+
+ // Without body-fin interference effect:
+// forces.CrollForce = fins * (macSpan+r) * cna1 * component.getCantAngle() /
+// conditions.getRefLength();
+ // With body-fin interference effect:
+ forces.CrollForce = fins * (macSpan+r) * cna1 * (1+tau) * component.getCantAngle() /
+ conditions.getRefLength();
+
+
+
+
+ if (conditions.getAOA() > STALL_ANGLE) {
+// System.out.println("Fin stalling in roll");
+ forces.CrollForce *= MathUtil.clamp(
+ 1-(conditions.getAOA() - STALL_ANGLE)/(STALL_ANGLE/2), 0, 1);
+ }
+ forces.CrollDamp = calculateDampingMoment(conditions);
+ forces.Croll = forces.CrollForce - forces.CrollDamp;
+
+
+
+// System.out.printf(component.getName() + ": roll rate:%.3f force:%.3f damp:%.3f " +
+// "total:%.3f\n",
+// conditions.getRollRate(), forces.CrollForce, forces.CrollDamp, forces.Croll);
+
+ forces.CNa = cna;
+ forces.CN = cna * MathUtil.min(conditions.getAOA(), STALL_ANGLE);
+ forces.cp = new Coordinate(x, 0, 0, cna);
+ forces.Cm = forces.CN * x / conditions.getRefLength();
+
+ if (fins == 1) {
+ forces.Cside = cna1 * Math.cos(theta-angle) * Math.sin(theta-angle);
+ forces.Cyaw = forces.Cside * x / conditions.getRefLength();
+ } else {
+ forces.Cside = 0;
+ forces.Cyaw = 0;
+ }
+
+ }
+
+
+ /**
+ * Returns the MAC length of the fin. This is required in the friction drag
+ * computation.
+ *
+ * @return the MAC length of the fin.
+ */
+ public double getMACLength() {
+ // Compute and cache the fin geometry
+ if (Double.isNaN(macLength)) {
+ calculateFinGeometry();
+ calculatePoly();
+ }
+
+ return macLength;
+ }
+
+ public double getMidchordPos() {
+ // Compute and cache the fin geometry
+ if (Double.isNaN(macLength)) {
+ calculateFinGeometry();
+ calculatePoly();
+ }
+
+ return macLead + 0.5 * macLength;
+ }
+
+
+
+ /**
+ * Pre-calculates the fin geometry values.
+ */
+ protected void calculateFinGeometry() {
+
+ span = component.getSpan();
+ finArea = component.getFinArea();
+ ar = 2 * pow2(span) / finArea;
+
+ Coordinate[] points = component.getFinPoints();
+
+ // Check for jagged edges
+ geometryWarnings.clear();
+ boolean down = false;
+ for (int i=1; i < points.length; i++) {
+ if ((points[i].y > points[i-1].y + 0.001) && down) {
+ geometryWarnings.add(Warning.JAGGED_EDGED_FIN);
+ break;
+ }
+ if (points[i].y < points[i-1].y - 0.001) {
+ down = true;
+ }
+ }
+
+
+ // Calculate the chord lead and trail positions and length
+
+ Arrays.fill(chordLead, Double.POSITIVE_INFINITY);
+ Arrays.fill(chordTrail, Double.NEGATIVE_INFINITY);
+ Arrays.fill(chordLength, 0);
+
+ for (int point=1; point < points.length; point++) {
+ double x1 = points[point-1].x;
+ double y1 = points[point-1].y;
+ double x2 = points[point].x;
+ double y2 = points[point].y;
+
+ if (MathUtil.equals(y1, y2))
+ continue;
+
+ int i1 = (int)(y1*1.0001/span*(DIVISIONS-1));
+ int i2 = (int)(y2*1.0001/span*(DIVISIONS-1));
+ i1 = MathUtil.clamp(i1, 0, DIVISIONS-1);
+ i2 = MathUtil.clamp(i2, 0, DIVISIONS-1);
+ if (i1 > i2) {
+ int tmp = i2;
+ i2 = i1;
+ i1 = tmp;
+ }
+
+ for (int i = i1; i <= i2; i++) {
+ // Intersection point (x,y)
+ double y = i*span/(DIVISIONS-1);
+ double x = (y-y2)/(y1-y2)*x1 + (y1-y)/(y1-y2)*x2;
+ if (x < chordLead[i])
+ chordLead[i] = x;
+ if (x > chordTrail[i])
+ chordTrail[i] = x;
+
+ // TODO: LOW: If fin point exactly on chord line, might be counted twice:
+ if (y1 < y2) {
+ chordLength[i] -= x;
+ } else {
+ chordLength[i] += x;
+ }
+ }
+ }
+
+ // Check and correct any inconsistencies
+ for (int i=0; i < DIVISIONS; i++) {
+ if (Double.isInfinite(chordLead[i]) || Double.isInfinite(chordTrail[i]) ||
+ Double.isNaN(chordLead[i]) || Double.isNaN(chordTrail[i])) {
+ chordLead[i] = 0;
+ chordTrail[i] = 0;
+ }
+ if (chordLength[i] < 0 || Double.isNaN(chordLength[i])) {
+ chordLength[i] = 0;
+ }
+ if (chordLength[i] > chordTrail[i] - chordLead[i]) {
+ chordLength[i] = chordTrail[i] - chordLead[i];
+ }
+ }
+
+
+ /* Calculate fin properties:
+ *
+ * macLength // MAC length
+ * macLead // MAC leading edge position
+ * macSpan // MAC spanwise position
+ * ar // Fin aspect ratio (already set)
+ * span // Fin span (already set)
+ */
+ macLength = 0;
+ macLead = 0;
+ macSpan = 0;
+ cosGamma = 0;
+ cosGammaLead = 0;
+ rollSum = 0;
+ double area = 0;
+ double radius = component.getBodyRadius();
+
+ final double dy = span/(DIVISIONS-1);
+ for (int i=0; i < DIVISIONS; i++) {
+ double length = chordTrail[i] - chordLead[i];
+ double y = i*dy;
+
+ macLength += length * length;
+ macSpan += y * length;
+ macLead += chordLead[i] * length;
+ area += length;
+ rollSum += chordLength[i] * pow2(radius + y);
+
+ if (i>0) {
+ double dx = (chordTrail[i]+chordLead[i])/2 - (chordTrail[i-1]+chordLead[i-1])/2;
+ cosGamma += dy/MathUtil.hypot(dx, dy);
+
+ dx = chordLead[i] - chordLead[i-1];
+ cosGammaLead += dy/MathUtil.hypot(dx, dy);
+ }
+ }
+
+ macLength *= dy;
+ macSpan *= dy;
+ macLead *= dy;
+ area *= dy;
+ rollSum *= dy;
+
+ macLength /= area;
+ macSpan /= area;
+ macLead /= area;
+ cosGamma /= (DIVISIONS-1);
+ cosGammaLead /= (DIVISIONS-1);
+ }
+
+
+ /////////////// CNa1 calculation ////////////////
+
+ private static final double CNA_SUBSONIC = 0.9;
+ private static final double CNA_SUPERSONIC = 1.5;
+ private static final double CNA_SUPERSONIC_B = pow(pow2(CNA_SUPERSONIC)-1, 1.5);
+ private static final double GAMMA = 1.4;
+ private static final LinearInterpolator K1, K2, K3;
+ private static final PolyInterpolator cnaInterpolator = new PolyInterpolator(
+ new double[] { CNA_SUBSONIC, CNA_SUPERSONIC },
+ new double[] { CNA_SUBSONIC, CNA_SUPERSONIC },
+ new double[] { CNA_SUBSONIC }
+ );
+ /* Pre-calculate the values for K1, K2 and K3 */
+ static {
+ // Up to Mach 5
+ int n = (int)((5.0-CNA_SUPERSONIC)*10);
+ double[] x = new double[n];
+ double[] k1 = new double[n];
+ double[] k2 = new double[n];
+ double[] k3 = new double[n];
+ for (int i=0; i<n; i++) {
+ double M = CNA_SUPERSONIC + i*0.1;
+ double beta = sqrt(M*M - 1);
+ x[i] = M;
+ k1[i] = 2.0/beta;
+ k2[i] = ((GAMMA+1)*pow(M, 4) - 4*pow2(beta)) / (4*pow(beta,4));
+ k3[i] = ((GAMMA+1)*pow(M, 8) + (2*pow2(GAMMA) - 7*GAMMA - 5) * pow(M,6) +
+ 10*(GAMMA+1)*pow(M,4) + 8) / (6*pow(beta,7));
+ }
+ K1 = new LinearInterpolator(x,k1);
+ K2 = new LinearInterpolator(x,k2);
+ K3 = new LinearInterpolator(x,k3);
+
+// System.out.println("K1[m="+CNA_SUPERSONIC+"] = "+k1[0]);
+// System.out.println("K2[m="+CNA_SUPERSONIC+"] = "+k2[0]);
+// System.out.println("K3[m="+CNA_SUPERSONIC+"] = "+k3[0]);
+ }
+
+
+ protected double calculateFinCNa1(FlightConditions conditions) {
+ double mach = conditions.getMach();
+ double ref = conditions.getRefArea();
+ double alpha = MathUtil.min(conditions.getAOA(),
+ Math.PI - conditions.getAOA(), STALL_ANGLE);
+
+ // Subsonic case
+ if (mach <= CNA_SUBSONIC) {
+ return 2*Math.PI*pow2(span)/(1 + sqrt(1 + (1-pow2(mach))*
+ pow2(pow2(span)/(finArea*cosGamma)))) / ref;
+ }
+
+ // Supersonic case
+ if (mach >= CNA_SUPERSONIC) {
+ return finArea * (K1.getValue(mach) + K2.getValue(mach)*alpha +
+ K3.getValue(mach)*pow2(alpha)) / ref;
+ }
+
+ // Transonic case, interpolate
+ double subV, superV;
+ double subD, superD;
+
+ double sq = sqrt(1 + (1-pow2(CNA_SUBSONIC)) * pow2(span*span/(finArea*cosGamma)));
+ subV = 2*Math.PI*pow2(span)/ref / (1+sq);
+ subD = 2*mach*Math.PI*pow(span,6) / (pow2(finArea*cosGamma) * ref *
+ sq * pow2(1+sq));
+
+ superV = finArea * (K1.getValue(CNA_SUPERSONIC) + K2.getValue(CNA_SUPERSONIC)*alpha +
+ K3.getValue(CNA_SUPERSONIC)*pow2(alpha)) / ref;
+ superD = -finArea/ref * 2*CNA_SUPERSONIC / CNA_SUPERSONIC_B;
+
+// System.out.println("subV="+subV+" superV="+superV+" subD="+subD+" superD="+superD);
+
+ return cnaInterpolator.interpolate(mach, subV, superV, subD, superD, 0);
+ }
+
+
+
+
+ private double calculateDampingMoment(FlightConditions conditions) {
+ double rollRate = conditions.getRollRate();
+
+ if (Math.abs(rollRate) < 0.1)
+ return 0;
+
+ double mach = conditions.getMach();
+ double radius = component.getBodyRadius();
+ double absRate = Math.abs(rollRate);
+
+
+ /*
+ * At low speeds and relatively large roll rates (i.e. near apogee) the
+ * fin tips rotate well above stall angle. In this case sum the chords
+ * separately.
+ */
+ if (absRate * (radius + span) / conditions.getVelocity() > 15*Math.PI/180) {
+ double sum = 0;
+ for (int i=0; i < DIVISIONS; i++) {
+ double dist = radius + span*i/DIVISIONS;
+ double aoa = Math.min(absRate*dist/conditions.getVelocity(), 15*Math.PI/180);
+ sum += chordLength[i] * dist * aoa;
+ }
+ sum = sum * (span/DIVISIONS) * 2*Math.PI/conditions.getBeta() /
+ (conditions.getRefArea() * conditions.getRefLength());
+
+// System.out.println("SPECIAL: " +
+// (MathUtil.sign(rollRate) *component.getFinCount() * sum));
+ return MathUtil.sign(rollRate) * component.getFinCount() * sum;
+ }
+
+
+
+ if (mach <= CNA_SUBSONIC) {
+// System.out.println("BASIC: "+
+// (component.getFinCount() * 2*Math.PI * rollRate * rollSum /
+// (conditions.getRefArea() * conditions.getRefLength() *
+// conditions.getVelocity() * conditions.getBeta())));
+
+ return component.getFinCount() * 2*Math.PI * rollRate * rollSum /
+ (conditions.getRefArea() * conditions.getRefLength() *
+ conditions.getVelocity() * conditions.getBeta());
+ }
+ if (mach >= CNA_SUPERSONIC) {
+
+ double vel = conditions.getVelocity();
+ double k1 = K1.getValue(mach);
+ double k2 = K2.getValue(mach);
+ double k3 = K3.getValue(mach);
+
+ double sum = 0;
+
+ for (int i=0; i < DIVISIONS; i++) {
+ double y = i*span/(DIVISIONS-1);
+ double angle = rollRate * (radius+y) / vel;
+
+ sum += (k1 * angle + k2 * angle*angle + k3 * angle*angle*angle)
+ * chordLength[i] * (radius+y);
+ }
+
+ return component.getFinCount() * sum * span/(DIVISIONS-1) /
+ (conditions.getRefArea() * conditions.getRefLength());
+ }
+
+ // Transonic, do linear interpolation
+
+ FlightConditions cond = conditions.clone();
+ cond.setMach(CNA_SUBSONIC - 0.01);
+ double subsonic = calculateDampingMoment(cond);
+ cond.setMach(CNA_SUPERSONIC + 0.01);
+ double supersonic = calculateDampingMoment(cond);
+
+ return subsonic * (CNA_SUPERSONIC - mach)/(CNA_SUPERSONIC - CNA_SUBSONIC) +
+ supersonic * (mach - CNA_SUBSONIC)/(CNA_SUPERSONIC - CNA_SUBSONIC);
+ }
+
+
+
+
+ /**
+ * Return the relative position of the CP along the mean aerodynamic chord.
+ * Below mach 0.5 it is at the quarter chord, above mach 2 calculated using an
+ * empirical formula, between these two using an interpolation polynomial.
+ *
+ * @param cond Mach speed used
+ * @return CP position along the MAC
+ */
+ private double calculateCPPos(FlightConditions cond) {
+ double m = cond.getMach();
+ if (m <= 0.5) {
+ // At subsonic speeds CP at quarter chord
+ return 0.25;
+ }
+ if (m >= 2) {
+ // At supersonic speeds use empirical formula
+ double beta = cond.getBeta();
+ return (ar * beta - 0.67) / (2*ar*beta - 1);
+ }
+
+ // In between use interpolation polynomial
+ double x = 1.0;
+ double val = 0;
+
+ for (int i=0; i < poly.length; i++) {
+ val += poly[i] * x;
+ x *= m;
+ }
+
+ return val;
+ }
+
+ /**
+ * Calculate CP position interpolation polynomial coefficients from the
+ * fin geometry. This is a fifth order polynomial that satisfies
+ *
+ * p(0.5)=0.25
+ * p'(0.5)=0
+ * p(2) = f(2)
+ * p'(2) = f'(2)
+ * p''(2) = 0
+ * p'''(2) = 0
+ *
+ * where f(M) = (ar*sqrt(M^2-1) - 0.67) / (2*ar*sqrt(M^2-1) - 1).
+ *
+ * The values were calculated analytically in Mathematica. The coefficients
+ * are used as poly[0] + poly[1]*x + poly[2]*x^2 + ...
+ */
+ private void calculatePoly() {
+ double denom = pow2(1 - 3.4641*ar); // common denominator
+
+ poly[5] = (-1.58025 * (-0.728769 + ar) * (-0.192105 + ar)) / denom;
+ poly[4] = (12.8395 * (-0.725688 + ar) * (-0.19292 + ar)) / denom;
+ poly[3] = (-39.5062 * (-0.72074 + ar) * (-0.194245 + ar)) / denom;
+ poly[2] = (55.3086 * (-0.711482 + ar) * (-0.196772 + ar)) / denom;
+ poly[1] = (-31.6049 * (-0.705375 + ar) * (-0.198476 + ar)) / denom;
+ poly[0] = (9.16049 * (-0.588838 + ar) * (-0.20624 + ar)) / denom;
+ }
+
+
+ @SuppressWarnings("null")
+ public static void main(String arg[]) {
+ Rocket rocket = Test.makeRocket();
+ FinSet finset = null;
+
+ Iterator<RocketComponent> iter = rocket.deepIterator();
+ while (iter.hasNext()) {
+ RocketComponent c = iter.next();
+ if (c instanceof FinSet) {
+ finset = (FinSet)c;
+ break;
+ }
+ }
+
+ ((TrapezoidFinSet)finset).setHeight(0.10);
+ ((TrapezoidFinSet)finset).setRootChord(0.10);
+ ((TrapezoidFinSet)finset).setTipChord(0.10);
+ ((TrapezoidFinSet)finset).setSweep(0.0);
+
+
+ FinSetCalc calc = new FinSetCalc(finset);
+
+ calc.calculateFinGeometry();
+ FlightConditions cond = new FlightConditions(new Configuration(rocket));
+ for (double m=0; m < 3; m+=0.05) {
+ cond.setMach(m);
+ cond.setAOA(0.0*Math.PI/180);
+ double cna = calc.calculateFinCNa1(cond);
+ System.out.printf("%5.2f "+cna+"\n", m);
+ }
+
+ }
+
+
+ @Override
+ public double calculatePressureDragForce(FlightConditions conditions,
+ double stagnationCD, double baseCD, WarningSet warnings) {
+
+ // Compute and cache the fin geometry
+ if (Double.isNaN(cosGammaLead)) {
+ calculateFinGeometry();
+ calculatePoly();
+ }
+
+
+ FinSet.CrossSection profile = component.getCrossSection();
+ double mach = conditions.getMach();
+ double drag = 0;
+
+ // Pressure fore-drag
+ if (profile == FinSet.CrossSection.AIRFOIL ||
+ profile == FinSet.CrossSection.ROUNDED) {
+
+ // Round leading edge
+ if (mach < 0.9) {
+ drag = Math.pow(1 - pow2(mach), -0.417) - 1;
+ } else if (mach < 1) {
+ drag = 1 - 1.785 * (mach-0.9);
+ } else {
+ drag = 1.214 - 0.502/pow2(mach) + 0.1095/pow2(pow2(mach));
+ }
+
+ } else if (profile == FinSet.CrossSection.SQUARE) {
+ drag = stagnationCD;
+ } else {
+ throw new UnsupportedOperationException("Unsupported fin profile: "+profile);
+ }
+
+ // Slanted leading edge
+ drag *= pow2(cosGammaLead);
+
+ // Trailing edge drag
+ if (profile == FinSet.CrossSection.SQUARE) {
+ drag += baseCD;
+ } else if (profile == FinSet.CrossSection.ROUNDED) {
+ drag += baseCD/2;
+ }
+ // Airfoil assumed to have zero base drag
+
+
+ // Scale to correct reference area
+ drag *= component.getFinCount() * component.getSpan() * component.getThickness() /
+ conditions.getRefArea();
+
+ return drag;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.aerodynamics.barrowman;
+
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.util.MathUtil;
+
+public class LaunchLugCalc extends RocketComponentCalc {
+
+ private double CDmul;
+ private double refArea;
+
+ public LaunchLugCalc(RocketComponent component) {
+ super(component);
+
+ LaunchLug lug = (LaunchLug)component;
+ double ld = lug.getLength() / (2*lug.getRadius());
+
+ CDmul = Math.max(1.3 - ld, 1);
+ refArea = Math.PI * MathUtil.pow2(lug.getRadius()) -
+ Math.PI * MathUtil.pow2(lug.getInnerRadius()) * Math.max(1 - ld, 0);
+ }
+
+ @Override
+ public void calculateNonaxialForces(FlightConditions conditions,
+ AerodynamicForces forces, WarningSet warnings) {
+ // Nothing to be done
+ }
+
+ @Override
+ public double calculatePressureDragForce(FlightConditions conditions,
+ double stagnationCD, double baseCD, WarningSet warnings) {
+
+ return CDmul*stagnationCD * refArea / conditions.getRefArea();
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.aerodynamics.barrowman;
+
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+public abstract class RocketComponentCalc {
+
+ public RocketComponentCalc(RocketComponent component) {
+
+ }
+
+ /**
+ * Calculate the non-axial forces produced by the component (normal and side forces,
+ * pitch, yaw and roll moments and CP position). The values are stored in the
+ * <code>AerodynamicForces</code> object. Additionally the value of CNa is computed
+ * and stored if possible without large amount of extra calculation, otherwise
+ * NaN is stored. The CP coordinate is stored in local coordinates and moments are
+ * computed around the local origin.
+ *
+ * @param conditions the flight conditions.
+ * @param forces the object in which to store the values.
+ * @param warnings set in which to store possible warnings.
+ */
+ public abstract void calculateNonaxialForces(FlightConditions conditions,
+ AerodynamicForces forces, WarningSet warnings);
+
+
+ /**
+ * Calculates the pressure drag of the component. This component does NOT include
+ * the effect of discontinuities in the rocket body.
+ *
+ * @param conditions the flight conditions.
+ * @param stagnationCD the current stagnation drag coefficient
+ * @param baseCD the current base drag coefficient
+ * @param warnings set in which to store possible warnings
+ * @return the pressure drag of the component
+ */
+ public abstract double calculatePressureDragForce(FlightConditions conditions,
+ double stagnationCD, double baseCD, WarningSet warnings);
+}
--- /dev/null
+package net.sf.openrocket.aerodynamics.barrowman;
+
+import static net.sf.openrocket.aerodynamics.AtmosphericConditions.GAMMA;
+import static net.sf.openrocket.util.MathUtil.pow2;
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.rocketcomponent.BodyTube;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.SymmetricComponent;
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.LinearInterpolator;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.PolyInterpolator;
+
+
+
+/**
+ * Calculates the aerodynamic properties of a <code>SymmetricComponent</code>.
+ * <p>
+ * CP and CNa are calculated by the Barrowman method extended to account for body lift
+ * by the method presented by Galejs. Supersonic CNa and CP are assumed to be the
+ * same as the subsonic values.
+ *
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class SymmetricComponentCalc extends RocketComponentCalc {
+
+ public static final double BODY_LIFT_K = 1.1;
+
+ private final SymmetricComponent component;
+
+ private final double length;
+ private final double r1, r2;
+ private final double fineness;
+ private final Transition.Shape shape;
+ private final double param;
+ private final double area;
+
+ public SymmetricComponentCalc(RocketComponent c) {
+ super(c);
+ if (!(c instanceof SymmetricComponent)) {
+ throw new IllegalArgumentException("Illegal component type "+c);
+ }
+ this.component = (SymmetricComponent) c;
+
+
+ length = component.getLength();
+ r1 = component.getForeRadius();
+ r2 = component.getAftRadius();
+
+ fineness = length / (2*Math.abs(r2-r1));
+
+ if (component instanceof BodyTube) {
+ shape = null;
+ param = 0;
+ area = 0;
+ } else if (component instanceof Transition) {
+ shape = ((Transition)component).getType();
+ param = ((Transition)component).getShapeParameter();
+ area = Math.abs(Math.PI * (r1*r1 - r2*r2));
+ } else {
+ throw new UnsupportedOperationException("Unknown component type " +
+ component.getComponentName());
+ }
+ }
+
+
+ private boolean isTube = false;
+ private double cnaCache = Double.NaN;
+ private double cpCache = Double.NaN;
+
+
+ /**
+ * Calculates the non-axial forces produced by the fins (normal and side forces,
+ * pitch, yaw and roll moments, CP position, CNa).
+ * <p>
+ * This method uses the Barrowman method for CP and CNa calculation and the
+ * extension presented by Galejs for the effect of body lift.
+ * <p>
+ * The CP and CNa at supersonic speeds are assumed to be the same as those at
+ * subsonic speeds.
+ */
+ @Override
+ public void calculateNonaxialForces(FlightConditions conditions,
+ AerodynamicForces forces, WarningSet warnings) {
+
+ // Pre-calculate and store the results
+ if (Double.isNaN(cnaCache)) {
+ final double r0 = component.getForeRadius();
+ final double r1 = component.getAftRadius();
+
+ if (MathUtil.equals(r0, r1)) {
+ isTube = true;
+ cnaCache = 0;
+ } else {
+ isTube = false;
+
+ final double A0 = Math.PI * pow2(r0);
+ final double A1 = Math.PI * pow2(r1);
+
+ cnaCache = 2 * (A1 - A0);
+ System.out.println("cnaCache = "+cnaCache);
+ cpCache = (component.getLength() * A1 - component.getFullVolume()) / (A1 - A0);
+ }
+ }
+
+ Coordinate cp;
+
+ // If fore == aft, only body lift is encountered
+ if (isTube) {
+ cp = getLiftCP(conditions, warnings);
+ } else {
+ cp = new Coordinate(cpCache,0,0,cnaCache * conditions.getSincAOA() /
+ conditions.getRefArea()).average(getLiftCP(conditions,warnings));
+ }
+
+ forces.cp = cp;
+ forces.CNa = cp.weight;
+ forces.CN = forces.CNa * conditions.getAOA();
+ forces.Cm = forces.CN * cp.x / conditions.getRefLength();
+ forces.Croll = 0;
+ forces.CrollDamp = 0;
+ forces.CrollForce = 0;
+ forces.Cside = 0;
+ forces.Cyaw = 0;
+
+
+ // Add warning on supersonic flight
+ if (conditions.getMach() > 1.1) {
+ warnings.add("Body calculations may not be entirely accurate at supersonic speeds.");
+ }
+
+ }
+
+
+
+ /**
+ * Calculate the body lift effect according to Galejs.
+ */
+ protected Coordinate getLiftCP(FlightConditions conditions, WarningSet warnings) {
+ double area = component.getComponentPlanformArea();
+ double center = component.getComponentPlanformCenter();
+
+ /*
+ * Without this extra multiplier the rocket may become unstable at apogee
+ * when turning around, and begin oscillating horizontally. During the flight
+ * of the rocket this has no effect. It is effective only when AOA > 45 deg
+ * and the velocity is less than 15 m/s.
+ */
+ double mul = 1;
+ if ((conditions.getMach() < 0.05) && (conditions.getAOA() > Math.PI/4)) {
+ mul = pow2(conditions.getMach() / 0.05);
+ }
+
+ return new Coordinate(center, 0, 0, mul*BODY_LIFT_K * area/conditions.getRefArea() *
+ conditions.getSinAOA() * conditions.getSincAOA()); // sin(aoa)^2 / aoa
+ }
+
+
+
+ private LinearInterpolator interpolator = null;
+
+ @Override
+ public double calculatePressureDragForce(FlightConditions conditions,
+ double stagnationCD, double baseCD, WarningSet warnings) {
+
+ if (component instanceof BodyTube)
+ return 0;
+
+ if (!(component instanceof Transition)) {
+ throw new RuntimeException("Pressure calculation of unknown type: "+
+ component.getComponentName());
+ }
+
+ // Check for simple cases first
+ if (r1 == r2)
+ return 0;
+
+ if (length < 0.001) {
+ if (r1 < r2) {
+ return stagnationCD * area / conditions.getRefArea();
+ } else {
+ return baseCD * area / conditions.getRefArea();
+ }
+ }
+
+
+ // Boattail drag computed directly from base drag
+ if (r2 < r1) {
+ if (fineness >= 3)
+ return 0;
+ double cd = baseCD * area / conditions.getRefArea();
+ if (fineness <= 1)
+ return cd;
+ return cd * (3-fineness)/2;
+ }
+
+
+ assert(r1 < r2); // Tube and boattail have been checked already
+
+
+ // All nose cones and shoulders from pre-calculated and interpolating
+ if (interpolator == null) {
+ calculateNoseInterpolator();
+ }
+
+ return interpolator.getValue(conditions.getMach()) * area / conditions.getRefArea();
+ }
+
+
+
+ /*
+ * Experimental values of pressure drag for different nose cone shapes with a fineness
+ * ratio of 3. The data is taken from 'Collection of Zero-Lift Drag Data on Bodies
+ * of Revolution from Free-Flight Investigations', NASA TR-R-100, NTRS 19630004995,
+ * page 16.
+ *
+ * This data is extrapolated for other fineness ratios.
+ */
+
+ private static final LinearInterpolator ellipsoidInterpolator = new LinearInterpolator(
+ new double[] { 1.2, 1.25, 1.3, 1.4, 1.6, 2.0, 2.4 },
+ new double[] {0.110, 0.128, 0.140, 0.148, 0.152, 0.159, 0.162 /* constant */ }
+ );
+ private static final LinearInterpolator x14Interpolator = new LinearInterpolator(
+ new double[] { 1.2, 1.3, 1.4, 1.6, 1.8, 2.2, 2.6, 3.0, 3.6},
+ new double[] {0.140, 0.156, 0.169, 0.192, 0.206, 0.227, 0.241, 0.249, 0.252}
+ );
+ private static final LinearInterpolator x12Interpolator = new LinearInterpolator(
+ new double[] {0.925, 0.95, 1.0, 1.05, 1.1, 1.2, 1.3, 1.7, 2.0},
+ new double[] { 0, 0.014, 0.050, 0.060, 0.059, 0.081, 0.084, 0.085, 0.078}
+ );
+ private static final LinearInterpolator x34Interpolator = new LinearInterpolator(
+ new double[] { 0.8, 0.9, 1.0, 1.06, 1.2, 1.4, 1.6, 2.0, 2.8, 3.4},
+ new double[] { 0, 0.015, 0.078, 0.121, 0.110, 0.098, 0.090, 0.084, 0.078, 0.074}
+ );
+ private static final LinearInterpolator vonKarmanInterpolator = new LinearInterpolator(
+ new double[] { 0.9, 0.95, 1.0, 1.05, 1.1, 1.2, 1.4, 1.6, 2.0, 3.0},
+ new double[] { 0, 0.010, 0.027, 0.055, 0.070, 0.081, 0.095, 0.097, 0.091, 0.083}
+ );
+ private static final LinearInterpolator lvHaackInterpolator = new LinearInterpolator(
+ new double[] { 0.9, 0.95, 1.0, 1.05, 1.1, 1.2, 1.4, 1.6, 2.0 },
+ new double[] { 0, 0.010, 0.024, 0.066, 0.084, 0.100, 0.114, 0.117, 0.113 }
+ );
+ private static final LinearInterpolator parabolicInterpolator = new LinearInterpolator(
+ new double[] {0.95, 0.975, 1.0, 1.05, 1.1, 1.2, 1.4, 1.7},
+ new double[] { 0, 0.016, 0.041, 0.092, 0.109, 0.119, 0.113, 0.108}
+ );
+ private static final LinearInterpolator parabolic12Interpolator = new LinearInterpolator(
+ new double[] { 0.8, 0.9, 0.95, 1.0, 1.05, 1.1, 1.3, 1.5, 1.8},
+ new double[] { 0, 0.016, 0.042, 0.100, 0.126, 0.125, 0.100, 0.090, 0.088}
+ );
+ private static final LinearInterpolator parabolic34Interpolator = new LinearInterpolator(
+ new double[] { 0.9, 0.95, 1.0, 1.05, 1.1, 1.2, 1.4, 1.7},
+ new double[] { 0, 0.023, 0.073, 0.098, 0.107, 0.106, 0.089, 0.082}
+ );
+ private static final LinearInterpolator bluntInterpolator = new LinearInterpolator();
+ static {
+ for (double m=0; m<3; m+=0.05)
+ bluntInterpolator.addPoint(m, BarrowmanCalculator.calculateStagnationCD(m));
+ }
+
+ /**
+ * Calculate the LinearInterpolator 'interpolator'. After this call, if can be used
+ * to get the pressure drag coefficient at any Mach number.
+ *
+ * First, the transonic/supersonic region is computed. For conical and ogive shapes
+ * this is calculated directly. For other shapes, the values for fineness-ratio 3
+ * transitions are taken from the experimental values stored above (for parameterized
+ * shapes the values are interpolated between the parameter values). These are then
+ * extrapolated to the current fineness ratio.
+ *
+ * Finally, if the first data points in the interpolator are not zero, the subsonic
+ * region is interpolated in the form Cd = a*M^b + Cd(M=0).
+ */
+ @SuppressWarnings("null")
+ private void calculateNoseInterpolator() {
+ LinearInterpolator int1=null, int2=null;
+ double p = 0;
+
+ interpolator = new LinearInterpolator();
+
+ double r = component.getRadius(0.99*length);
+ double sinphi = (r2-r)/MathUtil.hypot(r2-r, 0.01*length);
+
+ /*
+ * Take into account nose cone shape. Conical and ogive generate the interpolator
+ * directly. Others store a interpolator for fineness ratio 3 into int1, or
+ * for parameterized shapes store the bounding fineness ratio 3 interpolators into
+ * int1 and int2 and set 0 <= p <= 1 according to the bounds.
+ */
+ switch (shape) {
+ case CONICAL:
+ interpolator = calculateOgiveNoseInterpolator(0, sinphi); // param==0 -> conical
+ break;
+
+ case OGIVE:
+ interpolator = calculateOgiveNoseInterpolator(param, sinphi);
+ break;
+
+ case ELLIPSOID:
+ int1 = ellipsoidInterpolator;
+ break;
+
+ case POWER:
+ if (param <= 0.25) {
+ int1 = bluntInterpolator;
+ int2 = x14Interpolator;
+ p = param*4;
+ } else if (param <= 0.5) {
+ int1 = x14Interpolator;
+ int2 = x12Interpolator;
+ p = (param-0.25)*4;
+ } else if (param <= 0.75) {
+ int1 = x12Interpolator;
+ int2 = x34Interpolator;
+ p = (param-0.5)*4;
+ } else {
+ int1 = x34Interpolator;
+ int2 = calculateOgiveNoseInterpolator(0, 1/Math.sqrt(1+4*pow2(fineness)));
+ p = (param-0.75)*4;
+ }
+ break;
+
+ case PARABOLIC:
+ if (param <= 0.5) {
+ int1 = calculateOgiveNoseInterpolator(0, 1/Math.sqrt(1+4*pow2(fineness)));
+ int2 = parabolic12Interpolator;
+ p = param*2;
+ } else if (param <= 0.75) {
+ int1 = parabolic12Interpolator;
+ int2 = parabolic34Interpolator;
+ p = (param-0.5)*4;
+ } else {
+ int1 = parabolic34Interpolator;
+ int2 = parabolicInterpolator;
+ p = (param-0.75)*4;
+ }
+ break;
+
+ case HAACK:
+ int1 = vonKarmanInterpolator;
+ int2 = lvHaackInterpolator;
+ p = param*3;
+ break;
+
+ default:
+ throw new UnsupportedOperationException("Unknown transition shape: "+shape);
+ }
+
+ assert(p >= 0);
+ assert(p <= 1.001);
+
+
+ // Check for parameterized shape and interpolate if necessary
+ if (int2 != null) {
+ LinearInterpolator int3 = new LinearInterpolator();
+ for (double m: int1.getXPoints()) {
+ int3.addPoint(m, p*int2.getValue(m) + (1-p)*int1.getValue(m));
+ }
+ for (double m: int2.getXPoints()) {
+ int3.addPoint(m, p*int2.getValue(m) + (1-p)*int1.getValue(m));
+ }
+ int1 = int3;
+ }
+
+ // Extrapolate for fineness ratio if necessary
+ if (int1 != null) {
+ double log4 = Math.log(fineness+1) / Math.log(4);
+ for (double m: int1.getXPoints()) {
+ double stag = bluntInterpolator.getValue(m);
+ interpolator.addPoint(m, stag*Math.pow(int1.getValue(m)/stag, log4));
+ }
+ }
+
+
+ /*
+ * Now the transonic/supersonic region is ok. We still need to interpolate
+ * the subsonic region, if the values are non-zero.
+ */
+
+ double min = interpolator.getXPoints()[0];
+ double minValue = interpolator.getValue(min);
+ if (minValue < 0.001) {
+ // No interpolation necessary
+ return;
+ }
+
+ double cdMach0 = 0.8 * pow2(sinphi);
+ double minDeriv = (interpolator.getValue(min+0.01) - minValue)/0.01;
+
+ // These should not occur, but might cause havoc for the interpolation
+ if ((cdMach0 >= minValue-0.01) || (minDeriv <= 0.01)) {
+ return;
+ }
+
+ // Cd = a*M^b + cdMach0
+ double a = minValue - cdMach0;
+ double b = minDeriv / a;
+
+ for (double m=0; m < minValue; m+= 0.05) {
+ interpolator.addPoint(m, a*Math.pow(m, b) + cdMach0);
+ }
+ }
+
+
+ private static final PolyInterpolator conicalPolyInterpolator =
+ new PolyInterpolator(new double[] {1.0, 1.3}, new double[] {1.0, 1.3});
+
+ private static LinearInterpolator calculateOgiveNoseInterpolator(double param,
+ double sinphi) {
+ LinearInterpolator interpolator = new LinearInterpolator();
+
+ // In the range M = 1 ... 1.3 use polynomial approximation
+ double cdMach1 = 2.1*pow2(sinphi) + 0.6019*sinphi;
+
+ double[] poly = conicalPolyInterpolator.interpolator(
+ 1.0*sinphi, cdMach1,
+ 4/(GAMMA+1) * (1 - 0.5*cdMach1), -1.1341*sinphi
+ );
+
+ // Shape parameter multiplier
+ double mul = 0.72 * pow2(param-0.5) + 0.82;
+
+ for (double m = 1; m < 1.3001; m += 0.02) {
+ interpolator.addPoint(m, mul * PolyInterpolator.eval(m, poly));
+ }
+
+ // Above M = 1.3 use direct formula
+ for (double m = 1.32; m < 4; m += 0.02) {
+ interpolator.addPoint(m, mul * (2.1*pow2(sinphi) + 0.5*sinphi/Math.sqrt(m*m - 1)));
+ }
+
+ return interpolator;
+ }
+
+
+
+}
--- /dev/null
+package net.sf.openrocket.database;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FilenameFilter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.security.CodeSource;
+import java.util.AbstractSet;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.EventListenerList;
+
+import net.sf.openrocket.file.Loader;
+import net.sf.openrocket.util.ChangeSource;
+
+
+
+/**
+ * A database set. This class functions as a <code>Set</code> that contains items
+ * of a specific type. Additionally, the items can be accessed via an index number.
+ * The elements are always kept in their natural order.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+// TODO: HIGH: Database saving
+public class Database<T extends Comparable<T>> extends AbstractSet<T> implements ChangeSource {
+
+ private final List<T> list = new ArrayList<T>();
+ private final EventListenerList listenerList = new EventListenerList();
+ private final Loader<T> loader;
+
+
+ public Database() {
+ loader = null;
+ }
+
+ public Database(Loader<T> loader) {
+ this.loader = loader;
+ }
+
+
+ @Override
+ public Iterator<T> iterator() {
+ return new DBIterator();
+ }
+
+ @Override
+ public int size() {
+ return list.size();
+ }
+
+ @Override
+ public boolean add(T element) {
+ int index;
+
+ index = Collections.binarySearch(list, element);
+ if (index >= 0) {
+ // List might contain the element
+ if (list.contains(element)) {
+ return false;
+ }
+ } else {
+ index = -(index+1);
+ }
+ list.add(index,element);
+ fireChangeEvent();
+ return true;
+ }
+
+
+ /**
+ * Get the element with the specified index.
+ * @param index the index to retrieve.
+ * @return the element at the index.
+ */
+ public T get(int index) {
+ return list.get(index);
+ }
+
+ /**
+ * Return the index of the given <code>Motor</code>, or -1 if not in the database.
+ *
+ * @param m the motor
+ * @return the index of the motor
+ */
+ public int indexOf(T m) {
+ return list.indexOf(m);
+ }
+
+
+ @Override
+ public void addChangeListener(ChangeListener listener) {
+ listenerList .add(ChangeListener.class, listener);
+ }
+
+
+ @Override
+ public void removeChangeListener(ChangeListener listener) {
+ listenerList .remove(ChangeListener.class, listener);
+ }
+
+
+ protected void fireChangeEvent() {
+ Object[] listeners = listenerList.getListenerList();
+ ChangeEvent e = null;
+ for (int i = listeners.length-2; i>=0; i-=2) {
+ if (listeners[i]==ChangeListener.class) {
+ // Lazily create the event:
+ if (e == null)
+ e = new ChangeEvent(this);
+ ((ChangeListener)listeners[i+1]).stateChanged(e);
+ }
+ }
+ }
+
+
+
+ //////// Directory loading
+
+
+
+ /**
+ * Load all files in a directory to the motor database. Only files with file
+ * names matching the given pattern (as matched by <code>String.matches(String)</code>)
+ * are processed.
+ *
+ * @param dir the directory to read.
+ * @param pattern the pattern to match the file names to.
+ * @throws IOException if an IO error occurs when reading the JAR archive
+ * (errors reading individual files are printed to stderr).
+ */
+ public void loadDirectory(File dir, final String pattern) throws IOException {
+ if (loader == null) {
+ throw new IllegalStateException("no file loader set");
+ }
+
+ File[] files = dir.listFiles(new FilenameFilter() {
+ @Override
+ public boolean accept(File dir, String name) {
+ return name.matches(pattern);
+ }
+ });
+ if (files == null) {
+ throw new IOException("not a directory: "+dir);
+ }
+ for (File file: files) {
+ try {
+ this.addAll(loader.load(new FileInputStream(file), file.getName()));
+ } catch (IOException e) {
+ System.err.println("Error loading file "+file+": " + e.getMessage());
+ }
+ }
+ }
+
+
+ /**
+ * Read all files in a directory contained in the JAR file that this class belongs to.
+ * Only files whose names match the given pattern (as matched by
+ * <code>String.matches(String)</code>) will be read.
+ *
+ * @param dir the directory within the JAR archive to read.
+ * @param pattern the pattern to match the file names to.
+ * @throws IOException if an IO error occurs when reading the JAR archive
+ * (errors reading individual files are printed to stderr).
+ */
+ public void loadJarDirectory(String dir, String pattern) throws IOException {
+
+ // Process directory and extension
+ if (!dir.endsWith("/")) {
+ dir += "/";
+ }
+
+ // Find the jar file this class is contained in and open it
+ URL jarUrl = null;
+ CodeSource codeSource = Database.class.getProtectionDomain().getCodeSource();
+ if (codeSource != null)
+ jarUrl = codeSource.getLocation();
+
+ if (jarUrl == null) {
+ throw new IOException("Could not find containing JAR file.");
+ }
+ File file = urlToFile(jarUrl);
+ JarFile jarFile = new JarFile(file);
+
+ try {
+
+ // Loop through JAR entries searching for files to load
+ Enumeration<JarEntry> entries = jarFile.entries();
+ while (entries.hasMoreElements()) {
+ JarEntry entry = entries.nextElement();
+ String name = entry.getName();
+ if (name.startsWith(dir) && name.matches(pattern)) {
+ try {
+ InputStream stream = jarFile.getInputStream(entry);
+ this.addAll(loader.load(stream, name));
+ } catch (IOException e) {
+ System.err.println("Error loading file " + file + ": "
+ + e.getMessage());
+ }
+ }
+ }
+
+ } finally {
+ jarFile.close();
+ }
+ }
+
+
+ static File urlToFile(URL url) {
+ URI uri;
+ try {
+ uri = url.toURI();
+ } catch (URISyntaxException e) {
+ try {
+ uri = new URI(url.getProtocol(), url.getUserInfo(), url.getHost(), url.getPort(),
+ url.getPath(), url.getQuery(), url.getRef());
+ } catch (URISyntaxException e1) {
+ throw new IllegalArgumentException("Broken URL: " + url);
+ }
+ }
+ return new File(uri);
+ }
+
+
+
+ public void load(File file) throws IOException {
+ if (loader == null) {
+ throw new IllegalStateException("no file loader set");
+ }
+ this.addAll(loader.load(new FileInputStream(file), file.getName()));
+ }
+
+
+
+ /**
+ * Iterator class implementation that fires changes if remove() is called.
+ */
+ private class DBIterator implements Iterator<T> {
+ private Iterator<T> iterator = list.iterator();
+
+ @Override
+ public boolean hasNext() {
+ return iterator.hasNext();
+ }
+
+ @Override
+ public T next() {
+ return iterator.next();
+ }
+
+ @Override
+ public void remove() {
+ iterator.remove();
+ fireChangeEvent();
+ }
+ }
+}
--- /dev/null
+package net.sf.openrocket.database;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+
+import net.sf.openrocket.file.MotorLoader;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.util.MathUtil;
+
+
+/**
+ * A class that contains single instances of {@link Database} for specific purposes.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class Databases {
+
+ /* Static implementations of specific databases: */
+ /**
+ * The motor database.
+ */
+ public static final Database<Motor> MOTOR = new Database<Motor>(new MotorLoader());
+
+
+ /**
+ * A database of bulk materials (with bulk densities).
+ */
+ public static final Database<Material> BULK_MATERIAL = new Database<Material>();
+ /**
+ * A database of surface materials (with surface densities).
+ */
+ public static final Database<Material> SURFACE_MATERIAL = new Database<Material>();
+ /**
+ * A database of linear material (with length densities).
+ */
+ public static final Database<Material> LINE_MATERIAL = new Database<Material>();
+
+
+
+ // TODO: HIGH: loading the thrust curves and other databases
+ static {
+
+ try {
+ MOTOR.loadJarDirectory("datafiles/thrustcurves/", ".*\\.[eE][nN][gG]$");
+ } catch (IOException e) {
+ System.out.println("Could not read thrust curves from JAR: "+e.getMessage());
+
+ try {
+ MOTOR.loadDirectory(new File("datafiles/thrustcurves/"),".*\\.[eE][nN][gG]$");
+ } catch (IOException e1) {
+ System.out.println("Could not read thrust curves from directory either.");
+ throw new RuntimeException(e1);
+ }
+ }
+ }
+
+ // TODO: HIGH: Move materials into data files
+ static {
+
+ BULK_MATERIAL.add(new Material.Bulk("Acrylic", 1190));
+ BULK_MATERIAL.add(new Material.Bulk("Balsa", 170));
+ BULK_MATERIAL.add(new Material.Bulk("Birch", 670));
+ BULK_MATERIAL.add(new Material.Bulk("Cardboard", 680));
+ BULK_MATERIAL.add(new Material.Bulk("Carbon fiber", 1780));
+ BULK_MATERIAL.add(new Material.Bulk("Cork", 240));
+ BULK_MATERIAL.add(new Material.Bulk("Fiberglass", 1850));
+ BULK_MATERIAL.add(new Material.Bulk("Kraft phenolic",950));
+ BULK_MATERIAL.add(new Material.Bulk("Maple", 755));
+ BULK_MATERIAL.add(new Material.Bulk("Paper (office)",820));
+ BULK_MATERIAL.add(new Material.Bulk("Pine", 530));
+ BULK_MATERIAL.add(new Material.Bulk("Plywood (birch)",630));
+ BULK_MATERIAL.add(new Material.Bulk("Polycarbonate (Lexan)",1200));
+ BULK_MATERIAL.add(new Material.Bulk("Polystyrene", 1050));
+ BULK_MATERIAL.add(new Material.Bulk("PVC", 1390));
+ BULK_MATERIAL.add(new Material.Bulk("Spruce", 450));
+ BULK_MATERIAL.add(new Material.Bulk("Quantum tubing",1050));
+
+ SURFACE_MATERIAL.add(new Material.Surface("Ripstop nylon", 0.067));
+ SURFACE_MATERIAL.add(new Material.Surface("Mylar", 0.021));
+ SURFACE_MATERIAL.add(new Material.Surface("Polyethylene (thin)", 0.015));
+ SURFACE_MATERIAL.add(new Material.Surface("Polyethylene (heavy)", 0.040));
+ SURFACE_MATERIAL.add(new Material.Surface("Silk", 0.060));
+ SURFACE_MATERIAL.add(new Material.Surface("Paper (office)", 0.080));
+ SURFACE_MATERIAL.add(new Material.Surface("Cellophane", 0.018));
+ SURFACE_MATERIAL.add(new Material.Surface("Cr\u00eape paper", 0.025));
+
+ LINE_MATERIAL.add(new Material.Line("Thread (heavy-duty)", 0.0003));
+ LINE_MATERIAL.add(new Material.Line("Elastic cord (round 2mm, 1/16 in)",0.0018));
+ LINE_MATERIAL.add(new Material.Line("Elastic cord (flat 6mm, 1/4 in)", 0.0043));
+ LINE_MATERIAL.add(new Material.Line("Elastic cord (flat 12mm, 1/2 in)", 0.008));
+ LINE_MATERIAL.add(new Material.Line("Elastic cord (flat 19mm, 3/4 in)", 0.0012));
+ LINE_MATERIAL.add(new Material.Line("Elastic cord (flat 25mm, 1 in)", 0.0016));
+ LINE_MATERIAL.add(new Material.Line("Braided nylon (2 mm, 1/16 in)", 0.001));
+ LINE_MATERIAL.add(new Material.Line("Braided nylon (3 mm, 1/8 in)", 0.0035));
+ LINE_MATERIAL.add(new Material.Line("Tubular nylon (11 mm, 7/16 in)", 0.013));
+ LINE_MATERIAL.add(new Material.Line("Tubular nylon (14 mm, 9/16 in)", 0.016));
+ LINE_MATERIAL.add(new Material.Line("Tubular nylon (25 mm, 1 in)", 0.029));
+ }
+
+
+ /**
+ * Find a material from the database with the specified type and name. Returns
+ * <code>null</code> if the specified material could not be found.
+ *
+ * @param type the material type.
+ * @param name the material name in the database.
+ * @return the material, or <code>null</code> if not found.
+ */
+ public static Material findMaterial(Material.Type type, String name) {
+ Database<Material> db;
+ switch (type) {
+ case BULK:
+ db = BULK_MATERIAL;
+ break;
+ case SURFACE:
+ db = SURFACE_MATERIAL;
+ break;
+ case LINE:
+ db = LINE_MATERIAL;
+ break;
+ default:
+ throw new IllegalArgumentException("Illegal material type: "+type);
+ }
+
+ for (Material m: db) {
+ if (m.getName().equalsIgnoreCase(name)) {
+ return m;
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * Find a material from the database or return a new material if the specified
+ * material with the specified density is not found.
+ *
+ * @param type the material type.
+ * @param name the material name.
+ * @param density the density of the material.
+ * @return the material object from the database or a new material.
+ */
+ public static Material findMaterial(Material.Type type, String name, double density) {
+ Database<Material> db;
+ switch (type) {
+ case BULK:
+ db = BULK_MATERIAL;
+ break;
+ case SURFACE:
+ db = SURFACE_MATERIAL;
+ break;
+ case LINE:
+ db = LINE_MATERIAL;
+ break;
+ default:
+ throw new IllegalArgumentException("Illegal material type: "+type);
+ }
+
+ for (Material m: db) {
+ if (m.getName().equalsIgnoreCase(name) && MathUtil.equals(m.getDensity(), density)) {
+ return m;
+ }
+ }
+ return Material.newMaterial(type, name, density);
+ }
+
+
+
+ /**
+ * Return all motor in the database matching a search criteria. Any search criteria that
+ * is null or NaN is ignored.
+ *
+ * @param type the motor type, or null.
+ * @param manufacturer the manufacturer, or null.
+ * @param designation the designation, or null.
+ * @param diameter the diameter, or NaN.
+ * @param length the length, or NaN.
+ * @return an array of all the matching motors.
+ */
+ public static Motor[] findMotors(Motor.Type type, String manufacturer, String designation, double diameter, double length) {
+ ArrayList<Motor> results = new ArrayList<Motor>();
+
+ for (Motor m: MOTOR) {
+ boolean match = true;
+ if (type != null && type != m.getMotorType())
+ match = false;
+ else if (manufacturer != null && !manufacturer.equalsIgnoreCase(m.getManufacturer()))
+ match = false;
+ else if (designation != null && !designation.equalsIgnoreCase(m.getDesignation()))
+ match = false;
+ else if (!Double.isNaN(diameter) && (Math.abs(diameter - m.getDiameter()) > 0.0015))
+ match = false;
+ else if (!Double.isNaN(length) && (Math.abs(length - m.getLength()) > 0.0015))
+ match = false;
+
+ if (match)
+ results.add(m);
+ }
+
+ return results.toArray(new Motor[0]);
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.document;
+//TODO: LOW: move class somewhere else?
+
+import java.awt.event.ActionEvent;
+import java.io.File;
+import java.util.ArrayList;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+
+import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
+import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.util.Icons;
+
+
+public class OpenRocketDocument implements ComponentChangeListener {
+ /**
+ * The minimum number of undo levels that are stored.
+ */
+ public static final int UNDO_LEVELS = 50;
+ /**
+ * The margin of the undo levels. After the number of undo levels exceeds
+ * UNDO_LEVELS by this amount the undo is purged to that length.
+ */
+ public static final int UNDO_MARGIN = 10;
+
+
+ private final Rocket rocket;
+ private final Configuration configuration;
+
+ private final ArrayList<Simulation> simulations = new ArrayList<Simulation>();
+
+
+ private int undoPosition = -1; // Illegal position, init in constructor
+ private LinkedList<Rocket> undoHistory = new LinkedList<Rocket>();
+ private LinkedList<String> undoDescription = new LinkedList<String>();
+
+ private String nextDescription = null;
+
+
+ private File file = null;
+ private int savedID = -1;
+
+ private final StorageOptions storageOptions = new StorageOptions();
+
+
+ /* These must be initialized after undo history is set up. */
+ private final UndoRedoAction undoAction;
+ private final UndoRedoAction redoAction;
+
+
+ public OpenRocketDocument(Rocket rocket) {
+ this(rocket.getDefaultConfiguration());
+ }
+
+
+ private OpenRocketDocument(Configuration configuration) {
+ this.configuration = configuration;
+ this.rocket = configuration.getRocket();
+
+ undoHistory.add(rocket.copy());
+ undoDescription.add(null);
+ undoPosition = 0;
+
+ undoAction = new UndoRedoAction(UndoRedoAction.UNDO);
+ redoAction = new UndoRedoAction(UndoRedoAction.REDO);
+
+ rocket.addComponentChangeListener(this);
+
+
+ }
+
+
+
+
+ public Rocket getRocket() {
+ return rocket;
+ }
+
+
+ public Configuration getDefaultConfiguration() {
+ return configuration;
+ }
+
+
+ public File getFile() {
+ return file;
+ }
+
+ public void setFile(File file) {
+ this.file = file;
+ }
+
+
+ public boolean isSaved() {
+ return rocket.getModID() == savedID;
+ }
+
+ public void setSaved(boolean saved) {
+ if (saved == false)
+ this.savedID = -1;
+ else
+ this.savedID = rocket.getModID();
+ }
+
+ /**
+ * Retrieve the default storage options for this document.
+ *
+ * @return the storage options.
+ */
+ public StorageOptions getDefaultStorageOptions() {
+ return storageOptions;
+ }
+
+
+
+
+
+ @SuppressWarnings("unchecked")
+ public List<Simulation> getSimulations() {
+ return (ArrayList<Simulation>)simulations.clone();
+ }
+ public int getSimulationCount() {
+ return simulations.size();
+ }
+ public Simulation getSimulation(int n) {
+ return simulations.get(n);
+ }
+ public int getSimulationIndex(Simulation simulation) {
+ return simulations.indexOf(simulation);
+ }
+ public void addSimulation(Simulation simulation) {
+ simulations.add(simulation);
+ }
+ public void addSimulation(Simulation simulation, int n) {
+ simulations.add(n, simulation);
+ }
+ public void removeSimulation(Simulation simulation) {
+ simulations.remove(simulation);
+ }
+ public Simulation removeSimulation(int n) {
+ return simulations.remove(n);
+ }
+
+
+
+ /**
+ * Adds an undo point at this position. This method should be called *before* any
+ * action that is to be undoable. All actions after the call will be undone by a
+ * single "Undo" action.
+ * <p>
+ * The description should be a short, descriptive string of the actions that will
+ * follow. This is shown to the user e.g. in the Edit-menu, for example
+ * "Undo (Modify body tube)". If the actions are not known (in general should not
+ * be the case!) description may be null.
+ * <p>
+ * If this method is called successively without any change events occurring between the
+ * calls, only the last call will have any effect.
+ *
+ * @param description A short description of the following actions.
+ */
+ public void addUndoPosition(String description) {
+
+ // Check whether modifications have been done since last call
+ if (isCleanState()) {
+ // No modifications
+ nextDescription = description;
+ return;
+ }
+
+
+ /*
+ * Modifications have been made to the rocket. We should be at the end of the
+ * undo history, but check for consistency.
+ */
+ assert(undoPosition == undoHistory.size()-1): "Undo inconsistency, report bug!";
+ while (undoPosition < undoHistory.size()-1) {
+ undoHistory.removeLast();
+ undoDescription.removeLast();
+ }
+
+
+ // Add the current state to the undo history
+ undoHistory.add(rocket.copy());
+ undoDescription.add(description);
+ nextDescription = description;
+ undoPosition++;
+
+
+ // Maintain maximum undo size
+ if (undoHistory.size() > UNDO_LEVELS + UNDO_MARGIN) {
+ for (int i=0; i < UNDO_MARGIN+1; i++) {
+ undoHistory.removeFirst();
+ undoDescription.removeFirst();
+ undoPosition--;
+ }
+ }
+ }
+
+
+ public Action getUndoAction() {
+ return undoAction;
+ }
+
+
+ public Action getRedoAction() {
+ return redoAction;
+ }
+
+
+ @Override
+ public void componentChanged(ComponentChangeEvent e) {
+
+ if (!e.isUndoChange()) {
+ // Remove any redo information if available
+ while (undoPosition < undoHistory.size()-1) {
+ undoHistory.removeLast();
+ undoDescription.removeLast();
+ }
+
+ // Set the latest description
+ undoDescription.set(undoPosition, nextDescription);
+ }
+
+ undoAction.setAllValues();
+ redoAction.setAllValues();
+ }
+
+
+ public boolean isUndoAvailable() {
+ if (undoPosition > 0)
+ return true;
+
+ return !isCleanState();
+ }
+
+ public String getUndoDescription() {
+ if (!isUndoAvailable())
+ return null;
+
+ if (isCleanState()) {
+ return undoDescription.get(undoPosition-1);
+ } else {
+ return undoDescription.get(undoPosition);
+ }
+ }
+
+
+ public boolean isRedoAvailable() {
+ return undoPosition < undoHistory.size()-1;
+ }
+
+ public String getRedoDescription() {
+ if (!isRedoAvailable())
+ return null;
+
+ return undoDescription.get(undoPosition);
+ }
+
+
+
+ public void undo() {
+ if (!isUndoAvailable()) {
+ throw new IllegalStateException("Undo not available.");
+ }
+
+ // Update history position
+
+ if (isCleanState()) {
+ // We are in a clean state, simply move backwards in history
+ undoPosition--;
+ } else {
+ // Modifications have been made, save the state and restore previous state
+ undoHistory.add(rocket.copy());
+ undoDescription.add(null);
+ }
+
+ rocket.loadFrom(undoHistory.get(undoPosition).copy());
+ }
+
+
+ public void redo() {
+ if (!isRedoAvailable()) {
+ throw new IllegalStateException("Redo not available.");
+ }
+
+ undoPosition++;
+
+ rocket.loadFrom(undoHistory.get(undoPosition).copy());
+ }
+
+
+ private boolean isCleanState() {
+ return rocket.getModID() == undoHistory.get(undoPosition).getModID();
+ }
+
+
+
+
+
+
+ /**
+ * Inner class to implement undo/redo actions.
+ */
+ private class UndoRedoAction extends AbstractAction {
+ public static final int UNDO = 1;
+ public static final int REDO = 2;
+
+ private final int type;
+
+ // Sole constructor
+ public UndoRedoAction(int type) {
+ if (type != UNDO && type != REDO) {
+ throw new IllegalArgumentException("Unknown type = "+type);
+ }
+ this.type = type;
+ setAllValues();
+ }
+
+
+ // Actual action to make
+ public void actionPerformed(ActionEvent e) {
+ switch (type) {
+ case UNDO:
+ undo();
+ break;
+
+ case REDO:
+ redo();
+ break;
+ }
+ }
+
+
+ // Set all the values correctly (name and enabled/disabled status)
+ public void setAllValues() {
+ String name,desc;
+ boolean enabled;
+
+ switch (type) {
+ case UNDO:
+ name = "Undo";
+ desc = getUndoDescription();
+ enabled = isUndoAvailable();
+ this.putValue(SMALL_ICON, Icons.EDIT_UNDO);
+ break;
+
+ case REDO:
+ name = "Redo";
+ desc = getRedoDescription();
+ enabled = isRedoAvailable();
+ this.putValue(SMALL_ICON, Icons.EDIT_REDO);
+ break;
+
+ default:
+ throw new RuntimeException("EEEK!");
+ }
+
+ if (desc != null)
+ name = name + " ("+desc+")";
+
+ putValue(NAME, name);
+ setEnabled(enabled);
+ }
+ }
+}
--- /dev/null
+package net.sf.openrocket.document;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
+import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.simulation.FlightSimulator;
+import net.sf.openrocket.simulation.RK4Simulator;
+import net.sf.openrocket.simulation.SimulationConditions;
+import net.sf.openrocket.simulation.SimulationListener;
+import net.sf.openrocket.simulation.exception.SimulationException;
+import net.sf.openrocket.simulation.exception.SimulationListenerException;
+import net.sf.openrocket.util.ChangeSource;
+
+
+public class Simulation implements ChangeSource {
+
+ public static enum Status {
+ /** Up-to-date */
+ UPTODATE,
+
+ /** Loaded from file, status probably up-to-date */
+ LOADED,
+
+ /** Data outdated */
+ OUTDATED,
+
+ /** Imported external data */
+ EXTERNAL,
+
+ /** Not yet simulated */
+ NOT_SIMULATED
+ }
+
+
+ private final Rocket rocket;
+
+ private String name = "";
+
+ private Status status = Status.NOT_SIMULATED;
+
+ /** The conditions to use */
+ private final SimulationConditions conditions;
+
+ private List<String> simulationListeners = new ArrayList<String>();
+
+ private Class<? extends FlightSimulator> simulatorClass = RK4Simulator.class;
+ private Class<? extends AerodynamicCalculator> calculatorClass = BarrowmanCalculator.class;
+
+
+
+ /** Listeners for this object */
+ private final List<ChangeListener> listeners = new ArrayList<ChangeListener>();
+
+
+ /** The conditions actually used in the previous simulation, or null */
+ private SimulationConditions simulatedConditions = null;
+ private String simulatedMotors = null;
+ private FlightData simulatedData = null;
+ private int simulatedRocketID = -1;
+
+
+ /**
+ * Create a new simulation for the rocket. The initial motor configuration is
+ * taken from the default rocket configuration.
+ *
+ * @param rocket the rocket associated with the simulation.
+ */
+ public Simulation(Rocket rocket) {
+ this.rocket = rocket;
+ this.status = Status.NOT_SIMULATED;
+
+ conditions = new SimulationConditions(rocket);
+ conditions.setMotorConfigurationID(
+ rocket.getDefaultConfiguration().getMotorConfigurationID());
+ conditions.addChangeListener(new ConditionListener());
+ }
+
+
+ public Simulation(Rocket rocket, Status status, String name, SimulationConditions conditions,
+ List<String> listeners, FlightData data) {
+
+ if (rocket == null)
+ throw new IllegalArgumentException("rocket cannot be null");
+ if (status == null)
+ throw new IllegalArgumentException("status cannot be null");
+ if (name == null)
+ throw new IllegalArgumentException("name cannot be null");
+ if (conditions == null)
+ throw new IllegalArgumentException("conditions cannot be null");
+
+ this.rocket = rocket;
+
+ if (status == Status.UPTODATE) {
+ this.status = Status.LOADED;
+ } else if (data == null) {
+ this.status = Status.NOT_SIMULATED;
+ } else {
+ this.status = status;
+ }
+
+ this.name = name;
+
+ this.conditions = conditions;
+ conditions.addChangeListener(new ConditionListener());
+
+ if (listeners != null) {
+ this.simulationListeners.addAll(listeners);
+ }
+
+
+ if (data != null && this.status != Status.NOT_SIMULATED) {
+ simulatedData = data;
+ if (this.status == Status.LOADED) {
+ simulatedConditions = conditions.clone();
+ simulatedRocketID = rocket.getModID();
+ }
+ }
+
+ }
+
+
+
+
+ /**
+ * Return a newly created Configuration for this simulation. The configuration
+ * has the motor ID set and all stages active.
+ *
+ * @return a newly created Configuration of the launch conditions.
+ */
+ public Configuration getConfiguration() {
+ Configuration c = new Configuration(rocket);
+ c.setMotorConfigurationID(conditions.getMotorConfigurationID());
+ c.setAllStages();
+ return c;
+ }
+
+ /**
+ * Returns the simulation conditions attached to this simulation. The conditions
+ * may be modified freely, and the status of the simulation will change to reflect
+ * the changes.
+ *
+ * @return the simulation conditions.
+ */
+ public SimulationConditions getConditions() {
+ return conditions;
+ }
+
+
+ /**
+ * Get the list of simulation listeners. The returned list is the one used by
+ * this object; changes to it will reflect changes in the simulation.
+ *
+ * @return the actual list of simulation listeners.
+ */
+ public List<String> getSimulationListeners() {
+ return simulationListeners;
+ }
+
+
+ /**
+ * Return the user-defined name of the simulation.
+ *
+ * @return the name for the simulation.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Set the user-defined name of the simulation. Setting the name to
+ * null yields an empty name.
+ *
+ * @param name the name of the simulation.
+ */
+ public void setName(String name) {
+ if (this.name.equals(name))
+ return;
+
+ if (name == null)
+ this.name = "";
+ else
+ this.name = name;
+
+ fireChangeEvent();
+ }
+
+
+ /**
+ * Returns the status of this simulation. This method examines whether the
+ * simulation has been outdated and returns {@link Status#OUTDATED} accordingly.
+ *
+ * @return the status
+ * @see Status
+ */
+ public Status getStatus() {
+ if (status == Status.UPTODATE || status == Status.LOADED) {
+ if (rocket.getFunctionalModID() != simulatedRocketID ||
+ !conditions.equals(simulatedConditions))
+ return Status.OUTDATED;
+ }
+
+ return status;
+ }
+
+
+
+
+ public void simulate(SimulationListener ... additionalListeners)
+ throws SimulationException {
+
+ if (this.status == Status.EXTERNAL) {
+ throw new SimulationException("Cannot simulate imported simulation.");
+ }
+ Configuration configuration;
+ AerodynamicCalculator calculator;
+ FlightSimulator simulator;
+
+ try {
+ calculator = calculatorClass.newInstance();
+ simulator = simulatorClass.newInstance();
+ } catch (InstantiationException e) {
+ throw new IllegalStateException("Cannot instantiate calculator/simulator.",e);
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException("Cannot access calc/sim instance?! BUG!",e);
+ } catch (NullPointerException e) {
+ throw new IllegalStateException("Calculator or simulator null",e);
+ }
+
+ configuration = this.getConfiguration();
+ calculator.setConfiguration(configuration);
+ simulator.setCalculator(calculator);
+
+ for (SimulationListener l: additionalListeners) {
+ simulator.addSimulationListener(l);
+ }
+
+ for (String className: simulationListeners) {
+ SimulationListener l = null;
+ try {
+ Class<?> c = Class.forName(className);
+ l = (SimulationListener)c.newInstance();
+ } catch (Exception e) {
+ throw new SimulationListenerException("Could not instantiate listener of " +
+ "class: " + className);
+ }
+ simulator.addSimulationListener(l);
+ }
+
+ long t1, t2;
+ System.out.println("Simulation: calling simulator");
+ t1 = System.currentTimeMillis();
+ simulatedData = simulator.simulate(conditions);
+ t2 = System.currentTimeMillis();
+ System.out.println("Simulation: returning from simulator, " +
+ "simulation took "+(t2-t1)+"ms");
+
+ // Set simulated info after simulation, will not be set in case of exception
+ simulatedConditions = conditions.clone();
+ simulatedMotors = configuration.getMotorConfigurationDescription();
+ simulatedRocketID = rocket.getFunctionalModID();
+
+ status = Status.UPTODATE;
+ fireChangeEvent();
+ }
+
+
+ /**
+ * Return the conditions used in the previous simulation, or <code>null</code>
+ * if this simulation has not been run.
+ *
+ * @return the conditions used in the previous simulation, or <code>null</code>.
+ */
+ public SimulationConditions getSimulatedConditions() {
+ return simulatedConditions;
+ }
+
+ /**
+ * Return the warnings generated in the previous simulation, or
+ * <code>null</code> if this simulation has not been run. This is the same
+ * warning set as contained in the <code>FlightData</code> object.
+ *
+ * @return the warnings during the previous simulation, or <code>null</code>.
+ * @see FlightData#getWarningSet()
+ */
+ public WarningSet getSimulatedWarnings() {
+ if (simulatedData == null)
+ return null;
+ return simulatedData.getWarningSet();
+ }
+
+
+ /**
+ * Return a string describing the motor configuration of the previous simulation,
+ * or <code>null</code> if this simulation has not been run.
+ *
+ * @return a description of the motor configuration of the previous simulation, or
+ * <code>null</code>.
+ * @see Rocket#getMotorConfigurationDescription(String)
+ */
+ public String getSimulatedMotorDescription() {
+ return simulatedMotors;
+ }
+
+ /**
+ * Return the flight data of the previous simulation, or <code>null</code> if
+ * this simulation has not been run.
+ *
+ * @return the flight data of the previous simulation, or <code>null</code>.
+ */
+ public FlightData getSimulatedData() {
+ return simulatedData;
+ }
+
+
+
+
+
+
+ @Override
+ public void addChangeListener(ChangeListener listener) {
+ listeners.add(listener);
+ }
+
+ @Override
+ public void removeChangeListener(ChangeListener listener) {
+ listeners.remove(listener);
+ }
+
+ protected void fireChangeEvent() {
+ ChangeListener[] ls = listeners.toArray(new ChangeListener[0]);
+ ChangeEvent e = new ChangeEvent(this);
+ for (ChangeListener l: ls) {
+ l.stateChanged(e);
+ }
+ }
+
+
+
+
+ private class ConditionListener implements ChangeListener {
+
+ private Status oldStatus = null;
+
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ if (getStatus() != oldStatus) {
+ oldStatus = getStatus();
+ fireChangeEvent();
+ }
+ }
+ }
+}
--- /dev/null
+package net.sf.openrocket.document;
+
+public class StorageOptions implements Cloneable {
+
+ public static final double SIMULATION_DATA_NONE = Double.POSITIVE_INFINITY;
+ public static final double SIMULATION_DATA_ALL = 0;
+
+ private boolean compressionEnabled = true;
+
+ private double simulationTimeSkip = SIMULATION_DATA_NONE;
+
+ private boolean explicitlySet = false;
+
+
+ public boolean isCompressionEnabled() {
+ return compressionEnabled;
+ }
+
+ public void setCompressionEnabled(boolean compression) {
+ this.compressionEnabled = compression;
+ }
+
+ public double getSimulationTimeSkip() {
+ return simulationTimeSkip;
+ }
+
+ public void setSimulationTimeSkip(double simulationTimeSkip) {
+ this.simulationTimeSkip = simulationTimeSkip;
+ }
+
+
+
+ public boolean isExplicitlySet() {
+ return explicitlySet;
+ }
+
+ public void setExplicitlySet(boolean explicitlySet) {
+ this.explicitlySet = explicitlySet;
+ }
+
+
+
+ @Override
+ public StorageOptions clone() {
+ try {
+ return (StorageOptions)super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException("CloneNotSupportedException?!?", e);
+ }
+ }
+}
--- /dev/null
+package net.sf.openrocket.file;
+
+import java.io.BufferedInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.zip.GZIPInputStream;
+
+import net.sf.openrocket.document.OpenRocketDocument;
+
+
+/**
+ * A rocket loader that auto-detects the document type and uses the appropriate
+ * loading. Supports loading of GZIPed files as well with transparent
+ * uncompression.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class GeneralRocketLoader extends RocketLoader {
+
+ private static final int READ_BYTES = 300;
+
+ private static final byte[] GZIP_SIGNATURE = { 31, -117 }; // 0x1f, 0x8b
+ private static final byte[] OPENROCKET_SIGNATURE = "<openrocket".getBytes();
+
+ private static final OpenRocketLoader openRocketLoader = new OpenRocketLoader();
+
+ @Override
+ protected OpenRocketDocument loadFromStream(InputStream source) throws IOException,
+ RocketLoadException {
+
+ // Check for mark() support
+ if (!source.markSupported()) {
+ source = new BufferedInputStream(source);
+ }
+
+ // Read using mark()
+ byte[] buffer = new byte[READ_BYTES];
+ int count;
+ source.mark(READ_BYTES + 10);
+ count = source.read(buffer);
+ source.reset();
+
+ if (count < 10) {
+ throw new RocketLoadException("Unsupported or corrupt file.");
+ }
+
+ // Detect the appropriate loader
+
+ // Check for GZIP
+ if (buffer[0] == GZIP_SIGNATURE[0] && buffer[1] == GZIP_SIGNATURE[1]) {
+ OpenRocketDocument doc = loadFromStream(new GZIPInputStream(source));
+ doc.getDefaultStorageOptions().setCompressionEnabled(true);
+ return doc;
+ }
+
+ // Check for OpenRocket
+ int match = 0;
+ for (int i=0; i < count; i++) {
+ if (buffer[i] == OPENROCKET_SIGNATURE[match]) {
+ match++;
+ if (match == OPENROCKET_SIGNATURE.length) {
+ return openRocketLoader.load(source);
+ }
+ } else {
+ match = 0;
+ }
+ }
+
+ throw new RocketLoadException("Unsupported or corrupt file.");
+ }
+}
--- /dev/null
+package net.sf.openrocket.file;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Collection;
+
+public interface Loader<T> {
+
+ public Collection<T> load(InputStream stream, String filename) throws IOException;
+
+}
--- /dev/null
+package net.sf.openrocket.file;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.rocketcomponent.ThrustCurveMotor;
+import net.sf.openrocket.util.Coordinate;
+
+
+public class MotorLoader implements Loader<Motor> {
+
+ /** The charset used when reading RASP files. */
+ public static final String RASP_CHARSET = "ISO-8859-1";
+
+
+
+ public List<Motor> load(InputStream stream, String filename) throws IOException {
+ return loadMotor(stream, filename);
+ }
+
+
+ /**
+ * Load <code>Motor</code> objects from the specified <code>InputStream</code>.
+ * The file type is detected based on the filename extension.
+ *
+ * @param stream the stream from which to read the file.
+ * @param filename the file name, by which the format is detected.
+ * @return a list of <code>Motor</code> objects defined in the file.
+ * @throws IOException if an I/O exception occurs, the file format is unknown
+ * or illegal.
+ */
+ public static List<Motor> loadMotor(InputStream stream, String filename) throws IOException {
+ if (filename == null) {
+ throw new IOException("Unknown file type.");
+ }
+
+ String ext = "";
+ int point = filename.lastIndexOf('.');
+
+ if (point > 0)
+ ext = filename.substring(point+1);
+
+ if (ext.equalsIgnoreCase("eng")) {
+ return loadRASP(stream);
+ }
+
+ throw new IOException("Unknown file type.");
+ }
+
+
+
+
+ ////////////// RASP file format //////////////
+
+
+ /** Manufacturer codes to expand in RASP files */
+ private static final Map<String,String> manufacturerCodes =
+ new HashMap<String,String>();
+ static {
+ manufacturerCodes.put("A", "AeroTech");
+ manufacturerCodes.put("AT", "AeroTech");
+ manufacturerCodes.put("AT-RMS", "AeroTech");
+ manufacturerCodes.put("AT/RCS", "AeroTech");
+ manufacturerCodes.put("AERO", "AeroTech");
+ manufacturerCodes.put("AEROT", "AeroTech");
+ manufacturerCodes.put("ISP", "AeroTech");
+ manufacturerCodes.put("AEROTECH", "AeroTech");
+ manufacturerCodes.put("AEROTECH/APOGEE", "AeroTech");
+ manufacturerCodes.put("AMW", "Animal Motor Works");
+ manufacturerCodes.put("AW", "Animal Motor Works");
+ manufacturerCodes.put("ANIMAL", "Animal Motor Works");
+ manufacturerCodes.put("AP", "Apogee");
+ manufacturerCodes.put("APOG", "Apogee");
+ manufacturerCodes.put("P", "Apogee");
+ manufacturerCodes.put("CES", "Cesaroni");
+ manufacturerCodes.put("CTI", "Cesaroni");
+ manufacturerCodes.put("CS", "Cesaroni");
+ manufacturerCodes.put("CSR", "Cesaroni");
+ manufacturerCodes.put("PRO38", "Cesaroni");
+ manufacturerCodes.put("CR", "Contrail Rocket");
+ manufacturerCodes.put("CONTR", "Contrail Rocket");
+ manufacturerCodes.put("E", "Estes");
+ manufacturerCodes.put("ES", "Estes");
+ manufacturerCodes.put("EM", "Ellis Mountain");
+ manufacturerCodes.put("ELLIS", "Ellis Mountain");
+ manufacturerCodes.put("GR", "Gorilla Rocket Motors");
+ manufacturerCodes.put("GORILLA", "Gorilla Rocket Motors");
+ manufacturerCodes.put("H", "HyperTEK");
+ manufacturerCodes.put("HT", "HyperTEK");
+ manufacturerCodes.put("HYPER", "HyperTEK");
+ manufacturerCodes.put("HYPERTEK", "HyperTEK");
+ manufacturerCodes.put("K", "Kosdon by AeroTech");
+ manufacturerCodes.put("KBA", "Kosdon by AeroTech");
+ manufacturerCodes.put("K/AT", "Kosdon by AeroTech");
+ manufacturerCodes.put("KOSDON", "Kosdon by AeroTech");
+ manufacturerCodes.put("KOSDON/AT", "Kosdon by AeroTech");
+ manufacturerCodes.put("KOSDON-BY-AEROTECH", "Kosdon by AeroTech");
+ manufacturerCodes.put("LOKI", "Loki Research");
+ manufacturerCodes.put("LR", "Loki Research");
+ manufacturerCodes.put("PM", "Public Missiles");
+ manufacturerCodes.put("PML", "Public Missiles");
+ manufacturerCodes.put("PP", "Propulsion Polymers");
+ manufacturerCodes.put("PROP", "Propulsion Polymers");
+ manufacturerCodes.put("PROPULSION", "Propulsion Polymers");
+ manufacturerCodes.put("PROPULSION-POLYMERS", "Propulsion Polymers");
+ manufacturerCodes.put("Q", "Quest");
+ manufacturerCodes.put("QU", "Quest");
+ manufacturerCodes.put("RATT", "RATT Works");
+ manufacturerCodes.put("RT", "RATT Works");
+ manufacturerCodes.put("RTW", "RATT Works");
+ manufacturerCodes.put("RR", "Roadrunner Rocketry");
+ manufacturerCodes.put("ROADRUNNER", "Roadrunner Rocketry");
+ manufacturerCodes.put("RV", "Rocketvision");
+ manufacturerCodes.put("SR", "Sky Ripper Systems");
+ manufacturerCodes.put("SRS", "Sky Ripper Systems");
+ manufacturerCodes.put("SKYR", "Sky Ripper Systems");
+ manufacturerCodes.put("SKYRIPPER", "Sky Ripper Systems");
+ manufacturerCodes.put("WCH", "West Coast Hybrids");
+ manufacturerCodes.put("WCR", "West Coast Hybrids");
+
+ manufacturerCodes.put("SF", "WECO Feuerwerk"); // Previously Sachsen Feuerwerks
+ manufacturerCodes.put("WECO", "WECO Feuerwerk");
+
+ }
+
+ /**
+ * A helper method to load a <code>Motor</code> from a RASP file, read from the
+ * specified <code>InputStream</code>. The charset used is defined in
+ * {@link #RASP_CHARSET}.
+ *
+ * @param stream the InputStream to read.
+ * @return the <code>Motor</code> object.
+ * @throws IOException if an I/O error occurs or if the file format is illegal.
+ * @see #loadRASP(Reader)
+ */
+ public static List<Motor> loadRASP(InputStream stream) throws IOException {
+ return loadRASP(new InputStreamReader(stream, RASP_CHARSET));
+ }
+
+
+
+ /**
+ * Load a <code>Motor</code> from a RASP file specified by the <code>Reader</code>.
+ * The <code>Reader</code> is responsible for using the correct charset.
+ * <p>
+ * The CG is assumed to be located at the center of the motor casing and the mass
+ * is calculated from the thrust curve by assuming a constant exhaust velocity.
+ *
+ * @param reader the source of the file.
+ * @return a list of the {@link Motor} objects defined in the file.
+ * @throws IOException if an I/O error occurs or if the file format is illegal.
+ */
+ public static List<Motor> loadRASP(Reader reader) throws IOException {
+ List<Motor> motors = new ArrayList<Motor>();
+ BufferedReader in = new BufferedReader(reader);
+
+ String manufacturer = "";
+ String designation = "";
+ String comment = "";
+
+ double length = 0;
+ double diameter = 0;
+ ArrayList<Double> delays = null;
+
+ List<Double> time = new ArrayList<Double>();
+ List<Double> thrust = new ArrayList<Double>();
+
+ double propW = 0;
+ double totalW = 0;
+
+ try {
+ String line;
+ String[] pieces, buf;
+
+ line = in.readLine();
+ main: while (line != null) { // Until EOF
+
+ manufacturer = "";
+ designation = "";
+ comment = "";
+ length = 0;
+ diameter = 0;
+ delays = new ArrayList<Double>();
+ propW = 0;
+ totalW = 0;
+ time.clear();
+ thrust .clear();
+
+ // Read comment
+ while (line.length()==0 || line.charAt(0)==';') {
+ if (line.length() > 0) {
+ comment += line.substring(1).trim() + "\n";
+ }
+ line = in.readLine();
+ if (line == null)
+ break main;
+ }
+ comment = comment.trim();
+
+ // Parse header line, example:
+ // F32 24 124 5-10-15-P .0377 .0695 RV
+ // desig diam len delays prop.w tot.w manufacturer
+ pieces = split(line);
+ if (pieces.length != 7) {
+ throw new IOException("Illegal file format.");
+ }
+
+ designation = pieces[0];
+ diameter = Double.parseDouble(pieces[1]) / 1000.0;
+ length = Double.parseDouble(pieces[2]) / 1000.0;
+
+ if (pieces[3].equalsIgnoreCase("None")) {
+
+ } else {
+ buf = split(pieces[3],"[-,]+");
+ for (int i=0; i < buf.length; i++) {
+ if (buf[i].equalsIgnoreCase("P")) {
+ delays.add(Motor.PLUGGED);
+ } else {
+ // Many RASP files have "100" as an only delay
+ double d = Double.parseDouble(buf[i]);
+ if (d < 99)
+ delays.add(d);
+ }
+ }
+ Collections.sort(delays);
+ }
+
+ propW = Double.parseDouble(pieces[4]);
+ totalW = Double.parseDouble(pieces[5]);
+ if (manufacturerCodes.containsKey(pieces[6].toUpperCase())) {
+ manufacturer = manufacturerCodes.get(pieces[6].toUpperCase());
+ } else {
+ manufacturer = pieces[6].replace('_', ' ');
+ }
+
+ // Read the data
+ for (line = in.readLine();
+ (line != null) && (line.length()==0 || line.charAt(0) != ';');
+ line = in.readLine()) {
+
+ buf = split(line);
+ if (buf.length == 0) {
+ continue;
+ } else if (buf.length == 2) {
+
+ time.add(Double.parseDouble(buf[0]));
+ thrust .add(Double.parseDouble(buf[1]));
+
+ } else {
+ throw new IOException("Illegal file format.");
+ }
+ }
+
+ // Comment of EOF encountered, marks the start of the next motor
+ if (time.size() < 2) {
+ throw new IOException("Illegal file format, too short thrust-curve.");
+ }
+ double[] delayArray = new double[delays.size()];
+ for (int i=0; i<delays.size(); i++) {
+ delayArray[i] = delays.get(i);
+ }
+ motors.add(createRASPMotor(manufacturer, designation, comment,
+ length, diameter, delayArray, propW, totalW, time, thrust));
+ }
+
+ } catch (NumberFormatException e) {
+
+ throw new IOException("Illegal file format.");
+
+ } finally {
+
+ in.close();
+
+ }
+
+ return motors;
+ }
+
+
+ /**
+ * Create a motor from RASP file data.
+ * @throws IOException if the data is illegal for a thrust curve
+ */
+ private static Motor createRASPMotor(String manufacturer, String designation,
+ String comment, double length, double diameter, double[] delays,
+ double propW, double totalW, List<Double> time, List<Double> thrust)
+ throws IOException {
+
+ // Add zero time/thrust if necessary
+ if (time.get(0) > 0) {
+ time.add(0, 0.0);
+ thrust.add(0, 0.0);
+ }
+
+ List<Double> mass = calculateMass(time,thrust,totalW,propW);
+
+ double[] timeArray = new double[time.size()];
+ double[] thrustArray = new double[time.size()];
+ Coordinate[] cgArray = new Coordinate[time.size()];
+ for (int i=0; i < time.size(); i++) {
+ timeArray[i] = time.get(i);
+ thrustArray[i] = thrust.get(i);
+ cgArray[i] = new Coordinate(length/2,0,0,mass.get(i));
+ }
+
+ try {
+
+ return new ThrustCurveMotor(manufacturer, designation, comment, Motor.Type.UNKNOWN,
+ delays, diameter, length, timeArray, thrustArray, cgArray);
+
+ } catch (IllegalArgumentException e) {
+
+ // Bad data read from file.
+ throw new IOException("Illegal file format.", e);
+
+ }
+ }
+
+
+
+ /**
+ * Calculate the mass of a motor at distinct points in time based on the
+ * initial total mass, propellant weight and thrust.
+ * <p>
+ * This calculation assumes that the velocity of the exhaust remains constant
+ * during the burning. This derives from the mass-flow and thrust relation
+ * <pre>F = m' * v</pre>
+ *
+ * @param time list of time points
+ * @param thrust thrust at the discrete times
+ * @param total total weight of the motor
+ * @param prop propellant amount consumed during burning
+ * @return a list of the mass at the specified time points
+ */
+ private static List<Double> calculateMass(List<Double> time, List<Double> thrust,
+ double total, double prop) {
+ List<Double> mass = new ArrayList<Double>();
+ List<Double> deltam = new ArrayList<Double>();
+
+ double t0, f0;
+ double totalMassChange = 0;
+ double scale;
+
+ // First calculate mass change between points
+ t0 = time.get(0);
+ f0 = thrust.get(0);
+ for (int i=1; i < time.size(); i++) {
+ double t1 = time.get(i);
+ double f1 = thrust.get(i);
+
+ double dm = 0.5*(f0+f1)*(t1-t0);
+ deltam.add(dm);
+ totalMassChange += dm;
+ }
+
+ // Scale mass change and calculate mass
+ mass.add(total);
+ scale = prop / totalMassChange;
+ for (double dm: deltam) {
+ total -= dm*scale;
+ mass.add(total);
+ }
+
+ return mass;
+ }
+
+
+ /**
+ * Tokenizes a string using whitespace as the delimiter.
+ */
+ private static String[] split(String str) {
+ return split(str,"\\s+");
+ }
+
+ /**
+ * Tokenizes a string using the given delimiter.
+ */
+ private static String[] split(String str, String delim) {
+ String[] pieces = str.split(delim);
+ if (pieces.length==0 || !pieces[0].equals(""))
+ return pieces;
+ return Arrays.copyOfRange(pieces, 1, pieces.length);
+ }
+
+
+
+
+
+ /**
+ * For testing purposes.
+ */
+ public static void main(String[] args) throws IOException {
+ List<Motor> motors;
+
+ if (args.length != 1) {
+ System.out.println("Run with one argument, the RAPS file.");
+ System.exit(1);
+ }
+
+ motors = loadRASP(new FileInputStream(new File(args[0])));
+
+ for (Motor motor: motors) {
+ double time = motor.getTotalTime();
+
+ System.out.println("Motor " + motor);
+ System.out.println("Manufacturer: "+motor.getManufacturer());
+ System.out.println("Designation: "+motor.getDesignation());
+ System.out.println("Type: "+motor.getMotorType().getName());
+ System.out.printf( "Length: %.1f mm\n",motor.getLength()*1000);
+ System.out.printf( "Diameter: %.1f mm\n",motor.getDiameter()*1000);
+ System.out.println("Comment:\n" + motor.getDescription());
+
+ System.out.printf( "Total burn time: %.2f s\n", time);
+ System.out.printf( "Avg. burn time: %.2f s\n", motor.getAverageTime());
+ System.out.printf( "Avg. thrust: %.2f N\n", motor.getAverageThrust());
+ System.out.printf( "Max. thrust: %.2f N\n", motor.getMaxThrust());
+ System.out.printf( "Total impulse: %.2f Ns\n", motor.getTotalImpulse());
+ System.out.println("Delay times: " +
+ Arrays.toString(motor.getStandardDelays()));
+ System.out.println("");
+
+ final double COUNT = 20;
+ for (int i=0; i <= COUNT; i++) {
+ double t = time * i/COUNT;
+ System.out.printf("t=%.2fs F=%.2fN m=%.4fkg\n",
+ t, motor.getThrust(t), motor.getMass(t));
+ }
+ System.out.println("");
+ }
+
+ }
+}
--- /dev/null
+package net.sf.openrocket.file;
+
+import java.awt.Color;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Stack;
+
+import net.sf.openrocket.aerodynamics.Warning;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.database.Databases;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.document.StorageOptions;
+import net.sf.openrocket.document.Simulation.Status;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.BodyComponent;
+import net.sf.openrocket.rocketcomponent.BodyTube;
+import net.sf.openrocket.rocketcomponent.Bulkhead;
+import net.sf.openrocket.rocketcomponent.CenteringRing;
+import net.sf.openrocket.rocketcomponent.ClusterConfiguration;
+import net.sf.openrocket.rocketcomponent.Clusterable;
+import net.sf.openrocket.rocketcomponent.EllipticalFinSet;
+import net.sf.openrocket.rocketcomponent.EngineBlock;
+import net.sf.openrocket.rocketcomponent.ExternalComponent;
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.InnerTube;
+import net.sf.openrocket.rocketcomponent.InternalComponent;
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+import net.sf.openrocket.rocketcomponent.MassComponent;
+import net.sf.openrocket.rocketcomponent.MassObject;
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.NoseCone;
+import net.sf.openrocket.rocketcomponent.Parachute;
+import net.sf.openrocket.rocketcomponent.RadiusRingComponent;
+import net.sf.openrocket.rocketcomponent.RecoveryDevice;
+import net.sf.openrocket.rocketcomponent.ReferenceType;
+import net.sf.openrocket.rocketcomponent.RingComponent;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.ShockCord;
+import net.sf.openrocket.rocketcomponent.Stage;
+import net.sf.openrocket.rocketcomponent.Streamer;
+import net.sf.openrocket.rocketcomponent.StructuralComponent;
+import net.sf.openrocket.rocketcomponent.SymmetricComponent;
+import net.sf.openrocket.rocketcomponent.ThicknessRingComponent;
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
+import net.sf.openrocket.rocketcomponent.TubeCoupler;
+import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish;
+import net.sf.openrocket.rocketcomponent.RocketComponent.Position;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.simulation.FlightDataBranch;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationConditions;
+import net.sf.openrocket.simulation.FlightEvent.Type;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.LineStyle;
+import net.sf.openrocket.util.Reflection;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.XMLReader;
+import org.xml.sax.helpers.DefaultHandler;
+import org.xml.sax.helpers.XMLReaderFactory;
+
+
+/**
+ * Class that loads a rocket definition from an OpenRocket rocket file.
+ * <p>
+ * This class uses SAX to read the XML file format. The
+ * {@link #loadFromStream(InputStream)} method simply sets the system up and
+ * starts the parsing, while the actual logic is in the private inner class
+ * <code>OpenRocketHandler</code>.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class OpenRocketLoader extends RocketLoader {
+
+ @Override
+ public OpenRocketDocument loadFromStream(InputStream source) throws RocketLoadException,
+ IOException {
+ InputSource xmlSource = new InputSource(source);
+ OpenRocketHandler handler = new OpenRocketHandler();
+
+ DelegatorHandler xmlhandler = new DelegatorHandler(handler, warnings);
+
+ try {
+ XMLReader parser = XMLReaderFactory.createXMLReader();
+ parser.setContentHandler(xmlhandler);
+ parser.setErrorHandler(xmlhandler);
+ parser.parse(xmlSource);
+ } catch (SAXException e) {
+ throw new RocketLoadException("Malformed XML in input.", e);
+ }
+
+
+ OpenRocketDocument doc = handler.getDocument();
+ doc.getDefaultConfiguration().setAllStages();
+
+ // Deduce suitable time skip
+ double timeSkip = StorageOptions.SIMULATION_DATA_NONE;
+ for (Simulation s: doc.getSimulations()) {
+ if (s.getStatus() == Simulation.Status.EXTERNAL ||
+ s.getStatus() == Simulation.Status.NOT_SIMULATED)
+ continue;
+ if (s.getSimulatedData() == null)
+ continue;
+ if (s.getSimulatedData().getBranchCount() == 0)
+ continue;
+ FlightDataBranch branch = s.getSimulatedData().getBranch(0);
+ if (branch == null)
+ continue;
+ List<Double> list = branch.get(FlightDataBranch.TYPE_TIME);
+ if (list == null)
+ continue;
+
+ double previousTime = Double.NaN;
+ for (double time: list) {
+ if (time - previousTime < timeSkip)
+ timeSkip = time-previousTime;
+ previousTime = time;
+ }
+ }
+ // Round value
+ timeSkip = Math.rint(timeSkip*100)/100;
+
+ doc.getDefaultStorageOptions().setSimulationTimeSkip(timeSkip);
+ doc.getDefaultStorageOptions().setCompressionEnabled(false); // Set by caller if compressed
+ doc.getDefaultStorageOptions().setExplicitlySet(false);
+ return doc;
+ }
+
+}
+
+
+
+class DocumentConfig {
+
+ /* Remember to update OpenRocketSaver as well! */
+ public static final String[] SUPPORTED_VERSIONS = { "0.9", "1.0" };
+
+
+ //////// Component constructors
+ static final HashMap<String, Constructor<? extends RocketComponent>> constructors = new HashMap<String, Constructor<? extends RocketComponent>>();
+ static {
+ try {
+ // External components
+ constructors.put("bodytube", BodyTube.class.getConstructor(new Class<?>[0]));
+ constructors.put("transition", Transition.class.getConstructor(new Class<?>[0]));
+ constructors.put("nosecone", NoseCone.class.getConstructor(new Class<?>[0]));
+ constructors.put("trapezoidfinset", TrapezoidFinSet.class.getConstructor(new Class<?>[0]));
+ constructors.put("ellipticalfinset", EllipticalFinSet.class.getConstructor(new Class<?>[0]));
+ constructors.put("freeformfinset", FreeformFinSet.class.getConstructor(new Class<?>[0]));
+ constructors.put("launchlug", LaunchLug.class.getConstructor(new Class<?>[0]));
+
+ // Internal components
+ constructors.put("engineblock", EngineBlock.class.getConstructor(new Class<?>[0]));
+ constructors.put("innertube", InnerTube.class.getConstructor(new Class<?>[0]));
+ constructors.put("tubecoupler", TubeCoupler.class.getConstructor(new Class<?>[0]));
+ constructors.put("bulkhead", Bulkhead.class.getConstructor(new Class<?>[0]));
+ constructors.put("centeringring", CenteringRing.class.getConstructor(new Class<?>[0]));
+
+ constructors.put("masscomponent", MassComponent.class.getConstructor(new Class<?>[0]));
+ constructors.put("shockcord", ShockCord.class.getConstructor(new Class<?>[0]));
+ constructors.put("parachute", Parachute.class.getConstructor(new Class<?>[0]));
+ constructors.put("streamer", Streamer.class.getConstructor(new Class<?>[0]));
+
+ // Other
+ constructors.put("stage", Stage.class.getConstructor(new Class<?>[0]));
+
+ } catch (NoSuchMethodException e) {
+ throw new RuntimeException(
+ "Error in constructing the 'constructors' HashMap.");
+ }
+ }
+
+
+ //////// Parameter setters
+ /*
+ * The keys are of the form Class:param, where Class is the class name and param
+ * the element name. Setters are searched for in descending class order.
+ * A setter of null means setting the parameter is not allowed.
+ */
+ static final HashMap<String, Setter> setters = new HashMap<String, Setter>();
+ static {
+ // RocketComponent
+ setters.put("RocketComponent:name", new StringSetter(
+ Reflection.findMethodStatic(RocketComponent.class, "setName", String.class)));
+ setters.put("RocketComponent:color", new ColorSetter(
+ Reflection.findMethodStatic(RocketComponent.class, "setColor", Color.class)));
+ setters.put("RocketComponent:linestyle", new EnumSetter<LineStyle>(
+ Reflection.findMethodStatic(RocketComponent.class, "setLineStyle", LineStyle.class),
+ LineStyle.class));
+ setters.put("RocketComponent:position", new PositionSetter());
+ setters.put("RocketComponent:overridemass", new OverrideSetter(
+ Reflection.findMethodStatic(RocketComponent.class, "setOverrideMass", double.class),
+ Reflection.findMethodStatic(RocketComponent.class, "setMassOverridden", boolean.class)));
+ setters.put("RocketComponent:overridecg", new OverrideSetter(
+ Reflection.findMethodStatic(RocketComponent.class, "setOverrideCGX", double.class),
+ Reflection.findMethodStatic(RocketComponent.class, "setCGOverridden", boolean.class)));
+ setters.put("RocketComponent:overridesubcomponents", new BooleanSetter(
+ Reflection.findMethodStatic(RocketComponent.class, "setOverrideSubcomponents", boolean.class)));
+ setters.put("RocketComponent:comment", new StringSetter(
+ Reflection.findMethodStatic(RocketComponent.class, "setComment", String.class)));
+
+ // ExternalComponent
+ setters.put("ExternalComponent:finish", new EnumSetter<Finish>(
+ Reflection.findMethodStatic(ExternalComponent.class, "setFinish", Finish.class),
+ Finish.class));
+ setters.put("ExternalComponent:material", new MaterialSetter(
+ Reflection.findMethodStatic(ExternalComponent.class, "setMaterial", Material.class),
+ Material.Type.BULK));
+
+ // BodyComponent
+ setters.put("BodyComponent:length", new DoubleSetter(
+ Reflection.findMethodStatic(BodyComponent.class, "setLength", double.class)));
+
+ // SymmetricComponent
+ setters.put("SymmetricComponent:thickness", new DoubleSetter(
+ Reflection.findMethodStatic(SymmetricComponent.class,"setThickness", double.class),
+ "filled",
+ Reflection.findMethodStatic(SymmetricComponent.class,"setFilled", boolean.class)));
+
+ // BodyTube
+ setters.put("BodyTube:radius", new DoubleSetter(
+ Reflection.findMethodStatic(BodyTube.class, "setRadius", double.class),
+ "auto",
+ Reflection.findMethodStatic(BodyTube.class,"setRadiusAutomatic", boolean.class)));
+
+ // Transition
+ setters.put("Transition:shape", new EnumSetter<Transition.Shape>(
+ Reflection.findMethodStatic(Transition.class, "setType", Transition.Shape.class),
+ Transition.Shape.class));
+ setters.put("Transition:shapeclipped", new BooleanSetter(
+ Reflection.findMethodStatic(Transition.class, "setClipped", boolean.class)));
+ setters.put("Transition:shapeparameter", new DoubleSetter(
+ Reflection.findMethodStatic(Transition.class, "setShapeParameter", double.class)));
+
+ setters.put("Transition:foreradius", new DoubleSetter(
+ Reflection.findMethodStatic(Transition.class, "setForeRadius", double.class),
+ "auto",
+ Reflection.findMethodStatic(Transition.class, "setForeRadiusAutomatic", boolean.class)));
+ setters.put("Transition:aftradius", new DoubleSetter(
+ Reflection.findMethodStatic(Transition.class, "setAftRadius", double.class),
+ "auto",
+ Reflection.findMethodStatic(Transition.class, "setAftRadiusAutomatic", boolean.class)));
+
+ setters.put("Transition:foreshoulderradius", new DoubleSetter(
+ Reflection.findMethodStatic(Transition.class, "setForeShoulderRadius", double.class)));
+ setters.put("Transition:foreshoulderlength", new DoubleSetter(
+ Reflection.findMethodStatic(Transition.class, "setForeShoulderLength", double.class)));
+ setters.put("Transition:foreshoulderthickness", new DoubleSetter(
+ Reflection.findMethodStatic(Transition.class, "setForeShoulderThickness", double.class)));
+ setters.put("Transition:foreshouldercapped", new BooleanSetter(
+ Reflection.findMethodStatic(Transition.class, "setForeShoulderCapped", boolean.class)));
+
+ setters.put("Transition:aftshoulderradius", new DoubleSetter(
+ Reflection.findMethodStatic(Transition.class, "setAftShoulderRadius", double.class)));
+ setters.put("Transition:aftshoulderlength", new DoubleSetter(
+ Reflection.findMethodStatic(Transition.class, "setAftShoulderLength", double.class)));
+ setters.put("Transition:aftshoulderthickness", new DoubleSetter(
+ Reflection.findMethodStatic(Transition.class, "setAftShoulderThickness", double.class)));
+ setters.put("Transition:aftshouldercapped", new BooleanSetter(
+ Reflection.findMethodStatic(Transition.class, "setAftShoulderCapped", boolean.class)));
+
+ // NoseCone - disable disallowed elements
+ setters.put("NoseCone:foreradius", null);
+ setters.put("NoseCone:foreshoulderradius", null);
+ setters.put("NoseCone:foreshoulderlength", null);
+ setters.put("NoseCone:foreshoulderthickness", null);
+ setters.put("NoseCone:foreshouldercapped", null);
+
+ // FinSet
+ setters.put("FinSet:fincount", new IntSetter(
+ Reflection.findMethodStatic(FinSet.class, "setFinCount", int.class)));
+ setters.put("FinSet:rotation", new DoubleSetter(
+ Reflection.findMethodStatic(FinSet.class, "setBaseRotation", double.class), Math.PI/180.0));
+ setters.put("FinSet:thickness", new DoubleSetter(
+ Reflection.findMethodStatic(FinSet.class, "setThickness", double.class)));
+ setters.put("FinSet:crosssection", new EnumSetter<FinSet.CrossSection>(
+ Reflection.findMethodStatic(FinSet.class, "setCrossSection", FinSet.CrossSection.class),
+ FinSet.CrossSection.class));
+ setters.put("FinSet:cant", new DoubleSetter(
+ Reflection.findMethodStatic(FinSet.class, "setCantAngle", double.class), Math.PI/180.0));
+
+ // TrapezoidFinSet
+ setters.put("TrapezoidFinSet:rootchord", new DoubleSetter(
+ Reflection.findMethodStatic(TrapezoidFinSet.class, "setRootChord", double.class)));
+ setters.put("TrapezoidFinSet:tipchord", new DoubleSetter(
+ Reflection.findMethodStatic(TrapezoidFinSet.class, "setTipChord", double.class)));
+ setters.put("TrapezoidFinSet:sweeplength", new DoubleSetter(
+ Reflection.findMethodStatic(TrapezoidFinSet.class, "setSweep", double.class)));
+ setters.put("TrapezoidFinSet:height", new DoubleSetter(
+ Reflection.findMethodStatic(TrapezoidFinSet.class, "setHeight", double.class)));
+
+ // EllipticalFinSet
+ setters.put("EllipticalFinSet:rootchord", new DoubleSetter(
+ Reflection.findMethodStatic(EllipticalFinSet.class, "setLength", double.class)));
+ setters.put("EllipticalFinSet:height", new DoubleSetter(
+ Reflection.findMethodStatic(EllipticalFinSet.class, "setHeight", double.class)));
+
+ // FreeformFinSet points handled as a special handler
+
+ // LaunchLug
+ setters.put("LaunchLug:radius", new DoubleSetter(
+ Reflection.findMethodStatic(LaunchLug.class, "setRadius", double.class)));
+ setters.put("LaunchLug:length", new DoubleSetter(
+ Reflection.findMethodStatic(LaunchLug.class, "setLength", double.class)));
+ setters.put("LaunchLug:thickness", new DoubleSetter(
+ Reflection.findMethodStatic(LaunchLug.class, "setThickness", double.class)));
+ setters.put("LaunchLug:radialDirection", new DoubleSetter(
+ Reflection.findMethodStatic(LaunchLug.class, "setRadialDirection", double.class),
+ Math.PI/180.0));
+
+ // InternalComponent - nothing
+
+ // StructuralComponent
+ setters.put("StructuralComponent:material", new MaterialSetter(
+ Reflection.findMethodStatic(StructuralComponent.class, "setMaterial", Material.class),
+ Material.Type.BULK));
+
+ // RingComponent
+ setters.put("RingComponent:length", new DoubleSetter(
+ Reflection.findMethodStatic(RingComponent.class, "setLength", double.class)));
+ setters.put("RingComponent:radialposition", new DoubleSetter(
+ Reflection.findMethodStatic(RingComponent.class, "setRadialPosition", double.class)));
+ setters.put("RingComponent:radialdirection", new DoubleSetter(
+ Reflection.findMethodStatic(RingComponent.class, "setRadialDirection", double.class),
+ Math.PI / 180.0));
+
+ // ThicknessRingComponent - radius on separate components due to differing automatics
+ setters.put("ThicknessRingComponent:thickness", new DoubleSetter(
+ Reflection.findMethodStatic(ThicknessRingComponent.class, "setThickness", double.class)));
+
+ // EngineBlock
+ setters.put("EngineBlock:outerradius", new DoubleSetter(
+ Reflection.findMethodStatic(EngineBlock.class, "setOuterRadius", double.class),
+ "auto",
+ Reflection.findMethodStatic(EngineBlock.class, "setOuterRadiusAutomatic", boolean.class)));
+
+ // TubeCoupler
+ setters.put("TubeCoupler:outerradius", new DoubleSetter(
+ Reflection.findMethodStatic(TubeCoupler.class, "setOuterRadius", double.class),
+ "auto",
+ Reflection.findMethodStatic(TubeCoupler.class, "setOuterRadiusAutomatic", boolean.class)));
+
+ // InnerTube
+ setters.put("InnerTube:outerradius", new DoubleSetter(
+ Reflection.findMethodStatic(InnerTube.class, "setOuterRadius", double.class)));
+ setters.put("InnerTube:clusterconfiguration", new ClusterConfigurationSetter());
+ setters.put("InnerTube:clusterscale", new DoubleSetter(
+ Reflection.findMethodStatic(InnerTube.class, "setClusterScale", double.class)));
+ setters.put("InnerTube:clusterrotation", new DoubleSetter(
+ Reflection.findMethodStatic(InnerTube.class, "setClusterRotation", double.class),
+ Math.PI / 180.0));
+
+ // RadiusRingComponent
+
+ // Bulkhead
+ setters.put("RadiusRingComponent:innerradius", new DoubleSetter(
+ Reflection.findMethodStatic(RadiusRingComponent.class, "setInnerRadius", double.class)));
+ setters.put("Bulkhead:outerradius", new DoubleSetter(
+ Reflection.findMethodStatic(Bulkhead.class, "setOuterRadius", double.class),
+ "auto",
+ Reflection.findMethodStatic(Bulkhead.class, "setOuterRadiusAutomatic", boolean.class)));
+
+ // CenteringRing
+ setters.put("CenteringRing:innerradius", new DoubleSetter(
+ Reflection.findMethodStatic(CenteringRing.class, "setInnerRadius", double.class),
+ "auto",
+ Reflection.findMethodStatic(CenteringRing.class, "setInnerRadiusAutomatic", boolean.class)));
+ setters.put("CenteringRing:outerradius", new DoubleSetter(
+ Reflection.findMethodStatic(CenteringRing.class, "setOuterRadius", double.class),
+ "auto",
+ Reflection.findMethodStatic(CenteringRing.class, "setOuterRadiusAutomatic", boolean.class)));
+
+
+ // MassObject
+ setters.put("MassObject:packedlength", new DoubleSetter(
+ Reflection.findMethodStatic(MassObject.class, "setLength", double.class)));
+ setters.put("MassObject:packedradius", new DoubleSetter(
+ Reflection.findMethodStatic(MassObject.class, "setRadius", double.class)));
+ setters.put("MassObject:radialposition", new DoubleSetter(
+ Reflection.findMethodStatic(MassObject.class, "setRadialPosition", double.class)));
+ setters.put("MassObject:radialdirection", new DoubleSetter(
+ Reflection.findMethodStatic(MassObject.class, "setRadialDirection", double.class),
+ Math.PI / 180.0));
+
+ // MassComponent
+ setters.put("MassComponent:mass", new DoubleSetter(
+ Reflection.findMethodStatic(MassComponent.class, "setComponentMass", double.class)));
+
+ // ShockCord
+ setters.put("ShockCord:cordlength", new DoubleSetter(
+ Reflection.findMethodStatic(ShockCord.class, "setCordLength", double.class)));
+ setters.put("ShockCord:material", new MaterialSetter(
+ Reflection.findMethodStatic(ShockCord.class, "setMaterial", Material.class),
+ Material.Type.LINE));
+
+ // RecoveryDevice
+ setters.put("RecoveryDevice:cd", new DoubleSetter(
+ Reflection.findMethodStatic(RecoveryDevice.class, "setCD", double.class),
+ "auto",
+ Reflection.findMethodStatic(RecoveryDevice.class, "setCDAutomatic", boolean.class)));
+ setters.put("RecoveryDevice:deployevent", new EnumSetter<RecoveryDevice.DeployEvent>(
+ Reflection.findMethodStatic(RecoveryDevice.class, "setDeployEvent", RecoveryDevice.DeployEvent.class),
+ RecoveryDevice.DeployEvent.class));
+ setters.put("RecoveryDevice:deployaltitude", new DoubleSetter(
+ Reflection.findMethodStatic(RecoveryDevice.class, "setDeployAltitude", double.class)));
+ setters.put("RecoveryDevice:deploydelay", new DoubleSetter(
+ Reflection.findMethodStatic(RecoveryDevice.class, "setDeployDelay", double.class)));
+ setters.put("RecoveryDevice:material", new MaterialSetter(
+ Reflection.findMethodStatic(RecoveryDevice.class, "setMaterial", Material.class),
+ Material.Type.SURFACE));
+
+ // Parachute
+ setters.put("Parachute:diameter", new DoubleSetter(
+ Reflection.findMethodStatic(Parachute.class, "setDiameter", double.class)));
+ setters.put("Parachute:linecount", new IntSetter(
+ Reflection.findMethodStatic(Parachute.class, "setLineCount", int.class)));
+ setters.put("Parachute:linelength", new DoubleSetter(
+ Reflection.findMethodStatic(Parachute.class, "setLineLength", double.class)));
+ setters.put("Parachute:linematerial", new MaterialSetter(
+ Reflection.findMethodStatic(Parachute.class, "setLineMaterial", Material.class),
+ Material.Type.LINE));
+
+ // Streamer
+ setters.put("Streamer:striplength", new DoubleSetter(
+ Reflection.findMethodStatic(Streamer.class, "setStripLength", double.class)));
+ setters.put("Streamer:stripwidth", new DoubleSetter(
+ Reflection.findMethodStatic(Streamer.class, "setStripWidth", double.class)));
+
+ // Rocket
+ // <motorconfiguration> handled by separate handler
+ setters.put("Rocket:referencetype", new EnumSetter<ReferenceType>(
+ Reflection.findMethodStatic(Rocket.class, "setReferenceType", ReferenceType.class),
+ ReferenceType.class));
+ setters.put("Rocket:customreference", new DoubleSetter(
+ Reflection.findMethodStatic(Rocket.class, "setCustomReferenceLength", double.class)));
+ setters.put("Rocket:designer", new StringSetter(
+ Reflection.findMethodStatic(Rocket.class, "setDesigner", String.class)));
+ setters.put("Rocket:revision", new StringSetter(
+ Reflection.findMethodStatic(Rocket.class, "setRevision", String.class)));
+ }
+
+
+ /**
+ * Search for a enum value that has the corresponding name as an XML value. The current
+ * conversion from enum name to XML value is to lowercase the name and strip out all
+ * underscore characters. This method returns a match to these criteria, or <code>null</code>
+ * if no such enum exists.
+ *
+ * @param <T> then enum type.
+ * @param name the XML value, null ok.
+ * @param enumClass the class of the enum.
+ * @return the found enum value, or <code>null</code>.
+ */
+ public static <T extends Enum<T>> Enum<T> findEnum(String name, Class<? extends Enum<T>> enumClass) {
+
+ if (name == null)
+ return null;
+ name = name.trim();
+ for (Enum<T> e: enumClass.getEnumConstants()) {
+ if (e.name().toLowerCase().replace("_", "").equals(name)) {
+ return e;
+ }
+ }
+ return null;
+ }
+
+
+ /**
+ * Convert a string to a double including formatting specifications of the OpenRocket
+ * file format. This accepts all formatting that is valid for
+ * <code>Double.parseDouble(s)</code> and a few others as well ("Inf", "-Inf").
+ *
+ * @param s the string to parse.
+ * @return the numerical value.
+ * @throws NumberFormatException the the string cannot be parsed.
+ */
+ public static double stringToDouble(String s) throws NumberFormatException {
+ if (s == null)
+ throw new NumberFormatException("null string");
+ if (s.equalsIgnoreCase("NaN"))
+ return Double.NaN;
+ if (s.equalsIgnoreCase("Inf"))
+ return Double.POSITIVE_INFINITY;
+ if (s.equalsIgnoreCase("-Inf"))
+ return Double.NEGATIVE_INFINITY;
+ return Double.parseDouble(s);
+ }
+}
+
+
+
+
+/**
+ * The actual handler class. Contains the necessary methods for parsing the SAX source.
+ */
+class DelegatorHandler extends DefaultHandler {
+ private final WarningSet warnings;
+
+ private final Stack<ElementHandler> handlerStack = new Stack<ElementHandler>();
+ private final Stack<StringBuilder> elementData = new Stack<StringBuilder>();
+ private final Stack<HashMap<String, String>> elementAttributes = new Stack<HashMap<String, String>>();
+
+
+ // Ignore all elements as long as ignore > 0
+ private int ignore = 0;
+
+
+ public DelegatorHandler(ElementHandler initialHandler, WarningSet warnings) {
+ this.warnings = warnings;
+ handlerStack.add(initialHandler);
+ elementData.add(new StringBuilder()); // Just in case
+ }
+
+
+ ///////// SAX handlers
+
+ @Override
+ public void startElement(String uri, String localName, String name,
+ Attributes attributes) throws SAXException {
+
+ // Check for ignore
+ if (ignore > 0) {
+ ignore++;
+ return;
+ }
+
+ // Check for unknown namespace
+ if (!uri.equals("")) {
+ warnings.add(Warning.fromString("Unknown namespace element '" + uri
+ + "' encountered, ignoring."));
+ ignore++;
+ return;
+ }
+
+ // Add layer to data stacks
+ elementData.push(new StringBuilder());
+ elementAttributes.push(copyAttributes(attributes));
+
+ // Call the handler
+ ElementHandler h = handlerStack.peek();
+ h = h.openElement(localName, elementAttributes.peek(), warnings);
+ if (h != null) {
+ handlerStack.push(h);
+ } else {
+ // Start ignoring elements
+ ignore++;
+ }
+ }
+
+
+ /**
+ * Stores encountered characters in the elementData stack.
+ */
+ @Override
+ public void characters(char[] chars, int start, int length) throws SAXException {
+ // Check for ignore
+ if (ignore > 0)
+ return;
+
+ StringBuilder sb = elementData.peek();
+ sb.append(chars, start, length);
+ }
+
+
+ /**
+ * Removes the last layer from the stack.
+ */
+ @Override
+ public void endElement(String uri, String localName, String name) throws SAXException {
+
+ // Check for ignore
+ if (ignore > 0) {
+ ignore--;
+ return;
+ }
+
+ // Remove data from stack
+ String data = elementData.pop().toString(); // throws on error
+ HashMap<String, String> attr = elementAttributes.pop();
+
+ // Remove last handler and call the next one
+ ElementHandler h;
+
+ h = handlerStack.pop();
+ h.endHandler(localName, attr, data, warnings);
+
+ h = handlerStack.peek();
+ h.closeElement(localName, attr, data, warnings);
+ }
+
+
+ private static HashMap<String, String> copyAttributes(Attributes atts) {
+ HashMap<String, String> ret = new HashMap<String, String>();
+ for (int i = 0; i < atts.getLength(); i++) {
+ ret.put(atts.getLocalName(i), atts.getValue(i));
+ }
+ return ret;
+ }
+}
+
+
+
+
+abstract class ElementHandler {
+
+ /**
+ * Called when an opening element is encountered. Returns the handler that will handle
+ * the elements within that element, or <code>null</code> if the element and all of
+ * its contents is to be ignored.
+ *
+ * @param element the element name.
+ * @param attributes attributes of the element.
+ * @param warnings the warning set to store warnings in.
+ * @return the handler that handles elements encountered within this element,
+ * or <code>null</code> if the element is to be ignored.
+ */
+ public abstract ElementHandler openElement(String element,
+ HashMap<String, String> attributes, WarningSet warnings);
+
+ /**
+ * Called when an element is closed. The default implementation checks whether there is
+ * any non-space text within the element and if there exists any attributes, and adds
+ * a warning of both. This can be used at the and of the method to check for
+ * spurious data.
+ *
+ * @param element the element name.
+ * @param attributes attributes of the element.
+ * @param content the textual content of the element.
+ * @param warnings the warning set to store warnings in.
+ */
+ public void closeElement(String element, HashMap<String, String> attributes,
+ String content, WarningSet warnings) {
+
+ if (!content.trim().equals("")) {
+ warnings.add(Warning.fromString("Unknown text in element " + element
+ + ", ignoring."));
+ }
+ if (!attributes.isEmpty()) {
+ warnings.add(Warning.fromString("Unknown attributes in element " + element
+ + ", ignoring."));
+ }
+ }
+
+
+ /**
+ * Called when the element block that this handler is handling ends.
+ * The default implementation is a no-op.
+ *
+ * @param warnings the warning set to store warnings in.
+ */
+ public void endHandler(String element, HashMap<String, String> attributes,
+ String content, WarningSet warnings) {
+ // No-op
+ }
+
+}
+
+
+/**
+ * The starting point of the handlers. Accepts a single <openrocket> element and hands
+ * the contents to be read by a OpenRocketContentsHandler.
+ */
+class OpenRocketHandler extends ElementHandler {
+ private OpenRocketContentHandler handler = null;
+
+ /**
+ * Return the OpenRocketDocument read from the file, or <code>null</code> if a document
+ * has not been read yet.
+ *
+ * @return the document read, or null.
+ */
+ public OpenRocketDocument getDocument() {
+ return handler.getDocument();
+ }
+
+ @Override
+ public ElementHandler openElement(String element, HashMap<String, String> attributes,
+ WarningSet warnings) {
+
+ // Check for unknown elements
+ if (!element.equals("openrocket")) {
+ warnings.add(Warning.fromString("Unknown element " + element + ", ignoring."));
+ return null;
+ }
+
+ // Check for first call
+ if (handler != null) {
+ warnings.add(Warning.fromString("Multiple document elements found, ignoring later "
+ + "ones."));
+ return null;
+ }
+
+ // Check version number
+ String version = null;
+ String docVersion = attributes.remove("version");
+ for (String v : DocumentConfig.SUPPORTED_VERSIONS) {
+ if (v.equals(docVersion)) {
+ version = v;
+ break;
+ }
+ }
+ if (version == null) {
+ if (docVersion != null)
+ warnings.add(Warning.fromString("Unsupported document version "
+ + docVersion + ", attempting to read anyway."));
+ else
+ warnings.add(Warning.fromString("Unsupported document version, attempting to"
+ + " read anyway."));
+ }
+
+ handler = new OpenRocketContentHandler();
+ return handler;
+ }
+}
+
+
+/**
+ * Handles the content of the <openrocket> tag.
+ */
+class OpenRocketContentHandler extends ElementHandler {
+ private final OpenRocketDocument doc;
+ private final Rocket rocket;
+
+ private boolean rocketDefined = false;
+ private boolean simulationsDefined = false;
+
+ public OpenRocketContentHandler() {
+ this.rocket = new Rocket();
+ this.doc = new OpenRocketDocument(rocket);
+ }
+
+
+ public OpenRocketDocument getDocument() {
+ if (!rocketDefined)
+ return null;
+ return doc;
+ }
+
+ @Override
+ public ElementHandler openElement(String element, HashMap<String, String> attributes,
+ WarningSet warnings) {
+
+ if (element.equals("rocket")) {
+ if (rocketDefined) {
+ warnings.add(Warning
+ .fromString("Multiple rocket designs within one document, "
+ + "ignoring later ones."));
+ return null;
+ }
+ rocketDefined = true;
+ return new ComponentParameterHandler(rocket);
+ }
+
+ if (element.equals("simulations")) {
+ if (simulationsDefined) {
+ warnings.add(Warning
+ .fromString("Multiple simulation definitions within one document, "
+ + "ignoring later ones."));
+ return null;
+ }
+ simulationsDefined = true;
+ return new SimulationsHandler(doc);
+ }
+
+ warnings.add(Warning.fromString("Unknown element " + element + ", ignoring."));
+
+ return null;
+ }
+}
+
+
+/**
+ * An element handler that does not allow any sub-elements. If any are encountered
+ * a warning is generated and they are ignored.
+ */
+class PlainTextHandler extends ElementHandler {
+ public static final PlainTextHandler INSTANCE = new PlainTextHandler();
+
+ private PlainTextHandler() {
+ }
+
+ @Override
+ public ElementHandler openElement(String element, HashMap<String, String> attributes,
+ WarningSet warnings) {
+ warnings.add(Warning.fromString("Unknown element " + element + ", ignoring."));
+ return null;
+ }
+
+ @Override
+ public void closeElement(String element, HashMap<String, String> attributes,
+ String content, WarningSet warnings) {
+ // Warning from openElement is sufficient.
+ }
+}
+
+
+
+/**
+ * A handler that creates components from the corresponding elements. The control of the
+ * contents is passed on to ComponentParameterHandler.
+ */
+class ComponentHandler extends ElementHandler {
+ private final RocketComponent parent;
+
+ public ComponentHandler(RocketComponent parent) {
+ this.parent = parent;
+ }
+
+ @Override
+ public ElementHandler openElement(String element, HashMap<String, String> attributes,
+ WarningSet warnings) {
+
+ // Attempt to construct new component
+ Constructor<? extends RocketComponent> constructor = DocumentConfig.constructors
+ .get(element);
+ if (constructor == null) {
+ warnings.add(Warning.fromString("Unknown element " + element + ", ignoring."));
+ return null;
+ }
+
+ RocketComponent c;
+ try {
+ c = constructor.newInstance();
+ } catch (InstantiationException e) {
+ throw new RuntimeException("Error constructing component.", e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Error constructing component.", e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException("Error constructing component.", e);
+ }
+
+ parent.addChild(c);
+
+ return new ComponentParameterHandler(c);
+ }
+}
+
+
+/**
+ * A handler that populates the parameters of a previously constructed rocket component.
+ * This uses the setters, or delegates the handling to another handler for specific
+ * elements.
+ */
+class ComponentParameterHandler extends ElementHandler {
+ private final RocketComponent component;
+
+ public ComponentParameterHandler(RocketComponent c) {
+ this.component = c;
+ }
+
+ @Override
+ public ElementHandler openElement(String element, HashMap<String, String> attributes,
+ WarningSet warnings) {
+
+ // Check for specific elements that contain other elements
+ if (element.equals("subcomponents")) {
+ return new ComponentHandler(component);
+ }
+ if (element.equals("motormount")) {
+ if (!(component instanceof MotorMount)) {
+ warnings.add(Warning.fromString("Illegal component defined as motor mount."));
+ return null;
+ }
+ return new MotorMountHandler((MotorMount)component);
+ }
+ if (element.equals("finpoints")) {
+ if (!(component instanceof FreeformFinSet)) {
+ warnings.add(Warning.fromString("Illegal component defined for fin points."));
+ return null;
+ }
+ return new FinSetPointHandler((FreeformFinSet)component);
+ }
+ if (element.equals("motorconfiguration")) {
+ if (!(component instanceof Rocket)) {
+ warnings.add(Warning.fromString("Illegal component defined for motor configuration."));
+ return null;
+ }
+ return new MotorConfigurationHandler((Rocket)component);
+ }
+
+
+ return PlainTextHandler.INSTANCE;
+ }
+
+ @Override
+ public void closeElement(String element, HashMap<String, String> attributes,
+ String content, WarningSet warnings) {
+
+ if (element.equals("subcomponents") || element.equals("motormount") ||
+ element.equals("finpoints") || element.equals("motorconfiguration")) {
+ return;
+ }
+
+ // Search for the correct setter class
+
+ Class<?> c;
+ for (c = component.getClass(); c != null; c = c.getSuperclass()) {
+ String setterKey = c.getSimpleName() + ":" + element;
+ Setter s = DocumentConfig.setters.get(setterKey);
+ if (s != null) {
+ // Setter found
+ System.out.println("Calling with key "+setterKey);
+ s.set(component, content, attributes, warnings);
+ break;
+ }
+ if (DocumentConfig.setters.containsKey(setterKey)) {
+ // Key exists but is null -> invalid parameter
+ c = null;
+ break;
+ }
+ }
+ if (c == null) {
+ warnings.add(Warning.fromString("Unknown parameter type " + element + " for "
+ + component.getComponentName()));
+ }
+ }
+}
+
+
+/**
+ * A handler that reads the <point> specifications within the freeformfinset's
+ * <finpoints> elements.
+ */
+class FinSetPointHandler extends ElementHandler {
+ private final FreeformFinSet finset;
+ private final ArrayList<Coordinate> coordinates = new ArrayList<Coordinate>();
+
+ public FinSetPointHandler(FreeformFinSet finset) {
+ this.finset = finset;
+ }
+
+ @Override
+ public ElementHandler openElement(String element, HashMap<String, String> attributes,
+ WarningSet warnings) {
+ return PlainTextHandler.INSTANCE;
+ }
+
+
+ @Override
+ public void closeElement(String element, HashMap<String, String> attributes,
+ String content, WarningSet warnings) {
+
+ String strx = attributes.remove("x");
+ String stry = attributes.remove("y");
+ if (strx == null || stry == null) {
+ warnings.add(Warning.fromString("Illegal fin points specification, ignoring."));
+ return;
+ }
+ try {
+ double x = Double.parseDouble(strx);
+ double y = Double.parseDouble(stry);
+ coordinates.add(new Coordinate(x,y));
+ } catch (NumberFormatException e) {
+ warnings.add(Warning.fromString("Illegal fin points specification, ignoring."));
+ return;
+ }
+
+ super.closeElement(element, attributes, content, warnings);
+ }
+
+ @Override
+ public void endHandler(String element, HashMap<String, String> attributes,
+ String content, WarningSet warnings) {
+ try {
+ finset.setPoints(coordinates.toArray(new Coordinate[0]));
+ } catch (IllegalArgumentException e) {
+ warnings.add(Warning.fromString("Freeform fin set point definitions illegal, ignoring."));
+ }
+ }
+}
+
+
+class MotorMountHandler extends ElementHandler {
+ private final MotorMount mount;
+ private MotorHandler motorHandler;
+
+ public MotorMountHandler(MotorMount mount) {
+ this.mount = mount;
+ mount.setMotorMount(true);
+ }
+
+ @Override
+ public ElementHandler openElement(String element, HashMap<String, String> attributes,
+ WarningSet warnings) {
+
+ if (element.equals("motor")) {
+ motorHandler = new MotorHandler();
+ return motorHandler;
+ }
+
+ if (element.equals("ignitionevent") ||
+ element.equals("ignitiondelay") ||
+ element.equals("overhang")) {
+ return PlainTextHandler.INSTANCE;
+ }
+
+ warnings.add(Warning.fromString("Unknown element '"+element+"' encountered, ignoring."));
+ return null;
+ }
+
+
+
+ @Override
+ public void closeElement(String element, HashMap<String, String> attributes,
+ String content, WarningSet warnings) {
+
+ if (element.equals("motor")) {
+ String id = attributes.get("configid");
+ if (id == null || id.equals("")) {
+ warnings.add(Warning.fromString("Illegal motor specification, ignoring."));
+ return;
+ }
+
+ Motor motor = motorHandler.getMotor(warnings);
+ mount.setMotor(id, motor);
+ mount.setMotorDelay(id, motorHandler.getDelay(warnings));
+ return;
+ }
+
+ if (element.equals("ignitionevent")) {
+ MotorMount.IgnitionEvent event = null;
+ for (MotorMount.IgnitionEvent e : MotorMount.IgnitionEvent.values()) {
+ if (e.name().toLowerCase().replaceAll("_", "").equals(content)) {
+ event = e;
+ break;
+ }
+ }
+ if (event == null) {
+ warnings.add(Warning.fromString("Unknown ignition event type '"+content+"', ignoring."));
+ return;
+ }
+ mount.setIgnitionEvent(event);
+ return;
+ }
+
+ if (element.equals("ignitiondelay")) {
+ double d;
+ try {
+ d = Double.parseDouble(content);
+ } catch (NumberFormatException nfe) {
+ warnings.add(Warning.fromString("Illegal ignition delay specified, ignoring."));
+ return;
+ }
+ mount.setIgnitionDelay(d);
+ return;
+ }
+
+ if (element.equals("overhang")) {
+ double d;
+ try {
+ d = Double.parseDouble(content);
+ } catch (NumberFormatException nfe) {
+ warnings.add(Warning.fromString("Illegal overhang specified, ignoring."));
+ return;
+ }
+ mount.setMotorOverhang(d);
+ return;
+ }
+
+ super.closeElement(element, attributes, content, warnings);
+ }
+}
+
+
+
+
+class MotorConfigurationHandler extends ElementHandler {
+ private final Rocket rocket;
+ private String name = null;
+ private boolean inNameElement = false;
+
+ public MotorConfigurationHandler(Rocket rocket) {
+ this.rocket = rocket;
+ }
+
+ @Override
+ public ElementHandler openElement(String element, HashMap<String, String> attributes,
+ WarningSet warnings) {
+
+ if (inNameElement || !element.equals("name")) {
+ warnings.add(Warning.FILE_INVALID_PARAMETER);
+ return null;
+ }
+ inNameElement = true;
+
+ return PlainTextHandler.INSTANCE;
+ }
+
+ @Override
+ public void closeElement(String element, HashMap<String, String> attributes,
+ String content, WarningSet warnings) {
+ name = content;
+ }
+
+ @Override
+ public void endHandler(String element, HashMap<String, String> attributes,
+ String content, WarningSet warnings) {
+
+ String configid = attributes.remove("configid");
+ if (configid == null || configid.equals("")) {
+ warnings.add(Warning.FILE_INVALID_PARAMETER);
+ return;
+ }
+
+ if (!rocket.addMotorConfigurationID(configid)) {
+ warnings.add("Duplicate motor configuration ID used.");
+ return;
+ }
+
+ if (name != null && name.trim().length() > 0) {
+ rocket.setMotorConfigurationName(configid, name);
+ }
+
+ if ("true".equals(attributes.remove("default"))) {
+ rocket.getDefaultConfiguration().setMotorConfigurationID(configid);
+ }
+
+ super.closeElement(element, attributes, content, warnings);
+ }
+}
+
+
+class MotorHandler extends ElementHandler {
+ private Motor.Type type = null;
+ private String manufacturer = null;
+ private String designation = null;
+ private double diameter = Double.NaN;
+ private double length = Double.NaN;
+ private double delay = Double.NaN;
+
+ @Override
+ public ElementHandler openElement(String element, HashMap<String, String> attributes,
+ WarningSet warnings) {
+ return PlainTextHandler.INSTANCE;
+ }
+
+
+ /**
+ * Return the motor to use, or null.
+ */
+ public Motor getMotor(WarningSet warnings) {
+ if (designation == null) {
+ warnings.add(Warning.fromString("No motor specified, ignoring."));
+ return null;
+ }
+ Motor[] motors = Databases.findMotors(type, manufacturer, designation, diameter, length);
+ if (motors.length == 0) {
+ String str = "No motor with designation '"+designation+"'";
+ if (manufacturer != null)
+ str += " for manufacturer '" + manufacturer + "'";
+ warnings.add(Warning.fromString(str + " found."));
+ return null;
+ }
+ if (motors.length > 1) {
+ String str = "Multiple motors with designation '"+designation+"'";
+ if (manufacturer != null)
+ str += " for manufacturer '" + manufacturer + "'";
+ warnings.add(Warning.fromString(str + " found, one chosen arbitrarily."));
+ }
+ return motors[0];
+ }
+
+
+ /**
+ * Return the delay to use for the motor.
+ */
+ public double getDelay(WarningSet warnings) {
+ if (Double.isNaN(delay)) {
+ warnings.add(Warning.fromString("Motor delay not specified, assuming no ejection charge."));
+ return Motor.PLUGGED;
+ }
+ return delay;
+ }
+
+
+ @Override
+ public void closeElement(String element, HashMap<String, String> attributes,
+ String content, WarningSet warnings) {
+
+ content = content.trim();
+
+ if (element.equals("type")) {
+
+ // Motor type
+ type = null;
+ for (Motor.Type t: Motor.Type.values()) {
+ if (t.name().toLowerCase().equals(content)) {
+ type = t;
+ break;
+ }
+ }
+ if (type == null) {
+ warnings.add(Warning.fromString("Unknown motor type '"+content+"', ignoring."));
+ }
+
+ } else if (element.equals("manufacturer")) {
+
+ // Manufacturer
+ manufacturer = content;
+
+ } else if (element.equals("designation")) {
+
+ // Designation
+ designation = content;
+
+ } else if (element.equals("diameter")) {
+
+ // Diameter
+ diameter = Double.NaN;
+ try {
+ diameter = Double.parseDouble(content);
+ } catch (NumberFormatException e) {
+ // Ignore
+ }
+ if (Double.isNaN(diameter)) {
+ warnings.add(Warning.fromString("Illegal motor diameter specified, ignoring."));
+ }
+
+ } else if (element.equals("length")) {
+
+ // Length
+ length = Double.NaN;
+ try {
+ length = Double.parseDouble(content);
+ } catch (NumberFormatException ignore) { }
+
+ if (Double.isNaN(length)) {
+ warnings.add(Warning.fromString("Illegal motor diameter specified, ignoring."));
+ }
+
+ } else if (element.equals("delay")) {
+
+ // Delay
+ delay = Double.NaN;
+ if (content.equals("none")) {
+ delay = Motor.PLUGGED;
+ } else {
+ try {
+ delay = Double.parseDouble(content);
+ } catch (NumberFormatException ignore) { }
+
+ if (Double.isNaN(delay)) {
+ warnings.add(Warning.fromString("Illegal motor delay specified, ignoring."));
+ }
+
+ }
+
+ } else {
+ super.closeElement(element, attributes, content, warnings);
+ }
+ }
+
+}
+
+
+
+class SimulationsHandler extends ElementHandler {
+ private final OpenRocketDocument doc;
+ private SingleSimulationHandler handler;
+
+ public SimulationsHandler(OpenRocketDocument doc) {
+ this.doc = doc;
+ }
+
+ @Override
+ public ElementHandler openElement(String element, HashMap<String, String> attributes,
+ WarningSet warnings) {
+
+ if (!element.equals("simulation")) {
+ warnings.add("Unknown element '"+element+"', ignoring.");
+ return null;
+ }
+
+ handler = new SingleSimulationHandler(doc);
+ return handler;
+ }
+}
+
+class SingleSimulationHandler extends ElementHandler {
+
+ private final OpenRocketDocument doc;
+
+ private String name;
+
+ private SimulationConditionsHandler conditionHandler;
+ private FlightDataHandler dataHandler;
+
+ private final List<String> listeners = new ArrayList<String>();
+
+ public SingleSimulationHandler(OpenRocketDocument doc) {
+ this.doc = doc;
+ }
+
+
+
+ @Override
+ public ElementHandler openElement(String element, HashMap<String, String> attributes,
+ WarningSet warnings) {
+
+ if (element.equals("name") || element.equals("simulator") ||
+ element.equals("calculator") || element.equals("listener")) {
+ return PlainTextHandler.INSTANCE;
+ } else if (element.equals("conditions")) {
+ conditionHandler = new SimulationConditionsHandler(doc.getRocket());
+ return conditionHandler;
+ } else if (element.equals("flightdata")) {
+ dataHandler = new FlightDataHandler();
+ return dataHandler;
+ } else {
+ warnings.add("Unknown element '"+element+"', ignoring.");
+ return null;
+ }
+ }
+
+ @Override
+ public void closeElement(String element, HashMap<String, String> attributes,
+ String content, WarningSet warnings) {
+
+ if (element.equals("name")) {
+ name = content;
+ } else if (element.equals("simulator")) {
+ if (!content.equals("RK4Simulator")) {
+ warnings.add("Unknown simulator specified, ignoring.");
+ }
+ } else if (element.equals("calculator")) {
+ if (!content.equals("BarrowmanSimulator")) {
+ warnings.add("Unknown calculator specified, ignoring.");
+ }
+ } else if (element.equals("listener") && content.trim().length() > 0) {
+ listeners.add(content.trim());
+ }
+
+ }
+
+ @Override
+ public void endHandler(String element, HashMap<String, String> attributes,
+ String content, WarningSet warnings) {
+
+ String s = attributes.get("status");
+ Simulation.Status status = (Status) DocumentConfig.findEnum(s, Simulation.Status.class);
+ if (status == null) {
+ warnings.add("Simulation status unknown, assuming outdated.");
+ status = Simulation.Status.OUTDATED;
+ }
+
+ SimulationConditions conditions;
+ if (conditionHandler != null) {
+ conditions = conditionHandler.getConditions();
+ } else {
+ warnings.add("Simulation conditions not defined, using defaults.");
+ conditions = new SimulationConditions(doc.getRocket());
+ }
+
+ if (name == null)
+ name = "Simulation";
+
+ FlightData data;
+ if (dataHandler == null)
+ data = null;
+ else
+ data = dataHandler.getFlightData();
+
+ Simulation simulation = new Simulation(doc.getRocket(), status, name,
+ conditions, listeners, data);
+
+ doc.addSimulation(simulation);
+ }
+}
+
+
+
+class SimulationConditionsHandler extends ElementHandler {
+ private SimulationConditions conditions;
+ private AtmosphereHandler atmosphereHandler;
+
+ public SimulationConditionsHandler(Rocket rocket) {
+ conditions = new SimulationConditions(rocket);
+ }
+
+ public SimulationConditions getConditions() {
+ return conditions;
+ }
+
+ @Override
+ public ElementHandler openElement(String element, HashMap<String, String> attributes,
+ WarningSet warnings) {
+ if (element.equals("atmosphere")) {
+ atmosphereHandler = new AtmosphereHandler(attributes.get("model"));
+ return atmosphereHandler;
+ }
+ return PlainTextHandler.INSTANCE;
+ }
+
+ @Override
+ public void closeElement(String element, HashMap<String, String> attributes,
+ String content, WarningSet warnings) {
+
+ double d = Double.NaN;
+ try {
+ d = Double.parseDouble(content);
+ } catch (NumberFormatException ignore) { }
+
+
+ if (element.equals("configid")) {
+ if (content.equals("")) {
+ conditions.setMotorConfigurationID(null);
+ } else {
+ conditions.setMotorConfigurationID(content);
+ }
+ } else if (element.equals("launchrodlength")) {
+ if (Double.isNaN(d)) {
+ warnings.add("Illegal launch rod length defined, ignoring.");
+ } else {
+ conditions.setLaunchRodLength(d);
+ }
+ } else if (element.equals("launchrodangle")) {
+ if (Double.isNaN(d)) {
+ warnings.add("Illegal launch rod angle defined, ignoring.");
+ } else {
+ conditions.setLaunchRodAngle(d*Math.PI/180);
+ }
+ } else if (element.equals("launchroddirection")) {
+ if (Double.isNaN(d)) {
+ warnings.add("Illegal launch rod direction defined, ignoring.");
+ } else {
+ conditions.setLaunchRodDirection(d*Math.PI/180);
+ }
+ } else if (element.equals("windaverage")) {
+ if (Double.isNaN(d)) {
+ warnings.add("Illegal average windspeed defined, ignoring.");
+ } else {
+ conditions.setWindSpeedAverage(d);
+ }
+ } else if (element.equals("windturbulence")) {
+ if (Double.isNaN(d)) {
+ warnings.add("Illegal wind turbulence intensity defined, ignoring.");
+ } else {
+ conditions.setWindTurbulenceIntensity(d);
+ }
+ } else if (element.equals("launchaltitude")) {
+ if (Double.isNaN(d)) {
+ warnings.add("Illegal launch altitude defined, ignoring.");
+ } else {
+ conditions.setLaunchAltitude(d);
+ }
+ } else if (element.equals("launchlatitude")) {
+ if (Double.isNaN(d)) {
+ warnings.add("Illegal launch latitude defined, ignoring.");
+ } else {
+ conditions.setLaunchLatitude(d);
+ }
+ } else if (element.equals("atmosphere")) {
+ atmosphereHandler.storeSettings(conditions, warnings);
+ } else if (element.equals("timestep")) {
+ if (Double.isNaN(d)) {
+ warnings.add("Illegal time step defined, ignoring.");
+ } else {
+ conditions.setTimeStep(d);
+ }
+ }
+ }
+}
+
+
+class AtmosphereHandler extends ElementHandler {
+ private final String model;
+ private double temperature = Double.NaN;
+ private double pressure = Double.NaN;
+
+ public AtmosphereHandler(String model) {
+ this.model = model;
+ }
+
+ @Override
+ public ElementHandler openElement(String element, HashMap<String, String> attributes,
+ WarningSet warnings) {
+ return PlainTextHandler.INSTANCE;
+ }
+
+ @Override
+ public void closeElement(String element, HashMap<String, String> attributes,
+ String content, WarningSet warnings) {
+
+ double d = Double.NaN;
+ try {
+ d = Double.parseDouble(content);
+ } catch (NumberFormatException ignore) { }
+
+ if (element.equals("basetemperature")) {
+ if (Double.isNaN(d)) {
+ warnings.add("Illegal base temperature specified, ignoring.");
+ }
+ temperature = d;
+ } else if (element.equals("basepressure")) {
+ if (Double.isNaN(d)) {
+ warnings.add("Illegal base pressure specified, ignoring.");
+ }
+ pressure = d;
+ } else {
+ super.closeElement(element, attributes, content, warnings);
+ }
+ }
+
+
+ public void storeSettings(SimulationConditions cond, WarningSet warnings) {
+ if (!Double.isNaN(pressure)) {
+ cond.setLaunchPressure(pressure);
+ }
+ if (!Double.isNaN(temperature)) {
+ cond.setLaunchTemperature(temperature);
+ }
+
+ if ("isa".equals(model)) {
+ cond.setISAAtmosphere(true);
+ } else if ("extendedisa".equals(model)){
+ cond.setISAAtmosphere(false);
+ } else {
+ cond.setISAAtmosphere(true);
+ warnings.add("Unknown atmospheric model, using ISA.");
+ }
+ }
+
+}
+
+
+class FlightDataHandler extends ElementHandler {
+
+ private FlightDataBranchHandler dataHandler;
+ private WarningSet warningSet = new WarningSet();
+ private List<FlightDataBranch> branches = new ArrayList<FlightDataBranch>();
+
+ private FlightData data;
+
+ public FlightData getFlightData() {
+ return data;
+ }
+
+ @Override
+ public ElementHandler openElement(String element, HashMap<String, String> attributes,
+ WarningSet warnings) {
+
+ if (element.equals("warning")) {
+ return PlainTextHandler.INSTANCE;
+ }
+ if (element.equals("databranch")) {
+ if (attributes.get("name") == null || attributes.get("types")==null) {
+ warnings.add("Illegal flight data definition, ignoring.");
+ return null;
+ }
+ dataHandler = new FlightDataBranchHandler(attributes.get("name"),
+ attributes.get("types"));
+ return dataHandler;
+ }
+
+ warnings.add("Unknown element '"+element+"' encountered, ignoring.");
+ return null;
+ }
+
+
+ @Override
+ public void closeElement(String element, HashMap<String, String> attributes,
+ String content, WarningSet warnings) {
+
+ if (element.equals("databranch")) {
+ FlightDataBranch branch = dataHandler.getBranch();
+ if (branch.getLength() > 0) {
+ branches.add(branch);
+ }
+ } else if (element.equals("warning")) {
+ warningSet.add(Warning.fromString(content));
+ }
+ }
+
+
+ @Override
+ public void endHandler(String element, HashMap<String, String> attributes,
+ String content, WarningSet warnings) {
+
+ if (branches.size() > 0) {
+ data = new FlightData(branches.toArray(new FlightDataBranch[0]));
+ } else {
+ double maxAltitude = Double.NaN;
+ double maxVelocity = Double.NaN;
+ double maxAcceleration = Double.NaN;
+ double maxMach = Double.NaN;
+ double timeToApogee = Double.NaN;
+ double flightTime = Double.NaN;
+ double groundHitVelocity = Double.NaN;
+
+ try {
+ maxAltitude = DocumentConfig.stringToDouble(attributes.get("maxaltitude"));
+ } catch (NumberFormatException ignore) { }
+ try {
+ maxVelocity = DocumentConfig.stringToDouble(attributes.get("maxvelocity"));
+ } catch (NumberFormatException ignore) { }
+ try {
+ maxAcceleration = DocumentConfig.stringToDouble(attributes.get("maxacceleration"));
+ } catch (NumberFormatException ignore) { }
+ try {
+ maxMach = DocumentConfig.stringToDouble(attributes.get("maxmach"));
+ } catch (NumberFormatException ignore) { }
+ try {
+ timeToApogee = DocumentConfig.stringToDouble(attributes.get("timetoapogee"));
+ } catch (NumberFormatException ignore) { }
+ try {
+ flightTime = DocumentConfig.stringToDouble(attributes.get("flighttime"));
+ } catch (NumberFormatException ignore) { }
+ try {
+ groundHitVelocity =
+ DocumentConfig.stringToDouble(attributes.get("groundhitvelocity"));
+ } catch (NumberFormatException ignore) { }
+
+ data = new FlightData(maxAltitude, maxVelocity, maxAcceleration, maxMach,
+ timeToApogee, flightTime, groundHitVelocity);
+ }
+
+ data.getWarningSet().addAll(warningSet);
+ }
+
+
+}
+
+
+class FlightDataBranchHandler extends ElementHandler {
+ private final FlightDataBranch.Type[] types;
+ private final FlightDataBranch branch;
+
+ public FlightDataBranchHandler(String name, String typeList) {
+ String[] split = typeList.split(",");
+ types = new FlightDataBranch.Type[split.length];
+ for (int i=0; i < split.length; i++) {
+ types[i] = FlightDataBranch.getType(split[i], UnitGroup.UNITS_NONE);
+ }
+
+ // TODO: LOW: May throw an IllegalArgumentException
+ branch = new FlightDataBranch(name, types);
+ }
+
+ public FlightDataBranch getBranch() {
+ branch.immute();
+ return branch;
+ }
+
+ @Override
+ public ElementHandler openElement(String element, HashMap<String, String> attributes,
+ WarningSet warnings) {
+
+ if (element.equals("datapoint"))
+ return PlainTextHandler.INSTANCE;
+ if (element.equals("event"))
+ return PlainTextHandler.INSTANCE;
+
+ warnings.add("Unknown element '"+element+"' encountered, ignoring.");
+ return null;
+ }
+
+
+ @Override
+ public void closeElement(String element, HashMap<String, String> attributes,
+ String content, WarningSet warnings) {
+
+ if (element.equals("event")) {
+ double time;
+ FlightEvent.Type type;
+
+ try {
+ time = DocumentConfig.stringToDouble(attributes.get("time"));
+ } catch (NumberFormatException e) {
+ warnings.add("Illegal event specification, ignoring.");
+ return;
+ }
+
+ type = (Type) DocumentConfig.findEnum(attributes.get("type"), FlightEvent.Type.class);
+ if (type == null) {
+ warnings.add("Illegal event specification, ignoring.");
+ return;
+ }
+
+ branch.addEvent(time, new FlightEvent(type, time));
+ return;
+ }
+
+ if (!element.equals("datapoint")) {
+ warnings.add("Unknown element '"+element+"' encountered, ignoring.");
+ return;
+ }
+
+ // element == "datapoint"
+
+
+ // Check line format
+ String[] split = content.split(",");
+ if (split.length != types.length) {
+ warnings.add("Data point did not contain correct amount of values, ignoring point.");
+ return;
+ }
+
+ // Parse the doubles
+ double[] values = new double[split.length];
+ for (int i=0; i < values.length; i++) {
+ try {
+ values[i] = DocumentConfig.stringToDouble(split[i]);
+ } catch (NumberFormatException e) {
+ warnings.add("Data point format error, ignoring point.");
+ return;
+ }
+ }
+
+ // Add point to branch
+ branch.addPoint();
+ for (int i=0; i < types.length; i++) {
+ branch.setValue(types[i], values[i]);
+ }
+ }
+}
+
+
+
+
+
+
+///////////////// Setters implementation
+
+
+//// Interface
+interface Setter {
+ /**
+ * Set the specified value to the given component.
+ *
+ * @param component the component to which to set.
+ * @param value the value within the element.
+ * @param attributes attributes for the element.
+ * @param warnings the warning set to use.
+ */
+ public void set(RocketComponent component, String value,
+ HashMap<String, String> attributes, WarningSet warnings);
+}
+
+
+//// StringSetter - sets the value to the contained String
+class StringSetter implements Setter {
+ private final Reflection.Method setMethod;
+
+ public StringSetter(Reflection.Method set) {
+ setMethod = set;
+ }
+
+ public void set(RocketComponent c, String s, HashMap<String, String> attributes,
+ WarningSet warnings) {
+ setMethod.invoke(c, s);
+ }
+}
+
+//// IntSetter - set an integer value
+class IntSetter implements Setter {
+ private final Reflection.Method setMethod;
+
+ public IntSetter(Reflection.Method set) {
+ setMethod = set;
+ }
+
+ public void set(RocketComponent c, String s, HashMap<String, String> attributes,
+ WarningSet warnings) {
+ try {
+ int n = Integer.parseInt(s);
+ setMethod.invoke(c, n);
+ } catch (NumberFormatException e) {
+ warnings.add(Warning.FILE_INVALID_PARAMETER);
+ }
+ }
+}
+
+
+//// BooleanSetter - set a boolean value
+class BooleanSetter implements Setter {
+ private final Reflection.Method setMethod;
+
+ public BooleanSetter(Reflection.Method set) {
+ setMethod = set;
+ }
+
+ public void set(RocketComponent c, String s, HashMap<String, String> attributes,
+ WarningSet warnings) {
+
+ s = s.trim();
+ if (s.equalsIgnoreCase("true")) {
+ setMethod.invoke(c, true);
+ } else if (s.equalsIgnoreCase("false")) {
+ setMethod.invoke(c, false);
+ } else {
+ warnings.add(Warning.FILE_INVALID_PARAMETER);
+ }
+ }
+}
+
+
+
+//// DoubleSetter - sets a double value or (alternatively) if a specific string is encountered
+//// calls a setXXX(boolean) method.
+class DoubleSetter implements Setter {
+ private final Reflection.Method setMethod;
+ private final String specialString;
+ private final Reflection.Method specialMethod;
+ private final double multiplier;
+
+ /**
+ * Set only the double value.
+ * @param set set method for the double value.
+ */
+ public DoubleSetter(Reflection.Method set) {
+ this.setMethod = set;
+ this.specialString = null;
+ this.specialMethod = null;
+ this.multiplier = 1.0;
+ }
+
+ /**
+ * Multiply with the given multiplier and set the double value.
+ * @param set set method for the double value.
+ * @param mul multiplier.
+ */
+ public DoubleSetter(Reflection.Method set, double mul) {
+ this.setMethod = set;
+ this.specialString = null;
+ this.specialMethod = null;
+ this.multiplier = mul;
+ }
+
+ /**
+ * Set the double value, or if the value equals the special string, use the
+ * special setter and set it to true.
+ *
+ * @param set double setter.
+ * @param special special string
+ * @param specialMethod boolean setter.
+ */
+ public DoubleSetter(Reflection.Method set, String special,
+ Reflection.Method specialMethod) {
+ this.setMethod = set;
+ this.specialString = special;
+ this.specialMethod = specialMethod;
+ this.multiplier = 1.0;
+ }
+
+
+ public void set(RocketComponent c, String s, HashMap<String, String> attributes,
+ WarningSet warnings) {
+
+ s = s.trim();
+
+ // Check for special case
+ if (specialMethod != null && s.equalsIgnoreCase(specialString)) {
+ specialMethod.invoke(c, true);
+ return;
+ }
+
+ // Normal case
+ try {
+ double d = Double.parseDouble(s);
+ setMethod.invoke(c, d * multiplier);
+ } catch (NumberFormatException e) {
+ warnings.add(Warning.FILE_INVALID_PARAMETER);
+ }
+ }
+}
+
+
+class OverrideSetter implements Setter {
+ private final Reflection.Method setMethod;
+ private final Reflection.Method enabledMethod;
+
+ public OverrideSetter(Reflection.Method set, Reflection.Method enabledMethod) {
+ this.setMethod = set;
+ this.enabledMethod = enabledMethod;
+ }
+
+ public void set(RocketComponent c, String s, HashMap<String, String> attributes,
+ WarningSet warnings) {
+
+ try {
+ double d = Double.parseDouble(s);
+ setMethod.invoke(c, d);
+ enabledMethod.invoke(c, true);
+ } catch (NumberFormatException e) {
+ warnings.add(Warning.FILE_INVALID_PARAMETER);
+ }
+ }
+}
+
+//// EnumSetter - sets a generic enum type
+class EnumSetter<T extends Enum<T>> implements Setter {
+ private final Reflection.Method setter;
+ private final Class<T> enumClass;
+
+ public EnumSetter(Reflection.Method set, Class<T> enumClass) {
+ this.setter = set;
+ this.enumClass = enumClass;
+ }
+
+ @Override
+ public void set(RocketComponent c, String name, HashMap<String, String> attributes,
+ WarningSet warnings) {
+
+ Enum<?> setEnum = DocumentConfig.findEnum(name, enumClass);
+ if (setEnum == null) {
+ warnings.add(Warning.FILE_INVALID_PARAMETER);
+ return;
+ }
+
+ setter.invoke(c, setEnum);
+ }
+}
+
+
+//// ColorSetter - sets a Color value
+class ColorSetter implements Setter {
+ private final Reflection.Method setMethod;
+
+ public ColorSetter(Reflection.Method set) {
+ setMethod = set;
+ }
+
+ public void set(RocketComponent c, String s, HashMap<String, String> attributes,
+ WarningSet warnings) {
+
+ String red = attributes.get("red");
+ String green = attributes.get("green");
+ String blue = attributes.get("blue");
+
+ if (red == null || green == null || blue == null) {
+ warnings.add(Warning.FILE_INVALID_PARAMETER);
+ return;
+ }
+
+ int r, g, b;
+ try {
+ r = Integer.parseInt(red);
+ g = Integer.parseInt(green);
+ b = Integer.parseInt(blue);
+ } catch (NumberFormatException e) {
+ warnings.add(Warning.FILE_INVALID_PARAMETER);
+ return;
+ }
+
+ if (r < 0 || g < 0 || b < 0 || r > 255 || g > 255 || b > 255) {
+ warnings.add(Warning.FILE_INVALID_PARAMETER);
+ return;
+ }
+
+ Color color = new Color(r, g, b);
+ setMethod.invoke(c, color);
+
+ if (!s.trim().equals("")) {
+ warnings.add(Warning.FILE_INVALID_PARAMETER);
+ }
+ }
+}
+
+
+
+class MaterialSetter implements Setter {
+ private final Reflection.Method setMethod;
+ private final Material.Type type;
+
+ public MaterialSetter(Reflection.Method set, Material.Type type) {
+ this.setMethod = set;
+ this.type = type;
+ }
+
+ public void set(RocketComponent c, String name, HashMap<String, String> attributes,
+ WarningSet warnings) {
+
+ Material mat;
+
+ // Check name != ""
+ name = name.trim();
+ if (name.equals("")) {
+ warnings.add(Warning.fromString("Illegal material specification, ignoring."));
+ return;
+ }
+
+ // Parse density
+ double density;
+ String str;
+ str = attributes.remove("density");
+ if (str == null) {
+ warnings.add(Warning.fromString("Illegal material specification, ignoring."));
+ return;
+ }
+ try {
+ density = Double.parseDouble(str);
+ } catch (NumberFormatException e) {
+ warnings.add(Warning.fromString("Illegal material specification, ignoring."));
+ return;
+ }
+
+ // Parse thickness
+// double thickness = 0;
+// str = attributes.remove("thickness");
+// try {
+// if (str != null)
+// thickness = Double.parseDouble(str);
+// } catch (NumberFormatException e){
+// warnings.add(Warning.fromString("Illegal material specification, ignoring."));
+// return;
+// }
+
+ // Check type if specified
+ str = attributes.remove("type");
+ if (str != null && !type.name().toLowerCase().equals(str)) {
+ warnings.add(Warning.fromString("Illegal material type specified, ignoring."));
+ return;
+ }
+
+ mat = Material.newMaterial(type, name, density);
+
+ setMethod.invoke(c, mat);
+ }
+}
+
+
+
+
+class PositionSetter implements Setter {
+
+ public void set(RocketComponent c, String value, HashMap<String, String> attributes,
+ WarningSet warnings) {
+
+ RocketComponent.Position type = (Position) DocumentConfig.findEnum(attributes.get("type"),
+ RocketComponent.Position.class);
+ if (type == null) {
+ warnings.add(Warning.FILE_INVALID_PARAMETER);
+ return;
+ }
+
+ double pos;
+ try {
+ pos = Double.parseDouble(value);
+ } catch (NumberFormatException e) {
+ warnings.add(Warning.FILE_INVALID_PARAMETER);
+ return;
+ }
+
+ if (c instanceof FinSet) {
+ ((FinSet)c).setRelativePosition(type);
+ c.setPositionValue(pos);
+ } else if (c instanceof LaunchLug) {
+ ((LaunchLug)c).setRelativePosition(type);
+ c.setPositionValue(pos);
+ } else if (c instanceof InternalComponent) {
+ ((InternalComponent)c).setRelativePosition(type);
+ c.setPositionValue(pos);
+ } else {
+ warnings.add(Warning.FILE_INVALID_PARAMETER);
+ }
+
+ }
+}
+
+
+
+class ClusterConfigurationSetter implements Setter {
+
+ public void set(RocketComponent component, String value, HashMap<String, String> attributes,
+ WarningSet warnings) {
+
+ if (!(component instanceof Clusterable)) {
+ warnings.add("Illegal component defined as cluster.");
+ return;
+ }
+
+ ClusterConfiguration config = null;
+ for (ClusterConfiguration c: ClusterConfiguration.CONFIGURATIONS) {
+ if (c.getXMLName().equals(value)) {
+ config = c;
+ break;
+ }
+ }
+
+ if (config == null) {
+ warnings.add("Illegal cluster configuration specified.");
+ return;
+ }
+
+ ((Clusterable)component).setClusterConfiguration(config);
+ }
+}
+
+
--- /dev/null
+package net.sf.openrocket.file;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.zip.GZIPOutputStream;
+
+import net.sf.openrocket.aerodynamics.Warning;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.document.StorageOptions;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.simulation.FlightDataBranch;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationConditions;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Pair;
+import net.sf.openrocket.util.Prefs;
+import net.sf.openrocket.util.Reflection;
+
+public class OpenRocketSaver extends RocketSaver {
+
+ /* Remember to update OpenRocketLoader as well! */
+ public static final String FILE_VERSION = "1.0";
+
+ private static final String OPENROCKET_CHARSET = "UTF-8";
+
+ private static final String METHOD_PACKAGE = "net.sf.openrocket.file.openrocket";
+ private static final String METHOD_SUFFIX = "Saver";
+
+ private int indent;
+ private Writer dest;
+
+ @Override
+ public void save(OutputStream output, OpenRocketDocument document, StorageOptions options)
+ throws IOException {
+
+ if (options.isCompressionEnabled()) {
+ output = new GZIPOutputStream(output);
+ }
+
+ dest = new BufferedWriter(new OutputStreamWriter(output, OPENROCKET_CHARSET));
+
+
+ this.indent = 0;
+
+ System.out.println("Writing...");
+
+ writeln("<?xml version='1.0' encoding='utf-8'?>");
+ writeln("<openrocket version=\""+FILE_VERSION+"\" creator=\"OpenRocket "
+ +Prefs.getVersion()+ "\">");
+ indent++;
+
+ // Recursively save the rocket structure
+ saveComponent(document.getRocket());
+
+ writeln("");
+
+ // Save all simulations
+ writeln("<simulations>");
+ indent++;
+ boolean first = true;
+ for (Simulation s: document.getSimulations()) {
+ if (!first)
+ writeln("");
+ first = false;
+ saveSimulation(s, options.getSimulationTimeSkip());
+ }
+ indent--;
+ writeln("</simulations>");
+
+ indent--;
+ writeln("</openrocket>");
+
+ dest.flush();
+ if (output instanceof GZIPOutputStream)
+ ((GZIPOutputStream)output).finish();
+ }
+
+
+
+ @SuppressWarnings("unchecked")
+ private void saveComponent(RocketComponent component) throws IOException {
+
+ Reflection.Method m = Reflection.findMethod(METHOD_PACKAGE, component, METHOD_SUFFIX,
+ "getElements", RocketComponent.class);
+ if (m==null) {
+ throw new RuntimeException("Unable to find saving class for component "+
+ component.getComponentName());
+ }
+
+ // Get the strings to save
+ List<String> list = (List<String>) m.invokeStatic(component);
+ int length = list.size();
+
+ if (length == 0) // Nothing to do
+ return;
+
+ if (length < 2) {
+ throw new RuntimeException("BUG, component data length less than two lines.");
+ }
+
+ // Open element
+ writeln(list.get(0));
+ indent++;
+
+ // Write parameters
+ for (int i=1; i<length-1; i++) {
+ writeln(list.get(i));
+ }
+
+ // Recursively write subcomponents
+ if (component.getChildCount() > 0) {
+ writeln("");
+ writeln("<subcomponents>");
+ indent++;
+ boolean emptyline = false;
+ for (RocketComponent subcomponent: component) {
+ if (emptyline)
+ writeln("");
+ emptyline = true;
+ saveComponent(subcomponent);
+ }
+ indent--;
+ writeln("</subcomponents>");
+ }
+
+ // Close element
+ indent--;
+ writeln(list.get(length-1));
+ }
+
+
+
+ private void saveSimulation(Simulation simulation, double timeSkip) throws IOException {
+ SimulationConditions cond = simulation.getConditions();
+
+ writeln("<simulation status=\"" + enumToXMLName(simulation.getStatus()) +"\">");
+ indent++;
+
+ writeln("<name>" + escapeXML(simulation.getName()) + "</name>");
+ // TODO: MEDIUM: Other simulators/calculators
+ writeln("<simulator>RK4Simulator</simulator>");
+ writeln("<calculator>BarrowmanCalculator</calculator>");
+ writeln("<conditions>");
+ indent++;
+
+ writeElement("configid", cond.getMotorConfigurationID());
+ writeElement("launchrodlength", cond.getLaunchRodLength());
+ writeElement("launchrodangle", cond.getLaunchRodAngle() * 180.0/Math.PI);
+ writeElement("launchroddirection", cond.getLaunchRodDirection() * 180.0/Math.PI);
+ writeElement("windaverage", cond.getWindSpeedAverage());
+ writeElement("windturbulence", cond.getWindTurbulenceIntensity());
+ writeElement("launchaltitude", cond.getLaunchAltitude());
+ writeElement("launchlatitude", cond.getLaunchLatitude());
+
+ if (cond.isISAAtmosphere()) {
+ writeln("<atmosphere model=\"isa\"/>");
+ } else {
+ writeln("<atmosphere model=\"extendedisa\">");
+ indent++;
+ writeElement("basetemperature", cond.getLaunchTemperature());
+ writeElement("basepressure", cond.getLaunchPressure());
+ indent--;
+ writeln("</atmosphere>");
+ }
+
+ writeElement("timestep", cond.getTimeStep());
+
+ indent--;
+ writeln("</conditions>");
+
+
+ for (String s: simulation.getSimulationListeners()) {
+ writeElement("listener", escapeXML(s));
+ }
+
+
+ // Write basic simulation data
+
+ FlightData data = simulation.getSimulatedData();
+ if (data != null) {
+ String str = "<flightdata";
+ if (!Double.isNaN(data.getMaxAltitude()))
+ str += " maxaltitude=\"" + doubleToString(data.getMaxAltitude()) + "\"";
+ if (!Double.isNaN(data.getMaxVelocity()))
+ str += " maxvelocity=\"" + doubleToString(data.getMaxVelocity()) + "\"";
+ if (!Double.isNaN(data.getMaxAcceleration()))
+ str += " maxacceleration=\"" + doubleToString(data.getMaxAcceleration()) + "\"";
+ if (!Double.isNaN(data.getMaxMachNumber()))
+ str += " maxmach=\"" + doubleToString(data.getMaxMachNumber()) + "\"";
+ if (!Double.isNaN(data.getTimeToApogee()))
+ str += " timetoapogee=\"" + doubleToString(data.getTimeToApogee()) + "\"";
+ if (!Double.isNaN(data.getFlightTime()))
+ str += " flighttime=\"" + doubleToString(data.getFlightTime()) + "\"";
+ if (!Double.isNaN(data.getGroundHitVelocity()))
+ str += " groundhitvelocity=\"" + doubleToString(data.getGroundHitVelocity()) + "\"";
+ str += ">";
+ writeln(str);
+ indent++;
+
+ for (Warning w: data.getWarningSet()) {
+ writeElement("warning", escapeXML(w.toString()));
+ }
+
+ // Check whether to store data
+ if (simulation.getStatus() == Simulation.Status.EXTERNAL) // Always store external data
+ timeSkip = 0;
+
+ if (timeSkip != StorageOptions.SIMULATION_DATA_NONE) {
+ for (int i=0; i<data.getBranchCount(); i++) {
+ FlightDataBranch branch = data.getBranch(i);
+ saveFlightDataBranch(branch, timeSkip);
+ }
+ }
+
+ indent--;
+ writeln("</flightdata>");
+ }
+
+ indent--;
+ writeln("</simulation>");
+
+ }
+
+
+
+ private void saveFlightDataBranch(FlightDataBranch branch, double timeSkip) throws IOException {
+ double previousTime = -100;
+
+ if (branch == null)
+ return;
+
+ // Retrieve the types from the branch
+ FlightDataBranch.Type[] types = branch.getTypes();
+
+ if (types.length == 0)
+ return;
+
+ // Retrieve the data from the branch
+ List<List<Double>> data = new ArrayList<List<Double>>(types.length);
+ for (int i=0; i<types.length; i++) {
+ data.add(branch.get(types[i]));
+ }
+ List<Double> timeData = branch.get(FlightDataBranch.TYPE_TIME);
+ if (timeData == null) {
+ // TODO: MEDIUM: External data may not have time data
+ throw new IllegalArgumentException("Data did not contain time data");
+ }
+
+ // Build the <databranch> tag
+ StringBuilder sb = new StringBuilder();
+ sb.append("<databranch name=\"");
+ sb.append(escapeXML(branch.getBranchName()));
+ sb.append("\" types=\"");
+ for (int i=0; i<types.length; i++) {
+ if (i > 0)
+ sb.append(",");
+ sb.append(escapeXML(types[i].getName()));
+ }
+ sb.append("\">");
+ writeln(sb.toString());
+ indent++;
+
+ // Write events
+ for (Pair<Double,FlightEvent> p: branch.getEvents()) {
+ writeln("<event time=\"" + doubleToString(p.getU())
+ + "\" type=\"" + enumToXMLName(p.getV().getType()) + "\"/>");
+ }
+
+ // Write the data
+ int length = branch.getLength();
+ if (length > 0) {
+ writeDataPointString(data, 0, sb);
+ previousTime = timeData.get(0);
+ }
+
+ for (int i=1; i < length-1; i++) {
+ if (Math.abs(timeData.get(i) - previousTime - timeSkip) <
+ Math.abs(timeData.get(i+1) - previousTime - timeSkip)) {
+ writeDataPointString(data, i, sb);
+ previousTime = timeData.get(i);
+ }
+ }
+
+ if (length > 1) {
+ writeDataPointString(data, length-1, sb);
+ }
+
+ indent--;
+ writeln("</databranch>");
+ }
+
+ private void writeDataPointString(List<List<Double>> data, int index, StringBuilder sb)
+ throws IOException {
+ sb.setLength(0);
+ sb.append("<datapoint>");
+ for (int j=0; j < data.size(); j++) {
+ if (j > 0)
+ sb.append(",");
+ sb.append(doubleToString(data.get(j).get(index)));
+ }
+ sb.append("</datapoint>");
+ writeln(sb.toString());
+ }
+
+
+
+ private void writeElement(String element, Object content) throws IOException {
+ if (content == null)
+ content = "";
+ writeln("<"+element+">"+content+"</"+element+">");
+ }
+
+
+
+ private void writeln(String str) throws IOException {
+ if (str.length() == 0) {
+ dest.write("\n");
+ return;
+ }
+ String s="";
+ for (int i=0; i<indent; i++)
+ s=s+" ";
+ s = s+str+"\n";
+ dest.write(s);
+ }
+
+
+ /**
+ * Return a string of the double value with suitable precision.
+ * The string is the shortest representation of the value including the
+ * required precision.
+ *
+ * @param d the value to present.
+ * @return a representation with suitable precision.
+ */
+ public static final String doubleToString(double d) {
+
+ // Check for special cases
+ if (MathUtil.equals(d, 0))
+ return "0";
+
+ if (Double.isNaN(d))
+ return "NaN";
+
+ if (Double.isInfinite(d)) {
+ if (d < 0)
+ return "-Inf";
+ else
+ return "Inf";
+ }
+
+
+ double abs = Math.abs(d);
+
+ if (abs < 0.001) {
+ // Compact exponential notation
+ int exp = 0;
+
+ while (abs < 1.0) {
+ abs *= 10;
+ exp++;
+ }
+
+ String sign = (d < 0) ? "-" : "";
+ return sign + String.format((Locale)null, "%.4fe-%d", abs, exp);
+ }
+ if (abs < 0.01)
+ return String.format((Locale)null, "%.7f", d);
+ if (abs < 0.1)
+ return String.format((Locale)null, "%.6f", d);
+ if (abs < 1)
+ return String.format((Locale)null, "%.5f", d);
+ if (abs < 10)
+ return String.format((Locale)null, "%.4f", d);
+ if (abs < 100)
+ return String.format((Locale)null, "%.3f", d);
+ if (abs < 1000)
+ return String.format((Locale)null, "%.2f", d);
+ if (abs < 10000)
+ return String.format((Locale)null, "%.1f", d);
+ if (abs < 100000000.0)
+ return String.format((Locale)null, "%.0f", d);
+
+ // Compact exponential notation
+ int exp = 0;
+ while (abs >= 10.0) {
+ abs /= 10;
+ exp++;
+ }
+
+ String sign = (d < 0) ? "-" : "";
+ return sign + String.format((Locale)null, "%.4fe%d", abs, exp);
+ }
+
+
+
+ public static void main(String[] arg) {
+ double d = -0.000000123456789123;
+
+
+ for (int i=0; i< 20; i++) {
+ String str = doubleToString(d);
+ System.out.println(str + " -> " + Double.parseDouble(str));
+ d *= 10;
+ }
+
+
+ System.out.println("Value: "+ Double.parseDouble("1.2345e9"));
+
+ }
+
+
+ /**
+ * Return the XML equivalent of an enum name.
+ *
+ * @param e the enum to save.
+ * @return the corresponding XML name.
+ */
+ public static String enumToXMLName(Enum<?> e) {
+ return e.name().toLowerCase().replace("_", "");
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file;
+
+public class RocketLoadException extends Exception {
+
+ public RocketLoadException() {
+ }
+
+ public RocketLoadException(String message) {
+ super(message);
+ }
+
+ public RocketLoadException(Throwable cause) {
+ super(cause);
+ }
+
+ public RocketLoadException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.document.OpenRocketDocument;
+
+
+public abstract class RocketLoader {
+ protected final WarningSet warnings = new WarningSet();
+
+
+ /**
+ * Loads a rocket from the specified File object.
+ */
+ public final OpenRocketDocument load(File source) throws RocketLoadException {
+ warnings.clear();
+
+ try {
+ return load(new BufferedInputStream(new FileInputStream(source)));
+ } catch (FileNotFoundException e) {
+ throw new RocketLoadException("File not found: " + source);
+ }
+ }
+
+ /**
+ * Loads a rocket from the specified InputStream.
+ */
+ public final OpenRocketDocument load(InputStream source) throws RocketLoadException {
+ warnings.clear();
+
+ try {
+ return loadFromStream(source);
+ } catch (RocketLoadException e) {
+ throw e;
+ } catch (IOException e) {
+ throw new RocketLoadException("I/O error: " + e.getMessage());
+ } catch (Exception e) {
+ throw new RocketLoadException("An unknown error occurred. Please report a bug.", e);
+ } catch (Throwable e) {
+ throw new RocketLoadException("A serious error occurred and the software may be "
+ + "unstable. Save your designs and restart OpenRocket.", e);
+ }
+ }
+
+
+
+ /**
+ * This method is called by the default implementations of {@link #load(File)}
+ * and {@link #load(InputStream)} to load the rocket.
+ *
+ * @throws RocketLoadException if an error occurs during loading.
+ */
+ protected abstract OpenRocketDocument loadFromStream(InputStream source) throws IOException,
+ RocketLoadException;
+
+
+
+ public final WarningSet getWarnings() {
+ return warnings;
+ }
+}
--- /dev/null
+package net.sf.openrocket.file;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.document.StorageOptions;
+
+
+public abstract class RocketSaver {
+
+ /**
+ * Save the document to the specified file using the default storage options.
+ *
+ * @param dest the destination file.
+ * @param document the document to save.
+ * @throws IOException in case of an I/O error.
+ */
+ public final void save(File dest, OpenRocketDocument document) throws IOException {
+ save(dest, document, document.getDefaultStorageOptions());
+ }
+
+
+ /**
+ * Save the document to the specified file using the given storage options.
+ *
+ * @param dest the destination file.
+ * @param document the document to save.
+ * @param options the storage options.
+ * @throws IOException in case of an I/O error.
+ */
+ public void save(File dest, OpenRocketDocument document, StorageOptions options)
+ throws IOException {
+ OutputStream s = new BufferedOutputStream(new FileOutputStream(dest));
+ try {
+ save(s, document, options);
+ } finally {
+ s.close();
+ }
+ }
+
+
+ /**
+ * Save the document to the specified output stream using the default storage options.
+ *
+ * @param dest the destination stream.
+ * @param doc the document to save.
+ * @throws IOException in case of an I/O error.
+ */
+ public final void save(OutputStream dest, OpenRocketDocument doc) throws IOException {
+ save(dest, doc, doc.getDefaultStorageOptions());
+ }
+
+
+ /**
+ * Save the document to the specified output stream using the given storage options.
+ *
+ * @param dest the destination stream.
+ * @param doc the document to save.
+ * @param options the storage options.
+ * @throws IOException in case of an I/O error.
+ */
+ public abstract void save(OutputStream dest, OpenRocketDocument doc,
+ StorageOptions options) throws IOException;
+
+
+
+
+
+
+
+ public static String escapeXML(String s) {
+
+ s = s.replace("&", "&");
+ s = s.replace("<", "<");
+ s = s.replace(">", ">");
+ s = s.replace("\"",""");
+ s = s.replace("'", "'");
+
+ for (int i=0; i < s.length(); i++) {
+ char n = s.charAt(i);
+ if (((n < 32) && (n != 9) && (n != 10) && (n != 13)) || (n == 127)) {
+ s = s.substring(0,i) + "&#" + ((int)n) + ";" + s.substring(i+1);
+ }
+ }
+
+ return s;
+ }
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+public class BodyComponentSaver extends ExternalComponentSaver {
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+
+ // Body components have a natural length, store it now
+ elements.add("<length>"+((net.sf.openrocket.rocketcomponent.BodyComponent)c).getLength()+"</length>");
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BodyTubeSaver extends SymmetricComponentSaver {
+
+ private static final BodyTubeSaver instance = new BodyTubeSaver();
+
+ public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+ List<String> list = new ArrayList<String>();
+
+ list.add("<bodytube>");
+ instance.addParams(c, list);
+ list.add("</bodytube>");
+
+ return list;
+ }
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+ net.sf.openrocket.rocketcomponent.BodyTube tube = (net.sf.openrocket.rocketcomponent.BodyTube) c;
+
+ if (tube.isRadiusAutomatic())
+ elements.add("<radius>auto</radius>");
+ else
+ elements.add("<radius>" + tube.getRadius() + "</radius>");
+
+ if (tube.isMotorMount()) {
+ elements.addAll(motorMountParams(tube));
+ }
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class BulkheadSaver extends RadiusRingComponentSaver {
+
+ private static final BulkheadSaver instance = new BulkheadSaver();
+
+ public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+ List<String> list = new ArrayList<String>();
+
+ list.add("<bulkhead>");
+ instance.addParams(c, list);
+ list.add("</bulkhead>");
+
+ return list;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CenteringRingSaver extends RadiusRingComponentSaver {
+
+ private static final CenteringRingSaver instance = new CenteringRingSaver();
+
+ public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+ List<String> list = new ArrayList<String>();
+
+ list.add("<centeringring>");
+ instance.addParams(c, list);
+ list.add("</centeringring>");
+
+ return list;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+public class ComponentAssemblySaver extends RocketComponentSaver {
+
+ // No-op
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class EllipticalFinSetSaver extends FinSetSaver {
+
+ private static final EllipticalFinSetSaver instance = new EllipticalFinSetSaver();
+
+ public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+ ArrayList<String> list = new ArrayList<String>();
+
+ list.add("<ellipticalfinset>");
+ instance.addParams(c,list);
+ list.add("</ellipticalfinset>");
+
+ return list;
+ }
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+
+ net.sf.openrocket.rocketcomponent.EllipticalFinSet fins = (net.sf.openrocket.rocketcomponent.EllipticalFinSet)c;
+ elements.add("<rootchord>"+fins.getLength()+"</rootchord>");
+ elements.add("<height>"+fins.getHeight()+"</height>");
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class EngineBlockSaver extends ThicknessRingComponentSaver {
+
+ private static final EngineBlockSaver instance = new EngineBlockSaver();
+
+ public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+ List<String> list = new ArrayList<String>();
+
+ list.add("<engineblock>");
+ instance.addParams(c, list);
+ list.add("</engineblock>");
+
+ return list;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.ExternalComponent;
+
+
+public class ExternalComponentSaver extends RocketComponentSaver {
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+
+ ExternalComponent ext = (ExternalComponent)c;
+
+ // Finish enum names are currently the same except for case
+ elements.add("<finish>" + ext.getFinish().name().toLowerCase() + "</finish>");
+
+ // Material
+ elements.add(materialParam(ext.getMaterial()));
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+public class FinSetSaver extends ExternalComponentSaver {
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+
+ net.sf.openrocket.rocketcomponent.FinSet fins = (net.sf.openrocket.rocketcomponent.FinSet) c;
+ elements.add("<fincount>" + fins.getFinCount() + "</fincount>");
+ elements.add("<rotation>" + (fins.getBaseRotation() * 180.0 / Math.PI) + "</rotation>");
+ elements.add("<thickness>" + fins.getThickness() + "</thickness>");
+ elements.add("<crosssection>" + fins.getCrossSection().name().toLowerCase()
+ + "</crosssection>");
+ elements.add("<cant>" + (fins.getCantAngle() * 180.0 / Math.PI) + "</cant>");
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.util.Coordinate;
+
+
+public class FreeformFinSetSaver extends FinSetSaver {
+
+ private static final FreeformFinSetSaver instance = new FreeformFinSetSaver();
+
+ public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+ ArrayList<String> list = new ArrayList<String>();
+
+ list.add("<freeformfinset>");
+ instance.addParams(c,list);
+ list.add("</freeformfinset>");
+
+ return list;
+ }
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+
+ FreeformFinSet fins = (FreeformFinSet)c;
+ elements.add("<finpoints>");
+ for (Coordinate p: fins.getFinPoints()) {
+ elements.add(" <point x=\"" + p.x + "\" y=\"" + p.y + "\"/>");
+ }
+ elements.add("</finpoints>");
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.InnerTube;
+
+
+public class InnerTubeSaver extends ThicknessRingComponentSaver {
+
+ private static final InnerTubeSaver instance = new InnerTubeSaver();
+
+ public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+ List<String> list = new ArrayList<String>();
+
+ list.add("<innertube>");
+ instance.addParams(c, list);
+ list.add("</innertube>");
+
+ return list;
+ }
+
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+ InnerTube tube = (InnerTube) c;
+
+ elements.add("<clusterconfiguration>" + tube.getClusterConfiguration().getXMLName()
+ + "</clusterconfiguration>");
+ elements.add("<clusterscale>" + tube.getClusterScale() + "</clusterscale>");
+ elements.add("<clusterrotation>" + (tube.getClusterRotation() * 180.0 / Math.PI)
+ + "</clusterrotation>");
+
+ if (tube.isMotorMount()) {
+ elements.addAll(motorMountParams(tube));
+ }
+
+
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+public class InternalComponentSaver extends RocketComponentSaver {
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+
+ // Nothing to save
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+
+
+public class LaunchLugSaver extends ExternalComponentSaver {
+
+ private static final LaunchLugSaver instance = new LaunchLugSaver();
+
+ public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+ List<String> list = new ArrayList<String>();
+
+ list.add("<launchlug>");
+ instance.addParams(c, list);
+ list.add("</launchlug>");
+
+ return list;
+ }
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+ LaunchLug lug = (LaunchLug) c;
+
+ elements.add("<radius>" + lug.getRadius() + "</radius>");
+ elements.add("<length>" + lug.getLength() + "</length>");
+ elements.add("<thickness>" + lug.getThickness() + "</thickness>");
+ elements.add("<radialdirection>" + (lug.getRadialDirection()*180.0/Math.PI) + "</radialdirection>");
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.MassComponent;
+
+
+public class MassComponentSaver extends MassObjectSaver {
+
+ private static final MassComponentSaver instance = new MassComponentSaver();
+
+ public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+ List<String> list = new ArrayList<String>();
+
+ list.add("<masscomponent>");
+ instance.addParams(c, list);
+ list.add("</masscomponent>");
+
+ return list;
+ }
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+
+ MassComponent mass = (MassComponent) c;
+
+ elements.add("<mass>" + mass.getMass() + "</mass>");
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.MassObject;
+
+
+public class MassObjectSaver extends InternalComponentSaver {
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+
+ MassObject mass = (MassObject) c;
+
+ elements.add("<packedlength>" + mass.getLength() + "</packedlength>");
+ elements.add("<packedradius>" + mass.getRadius() + "</packedradius>");
+ elements.add("<radialposition>" + mass.getRadialPosition() + "</radialposition>");
+ elements.add("<radialdirection>" + (mass.getRadialDirection() * 180.0 / Math.PI)
+ + "</radialdirection>");
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class NoseConeSaver extends TransitionSaver {
+
+ private static final NoseConeSaver instance = new NoseConeSaver();
+
+ public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+ ArrayList<String> list = new ArrayList<String>();
+
+ list.add("<nosecone>");
+ instance.addParams(c,list);
+ list.add("</nosecone>");
+
+ return list;
+ }
+
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+
+ // Transition handles nose cone saving as well
+ }
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.Parachute;
+
+
+public class ParachuteSaver extends RecoveryDeviceSaver {
+
+ private static final ParachuteSaver instance = new ParachuteSaver();
+
+ public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+ List<String> list = new ArrayList<String>();
+
+ list.add("<parachute>");
+ instance.addParams(c, list);
+ list.add("</parachute>");
+
+ return list;
+ }
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+ Parachute para = (Parachute) c;
+
+ elements.add("<diameter>" + para.getDiameter() + "</diameter>");
+ elements.add("<linecount>" + para.getLineCount() + "</linecount>");
+ elements.add("<linelength>" + para.getLineLength() + "</linelength>");
+ elements.add(materialParam("linematerial", para.getLineMaterial()));
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.Bulkhead;
+import net.sf.openrocket.rocketcomponent.RadiusRingComponent;
+
+
+public class RadiusRingComponentSaver extends RingComponentSaver {
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+
+ RadiusRingComponent comp = (RadiusRingComponent)c;
+ if (comp.isOuterRadiusAutomatic())
+ elements.add("<outerradius>auto</outerradius>");
+ else
+ elements.add("<outerradius>" + comp.getOuterRadius() + "</outerradius>");
+ if (!(comp instanceof Bulkhead)) {
+ if (comp.isInnerRadiusAutomatic())
+ elements.add("<innerradius>auto</innerradius>");
+ else
+ elements.add("<innerradius>" + comp.getInnerRadius() + "</innerradius>");
+ }
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.RecoveryDevice;
+
+
+public class RecoveryDeviceSaver extends MassObjectSaver {
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+
+ RecoveryDevice dev = (RecoveryDevice) c;
+
+ if (dev.isCDAutomatic())
+ elements.add("<cd>auto</cd>");
+ else
+ elements.add("<cd>" + dev.getCD() + "</cd>");
+
+ elements.add("<deployevent>" + dev.getDeployEvent().name().toLowerCase() + "</deployevent>");
+ elements.add("<deployaltitude>" + dev.getDeployAltitude() + "</deployaltitude>");
+ elements.add("<deploydelay>" + dev.getDeployDelay() + "</deploydelay>");
+ elements.add(materialParam(dev.getMaterial()));
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.RingComponent;
+
+
+public class RingComponentSaver extends StructuralComponentSaver {
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+
+ RingComponent ring = (RingComponent) c;
+
+ elements.add("<length>" + ring.getLength() + "</length>");
+ elements.add("<radialposition>" + ring.getRadialPosition() + "</radialposition>");
+ elements.add("<radialdirection>" + (ring.getRadialDirection() * 180.0 / Math.PI)
+ + "</radialdirection>");
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import net.sf.openrocket.file.RocketSaver;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.ComponentAssembly;
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.util.LineStyle;
+
+
+public class RocketComponentSaver {
+
+ protected RocketComponentSaver() {
+ // Prevent instantiation from outside the package
+ }
+
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ elements.add("<name>" + RocketSaver.escapeXML(c.getName()) + "</name>");
+
+
+ // Save color and line style if significant
+ if (!(c instanceof Rocket || c instanceof ComponentAssembly)) {
+ Color color = c.getColor();
+ if (color != null) {
+ elements.add("<color red=\"" + color.getRed() + "\" green=\"" + color.getGreen()
+ + "\" blue=\"" + color.getBlue() + "\"/>");
+ }
+
+ LineStyle style = c.getLineStyle();
+ if (style != null) {
+ // Type names currently equivalent to the enum names except for case.
+ elements.add("<linestyle>" + style.name().toLowerCase() + "</linestyle>");
+ }
+ }
+
+
+ // Save position unless "AFTER"
+ if (c.getRelativePosition() != RocketComponent.Position.AFTER) {
+ // The type names are currently equivalent to the enum names except for case.
+ String type = c.getRelativePosition().name().toLowerCase();
+ elements.add("<position type=\"" + type + "\">" + c.getPositionValue() + "</position>");
+ }
+
+
+ // Overrides
+ boolean overridden = false;
+ if (c.isMassOverridden()) {
+ elements.add("<overridemass>" + c.getOverrideMass() + "</overridemass>");
+ overridden = true;
+ }
+ if (c.isCGOverridden()) {
+ elements.add("<overridecg>" + c.getOverrideCGX() + "</overridecg>");
+ overridden = true;
+ }
+ if (overridden) {
+ elements.add("<overridesubcomponents>" + c.getOverrideSubcomponents()
+ + "</overridesubcomponents>");
+ }
+
+
+ // Comment
+ if (c.getComment().length() > 0) {
+ elements.add("<comment>" + RocketSaver.escapeXML(c.getComment()) + "</comment>");
+ }
+
+ }
+
+
+
+
+ protected final String materialParam(Material mat) {
+ return materialParam("material", mat);
+ }
+
+
+ protected final String materialParam(String tag, Material mat) {
+ String str = "<" + tag;
+
+ switch (mat.getType()) {
+ case LINE:
+ str += " type=\"line\"";
+ break;
+ case SURFACE:
+ str += " type=\"surface\"";
+ break;
+ case BULK:
+ str += " type=\"bulk\"";
+ break;
+ default:
+ throw new RuntimeException("Unknown material type: " + mat.getType());
+ }
+
+ return str + " density=\"" + mat.getDensity() + "\">" + RocketSaver.escapeXML(mat.getName()) + "</"+tag+">";
+ }
+
+
+ protected final List<String> motorMountParams(MotorMount mount) {
+ if (!mount.isMotorMount())
+ return Collections.emptyList();
+
+ String[] motorConfigIDs = ((RocketComponent) mount).getRocket().getMotorConfigurationIDs();
+ List<String> elements = new ArrayList<String>();
+
+ elements.add("<motormount>");
+
+ for (String id : motorConfigIDs) {
+ Motor motor = mount.getMotor(id);
+
+ // Nothing is stored if no motor loaded
+ if (motor == null)
+ continue;
+
+ elements.add(" <motor configid=\"" + id + "\">");
+ if (motor.getMotorType() != Motor.Type.UNKNOWN) {
+ elements.add(" <type>" + motor.getMotorType().name().toLowerCase() + "</type>");
+ }
+ elements.add(" <manufacturer>" + RocketSaver.escapeXML(motor.getManufacturer()) + "</manufacturer>");
+ elements.add(" <designation>" + RocketSaver.escapeXML(motor.getDesignation()) + "</designation>");
+ elements.add(" <diameter>" + motor.getDiameter() + "</diameter>");
+ elements.add(" <length>" + motor.getLength() + "</length>");
+
+ // Motor delay
+ if (mount.getMotorDelay(id) == Motor.PLUGGED) {
+ elements.add(" <delay>none</delay>");
+ } else {
+ elements.add(" <delay>" + mount.getMotorDelay(id) + "</delay>");
+ }
+
+ elements.add(" </motor>");
+ }
+
+ elements.add(" <ignitionevent>"
+ + mount.getIgnitionEvent().name().toLowerCase().replace("_", "")
+ + "</ignitionevent>");
+
+ elements.add(" <ignitiondelay>" + mount.getIgnitionDelay() + "</ignitiondelay>");
+ elements.add(" <overhang>" + mount.getMotorOverhang() + "</overhang>");
+
+ elements.add("</motormount>");
+
+ return elements;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.ReferenceType;
+import net.sf.openrocket.rocketcomponent.Rocket;
+
+
+public class RocketSaver extends RocketComponentSaver {
+
+ private static final RocketSaver instance = new RocketSaver();
+
+ public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+ ArrayList<String> list = new ArrayList<String>();
+
+ list.add("<rocket>");
+ instance.addParams(c, list);
+ list.add("</rocket>");
+
+ return list;
+ }
+
+
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+
+ Rocket rocket = (Rocket) c;
+
+ if (rocket.getDesigner().length() > 0) {
+ elements.add("<designer>"
+ + net.sf.openrocket.file.RocketSaver.escapeXML(rocket.getDesigner())
+ + "</designer>");
+ }
+ if (rocket.getRevision().length() > 0) {
+ elements.add("<revision>"
+ + net.sf.openrocket.file.RocketSaver.escapeXML(rocket.getRevision())
+ + "</revision>");
+ }
+
+
+ // Motor configurations
+ String defId = rocket.getDefaultConfiguration().getMotorConfigurationID();
+ for (String id : rocket.getMotorConfigurationIDs()) {
+ if (id == null)
+ continue;
+
+ String str = "<motorconfiguration configid=\"" + id + "\"";
+ if (id.equals(defId))
+ str += " default=\"true\"";
+
+ if (rocket.getMotorConfigurationName(id) == "") {
+ str += "/>";
+ } else {
+ str += "><name>" + net.sf.openrocket.file.RocketSaver.escapeXML(rocket.getMotorConfigurationName(id))
+ + "</name></motorconfiguration>";
+ }
+ elements.add(str);
+ }
+
+ // Reference diameter
+ elements.add("<referencetype>" + rocket.getReferenceType().name().toLowerCase()
+ + "</referencetype>");
+ if (rocket.getReferenceType() == ReferenceType.CUSTOM) {
+ elements.add("<customreference>" + rocket.getCustomReferenceLength()
+ + "</customreference>");
+ }
+
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.ShockCord;
+
+
+public class ShockCordSaver extends MassObjectSaver {
+
+ private static final ShockCordSaver instance = new ShockCordSaver();
+
+ public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+ List<String> list = new ArrayList<String>();
+
+ list.add("<shockcord>");
+ instance.addParams(c, list);
+ list.add("</shockcord>");
+
+ return list;
+ }
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+
+ ShockCord mass = (ShockCord) c;
+
+ elements.add("<cordlength>" + mass.getCordLength() + "</cordlength>");
+ elements.add(materialParam(mass.getMaterial()));
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+
+public class StageSaver extends ComponentAssemblySaver {
+
+ private static final StageSaver instance = new StageSaver();
+
+ public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+ ArrayList<String> list = new ArrayList<String>();
+
+ list.add("<stage>");
+ instance.addParams(c,list);
+ list.add("</stage>");
+
+ return list;
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.Streamer;
+
+
+public class StreamerSaver extends RecoveryDeviceSaver {
+
+ private static final StreamerSaver instance = new StreamerSaver();
+
+ public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+ List<String> list = new ArrayList<String>();
+
+ list.add("<streamer>");
+ instance.addParams(c, list);
+ list.add("</streamer>");
+
+ return list;
+ }
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+ Streamer st = (Streamer) c;
+
+ elements.add("<striplength>" + st.getStripLength() + "</striplength>");
+ elements.add("<stripwidth>" + st.getStripWidth() + "</stripwidth>");
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.StructuralComponent;
+
+
+public class StructuralComponentSaver extends InternalComponentSaver {
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+
+ StructuralComponent comp = (StructuralComponent)c;
+ elements.add(materialParam(comp.getMaterial()));
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+public class SymmetricComponentSaver extends BodyComponentSaver {
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+
+ net.sf.openrocket.rocketcomponent.SymmetricComponent comp = (net.sf.openrocket.rocketcomponent.SymmetricComponent)c;
+ if (comp.isFilled())
+ elements.add("<thickness>filled</thickness>");
+ else
+ elements.add("<thickness>"+comp.getThickness()+"</thickness>");
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.ThicknessRingComponent;
+
+
+public class ThicknessRingComponentSaver extends RingComponentSaver {
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+
+ ThicknessRingComponent comp = (ThicknessRingComponent)c;
+ if (comp.isOuterRadiusAutomatic())
+ elements.add("<outerradius>auto</outerradius>");
+ else
+ elements.add("<outerradius>" + comp.getOuterRadius() + "</outerradius>");
+ elements.add("<thickness>" + comp.getThickness() + "</thickness>");
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.rocketcomponent.NoseCone;
+import net.sf.openrocket.rocketcomponent.Transition;
+
+
+public class TransitionSaver extends SymmetricComponentSaver {
+
+ private static final TransitionSaver instance = new TransitionSaver();
+
+ public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+ ArrayList<String> list = new ArrayList<String>();
+
+ list.add("<transition>");
+ instance.addParams(c, list);
+ list.add("</transition>");
+
+ return list;
+ }
+
+
+ /*
+ * Note: This method must be capable of handling nose cones as well.
+ */
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+ net.sf.openrocket.rocketcomponent.Transition trans = (net.sf.openrocket.rocketcomponent.Transition) c;
+ boolean nosecone = (trans instanceof NoseCone);
+
+
+ Transition.Shape shape = trans.getType();
+ elements.add("<shape>" + shape.getName().toLowerCase() + "</shape>");
+ if (shape.isClippable()) {
+ elements.add("<shapeclipped>" + trans.isClipped() + "</shapeclipped>");
+ }
+ if (shape.usesParameter()) {
+ elements.add("<shapeparameter>" + trans.getShapeParameter() + "</shapeparameter>");
+ }
+
+
+ if (!nosecone) {
+ if (trans.isForeRadiusAutomatic())
+ elements.add("<foreradius>auto</foreradius>");
+ else
+ elements.add("<foreradius>" + trans.getForeRadius() + "</foreradius>");
+ }
+
+ if (trans.isAftRadiusAutomatic())
+ elements.add("<aftradius>auto</aftradius>");
+ else
+ elements.add("<aftradius>" + trans.getAftRadius() + "</aftradius>");
+
+
+ if (!nosecone) {
+ elements.add("<foreshoulderradius>" + trans.getForeShoulderRadius()
+ + "</foreshoulderradius>");
+ elements.add("<foreshoulderlength>" + trans.getForeShoulderLength()
+ + "</foreshoulderlength>");
+ elements.add("<foreshoulderthickness>" + trans.getForeShoulderThickness()
+ + "</foreshoulderthickness>");
+ elements.add("<foreshouldercapped>" + trans.isForeShoulderCapped()
+ + "</foreshouldercapped>");
+ }
+
+ elements.add("<aftshoulderradius>" + trans.getAftShoulderRadius()
+ + "</aftshoulderradius>");
+ elements.add("<aftshoulderlength>" + trans.getAftShoulderLength()
+ + "</aftshoulderlength>");
+ elements.add("<aftshoulderthickness>" + trans.getAftShoulderThickness()
+ + "</aftshoulderthickness>");
+ elements.add("<aftshouldercapped>" + trans.isAftShoulderCapped()
+ + "</aftshouldercapped>");
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TrapezoidFinSetSaver extends FinSetSaver {
+
+ private static final TrapezoidFinSetSaver instance = new TrapezoidFinSetSaver();
+
+ public static ArrayList<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+ ArrayList<String> list = new ArrayList<String>();
+
+ list.add("<trapezoidfinset>");
+ instance.addParams(c,list);
+ list.add("</trapezoidfinset>");
+
+ return list;
+ }
+
+ @Override
+ protected void addParams(net.sf.openrocket.rocketcomponent.RocketComponent c, List<String> elements) {
+ super.addParams(c, elements);
+
+ net.sf.openrocket.rocketcomponent.TrapezoidFinSet fins = (net.sf.openrocket.rocketcomponent.TrapezoidFinSet)c;
+ elements.add("<rootchord>"+fins.getRootChord()+"</rootchord>");
+ elements.add("<tipchord>"+fins.getTipChord()+"</tipchord>");
+ elements.add("<sweeplength>"+fins.getSweep()+"</sweeplength>");
+ elements.add("<height>"+fins.getHeight()+"</height>");
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.file.openrocket;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class TubeCouplerSaver extends ThicknessRingComponentSaver {
+
+ private static final TubeCouplerSaver instance = new TubeCouplerSaver();
+
+ public static List<String> getElements(net.sf.openrocket.rocketcomponent.RocketComponent c) {
+ List<String> list = new ArrayList<String>();
+
+ list.add("<tubecoupler>");
+ instance.addParams(c, list);
+ list.add("</tubecoupler>");
+
+ return list;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui;
+
+import javax.swing.BoundedRangeModel;
+import javax.swing.JSlider;
+import javax.swing.plaf.basic.BasicSliderUI;
+
+/**
+ * A simple slider that does not show the current value. GTK l&f shows the value, and cannot
+ * be configured otherwise(!).
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class BasicSlider extends JSlider {
+
+ public BasicSlider(BoundedRangeModel brm) {
+ this(brm,JSlider.HORIZONTAL,false);
+ }
+
+ public BasicSlider(BoundedRangeModel brm, int orientation) {
+ this(brm,orientation,false);
+ }
+
+ public BasicSlider(BoundedRangeModel brm, int orientation, boolean inverted) {
+ super(brm);
+ setOrientation(orientation);
+ setInverted(inverted);
+ setUI(new BasicSliderUI(this));
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui;
+
+import static net.sf.openrocket.unit.Unit.NOUNIT2;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Vector;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTabbedPane;
+import javax.swing.JTable;
+import javax.swing.JToggleButton;
+import javax.swing.ListSelectionModel;
+import javax.swing.SwingConstants;
+import javax.swing.SwingUtilities;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.table.TableCellRenderer;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.aerodynamics.Warning;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.gui.adaptors.Column;
+import net.sf.openrocket.gui.adaptors.ColumnTableModel;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.MotorConfigurationModel;
+import net.sf.openrocket.gui.scalefigure.RocketPanel;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.GUIUtil;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Prefs;
+
+public class ComponentAnalysisDialog extends JDialog implements ChangeListener {
+
+ private static ComponentAnalysisDialog singletonDialog = null;
+
+
+ private final FlightConditions conditions;
+ private final Configuration configuration;
+ private final DoubleModel theta, aoa, mach, roll;
+ private final JToggleButton worstToggle;
+ private boolean fakeChange = false;
+ private AerodynamicCalculator calculator;
+
+ private final ColumnTableModel cpTableModel;
+ private final ColumnTableModel dragTableModel;
+ private final ColumnTableModel rollTableModel;
+
+ private final JList warningList;
+
+
+ private final List<AerodynamicForces> cpData = new ArrayList<AerodynamicForces>();
+ private final List<AerodynamicForces> dragData = new ArrayList<AerodynamicForces>();
+ private double totalCD = 0;
+ private final List<AerodynamicForces> rollData = new ArrayList<AerodynamicForces>();
+
+
+ public ComponentAnalysisDialog(final RocketPanel rocketPanel) {
+ super(SwingUtilities.getWindowAncestor(rocketPanel), "Component analysis");
+
+ JTable table;
+
+ JPanel panel = new JPanel(new MigLayout("fill","[][35lp::][fill][fill]"));
+ add(panel);
+
+ this.configuration = rocketPanel.getConfiguration();
+ this.calculator = rocketPanel.getCalculator().newInstance();
+ this.calculator.setConfiguration(configuration);
+
+
+ conditions = new FlightConditions(configuration);
+
+ rocketPanel.setCPAOA(0);
+ aoa = new DoubleModel(rocketPanel, "CPAOA", UnitGroup.UNITS_ANGLE, 0, Math.PI);
+ rocketPanel.setCPMach(Prefs.getDefaultMach());
+ mach = new DoubleModel(rocketPanel, "CPMach", UnitGroup.UNITS_COEFFICIENT, 0);
+ rocketPanel.setCPTheta(rocketPanel.getFigure().getRotation());
+ theta = new DoubleModel(rocketPanel, "CPTheta", UnitGroup.UNITS_ANGLE, 0, 2*Math.PI);
+ rocketPanel.setCPRoll(0);
+ roll = new DoubleModel(rocketPanel, "CPRoll", UnitGroup.UNITS_ROLL);
+
+
+ panel.add(new JLabel("Wind direction:"),"width 100lp!");
+ panel.add(new UnitSelector(theta,true),"width 50lp!");
+ BasicSlider slider = new BasicSlider(theta.getSliderModel(0, 2*Math.PI));
+ panel.add(slider,"growx, split 2");
+ worstToggle = new JToggleButton("Worst");
+ worstToggle.setSelected(true);
+ worstToggle.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ stateChanged(null);
+ }
+ });
+ slider.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ if (!fakeChange)
+ worstToggle.setSelected(false);
+ }
+ });
+ panel.add(worstToggle,"");
+
+
+ warningList = new JList();
+ JScrollPane scrollPane = new JScrollPane(warningList);
+ scrollPane.setBorder(BorderFactory.createTitledBorder("Warnings:"));
+ panel.add(scrollPane,"gap paragraph, spany 4, width 300lp!, growy 1, height :100lp:, wrap");
+
+
+ panel.add(new JLabel("Angle of attack:"),"width 100lp!");
+ panel.add(new UnitSelector(aoa,true),"width 50lp!");
+ panel.add(new BasicSlider(aoa.getSliderModel(0, Math.PI)),"growx, wrap");
+
+ panel.add(new JLabel("Mach number:"),"width 100lp!");
+ panel.add(new UnitSelector(mach,true),"width 50lp!");
+ panel.add(new BasicSlider(mach.getSliderModel(0, 3)),"growx, wrap");
+
+ panel.add(new JLabel("Roll rate:"), "width 100lp!");
+ panel.add(new UnitSelector(roll,true),"width 50lp!");
+ panel.add(new BasicSlider(roll.getSliderModel(-20*2*Math.PI, 20*2*Math.PI)),
+ "growx, wrap paragraph");
+
+
+ // Stage and motor selection:
+
+ panel.add(new JLabel("Active stages:"),"spanx, split, gapafter rel");
+ panel.add(new StageSelector(configuration),"gapafter paragraph");
+
+ JLabel label = new JLabel("Motor configuration:");
+ label.setHorizontalAlignment(JLabel.RIGHT);
+ panel.add(label,"growx, right");
+ panel.add(new JComboBox(new MotorConfigurationModel(configuration)),"wrap");
+
+
+
+ // Tabbed pane
+
+ JTabbedPane tabbedPane = new JTabbedPane();
+ panel.add(tabbedPane, "spanx, growx, growy");
+
+
+ // Create the CP data table
+ cpTableModel = new ColumnTableModel(
+
+ new Column("Component") {
+ @Override public Object getValueAt(int row) {
+ RocketComponent c = cpData.get(row).component;
+ if (c instanceof Rocket) {
+ return "Total";
+ }
+ return c.toString();
+ }
+ @Override public int getDefaultWidth() {
+ return 200;
+ }
+ },
+ new Column("CG / " + UnitGroup.UNITS_LENGTH.getDefaultUnit().getUnit()) {
+ private Unit unit = UnitGroup.UNITS_LENGTH.getDefaultUnit();
+ @Override public Object getValueAt(int row) {
+ return unit.toString(cpData.get(row).cg.x);
+ }
+ },
+ new Column("Mass / " + UnitGroup.UNITS_MASS.getDefaultUnit().getUnit()) {
+ private Unit unit = UnitGroup.UNITS_MASS.getDefaultUnit();
+ @Override
+ public Object getValueAt(int row) {
+ return unit.toString(cpData.get(row).cg.weight);
+ }
+ },
+ new Column("CP / " + UnitGroup.UNITS_LENGTH.getDefaultUnit().getUnit()) {
+ private Unit unit = UnitGroup.UNITS_LENGTH.getDefaultUnit();
+ @Override public Object getValueAt(int row) {
+ return unit.toString(cpData.get(row).cp.x);
+ }
+ },
+ new Column("<html>C<sub>N<sub>\u03b1</sub></sub>") {
+ @Override public Object getValueAt(int row) {
+ return NOUNIT2.toString(cpData.get(row).cp.weight);
+ }
+ }
+
+ ) {
+ @Override public int getRowCount() {
+ return cpData.size();
+ }
+ };
+
+ table = new JTable(cpTableModel);
+ table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+ table.setSelectionBackground(Color.LIGHT_GRAY);
+ table.setSelectionForeground(Color.BLACK);
+ cpTableModel.setColumnWidths(table.getColumnModel());
+
+ table.setDefaultRenderer(Object.class, new CustomCellRenderer());
+// table.setShowHorizontalLines(false);
+// table.setShowVerticalLines(true);
+
+ JScrollPane scrollpane = new JScrollPane(table);
+ scrollpane.setPreferredSize(new Dimension(600,200));
+
+ tabbedPane.addTab("Stability", null, scrollpane, "Stability information");
+
+
+
+ // Create the drag data table
+ dragTableModel = new ColumnTableModel(
+ new Column("Component") {
+ @Override public Object getValueAt(int row) {
+ RocketComponent c = dragData.get(row).component;
+ if (c instanceof Rocket) {
+ return "Total";
+ }
+ return c.toString();
+ }
+ @Override public int getDefaultWidth() {
+ return 200;
+ }
+ },
+ new Column("<html>Pressure C<sub>D</sub>") {
+ @Override public Object getValueAt(int row) {
+ return dragData.get(row).pressureCD;
+ }
+ },
+ new Column("<html>Base C<sub>D</sub>") {
+ @Override public Object getValueAt(int row) {
+ return dragData.get(row).baseCD;
+ }
+ },
+ new Column("<html>Friction C<sub>D</sub>") {
+ @Override public Object getValueAt(int row) {
+ return dragData.get(row).frictionCD;
+ }
+ },
+ new Column("<html>Total C<sub>D</sub>") {
+ @Override public Object getValueAt(int row) {
+ return dragData.get(row).CD;
+ }
+ }
+ ) {
+ @Override public int getRowCount() {
+ return dragData.size();
+ }
+ };
+
+
+ table = new JTable(dragTableModel);
+ table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+ table.setSelectionBackground(Color.LIGHT_GRAY);
+ table.setSelectionForeground(Color.BLACK);
+ dragTableModel.setColumnWidths(table.getColumnModel());
+
+ table.setDefaultRenderer(Object.class, new DragCellRenderer(new Color(0.5f,1.0f,0.5f)));
+// table.setShowHorizontalLines(false);
+// table.setShowVerticalLines(true);
+
+ scrollpane = new JScrollPane(table);
+ scrollpane.setPreferredSize(new Dimension(600,200));
+
+ tabbedPane.addTab("Drag characteristics", null, scrollpane, "Drag characteristics");
+
+
+
+
+ // Create the roll data table
+ rollTableModel = new ColumnTableModel(
+ new Column("Component") {
+ @Override public Object getValueAt(int row) {
+ RocketComponent c = rollData.get(row).component;
+ if (c instanceof Rocket) {
+ return "Total";
+ }
+ return c.toString();
+ }
+ },
+ new Column("Roll forcing coefficient") {
+ @Override public Object getValueAt(int row) {
+ return rollData.get(row).CrollForce;
+ }
+ },
+ new Column("Roll damping coefficient") {
+ @Override public Object getValueAt(int row) {
+ return rollData.get(row).CrollDamp;
+ }
+ },
+ new Column("<html>Total C<sub>l</sub>") {
+ @Override public Object getValueAt(int row) {
+ return rollData.get(row).Croll;
+ }
+ }
+ ) {
+ @Override public int getRowCount() {
+ return rollData.size();
+ }
+ };
+
+
+ table = new JTable(rollTableModel);
+ table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+ table.setSelectionBackground(Color.LIGHT_GRAY);
+ table.setSelectionForeground(Color.BLACK);
+ rollTableModel.setColumnWidths(table.getColumnModel());
+
+ scrollpane = new JScrollPane(table);
+ scrollpane.setPreferredSize(new Dimension(600,200));
+
+ tabbedPane.addTab("Roll dynamics", null, scrollpane, "Roll dynamics");
+
+
+
+
+
+
+ // Add the data updater to listen to changes in aoa and theta
+ mach.addChangeListener(this);
+ theta.addChangeListener(this);
+ aoa.addChangeListener(this);
+ roll.addChangeListener(this);
+ configuration.addChangeListener(this);
+ this.stateChanged(null);
+
+
+
+ // Remove listeners when closing window
+ this.addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosed(WindowEvent e) {
+ System.out.println("Closing method called: "+this);
+ theta.removeChangeListener(ComponentAnalysisDialog.this);
+ aoa.removeChangeListener(ComponentAnalysisDialog.this);
+ mach.removeChangeListener(ComponentAnalysisDialog.this);
+ roll.removeChangeListener(ComponentAnalysisDialog.this);
+ configuration.removeChangeListener(ComponentAnalysisDialog.this);
+ System.out.println("SETTING NAN VALUES");
+ rocketPanel.setCPAOA(Double.NaN);
+ rocketPanel.setCPTheta(Double.NaN);
+ rocketPanel.setCPMach(Double.NaN);
+ rocketPanel.setCPRoll(Double.NaN);
+ singletonDialog = null;
+ }
+ });
+
+
+ panel.add(new ResizeLabel("Reference length: ", -1),
+ "span, split, gapleft para, gapright rel");
+ DoubleModel dm = new DoubleModel(conditions, "RefLength", UnitGroup.UNITS_LENGTH);
+ UnitSelector sel = new UnitSelector(dm, true);
+ sel.resizeFont(-1);
+ panel.add(sel, "gapright para");
+
+ panel.add(new ResizeLabel("Reference area: ", -1), "gapright rel");
+ dm = new DoubleModel(conditions, "RefArea", UnitGroup.UNITS_AREA);
+ sel = new UnitSelector(dm, true);
+ sel.resizeFont(-1);
+ panel.add(sel, "wrap");
+
+
+
+ // Buttons
+ JButton button;
+
+ // TODO: LOW: printing
+// button = new JButton("Print");
+// button.addActionListener(new ActionListener() {
+// public void actionPerformed(ActionEvent e) {
+// try {
+// table.print();
+// } catch (PrinterException e1) {
+// JOptionPane.showMessageDialog(ComponentAnalysisDialog.this,
+// "An error occurred while printing.", "Print error",
+// JOptionPane.ERROR_MESSAGE);
+// }
+// }
+// });
+// panel.add(button,"tag ok");
+
+ button = new JButton("Close");
+ button.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ ComponentAnalysisDialog.this.dispose();
+ }
+ });
+ panel.add(button,"span, split, tag cancel");
+
+
+ setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
+ GUIUtil.installEscapeCloseOperation(this);
+ pack();
+ }
+
+
+
+ /**
+ * Updates the data in the table and fires a table data change event.
+ */
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ AerodynamicForces forces;
+ WarningSet set = new WarningSet();
+ conditions.setAOA(aoa.getValue());
+ conditions.setTheta(theta.getValue());
+ conditions.setMach(mach.getValue());
+ conditions.setRollRate(roll.getValue());
+ conditions.setReference(configuration);
+
+ if (worstToggle.isSelected()) {
+ calculator.getWorstCP(conditions, null);
+ if (!MathUtil.equals(conditions.getTheta(), theta.getValue())) {
+ fakeChange = true;
+ theta.setValue(conditions.getTheta()); // Fires a stateChanged event
+ fakeChange = false;
+ return;
+ }
+ }
+
+ Map<RocketComponent, AerodynamicForces> data = calculator.getForceAnalysis(conditions, set);
+
+ cpData.clear();
+ dragData.clear();
+ rollData.clear();
+ for (RocketComponent c: configuration) {
+ forces = data.get(c);
+ if (forces == null)
+ continue;
+ if (forces.cp != null) {
+ cpData.add(forces);
+ }
+ if (!Double.isNaN(forces.CD)) {
+ dragData.add(forces);
+ }
+ if (c instanceof FinSet) {
+ rollData.add(forces);
+ }
+ }
+ forces = data.get(configuration.getRocket());
+ if (forces != null) {
+ cpData.add(forces);
+ dragData.add(forces);
+ rollData.add(forces);
+ totalCD = forces.CD;
+ } else {
+ totalCD = 0;
+ }
+
+ // Set warnings
+ if (set.isEmpty()) {
+ warningList.setListData(new String[] {
+ "<html><i><font color=\"gray\">No warnings.</font></i>"
+ });
+ } else {
+ warningList.setListData(new Vector<Warning>(set));
+ }
+
+ cpTableModel.fireTableDataChanged();
+ dragTableModel.fireTableDataChanged();
+ rollTableModel.fireTableDataChanged();
+ }
+
+
+ private class CustomCellRenderer extends JLabel implements TableCellRenderer {
+ private final Font normalFont;
+ private final Font boldFont;
+
+ public CustomCellRenderer() {
+ super();
+ normalFont = getFont();
+ boldFont = normalFont.deriveFont(Font.BOLD);
+ }
+ @Override
+ public Component getTableCellRendererComponent(JTable table, Object value,
+ boolean isSelected, boolean hasFocus, int row, int column) {
+
+ this.setText(value.toString());
+
+ if ((row < 0) || (row >= cpData.size()))
+ return this;
+
+ if (cpData.get(row).component instanceof Rocket) {
+ this.setFont(boldFont);
+ } else {
+ this.setFont(normalFont);
+ }
+ return this;
+ }
+ }
+
+
+
+ private class DragCellRenderer extends JLabel implements TableCellRenderer {
+ private final Font normalFont;
+ private final Font boldFont;
+
+ private final float[] start = { 0.3333f, 0.2f, 1.0f };
+ private final float[] end = { 0.0f, 0.8f, 1.0f };
+
+
+ public DragCellRenderer(Color baseColor) {
+ super();
+ normalFont = getFont();
+ boldFont = normalFont.deriveFont(Font.BOLD);
+ }
+ @Override
+ public Component getTableCellRendererComponent(JTable table, Object value,
+ boolean isSelected, boolean hasFocus, int row, int column) {
+
+ if (value instanceof Double) {
+
+ // A drag coefficient
+ double cd = (Double)value;
+ this.setText(String.format("%.2f (%.0f%%)", cd, 100*cd/totalCD));
+
+ float r = (float)(cd/1.5);
+
+ float hue = MathUtil.clamp(0.3333f * (1-2.0f*r), 0, 0.3333f);
+ float sat = MathUtil.clamp(0.8f*r + 0.1f*(1-r), 0, 1);
+ float val = 1.0f;
+
+ this.setBackground(Color.getHSBColor(hue, sat, val));
+ this.setOpaque(true);
+ this.setHorizontalAlignment(SwingConstants.CENTER);
+
+ } else {
+
+ // Other
+ this.setText(value.toString());
+ this.setOpaque(false);
+ this.setHorizontalAlignment(SwingConstants.LEFT);
+
+ }
+
+ if ((row < 0) || (row >= dragData.size()))
+ return this;
+
+ if ((dragData.get(row).component instanceof Rocket) || (column == 4)){
+ this.setFont(boldFont);
+ } else {
+ this.setFont(normalFont);
+ }
+ return this;
+ }
+ }
+
+
+ ///////// Singleton implementation
+
+ public static void showDialog(RocketPanel rocketpanel) {
+ if (singletonDialog != null)
+ singletonDialog.dispose();
+ singletonDialog = new ComponentAnalysisDialog(rocketpanel);
+ singletonDialog.setVisible(true);
+ }
+
+ public static void hideDialog() {
+ if (singletonDialog != null)
+ singletonDialog.dispose();
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui;
+
+import java.awt.Dimension;
+import java.awt.Rectangle;
+
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.ScrollPaneConstants;
+
+import net.miginfocom.swing.MigLayout;
+
+public class DescriptionArea extends JScrollPane {
+
+ private ResizeLabel text;
+ private MigLayout layout;
+ private JPanel panel;
+
+ public DescriptionArea(int rows) {
+ this(rows, -2);
+ }
+
+ public DescriptionArea(int rows, float size) {
+ super(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
+ ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+
+ layout = new MigLayout("ins 0 2px, fill");
+ panel = new JPanel(layout);
+
+ text = new ResizeLabel(" ",size);
+ text.validate();
+ Dimension dim = text.getPreferredSize();
+ dim.height = (dim.height+2)*rows + 2;
+ this.setPreferredSize(dim);
+
+ panel.add(text, "growx");
+
+ this.setViewportView(panel);
+ this.revalidate();
+ }
+
+ public void setText(String txt) {
+ if (!txt.startsWith("<html>"))
+ txt = "<html>" + txt;
+ text.setText(txt);
+ }
+
+
+ @Override
+ public void validate() {
+
+ Rectangle dim = this.getViewportBorderBounds();
+ layout.setComponentConstraints(text, "width "+ dim.width + ", growx");
+ super.validate();
+ text.validate();
+
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui;
+
+import java.awt.Component;
+
+import javax.swing.JOptionPane;
+
+public class DetailDialog {
+
+ public static void showDetailedMessageDialog(Component parentComponent, Object message,
+ String details, String title, int messageType) {
+
+ // TODO: HIGH: Detailed dialog
+ JOptionPane.showMessageDialog(parentComponent, message, title, messageType, null);
+
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.gui;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.AbstractListModel;
+import javax.swing.ComboBoxModel;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JTabbedPane;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.GUIUtil;
+import net.sf.openrocket.util.Prefs;
+
+public class PreferencesDialog extends JDialog {
+
+ private final List<DefaultUnitSelector> unitSelectors = new ArrayList<DefaultUnitSelector>();
+
+ private PreferencesDialog() {
+ super((JFrame)null, "Preferences", true);
+
+ JPanel panel = new JPanel(new MigLayout("fill, gap unrel","[grow]","[grow][]"));
+
+ JTabbedPane tabbedPane = new JTabbedPane();
+ panel.add(tabbedPane,"grow, wrap");
+
+
+ tabbedPane.addTab("Units", null, unitsPane(), "Default units");
+ tabbedPane.addTab("Confirmation", null, confirmationPane(), "Confirmation dialog settings");
+
+
+
+ JButton close = new JButton("Close");
+ close.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent arg0) {
+ PreferencesDialog.this.setVisible(false);
+ PreferencesDialog.this.dispose();
+ }
+ });
+ panel.add(close,"span, right, tag close");
+
+ this.setContentPane(panel);
+ pack();
+ setAlwaysOnTop(true);
+ this.setLocationRelativeTo(null);
+
+ this.addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosed(WindowEvent e) {
+ Prefs.storeDefaultUnits();
+ }
+ });
+
+ GUIUtil.setDefaultButton(close);
+ GUIUtil.installEscapeCloseOperation(this);
+ }
+
+
+ private JPanel confirmationPane() {
+ JPanel panel = new JPanel(new MigLayout("fill"));
+
+ panel.add(new JLabel("Position to insert new body components:"));
+ panel.add(new JComboBox(new PrefChoiseSelector(Prefs.BODY_COMPONENT_INSERT_POSITION_KEY,
+ "Always ask", "Insert in middle", "Add to end")), "wrap para, sg combos");
+
+ panel.add(new JLabel("Confirm deletion of simulations:"));
+ panel.add(new JComboBox(new PrefBooleanSelector(Prefs.CONFIRM_DELETE_SIMULATION,
+ "Delete", "Confirm", true)), "wrap para, sg combos");
+
+ return panel;
+ }
+
+ private JPanel unitsPane() {
+ JPanel panel = new JPanel(new MigLayout("", "[][]40lp[][]"));
+ JComboBox combo;
+
+ panel.add(new JLabel("Select your preferred units:"), "span, wrap paragraph");
+
+/*
+ public static final UnitGroup UNITS_LENGTH;
+ public static final UnitGroup UNITS_MOTOR_DIMENSIONS;
+ public static final UnitGroup UNITS_DISTANCE;
+
+ public static final UnitGroup UNITS_VELOCITY;
+ public static final UnitGroup UNITS_ACCELERATION;
+ public static final UnitGroup UNITS_MASS;
+ public static final UnitGroup UNITS_FORCE;
+ public static final UnitGroup UNITS_IMPULSE;
+
+ public static final UnitGroup UNITS_STABILITY;
+ public static final UnitGroup UNITS_FLIGHT_TIME;
+ public static final UnitGroup UNITS_ROLL;
+
+ public static final UnitGroup UNITS_AREA;
+ public static final UnitGroup UNITS_DENSITY_LINE;
+ public static final UnitGroup UNITS_DENSITY_SURFACE;
+ public static final UnitGroup UNITS_DENSITY_BULK;
+ public static final UnitGroup UNITS_ROUGHNESS;
+
+ public static final UnitGroup UNITS_TEMPERATURE;
+ public static final UnitGroup UNITS_PRESSURE;
+ public static final UnitGroup UNITS_ANGLE;
+*/
+
+ panel.add(new JLabel("Rocket dimensions:"));
+ combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_LENGTH));
+ panel.add(combo, "sizegroup boxes");
+
+ panel.add(new JLabel("Line density:"));
+ combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DENSITY_LINE));
+ panel.add(combo, "sizegroup boxes, wrap");
+
+
+
+ panel.add(new JLabel("Motor dimensions:"));
+ combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_MOTOR_DIMENSIONS));
+ panel.add(combo, "sizegroup boxes");
+
+ panel.add(new JLabel("Surface density:"));
+ combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DENSITY_SURFACE));
+ panel.add(combo, "sizegroup boxes, wrap");
+
+
+
+ panel.add(new JLabel("Distance:"));
+ combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DISTANCE));
+ panel.add(combo, "sizegroup boxes");
+
+ panel.add(new JLabel("Bulk density::"));
+ combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_DENSITY_BULK));
+ panel.add(combo, "sizegroup boxes, wrap");
+
+
+
+ panel.add(new JLabel("Velocity:"));
+ combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_VELOCITY));
+ panel.add(combo, "sizegroup boxes");
+
+ panel.add(new JLabel("Surface roughness:"));
+ combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ROUGHNESS));
+ panel.add(combo, "sizegroup boxes, wrap");
+
+
+
+ panel.add(new JLabel("Acceleration:"));
+ combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ACCELERATION));
+ panel.add(combo, "sizegroup boxes");
+
+ panel.add(new JLabel("Area:"));
+ combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_AREA));
+ panel.add(combo, "sizegroup boxes, wrap");
+
+
+
+ panel.add(new JLabel("Mass:"));
+ combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_MASS));
+ panel.add(combo, "sizegroup boxes");
+
+ panel.add(new JLabel("Angle:"));
+ combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ANGLE));
+ panel.add(combo, "sizegroup boxes, wrap");
+
+
+
+ panel.add(new JLabel("Force:"));
+ combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_FORCE));
+ panel.add(combo, "sizegroup boxes");
+
+ panel.add(new JLabel("Roll rate:"));
+ combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_ROLL));
+ panel.add(combo, "sizegroup boxes, wrap");
+
+
+
+ panel.add(new JLabel("Total impulse:"));
+ combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_IMPULSE));
+ panel.add(combo, "sizegroup boxes");
+
+ panel.add(new JLabel("Temperature:"));
+ combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_TEMPERATURE));
+ panel.add(combo, "sizegroup boxes, wrap");
+
+
+
+ panel.add(new JLabel("Stability:"));
+ combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_STABILITY));
+ panel.add(combo, "sizegroup boxes");
+
+ panel.add(new JLabel("Pressure:"));
+ combo = new JComboBox(new DefaultUnitSelector(UnitGroup.UNITS_PRESSURE));
+ panel.add(combo, "sizegroup boxes, wrap para");
+
+
+
+ JButton button = new JButton("Default metric");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ UnitGroup.setDefaultMetricUnits();
+ for (DefaultUnitSelector s: unitSelectors)
+ s.fireChange();
+ }
+ });
+ panel.add(button, "spanx, split 2, grow");
+
+ button = new JButton("Default imperial");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ UnitGroup.setDefaultImperialUnits();
+ for (DefaultUnitSelector s: unitSelectors)
+ s.fireChange();
+ }
+ });
+ panel.add(button, "grow, wrap para");
+
+
+ panel.add(new ResizeLabel("The effects will take place the next time you open a window.",-2),
+ "spanx, wrap");
+
+
+ return panel;
+ }
+
+
+
+
+ private class DefaultUnitSelector extends AbstractListModel implements ComboBoxModel {
+
+ private final UnitGroup group;
+ public DefaultUnitSelector(UnitGroup group) {
+ this.group = group;
+ unitSelectors.add(this);
+ }
+
+ @Override
+ public Object getSelectedItem() {
+ return group.getDefaultUnit();
+ }
+ @Override
+ public void setSelectedItem(Object item) {
+ if (!(item instanceof Unit)) {
+ throw new IllegalArgumentException("Illegal argument "+item);
+ }
+ group.setDefaultUnit(group.getUnitIndex((Unit)item));
+ }
+ @Override
+ public Object getElementAt(int index) {
+ return group.getUnit(index);
+ }
+ @Override
+ public int getSize() {
+ return group.getUnitCount();
+ }
+
+
+ public void fireChange() {
+ this.fireContentsChanged(this, 0, this.getSize());
+ }
+ }
+
+
+
+ private class PrefChoiseSelector extends AbstractListModel implements ComboBoxModel {
+ private final String preference;
+ private final String[] descriptions;
+
+ public PrefChoiseSelector(String preference, String ... descriptions) {
+ this.preference = preference;
+ this.descriptions = descriptions;
+ }
+
+ @Override
+ public Object getSelectedItem() {
+ return descriptions[Prefs.getChoise(preference, descriptions.length, 0)];
+ }
+
+ @Override
+ public void setSelectedItem(Object item) {
+ if (!(item instanceof String)) {
+ throw new IllegalArgumentException("Illegal argument "+item);
+ }
+ int index;
+ for (index = 0; index < descriptions.length; index++) {
+ if (((String)item).equalsIgnoreCase(descriptions[index]))
+ break;
+ }
+ if (index >= descriptions.length) {
+ throw new IllegalArgumentException("Illegal argument "+item);
+ }
+
+ Prefs.putChoise(preference, index);
+ }
+
+ @Override
+ public Object getElementAt(int index) {
+ return descriptions[index];
+ }
+ @Override
+ public int getSize() {
+ return descriptions.length;
+ }
+ }
+
+
+ private class PrefBooleanSelector extends AbstractListModel implements ComboBoxModel {
+ private final String preference;
+ private final String trueDesc, falseDesc;
+ private final boolean def;
+
+ public PrefBooleanSelector(String preference, String falseDescription,
+ String trueDescription, boolean defaultState) {
+ this.preference = preference;
+ this.trueDesc = trueDescription;
+ this.falseDesc = falseDescription;
+ this.def = defaultState;
+ }
+
+ @Override
+ public Object getSelectedItem() {
+ if (Prefs.NODE.getBoolean(preference, def)) {
+ return trueDesc;
+ } else {
+ return falseDesc;
+ }
+ }
+
+ @Override
+ public void setSelectedItem(Object item) {
+ if (!(item instanceof String)) {
+ throw new IllegalArgumentException("Illegal argument "+item);
+ }
+
+ if (trueDesc.equals(item)) {
+ Prefs.NODE.putBoolean(preference, true);
+ } else if (falseDesc.equals(item)) {
+ Prefs.NODE.putBoolean(preference, false);
+ } else {
+ throw new IllegalArgumentException("Illegal argument "+item);
+ }
+ }
+
+ @Override
+ public Object getElementAt(int index) {
+ switch (index) {
+ case 0:
+ return def ? trueDesc : falseDesc;
+
+ case 1:
+ return def ? falseDesc: trueDesc;
+
+ default:
+ throw new IndexOutOfBoundsException("Boolean asked for index="+index);
+ }
+ }
+ @Override
+ public int getSize() {
+ return 2;
+ }
+ }
+
+
+
+ //////// Singleton implementation ////////
+
+ private static PreferencesDialog dialog = null;
+
+ public static void showPreferences() {
+ if (dialog != null) {
+ dialog.dispose();
+ }
+ dialog = new PreferencesDialog();
+ dialog.setVisible(true);
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.gui;
+
+/**
+ * An interface for GUI elements with a resettable model. The resetModel() method in
+ * this interface resets the model to some default model, releasing the old model
+ * listening connections.
+ *
+ * Some components that don't have a settable model simply release the current model.
+ * These components cannot therefore be reused after calling resetModel().
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface Resettable {
+ public void resetModel();
+}
--- /dev/null
+package net.sf.openrocket.gui;
+
+import java.awt.Font;
+import javax.swing.JLabel;
+
+/**
+ * A resizeable JLabel. The method resizeFont(float) changes the current font size by the
+ * given (positive or negative) amount. The change is relative to the current font size.
+ * <p>
+ * A nice small text is achievable by <code>new ResizeLabel("My text", -2);</code>
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class ResizeLabel extends JLabel {
+
+ public ResizeLabel() {
+ super();
+ }
+
+ public ResizeLabel(String text) {
+ super(text);
+ }
+
+ public ResizeLabel(float size) {
+ super();
+ resizeFont(size);
+ }
+
+ public ResizeLabel(String text, float size) {
+ super(text);
+ resizeFont(size);
+ }
+
+ public ResizeLabel(String text, int horizontalAlignment, float size) {
+ super(text, horizontalAlignment);
+ resizeFont(size);
+ }
+
+
+ public void resizeFont(float size) {
+ Font font = this.getFont();
+ font = font.deriveFont(font.getSize2D()+size);
+ this.setFont(font);
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui;
+
+import javax.swing.JSpinner;
+
+/**
+ * Editable editor for a JSpinner. Simply uses JSpinner.DefaultEditor, which has been made
+ * editable. Why the f*** isn't this possible in the normal API?
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class SpinnerEditor extends JSpinner.NumberEditor {
+//public class SpinnerEditor extends JSpinner.DefaultEditor {
+
+ public SpinnerEditor(JSpinner spinner) {
+ //super(spinner);
+ super(spinner,"0.0##");
+ //getTextField().setEditable(true);
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui;
+
+import java.awt.event.ActionEvent;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.AbstractAction;
+import javax.swing.JPanel;
+import javax.swing.JToggleButton;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.rocketcomponent.Configuration;
+
+
+public class StageSelector extends JPanel implements ChangeListener {
+
+ private final Configuration configuration;
+
+ private List<JToggleButton> buttons = new ArrayList<JToggleButton>();
+
+ public StageSelector(Configuration configuration) {
+ super(new MigLayout("gap 0!"));
+ this.configuration = configuration;
+
+ JToggleButton button = new JToggleButton(new StageAction(0));
+ this.add(button);
+ buttons.add(button);
+
+ updateButtons();
+ configuration.addChangeListener(this);
+ }
+
+ private void updateButtons() {
+ int stages = configuration.getStageCount();
+ if (buttons.size() == stages)
+ return;
+
+ while (buttons.size() > stages) {
+ JToggleButton button = buttons.remove(buttons.size()-1);
+ this.remove(button);
+ }
+
+ while (buttons.size() < stages) {
+ JToggleButton button = new JToggleButton(new StageAction(buttons.size()));
+ this.add(button);
+ buttons.add(button);
+ }
+
+ this.revalidate();
+ }
+
+
+
+
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ updateButtons();
+ }
+
+
+ private class StageAction extends AbstractAction implements ChangeListener {
+ private final int stage;
+
+ public StageAction(final int stage) {
+ this.stage = stage;
+ configuration.addChangeListener(this);
+ stateChanged(null);
+ }
+
+ @Override
+ public Object getValue(String key) {
+ if (key.equals(NAME)) {
+ return "Stage "+(stage+1);
+ }
+ return super.getValue(key);
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ configuration.setToStage(stage);
+
+// boolean state = (Boolean)getValue(SELECTED_KEY);
+// if (state == true) {
+// // Was disabled, now enabled
+// configuration.setToStage(stage);
+// } else {
+// // Was enabled, check what to do
+// if (configuration.isStageActive(stage + 1)) {
+// configuration.setToStage(stage);
+// } else {
+// if (stage == 0)
+// configuration.setAllStages();
+// else
+// configuration.setToStage(stage-1);
+// }
+// }
+// stateChanged(null);
+ }
+
+
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ this.putValue(SELECTED_KEY, configuration.isStageActive(stage));
+ }
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui;
+
+import javax.swing.BorderFactory;
+import javax.swing.ButtonGroup;
+import javax.swing.JCheckBox;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JRadioButton;
+import javax.swing.JSpinner;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.document.StorageOptions;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.simulation.FlightDataBranch;
+
+public class StorageOptionChooser extends JPanel {
+
+ public static final double DEFAULT_SAVE_TIME_SKIP = 0.20;
+
+ private JRadioButton allButton;
+ private JRadioButton someButton;
+ private JRadioButton noneButton;
+
+ private JSpinner timeSpinner;
+
+ private JCheckBox compressButton;
+
+
+ private boolean artificialEvent = false;
+
+ public StorageOptionChooser(StorageOptions opts) {
+ super(new MigLayout());
+
+ ButtonGroup buttonGroup = new ButtonGroup();
+ String tip;
+
+ this.add(new JLabel("Simulated data to store:"), "spanx, wrap unrel");
+
+ allButton = new JRadioButton("All simulated data");
+ allButton.setToolTipText("<html>Store all simulated data.<br>" +
+ "This can result in very large files!");
+ buttonGroup.add(allButton);
+ this.add(allButton, "spanx, wrap rel");
+
+
+ someButton = new JRadioButton("Every");
+ tip = "<html>Store plottable values approximately this far apart.<br>" +
+ "Larger values result in smaller files.";
+ someButton.setToolTipText(tip);
+ buttonGroup.add(someButton);
+ this.add(someButton, "");
+
+ timeSpinner = new JSpinner(new SpinnerNumberModel(0.0, 0.0, 5.0, 0.1));
+ timeSpinner.setToolTipText(tip);
+ timeSpinner.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ if (artificialEvent)
+ return;
+ someButton.setSelected(true);
+ }
+ });
+ this.add(timeSpinner, "wmin 55lp");
+
+ JLabel label = new JLabel("seconds");
+ label.setToolTipText(tip);
+ this.add(label, "wrap rel");
+
+
+ noneButton = new JRadioButton("Only primary figures");
+ noneButton.setToolTipText("<html>Store only the values shown in the summary table.<br>" +
+ "This results in the smallest files.");
+ buttonGroup.add(noneButton);
+
+ this.add(noneButton, "spanx, wrap 20lp");
+
+
+ compressButton = new JCheckBox("Compress file");
+ compressButton.setToolTipText("Using compression reduces the file size significantly.");
+ this.add(compressButton, "spanx");
+
+
+ this.setBorder(BorderFactory.createCompoundBorder(
+ BorderFactory.createEmptyBorder(0, 10, 0, 0),
+ BorderFactory.createTitledBorder("Save options")));
+
+ loadOptions(opts);
+ }
+
+
+ public void loadOptions(StorageOptions opts) {
+ double t;
+
+ // Data storage radio button
+ t = opts.getSimulationTimeSkip();
+ if (t == StorageOptions.SIMULATION_DATA_ALL) {
+ allButton.setSelected(true);
+ t = DEFAULT_SAVE_TIME_SKIP;
+ } else if (t == StorageOptions.SIMULATION_DATA_NONE) {
+ noneButton.setSelected(true);
+ t = DEFAULT_SAVE_TIME_SKIP;
+ } else {
+ someButton.setSelected(true);
+ }
+
+ // Time skip spinner
+ artificialEvent = true;
+ timeSpinner.setValue(t);
+ artificialEvent = false;
+
+ // Compression checkbox
+ compressButton.setSelected(opts.isCompressionEnabled());
+ }
+
+
+ public void storeOptions(StorageOptions opts) {
+ double t;
+
+ if (allButton.isSelected()) {
+ t = StorageOptions.SIMULATION_DATA_ALL;
+ } else if (noneButton.isSelected()) {
+ t = StorageOptions.SIMULATION_DATA_NONE;
+ } else {
+ t = (Double)timeSpinner.getValue();
+ }
+
+ opts.setSimulationTimeSkip(t);
+
+ opts.setCompressionEnabled(compressButton.isSelected());
+
+ opts.setExplicitlySet(true);
+ }
+
+
+
+ /**
+ * Asks the user the storage options using a modal dialog window if the document
+ * contains simulated data and the user has not explicitly set how to store the data.
+ *
+ * @param document the document to check.
+ * @param parent the parent frame for the dialog.
+ * @return <code>true</code> to continue, <code>false</code> if the user cancelled.
+ */
+ public static boolean verifyStorageOptions(OpenRocketDocument document, JFrame parent) {
+ StorageOptions options = document.getDefaultStorageOptions();
+
+ if (options.isExplicitlySet()) {
+ // User has explicitly set the values, save as is
+ return true;
+ }
+
+
+ boolean hasData = false;
+
+ simulationLoop:
+ for (Simulation s: document.getSimulations()) {
+ if (s.getStatus() == Simulation.Status.NOT_SIMULATED ||
+ s.getStatus() == Simulation.Status.EXTERNAL)
+ continue;
+
+ FlightData data = s.getSimulatedData();
+ if (data == null)
+ continue;
+
+ for (int i=0; i < data.getBranchCount(); i++) {
+ FlightDataBranch branch = data.getBranch(i);
+ if (branch == null)
+ continue;
+ if (branch.getLength() > 0) {
+ hasData = true;
+ break simulationLoop;
+ }
+ }
+ }
+
+
+ if (!hasData) {
+ // No data to store, do not ask only about compression
+ return true;
+ }
+
+
+ StorageOptionChooser chooser = new StorageOptionChooser(options);
+
+ if (JOptionPane.showConfirmDialog(parent, chooser, "Save options",
+ JOptionPane.OK_CANCEL_OPTION, JOptionPane.QUESTION_MESSAGE) !=
+ JOptionPane.OK_OPTION) {
+ // User cancelled
+ return false;
+ }
+
+ chooser.storeOptions(options);
+ return true;
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.gui;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+
+import javax.swing.JTextField;
+
+public abstract class TextFieldListener implements ActionListener, FocusListener {
+ private JTextField field;
+
+ public void listenTo(JTextField newField) {
+ if (field != null) {
+ field.removeActionListener(this);
+ field.removeFocusListener(this);
+ }
+ field = newField;
+ if (field != null) {
+ field.addActionListener(this);
+ field.addFocusListener(this);
+ }
+ }
+
+ public abstract void setText(String text);
+
+ public void actionPerformed(ActionEvent e) {
+ setText(field.getText());
+ }
+ public void focusGained(FocusEvent e) { }
+ public void focusLost(FocusEvent e) {
+ setText(field.getText());
+ }
+
+}
\ No newline at end of file
--- /dev/null
+package net.sf.openrocket.gui;
+
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.ItemSelectable;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.Action;
+import javax.swing.JMenuItem;
+import javax.swing.JPopupMenu;
+import javax.swing.border.Border;
+import javax.swing.border.CompoundBorder;
+import javax.swing.border.EmptyBorder;
+import javax.swing.border.LineBorder;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+
+
+/**
+ * A Swing component that allows one to choose a unit from a UnitGroup within
+ * a DoubleModel model. The current unit of the model is shown as a JLabel, and
+ * the unit can be changed by clicking on the label.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class UnitSelector extends ResizeLabel implements ChangeListener, MouseListener,
+ ItemSelectable {
+
+ private final DoubleModel model;
+ private final Action[] extraActions;
+
+ private UnitGroup unitGroup;
+ private Unit currentUnit;
+
+ private final boolean showValue;
+
+ private final Border normalBorder;
+ private final Border withinBorder;
+
+
+ private final List<ItemListener> itemListeners = new ArrayList<ItemListener>();
+
+
+ /**
+ * Common private constructor that sets the values and sets up the borders.
+ * Either model or group must be null.
+ *
+ * @param model
+ * @param showValue
+ * @param group
+ * @param actions
+ */
+ private UnitSelector(DoubleModel model, boolean showValue, UnitGroup group,
+ Action[] actions) {
+ super();
+
+ this.model = model;
+ this.showValue = showValue;
+
+ if (model != null) {
+ this.unitGroup = model.getUnitGroup();
+ this.currentUnit = model.getCurrentUnit();
+ } else {
+ this.unitGroup = group;
+ this.currentUnit = group.getDefaultUnit();
+ }
+
+ this.extraActions = actions;
+
+ addMouseListener(this);
+
+ // Define borders to use:
+
+ normalBorder = new CompoundBorder(
+ new LineBorder(new Color(0f, 0f, 0f, 0.08f), 1), new EmptyBorder(1, 1, 1,
+ 1));
+ withinBorder = new CompoundBorder(new LineBorder(new Color(0f, 0f, 0f, 0.6f)),
+ new EmptyBorder(1, 1, 1, 1));
+
+ setBorder(normalBorder);
+ updateText();
+ }
+
+
+
+ public UnitSelector(DoubleModel model, Action... actions) {
+ this(model, false, actions);
+ }
+
+ public UnitSelector(DoubleModel model, boolean showValue, Action... actions) {
+ this(model, showValue, null, actions);
+
+ // Add model listener
+ this.model.addChangeListener(this);
+ }
+
+
+ public UnitSelector(UnitGroup group, Action... actions) {
+ this(null, false, group, actions);
+ }
+
+
+
+
+ /**
+ * Return the DoubleModel that is backing this selector up, or <code>null</code>.
+ * Either this method or {@link #getUnitGroup()} always returns <code>null</code>.
+ *
+ * @return the DoubleModel being used, or <code>null</code>.
+ */
+ public DoubleModel getModel() {
+ return model;
+ }
+
+
+ /**
+ * Return the unit group that is being shown, or <code>null</code>. Either this method
+ * or {@link #getModel()} always returns <code>null</code>.
+ *
+ * @return the UnitGroup being used, or <code>null</code>.
+ */
+ public UnitGroup getUnitGroup() {
+ return unitGroup;
+ }
+
+
+ public void setUnitGroup(UnitGroup group) {
+ if (model != null) {
+ throw new IllegalStateException(
+ "UnitGroup cannot be set when backed up with model.");
+ }
+
+ if (this.unitGroup == group)
+ return;
+
+ this.unitGroup = group;
+ this.currentUnit = group.getDefaultUnit();
+ updateText();
+ }
+
+
+ /**
+ * Return the currently selected unit. Works both when backup up with a DoubleModel
+ * and UnitGroup.
+ *
+ * @return the currently selected unit.
+ */
+ public Unit getSelectedUnit() {
+ return currentUnit;
+ }
+
+
+ /**
+ * Set the currently selected unit. Sets it to the DoubleModel if it is backed up
+ * by it.
+ *
+ * @param unit the unit to select.
+ */
+ public void setSelectedUnit(Unit unit) {
+ if (!unitGroup.contains(unit)) {
+ throw new IllegalArgumentException("unit " + unit
+ + " not contained in group " + unitGroup);
+ }
+
+ this.currentUnit = unit;
+ if (model != null) {
+ model.setCurrentUnit(unit);
+ }
+ updateText();
+ fireItemEvent();
+ }
+
+
+
+ /**
+ * Updates the text of the label
+ */
+ private void updateText() {
+ if (model != null) {
+
+ Unit unit = model.getCurrentUnit();
+ if (showValue) {
+ setText(unit.toStringUnit(model.getValue()));
+ } else {
+ setText(unit.getUnit());
+ }
+
+ } else if (unitGroup != null) {
+
+ setText(currentUnit.getUnit());
+
+ } else {
+ throw new IllegalStateException("Both model and unitGroup are null.");
+ }
+ }
+
+
+ /**
+ * Update the component when the DoubleModel changes.
+ */
+ public void stateChanged(ChangeEvent e) {
+ updateText();
+ }
+
+
+
+ //////// ItemListener handling ////////
+
+ public void addItemListener(ItemListener listener) {
+ itemListeners.add(listener);
+ }
+
+ public void removeItemListener(ItemListener listener) {
+ itemListeners.remove(listener);
+ }
+
+ protected void fireItemEvent() {
+ ItemEvent event = null;
+ ItemListener[] listeners = itemListeners.toArray(new ItemListener[0]);
+ for (ItemListener l: listeners) {
+ if (event == null) {
+ event = new ItemEvent(this, ItemEvent.ITEM_STATE_CHANGED, getSelectedUnit(),
+ ItemEvent.SELECTED);
+ }
+ l.itemStateChanged(event);
+ }
+ }
+
+
+
+ //////// Popup ////////
+
+ private void popup() {
+ JPopupMenu popup = new JPopupMenu();
+
+ for (int i = 0; i < unitGroup.getUnitCount(); i++) {
+ Unit unit = unitGroup.getUnit(i);
+ JMenuItem item = new JMenuItem(unit.getUnit());
+ item.addActionListener(new UnitSelectorItem(unit));
+ popup.add(item);
+ }
+
+ for (int i = 0; i < extraActions.length; i++) {
+ if (extraActions[i] == null && i < extraActions.length - 1) {
+ popup.addSeparator();
+ } else {
+ popup.add(new JMenuItem(extraActions[i]));
+ }
+ }
+
+ Dimension d = getSize();
+ popup.show(this, 0, d.height);
+ }
+
+
+ /**
+ * ActionListener class that sets the currently selected unit.
+ */
+ private class UnitSelectorItem implements ActionListener {
+ private final Unit unit;
+
+ public UnitSelectorItem(Unit u) {
+ unit = u;
+ }
+
+ public void actionPerformed(ActionEvent e) {
+ setSelectedUnit(unit);
+ }
+ }
+
+
+ @Override
+ public Object[] getSelectedObjects() {
+ return new Object[]{ getSelectedUnit() };
+ }
+
+
+
+ //////// Mouse handling ////////
+
+ public void mouseClicked(MouseEvent e) {
+ if (unitGroup.getUnitCount() > 1)
+ popup();
+ }
+
+ public void mouseEntered(MouseEvent e) {
+ if (unitGroup.getUnitCount() > 1)
+ setBorder(withinBorder);
+ }
+
+ public void mouseExited(MouseEvent e) {
+ setBorder(normalBorder);
+ }
+
+ public void mousePressed(MouseEvent e) {
+ } // Ignore
+
+ public void mouseReleased(MouseEvent e) {
+ } // Ignore
+
+}
--- /dev/null
+package net.sf.openrocket.gui.adaptors;
+
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.AbstractAction;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.util.ChangeSource;
+
+
+/**
+ * A class that adapts an isXXX/setXXX boolean variable. It functions as an Action suitable
+ * for usage in JCheckBox or JToggleButton. You can create a suitable button with
+ * <code>
+ * check = new JCheckBox(new BooleanModel(component,"Value"))
+ * check.setText("Label");
+ * </code>
+ * This will produce a button that uses isValue() and setValue(boolean) of the corresponding
+ * component.
+ * <p>
+ * Additionally a number of component enabled states may be controlled by this class using
+ * the method {@link #addEnableComponent(Component, boolean)}.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class BooleanModel extends AbstractAction implements ChangeListener {
+
+ private final ChangeSource source;
+ private final String valueName;
+
+ private final Method getMethod;
+ private final Method setMethod;
+ private final Method getEnabled;
+
+ private final List<Component> components = new ArrayList<Component>();
+ private final List<Boolean> componentEnableState = new ArrayList<Boolean>();
+
+ private int firing = 0;
+
+ private boolean oldValue;
+ private boolean oldEnabled;
+
+ public BooleanModel(ChangeSource source, String valueName) {
+ this.source = source;
+ this.valueName = valueName;
+
+ Method getter=null, setter=null;
+
+
+ // Try get/is and set
+ try {
+ getter = source.getClass().getMethod("is" + valueName);
+ } catch (NoSuchMethodException ignore) { }
+ if (getter == null) {
+ try {
+ getter = source.getClass().getMethod("get" + valueName);
+ } catch (NoSuchMethodException ignore) { }
+ }
+ try {
+ setter = source.getClass().getMethod("set" + valueName,boolean.class);
+ } catch (NoSuchMethodException ignore) { }
+
+ if (getter==null || setter==null) {
+ throw new IllegalArgumentException("get/is methods for boolean '"+valueName+
+ "' not present in class "+source.getClass().getCanonicalName());
+ }
+
+ getMethod = getter;
+ setMethod = setter;
+
+ Method e = null;
+ try {
+ e = source.getClass().getMethod("is" + valueName + "Enabled");
+ } catch (NoSuchMethodException ignore) { }
+ getEnabled = e;
+
+ oldValue = getValue();
+ oldEnabled = getIsEnabled();
+
+ this.setEnabled(oldEnabled);
+ this.putValue(SELECTED_KEY, oldValue);
+
+ source.addChangeListener(this);
+ }
+
+ public boolean getValue() {
+ try {
+ return (Boolean)getMethod.invoke(source);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("getMethod execution error for source "+source,e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException("getMethod execution error for source "+source,e);
+ }
+ }
+
+ public void setValue(boolean b) {
+ try {
+ setMethod.invoke(source, new Object[] { (Boolean)b });
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("setMethod execution error for source "+source,e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException("setMethod execution error for source "+source,e);
+ }
+ }
+
+
+ /**
+ * Add a component the enabled status of which will be controlled by the value
+ * of this boolean. The <code>component</code> will be enabled exactly when
+ * the state of this model is equal to that of <code>enableState</code>.
+ *
+ * @param component the component to control.
+ * @param enableState the state in which the component should be enabled.
+ */
+ public void addEnableComponent(Component component, boolean enableState) {
+ components.add(component);
+ componentEnableState.add(enableState);
+ updateEnableStatus();
+ }
+
+ /**
+ * Add a component which will be enabled when this boolean is <code>true</code>.
+ * This is equivalent to <code>booleanModel.addEnableComponent(component, true)</code>.
+ *
+ * @param component the component to control.
+ * @see #addEnableComponent(Component, boolean)
+ */
+ public void addEnableComponent(Component component) {
+ addEnableComponent(component, true);
+ }
+
+ private void updateEnableStatus() {
+ boolean state = getValue();
+
+ for (int i=0; i < components.size(); i++) {
+ Component c = components.get(i);
+ boolean b = componentEnableState.get(i);
+ c.setEnabled(state == b);
+ }
+ }
+
+
+// @Override
+// public boolean isEnabled() {
+// if (getEnabled == null)
+// return true;
+// try {
+// return (Boolean)getEnabled.invoke(source);
+// } catch (IllegalAccessException e) {
+// throw new RuntimeException("getEnabled execution error for source "+source,e);
+// } catch (InvocationTargetException e) {
+// throw new RuntimeException("getEnabled execution error for source "+source,e);
+// }
+// }
+
+
+ private boolean getIsEnabled() {
+ if (getEnabled == null)
+ return true;
+ try {
+ return (Boolean)getEnabled.invoke(source);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("getEnabled execution error for source "+source,e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException("getEnabled execution error for source "+source,e);
+ }
+ }
+
+// @Override
+// public Object getValue(String key) {
+// if (key.equals(SELECTED_KEY)) {
+// return getValue();
+// }
+// return super.getValue(key);
+// }
+//
+// @Override
+// public void putValue(String key, Object value) {
+// if (firing > 0) // Ignore if currently firing event
+// return;
+// if (key.equals(SELECTED_KEY) && (value instanceof Boolean)) {
+// setValue((Boolean)value);
+// } else {
+// super.putValue(key, value);
+// }
+// updateEnableStatus();
+// }
+
+
+ @Override
+ public void stateChanged(ChangeEvent event) {
+ if (firing > 0)
+ return;
+
+ boolean v = getValue();
+ boolean e = getIsEnabled();
+ if (oldValue != v) {
+ oldValue = v;
+ firing++;
+ this.putValue(SELECTED_KEY, getValue());
+// this.firePropertyChange(SELECTED_KEY, !v, v);
+ updateEnableStatus();
+ firing--;
+ }
+ if (oldEnabled != e) {
+ oldEnabled = e;
+ setEnabled(e);
+ }
+ }
+
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ if (firing > 0)
+ return;
+
+ boolean v = (Boolean)this.getValue(SELECTED_KEY);
+ if (v != oldValue) {
+ firing++;
+ setValue(v);
+ oldValue = getValue();
+ // Update all states
+ this.putValue(SELECTED_KEY, oldValue);
+ this.setEnabled(getIsEnabled());
+ updateEnableStatus();
+ firing--;
+ }
+ }
+
+ @Override
+ public String toString() {
+ return "BooleanModel["+source.getClass().getCanonicalName()+":"+valueName+"]";
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.adaptors;
+
+import javax.swing.table.TableColumnModel;
+
+public abstract class Column {
+ private final String name;
+
+ /**
+ * Create a new column with specified name. Additionally, the {@link #getValueAt(int)}
+ * method must be implemented.
+ *
+ * @param name the caption of the column.
+ */
+ public Column(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Return the caption of the column.
+ */
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ /**
+ * Return the default width of the column. This is used by the method
+ * {@link #ColumnTableModel.setColumnWidth(TableColumnModel)}. The default width is
+ * 100, the method may be overridden to return other values relative to this value.
+ *
+ * @return the relative width of the column (default 100).
+ */
+ public int getDefaultWidth() {
+ return 100;
+ }
+
+
+ /**
+ * Returns the exact width of this column. If the return value is positive,
+ * both the minimum and maximum widths of this column are set to this value
+ *
+ * @return the absolute exact width of the column (default 0).
+ */
+ public int getExactWidth() {
+ return 0;
+ }
+
+
+ public Class<?> getColumnClass() {
+ return Object.class;
+ }
+
+ /**
+ * Return the value in this column at the specified row.
+ *
+ * @param row the row of the data.
+ * @return the value at the specified position.
+ */
+ public abstract Object getValueAt(int row);
+}
--- /dev/null
+package net.sf.openrocket.gui.adaptors;
+
+import javax.swing.table.AbstractTableModel;
+import javax.swing.table.TableColumn;
+import javax.swing.table.TableColumnModel;
+
+public abstract class ColumnTableModel extends AbstractTableModel {
+ private final Column[] columns;
+
+ public ColumnTableModel(Column... columns) {
+ this.columns = columns;
+ }
+
+ public void setColumnWidths(TableColumnModel model) {
+ for (int i=0; i < columns.length; i++) {
+ if (columns[i].getExactWidth() > 0) {
+ TableColumn col = model.getColumn(i);
+ int w = columns[i].getExactWidth();
+ col.setResizable(false);
+ col.setMinWidth(w);
+ col.setMaxWidth(w);
+ col.setPreferredWidth(w);
+ } else {
+ model.getColumn(i).setPreferredWidth(columns[i].getDefaultWidth());
+ }
+ }
+ }
+
+ @Override
+ public int getColumnCount() {
+ return columns.length;
+ }
+
+ @Override
+ public String getColumnName(int col) {
+ return columns[col].toString();
+ }
+
+ @Override
+ public Class<?> getColumnClass(int col) {
+ return columns[col].getColumnClass();
+ }
+
+ @Override
+ public Object getValueAt(int row, int col) {
+ if ((row < 0) || (row >= getRowCount()) ||
+ (col < 0) || (col >= columns.length)) {
+ System.err.println("Error: Requested illegal column/row = "+col+"/"+row+".");
+ assert(false);
+ return null;
+ }
+ return columns[col].getValueAt(row);
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.adaptors;
+
+import java.awt.event.ActionEvent;
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.BoundedRangeModel;
+import javax.swing.SpinnerModel;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.ChangeSource;
+import net.sf.openrocket.util.MathUtil;
+
+
+/**
+ * A model connector that can read and modify any value of any ChangeSource that
+ * has the appropriate get/set methods defined.
+ *
+ * The variable is defined in the constructor by providing the variable name as a string
+ * (e.g. "Radius" -> getRadius()/setRadius()). Additional scaling may be applied, e.g. a
+ * DoubleModel for the diameter can be defined by the variable "Radius" and a multiplier of 2.
+ *
+ * Sub-models suitable for JSpinners and other components are available from the appropriate
+ * methods.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class DoubleModel implements ChangeListener, ChangeSource {
+ private static final boolean DEBUG_LISTENERS = false;
+
+ //////////// JSpinner Model ////////////
+
+ /**
+ * Model suitable for JSpinner using JSpinner.NumberEditor. It extends SpinnerNumberModel
+ * to be compatible with the NumberEditor, but only has the necessary methods defined.
+ */
+ private class ValueSpinnerModel extends SpinnerNumberModel {
+
+ @Override
+ public Object getValue() {
+ return currentUnit.toUnit(DoubleModel.this.getValue());
+// return makeString(currentUnit.toUnit(DoubleModel.this.getValue()));
+ }
+
+ @Override
+ public void setValue(Object value) {
+
+ System.out.println("setValue("+value+") called, valueName="+valueName+
+ " firing="+firing);
+
+ if (firing > 0) // Ignore, if called when model is sending events
+ return;
+ Number num = (Number)value;
+ double newValue = num.doubleValue();
+ DoubleModel.this.setValue(currentUnit.fromUnit(newValue));
+
+
+// try {
+// double newValue = Double.parseDouble((String)value);
+// DoubleModel.this.setValue(currentUnit.fromUnit(newValue));
+// } catch (NumberFormatException e) {
+// DoubleModel.this.fireStateChanged();
+// };
+ }
+
+ @Override
+ public Object getNextValue() {
+ double d = currentUnit.toUnit(DoubleModel.this.getValue());
+ double max = currentUnit.toUnit(maxValue);
+ if (MathUtil.equals(d,max))
+ return null;
+ d = currentUnit.getNextValue(d);
+ if (d > max)
+ d = max;
+ return d;
+// return makeString(d);
+ }
+
+ @Override
+ public Object getPreviousValue() {
+ double d = currentUnit.toUnit(DoubleModel.this.getValue());
+ double min = currentUnit.toUnit(minValue);
+ if (MathUtil.equals(d,min))
+ return null;
+ d = currentUnit.getPreviousValue(d);
+ if (d < min)
+ d = min;
+ return d;
+// return makeString(d);
+ }
+
+
+ @Override
+ public Comparable<Double> getMinimum() {
+ return currentUnit.toUnit(minValue);
+ }
+
+ @Override
+ public Comparable<Double> getMaximum() {
+ return currentUnit.toUnit(maxValue);
+ }
+
+
+ @Override
+ public void addChangeListener(ChangeListener l) {
+ DoubleModel.this.addChangeListener(l);
+ }
+
+ @Override
+ public void removeChangeListener(ChangeListener l) {
+ DoubleModel.this.removeChangeListener(l);
+ }
+ }
+
+ /**
+ * Returns a new SpinnerModel with the same base as the DoubleModel.
+ * The values given to the JSpinner are in the currently selected units.
+ *
+ * @return A compatibility layer for a SpinnerModel.
+ */
+ public SpinnerModel getSpinnerModel() {
+ return new ValueSpinnerModel();
+ }
+
+
+
+
+
+ //////////// JSlider model ////////////
+
+ private class ValueSliderModel implements BoundedRangeModel, ChangeListener {
+ private static final int MAX = 1000;
+
+ /*
+ * Use linear scale value = linear1 * x + linear0 when x < linearPosition
+ * Use quadratic scale value = quad2 * x^2 + quad1 * x + quad0 otherwise
+ */
+
+ // Linear in range x <= linearPosition
+ private final double linearPosition;
+
+ // May be changing DoubleModels when using linear model
+ private final DoubleModel min, mid, max;
+
+ // Linear multiplier and constant
+ //private final double linear1;
+ //private final double linear0;
+
+ // Non-linear multiplier, exponent and constant
+ private final double quad2,quad1,quad0;
+
+
+
+ public ValueSliderModel(DoubleModel min, DoubleModel max) {
+ linearPosition = 1.0;
+
+ this.min = min;
+ this.mid = max; // Never use exponential scale
+ this.max = max;
+
+ min.addChangeListener(this);
+ max.addChangeListener(this);
+
+ quad2 = quad1 = quad0 = 0; // Not used
+ }
+
+
+
+ /**
+ * Generate a linear model from min to max.
+ */
+ public ValueSliderModel(double min, double max) {
+ linearPosition = 1.0;
+
+ this.min = new DoubleModel(min);
+ this.mid = new DoubleModel(max); // Never use exponential scale
+ this.max = new DoubleModel(max);
+
+ quad2 = quad1 = quad0 = 0; // Not used
+ }
+
+ public ValueSliderModel(double min, double mid, double max) {
+ this(min,0.5,mid,max);
+ }
+
+ /*
+ * v(x) = mul * x^exp + add
+ *
+ * v(pos) = mul * pos^exp + add = mid
+ * v(1) = mul + add = max
+ * v'(pos) = mul*exp * pos^(exp-1) = linearMul
+ */
+ public ValueSliderModel(double min, double pos, double mid, double max) {
+ this.min = new DoubleModel(min);
+ this.mid = new DoubleModel(mid);
+ this.max = new DoubleModel(max);
+
+
+ linearPosition = pos;
+ //linear0 = min;
+ //linear1 = (mid-min)/pos;
+
+ if (!(min < mid && mid <= max && 0 < pos && pos < 1)) {
+ throw new IllegalArgumentException("Bad arguments for ValueSliderModel "+
+ "min="+min+" mid="+mid+" max="+max+" pos="+pos);
+ }
+
+ /*
+ * quad2..0 are calculated such that
+ * f(pos) = mid - continuity
+ * f(1) = max - end point
+ * f'(pos) = linear1 - continuity of derivative
+ */
+
+ double delta = (mid-min)/pos;
+ quad2 = (max - mid - delta + delta*pos) / pow2(pos-1);
+ quad1 = (delta + 2*(mid-max)*pos - delta*pos*pos) / pow2(pos-1);
+ quad0 = (mid - (2*mid+delta)*pos + (max+delta)*pos*pos) / pow2(pos-1);
+
+ }
+
+ private double pow2(double x) {
+ return x*x;
+ }
+
+ public int getValue() {
+ double value = DoubleModel.this.getValue();
+ if (value <= min.getValue())
+ return 0;
+ if (value >= max.getValue())
+ return MAX;
+
+ double x;
+ if (value <= mid.getValue()) {
+ // Use linear scale
+ //linear0 = min;
+ //linear1 = (mid-min)/pos;
+
+ x = (value - min.getValue())*linearPosition/(mid.getValue()-min.getValue());
+ } else {
+ // Use quadratic scale
+ // Further solution of the quadratic equation
+ // a*x^2 + b*x + c-value == 0
+ x = (Math.sqrt(quad1*quad1 - 4*quad2*(quad0-value)) - quad1) / (2*quad2);
+ }
+ return (int)(x*MAX);
+ }
+
+
+ public void setValue(int newValue) {
+ if (firing > 0) // Ignore loops
+ return;
+
+ double x = (double)newValue/MAX;
+ double value;
+
+ if (x <= linearPosition) {
+ // Use linear scale
+ //linear0 = min;
+ //linear1 = (mid-min)/pos;
+
+ value = (mid.getValue()-min.getValue())/linearPosition*x + min.getValue();
+ } else {
+ // Use quadratic scale
+ value = quad2*x*x + quad1*x + quad0;
+ }
+
+ DoubleModel.this.setValue(currentUnit.fromUnit(
+ currentUnit.round(currentUnit.toUnit(value))));
+ }
+
+
+ // Static get-methods
+ private boolean isAdjusting;
+ public int getExtent() { return 0; }
+ public int getMaximum() { return MAX; }
+ public int getMinimum() { return 0; }
+ public boolean getValueIsAdjusting() { return isAdjusting; }
+
+ // Ignore set-values
+ public void setExtent(int newExtent) { }
+ public void setMaximum(int newMaximum) { }
+ public void setMinimum(int newMinimum) { }
+ public void setValueIsAdjusting(boolean b) { isAdjusting = b; }
+
+ public void setRangeProperties(int value, int extent, int min, int max, boolean adjusting) {
+ setValueIsAdjusting(adjusting);
+ setValue(value);
+ }
+
+ // Pass change listeners to the underlying model
+ public void addChangeListener(ChangeListener l) {
+ DoubleModel.this.addChangeListener(l);
+ }
+
+ public void removeChangeListener(ChangeListener l) {
+ DoubleModel.this.removeChangeListener(l);
+ }
+
+
+
+ public void stateChanged(ChangeEvent e) {
+ // Min or max range has changed.
+ // Fire if not already firing
+ if (firing == 0)
+ fireStateChanged();
+ }
+ }
+
+
+ public BoundedRangeModel getSliderModel(DoubleModel min, DoubleModel max) {
+ return new ValueSliderModel(min,max);
+ }
+
+ public BoundedRangeModel getSliderModel(double min, double max) {
+ return new ValueSliderModel(min,max);
+ }
+
+ public BoundedRangeModel getSliderModel(double min, double mid, double max) {
+ return new ValueSliderModel(min,mid,max);
+ }
+
+ public BoundedRangeModel getSliderModel(double min, double pos, double mid, double max) {
+ return new ValueSliderModel(min,pos,mid,max);
+ }
+
+
+
+
+
+ //////////// Action model ////////////
+
+ private class AutomaticActionModel extends AbstractAction implements ChangeListener {
+ private boolean oldValue = false;
+
+ public AutomaticActionModel() {
+ oldValue = isAutomatic();
+ addChangeListener(this);
+ }
+
+
+ @Override
+ public boolean isEnabled() {
+ // TODO: LOW: does not reflect if component is currently able to support automatic setting
+ return isAutomaticAvailable();
+ }
+
+ @Override
+ public Object getValue(String key) {
+ if (key.equals(Action.SELECTED_KEY)) {
+ oldValue = isAutomatic();
+ return oldValue;
+ }
+ return super.getValue(key);
+ }
+
+ @Override
+ public void putValue(String key, Object value) {
+ if (firing > 0)
+ return;
+ if (key.equals(Action.SELECTED_KEY) && (value instanceof Boolean)) {
+ oldValue = (Boolean)value;
+ setAutomatic((Boolean)value);
+ } else {
+ super.putValue(key, value);
+ }
+ }
+
+ // Implement a wrapper to the ChangeListeners
+ ArrayList<PropertyChangeListener> listeners = new ArrayList<PropertyChangeListener>();
+ @Override
+ public void addPropertyChangeListener(PropertyChangeListener listener) {
+ listeners.add(listener);
+ DoubleModel.this.addChangeListener(this);
+ }
+ @Override
+ public void removePropertyChangeListener(PropertyChangeListener listener) {
+ listeners.remove(listener);
+ if (listeners.isEmpty())
+ DoubleModel.this.removeChangeListener(this);
+ }
+ // If the value has changed, generate an event to the listeners
+ public void stateChanged(ChangeEvent e) {
+ boolean newValue = isAutomatic();
+ if (oldValue == newValue)
+ return;
+ PropertyChangeEvent event = new PropertyChangeEvent(this,Action.SELECTED_KEY,
+ oldValue,newValue);
+ oldValue = newValue;
+ Object[] l = listeners.toArray();
+ for (int i=0; i<l.length; i++) {
+ ((PropertyChangeListener)l[i]).propertyChange(event);
+ }
+ }
+
+ public void actionPerformed(ActionEvent e) {
+ // Setting performed in putValue
+ }
+
+ }
+
+ /**
+ * Returns a new Action corresponding to the changes of the automatic setting
+ * property of the value model. This may be used directly with e.g. check buttons.
+ *
+ * @return A compatibility layer for an Action.
+ */
+ public Action getAutomaticAction() {
+ return new AutomaticActionModel();
+ }
+
+
+
+
+
+
+ //////////// Main model /////////////
+
+ /*
+ * The main model handles all values in SI units, i.e. no conversion is made within the model.
+ */
+
+ private final ChangeSource source;
+ private final String valueName;
+ private final double multiplier;
+
+ private final Method getMethod;
+ private final Method setMethod;
+
+ private final Method getAutoMethod;
+ private final Method setAutoMethod;
+
+ private final ArrayList<ChangeListener> listeners = new ArrayList<ChangeListener>();
+
+ private final UnitGroup units;
+ private Unit currentUnit;
+
+ private final double minValue;
+ private final double maxValue;
+
+
+ private int firing = 0; // >0 when model itself is sending events
+
+
+ // Used to differentiate changes in valueName and other changes in the component:
+ private double lastValue = 0;
+ private boolean lastAutomatic = false;
+
+
+ public DoubleModel(double value) {
+ this(value, UnitGroup.UNITS_NONE,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
+ }
+
+ public DoubleModel(double value, UnitGroup unit) {
+ this(value,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
+ }
+
+ public DoubleModel(double value, UnitGroup unit, double min) {
+ this(value,unit,min,Double.POSITIVE_INFINITY);
+ }
+
+ public DoubleModel(double value, UnitGroup unit, double min, double max) {
+ this.lastValue = value;
+ this.minValue = min;
+ this.maxValue = max;
+
+ source = null;
+ valueName = "Constant value";
+ multiplier = 1;
+
+ getMethod = setMethod = null;
+ getAutoMethod = setAutoMethod = null;
+ units = unit;
+ currentUnit = units.getDefaultUnit();
+ }
+
+
+ /**
+ * Generates a new DoubleModel that changes the values of the specified component.
+ * The double value is read and written using the methods "get"/"set" + valueName.
+ *
+ * @param source Component whose parameter to use.
+ * @param valueName Name of metods used to get/set the parameter.
+ * @param multiplier Value shown by the model is the value from component.getXXX * multiplier
+ * @param min Minimum value allowed (in SI units)
+ * @param max Maximum value allowed (in SI units)
+ */
+ public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
+ double min, double max) {
+ this.source = source;
+ this.valueName = valueName;
+ this.multiplier = multiplier;
+
+ this.units = unit;
+ currentUnit = units.getDefaultUnit();
+
+ this.minValue = min;
+ this.maxValue = max;
+
+ try {
+ getMethod = source.getClass().getMethod("get" + valueName);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException("get method for value '"+valueName+
+ "' not present in class "+source.getClass().getCanonicalName());
+ }
+
+ Method s=null;
+ try {
+ s = source.getClass().getMethod("set" + valueName,double.class);
+ } catch (NoSuchMethodException e1) { } // Ignore
+ setMethod = s;
+
+ // Automatic selection methods
+
+ Method set=null,get=null;
+
+ try {
+ get = source.getClass().getMethod("is" + valueName + "Automatic");
+ set = source.getClass().getMethod("set" + valueName + "Automatic",boolean.class);
+ } catch (NoSuchMethodException e) { } // ignore
+
+ if (set!=null && get!=null) {
+ getAutoMethod = get;
+ setAutoMethod = set;
+ } else {
+ getAutoMethod = null;
+ setAutoMethod = null;
+ }
+
+ }
+
+ public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit,
+ double min) {
+ this(source,valueName,multiplier,unit,min,Double.POSITIVE_INFINITY);
+ }
+
+ public DoubleModel(ChangeSource source, String valueName, double multiplier, UnitGroup unit) {
+ this(source,valueName,multiplier,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
+ }
+
+ public DoubleModel(ChangeSource source, String valueName, UnitGroup unit,
+ double min, double max) {
+ this(source,valueName,1.0,unit,min,max);
+ }
+
+ public DoubleModel(ChangeSource source, String valueName, UnitGroup unit, double min) {
+ this(source,valueName,1.0,unit,min,Double.POSITIVE_INFINITY);
+ }
+
+ public DoubleModel(ChangeSource source, String valueName, UnitGroup unit) {
+ this(source,valueName,1.0,unit,Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
+ }
+
+ public DoubleModel(ChangeSource source, String valueName) {
+ this(source,valueName,1.0,UnitGroup.UNITS_NONE,
+ Double.NEGATIVE_INFINITY,Double.POSITIVE_INFINITY);
+ }
+
+ public DoubleModel(ChangeSource source, String valueName, double min) {
+ this(source,valueName,1.0,UnitGroup.UNITS_NONE,min,Double.POSITIVE_INFINITY);
+ }
+
+ public DoubleModel(ChangeSource source, String valueName, double min, double max) {
+ this(source,valueName,1.0,UnitGroup.UNITS_NONE,min,max);
+ }
+
+
+
+ /**
+ * Returns the value of the variable (in SI units).
+ */
+ public double getValue() {
+ if (getMethod==null) // Constant value
+ return lastValue;
+
+ try {
+ return (Double)getMethod.invoke(source)*multiplier;
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ return lastValue; // Should not occur
+ }
+
+ /**
+ * Sets the value of the variable.
+ * @param v New value for parameter in SI units.
+ */
+ public void setValue(double v) {
+ if (setMethod==null) {
+ if (getMethod != null) {
+ throw new RuntimeException("setMethod not available for variable '"+valueName+
+ "' in class "+source.getClass().getCanonicalName());
+ }
+ lastValue = v;
+ fireStateChanged();
+ return;
+ }
+
+ try {
+ setMethod.invoke(source, v/multiplier);
+ return;
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ fireStateChanged(); // Should not occur
+ }
+
+
+ /**
+ * Returns whether setting the value automatically is available.
+ */
+ public boolean isAutomaticAvailable() {
+ return (getAutoMethod != null) && (setAutoMethod != null);
+ }
+
+ /**
+ * Returns whether the value is currently being set automatically.
+ * Returns false if automatic setting is not available at all.
+ */
+ public boolean isAutomatic() {
+ if (getAutoMethod == null)
+ return false;
+
+ try {
+ return (Boolean)getAutoMethod.invoke(source);
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ return false; // Should not occur
+ }
+
+ /**
+ * Sets whether the value should be set automatically. Simply fires a
+ * state change event if automatic setting is not available.
+ */
+ public void setAutomatic(boolean auto) {
+ if (setAutoMethod == null) {
+ fireStateChanged(); // in case something is out-of-sync
+ return;
+ }
+
+ try {
+ lastAutomatic = auto;
+ setAutoMethod.invoke(source, auto);
+ return;
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ fireStateChanged(); // Should not occur
+ }
+
+
+ /**
+ * Returns the current Unit. At the beginning it is the default unit of the UnitGroup.
+ * @return The most recently set unit.
+ */
+ public Unit getCurrentUnit() {
+ return currentUnit;
+ }
+
+ /**
+ * Sets the current Unit. The unit must be one of those included in the UnitGroup.
+ * @param u The unit to set active.
+ */
+ public void setCurrentUnit(Unit u) {
+ if (currentUnit == u)
+ return;
+ currentUnit = u;
+ fireStateChanged();
+ }
+
+
+ /**
+ * Returns the UnitGroup associated with the parameter value.
+ *
+ * @return The UnitGroup given to the constructor.
+ */
+ public UnitGroup getUnitGroup() {
+ return units;
+ }
+
+
+
+ /**
+ * Add a listener to the model. Adds the model as a listener to the Component if this
+ * is the first listener.
+ * @param l Listener to add.
+ */
+ public void addChangeListener(ChangeListener l) {
+ if (listeners.isEmpty()) {
+ if (source != null) {
+ source.addChangeListener(this);
+ lastValue = getValue();
+ lastAutomatic = isAutomatic();
+ }
+ }
+
+ listeners.add(l);
+ if (DEBUG_LISTENERS)
+ System.out.println(this+" adding listener (total "+listeners.size()+"): "+l);
+ }
+
+ /**
+ * Remove a listener from the model. Removes the model from being a listener to the Component
+ * if this was the last listener of the model.
+ * @param l Listener to remove.
+ */
+ public void removeChangeListener(ChangeListener l) {
+ listeners.remove(l);
+ if (listeners.isEmpty() && source != null) {
+ source.removeChangeListener(this);
+ }
+ if (DEBUG_LISTENERS)
+ System.out.println(this+" removing listener (total "+listeners.size()+"): "+l);
+ }
+
+ /**
+ * Fire a ChangeEvent to all listeners.
+ */
+ protected void fireStateChanged() {
+ Object[] l = listeners.toArray();
+ ChangeEvent event = new ChangeEvent(this);
+ firing++;
+ for (int i=0; i<l.length; i++)
+ ((ChangeListener)l[i]).stateChanged(event);
+ firing--;
+ }
+
+ /**
+ * Called when the component changes. Checks whether the modeled value has changed, and if
+ * it has, updates lastValue and generates ChangeEvents for all listeners of the model.
+ */
+ public void stateChanged(ChangeEvent e) {
+ double v = getValue();
+ boolean b = isAutomatic();
+ if (lastValue == v && lastAutomatic == b)
+ return;
+ lastValue = v;
+ lastAutomatic = b;
+ fireStateChanged();
+ }
+
+ /**
+ * Explain the DoubleModel as a String.
+ */
+ @Override
+ public String toString() {
+ if (source == null)
+ return "DoubleModel[constant="+lastValue+"]";
+ return "DoubleModel["+source.getClass().getCanonicalName()+":"+valueName+"]";
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.adaptors;
+
+import javax.swing.AbstractListModel;
+import javax.swing.ComboBoxModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.util.ChangeSource;
+import net.sf.openrocket.util.Reflection;
+
+
+public class EnumModel<T extends Enum<T>> extends AbstractListModel
+ implements ComboBoxModel, ChangeListener {
+
+ private final ChangeSource source;
+ private final String valueName;
+ private final String nullText;
+
+ private final Enum<T>[] values;
+ private Enum<T> currentValue = null;
+
+ private final Reflection.Method getMethod;
+ private final Reflection.Method setMethod;
+
+
+
+ public EnumModel(ChangeSource source, String valueName) {
+ this(source,valueName,null,null);
+ }
+
+ public EnumModel(ChangeSource source, String valueName, Enum<T>[] values) {
+ this(source, valueName, values, null);
+ }
+
+ @SuppressWarnings("unchecked")
+ public EnumModel(ChangeSource source, String valueName, Enum<T>[] values, String nullText) {
+ Class<? extends Enum<T>> enumClass;
+ this.source = source;
+ this.valueName = valueName;
+
+ try {
+ java.lang.reflect.Method getM = source.getClass().getMethod("get" + valueName);
+ enumClass = (Class<? extends Enum<T>>) getM.getReturnType();
+ if (!enumClass.isEnum()) {
+ throw new IllegalArgumentException("Return type of get" + valueName +
+ " not an enum type");
+ }
+
+ getMethod = new Reflection.Method(getM);
+ setMethod = new Reflection.Method(source.getClass().getMethod("set" + valueName,
+ enumClass));
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException("get/is methods for enum '"+valueName+
+ "' not present in class "+source.getClass().getCanonicalName());
+ }
+
+ if (values != null)
+ this.values = values;
+ else
+ this.values = enumClass.getEnumConstants();
+
+ this.nullText = nullText;
+
+ stateChanged(null); // Update current value
+ source.addChangeListener(this);
+ }
+
+
+
+ @Override
+ public Object getSelectedItem() {
+ if (currentValue==null)
+ return nullText;
+ return currentValue;
+ }
+
+ @Override
+ public void setSelectedItem(Object item) {
+ if (item instanceof String) {
+ if (currentValue != null)
+ setMethod.invoke(source, (Object)null);
+ return;
+ }
+
+ if (!(item instanceof Enum<?>)) {
+ throw new IllegalArgumentException("Not String or Enum");
+ }
+
+ // Comparison with == ok, since both are enums
+ if (currentValue == item)
+ return;
+ setMethod.invoke(source, item);
+ }
+
+ @Override
+ public Object getElementAt(int index) {
+ if (values[index] == null)
+ return nullText;
+ return values[index];
+ }
+
+ @Override
+ public int getSize() {
+ return values.length;
+ }
+
+
+
+
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ Enum<T> value = (Enum<T>) getMethod.invoke(source);
+ if (value != currentValue) {
+ currentValue = value;
+ this.fireContentsChanged(this, 0, values.length);
+ }
+ }
+
+
+
+ @Override
+ public String toString() {
+ return "EnumModel["+source.getClass().getCanonicalName()+":"+valueName+"]";
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.adaptors;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+
+import javax.swing.SpinnerModel;
+import javax.swing.SpinnerNumberModel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.util.ChangeSource;
+
+
+public class IntegerModel implements ChangeListener {
+
+
+ //////////// JSpinner Model ////////////
+
+ private class IntegerSpinnerModel extends SpinnerNumberModel {
+ @Override
+ public Object getValue() {
+ return IntegerModel.this.getValue();
+ }
+
+ @Override
+ public void setValue(Object value) {
+ if (firing > 0) // Ignore, if called when model is sending events
+ return;
+ Number num = (Number)value;
+ int newValue = num.intValue();
+ IntegerModel.this.setValue(newValue);
+
+// try {
+// int newValue = Integer.parseInt((String)value);
+// IntegerModel.this.setValue(newValue);
+// } catch (NumberFormatException e) {
+// IntegerModel.this.fireStateChanged();
+// };
+ }
+
+ @Override
+ public Object getNextValue() {
+ int d = IntegerModel.this.getValue();
+ if (d >= maxValue)
+ return null;
+ return (d+1);
+ }
+
+ @Override
+ public Object getPreviousValue() {
+ int d = IntegerModel.this.getValue();
+ if (d <= minValue)
+ return null;
+ return (d-1);
+ }
+
+ @Override
+ public void addChangeListener(ChangeListener l) {
+ IntegerModel.this.addChangeListener(l);
+ }
+
+ @Override
+ public void removeChangeListener(ChangeListener l) {
+ IntegerModel.this.removeChangeListener(l);
+ }
+ }
+
+ /**
+ * Returns a new SpinnerModel with the same base as the DoubleModel.
+ * The values given to the JSpinner are in the currently selected units.
+ *
+ * @return A compatibility layer for a SpinnerModel.
+ */
+ public SpinnerModel getSpinnerModel() {
+ return new IntegerSpinnerModel();
+ }
+
+
+
+
+ //////////// Main model /////////////
+
+ /*
+ * The main model handles all values in SI units, i.e. no conversion is made within the model.
+ */
+
+ private final ChangeSource source;
+ private final String valueName;
+
+ private final Method getMethod;
+ private final Method setMethod;
+
+ private final ArrayList<ChangeListener> listeners = new ArrayList<ChangeListener>();
+
+ private final int minValue;
+ private final int maxValue;
+
+
+ private int firing = 0; // >0 when model itself is sending events
+
+
+ // Used to differentiate changes in valueName and other changes in the source:
+ private int lastValue = 0;
+
+
+
+ /**
+ * Generates a new DoubleModel that changes the values of the specified source.
+ * The double value is read and written using the methods "get"/"set" + valueName.
+ *
+ * @param source Component whose parameter to use.
+ * @param valueName Name of metods used to get/set the parameter.
+ * @param multiplier Value shown by the model is the value from source.getXXX * multiplier
+ * @param min Minimum value allowed (in SI units)
+ * @param max Maximum value allowed (in SI units)
+ */
+ public IntegerModel(ChangeSource source, String valueName, int min, int max) {
+ this.source = source;
+ this.valueName = valueName;
+
+ this.minValue = min;
+ this.maxValue = max;
+
+ try {
+ getMethod = source.getClass().getMethod("get" + valueName);
+ setMethod = source.getClass().getMethod("set" + valueName,int.class);
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException("get/set methods for value '"+valueName+
+ "' not present in class "+source.getClass().getCanonicalName());
+ }
+ }
+
+ public IntegerModel(ChangeSource source, String valueName, int min) {
+ this(source,valueName,min,Integer.MAX_VALUE);
+ }
+
+ public IntegerModel(ChangeSource source, String valueName) {
+ this(source,valueName,Integer.MIN_VALUE,Integer.MAX_VALUE);
+ }
+
+
+
+
+ /**
+ * Returns the value of the variable.
+ */
+ public int getValue() {
+ try {
+ return (Integer)getMethod.invoke(source);
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ return lastValue; // Should not occur
+ }
+
+ /**
+ * Sets the value of the variable.
+ */
+ public void setValue(int v) {
+ try {
+ setMethod.invoke(source, v);
+ return;
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ } catch (IllegalAccessException e) {
+ e.printStackTrace();
+ } catch (InvocationTargetException e) {
+ e.printStackTrace();
+ }
+ fireStateChanged(); // Should not occur
+ }
+
+
+ /**
+ * Add a listener to the model. Adds the model as a listener to the Component if this
+ * is the first listener.
+ * @param l Listener to add.
+ */
+ public void addChangeListener(ChangeListener l) {
+ if (listeners.isEmpty()) {
+ source.addChangeListener(this);
+ lastValue = getValue();
+ }
+
+ listeners.add(l);
+ }
+
+ /**
+ * Remove a listener from the model. Removes the model from being a listener to the Component
+ * if this was the last listener of the model.
+ * @param l Listener to remove.
+ */
+ public void removeChangeListener(ChangeListener l) {
+ listeners.remove(l);
+ if (listeners.isEmpty()) {
+ source.removeChangeListener(this);
+ }
+ }
+
+ public void fireStateChanged() {
+ Object[] l = listeners.toArray();
+ ChangeEvent event = new ChangeEvent(this);
+ firing++;
+ for (int i=0; i<l.length; i++)
+ ((ChangeListener)l[i]).stateChanged(event);
+ firing--;
+ }
+
+ /**
+ * Called when the source changes. Checks whether the modeled value has changed, and if
+ * it has, updates lastValue and generates ChangeEvents for all listeners of the model.
+ */
+ public void stateChanged(ChangeEvent e) {
+ int v = getValue();
+ if (lastValue == v)
+ return;
+ lastValue = v;
+ fireStateChanged();
+ }
+
+ /**
+ * Explain the DoubleModel as a String.
+ */
+ @Override
+ public String toString() {
+ return "IntegerModel["+source.getClass().getCanonicalName()+":"+valueName+"]";
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.adaptors;
+
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.AbstractListModel;
+import javax.swing.ComboBoxModel;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+import javax.swing.JTextField;
+import javax.swing.SwingUtilities;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.database.Database;
+import net.sf.openrocket.database.Databases;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.Reflection;
+
+public class MaterialModel extends AbstractListModel implements
+ ComboBoxModel, ChangeListener {
+
+ private static final String CUSTOM = "Custom";
+
+
+ private final RocketComponent component;
+ private final Material.Type type;
+ private final Database<Material> database;
+
+ private final Reflection.Method getMethod;
+ private final Reflection.Method setMethod;
+
+
+ public MaterialModel(RocketComponent component, Material.Type type) {
+ this(component, type, "Material");
+ }
+
+ public MaterialModel(RocketComponent component, Material.Type type, String name) {
+ this.component = component;
+ this.type = type;
+
+ switch (type) {
+ case LINE:
+ this.database = Databases.LINE_MATERIAL;
+ break;
+
+ case BULK:
+ this.database = Databases.BULK_MATERIAL;
+ break;
+
+ case SURFACE:
+ this.database = Databases.SURFACE_MATERIAL;
+ break;
+
+ default:
+ throw new IllegalArgumentException("Unknown material type:"+type);
+ }
+
+ try {
+ getMethod = new Reflection.Method(component.getClass().getMethod("get"+name));
+ setMethod = new Reflection.Method(component.getClass().getMethod("set"+name,
+ Material.class));
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException("get/is methods for material " +
+ "not present in class "+component.getClass().getCanonicalName());
+ }
+
+ component.addChangeListener(this);
+ database.addChangeListener(this);
+ }
+
+ @Override
+ public Object getSelectedItem() {
+ return getMethod.invoke(component);
+ }
+
+ @Override
+ public void setSelectedItem(Object item) {
+ if (item == CUSTOM) {
+
+ // Open custom material dialog in the future, after combo box has closed
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ AddMaterialDialog dialog = new AddMaterialDialog();
+ dialog.setVisible(true);
+
+ if (!dialog.okClicked)
+ return;
+
+ Material material = Material.newMaterial(type,
+ dialog.nameField.getText().trim(),
+ dialog.density.getValue());
+ setMethod.invoke(component, material);
+
+ // TODO: HIGH: Allow saving added material to database
+// if (dialog.addBox.isSelected()) {
+// database.add(material);
+// }
+ }
+ });
+
+ } else if (item instanceof Material) {
+
+ setMethod.invoke(component, item);
+
+ } else {
+ assert(false): "Should not occur";
+ }
+ }
+
+ @Override
+ public Object getElementAt(int index) {
+ if (index == database.size()) {
+ return CUSTOM;
+ } else if (index >= database.size()+1) {
+ return null;
+ }
+ return database.get(index);
+ }
+
+ @Override
+ public int getSize() {
+ return database.size() + 1;
+ }
+
+
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ if (e instanceof ComponentChangeEvent) {
+ if (((ComponentChangeEvent)e).isMassChange()) {
+ this.fireContentsChanged(this, 0, 0);
+ }
+ } else {
+ this.fireContentsChanged(this, 0, database.size());
+ }
+ }
+
+
+
+
+ private class AddMaterialDialog extends JDialog {
+
+ private boolean okClicked = false;
+ private JTextField nameField;
+ private DoubleModel density;
+// private JCheckBox addBox;
+
+ public AddMaterialDialog() {
+ super((JFrame)null, "Custom material", true);
+
+ Material material = (Material) getSelectedItem();
+
+ JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]"));
+
+ panel.add(new JLabel("Material name:"));
+ nameField = new JTextField(15);
+ nameField.setText(material.getName());
+ panel.add(nameField,"span 2, growx, wrap");
+
+ panel.add(new JLabel("Material density:"));
+ density = new DoubleModel(material.getDensity(),UnitGroup.UNITS_DENSITY_BULK,0);
+ JSpinner spinner = new JSpinner(density.getSpinnerModel());
+ panel.add(spinner, "growx");
+ panel.add(new UnitSelector(density),"wrap");
+
+// addBox = new JCheckBox("Add material to database");
+// panel.add(addBox,"span, wrap");
+
+ JButton button = new JButton("OK");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ okClicked = true;
+ AddMaterialDialog.this.setVisible(false);
+ }
+ });
+ panel.add(button,"span, split, tag ok");
+
+ button = new JButton("Cancel");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ AddMaterialDialog.this.setVisible(false);
+ }
+ });
+ panel.add(button,"tag cancel");
+
+ this.setContentPane(panel);
+ this.pack();
+ this.setAlwaysOnTop(true);
+ this.setLocationRelativeTo(null);
+ }
+
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.adaptors;
+
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+
+import javax.swing.ComboBoxModel;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.JTextField;
+import javax.swing.ListSelectionModel;
+import javax.swing.SwingUtilities;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.EventListenerList;
+import javax.swing.event.ListDataEvent;
+import javax.swing.event.ListDataListener;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.ResizeLabel;
+import net.sf.openrocket.gui.TextFieldListener;
+import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.util.GUIUtil;
+
+public class MotorConfigurationModel implements ComboBoxModel, ChangeListener {
+
+ private static final String EDIT = "Edit configurations";
+
+
+ private EventListenerList listenerList = new EventListenerList();
+
+ private final Configuration config;
+ private final Rocket rocket;
+
+ private Map<String, ID> map = new HashMap<String, ID>();
+
+
+ public MotorConfigurationModel(Configuration config) {
+ this.config = config;
+ this.rocket = config.getRocket();
+ config.addChangeListener(this);
+ }
+
+
+
+ @Override
+ public Object getElementAt(int index) {
+ String[] ids = rocket.getMotorConfigurationIDs();
+ if (index < 0 || index > ids.length)
+ return null;
+
+ if (index == ids.length)
+ return EDIT;
+
+ return get(ids[index]);
+ }
+
+ @Override
+ public int getSize() {
+ return rocket.getMotorConfigurationIDs().length + 1;
+ }
+
+ @Override
+ public Object getSelectedItem() {
+ return get(config.getMotorConfigurationID());
+ }
+
+ @Override
+ public void setSelectedItem(Object item) {
+ if (item == EDIT) {
+
+ // Open edit dialog in the future, after combo box has closed
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ EditConfigurationDialog dialog = new EditConfigurationDialog();
+ dialog.setVisible(true);
+
+ if (dialog.isRowSelected()) {
+ rocket.getDefaultConfiguration().setMotorConfigurationID(
+ dialog.getSelectedID());
+ }
+ }
+ });
+
+ return;
+ }
+ if (!(item instanceof ID))
+ return;
+
+ ID idObject = (ID) item;
+ config.setMotorConfigurationID(idObject.getID());
+ }
+
+
+
+ //////////////// Event/listener handling ////////////////
+
+
+ @Override
+ public void addListDataListener(ListDataListener l) {
+ listenerList.add(ListDataListener.class, l);
+ }
+
+ @Override
+ public void removeListDataListener(ListDataListener l) {
+ listenerList.remove(ListDataListener.class, l);
+ }
+
+ protected void fireListDataEvent() {
+ Object[] listeners = listenerList.getListenerList();
+ ListDataEvent e = null;
+
+ for (int i = listeners.length-2; i>=0; i-=2) {
+ if (listeners[i] == ListDataListener.class) {
+ if (e == null)
+ e = new ListDataEvent(this, ListDataEvent.CONTENTS_CHANGED, 0, getSize());
+ ((ListDataListener) listeners[i+1]).contentsChanged(e);
+ }
+ }
+ }
+
+
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ if (e instanceof ComponentChangeEvent) {
+ // Ignore unnecessary changes
+ if (!((ComponentChangeEvent)e).isMotorChange())
+ return;
+ }
+ fireListDataEvent();
+ }
+
+
+
+ /*
+ * The ID class is an adapter, that contains the actual configuration ID,
+ * but gives the configuration description as its String representation.
+ * The get(id) method retrieves ID objects and caches them for reuse.
+ */
+
+ private ID get(String id) {
+ ID idObject = map.get(id);
+ if (idObject != null)
+ return idObject;
+
+ idObject = new ID(id);
+ map.put(id, idObject);
+ return idObject;
+ }
+
+
+ private class ID {
+ private final String id;
+
+ public ID(String id) {
+ this.id = id;
+ }
+
+ public String getID() {
+ return id;
+ }
+
+ @Override
+ public String toString() {
+ return rocket.getMotorConfigurationDescription(id);
+ }
+ }
+
+
+ private class EditConfigurationDialog extends JDialog {
+ private final ColumnTableModel tableModel;
+ private String[] ids;
+ int selection = -1;
+
+ private final JButton addButton;
+ private final JButton removeButton;
+ private final JTextField nameField;
+
+ private final JTable table;
+
+
+ public boolean isRowSelected() {
+ return selection >= 0;
+ }
+
+ public String getSelectedID() {
+ if (selection >= 0)
+ return ids[selection];
+ return null;
+ }
+
+
+ public EditConfigurationDialog() {
+ super((JFrame)null, "Edit configurations", true);
+
+ ids = rocket.getMotorConfigurationIDs();
+
+ // Create columns
+ ArrayList<Column> columnList = new ArrayList<Column>();
+ columnList.add(new Column("Name") {
+ @Override
+ public Object getValueAt(int row) {
+ return rocket.getMotorConfigurationDescription(ids[row]);
+ }
+ });
+
+ // Create columns from the motor mounts
+ Iterator<RocketComponent> iterator = rocket.deepIterator();
+ while (iterator.hasNext()) {
+ RocketComponent c = iterator.next();
+ if (!(c instanceof MotorMount))
+ continue;
+
+ final MotorMount mount = (MotorMount)c;
+ if (!mount.isMotorMount())
+ continue;
+
+ Column col = new Column(c.getName()) {
+ @Override
+ public Object getValueAt(int row) {
+ Motor motor = mount.getMotor(ids[row]);
+ if (motor == null)
+ return "";
+ return motor.getDesignation(mount.getMotorDelay(ids[row]));
+ }
+ };
+ columnList.add(col);
+ }
+ tableModel = new ColumnTableModel(columnList.toArray(new Column[0])) {
+ @Override
+ public int getRowCount() {
+ return ids.length;
+ }
+ };
+
+
+
+ // Create the panel
+ JPanel panel = new JPanel(new MigLayout("fill","[shrink][grow]"));
+
+
+ panel.add(new JLabel("Configuration name:"), "gapright para");
+ nameField = new JTextField();
+ new TextFieldListener() {
+ @Override
+ public void setText(String text) {
+ if (selection < 0 || ids[selection] == null)
+ return;
+ rocket.setMotorConfigurationName(ids[selection], text);
+ fireChange();
+ }
+ }.listenTo(nameField);
+ panel.add(nameField, "growx, wrap");
+
+ panel.add(new ResizeLabel("Leave empty for default description", -2),
+ "skip, growx, wrap para");
+
+
+ table = new JTable(tableModel);
+ table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+ table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ updateSelection();
+ }
+ });
+
+ // Mouse listener to act on double-clicks
+ table.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
+ EditConfigurationDialog.this.dispose();
+ }
+ }
+ });
+
+
+
+ JScrollPane scrollpane = new JScrollPane(table);
+ panel.add(scrollpane, "spanx, height 150lp, width 400lp, grow, wrap");
+
+
+ addButton = new JButton("New");
+ addButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ String id = rocket.newMotorConfigurationID();
+ ids = rocket.getMotorConfigurationIDs();
+ tableModel.fireTableDataChanged();
+ int sel;
+ for (sel=0; sel < ids.length; sel++) {
+ if (id.equals(ids[sel]))
+ break;
+ }
+ table.getSelectionModel().addSelectionInterval(sel, sel);
+ }
+ });
+ panel.add(addButton, "growx, spanx, split 2");
+
+ removeButton = new JButton("Remove");
+ removeButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ int sel = table.getSelectedRow();
+ if (sel < 0 || sel >= ids.length || ids[sel] == null)
+ return;
+ rocket.removeMotorConfigurationID(ids[sel]);
+ ids = rocket.getMotorConfigurationIDs();
+ tableModel.fireTableDataChanged();
+ if (sel >= ids.length)
+ sel--;
+ table.getSelectionModel().addSelectionInterval(sel, sel);
+ }
+ });
+ panel.add(removeButton, "growx, wrap para");
+
+
+ JButton close = new JButton("Close");
+ close.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ EditConfigurationDialog.this.dispose();
+ }
+ });
+ panel.add(close, "spanx, alignx 100%");
+
+ this.getRootPane().setDefaultButton(close);
+
+
+ this.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
+ GUIUtil.installEscapeCloseOperation(this);
+ this.setLocationByPlatform(true);
+
+ updateSelection();
+
+ this.add(panel);
+ this.validate();
+ this.pack();
+ }
+
+ private void fireChange() {
+ int sel = table.getSelectedRow();
+ tableModel.fireTableDataChanged();
+ table.getSelectionModel().addSelectionInterval(sel, sel);
+ }
+
+ private void updateSelection() {
+ selection = table.getSelectedRow();
+ if (selection < 0 || ids[selection] == null) {
+ removeButton.setEnabled(false);
+ nameField.setEnabled(false);
+ nameField.setText("");
+ } else {
+ removeButton.setEnabled(true);
+ nameField.setEnabled(true);
+ nameField.setText(rocket.getMotorConfigurationName(ids[selection]));
+ }
+ }
+ }
+
+}
+
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+
+import javax.swing.JCheckBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.BooleanModel;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.BodyTube;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.UnitGroup;
+
+public class BodyTubeConfig extends RocketComponentConfig {
+
+ private MotorConfig motorConfigPane = null;
+
+ public BodyTubeConfig(RocketComponent c) {
+ super(c);
+
+ JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::][]",""));
+
+ //// Body tube length
+ panel.add(new JLabel("Body tube length:"));
+
+ DoubleModel m = new DoubleModel(component,"Length",UnitGroup.UNITS_LENGTH,0);
+
+ JSpinner spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0, 0.5, 2.0)),"w 100lp, wrap");
+
+
+ //// Body tube diameter
+ panel.add(new JLabel("Outer diameter:"));
+
+ DoubleModel od = new DoubleModel(component,"Radius",2,UnitGroup.UNITS_LENGTH,0);
+ // Diameter = 2*Radius
+
+ spin = new JSpinner(od.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(od),"growx");
+ panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)),"w 100lp, wrap 0px");
+
+ JCheckBox check = new JCheckBox(od.getAutomaticAction());
+ check.setText("Automatic");
+ panel.add(check,"skip, span 2, wrap");
+
+
+ //// Inner diameter
+ panel.add(new JLabel("Inner diameter:"));
+
+ // Diameter = 2*Radius
+ m = new DoubleModel(component,"InnerRadius",2,UnitGroup.UNITS_LENGTH,0);
+
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(new DoubleModel(0), od)),"w 100lp, wrap");
+
+
+ //// Wall thickness
+ panel.add(new JLabel("Wall thickness:"));
+
+ m = new DoubleModel(component,"Thickness",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0,0.01)),"w 100lp, wrap 0px");
+
+
+ check = new JCheckBox(new BooleanModel(component,"Filled"));
+ check.setText("Filled");
+ panel.add(check,"skip, span 2, wrap");
+
+
+ //// Material
+ panel.add(materialPanel(new JPanel(new MigLayout()), Material.Type.BULK),
+ "cell 4 0, gapleft paragraph, aligny 0%, spany");
+
+
+ tabbedPane.insertTab("General", null, panel, "General properties", 0);
+ motorConfigPane = new MotorConfig((BodyTube)c);
+ tabbedPane.insertTab("Motor", null, motorConfigPane, "Motor mount configuration", 1);
+ tabbedPane.setSelectedIndex(0);
+ }
+
+ @Override
+ public void updateFields() {
+ super.updateFields();
+ if (motorConfigPane != null)
+ motorConfigPane.updateFields();
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+
+import javax.swing.JPanel;
+
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+
+
+public class BulkheadConfig extends RingComponentConfig {
+
+ public BulkheadConfig(RocketComponent c) {
+ super(c);
+
+ JPanel tab;
+
+ tab = generalTab("Radius:", null, null, "Thickness:");
+ tabbedPane.insertTab("General", null, tab, "General properties", 0);
+ tabbedPane.setSelectedIndex(0);
+ }
+
+}
\ No newline at end of file
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+
+import javax.swing.JPanel;
+
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+
+
+public class CenteringRingConfig extends RingComponentConfig {
+
+ public CenteringRingConfig(RocketComponent c) {
+ super(c);
+
+ JPanel tab;
+
+ tab = generalTab("Outer diameter:", "Inner diameter:", null, "Thickness:");
+ tabbedPane.insertTab("General", null, tab, "General properties", 0);
+ tabbedPane.setSelectedIndex(0);
+ }
+
+}
\ No newline at end of file
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.Point;
+import java.awt.Window;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+
+import javax.swing.DefaultBoundedRangeModel;
+import javax.swing.JDialog;
+import javax.swing.JSlider;
+import javax.swing.JSpinner;
+import javax.swing.SpinnerNumberModel;
+
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.gui.Resettable;
+import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
+import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.util.GUIUtil;
+import net.sf.openrocket.util.Prefs;
+
+/**
+ * A JFrame dialog that contains the configuration elements of one component.
+ * The contents of the dialog are instantiated from CONFIGDIALOGPACKAGE according
+ * to the current component.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class ComponentConfigDialog extends JDialog implements ComponentChangeListener {
+ private static final long serialVersionUID = 1L;
+ private static final String CONFIGDIALOGPACKAGE = "net.sf.openrocket.gui.configdialog";
+ private static final String CONFIGDIALOGPOSTFIX = "Config";
+
+
+ private static ComponentConfigDialog dialog = null;
+
+
+ private OpenRocketDocument document = null;
+ private RocketComponent component = null;
+ private RocketComponentConfig configurator = null;
+
+ private final Window parent;
+
+ private ComponentConfigDialog(Window parent, OpenRocketDocument document,
+ RocketComponent component) {
+ super(parent);
+ this.parent = parent;
+
+ setComponent(document, component);
+
+ // Set window position according to preferences, and set prefs when moving
+ Point position = Prefs.getWindowPosition(this.getClass());
+ if (position == null)
+ this.setLocationByPlatform(true);
+ else
+ this.setLocation(position);
+
+ this.addComponentListener(new ComponentAdapter() {
+ @Override
+ public void componentMoved(ComponentEvent e) {
+ Prefs.setWindowPosition(ComponentConfigDialog.this.getClass(),
+ ComponentConfigDialog.this.getLocation());
+ }
+ });
+
+
+ // Install ESC listener
+ GUIUtil.installEscapeCloseOperation(this);
+ }
+
+
+ /**
+ * Set the component being configured. The listening connections of the old configurator
+ * will be removed and the new ones created.
+ *
+ * @param component Component to configure.
+ */
+ private void setComponent(OpenRocketDocument document, RocketComponent component) {
+ if (this.document != null) {
+ this.document.getRocket().removeComponentChangeListener(this);
+ }
+
+ if (configurator != null) {
+ // Remove listeners by setting all applicable models to null
+ setNullModels(configurator); // null-safe
+
+// mainPanel.remove(configurator);
+ }
+
+ this.document = document;
+ this.component = component;
+ this.document.getRocket().addComponentChangeListener(this);
+
+ configurator = getDialogContents();
+ this.setContentPane(configurator);
+ configurator.updateFields();
+// mainPanel.add(configurator,"cell 0 0, growx, growy");
+
+ setTitle(component.getComponentName()+" configuration");
+
+// Dimension pref = getPreferredSize();
+// Dimension real = getSize();
+// if (pref.width > real.width || pref.height > real.height)
+ pack();
+ }
+
+ /**
+ * Traverses recursively the component tree, and sets all applicable component
+ * models to null, so as to remove the listener connections.
+ *
+ * NOTE: All components in the configuration dialogs that use custom models must be added
+ * to this method.
+ */
+ private void setNullModels(Component c) {
+ if (c==null)
+ return;
+
+ // Remove models for known components
+ // Why the FSCK must this be so hard?!?!?
+
+ if (c instanceof JSpinner) {
+ ((JSpinner)c).setModel(new SpinnerNumberModel());
+ } else if (c instanceof JSlider) {
+ ((JSlider)c).setModel(new DefaultBoundedRangeModel());
+ } else if (c instanceof Resettable) {
+ ((Resettable)c).resetModel();
+ }
+
+
+ if (c instanceof Container) {
+ Component[] cs = ((Container)c).getComponents();
+ for (Component sub: cs)
+ setNullModels(sub);
+ }
+
+ }
+
+
+ /**
+ * Return the configurator panel of the current component.
+ */
+ private RocketComponentConfig getDialogContents() {
+ Constructor<? extends RocketComponentConfig> c =
+ findDialogContentsConstructor(component);
+ if (c != null) {
+ try {
+ return (RocketComponentConfig) c.newInstance(component);
+ } catch (InstantiationException e) {
+ throw new RuntimeException("BUG in constructor reflection",e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("BUG in constructor reflection",e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException("BUG in constructor reflection",e);
+ }
+ }
+
+ // Should never be reached, since RocketComponentConfig should catch all
+ // components without their own configurator.
+ throw new RuntimeException("Unable to find any configurator for "+component);
+ }
+
+ /**
+ * Finds the Constructor of the given component's config dialog panel in
+ * CONFIGDIALOGPACKAGE.
+ */
+ @SuppressWarnings("unchecked")
+ private static Constructor<? extends RocketComponentConfig>
+ findDialogContentsConstructor(RocketComponent component) {
+ Class<?> currentclass;
+ String currentclassname;
+ String configclassname;
+
+ Class<?> configclass;
+ Constructor<? extends RocketComponentConfig> c;
+
+ currentclass = component.getClass();
+ while ((currentclass != null) && (currentclass != Object.class)) {
+ currentclassname = currentclass.getCanonicalName();
+ int index = currentclassname.lastIndexOf('.');
+ if (index >= 0)
+ currentclassname = currentclassname.substring(index + 1);
+ configclassname = CONFIGDIALOGPACKAGE + "." + currentclassname +
+ CONFIGDIALOGPOSTFIX;
+
+ try {
+ configclass = Class.forName(configclassname);
+ c = (Constructor<? extends RocketComponentConfig>)
+ configclass.getConstructor(RocketComponent.class);
+ return c;
+ } catch (Exception ignore) { }
+
+ currentclass = currentclass.getSuperclass();
+ }
+ return null;
+ }
+
+
+
+
+ ////////// Static dialog /////////
+
+ /**
+ * A singleton configuration dialog. Will create and show a new dialog if one has not
+ * previously been used, or update the dialog and show it if a previous one exists.
+ *
+ * @param document the document to configure.
+ * @param component the component to configure.
+ */
+ public static void showDialog(Window parent, OpenRocketDocument document,
+ RocketComponent component) {
+ if (dialog != null)
+ dialog.dispose();
+
+ dialog = new ComponentConfigDialog(parent, document, component);
+ dialog.setVisible(true);
+
+ document.addUndoPosition("Modify "+component.getComponentName());
+ }
+
+
+ /* package */
+ static void showDialog(RocketComponent component) {
+ showDialog(dialog.parent, dialog.document, component);
+ }
+
+ /**
+ * Hides the configuration dialog. May be used even if not currently visible.
+ */
+ public static void hideDialog() {
+ if (dialog != null)
+ dialog.setVisible(false);
+ }
+
+
+ /**
+ * Add an undo position for the current document. This is intended for use only
+ * by the currently open dialog.
+ *
+ * @param description Description of the undoable action
+ */
+ /*package*/ static void addUndoPosition(String description) {
+ if (dialog == null) {
+ throw new IllegalStateException("Dialog not open, report bug!");
+ }
+ dialog.document.addUndoPosition(description);
+ }
+
+ /*package*/
+ static String getUndoDescription() {
+ if (dialog == null) {
+ throw new IllegalStateException("Dialog not open, report bug!");
+ }
+ return dialog.document.getUndoDescription();
+ }
+
+ /**
+ * Returns whether the singleton configuration dialog is currently visible or not.
+ */
+ public static boolean isDialogVisible() {
+ return (dialog!=null) && (dialog.isVisible());
+ }
+
+
+ public void componentChanged(ComponentChangeEvent e) {
+ if (e.isTreeChange() || e.isUndoChange()) {
+
+ // Hide dialog in case of tree or undo change
+ dialog.setVisible(false);
+
+ } else {
+ configurator.updateFields();
+ }
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSeparator;
+import javax.swing.JSpinner;
+import javax.swing.SwingConstants;
+import javax.swing.SwingUtilities;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.gui.adaptors.IntegerModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.UnitGroup;
+
+public class EllipticalFinSetConfig extends FinSetConfig {
+
+ public EllipticalFinSetConfig(final RocketComponent component) {
+ super(component);
+
+ DoubleModel m;
+ JSpinner spin;
+ JComboBox combo;
+
+ JPanel mainPanel = new JPanel(new MigLayout());
+
+
+
+ JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+
+ //// Number of fins
+ panel.add(new JLabel("Number of fins:"));
+
+ IntegerModel im = new IntegerModel(component,"FinCount",1,8);
+
+ spin = new JSpinner(im.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx, wrap");
+
+
+ //// Base rotation
+ panel.add(new JLabel("Rotation:"));
+
+ m = new DoubleModel(component, "BaseRotation", UnitGroup.UNITS_ANGLE,-Math.PI,Math.PI);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(-Math.PI,Math.PI)),"w 100lp, wrap");
+
+
+ //// Root chord
+ panel.add(new JLabel("Root chord:"));
+
+ m = new DoubleModel(component,"Length",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0,0.05,0.2)),"w 100lp, wrap");
+
+
+ //// Height
+ panel.add(new JLabel("Height:"));
+
+ m = new DoubleModel(component,"Height",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0,0.05,0.2)),"w 100lp, wrap");
+
+
+ //// Position
+
+ panel.add(new JLabel("Position relative to:"));
+
+ combo = new JComboBox(
+ new EnumModel<RocketComponent.Position>(component, "RelativePosition",
+ new RocketComponent.Position[] {
+ RocketComponent.Position.TOP,
+ RocketComponent.Position.MIDDLE,
+ RocketComponent.Position.BOTTOM,
+ RocketComponent.Position.ABSOLUTE
+ }));
+ panel.add(combo,"spanx, growx, wrap");
+
+ panel.add(new JLabel("plus"),"right");
+
+ m = new DoubleModel(component,"PositionValue",UnitGroup.UNITS_LENGTH);
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(
+ new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE),
+ new DoubleModel(component.getParent(), "Length"))),
+ "w 100lp, wrap");
+
+
+
+ //// Right portion
+ mainPanel.add(panel,"aligny 20%");
+
+ mainPanel.add(new JSeparator(SwingConstants.VERTICAL),"growy");
+
+
+
+ panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+
+
+ //// Cross section
+ panel.add(new JLabel("Fin cross section:"),"span, split");
+ combo = new JComboBox(
+ new EnumModel<FinSet.CrossSection>(component,"CrossSection"));
+ panel.add(combo,"growx, wrap unrel");
+
+
+ //// Thickness
+ panel.add(new JLabel("Thickness:"));
+
+ m = new DoubleModel(component,"Thickness",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0,0.01)),"w 100lp, wrap 30lp");
+
+
+
+ //// Material
+ materialPanel(panel, Material.Type.BULK);
+
+
+
+ //// Convert button
+
+ JButton button = new JButton("Convert to freeform fin set");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ // Do change in future for overall safety
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ FreeformFinSet freeform = new FreeformFinSet((FinSet)component);
+ RocketComponent parent = component.getParent();
+ int index = parent.getChildPosition(component);
+
+ ComponentConfigDialog.addUndoPosition("Convert fin set");
+ parent.removeChild(index);
+ parent.addChild(freeform, index);
+ ComponentConfigDialog.showDialog(freeform);
+ }
+ });
+
+ ComponentConfigDialog.hideDialog();
+ }
+ });
+ panel.add(button,"span, growx, gaptop paragraph");
+
+
+
+ mainPanel.add(panel,"aligny 20%");
+
+ addFinSetButtons();
+
+
+ tabbedPane.insertTab("General", null, mainPanel, "General properties", 0);
+ tabbedPane.setSelectedIndex(0);
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JButton;
+import javax.swing.SwingUtilities;
+
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+
+public abstract class FinSetConfig extends RocketComponentConfig {
+
+ private JButton split = null;
+
+ public FinSetConfig(RocketComponent component) {
+ super(component);
+ }
+
+
+
+ protected void addFinSetButtons() {
+ JButton convert=null;
+
+ //// Convert buttons
+ if (!(component instanceof FreeformFinSet)) {
+ convert = new JButton("Convert to freeform");
+ convert.setToolTipText("Convert this fin set into a freeform fin set");
+ convert.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ // Do change in future for overall safety
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ FreeformFinSet freeform = new FreeformFinSet((FinSet)component);
+ String name = component.getComponentName();
+
+ if (freeform.getName().startsWith(name)) {
+ freeform.setName(freeform.getComponentName() +
+ freeform.getName().substring(name.length()));
+ }
+
+ RocketComponent parent = component.getParent();
+ int index = parent.getChildPosition(component);
+
+ ComponentConfigDialog.addUndoPosition("Convert fin set");
+ parent.removeChild(index);
+ parent.addChild(freeform, index);
+ ComponentConfigDialog.showDialog(freeform);
+ }
+ });
+
+ ComponentConfigDialog.hideDialog();
+ }
+ });
+ }
+
+ split = new JButton("Split fins");
+ split.setToolTipText("Split the fin set into separate fins");
+ split.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ // Do change in future for overall safety
+ SwingUtilities.invokeLater(new Runnable() {
+ @Override
+ public void run() {
+ RocketComponent parent = component.getParent();
+ int index = parent.getChildPosition(component);
+ int count = ((FinSet)component).getFinCount();
+ double base = ((FinSet)component).getBaseRotation();
+ if (count <= 1)
+ return;
+
+ ComponentConfigDialog.addUndoPosition("Split fin set");
+ parent.removeChild(index);
+ for (int i=0; i<count; i++) {
+ FinSet copy = (FinSet)component.copy();
+ copy.setFinCount(1);
+ copy.setBaseRotation(base + i*2*Math.PI/count);
+ copy.setName(copy.getName() + " #" + (i+1));
+ parent.addChild(copy, index+i);
+ }
+ }
+ });
+
+ ComponentConfigDialog.hideDialog();
+ }
+ });
+ split.setEnabled(((FinSet)component).getFinCount() > 1);
+
+ if (convert==null)
+ addButtons(split);
+ else
+ addButtons(split,convert);
+
+ }
+
+
+ @Override
+ public void updateFields() {
+ super.updateFields();
+ if (split != null)
+ split.setEnabled(((FinSet)component).getFinCount() > 1);
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.Point;
+import java.awt.event.MouseEvent;
+import java.awt.geom.Point2D;
+
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSeparator;
+import javax.swing.JSpinner;
+import javax.swing.JTable;
+import javax.swing.ListSelectionModel;
+import javax.swing.SwingConstants;
+import javax.swing.table.AbstractTableModel;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.ResizeLabel;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.gui.adaptors.IntegerModel;
+import net.sf.openrocket.gui.scalefigure.FinPointFigure;
+import net.sf.openrocket.gui.scalefigure.ScaleScrollPane;
+import net.sf.openrocket.gui.scalefigure.ScaleSelector;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.Coordinate;
+
+public class FreeformFinSetConfig extends FinSetConfig {
+
+ private final FreeformFinSet finset;
+ private JTable table = null;
+ private FinPointTableModel tableModel = null;
+
+ private FinPointFigure figure = null;
+
+
+ public FreeformFinSetConfig(RocketComponent component) {
+ super(component);
+ this.finset = (FreeformFinSet)component;
+
+
+ tabbedPane.insertTab("General", null, generalPane(), "General properties", 0);
+ tabbedPane.insertTab("Shape", null, shapePane(), "Fin shape", 1);
+ tabbedPane.setSelectedIndex(0);
+
+ addFinSetButtons();
+ }
+
+
+
+ private JPanel generalPane() {
+
+ DoubleModel m;
+ JSpinner spin;
+ JComboBox combo;
+
+ JPanel mainPanel = new JPanel(new MigLayout("fill"));
+
+ JPanel panel = new JPanel(new MigLayout("fill, gap rel unrel","[][65lp::][30lp::]",""));
+
+
+
+ //// Number of fins
+ panel.add(new JLabel("Number of fins:"));
+
+ IntegerModel im = new IntegerModel(component,"FinCount",1,8);
+
+ spin = new JSpinner(im.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx, wrap");
+
+
+ //// Base rotation
+ panel.add(new JLabel("Fin rotation:"));
+
+ m = new DoubleModel(component, "BaseRotation", UnitGroup.UNITS_ANGLE,-Math.PI,Math.PI);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(-Math.PI,Math.PI)),"w 100lp, wrap");
+
+
+
+ //// Fin cant
+ JLabel label = new JLabel("Fin cant:");
+ label.setToolTipText("The angle that the fins are canted with respect to the rocket " +
+ "body.");
+ panel.add(label);
+
+ m = new DoubleModel(component, "CantAngle", UnitGroup.UNITS_ANGLE,
+ -FinSet.MAX_CANT,FinSet.MAX_CANT);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(-FinSet.MAX_CANT,FinSet.MAX_CANT)),
+ "w 100lp, wrap 40lp");
+
+
+
+ //// Position
+ panel.add(new JLabel("Position relative to:"));
+
+ combo = new JComboBox(
+ new EnumModel<RocketComponent.Position>(component, "RelativePosition",
+ new RocketComponent.Position[] {
+ RocketComponent.Position.TOP,
+ RocketComponent.Position.MIDDLE,
+ RocketComponent.Position.BOTTOM,
+ RocketComponent.Position.ABSOLUTE
+ }));
+ panel.add(combo,"spanx 3, growx, wrap");
+
+ panel.add(new JLabel("plus"),"right");
+
+ m = new DoubleModel(component,"PositionValue",UnitGroup.UNITS_LENGTH);
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(
+ new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE),
+ new DoubleModel(component.getParent(), "Length"))),
+ "w 100lp, wrap");
+
+
+
+
+
+ mainPanel.add(panel, "aligny 20%");
+ mainPanel.add(new JSeparator(SwingConstants.VERTICAL), "growy, height 150lp");
+
+
+ panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+
+
+
+
+ //// Cross section
+ panel.add(new JLabel("Fin cross section:"),"span, split");
+ combo = new JComboBox(
+ new EnumModel<FinSet.CrossSection>(component,"CrossSection"));
+ panel.add(combo,"growx, wrap unrel");
+
+
+ //// Thickness
+ panel.add(new JLabel("Thickness:"));
+
+ m = new DoubleModel(component,"Thickness",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0,0.01)),"w 100lp, wrap 30lp");
+
+
+ //// Material
+ materialPanel(panel, Material.Type.BULK);
+
+
+
+ mainPanel.add(panel, "aligny 20%");
+
+ return mainPanel;
+ }
+
+
+
+ private JPanel shapePane() {
+ JPanel panel = new JPanel(new MigLayout("fill"));
+
+
+ // Create the figure
+ figure = new FinPointFigure(finset);
+ ScaleScrollPane figurePane = new FinPointScrollPane();
+ figurePane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
+ figurePane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
+
+ // Create the table
+ tableModel = new FinPointTableModel();
+ table = new JTable(tableModel);
+ table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+ for (int i=0; i < Columns.values().length; i++) {
+ table.getColumnModel().getColumn(i).
+ setPreferredWidth(Columns.values()[i].getWidth());
+ }
+ JScrollPane tablePane = new JScrollPane(table);
+
+
+// panel.add(new JLabel("Coordinates:"), "aligny bottom, alignx 50%");
+// panel.add(new JLabel(" View:"), "wrap, aligny bottom");
+
+
+ panel.add(tablePane,"growy, width :100lp:, height 100lp:250lp:");
+ panel.add(figurePane,"gap unrel, spanx, growx, growy 1000, height 100lp:250lp:, wrap");
+
+ panel.add(new ResizeLabel("Double-click", -2), "alignx 50%");
+
+ panel.add(new ScaleSelector(figurePane),"spany 2");
+ panel.add(new ResizeLabel("Click+drag: Add and move points " +
+ "Ctrl+click: Remove point", -2), "spany 2, right, wrap");
+
+
+ panel.add(new ResizeLabel("to edit", -2), "alignx 50%");
+
+ return panel;
+ }
+
+
+
+
+
+ @Override
+ public void updateFields() {
+ super.updateFields();
+
+ if (tableModel != null) {
+ tableModel.fireTableDataChanged();
+ }
+ if (figure != null) {
+ figure.updateFigure();
+ }
+ }
+
+
+
+
+ private class FinPointScrollPane extends ScaleScrollPane {
+ private static final int ANY_MASK =
+ (MouseEvent.ALT_DOWN_MASK | MouseEvent.ALT_GRAPH_DOWN_MASK |
+ MouseEvent.META_DOWN_MASK | MouseEvent.CTRL_DOWN_MASK |
+ MouseEvent.SHIFT_DOWN_MASK);
+
+ private int dragIndex = -1;
+
+ public FinPointScrollPane() {
+ super(figure, false); // Disallow fitting as it's buggy
+ }
+
+ @Override
+ public void mousePressed(MouseEvent event) {
+ int mods = event.getModifiersEx();
+
+ if (event.getButton() != MouseEvent.BUTTON1 ||
+ (mods & ANY_MASK) != 0) {
+ super.mousePressed(event);
+ return;
+ }
+
+ int index = getPoint(event);
+ if (index >= 0) {
+ dragIndex = index;
+ return;
+ }
+ index = getSegment(event);
+ if (index >= 0) {
+ Point2D.Double point = getCoordinates(event);
+ finset.addPoint(index);
+ try {
+ finset.setPoint(index, point.x, point.y);
+ } catch (IllegalArgumentException ignore) { }
+ dragIndex = index;
+
+ return;
+ }
+
+ super.mousePressed(event);
+ return;
+ }
+
+
+ @Override
+ public void mouseDragged(MouseEvent event) {
+ int mods = event.getModifiersEx();
+ if (dragIndex < 0 ||
+ (mods & (ANY_MASK | MouseEvent.BUTTON1_DOWN_MASK)) !=
+ MouseEvent.BUTTON1_DOWN_MASK) {
+ super.mouseDragged(event);
+ return;
+ }
+ Point2D.Double point = getCoordinates(event);
+
+ try {
+ finset.setPoint(dragIndex, point.x, point.y);
+ } catch (IllegalArgumentException ignore) {
+ System.out.println("IAE:"+ignore);
+ }
+ }
+
+
+ @Override
+ public void mouseReleased(MouseEvent event) {
+ dragIndex = -1;
+ super.mouseReleased(event);
+ }
+
+ @Override
+ public void mouseClicked(MouseEvent event) {
+ int mods = event.getModifiersEx();
+ if (event.getButton() != MouseEvent.BUTTON1 ||
+ (mods & ANY_MASK) != MouseEvent.CTRL_DOWN_MASK) {
+ super.mouseClicked(event);
+ return;
+ }
+
+ int index = getPoint(event);
+ if (index < 0) {
+ super.mouseClicked(event);
+ return;
+ }
+
+ try {
+ finset.removePoint(index);
+ } catch (IllegalArgumentException ignore) {
+ }
+ }
+
+
+ private int getPoint(MouseEvent event) {
+ Point p0 = event.getPoint();
+ Point p1 = this.getViewport().getViewPosition();
+ int x = p0.x + p1.x;
+ int y = p0.y + p1.y;
+
+ return figure.getIndexByPoint(x, y);
+ }
+
+ private int getSegment(MouseEvent event) {
+ Point p0 = event.getPoint();
+ Point p1 = this.getViewport().getViewPosition();
+ int x = p0.x + p1.x;
+ int y = p0.y + p1.y;
+
+ return figure.getSegmentByPoint(x, y);
+ }
+
+ private Point2D.Double getCoordinates(MouseEvent event) {
+ Point p0 = event.getPoint();
+ Point p1 = this.getViewport().getViewPosition();
+ int x = p0.x + p1.x;
+ int y = p0.y + p1.y;
+
+ return figure.convertPoint(x, y);
+ }
+
+
+ }
+
+
+
+
+
+ private enum Columns {
+// NUMBER {
+// @Override
+// public String toString() {
+// return "#";
+// }
+// @Override
+// public String getValue(FreeformFinSet finset, int row) {
+// return "" + (row+1) + ".";
+// }
+// @Override
+// public int getWidth() {
+// return 10;
+// }
+// },
+ X {
+ @Override
+ public String toString() {
+ return "X / " + UnitGroup.UNITS_LENGTH.getDefaultUnit().toString();
+ }
+ @Override
+ public String getValue(FreeformFinSet finset, int row) {
+ return UnitGroup.UNITS_LENGTH.getDefaultUnit()
+ .toString(finset.getFinPoints()[row].x);
+ }
+ },
+ Y {
+ @Override
+ public String toString() {
+ return "Y / " + UnitGroup.UNITS_LENGTH.getDefaultUnit().toString();
+ }
+ @Override
+ public String getValue(FreeformFinSet finset, int row) {
+ return UnitGroup.UNITS_LENGTH.getDefaultUnit()
+ .toString(finset.getFinPoints()[row].y);
+ }
+ };
+
+ public abstract String getValue(FreeformFinSet finset, int row);
+ @Override
+ public abstract String toString();
+ public int getWidth() {
+ return 20;
+ }
+ }
+
+ private class FinPointTableModel extends AbstractTableModel {
+
+ @Override
+ public int getColumnCount() {
+ return Columns.values().length;
+ }
+
+ @Override
+ public int getRowCount() {
+ return finset.getPointCount();
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ return Columns.values()[columnIndex].getValue(finset, rowIndex);
+ }
+
+ @Override
+ public String getColumnName(int columnIndex) {
+ return Columns.values()[columnIndex].toString();
+ }
+
+ @Override
+ public boolean isCellEditable(int rowIndex, int columnIndex) {
+ if (rowIndex == 0 || rowIndex == getRowCount()-1) {
+ return (columnIndex == Columns.X.ordinal());
+ }
+
+ return (columnIndex == Columns.X.ordinal() || columnIndex == Columns.Y.ordinal());
+ }
+
+ @Override
+ public void setValueAt(Object o, int rowIndex, int columnIndex) {
+ if (!(o instanceof String))
+ return;
+
+ String str = (String)o;
+ try {
+
+ double value = UnitGroup.UNITS_LENGTH.fromString(str);
+ Coordinate c = finset.getFinPoints()[rowIndex];
+ if (columnIndex == Columns.X.ordinal())
+ c = c.setX(value);
+ else
+ c = c.setY(value);
+
+ finset.setPoint(rowIndex, c.x, c.y);
+
+ } catch (NumberFormatException ignore) {
+ }
+ }
+
+
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.geom.Ellipse2D;
+import java.util.List;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+import javax.swing.border.BevelBorder;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.Resettable;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.rocketcomponent.ClusterConfiguration;
+import net.sf.openrocket.rocketcomponent.Clusterable;
+import net.sf.openrocket.rocketcomponent.InnerTube;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.UnitGroup;
+
+
+public class InnerTubeConfig extends ThicknessRingComponentConfig {
+
+
+ public InnerTubeConfig(RocketComponent c) {
+ super(c);
+
+ JPanel tab;
+
+ tab = positionTab();
+ tabbedPane.insertTab("Radial position", null, tab, "Radial position", 1);
+
+ tab = clusterTab();
+ tabbedPane.insertTab("Cluster", null, tab, "Cluster configuration", 2);
+
+ tab = new MotorConfig((MotorMount)c);
+ tabbedPane.insertTab("Motor", null, tab, "Motor mount configuration", 3);
+
+ tabbedPane.setSelectedIndex(0);
+ }
+
+
+ private JPanel clusterTab() {
+ JPanel panel = new JPanel(new MigLayout());
+
+ JPanel subPanel = new JPanel(new MigLayout());
+
+ // Cluster type selection
+ subPanel.add(new JLabel("Select cluster configuration:"),"spanx, wrap");
+ subPanel.add(new ClusterSelectionPanel((InnerTube)component),"spanx, wrap");
+// JPanel clusterSelection = new ClusterSelectionPanel((InnerTube)component);
+// clusterSelection.setBackground(Color.blue);
+// subPanel.add(clusterSelection);
+
+ panel.add(subPanel);
+
+
+ subPanel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]"));
+
+ // Tube separation scale
+ JLabel l = new JLabel("Tube separation:");
+ l.setToolTipText("The separation of the tubes, 1.0 = touching each other");
+ subPanel.add(l);
+ DoubleModel dm = new DoubleModel(component,"ClusterScale",1,UnitGroup.UNITS_NONE,0);
+
+ JSpinner spin = new JSpinner(dm.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ spin.setToolTipText("The separation of the tubes, 1.0 = touching each other");
+ subPanel.add(spin,"growx");
+
+ BasicSlider bs = new BasicSlider(dm.getSliderModel(0, 1, 4));
+ bs.setToolTipText("The separation of the tubes, 1.0 = touching each other");
+ subPanel.add(bs,"skip,w 100lp, wrap");
+
+ // Rotation
+ l = new JLabel("Rotation:");
+ l.setToolTipText("Rotation angle of the cluster configuration");
+ subPanel.add(l);
+ dm = new DoubleModel(component,"ClusterRotation",1,UnitGroup.UNITS_ANGLE,0);
+
+ spin = new JSpinner(dm.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ spin.setToolTipText("Rotation angle of the cluster configuration");
+ subPanel.add(spin,"growx");
+
+ subPanel.add(new UnitSelector(dm),"growx");
+ bs = new BasicSlider(dm.getSliderModel(-Math.PI, 0, Math.PI));
+ bs.setToolTipText("Rotation angle of the cluster configuration");
+ subPanel.add(bs,"w 100lp, wrap");
+
+ // Reset button
+ JButton reset = new JButton("Reset");
+ reset.setToolTipText("Reset the separation and rotation to the default values");
+ reset.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent arg0) {
+ ((InnerTube)component).setClusterScale(1.0);
+ ((InnerTube)component).setClusterRotation(0.0);
+ }
+ });
+ subPanel.add(reset,"spanx,right");
+
+ panel.add(subPanel,"grow");
+
+
+ return panel;
+ }
+}
+
+
+class ClusterSelectionPanel extends JPanel {
+ private static final int BUTTON_SIZE = 50;
+ private static final int MOTOR_DIAMETER = 10;
+
+ private static final Color SELECTED_COLOR = Color.RED;
+ private static final Color UNSELECTED_COLOR = Color.WHITE;
+ private static final Color MOTOR_FILL_COLOR = Color.GREEN;
+ private static final Color MOTOR_BORDER_COLOR = Color.BLACK;
+
+ public ClusterSelectionPanel(Clusterable component) {
+ super(new MigLayout("gap 0 0",
+ "["+BUTTON_SIZE+"!]["+BUTTON_SIZE+"!]["+BUTTON_SIZE+"!]["+BUTTON_SIZE+"!]",
+ "["+BUTTON_SIZE+"!]["+BUTTON_SIZE+"!]["+BUTTON_SIZE+"!]"));
+
+ for (int i=0; i<ClusterConfiguration.CONFIGURATIONS.length; i++) {
+ ClusterConfiguration config = ClusterConfiguration.CONFIGURATIONS[i];
+
+ JComponent button = new ClusterButton(component,config);
+ if (i%4 == 3)
+ add(button,"wrap");
+ else
+ add(button);
+ }
+
+ }
+
+
+ private class ClusterButton extends JPanel implements ChangeListener, MouseListener,
+ Resettable {
+ private Clusterable component;
+ private ClusterConfiguration config;
+
+ public ClusterButton(Clusterable c, ClusterConfiguration config) {
+ component = c;
+ this.config = config;
+ setMinimumSize(new Dimension(BUTTON_SIZE,BUTTON_SIZE));
+ setPreferredSize(new Dimension(BUTTON_SIZE,BUTTON_SIZE));
+ setMaximumSize(new Dimension(BUTTON_SIZE,BUTTON_SIZE));
+ setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));
+// setBorder(BorderFactory.createLineBorder(Color.BLACK, 1));
+ component.addChangeListener(this);
+ addMouseListener(this);
+ }
+
+
+ @Override
+ public void paintComponent(Graphics g) {
+ super.paintComponent(g);
+ Graphics2D g2 = (Graphics2D)g;
+ Rectangle area = g2.getClipBounds();
+
+ if (component.getClusterConfiguration() == config)
+ g2.setColor(SELECTED_COLOR);
+ else
+ g2.setColor(UNSELECTED_COLOR);
+
+ g2.fillRect(area.x, area.y, area.width, area.height);
+
+ g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
+ RenderingHints.VALUE_STROKE_NORMALIZE);
+ g2.setRenderingHint(RenderingHints.KEY_RENDERING,
+ RenderingHints.VALUE_RENDER_QUALITY);
+ g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON);
+
+ List<Double> points = config.getPoints();
+ Ellipse2D.Float circle = new Ellipse2D.Float();
+ for (int i=0; i < points.size()/2; i++) {
+ double x = points.get(i*2);
+ double y = points.get(i*2+1);
+
+ double px = BUTTON_SIZE/2 + x*MOTOR_DIAMETER;
+ double py = BUTTON_SIZE/2 - y*MOTOR_DIAMETER;
+ circle.setFrameFromCenter(px,py,px+MOTOR_DIAMETER/2,py+MOTOR_DIAMETER/2);
+
+ g2.setColor(MOTOR_FILL_COLOR);
+ g2.fill(circle);
+ g2.setColor(MOTOR_BORDER_COLOR);
+ g2.draw(circle);
+ }
+ }
+
+
+ public void stateChanged(ChangeEvent e) {
+ repaint();
+ }
+
+
+ public void mouseClicked(MouseEvent e) {
+ if (e.getButton() == MouseEvent.BUTTON1) {
+ component.setClusterConfiguration(config);
+ }
+ }
+
+ public void mouseEntered(MouseEvent e) { }
+ public void mouseExited(MouseEvent e) { }
+ public void mousePressed(MouseEvent e) { }
+ public void mouseReleased(MouseEvent e) { }
+
+
+ public void resetModel() {
+ component.removeChangeListener(this);
+ removeMouseListener(this);
+ }
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.UnitGroup;
+
+public class LaunchLugConfig extends RocketComponentConfig {
+
+ private MotorConfig motorConfigPane = null;
+
+ public LaunchLugConfig(RocketComponent c) {
+ super(c);
+
+ JPanel primary = new JPanel(new MigLayout("fill"));
+
+
+ JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::][]",""));
+
+ //// Body tube length
+ panel.add(new JLabel("Length:"));
+
+ DoubleModel m = new DoubleModel(component,"Length",UnitGroup.UNITS_LENGTH,0);
+
+ JSpinner spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0, 0.02, 0.1)),"w 100lp, wrap para");
+
+
+ //// Body tube diameter
+ panel.add(new JLabel("Outer diameter:"));
+
+ DoubleModel od = new DoubleModel(component,"Radius",2,UnitGroup.UNITS_LENGTH,0);
+ // Diameter = 2*Radius
+
+ spin = new JSpinner(od.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(od),"growx");
+ panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)),"w 100lp, wrap rel");
+
+
+ //// Inner diameter
+ panel.add(new JLabel("Inner diameter:"));
+
+ // Diameter = 2*Radius
+ m = new DoubleModel(component,"InnerRadius",2,UnitGroup.UNITS_LENGTH,0);
+
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(new DoubleModel(0), od)),"w 100lp, wrap rel");
+
+
+ //// Wall thickness
+ panel.add(new JLabel("Thickness:"));
+
+ m = new DoubleModel(component,"Thickness",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0,0.01)),"w 100lp, wrap 20lp");
+
+
+ //// Radial direction
+ panel.add(new JLabel("Radial position:"));
+
+ m = new DoubleModel(component,"RadialDirection",UnitGroup.UNITS_ANGLE,
+ -Math.PI, Math.PI);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)),"w 100lp, wrap");
+
+
+
+
+ primary.add(panel, "grow, gapright 20lp");
+ panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::][]",""));
+
+
+
+
+ panel.add(new JLabel("Position relative to:"));
+
+ JComboBox combo = new JComboBox(
+ new EnumModel<RocketComponent.Position>(component, "RelativePosition",
+ new RocketComponent.Position[] {
+ RocketComponent.Position.TOP,
+ RocketComponent.Position.MIDDLE,
+ RocketComponent.Position.BOTTOM,
+ RocketComponent.Position.ABSOLUTE
+ }));
+ panel.add(combo,"spanx, growx, wrap");
+
+ panel.add(new JLabel("plus"),"right");
+
+ m = new DoubleModel(component,"PositionValue",UnitGroup.UNITS_LENGTH);
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(
+ new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE),
+ new DoubleModel(component.getParent(), "Length"))),
+ "w 100lp, wrap para");
+
+
+
+ //// Material
+ materialPanel(panel, Material.Type.BULK);
+
+
+ primary.add(panel,"grow");
+
+
+ tabbedPane.insertTab("General", null, primary, "General properties", 0);
+ tabbedPane.setSelectedIndex(0);
+ }
+
+ @Override
+ public void updateFields() {
+ super.updateFields();
+ if (motorConfigPane != null)
+ motorConfigPane.updateFields();
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.rocketcomponent.MassComponent;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.UnitGroup;
+
+
+public class MassComponentConfig extends RocketComponentConfig {
+
+ public MassComponentConfig(RocketComponent component) {
+ super(component);
+
+
+ JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+
+
+
+ //// Mass
+ panel.add(new JLabel("Mass"));
+
+ DoubleModel m = new DoubleModel(component,"ComponentMass",UnitGroup.UNITS_MASS,0);
+
+ JSpinner spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0, 0.05, 0.5)),"w 100lp, wrap");
+
+
+
+ //// Mass length
+ panel.add(new JLabel("Length"));
+
+ m = new DoubleModel(component,"Length",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 0.5)),"w 100lp, wrap");
+
+
+ //// Tube diameter
+ panel.add(new JLabel("Diameter:"));
+
+ DoubleModel od = new DoubleModel(component,"Radius",2,UnitGroup.UNITS_LENGTH,0);
+ // Diameter = 2*Radius
+
+ spin = new JSpinner(od.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(od),"growx");
+ panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)),"w 100lp, wrap");
+
+
+ //// Position
+
+ panel.add(new JLabel("Position relative to:"));
+
+ JComboBox combo = new JComboBox(
+ new EnumModel<RocketComponent.Position>(component, "RelativePosition",
+ new RocketComponent.Position[] {
+ RocketComponent.Position.TOP,
+ RocketComponent.Position.MIDDLE,
+ RocketComponent.Position.BOTTOM,
+ RocketComponent.Position.ABSOLUTE
+ }));
+ panel.add(combo,"spanx, growx, wrap");
+
+ panel.add(new JLabel("plus"),"right");
+
+ m = new DoubleModel(component,"PositionValue",UnitGroup.UNITS_LENGTH);
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(
+ new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE),
+ new DoubleModel(component.getParent(), "Length"))),
+ "w 100lp, wrap");
+
+
+ tabbedPane.insertTab("General", null, panel, "General properties", 0);
+ tabbedPane.insertTab("Radial position", null, positionTab(),
+ "Radial position configuration", 1);
+ tabbedPane.setSelectedIndex(0);
+ }
+
+
+ protected JPanel positionTab() {
+ JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+
+ //// Radial position
+ panel.add(new JLabel("Radial distance:"));
+
+ DoubleModel m = new DoubleModel(component,"RadialPosition",UnitGroup.UNITS_LENGTH,0);
+
+ JSpinner spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 1.0)),"w 100lp, wrap");
+
+
+ //// Radial direction
+ panel.add(new JLabel("Radial direction:"));
+
+ m = new DoubleModel(component,"RadialDirection",UnitGroup.UNITS_ANGLE,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)),"w 100lp, wrap");
+
+
+ //// Reset button
+ JButton button = new JButton("Reset");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ ((MassComponent) component).setRadialDirection(0.0);
+ ((MassComponent) component).setRadialPosition(0.0);
+ }
+ });
+ panel.add(button,"spanx, right");
+
+ return panel;
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.Component;
+import java.awt.Container;
+import java.awt.Font;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.BooleanModel;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.gui.adaptors.MotorConfigurationModel;
+import net.sf.openrocket.gui.main.MotorChooserDialog;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent;
+import net.sf.openrocket.unit.UnitGroup;
+
+public class MotorConfig extends JPanel {
+
+ private final Rocket rocket;
+ private final MotorMount mount;
+ private final Configuration configuration;
+ private JPanel panel;
+ private JLabel motorLabel;
+
+ public MotorConfig(MotorMount motorMount) {
+ super(new MigLayout("fill"));
+
+ this.rocket = ((RocketComponent)motorMount).getRocket();
+ this.mount = motorMount;
+ this.configuration = ((RocketComponent)motorMount).getRocket()
+ .getDefaultConfiguration();
+
+ BooleanModel model;
+
+ model = new BooleanModel(motorMount, "MotorMount");
+ JCheckBox check = new JCheckBox(model);
+ check.setText("This component is a motor mount");
+ this.add(check,"wrap");
+
+
+ panel = new JPanel(new MigLayout("fill"));
+ this.add(panel,"grow, wrap");
+
+
+ // Motor configuration selector
+ panel.add(new JLabel("Motor configuration:"), "shrink");
+
+ JComboBox combo = new JComboBox(new MotorConfigurationModel(configuration));
+ panel.add(combo,"growx");
+
+ configuration.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ updateFields();
+ }
+ });
+
+ JButton button = new JButton("New");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ String id = rocket.newMotorConfigurationID();
+ configuration.setMotorConfigurationID(id);
+ }
+ });
+ panel.add(button, "wrap unrel");
+
+
+ // Current motor
+ panel.add(new JLabel("Current motor:"), "shrink");
+
+ motorLabel = new JLabel();
+ motorLabel.setFont(motorLabel.getFont().deriveFont(Font.BOLD));
+ updateFields();
+ panel.add(motorLabel,"wrap unrel");
+
+
+
+ // Overhang
+ panel.add(new JLabel("Motor overhang:"));
+
+ DoubleModel m = new DoubleModel(motorMount, "MotorOverhang", UnitGroup.UNITS_LENGTH);
+
+ JSpinner spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"span, split, width :65lp:");
+
+ panel.add(new UnitSelector(m),"width :30lp:");
+ panel.add(new BasicSlider(m.getSliderModel(-0.02,0.06)),"w 100lp, wrap unrel");
+
+
+
+ // Select ignition event
+ panel.add(new JLabel("Ignition at:"),"");
+
+ combo = new JComboBox(new EnumModel<IgnitionEvent>(mount, "IgnitionEvent"));
+ panel.add(combo,"growx, wrap");
+
+ // ... and delay
+ panel.add(new JLabel("plus"),"gap indent, skip 1, span, split");
+
+ m = new DoubleModel(mount,"IgnitionDelay",0);
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"gap rel rel");
+
+ panel.add(new JLabel("seconds"),"wrap paragraph");
+
+
+
+
+ // Select etc. buttons
+ button = new JButton("Select motor");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ String id = configuration.getMotorConfigurationID();
+
+ MotorChooserDialog dialog = new MotorChooserDialog(mount.getMotor(id),
+ mount.getMotorDelay(id), mount.getMotorMountDiameter());
+ dialog.setVisible(true);
+ Motor m = dialog.getSelectedMotor();
+ double d = dialog.getSelectedDelay();
+
+ if (m != null) {
+ if (id == null) {
+ id = rocket.newMotorConfigurationID();
+ configuration.setMotorConfigurationID(id);
+ }
+ mount.setMotor(id, m);
+ mount.setMotorDelay(id, d);
+ }
+ updateFields();
+ }
+ });
+ panel.add(button,"span, split, grow");
+
+ button = new JButton("Remove motor");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ mount.setMotor(configuration.getMotorConfigurationID(), null);
+ updateFields();
+ }
+ });
+ panel.add(button,"grow, wrap");
+
+
+
+
+
+ // Set enabled status
+
+ setDeepEnabled(panel, motorMount.isMotorMount());
+ check.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ setDeepEnabled(panel, mount.isMotorMount());
+ }
+ });
+
+ }
+
+ public void updateFields() {
+ String id = configuration.getMotorConfigurationID();
+ Motor m = mount.getMotor(id);
+ if (m == null)
+ motorLabel.setText("None");
+ else
+ motorLabel.setText(m.getManufacturer() + " " +
+ m.getDesignation(mount.getMotorDelay(id)));
+ }
+
+
+ private static void setDeepEnabled(Component component, boolean enabled) {
+ component.setEnabled(enabled);
+ if (component instanceof Container) {
+ for (Component c: ((Container) component).getComponents()) {
+ setDeepEnabled(c,enabled);
+ }
+ }
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSlider;
+import javax.swing.JSpinner;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.DescriptionArea;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.BooleanModel;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.NoseCone;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.unit.UnitGroup;
+
+public class NoseConeConfig extends RocketComponentConfig {
+
+ private JComboBox typeBox;
+
+ private DescriptionArea description;
+
+ private JLabel shapeLabel;
+ private JSpinner shapeSpinner;
+ private JSlider shapeSlider;
+
+ // Prepended to the description from NoseCone.DESCRIPTIONS
+ private static final String PREDESC = "<html><p style=\"font-size: x-small\">";
+
+ public NoseConeConfig(RocketComponent c) {
+ super(c);
+
+ DoubleModel m;
+ JPanel panel = new JPanel(new MigLayout("","[][65lp::][30lp::]"));
+
+
+
+
+ //// Shape selection
+
+ panel.add(new JLabel("Nose cone shape:"));
+
+ Transition.Shape selected = ((NoseCone)component).getType();
+ Transition.Shape[] typeList = Transition.Shape.values();
+
+ typeBox = new JComboBox(typeList);
+ typeBox.setEditable(false);
+ typeBox.setSelectedItem(selected);
+ typeBox.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ Transition.Shape s = (Transition.Shape)typeBox.getSelectedItem();
+ ((NoseCone)component).setType(s);
+ description.setText(PREDESC + s.getNoseConeDescription());
+ updateEnabled();
+ }
+ });
+ panel.add(typeBox,"span, wrap rel");
+
+
+
+
+ //// Shape parameter
+ shapeLabel = new JLabel("Shape parameter:");
+ panel.add(shapeLabel);
+
+ m = new DoubleModel(component,"ShapeParameter");
+
+ shapeSpinner = new JSpinner(m.getSpinnerModel());
+ shapeSpinner.setEditor(new SpinnerEditor(shapeSpinner));
+ panel.add(shapeSpinner,"growx");
+
+ DoubleModel min = new DoubleModel(component,"ShapeParameterMin");
+ DoubleModel max = new DoubleModel(component,"ShapeParameterMax");
+ shapeSlider = new BasicSlider(m.getSliderModel(min,max));
+ panel.add(shapeSlider,"skip, w 100lp, wrap para");
+
+ updateEnabled();
+
+
+ //// Length
+
+ panel.add(new JLabel("Nose cone length:"));
+
+ m = new DoubleModel(component,"Length",UnitGroup.UNITS_LENGTH,0);
+ JSpinner spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 0.7)),"w 100lp, wrap");
+
+ //// Diameter
+
+ panel.add(new JLabel("Base diameter:"));
+
+ m = new DoubleModel(component,"AftRadius",2.0,UnitGroup.UNITS_LENGTH,0); // Diameter = 2*Radius
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0, 0.04, 0.2)),"w 100lp, wrap 0px");
+
+ JCheckBox check = new JCheckBox(m.getAutomaticAction());
+ check.setText("Automatic");
+ panel.add(check,"skip, span 2, wrap");
+
+
+ //// Wall thickness
+ panel.add(new JLabel("Wall thickness:"));
+
+ m = new DoubleModel(component,"Thickness",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0,0.01)),"w 100lp, wrap 0px");
+
+
+ check = new JCheckBox(new BooleanModel(component,"Filled"));
+ check.setText("Filled");
+ panel.add(check,"skip, span 2, wrap");
+
+
+ panel.add(new JLabel(""), "growy");
+
+
+
+ //// Description
+
+ JPanel panel2 = new JPanel(new MigLayout("ins 0"));
+
+ description = new DescriptionArea(5);
+ description.setText(PREDESC + ((NoseCone)component).getType().getNoseConeDescription());
+ panel2.add(description, "wmin 250lp, spanx, growx, wrap para");
+
+
+ //// Material
+
+
+ materialPanel(panel2, Material.Type.BULK);
+ panel.add(panel2, "cell 4 0, gapleft paragraph, aligny 0%, spany");
+
+
+
+ tabbedPane.insertTab("General", null, panel, "General properties", 0);
+ tabbedPane.insertTab("Shoulder", null, shoulderTab(), "Shoulder properties", 1);
+ tabbedPane.setSelectedIndex(0);
+ }
+
+
+ private void updateEnabled() {
+ boolean e = ((NoseCone)component).getType().usesParameter();
+ shapeLabel.setEnabled(e);
+ shapeSpinner.setEnabled(e);
+ shapeSlider.setEnabled(e);
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.gui.adaptors.IntegerModel;
+import net.sf.openrocket.gui.adaptors.MaterialModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.MassComponent;
+import net.sf.openrocket.rocketcomponent.Parachute;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent;
+import net.sf.openrocket.unit.UnitGroup;
+
+public class ParachuteConfig extends RecoveryDeviceConfig {
+
+ public ParachuteConfig(final RocketComponent component) {
+ super(component);
+
+ JPanel primary = new JPanel(new MigLayout());
+
+ JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::][]",""));
+
+
+ //// Canopy
+ panel.add(new JLabel("<html><b>Canopy:</b>"), "wrap unrel");
+
+
+ panel.add(new JLabel("Diameter:"));
+
+ DoubleModel m = new DoubleModel(component,"Diameter",UnitGroup.UNITS_LENGTH,0);
+
+ JSpinner spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0, 0.4, 1.5)),"w 100lp, wrap");
+
+
+ panel.add(new JLabel("Material:"));
+
+ JComboBox combo = new JComboBox(new MaterialModel(component, Material.Type.SURFACE));
+ combo.setToolTipText("The component material affects the weight of the component.");
+ panel.add(combo,"spanx 3, growx, wrap paragraph");
+
+// materialPanel(panel, Material.Type.SURFACE, "Material:", null);
+
+
+
+ // CD
+ JLabel label = new JLabel("<html>Drag coefficient C<sub>D</sub>:");
+ String tip = "<html>The drag coefficient relative to the total area of the parachute.<br>" +
+ "A larger drag coefficient yields a slowed descent rate. " +
+ "A typical value for parachutes is 0.8.";
+ label.setToolTipText(tip);
+ panel.add(label);
+
+ m = new DoubleModel(component,"CD",UnitGroup.UNITS_COEFFICIENT,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setToolTipText(tip);
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ JButton button = new JButton("Reset");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ Parachute p = (Parachute)component;
+ p.setCD(Parachute.DEFAULT_CD);
+ }
+ });
+ panel.add(button,"spanx, wrap 30lp");
+
+
+
+ //// Shroud lines
+ panel.add(new JLabel("<html><b>Shroud lines:</b>"), "wrap unrel");
+
+
+ panel.add(new JLabel("Number of lines:"));
+ IntegerModel im = new IntegerModel(component,"LineCount",0);
+
+ spin = new JSpinner(im.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx, wrap");
+
+
+ panel.add(new JLabel("Line length:"));
+
+ m = new DoubleModel(component,"LineLength",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0, 0.4, 1.5)),"w 100lp, wrap");
+
+
+ panel.add(new JLabel("Material:"));
+
+ combo = new JComboBox(new MaterialModel(component, Material.Type.LINE,
+ "LineMaterial"));
+ panel.add(combo,"spanx 3, growx, wrap");
+
+
+
+ primary.add(panel, "grow, gapright 20lp");
+ panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::][]",""));
+
+
+
+
+ //// Position
+
+ panel.add(new JLabel("Position relative to:"));
+
+ combo = new JComboBox(
+ new EnumModel<RocketComponent.Position>(component, "RelativePosition",
+ new RocketComponent.Position[] {
+ RocketComponent.Position.TOP,
+ RocketComponent.Position.MIDDLE,
+ RocketComponent.Position.BOTTOM,
+ RocketComponent.Position.ABSOLUTE
+ }));
+ panel.add(combo,"spanx, growx, wrap");
+
+ panel.add(new JLabel("plus"),"right");
+
+ m = new DoubleModel(component,"PositionValue",UnitGroup.UNITS_LENGTH);
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(
+ new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE),
+ new DoubleModel(component.getParent(), "Length"))),
+ "w 100lp, wrap");
+
+
+ //// Spatial length
+ panel.add(new JLabel("Packed length:"));
+
+ m = new DoubleModel(component,"Length",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 0.5)),"w 100lp, wrap");
+
+
+ //// Tube diameter
+ panel.add(new JLabel("Packed diameter:"));
+
+ DoubleModel od = new DoubleModel(component,"Radius",2,UnitGroup.UNITS_LENGTH,0);
+ // Diameter = 2*Radius
+
+ spin = new JSpinner(od.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(od),"growx");
+ panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)),"w 100lp, wrap 30lp");
+
+
+ //// Deployment
+
+ panel.add(new JLabel("Deploys at:"),"");
+
+ combo = new JComboBox(new EnumModel<IgnitionEvent>(component, "DeployEvent"));
+ panel.add(combo,"spanx 3, growx, wrap");
+
+ // ... and delay
+ panel.add(new JLabel("plus"),"right");
+
+ m = new DoubleModel(component,"DeployDelay",0);
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"spanx, split");
+
+ panel.add(new JLabel("seconds"),"wrap paragraph");
+
+ // Altitude
+ label = new JLabel("Altitude:");
+ altitudeComponents.add(label);
+ panel.add(label);
+
+ m = new DoubleModel(component,"DeployAltitude",UnitGroup.UNITS_DISTANCE,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ altitudeComponents.add(spin);
+ panel.add(spin,"growx");
+ UnitSelector unit = new UnitSelector(m);
+ altitudeComponents.add(unit);
+ panel.add(unit,"growx");
+ BasicSlider slider = new BasicSlider(m.getSliderModel(100, 1000));
+ altitudeComponents.add(slider);
+ panel.add(slider,"w 100lp, wrap");
+
+
+ primary.add(panel, "grow");
+
+ updateFields();
+
+ tabbedPane.insertTab("General", null, primary, "General properties", 0);
+ tabbedPane.insertTab("Radial position", null, positionTab(),
+ "Radial position configuration", 1);
+ tabbedPane.setSelectedIndex(0);
+ }
+
+
+
+
+
+ protected JPanel positionTab() {
+ JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+
+ //// Radial position
+ panel.add(new JLabel("Radial distance:"));
+
+ DoubleModel m = new DoubleModel(component,"RadialPosition",UnitGroup.UNITS_LENGTH,0);
+
+ JSpinner spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 1.0)),"w 100lp, wrap");
+
+
+ //// Radial direction
+ panel.add(new JLabel("Radial direction:"));
+
+ m = new DoubleModel(component,"RadialDirection",UnitGroup.UNITS_ANGLE,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)),"w 100lp, wrap");
+
+
+ //// Reset button
+ JButton button = new JButton("Reset");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ ((MassComponent) component).setRadialDirection(0.0);
+ ((MassComponent) component).setRadialPosition(0.0);
+ }
+ });
+ panel.add(button,"spanx, right");
+
+ return panel;
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.JComponent;
+
+import net.sf.openrocket.rocketcomponent.RecoveryDevice;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+
+public abstract class RecoveryDeviceConfig extends RocketComponentConfig {
+
+ protected final List<JComponent> altitudeComponents = new ArrayList<JComponent>();
+
+ public RecoveryDeviceConfig(RocketComponent component) {
+ super(component);
+ }
+
+
+
+ @Override
+ public void updateFields() {
+ super.updateFields();
+
+ if (altitudeComponents == null)
+ return;
+
+ boolean enabled = (((RecoveryDevice)component).getDeployEvent()
+ == RecoveryDevice.DeployEvent.ALTITUDE);
+
+ for (JComponent c: altitudeComponents) {
+ c.setEnabled(enabled);
+ }
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.RingComponent;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.UnitGroup;
+
+public class RingComponentConfig extends RocketComponentConfig {
+
+ public RingComponentConfig(RocketComponent component) {
+ super(component);
+ }
+
+
+ protected JPanel generalTab(String outer, String inner, String thickness, String length) {
+ JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+ DoubleModel m;
+ JSpinner spin;
+ DoubleModel od=null;
+
+
+ //// Outer diameter
+ if (outer != null) {
+ panel.add(new JLabel(outer));
+
+ od = new DoubleModel(component,"OuterRadius",2,UnitGroup.UNITS_LENGTH,0);
+ // Diameter = 2*Radius
+
+ spin = new JSpinner(od.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(od),"growx");
+ panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)),"w 100lp, wrap");
+
+ if (od.isAutomaticAvailable()) {
+ JCheckBox check = new JCheckBox(od.getAutomaticAction());
+ check.setText("Automatic");
+ panel.add(check,"skip, span 2, wrap");
+ }
+ }
+
+
+ //// Inner diameter
+ if (inner != null) {
+ panel.add(new JLabel(inner));
+
+ m = new DoubleModel(component,"InnerRadius",2,UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ if (od == null)
+ panel.add(new BasicSlider(m.getSliderModel(0, 0.04, 0.2)), "w 100lp, wrap");
+ else
+ panel.add(new BasicSlider(m.getSliderModel(new DoubleModel(0), od)),
+ "w 100lp, wrap");
+
+ if (m.isAutomaticAvailable()) {
+ JCheckBox check = new JCheckBox(m.getAutomaticAction());
+ check.setText("Automatic");
+ panel.add(check,"skip, span 2, wrap");
+ }
+ }
+
+
+ //// Wall thickness
+ if (thickness != null) {
+ panel.add(new JLabel(thickness));
+
+ m = new DoubleModel(component,"Thickness",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0,0.01)),"w 100lp, wrap");
+ }
+
+
+ //// Inner tube length
+ if (length != null) {
+ panel.add(new JLabel(length));
+
+ m = new DoubleModel(component,"Length",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 1.0)),"w 100lp, wrap");
+ }
+
+
+ //// Position
+
+ panel.add(new JLabel("Position relative to:"));
+
+ JComboBox combo = new JComboBox(
+ new EnumModel<RocketComponent.Position>(component, "RelativePosition",
+ new RocketComponent.Position[] {
+ RocketComponent.Position.TOP,
+ RocketComponent.Position.MIDDLE,
+ RocketComponent.Position.BOTTOM,
+ RocketComponent.Position.ABSOLUTE
+ }));
+ panel.add(combo,"spanx 3, growx, wrap");
+
+ panel.add(new JLabel("plus"),"right");
+
+ m = new DoubleModel(component,"PositionValue",UnitGroup.UNITS_LENGTH);
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(
+ new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE),
+ new DoubleModel(component.getParent(), "Length"))),
+ "w 100lp, wrap");
+
+
+ //// Material
+ panel.add(materialPanel(new JPanel(new MigLayout()), Material.Type.BULK),
+ "cell 4 0, gapleft paragraph, aligny 0%, spany");
+
+ return panel;
+ }
+
+
+ protected JPanel positionTab() {
+ JPanel panel = new JPanel(new MigLayout("align 20% 20%, gap rel unrel",
+ "[][65lp::][30lp::]",""));
+
+ //// Radial position
+ JLabel l = new JLabel("Radial distance:");
+ l.setToolTipText("Distance from the rocket centerline");
+ panel.add(l);
+
+ DoubleModel m = new DoubleModel(component,"RadialPosition",UnitGroup.UNITS_LENGTH,0);
+
+ JSpinner spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ spin.setToolTipText("Distance from the rocket centerline");
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ BasicSlider bs = new BasicSlider(m.getSliderModel(0, 0.1, 1.0));
+ bs.setToolTipText("Distance from the rocket centerline");
+ panel.add(bs,"w 100lp, wrap");
+
+
+ //// Radial direction
+ l = new JLabel("Radial direction:");
+ l.setToolTipText("The radial direction from the rocket centerline");
+ panel.add(l);
+
+ m = new DoubleModel(component,"RadialDirection",UnitGroup.UNITS_ANGLE,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ spin.setToolTipText("The radial direction from the rocket centerline");
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ bs = new BasicSlider(m.getSliderModel(-Math.PI, Math.PI));
+ bs.setToolTipText("The radial direction from the rocket centerline");
+ panel.add(bs,"w 100lp, wrap");
+
+
+ //// Reset button
+ JButton button = new JButton("Reset");
+ button.setToolTipText("Reset the component to the rocket centerline");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ ((RingComponent) component).setRadialDirection(0.0);
+ ((RingComponent) component).setRadialPosition(0.0);
+ }
+ });
+ panel.add(button,"spanx, right");
+
+
+ return panel;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Graphics;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+import java.util.Iterator;
+
+import javax.swing.BorderFactory;
+import javax.swing.Icon;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JColorChooser;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSpinner;
+import javax.swing.JTabbedPane;
+import javax.swing.JTextArea;
+import javax.swing.JTextField;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.ResizeLabel;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.BooleanModel;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.gui.adaptors.MaterialModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.ComponentAssembly;
+import net.sf.openrocket.rocketcomponent.ExternalComponent;
+import net.sf.openrocket.rocketcomponent.NoseCone;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.ExternalComponent.Finish;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.GUIUtil;
+import net.sf.openrocket.util.LineStyle;
+import net.sf.openrocket.util.Prefs;
+
+public class RocketComponentConfig extends JPanel {
+
+ protected final RocketComponent component;
+ protected final JTabbedPane tabbedPane;
+
+
+ protected final JTextField componentNameField;
+ protected JTextArea commentTextArea;
+ private final TextFieldListener textFieldListener;
+ private JButton colorButton;
+ private JCheckBox colorDefault;
+ private JPanel buttonPanel;
+
+ private JLabel massLabel;
+
+
+ public RocketComponentConfig(RocketComponent component) {
+ setLayout(new MigLayout("fill","[grow, fill]"));
+ this.component = component;
+
+ JLabel label = new JLabel("Component name:");
+ label.setToolTipText("The component name.");
+ this.add(label,"split, gapright 10");
+
+ componentNameField = new JTextField(15);
+ textFieldListener = new TextFieldListener();
+ componentNameField.addActionListener(textFieldListener);
+ componentNameField.addFocusListener(textFieldListener);
+ componentNameField.setToolTipText("The component name.");
+ this.add(componentNameField,"growx, growy 0, wrap");
+
+
+ tabbedPane = new JTabbedPane();
+ this.add(tabbedPane,"growx, growy 1, wrap");
+
+ tabbedPane.addTab("Override", null, overrideTab(), "Mass and CG override options");
+ if (component.isMassive())
+ tabbedPane.addTab("Figure", null, figureTab(), "Figure style options");
+ tabbedPane.addTab("Comment", null, commentTab(), "Specify a comment for the component");
+
+ addButtons();
+
+ updateFields();
+ }
+
+
+ protected void addButtons(JButton... buttons) {
+ if (buttonPanel != null) {
+ this.remove(buttonPanel);
+ }
+
+ buttonPanel = new JPanel(new MigLayout("fill, ins 0"));
+
+ massLabel = new ResizeLabel("Mass: ", -1);
+ buttonPanel.add(massLabel, "growx");
+
+ for (JButton b: buttons) {
+ buttonPanel.add(b, "right, gap para");
+ }
+
+ JButton closeButton = new JButton("Close");
+ closeButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent arg0) {
+ ComponentConfigDialog.hideDialog();
+ }
+ });
+ buttonPanel.add(closeButton, "right, gap 30lp");
+
+ updateFields();
+
+ this.add(buttonPanel, "spanx, growx");
+ }
+
+
+ /**
+ * Called when a change occurs, so that the fields can be updated if necessary.
+ * When overriding this method, the supermethod must always be called.
+ */
+ public void updateFields() {
+ // Component name
+ componentNameField.setText(component.getName());
+
+ // Component color and "Use default color" checkbox
+ if (colorButton != null && colorDefault != null) {
+ colorButton.setIcon(new ColorIcon(component.getColor()));
+
+ if ((component.getColor()==null) != colorDefault.isSelected())
+ colorDefault.setSelected(component.getColor()==null);
+ }
+
+ // Mass label
+ if (component.isMassive()) {
+ String text = "Component mass: ";
+ text += UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(
+ component.getComponentMass());
+
+ String overridetext = null;
+ if (component.isMassOverridden()) {
+ overridetext = "(overridden to " + UnitGroup.UNITS_MASS.getDefaultUnit().
+ toStringUnit(component.getOverrideMass()) + ")";
+ }
+
+ for (RocketComponent c = component.getParent(); c != null; c = c.getParent()) {
+ if (c.isMassOverridden() && c.getOverrideSubcomponents()) {
+ overridetext = "(overridden by " + c.getName() + ")";
+ }
+ }
+
+ if (overridetext != null)
+ text = text + " " + overridetext;
+
+ massLabel.setText(text);
+ } else {
+ massLabel.setText("");
+ }
+ }
+
+
+ protected JPanel materialPanel(JPanel panel, Material.Type type) {
+ return materialPanel(panel, type, "Component material:", "Component finish:");
+ }
+
+ protected JPanel materialPanel(JPanel panel, Material.Type type,
+ String materialString, String finishString) {
+ JLabel label = new JLabel(materialString);
+ label.setToolTipText("The component material affects the weight of the component.");
+ panel.add(label,"spanx 4, wrap rel");
+
+ JComboBox combo = new JComboBox(new MaterialModel(component,type));
+ combo.setToolTipText("The component material affects the weight of the component.");
+ panel.add(combo,"spanx 4, growx, wrap paragraph");
+
+
+ if (component instanceof ExternalComponent) {
+ label = new JLabel(finishString);
+ String tip = "<html>The component finish affects the aerodynamic drag of the "
+ +"component.<br>"
+ + "The value indicated is the average roughness height of the surface.";
+ label.setToolTipText(tip);
+ panel.add(label,"spanx 4, wmin 220lp, wrap rel");
+
+ combo = new JComboBox(new EnumModel<ExternalComponent.Finish>(component,"Finish"));
+ combo.setToolTipText(tip);
+ panel.add(combo,"spanx 4, growx, split");
+
+ JButton button = new JButton("Set for all");
+ button.setToolTipText("Set this finish for all components of the rocket.");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ Finish f = ((ExternalComponent)component).getFinish();
+ Rocket rocket = component.getRocket();
+ try {
+ rocket.freeze();
+ // Store previous undo description
+ String desc = ComponentConfigDialog.getUndoDescription();
+ ComponentConfigDialog.addUndoPosition("Set rocket finish");
+ // Do changes
+ Iterator<RocketComponent> iter = rocket.deepIterator();
+ while (iter.hasNext()) {
+ RocketComponent c = iter.next();
+ if (c instanceof ExternalComponent) {
+ ((ExternalComponent)c).setFinish(f);
+ }
+ }
+ // Restore undo description
+ ComponentConfigDialog.addUndoPosition(desc);
+ } finally {
+ rocket.thaw();
+ }
+ }
+ });
+ panel.add(button, "wrap paragraph");
+ }
+
+ return panel;
+ }
+
+
+ private JPanel overrideTab() {
+ JPanel panel = new JPanel(new MigLayout("align 50% 20%, fillx, gap rel unrel",
+ "[][65lp::][30lp::][]",""));
+
+ panel.add(new JLabel("Override the mass or center of gravity of the " +
+ component.getComponentName() + ":"),"spanx, wrap 20lp");
+
+ JCheckBox check;
+ BooleanModel bm;
+ UnitSelector us;
+ BasicSlider bs;
+
+ //// Mass
+ bm = new BooleanModel(component, "MassOverridden");
+ check = new JCheckBox(bm);
+ check.setText("Override mass:");
+ panel.add(check, "growx 1, gapright 20lp");
+
+ DoubleModel m = new DoubleModel(component,"OverrideMass",UnitGroup.UNITS_MASS,0);
+
+ JSpinner spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ bm.addEnableComponent(spin, true);
+ panel.add(spin,"growx 1");
+
+ us = new UnitSelector(m);
+ bm.addEnableComponent(us, true);
+ panel.add(us,"growx 1");
+
+ bs = new BasicSlider(m.getSliderModel(0, 0.03, 1.0));
+ bm.addEnableComponent(bs);
+ panel.add(bs,"growx 5, w 100lp, wrap");
+
+
+ //// CG override
+ bm = new BooleanModel(component, "CGOverridden");
+ check = new JCheckBox(bm);
+ check.setText("Override center of gravity:");
+ panel.add(check, "growx 1, gapright 20lp");
+
+ m = new DoubleModel(component,"OverrideCGX",UnitGroup.UNITS_LENGTH,0);
+ // Calculate suitable length for slider
+ DoubleModel length;
+ if (component instanceof ComponentAssembly) {
+ double l=0;
+
+ Iterator<RocketComponent> iterator = component.deepIterator();
+ while (iterator.hasNext()) {
+ RocketComponent c = iterator.next();
+ if (c.getRelativePosition() == RocketComponent.Position.AFTER)
+ l += c.getLength();
+ }
+ length = new DoubleModel(l);
+ } else {
+ length = new DoubleModel(component, "Length", UnitGroup.UNITS_LENGTH,0);
+ }
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ bm.addEnableComponent(spin, true);
+ panel.add(spin,"growx 1");
+
+ us = new UnitSelector(m);
+ bm.addEnableComponent(us, true);
+ panel.add(us,"growx 1");
+
+ bs = new BasicSlider(m.getSliderModel(new DoubleModel(0), length));
+ bm.addEnableComponent(bs);
+ panel.add(bs,"growx 5, w 100lp, wrap 35lp");
+
+
+ // Override subcomponents checkbox
+ bm = new BooleanModel(component, "OverrideSubcomponents");
+ check = new JCheckBox(bm);
+ check.setText("Override mass and CG of all subcomponents");
+ panel.add(check, "gap para, spanx, wrap para");
+
+
+ panel.add(new ResizeLabel("<html>The overridden mass does not include motors.<br>" +
+ "The center of gravity is measured from the front end of the " +
+ component.getComponentName().toLowerCase()+".", -1),
+ "spanx, wrap, gap para, height 0::30lp");
+
+ return panel;
+ }
+
+
+ private JPanel commentTab() {
+ JPanel panel = new JPanel(new MigLayout("fill"));
+
+ panel.add(new JLabel("Comments on the "+component.getComponentName()+":"), "wrap");
+
+ // TODO: LOW: Changes in comment from other sources not reflected in component
+ commentTextArea = new JTextArea(component.getComment());
+ commentTextArea.setLineWrap(true);
+ commentTextArea.setWrapStyleWord(true);
+ commentTextArea.setEditable(true);
+ GUIUtil.setTabToFocusing(commentTextArea);
+ commentTextArea.addFocusListener(textFieldListener);
+
+ panel.add(new JScrollPane(commentTextArea), "growx, growy");
+
+ return panel;
+ }
+
+
+
+ private JPanel figureTab() {
+ JPanel panel = new JPanel(new MigLayout("align 20% 20%"));
+
+ panel.add(new JLabel("Figure style:"), "wrap para");
+
+
+ panel.add(new JLabel("Component color:"), "gapleft para, gapright 10lp");
+
+ colorButton = new JButton(new ColorIcon(component.getColor()));
+ colorButton.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ Color c = component.getColor();
+ if (c == null) {
+ c = Prefs.getDefaultColor(component.getClass());
+ }
+
+ c = JColorChooser.showDialog(tabbedPane, "Choose color", c);
+ if (c!=null) {
+ component.setColor(c);
+ }
+ }
+ });
+ panel.add(colorButton, "gapright 10lp");
+
+ colorDefault = new JCheckBox("Use default color");
+ if (component.getColor()==null)
+ colorDefault.setSelected(true);
+ colorDefault.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ if (colorDefault.isSelected())
+ component.setColor(null);
+ else
+ component.setColor(Prefs.getDefaultColor(component.getClass()));
+ }
+ });
+ panel.add(colorDefault, "wrap para");
+
+
+ panel.add(new JLabel("Component line style:"), "gapleft para, gapright 10lp");
+
+ LineStyle[] list = new LineStyle[LineStyle.values().length+1];
+ System.arraycopy(LineStyle.values(), 0, list, 1, LineStyle.values().length);
+
+ JComboBox combo = new JComboBox(new EnumModel<LineStyle>(component, "LineStyle",
+ list, "Default style"));
+ panel.add(combo, "spanx 2, growx, wrap 50lp");
+
+
+ JButton button = new JButton("Save as default style");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ if (component.getColor() != null) {
+ Prefs.setDefaultColor(component.getClass(), component.getColor());
+ component.setColor(null);
+ }
+ if (component.getLineStyle() != null) {
+ Prefs.setDefaultLineStyle(component.getClass(), component.getLineStyle());
+ component.setLineStyle(null);
+ }
+ }
+ });
+ panel.add(button, "gapleft para, spanx 3, growx, wrap");
+
+ return panel;
+ }
+
+
+
+
+ protected JPanel shoulderTab() {
+ JPanel panel = new JPanel(new MigLayout("fill"));
+ JPanel sub;
+ DoubleModel m, m2;
+ DoubleModel m0 = new DoubleModel(0);
+ BooleanModel bm;
+ JCheckBox check;
+ JSpinner spin;
+
+
+ //// Fore shoulder, not for NoseCone
+
+ if (!(component instanceof NoseCone)) {
+ sub = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+
+ sub.setBorder(BorderFactory.createTitledBorder("Fore shoulder"));
+
+
+ //// Radius
+ sub.add(new JLabel("Diameter:"));
+
+ m = new DoubleModel(component,"ForeShoulderRadius",2,UnitGroup.UNITS_LENGTH,0);
+ m2 = new DoubleModel(component,"ForeRadius",2,UnitGroup.UNITS_LENGTH);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ sub.add(spin,"growx");
+
+ sub.add(new UnitSelector(m),"growx");
+ sub.add(new BasicSlider(m.getSliderModel(m0, m2)),"w 100lp, wrap");
+
+
+ //// Length
+ sub.add(new JLabel("Length:"));
+
+ m = new DoubleModel(component,"ForeShoulderLength",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ sub.add(spin,"growx");
+
+ sub.add(new UnitSelector(m),"growx");
+ sub.add(new BasicSlider(m.getSliderModel(0, 0.02, 0.2)),"w 100lp, wrap");
+
+
+ //// Thickness
+ sub.add(new JLabel("Thickness:"));
+
+ m = new DoubleModel(component,"ForeShoulderThickness",UnitGroup.UNITS_LENGTH,0);
+ m2 = new DoubleModel(component,"ForeShoulderRadius",UnitGroup.UNITS_LENGTH);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ sub.add(spin,"growx");
+
+ sub.add(new UnitSelector(m),"growx");
+ sub.add(new BasicSlider(m.getSliderModel(m0, m2)),"w 100lp, wrap");
+
+
+ //// Capped
+ bm = new BooleanModel(component, "ForeShoulderCapped");
+ check = new JCheckBox(bm);
+ check.setText("End capped");
+ check.setToolTipText("Whether the end of the shoulder is capped.");
+ sub.add(check, "spanx");
+
+
+ panel.add(sub);
+ }
+
+
+ //// Aft shoulder
+ sub = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+
+ if (component instanceof NoseCone)
+ sub.setBorder(BorderFactory.createTitledBorder("Nose cone shoulder"));
+ else
+ sub.setBorder(BorderFactory.createTitledBorder("Aft shoulder"));
+
+
+ //// Radius
+ sub.add(new JLabel("Diameter:"));
+
+ m = new DoubleModel(component,"AftShoulderRadius",2,UnitGroup.UNITS_LENGTH,0);
+ m2 = new DoubleModel(component,"AftRadius",2,UnitGroup.UNITS_LENGTH);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ sub.add(spin,"growx");
+
+ sub.add(new UnitSelector(m),"growx");
+ sub.add(new BasicSlider(m.getSliderModel(m0, m2)),"w 100lp, wrap");
+
+
+ //// Length
+ sub.add(new JLabel("Length:"));
+
+ m = new DoubleModel(component,"AftShoulderLength",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ sub.add(spin,"growx");
+
+ sub.add(new UnitSelector(m),"growx");
+ sub.add(new BasicSlider(m.getSliderModel(0, 0.02, 0.2)),"w 100lp, wrap");
+
+
+ //// Thickness
+ sub.add(new JLabel("Thickness:"));
+
+ m = new DoubleModel(component,"AftShoulderThickness",UnitGroup.UNITS_LENGTH,0);
+ m2 = new DoubleModel(component,"AftShoulderRadius",UnitGroup.UNITS_LENGTH);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ sub.add(spin,"growx");
+
+ sub.add(new UnitSelector(m),"growx");
+ sub.add(new BasicSlider(m.getSliderModel(m0, m2)),"w 100lp, wrap");
+
+
+ //// Capped
+ bm = new BooleanModel(component, "AftShoulderCapped");
+ check = new JCheckBox(bm);
+ check.setText("End capped");
+ check.setToolTipText("Whether the end of the shoulder is capped.");
+ sub.add(check, "spanx");
+
+
+ panel.add(sub);
+
+
+ return panel;
+ }
+
+
+
+
+ /*
+ * Private inner class to handle events in componentNameField.
+ */
+ private class TextFieldListener implements ActionListener, FocusListener {
+ public void actionPerformed(ActionEvent e) {
+ setName();
+ }
+ public void focusGained(FocusEvent e) { }
+ public void focusLost(FocusEvent e) {
+ setName();
+ }
+ private void setName() {
+ if (!component.getName().equals(componentNameField.getText())) {
+ component.setName(componentNameField.getText());
+ }
+ if (!component.getComment().equals(commentTextArea.getText())) {
+ component.setComment(commentTextArea.getText());
+ }
+ }
+ }
+
+
+ private class ColorIcon implements Icon {
+ private final Color color;
+
+ public ColorIcon(Color c) {
+ this.color = c;
+ }
+
+ @Override
+ public int getIconHeight() {
+ return 15;
+ }
+
+ @Override
+ public int getIconWidth() {
+ return 25;
+ }
+
+ @Override
+ public void paintIcon(Component c, Graphics g, int x, int y) {
+ if (color==null) {
+ g.setColor(Prefs.getDefaultColor(component.getClass()));
+ } else {
+ g.setColor(color);
+ }
+ g.fill3DRect(x, y, getIconWidth(), getIconHeight(), false);
+ }
+
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+
+import javax.swing.JLabel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.util.GUIUtil;
+
+public class RocketConfig extends RocketComponentConfig {
+
+ private TextFieldListener textFieldListener;
+
+ private JTextArea designerTextArea;
+ private JTextArea revisionTextArea;
+
+ private final Rocket rocket;
+
+ public RocketConfig(RocketComponent c) {
+ super(c);
+
+ rocket = (Rocket)c;
+
+ this.removeAll();
+ setLayout(new MigLayout("fill"));
+
+ this.add(new JLabel("Design name:"), "top, pad 4lp, gapright 10lp");
+ this.add(componentNameField, "growx, wrap para");
+
+
+
+ this.add(new JLabel("Designer:"), "top, pad 4lp, gapright 10lp");
+
+ textFieldListener = new TextFieldListener();
+ designerTextArea = new JTextArea(rocket.getDesigner());
+ designerTextArea.setLineWrap(true);
+ designerTextArea.setWrapStyleWord(true);
+ designerTextArea.setEditable(true);
+ GUIUtil.setTabToFocusing(designerTextArea);
+ designerTextArea.addFocusListener(textFieldListener);
+ this.add(new JScrollPane(designerTextArea), "wmin 300lp, hmin 45lp, grow 30, wrap para");
+
+
+ this.add(new JLabel("Comments:"), "top, pad 4lp, gapright 10lp");
+ this.add(new JScrollPane(commentTextArea), "wmin 300lp, hmin 105lp, grow 100, wrap para");
+
+
+ this.add(new JLabel("Revision history:"), "top, pad 4lp, gapright 10lp");
+ revisionTextArea = new JTextArea(rocket.getRevision());
+ revisionTextArea.setLineWrap(true);
+ revisionTextArea.setWrapStyleWord(true);
+ revisionTextArea.setEditable(true);
+ GUIUtil.setTabToFocusing(revisionTextArea);
+ revisionTextArea.addFocusListener(textFieldListener);
+
+ this.add(new JScrollPane(revisionTextArea), "wmin 300lp, hmin 45lp, grow 30, wrap para");
+
+
+ addButtons();
+ }
+
+
+
+ private class TextFieldListener implements ActionListener, FocusListener {
+ public void actionPerformed(ActionEvent e) {
+ setName();
+ }
+ public void focusGained(FocusEvent e) { }
+ public void focusLost(FocusEvent e) {
+ setName();
+ }
+ private void setName() {
+ if (!rocket.getDesigner().equals(designerTextArea.getText())) {
+ rocket.setDesigner(designerTextArea.getText());
+ }
+ if (!rocket.getRevision().equals(revisionTextArea.getText())) {
+ rocket.setRevision(revisionTextArea.getText());
+ }
+ }
+ }
+
+
+
+}
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.unit.UnitGroup;
+
+public class ShockCordConfig extends RocketComponentConfig {
+
+
+ public ShockCordConfig(RocketComponent component) {
+ super(component);
+
+ JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+ JLabel label;
+ DoubleModel m;
+ JSpinner spin;
+ String tip;
+
+
+ ////// Left side
+
+ // Cord length
+ label = new JLabel("Shock cord length");
+ panel.add(label);
+
+ m = new DoubleModel(component,"CordLength",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0, 1, 10)),"w 100lp, wrap");
+
+
+ // Material
+ materialPanel(panel, Material.Type.LINE, "Shock cord material:", null);
+
+
+
+ ///// Right side
+ JPanel panel2 = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+ panel.add(panel2, "cell 4 0, gapleft paragraph, aligny 0%, spany");
+
+
+ //// Position
+
+ panel2.add(new JLabel("Position relative to:"));
+
+ JComboBox combo = new JComboBox(
+ new EnumModel<RocketComponent.Position>(component, "RelativePosition",
+ new RocketComponent.Position[] {
+ RocketComponent.Position.TOP,
+ RocketComponent.Position.MIDDLE,
+ RocketComponent.Position.BOTTOM,
+ RocketComponent.Position.ABSOLUTE
+ }));
+ panel2.add(combo,"spanx, growx, wrap");
+
+ panel2.add(new JLabel("plus"),"right");
+
+ m = new DoubleModel(component,"PositionValue",UnitGroup.UNITS_LENGTH);
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel2.add(spin,"growx");
+
+ panel2.add(new UnitSelector(m),"growx");
+ panel2.add(new BasicSlider(m.getSliderModel(
+ new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE),
+ new DoubleModel(component.getParent(), "Length"))),
+ "w 100lp, wrap");
+
+
+ //// Spatial length
+ panel2.add(new JLabel("Packed length:"));
+
+ m = new DoubleModel(component,"Length",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel2.add(spin,"growx");
+
+ panel2.add(new UnitSelector(m),"growx");
+ panel2.add(new BasicSlider(m.getSliderModel(0, 0.1, 0.5)),"w 100lp, wrap");
+
+
+ //// Tube diameter
+ panel2.add(new JLabel("Packed diameter:"));
+
+ DoubleModel od = new DoubleModel(component,"Radius",2,UnitGroup.UNITS_LENGTH,0);
+ // Diameter = 2*Radius
+
+ spin = new JSpinner(od.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel2.add(spin,"growx");
+
+ panel2.add(new UnitSelector(od),"growx");
+ panel2.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)),"w 100lp, wrap");
+
+
+
+
+ tabbedPane.insertTab("General", null, panel, "General properties", 0);
+// tabbedPane.insertTab("Radial position", null, positionTab(),
+// "Radial position configuration", 1);
+ tabbedPane.setSelectedIndex(0);
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+
+import javax.swing.JPanel;
+
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+
+
+public class SleeveConfig extends RingComponentConfig {
+
+ public SleeveConfig(RocketComponent c) {
+ super(c);
+
+ JPanel tab;
+
+ tab = generalTab("Outer diameter:", "Inner diameter:", "Wall thickness:", "Length:");
+ tabbedPane.insertTab("General", null, tab, "General properties", 0);
+ tabbedPane.setSelectedIndex(0);
+ }
+
+}
\ No newline at end of file
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.ResizeLabel;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.gui.adaptors.MaterialModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.MassComponent;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent;
+import net.sf.openrocket.unit.UnitGroup;
+
+public class StreamerConfig extends RecoveryDeviceConfig {
+
+ public StreamerConfig(final RocketComponent component) {
+ super(component);
+
+ JPanel primary = new JPanel(new MigLayout());
+
+ JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::][]",""));
+
+
+
+ panel.add(new JLabel("Strip length:"));
+
+ DoubleModel m = new DoubleModel(component,"StripLength",UnitGroup.UNITS_LENGTH,0);
+
+ JSpinner spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0, 0.6, 1.5)),"w 100lp, wrap");
+
+
+ panel.add(new JLabel("Strip width:"));
+
+ m = new DoubleModel(component,"StripWidth",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0, 0.2)),"w 100lp, wrap 20lp");
+
+
+
+
+ panel.add(new JLabel("Strip area:"));
+
+ m = new DoubleModel(component,"Area",UnitGroup.UNITS_AREA,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0, 0.04, 0.25)),"w 100lp, wrap");
+
+
+ panel.add(new JLabel("Aspect ratio:"));
+
+ m = new DoubleModel(component,"AspectRatio",UnitGroup.UNITS_NONE,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+// panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(2, 15)),"skip, w 100lp, wrap 20lp");
+
+
+
+ panel.add(new JLabel("Material:"));
+
+ JComboBox combo = new JComboBox(new MaterialModel(component, Material.Type.SURFACE));
+ combo.setToolTipText("The component material affects the weight of the component.");
+ panel.add(combo,"spanx 3, growx, wrap 20lp");
+
+
+
+ // CD
+ JLabel label = new JLabel("<html>Drag coefficient C<sub>D</sub>:");
+ String tip = "<html>The drag coefficient relative to the total area of the streamer.<br>" +
+ "A larger drag coefficient yields a slowed descent rate.";
+ label.setToolTipText(tip);
+ panel.add(label);
+
+ m = new DoubleModel(component,"CD",UnitGroup.UNITS_COEFFICIENT,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setToolTipText(tip);
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ JCheckBox check = new JCheckBox(m.getAutomaticAction());
+ check.setText("Automatic");
+ panel.add(check,"skip, span, wrap");
+
+ panel.add(new ResizeLabel("The drag coefficient is relative to the area of the streamer.",
+ -2), "span, wrap");
+
+
+
+ primary.add(panel, "grow, gapright 20lp");
+ panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::][]",""));
+
+
+
+
+ //// Position
+
+ panel.add(new JLabel("Position relative to:"));
+
+ combo = new JComboBox(
+ new EnumModel<RocketComponent.Position>(component, "RelativePosition",
+ new RocketComponent.Position[] {
+ RocketComponent.Position.TOP,
+ RocketComponent.Position.MIDDLE,
+ RocketComponent.Position.BOTTOM,
+ RocketComponent.Position.ABSOLUTE
+ }));
+ panel.add(combo,"spanx, growx, wrap");
+
+ panel.add(new JLabel("plus"),"right");
+
+ m = new DoubleModel(component,"PositionValue",UnitGroup.UNITS_LENGTH);
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(
+ new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE),
+ new DoubleModel(component.getParent(), "Length"))),
+ "w 100lp, wrap");
+
+
+ //// Spatial length
+ panel.add(new JLabel("Packed length:"));
+
+ m = new DoubleModel(component,"Length",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 0.5)),"w 100lp, wrap");
+
+
+ //// Tube diameter
+ panel.add(new JLabel("Packed diameter:"));
+
+ DoubleModel od = new DoubleModel(component,"Radius",2,UnitGroup.UNITS_LENGTH,0);
+ // Diameter = 2*Radius
+
+ spin = new JSpinner(od.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(od),"growx");
+ panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)),"w 100lp, wrap 30lp");
+
+
+ //// Deployment
+
+ panel.add(new JLabel("Deploys at:"),"");
+
+ combo = new JComboBox(new EnumModel<IgnitionEvent>(component, "DeployEvent"));
+ panel.add(combo,"spanx 3, growx, wrap");
+
+ // ... and delay
+ panel.add(new JLabel("plus"),"right");
+
+ m = new DoubleModel(component,"DeployDelay",0);
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"spanx, split");
+
+ panel.add(new JLabel("seconds"),"wrap paragraph");
+
+ // Altitude
+ label = new JLabel("Altitude:");
+ altitudeComponents.add(label);
+ panel.add(label);
+
+ m = new DoubleModel(component,"DeployAltitude",UnitGroup.UNITS_DISTANCE,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ altitudeComponents.add(spin);
+ panel.add(spin,"growx");
+ UnitSelector unit = new UnitSelector(m);
+ altitudeComponents.add(unit);
+ panel.add(unit,"growx");
+ BasicSlider slider = new BasicSlider(m.getSliderModel(100, 1000));
+ altitudeComponents.add(slider);
+ panel.add(slider,"w 100lp, wrap");
+
+
+ primary.add(panel, "grow");
+
+ updateFields();
+
+ tabbedPane.insertTab("General", null, primary, "General properties", 0);
+ tabbedPane.insertTab("Radial position", null, positionTab(),
+ "Radial position configuration", 1);
+ tabbedPane.setSelectedIndex(0);
+ }
+
+
+
+
+
+ protected JPanel positionTab() {
+ JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+
+ //// Radial position
+ panel.add(new JLabel("Radial distance:"));
+
+ DoubleModel m = new DoubleModel(component,"RadialPosition",UnitGroup.UNITS_LENGTH,0);
+
+ JSpinner spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0, 0.1, 1.0)),"w 100lp, wrap");
+
+
+ //// Radial direction
+ panel.add(new JLabel("Radial direction:"));
+
+ m = new DoubleModel(component,"RadialDirection",UnitGroup.UNITS_ANGLE,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(-Math.PI, Math.PI)),"w 100lp, wrap");
+
+
+ //// Reset button
+ JButton button = new JButton("Reset");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ ((MassComponent) component).setRadialDirection(0.0);
+ ((MassComponent) component).setRadialPosition(0.0);
+ }
+ });
+ panel.add(button,"spanx, right");
+
+ return panel;
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+
+import javax.swing.JPanel;
+
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+
+
+public class ThicknessRingComponentConfig extends RingComponentConfig {
+
+ public ThicknessRingComponentConfig(RocketComponent c) {
+ super(c);
+
+ JPanel tab;
+
+ tab = generalTab("Outer diameter:", "Inner diameter:", "Wall thickness:", "Length:");
+ tabbedPane.insertTab("General", null, tab, "General properties", 0);
+ tabbedPane.setSelectedIndex(0);
+ }
+
+}
\ No newline at end of file
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSpinner;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.DescriptionArea;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.BooleanModel;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.unit.UnitGroup;
+
+public class TransitionConfig extends RocketComponentConfig {
+
+ private JComboBox typeBox;
+ //private JLabel description;
+
+ private JLabel shapeLabel;
+ private JSpinner shapeSpinner;
+ private BasicSlider shapeSlider;
+ private DescriptionArea description;
+
+
+ // Prepended to the description from Transition.DESCRIPTIONS
+ private static final String PREDESC = "<html><p style=\"font-size: x-small\">";
+
+
+ public TransitionConfig(RocketComponent c) {
+ super(c);
+
+ DoubleModel m;
+ JSpinner spin;
+ JCheckBox checkbox;
+
+ JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+
+
+
+ //// Shape selection
+
+ panel.add(new JLabel("Transition shape:"));
+
+ Transition.Shape selected = ((Transition)component).getType();
+ Transition.Shape[] typeList = Transition.Shape.values();
+
+ typeBox = new JComboBox(typeList);
+ typeBox.setEditable(false);
+ typeBox.setSelectedItem(selected);
+ typeBox.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ Transition.Shape s = (Transition.Shape)typeBox.getSelectedItem();
+ ((Transition)component).setType(s);
+ description.setText(PREDESC + s.getTransitionDescription());
+ updateEnabled();
+ }
+ });
+ panel.add(typeBox,"span, split 2");
+
+
+ checkbox = new JCheckBox(new BooleanModel(component,"Clipped"));
+ checkbox.setText("Clipped");
+ panel.add(checkbox,"wrap");
+
+
+ //// Shape parameter
+ shapeLabel = new JLabel("Shape parameter:");
+ panel.add(shapeLabel);
+
+ m = new DoubleModel(component,"ShapeParameter");
+
+ shapeSpinner = new JSpinner(m.getSpinnerModel());
+ shapeSpinner.setEditor(new SpinnerEditor(shapeSpinner));
+ panel.add(shapeSpinner,"growx");
+
+ DoubleModel min = new DoubleModel(component,"ShapeParameterMin");
+ DoubleModel max = new DoubleModel(component,"ShapeParameterMax");
+ shapeSlider = new BasicSlider(m.getSliderModel(min,max));
+ panel.add(shapeSlider,"skip, w 100lp, wrap");
+
+ updateEnabled();
+
+
+ //// Length
+ panel.add(new JLabel("Transition length:"));
+
+ m = new DoubleModel(component,"Length",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0, 0.05, 0.3)),"w 100lp, wrap");
+
+
+ //// Transition diameter 1
+ panel.add(new JLabel("Fore diameter:"));
+
+ DoubleModel od = new DoubleModel(component,"ForeRadius",2,UnitGroup.UNITS_LENGTH,0);
+ // Diameter = 2*Radius
+
+ spin = new JSpinner(od.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(od),"growx");
+ panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)),"w 100lp, wrap 0px");
+
+ checkbox = new JCheckBox(od.getAutomaticAction());
+ checkbox.setText("Automatic");
+ panel.add(checkbox,"skip, span 2, wrap");
+
+
+ //// Transition diameter 2
+ panel.add(new JLabel("Aft diameter:"));
+
+ od = new DoubleModel(component,"AftRadius",2,UnitGroup.UNITS_LENGTH,0);
+ // Diameter = 2*Radius
+
+ spin = new JSpinner(od.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(od),"growx");
+ panel.add(new BasicSlider(od.getSliderModel(0, 0.04, 0.2)),"w 100lp, wrap 0px");
+
+ checkbox = new JCheckBox(od.getAutomaticAction());
+ checkbox.setText("Automatic");
+ panel.add(checkbox,"skip, span 2, wrap");
+
+
+ //// Wall thickness
+ panel.add(new JLabel("Wall thickness:"));
+
+ m = new DoubleModel(component,"Thickness",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0,0.01)),"w 100lp, wrap 0px");
+
+
+ checkbox = new JCheckBox(new BooleanModel(component,"Filled"));
+ checkbox.setText("Filled");
+ panel.add(checkbox,"skip, span 2, wrap");
+
+
+
+ //// Description
+
+ JPanel panel2 = new JPanel(new MigLayout("ins 0"));
+
+ description = new DescriptionArea(5);
+ description.setText(PREDESC + ((Transition)component).getType().
+ getTransitionDescription());
+ panel2.add(description, "wmin 250lp, spanx, growx, wrap para");
+
+
+ //// Material
+
+
+ materialPanel(panel2, Material.Type.BULK);
+ panel.add(panel2, "cell 4 0, gapleft paragraph, aligny 0%, spany");
+
+
+ tabbedPane.insertTab("General", null, panel, "General properties", 0);
+ tabbedPane.insertTab("Shoulder", null, shoulderTab(), "Shoulder properties", 1);
+ tabbedPane.setSelectedIndex(0);
+ }
+
+
+
+
+
+ private void updateEnabled() {
+ boolean e = ((Transition)component).getType().usesParameter();
+ shapeLabel.setEnabled(e);
+ shapeSpinner.setEnabled(e);
+ shapeSlider.setEnabled(e);
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.configdialog;
+
+
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSeparator;
+import javax.swing.JSpinner;
+import javax.swing.SwingConstants;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.EnumModel;
+import net.sf.openrocket.gui.adaptors.IntegerModel;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
+import net.sf.openrocket.unit.UnitGroup;
+
+
+public class TrapezoidFinSetConfig extends FinSetConfig {
+
+ public TrapezoidFinSetConfig(final RocketComponent component) {
+ super(component);
+
+ DoubleModel m;
+ JSpinner spin;
+ JComboBox combo;
+
+ JPanel mainPanel = new JPanel(new MigLayout());
+
+
+ JPanel panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+
+ //// Number of fins
+ JLabel label = new JLabel("Number of fins:");
+ label.setToolTipText("The number of fins in the fin set.");
+ panel.add(label);
+
+ IntegerModel im = new IntegerModel(component,"FinCount",1,8);
+
+ spin = new JSpinner(im.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ spin.setToolTipText("The number of fins in the fin set.");
+ panel.add(spin,"growx, wrap");
+
+
+ //// Base rotation
+ label = new JLabel("Fin rotation:");
+ label.setToolTipText("The angle of the first fin in the fin set.");
+ panel.add(label);
+
+ m = new DoubleModel(component, "BaseRotation", UnitGroup.UNITS_ANGLE,-Math.PI,Math.PI);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(-Math.PI,Math.PI)),"w 100lp, wrap");
+
+
+ //// Fin cant
+ label = new JLabel("Fin cant:");
+ label.setToolTipText("The angle that the fins are canted with respect to the rocket " +
+ "body.");
+ panel.add(label);
+
+ m = new DoubleModel(component, "CantAngle", UnitGroup.UNITS_ANGLE,
+ -FinSet.MAX_CANT, FinSet.MAX_CANT);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(-FinSet.MAX_CANT,FinSet.MAX_CANT)),
+ "w 100lp, wrap");
+
+
+ //// Root chord
+ panel.add(new JLabel("Root chord:"));
+
+ m = new DoubleModel(component,"RootChord",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0,0.05,0.2)),"w 100lp, wrap");
+
+
+
+ //// Tip chord
+ panel.add(new JLabel("Tip chord:"));
+
+ m = new DoubleModel(component,"TipChord",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0,0.05,0.2)),"w 100lp, wrap");
+
+
+ //// Height
+ panel.add(new JLabel("Height:"));
+
+ m = new DoubleModel(component,"Height",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0,0.05,0.2)),"w 100lp, wrap");
+
+
+
+ //// Sweep
+ panel.add(new JLabel("Sweep length:"));
+
+ m = new DoubleModel(component,"Sweep",UnitGroup.UNITS_LENGTH);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+
+ // sweep slider from -1.1*TipChord to 1.1*RootChord
+ DoubleModel tc = new DoubleModel(component,"TipChord",-1.1,UnitGroup.UNITS_LENGTH);
+ DoubleModel rc = new DoubleModel(component,"RootChord",1.1,UnitGroup.UNITS_LENGTH);
+ panel.add(new BasicSlider(m.getSliderModel(tc,rc)),"w 100lp, wrap");
+
+
+ //// Sweep angle
+ panel.add(new JLabel("Sweep angle:"));
+
+ m = new DoubleModel(component, "SweepAngle",UnitGroup.UNITS_ANGLE,
+ -TrapezoidFinSet.MAX_SWEEP_ANGLE,TrapezoidFinSet.MAX_SWEEP_ANGLE);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(-Math.PI/4,Math.PI/4)),
+ "w 100lp, wrap paragraph");
+
+
+
+
+
+ mainPanel.add(panel,"aligny 20%");
+
+ mainPanel.add(new JSeparator(SwingConstants.VERTICAL),"growy");
+
+
+
+ panel = new JPanel(new MigLayout("gap rel unrel","[][65lp::][30lp::]",""));
+
+
+
+ //// Cross section
+ panel.add(new JLabel("Fin cross section:"));
+ combo = new JComboBox(
+ new EnumModel<FinSet.CrossSection>(component,"CrossSection"));
+ panel.add(combo,"span, growx, wrap");
+
+
+ //// Thickness
+ panel.add(new JLabel("Thickness:"));
+
+ m = new DoubleModel(component,"Thickness",UnitGroup.UNITS_LENGTH,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(0,0.01)),"w 100lp, wrap para");
+
+
+ //// Position
+
+ panel.add(new JLabel("Position relative to:"));
+
+ combo = new JComboBox(
+ new EnumModel<RocketComponent.Position>(component, "RelativePosition",
+ new RocketComponent.Position[] {
+ RocketComponent.Position.TOP,
+ RocketComponent.Position.MIDDLE,
+ RocketComponent.Position.BOTTOM,
+ RocketComponent.Position.ABSOLUTE
+ }));
+ panel.add(combo,"spanx, growx, wrap");
+
+ panel.add(new JLabel("plus"),"right");
+
+ m = new DoubleModel(component,"PositionValue",UnitGroup.UNITS_LENGTH);
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ panel.add(spin,"growx");
+
+ panel.add(new UnitSelector(m),"growx");
+ panel.add(new BasicSlider(m.getSliderModel(
+ new DoubleModel(component.getParent(), "Length", -1.0, UnitGroup.UNITS_NONE),
+ new DoubleModel(component.getParent(), "Length"))),
+ "w 100lp, wrap para");
+
+
+
+ //// Material
+ materialPanel(panel, Material.Type.BULK);
+
+
+
+
+ mainPanel.add(panel,"aligny 20%");
+
+
+ tabbedPane.insertTab("General", null, mainPanel, "General properties", 0);
+ tabbedPane.setSelectedIndex(0);
+
+ addFinSetButtons();
+
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.figureelements;
+
+import java.awt.Color;
+import java.awt.geom.Area;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Rectangle2D;
+
+/**
+ * A mark indicating the position of the center of gravity. It is a blue circle with every
+ * second quarter filled with blue.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class CGCaret extends Caret {
+ private static final float RADIUS = 7;
+
+ private static Area caret = null;
+
+ /**
+ * Create a new CGCaret at the specified coordinates.
+ */
+ public CGCaret(double x, double y) {
+ super(x,y);
+ }
+
+ /**
+ * Returns the Area corresponding to the caret. The Area object is created only once,
+ * after which the object is cloned for new copies.
+ */
+ @Override
+ protected Area getCaret() {
+ if (caret != null) {
+ return (Area)caret.clone();
+ }
+
+ Ellipse2D.Float e = new Ellipse2D.Float(-RADIUS,-RADIUS,2*RADIUS,2*RADIUS);
+ caret = new Area(e);
+
+ Area a;
+ a = new Area(new Rectangle2D.Float(-RADIUS,-RADIUS,RADIUS,RADIUS));
+ caret.subtract(a);
+ a = new Area(new Rectangle2D.Float(0,0,RADIUS,RADIUS));
+ caret.subtract(a);
+
+ a = new Area(new Ellipse2D.Float(-RADIUS,-RADIUS,2*RADIUS,2*RADIUS));
+ a.subtract(new Area(new Ellipse2D.Float(-RADIUS*0.9f,-RADIUS*0.9f,
+ 2*0.9f*RADIUS,2*0.9f*RADIUS)));
+ caret.add(a);
+
+ return (Area) caret.clone();
+ }
+
+ /**
+ * Return the color of the caret (blue).
+ */
+ @Override
+ protected Color getColor() {
+ return Color.BLUE;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.figureelements;
+
+import java.awt.Color;
+import java.awt.geom.Area;
+import java.awt.geom.Ellipse2D;
+
+/**
+ * A mark indicating the position of the center of pressure. It is a red filled circle
+ * inside a slightly larger red circle.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class CPCaret extends Caret {
+ private static final float RADIUS = 7;
+
+ private static Area caret = null;
+
+ /**
+ * Create a new CPCaret at the specified coordinates.
+ */
+ public CPCaret(double x, double y) {
+ super(x,y);
+ }
+
+ /**
+ * Returns the Area object of the caret. The Area object is created only once,
+ * after which new copies are cloned from it.
+ */
+ @Override
+ protected Area getCaret() {
+ if (caret != null) {
+ return (Area)caret.clone();
+ }
+
+ Ellipse2D.Float e = new Ellipse2D.Float(-RADIUS,-RADIUS,2*RADIUS,2*RADIUS);
+ caret = new Area(e);
+
+ caret.subtract(new Area(new Ellipse2D.Float(-RADIUS*0.9f,-RADIUS*0.9f,
+ 2*0.9f*RADIUS,2*0.9f*RADIUS)));
+
+ caret.add(new Area(new Ellipse2D.Float(-RADIUS*0.75f,-RADIUS*0.75f,
+ 2*0.75f*RADIUS,2*0.75f*RADIUS)));
+
+ return (Area) caret.clone();
+ }
+
+
+ /**
+ * Return the color of the caret (red).
+ */
+ @Override
+ protected Color getColor() {
+ return Color.RED;
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.figureelements;
+
+
+import java.awt.Color;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Area;
+
+public abstract class Caret implements FigureElement {
+ private double x,y;
+
+ /**
+ * Creates a new caret at the specified coordinates.
+ */
+ public Caret(double x, double y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ /**
+ * Sets the position of the caret to the new coordinates.
+ */
+ public void setPosition(double x, double y) {
+ this.x = x;
+ this.y = y;
+ }
+
+ /**
+ * Paints the caret to the Graphics2D element.
+ */
+ public void paint(Graphics2D g2, double scale) {
+ Area caret = getCaret();
+ AffineTransform t = new AffineTransform(1.0/scale, 0, 0, 1.0/scale, x, y);
+ caret.transform(t);
+
+ g2.setColor(getColor());
+ g2.fill(caret);
+ }
+
+
+ public void paint(Graphics2D g2, double scale, Rectangle visible) {
+ throw new UnsupportedOperationException("paint() with rectangle unsupported.");
+ }
+
+ /**
+ * Return the Area object corresponding to the mark.
+ */
+ protected abstract Area getCaret();
+
+ /**
+ * Return the color to be used when drawing the mark.
+ */
+ protected abstract Color getColor();
+}
--- /dev/null
+package net.sf.openrocket.gui.figureelements;
+
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+
+public interface FigureElement {
+
+ public void paint(Graphics2D g2, double scale);
+
+ public void paint(Graphics2D g2, double scale, Rectangle visible);
+
+}
--- /dev/null
+package net.sf.openrocket.gui.figureelements;
+
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.font.GlyphVector;
+import java.awt.geom.Rectangle2D;
+
+import net.sf.openrocket.aerodynamics.Warning;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Prefs;
+
+
+
+/**
+ * A <code>FigureElement</code> that draws text at different positions in the figure
+ * with general data about the rocket.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class RocketInfo implements FigureElement {
+
+ // Margin around the figure edges, pixels
+ private static final int MARGIN = 8;
+
+ // Font to use
+ private static final Font FONT = new Font(Font.SANS_SERIF, Font.PLAIN, 11);
+ private static final Font SMALLFONT = new Font(Font.SANS_SERIF, Font.PLAIN, 9);
+
+
+ private final Caret cpCaret = new CPCaret(0,0);
+ private final Caret cgCaret = new CGCaret(0,0);
+
+ private final Configuration configuration;
+ private final UnitGroup stabilityUnits;
+
+ private double cg = 0, cp = 0;
+ private double length = 0, diameter = 0;
+ private double mass = 0;
+ private double aoa = Double.NaN, theta = Double.NaN, mach = Prefs.getDefaultMach();
+
+ private WarningSet warnings = null;
+
+ private boolean calculatingData = false;
+ private FlightData flightData = null;
+
+ private Graphics2D g2 = null;
+ private float line = 0;
+ private float x1, x2, y1, y2;
+
+
+
+
+
+ public RocketInfo(Configuration configuration) {
+ this.configuration = configuration;
+ this.stabilityUnits = UnitGroup.stabilityUnits(configuration);
+ }
+
+
+ @Override
+ public void paint(Graphics2D g2, double scale) {
+ throw new UnsupportedOperationException("paint() must be called with coordinates");
+ }
+
+ @Override
+ public void paint(Graphics2D g2, double scale, Rectangle visible) {
+ this.g2 = g2;
+ this.line = FONT.getLineMetrics("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz",
+ g2.getFontRenderContext()).getHeight();
+
+ x1 = visible.x + MARGIN;
+ x2 = visible.x + visible.width - MARGIN;
+ y1 = visible.y + line ;
+ y2 = visible.y + visible.height - MARGIN;
+
+ drawMainInfo();
+ drawStabilityInfo();
+ drawWarnings();
+ drawFlightInformation();
+ }
+
+
+ public void setCG(double cg) {
+ this.cg = cg;
+ }
+
+ public void setCP(double cp) {
+ this.cp = cp;
+ }
+
+ public void setLength(double length) {
+ this.length = length;
+ }
+
+ public void setDiameter(double diameter) {
+ this.diameter = diameter;
+ }
+
+ public void setMass(double mass) {
+ this.mass = mass;
+ }
+
+ public void setWarnings(WarningSet warnings) {
+ this.warnings = warnings.clone();
+ }
+
+ public void setAOA(double aoa) {
+ this.aoa = aoa;
+ }
+
+ public void setTheta(double theta) {
+ this.theta = theta;
+ }
+
+ public void setMach(double mach) {
+ this.mach = mach;
+ }
+
+
+ public void setFlightData(FlightData data) {
+ this.flightData = data;
+ }
+
+ public void setCalculatingData(boolean calc) {
+ this.calculatingData = calc;
+ }
+
+
+
+
+ private void drawMainInfo() {
+ GlyphVector name = createText(configuration.getRocket().getName());
+ GlyphVector lengthLine = createText(
+ "Length " + UnitGroup.UNITS_LENGTH.getDefaultUnit().toStringUnit(length) +
+ ", max. diameter " +
+ UnitGroup.UNITS_LENGTH.getDefaultUnit().toStringUnit(diameter));
+
+ String massText;
+ if (configuration.hasMotors())
+ massText = "Mass with motors ";
+ else
+ massText = "Mass with no motors ";
+
+ massText += UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(mass);
+
+ GlyphVector massLine = createText(massText);
+
+
+ g2.setColor(Color.BLACK);
+
+ g2.drawGlyphVector(name, x1, y1);
+ g2.drawGlyphVector(lengthLine, x1, y1+line);
+ g2.drawGlyphVector(massLine, x1, y1+2*line);
+
+ }
+
+
+ private void drawStabilityInfo() {
+ String at;
+
+ at = "at M="+UnitGroup.UNITS_COEFFICIENT.getDefaultUnit().toStringUnit(mach);
+ if (!Double.isNaN(aoa)) {
+ at += " \u03b1=" + UnitGroup.UNITS_ANGLE.getDefaultUnit().toStringUnit(aoa);
+ }
+ if (!Double.isNaN(theta)) {
+ at += " \u0398=" + UnitGroup.UNITS_ANGLE.getDefaultUnit().toStringUnit(theta);
+ }
+
+ GlyphVector cgValue = createText(
+ UnitGroup.UNITS_LENGTH.getDefaultUnit().toStringUnit(cg));
+ GlyphVector cpValue = createText(
+ UnitGroup.UNITS_LENGTH.getDefaultUnit().toStringUnit(cp));
+ GlyphVector stabValue = createText(
+ stabilityUnits.getDefaultUnit().toStringUnit(cp-cg));
+
+ GlyphVector cgText = createText("CG: ");
+ GlyphVector cpText = createText("CP: ");
+ GlyphVector stabText = createText("Stability: ");
+ GlyphVector atText = createSmallText(at);
+
+ Rectangle2D cgRect = cgValue.getVisualBounds();
+ Rectangle2D cpRect = cpValue.getVisualBounds();
+ Rectangle2D cgTextRect = cgText.getVisualBounds();
+ Rectangle2D cpTextRect = cpText.getVisualBounds();
+ Rectangle2D stabRect = stabValue.getVisualBounds();
+ Rectangle2D stabTextRect = stabText.getVisualBounds();
+ Rectangle2D atTextRect = atText.getVisualBounds();
+
+ double unitWidth = MathUtil.max(cpRect.getWidth(), cgRect.getWidth(),
+ stabRect.getWidth());
+ double textWidth = Math.max(cpTextRect.getWidth(), cgTextRect.getWidth());
+
+
+ g2.setColor(Color.BLACK);
+
+ g2.drawGlyphVector(stabValue, (float)(x2-stabRect.getWidth()), y1);
+ g2.drawGlyphVector(cgValue, (float)(x2-cgRect.getWidth()), y1+line);
+ g2.drawGlyphVector(cpValue, (float)(x2-cpRect.getWidth()), y1+2*line);
+
+ g2.drawGlyphVector(stabText, (float)(x2-unitWidth-stabTextRect.getWidth()), y1);
+ g2.drawGlyphVector(cgText, (float)(x2-unitWidth-cgTextRect.getWidth()), y1+line);
+ g2.drawGlyphVector(cpText, (float)(x2-unitWidth-cpTextRect.getWidth()), y1+2*line);
+
+ cgCaret.setPosition(x2 - unitWidth - textWidth - 10, y1+line-0.3*line);
+ cgCaret.paint(g2, 1.7);
+
+ cpCaret.setPosition(x2 - unitWidth - textWidth - 10, y1+2*line-0.3*line);
+ cpCaret.paint(g2, 1.7);
+
+ float atPos;
+ if (unitWidth + textWidth + 10 > atTextRect.getWidth()) {
+ atPos = (float)(x2-(unitWidth+textWidth+10+atTextRect.getWidth())/2);
+ } else {
+ atPos = (float)(x2 - atTextRect.getWidth());
+ }
+
+ g2.setColor(Color.GRAY);
+ g2.drawGlyphVector(atText, atPos, y1 + 3*line);
+
+ }
+
+
+ private void drawWarnings() {
+ if (warnings == null || warnings.isEmpty())
+ return;
+
+ GlyphVector[] texts = new GlyphVector[warnings.size()+1];
+ double max = 0;
+
+ texts[0] = createText("Warning:");
+ int i=1;
+ for (Warning w: warnings) {
+ texts[i] = createText(w.toString());
+ i++;
+ }
+
+ for (GlyphVector v: texts) {
+ Rectangle2D rect = v.getVisualBounds();
+ if (rect.getWidth() > max)
+ max = rect.getWidth();
+ }
+
+
+ float y = y2 - line * warnings.size();
+ g2.setColor(new Color(255,0,0,130));
+
+ for (GlyphVector v: texts) {
+ Rectangle2D rect = v.getVisualBounds();
+ g2.drawGlyphVector(v, (float)(x2 - max/2 - rect.getWidth()/2), y);
+ y += line;
+ }
+ }
+
+
+ private void drawFlightInformation() {
+ double height = drawFlightData();
+
+ if (calculatingData) {
+ GlyphVector calculating = createText("Calculating...");
+ g2.setColor(Color.BLACK);
+ g2.drawGlyphVector(calculating, x1, (float)(y2-height));
+ }
+ }
+
+
+ private double drawFlightData() {
+ if (flightData == null)
+ return 0;
+
+ double width=0;
+
+ GlyphVector apogee = createText("Apogee: ");
+ GlyphVector maxVelocity = createText("Max. velocity: ");
+ GlyphVector maxAcceleration = createText("Max. acceleration: ");
+
+ GlyphVector apogeeValue, velocityValue, accelerationValue;
+ if (!Double.isNaN(flightData.getMaxAltitude())) {
+ apogeeValue = createText(
+ UnitGroup.UNITS_DISTANCE.toStringUnit(flightData.getMaxAltitude()));
+ } else {
+ apogeeValue = createText("N/A");
+ }
+ if (!Double.isNaN(flightData.getMaxVelocity())) {
+ velocityValue = createText(
+ UnitGroup.UNITS_VELOCITY.toStringUnit(flightData.getMaxVelocity()) +
+ " (Mach " +
+ UnitGroup.UNITS_COEFFICIENT.toString(flightData.getMaxMachNumber()) + ")");
+ } else {
+ velocityValue = createText("N/A");
+ }
+ if (!Double.isNaN(flightData.getMaxAcceleration())) {
+ accelerationValue = createText(
+ UnitGroup.UNITS_ACCELERATION.toStringUnit(flightData.getMaxAcceleration()));
+ } else {
+ accelerationValue = createText("N/A");
+ }
+
+ Rectangle2D rect;
+ rect = apogee.getVisualBounds();
+ width = MathUtil.max(width, rect.getWidth());
+
+ rect = maxVelocity.getVisualBounds();
+ width = MathUtil.max(width, rect.getWidth());
+
+ rect = maxAcceleration.getVisualBounds();
+ width = MathUtil.max(width, rect.getWidth());
+
+ width += 5;
+
+ if (!calculatingData)
+ g2.setColor(new Color(0,0,127));
+ else
+ g2.setColor(new Color(0,0,127,127));
+
+
+ g2.drawGlyphVector(apogee, (float)x1, (float)(y2-2*line));
+ g2.drawGlyphVector(maxVelocity, (float)x1, (float)(y2-line));
+ g2.drawGlyphVector(maxAcceleration, (float)x1, (float)(y2));
+
+ g2.drawGlyphVector(apogeeValue, (float)(x1+width), (float)(y2-2*line));
+ g2.drawGlyphVector(velocityValue, (float)(x1+width), (float)(y2-line));
+ g2.drawGlyphVector(accelerationValue, (float)(x1+width), (float)(y2));
+
+ return 3*line;
+ }
+
+
+
+ private GlyphVector createText(String text) {
+ return FONT.createGlyphVector(g2.getFontRenderContext(), text);
+ }
+
+ private GlyphVector createSmallText(String text) {
+ return SMALLFONT.createGlyphVector(g2.getFontRenderContext(), text);
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.main;
+
+import java.awt.Desktop;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.ResizeLabel;
+import net.sf.openrocket.util.GUIUtil;
+import net.sf.openrocket.util.Prefs;
+
+public class AboutDialog extends JDialog {
+
+ public static final String OPENROCKET_URL = "http://openrocket.sourceforge.net/";
+
+
+ public AboutDialog(JFrame parent) {
+ super(parent, true);
+
+ final String version = Prefs.getVersion();
+
+ JPanel panel = new JPanel(new MigLayout("fill"));
+
+ panel.add(new ResizeLabel("OpenRocket", 20), "ax 50%, wrap para");
+ panel.add(new ResizeLabel("Version " + version, 3), "ax 50%, wrap 30lp");
+
+ panel.add(new ResizeLabel("Copyright \u00A9 2007-2009 Sampo Niskanen"), "ax 50%, wrap para");
+
+ JLabel link;
+
+ if (Desktop.isDesktopSupported()) {
+
+ link = new JLabel("<html><a href=\"" + OPENROCKET_URL + "\">" +
+ OPENROCKET_URL + "</a>");
+ link.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ Desktop d = Desktop.getDesktop();
+ try {
+ d.browse(new URI(OPENROCKET_URL));
+
+ } catch (URISyntaxException e1) {
+ throw new RuntimeException("BUG: Illegal OpenRocket URL: "+OPENROCKET_URL,
+ e1);
+ } catch (IOException e1) {
+ System.err.println("Unable to launch browser:");
+ e1.printStackTrace();
+ }
+ }
+ });
+
+ } else {
+ link = new JLabel(OPENROCKET_URL);
+ }
+ panel.add(link, "ax 50%, wrap para");
+
+
+ JButton close = new JButton("Close");
+ close.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ AboutDialog.this.dispose();
+ }
+ });
+ panel.add(close, "right");
+
+ this.add(panel);
+ this.setTitle("OpenRocket " + version);
+ this.pack();
+ this.setResizable(false);
+ this.setLocationRelativeTo(null);
+ GUIUtil.setDefaultButton(close);
+ GUIUtil.installEscapeCloseOperation(this);
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.gui.main;
+
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.Iterator;
+
+import javax.swing.JTree;
+import javax.swing.event.TreeModelEvent;
+import javax.swing.event.TreeModelListener;
+import javax.swing.tree.TreeModel;
+import javax.swing.tree.TreePath;
+
+import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
+import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+
+/**
+ * A TreeModel that implements viewing of the rocket tree structure.
+ * This class shows the internal structure of the tree, as opposed to the regular
+ * user-side view.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class BareComponentTreeModel implements TreeModel, ComponentChangeListener {
+ ArrayList<TreeModelListener> listeners = new ArrayList<TreeModelListener>();
+
+ private final RocketComponent root;
+ private final JTree tree;
+
+ public BareComponentTreeModel(RocketComponent root, JTree tree) {
+ this.root = root;
+ this.tree = tree;
+ root.addComponentChangeListener(this);
+ }
+
+
+ public Object getChild(Object parent, int index) {
+ RocketComponent component = (RocketComponent)parent;
+ try {
+ return component.getChild(index);
+ } catch (IndexOutOfBoundsException e) {
+ return null;
+ }
+ }
+
+ public int getChildCount(Object parent) {
+ return ((RocketComponent)parent).getChildCount();
+ }
+
+ public int getIndexOfChild(Object parent, Object child) {
+ RocketComponent p = (RocketComponent)parent;
+ RocketComponent c = (RocketComponent)child;
+ return p.getChildPosition(c);
+ }
+
+ public Object getRoot() {
+ return root;
+ }
+
+ public boolean isLeaf(Object node) {
+ RocketComponent c = (RocketComponent)node;
+ return (c.getChildCount()==0);
+ }
+
+ public void addTreeModelListener(TreeModelListener l) {
+ listeners.add(l);
+ }
+
+ public void removeTreeModelListener(TreeModelListener l) {
+ listeners.remove(l);
+ }
+
+ private void fireTreeNodesChanged() {
+ Object[] path = { root };
+ TreeModelEvent e = new TreeModelEvent(this,path);
+ Object[] l = listeners.toArray();
+ for (int i=0; i<l.length; i++)
+ ((TreeModelListener)l[i]).treeNodesChanged(e);
+ }
+
+
+ private void printStructure(TreePath p, int level) {
+ String indent="";
+ for (int i=0; i<level; i++)
+ indent += " ";
+ System.out.println(indent+p+
+ ": isVisible:"+tree.isVisible(p)+
+ " isCollapsed:"+tree.isCollapsed(p)+
+ " isExpanded:"+tree.isExpanded(p));
+ Object parent = p.getLastPathComponent();
+ for (int i=0; i<getChildCount(parent); i++) {
+ Object child = getChild(parent,i);
+ TreePath path = makeTreePath((RocketComponent)child);
+ printStructure(path,level+1);
+ }
+ }
+
+
+ private void fireTreeStructureChanged(RocketComponent source) {
+ Object[] path = { root };
+
+
+ // Get currently expanded path IDs
+ Enumeration<TreePath> enumer = tree.getExpandedDescendants(new TreePath(path));
+ ArrayList<String> expanded = new ArrayList<String>();
+ if (enumer != null) {
+ while (enumer.hasMoreElements()) {
+ TreePath p = enumer.nextElement();
+ expanded.add(((RocketComponent)p.getLastPathComponent()).getID());
+ }
+ }
+
+ // Send structure change event
+ TreeModelEvent e = new TreeModelEvent(this,path);
+ Object[] l = listeners.toArray();
+ for (int i=0; i<l.length; i++)
+ ((TreeModelListener)l[i]).treeStructureChanged(e);
+
+ // Re-expand the paths
+ Iterator<String> iter = expanded.iterator();
+ while (iter.hasNext()) {
+ RocketComponent c = root.findComponent(iter.next());
+ if (c==null)
+ continue;
+ tree.expandPath(makeTreePath(c));
+ }
+ if (source != null) {
+ TreePath p = makeTreePath(source);
+ tree.makeVisible(p);
+ tree.expandPath(p);
+ }
+ }
+
+ public void valueForPathChanged(TreePath path, Object newValue) {
+ System.err.println("ERROR: valueForPathChanged called?!");
+ }
+
+
+ public void componentChanged(ComponentChangeEvent e) {
+ if (e.isTreeChange() || e.isUndoChange()) {
+ // Tree must be fully updated also in case of an undo change
+ fireTreeStructureChanged((RocketComponent)e.getSource());
+ if (e.isTreeChange() && e.isUndoChange()) {
+ // If the undo has changed the tree structure, some elements may be hidden unnecessarily
+ // TODO: LOW: Could this be performed better?
+ expandAll();
+ }
+ } else if (e.isOtherChange()) {
+ fireTreeNodesChanged();
+ }
+ }
+
+ public void expandAll() {
+ Iterator<RocketComponent> iterator = root.deepIterator();
+ while (iterator.hasNext()) {
+ tree.makeVisible(makeTreePath(iterator.next()));
+ }
+ }
+
+ public static TreePath makeTreePath(RocketComponent component) {
+ int count = 0;
+ RocketComponent c = component;
+
+ while (c != null) {
+ count++;
+ c = c.getParent();
+ }
+
+ Object[] list = new Object[count];
+
+ count--;
+ c=component;
+ while (c!=null) {
+ list[count] = c;
+ count--;
+ c = c.getParent();
+ }
+
+ return new TreePath(list);
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.main;
+
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Toolkit;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+
+import javax.swing.Action;
+import javax.swing.InputMap;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JFileChooser;
+import javax.swing.JFrame;
+import javax.swing.JMenu;
+import javax.swing.JMenuBar;
+import javax.swing.JMenuItem;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSeparator;
+import javax.swing.JSplitPane;
+import javax.swing.JTabbedPane;
+import javax.swing.KeyStroke;
+import javax.swing.LookAndFeel;
+import javax.swing.ScrollPaneConstants;
+import javax.swing.SwingUtilities;
+import javax.swing.ToolTipManager;
+import javax.swing.UIManager;
+import javax.swing.border.TitledBorder;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.filechooser.FileFilter;
+import javax.swing.tree.DefaultTreeSelectionModel;
+import javax.swing.tree.TreePath;
+import javax.swing.tree.TreeSelectionModel;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.aerodynamics.Warning;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.file.GeneralRocketLoader;
+import net.sf.openrocket.file.OpenRocketSaver;
+import net.sf.openrocket.file.RocketLoadException;
+import net.sf.openrocket.file.RocketLoader;
+import net.sf.openrocket.file.RocketSaver;
+import net.sf.openrocket.gui.ComponentAnalysisDialog;
+import net.sf.openrocket.gui.PreferencesDialog;
+import net.sf.openrocket.gui.StorageOptionChooser;
+import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
+import net.sf.openrocket.gui.scalefigure.RocketPanel;
+import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
+import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.Stage;
+import net.sf.openrocket.util.Icons;
+import net.sf.openrocket.util.Prefs;
+
+public class BasicFrame extends JFrame {
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * The RocketLoader instance used for loading all rocket designs.
+ */
+ private static final RocketLoader ROCKET_LOADER = new GeneralRocketLoader();
+
+
+ /**
+ * File filter for filtering only rocket designs.
+ */
+ private static final FileFilter ROCKET_DESIGN_FILTER = new FileFilter() {
+ @Override
+ public String getDescription() {
+ return "OpenRocket designs (*.ork)";
+ }
+ @Override
+ public boolean accept(File f) {
+ String name = f.getName().toLowerCase();
+ return name.endsWith(".ork") || name.endsWith(".ork.gz");
+ }
+ };
+
+
+
+ /**
+ * List of currently open frames. When the list goes empty
+ * it is time to exit the application.
+ */
+ private static final ArrayList<BasicFrame> frames = new ArrayList<BasicFrame>();
+
+
+
+
+
+ /**
+ * Whether "New" and "Open" should replace this frame.
+ * Should be set to false on the first rocket modification.
+ */
+ private boolean replaceable = false;
+
+
+
+ private final OpenRocketDocument document;
+ private final Rocket rocket;
+
+ private RocketPanel rocketpanel;
+ private ComponentTree tree = null;
+ private final TreeSelectionModel selectionModel;
+
+ /** Actions available for rocket modifications */
+ private final RocketActions actions;
+
+
+
+ /**
+ * Sole constructor. Creates a new frame based on the supplied document
+ * and adds it to the current frames list.
+ *
+ * @param document the document to show.
+ */
+ public BasicFrame(OpenRocketDocument document) {
+
+ this.document = document;
+ this.rocket = document.getRocket();
+ this.rocket.getDefaultConfiguration().setAllStages();
+
+
+ // Set replaceable flag to false at first modification
+ rocket.addComponentChangeListener(new ComponentChangeListener() {
+ public void componentChanged(ComponentChangeEvent e) {
+ replaceable = false;
+ BasicFrame.this.rocket.removeComponentChangeListener(this);
+ }
+ });
+
+
+ // Create the selection model that will be used
+ selectionModel = new DefaultTreeSelectionModel();
+ selectionModel.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
+
+ actions = new RocketActions(document, selectionModel, this);
+
+
+ // The main vertical split pane
+ JSplitPane vertical = new JSplitPane(JSplitPane.VERTICAL_SPLIT, true);
+ vertical.setResizeWeight(0.5);
+ this.add(vertical);
+
+
+ // The top tabbed pane
+ JTabbedPane tabbed = new JTabbedPane();
+ tabbed.addTab("Rocket design", null, designTab());
+ tabbed.addTab("Flight simulations", null, simulationsTab());
+
+ vertical.setTopComponent(tabbed);
+
+
+
+ // Bottom segment, rocket figure
+
+ rocketpanel = new RocketPanel(document);
+ vertical.setBottomComponent(rocketpanel);
+
+ rocketpanel.setSelectionModel(tree.getSelectionModel());
+
+
+ createMenu();
+
+
+ rocket.addComponentChangeListener(new ComponentChangeListener() {
+ public void componentChanged(ComponentChangeEvent e) {
+ setTitle();
+ }
+ });
+
+ setTitle();
+ this.pack();
+
+ Dimension size = Prefs.getWindowSize(this.getClass());
+ if (size == null) {
+ size = Toolkit.getDefaultToolkit().getScreenSize();
+ size.width = size.width*9/10;
+ size.height = size.height*9/10;
+ }
+ this.setSize(size);
+ this.addComponentListener(new ComponentAdapter() {
+ @Override
+ public void componentResized(ComponentEvent e) {
+ Prefs.setWindowSize(BasicFrame.this.getClass(), BasicFrame.this.getSize());
+ }
+ });
+ this.setLocationByPlatform(true);
+
+ this.validate();
+ vertical.setDividerLocation(0.4);
+ setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
+ addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent e) {
+ closeAction();
+ }
+ });
+ frames.add(this);
+
+ }
+
+
+ /**
+ * Construct the "Rocket design" tab. This contains a horizontal split pane
+ * with the left component the design tree and the right component buttons
+ * for adding components.
+ */
+ private JComponent designTab() {
+ JSplitPane horizontal = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT,true);
+ horizontal.setResizeWeight(0.5);
+
+
+ // Upper-left segment, component tree
+
+ JPanel panel = new JPanel(new MigLayout("fill, flowy","","[grow]"));
+
+ tree = new ComponentTree(rocket);
+ tree.setSelectionModel(selectionModel);
+
+ // Remove JTree key events that interfere with menu accelerators
+ InputMap im = SwingUtilities.getUIInputMap(tree, JComponent.WHEN_FOCUSED);
+ im.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, ActionEvent.CTRL_MASK), null);
+ im.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, ActionEvent.CTRL_MASK), null);
+ im.put(KeyStroke.getKeyStroke(KeyEvent.VK_V, ActionEvent.CTRL_MASK), null);
+ im.put(KeyStroke.getKeyStroke(KeyEvent.VK_A, ActionEvent.CTRL_MASK), null);
+ im.put(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK), null);
+ im.put(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK), null);
+ im.put(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK), null);
+
+
+
+ // Double-click opens config dialog
+ MouseListener ml = new MouseAdapter() {
+ @Override
+ public void mousePressed(MouseEvent e) {
+ int selRow = tree.getRowForLocation(e.getX(), e.getY());
+ TreePath selPath = tree.getPathForLocation(e.getX(), e.getY());
+ if(selRow != -1) {
+ if(e.getClickCount() == 2) {
+ // Double-click
+ RocketComponent c = (RocketComponent)selPath.getLastPathComponent();
+ ComponentConfigDialog.showDialog(BasicFrame.this,
+ BasicFrame.this.document, c);
+ }
+ }
+ }
+ };
+ tree.addMouseListener(ml);
+
+ // Update dialog when selection is changed
+ selectionModel.addTreeSelectionListener(new TreeSelectionListener() {
+ public void valueChanged(TreeSelectionEvent e) {
+ // Scroll tree to the selected item
+ TreePath path = selectionModel.getSelectionPath();
+ if (path == null)
+ return;
+ tree.scrollPathToVisible(path);
+
+ if (!ComponentConfigDialog.isDialogVisible())
+ return;
+ RocketComponent c = (RocketComponent)path.getLastPathComponent();
+ ComponentConfigDialog.showDialog(BasicFrame.this,
+ BasicFrame.this.document, c);
+ }
+ });
+
+ // Place tree inside scroll pane
+ JScrollPane scroll = new JScrollPane(tree);
+ panel.add(scroll,"spany, grow, wrap");
+
+
+ // Buttons
+ JButton button = new JButton(actions.getMoveUpAction());
+ panel.add(button,"sizegroup buttons, aligny 65%");
+
+ button = new JButton(actions.getMoveDownAction());
+ panel.add(button,"sizegroup buttons, aligny 0%");
+
+ button = new JButton(actions.getEditAction());
+ panel.add(button, "sizegroup buttons");
+
+ button = new JButton(actions.getNewStageAction());
+ panel.add(button,"sizegroup buttons");
+
+ button = new JButton(actions.getDeleteAction());
+ button.setIcon(null);
+ button.setMnemonic(0);
+ panel.add(button,"sizegroup buttons");
+
+ horizontal.setLeftComponent(panel);
+
+
+ // Upper-right segment, component addition buttons
+
+ panel = new JPanel(new MigLayout("fill, insets 0","[0::]"));
+
+ scroll = new JScrollPane(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED,
+ ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+ scroll.setViewportView(new ComponentAddButtons(document, selectionModel,
+ scroll.getViewport()));
+ scroll.setBorder(null);
+ scroll.setViewportBorder(null);
+
+ TitledBorder border = new TitledBorder("Add new component");
+ border.setTitleFont(border.getTitleFont().deriveFont(Font.BOLD));
+ scroll.setBorder(border);
+
+ panel.add(scroll,"grow");
+
+ horizontal.setRightComponent(panel);
+
+ return horizontal;
+ }
+
+
+ /**
+ * Construct the "Flight simulations" tab.
+ * @return
+ */
+ private JComponent simulationsTab() {
+ return new SimulationPanel(document);
+ }
+
+
+
+ /**
+ * Creates the menu for the window.
+ */
+ private void createMenu() {
+ JMenuBar menubar = new JMenuBar();
+ JMenu menu;
+ JMenuItem item;
+
+ //// File
+ menu = new JMenu("File");
+ menu.setMnemonic(KeyEvent.VK_F);
+ menu.getAccessibleContext().setAccessibleDescription("File-handling related tasks");
+ menubar.add(menu);
+
+ item = new JMenuItem("New",KeyEvent.VK_N);
+ item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_N, ActionEvent.CTRL_MASK));
+ item.setMnemonic(KeyEvent.VK_N);
+ item.getAccessibleContext().setAccessibleDescription("Create a new rocket design");
+ item.setIcon(Icons.FILE_NEW);
+ item.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ newAction();
+ if (replaceable)
+ closeAction();
+ }
+ });
+ menu.add(item);
+
+ item = new JMenuItem("Open...",KeyEvent.VK_O);
+ item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, ActionEvent.CTRL_MASK));
+ item.getAccessibleContext().setAccessibleDescription("Open a rocket design");
+ item.setIcon(Icons.FILE_OPEN);
+ item.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ openAction();
+ }
+ });
+ menu.add(item);
+
+ menu.addSeparator();
+
+ item = new JMenuItem("Save",KeyEvent.VK_S);
+ item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, ActionEvent.CTRL_MASK));
+ item.getAccessibleContext().setAccessibleDescription("Save the current rocket design");
+ item.setIcon(Icons.FILE_SAVE);
+ item.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ saveAction();
+ }
+ });
+ menu.add(item);
+
+ item = new JMenuItem("Save as...",KeyEvent.VK_A);
+ item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S,
+ ActionEvent.CTRL_MASK | ActionEvent.SHIFT_MASK));
+ item.getAccessibleContext().setAccessibleDescription("Save the current rocket design "+
+ "to a new file");
+ item.setIcon(Icons.FILE_SAVE_AS);
+ item.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ saveAsAction();
+ }
+ });
+ menu.add(item);
+
+// menu.addSeparator();
+ menu.add(new JSeparator());
+
+ item = new JMenuItem("Close",KeyEvent.VK_C);
+ item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_W, ActionEvent.CTRL_MASK));
+ item.getAccessibleContext().setAccessibleDescription("Close the current rocket design");
+ item.setIcon(Icons.FILE_CLOSE);
+ item.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ closeAction();
+ }
+ });
+ menu.add(item);
+
+ menu.addSeparator();
+
+ item = new JMenuItem("Quit",KeyEvent.VK_Q);
+ item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Q, ActionEvent.CTRL_MASK));
+ item.getAccessibleContext().setAccessibleDescription("Quit the program");
+ item.setIcon(Icons.FILE_QUIT);
+ item.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ quitAction();
+ }
+ });
+ menu.add(item);
+
+
+
+ //// Edit
+ menu = new JMenu("Edit");
+ menu.setMnemonic(KeyEvent.VK_E);
+ menu.getAccessibleContext().setAccessibleDescription("Rocket editing");
+ menubar.add(menu);
+
+
+ Action action = document.getUndoAction();
+ item = new JMenuItem(action);
+ item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Z, ActionEvent.CTRL_MASK));
+ item.setMnemonic(KeyEvent.VK_U);
+ item.getAccessibleContext().setAccessibleDescription("Undo the previous operation");
+
+ menu.add(item);
+
+ action = document.getRedoAction();
+ item = new JMenuItem(action);
+ item.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_Y, ActionEvent.CTRL_MASK));
+ item.setMnemonic(KeyEvent.VK_R);
+ item.getAccessibleContext().setAccessibleDescription("Redo the previously undone " +
+ "operation");
+ menu.add(item);
+
+ menu.addSeparator();
+
+
+ item = new JMenuItem(actions.getCutAction());
+ menu.add(item);
+
+ item = new JMenuItem(actions.getCopyAction());
+ menu.add(item);
+
+ item = new JMenuItem(actions.getPasteAction());
+ menu.add(item);
+
+ item = new JMenuItem(actions.getDeleteAction());
+ menu.add(item);
+
+ menu.addSeparator();
+
+ item = new JMenuItem("Preferences");
+ item.setIcon(Icons.PREFERENCES);
+ item.getAccessibleContext().setAccessibleDescription("Setup the application "+
+ "preferences");
+ item.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ PreferencesDialog.showPreferences();
+ }
+ });
+ menu.add(item);
+
+
+
+
+ //// Analyze
+ menu = new JMenu("Analyze");
+ menu.setMnemonic(KeyEvent.VK_A);
+ menu.getAccessibleContext().setAccessibleDescription("Analyzing the rocket");
+ menubar.add(menu);
+
+ item = new JMenuItem("Component analysis",KeyEvent.VK_C);
+ item.getAccessibleContext().setAccessibleDescription("Analyze the rocket components " +
+ "separately");
+ item.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ ComponentAnalysisDialog.showDialog(rocketpanel);
+ }
+ });
+ menu.add(item);
+
+
+
+ //// Help
+
+ menu = new JMenu("Help");
+ menu.setMnemonic(KeyEvent.VK_H);
+ menu.getAccessibleContext().setAccessibleDescription("Information about OpenRocket");
+ menubar.add(menu);
+
+ item = new JMenuItem("License",KeyEvent.VK_L);
+ item.getAccessibleContext().setAccessibleDescription("OpenRocket license information");
+ item.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ new LicenseDialog(BasicFrame.this).setVisible(true);
+ }
+ });
+ menu.add(item);
+
+ item = new JMenuItem("About",KeyEvent.VK_A);
+ item.getAccessibleContext().setAccessibleDescription("About OpenRocket");
+ item.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ new AboutDialog(BasicFrame.this).setVisible(true);
+ }
+ });
+ menu.add(item);
+
+
+ this.setJMenuBar(menubar);
+ }
+
+
+
+ // TODO: HIGH: Remember last directory on open/save
+
+ private void openAction() {
+ JFileChooser chooser = new JFileChooser();
+ chooser.setFileFilter(ROCKET_DESIGN_FILTER);
+ chooser.setMultiSelectionEnabled(true);
+ chooser.setCurrentDirectory(Prefs.getDefaultDirectory());
+ if (chooser.showOpenDialog(BasicFrame.this) != JFileChooser.APPROVE_OPTION)
+ return;
+
+ Prefs.setDefaultDirectory(chooser.getCurrentDirectory());
+
+ File[] files = chooser.getSelectedFiles();
+ boolean opened = false;
+
+ for (File file: files) {
+ System.out.println("Opening file: " + file);
+ if (open(file)) {
+ opened = true;
+ }
+ }
+
+ // Close this frame if replaceable and file opened successfully
+ if (replaceable && opened) {
+ closeAction();
+ }
+ }
+
+
+ /**
+ * Open the specified file in a new design frame. If an error occurs, an error dialog
+ * is shown and <code>false</code> is returned.
+ *
+ * @param file the file to open.
+ * @return whether the file was successfully loaded and opened.
+ */
+ private static boolean open(File file) {
+ OpenRocketDocument doc = null;
+ try {
+ doc = ROCKET_LOADER.load(file);
+ } catch (RocketLoadException e) {
+ JOptionPane.showMessageDialog(null, "Unable to open file '" + file.getName()
+ +"': " + e.getMessage(), "Error opening file", JOptionPane.ERROR_MESSAGE);
+ e.printStackTrace();
+ return false;
+ }
+
+ if (doc == null) {
+ throw new RuntimeException("BUG: Rocket loader returned null");
+ }
+
+ // Show warnings
+ Iterator<Warning> warns = ROCKET_LOADER.getWarnings().iterator();
+ System.out.println("Warnings:");
+ while (warns.hasNext()) {
+ System.out.println(" "+warns.next());
+ // TODO: HIGH: dialog
+ }
+
+ // Set document state
+ doc.setFile(file);
+ doc.setSaved(true);
+
+ // Open the frame
+ BasicFrame frame = new BasicFrame(doc);
+ frame.setVisible(true);
+
+ return true;
+ }
+
+
+
+ private boolean saveAction() {
+ File file = document.getFile();
+ if (file==null) {
+ return saveAsAction();
+ } else {
+ return saveAs(file);
+ }
+ }
+
+ private boolean saveAsAction() {
+ File file = null;
+ while (file == null) {
+ StorageOptionChooser storageChooser =
+ new StorageOptionChooser(document.getDefaultStorageOptions());
+ JFileChooser chooser = new JFileChooser();
+ chooser.setFileFilter(ROCKET_DESIGN_FILTER);
+ chooser.setCurrentDirectory(Prefs.getDefaultDirectory());
+ chooser.setAccessory(storageChooser);
+ if (document.getFile() != null)
+ chooser.setSelectedFile(document.getFile());
+
+ if (chooser.showSaveDialog(BasicFrame.this) != JFileChooser.APPROVE_OPTION)
+ return false;
+
+ file = chooser.getSelectedFile();
+ if (file == null)
+ return false;
+
+ Prefs.setDefaultDirectory(chooser.getCurrentDirectory());
+ storageChooser.storeOptions(document.getDefaultStorageOptions());
+
+ if (file.getName().indexOf('.') < 0) {
+ String name = file.getAbsolutePath();
+ name = name + ".ork";
+ file = new File(name);
+ }
+
+ if (file.exists()) {
+ int result = JOptionPane.showConfirmDialog(this,
+ "File '"+file.getName()+"' exists. Do you want to overwrite it?",
+ "File exists", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
+ if (result != JOptionPane.YES_OPTION)
+ return false;
+ }
+ }
+ saveAs(file);
+ return true;
+ }
+
+
+ private boolean saveAs(File file) {
+ System.out.println("Saving to file: " + file.getName());
+ boolean saved = false;
+
+ if (!StorageOptionChooser.verifyStorageOptions(document, this)) {
+ // User cancelled the dialog
+ return false;
+ }
+
+ RocketSaver saver = new OpenRocketSaver();
+ try {
+ saver.save(file, document);
+ document.setFile(file);
+ document.setSaved(true);
+ saved = true;
+ } catch (IOException e) {
+ JOptionPane.showMessageDialog(this, new String[] {
+ "An I/O error occurred while saving:",
+ e.getMessage() }, "Saving failed", JOptionPane.ERROR_MESSAGE);
+ }
+ setTitle();
+ return saved;
+ }
+
+
+ private boolean closeAction() {
+ if (!document.isSaved()) {
+ ComponentConfigDialog.hideDialog();
+ int result = JOptionPane.showConfirmDialog(this,
+ "Design '"+rocket.getName()+"' has not been saved. " +
+ "Do you want to save it?",
+ "Design not saved", JOptionPane.YES_NO_CANCEL_OPTION,
+ JOptionPane.QUESTION_MESSAGE);
+ if (result == JOptionPane.YES_OPTION) {
+ // Save
+ if (!saveAction())
+ return false; // If save was interrupted
+ } else if (result == JOptionPane.NO_OPTION) {
+ // Don't save: No-op
+ } else {
+ // Cancel or close
+ return false;
+ }
+ }
+
+ // Rocket has been saved or discarded
+ this.dispose();
+
+ // TODO: LOW: Close only dialogs that have this frame as their parent
+ ComponentConfigDialog.hideDialog();
+ ComponentAnalysisDialog.hideDialog();
+
+ frames.remove(this);
+ if (frames.isEmpty())
+ System.exit(0);
+ return true;
+ }
+
+ /**
+ * Open a new design window with a basic rocket+stage.
+ */
+ public static void newAction() {
+ Rocket rocket = new Rocket();
+ Stage stage = new Stage();
+ stage.setName("Sustainer");
+ rocket.addChild(stage);
+ OpenRocketDocument doc = new OpenRocketDocument(rocket);
+ doc.setSaved(true);
+
+ BasicFrame frame = new BasicFrame(doc);
+ frame.replaceable = true;
+ frame.setVisible(true);
+ ComponentConfigDialog.showDialog(frame, doc, rocket);
+ }
+
+ /**
+ * Quit the application. Confirms saving unsaved designs. The action of File->Quit.
+ */
+ public static void quitAction() {
+ for (int i=frames.size()-1; i>=0; i--) {
+ if (!frames.get(i).closeAction()) {
+ // Close canceled
+ return;
+ }
+ }
+ // Should not be reached, but just in case
+ System.exit(0);
+ }
+
+
+ /**
+ * Set the title of the frame, taking into account the name of the rocket, file it
+ * has been saved to (if any) and saved status.
+ */
+ private void setTitle() {
+ File file = document.getFile();
+ boolean saved = document.isSaved();
+ String title;
+
+ title = rocket.getName();
+ if (file!=null) {
+ title = title + " ("+file.getName()+")";
+ }
+ if (!saved)
+ title = "*" + title;
+
+ setTitle(title);
+ }
+
+
+
+
+
+
+ public static void main(String[] args) {
+
+ /*
+ * Set the look-and-feel. On Linux, Motif/Metal is sometimes incorrectly used
+ * which is butt-ugly, so if the system l&f is Motif/Metal, we search for a few
+ * other alternatives.
+ */
+ try {
+ UIManager.LookAndFeelInfo[] info = UIManager.getInstalledLookAndFeels();
+// System.out.println("Available look-and-feels:");
+// for (int i=0; i<info.length; i++) {
+// System.out.println(" "+info[i]);
+// }
+
+ // Set system L&F
+ UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
+
+ // Check whether we have an ugly L&F
+ LookAndFeel laf = UIManager.getLookAndFeel();
+ if (laf == null ||
+ laf.getName().matches(".*[mM][oO][tT][iI][fF].*") ||
+ laf.getName().matches(".*[mM][eE][tT][aA][lL].*")) {
+
+ // Search for better LAF
+ for (UIManager.LookAndFeelInfo l: info) {
+ if (l.getName().matches(".*[gG][tT][kK].*")) {
+ UIManager.setLookAndFeel(l.getClassName());
+ break;
+ }
+ if (l.getName().contains(".*[wW][iI][nN].*")) {
+ UIManager.setLookAndFeel(l.getClassName());
+ break;
+ }
+ if (l.getName().contains(".*[mM][aA][cC].*")) {
+ UIManager.setLookAndFeel(l.getClassName());
+ break;
+ }
+ }
+ }
+ } catch (Exception e) {
+ System.err.println("Error setting LAF: " + e);
+ }
+
+ // Set tooltip delay time. Tooltips are used in MotorChooserDialog extensively.
+ ToolTipManager.sharedInstance().setDismissDelay(30000);
+
+
+ // Load defaults
+ Prefs.loadDefaultUnits();
+
+
+ // Check command-line for files
+ boolean opened = false;
+ for (String file: args) {
+ if (open(new File(file))) {
+ opened = true;
+ }
+ }
+
+ if (!opened) {
+ newAction();
+ }
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.main;
+
+
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+import java.lang.reflect.Constructor;
+
+import javax.swing.Icon;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JFrame;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JViewport;
+import javax.swing.Scrollable;
+import javax.swing.SwingConstants;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.tree.TreePath;
+import javax.swing.tree.TreeSelectionModel;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.gui.ResizeLabel;
+import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
+import net.sf.openrocket.rocketcomponent.BodyComponent;
+import net.sf.openrocket.rocketcomponent.BodyTube;
+import net.sf.openrocket.rocketcomponent.Bulkhead;
+import net.sf.openrocket.rocketcomponent.CenteringRing;
+import net.sf.openrocket.rocketcomponent.EllipticalFinSet;
+import net.sf.openrocket.rocketcomponent.EngineBlock;
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.InnerTube;
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+import net.sf.openrocket.rocketcomponent.MassComponent;
+import net.sf.openrocket.rocketcomponent.NoseCone;
+import net.sf.openrocket.rocketcomponent.Parachute;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.ShockCord;
+import net.sf.openrocket.rocketcomponent.Streamer;
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
+import net.sf.openrocket.rocketcomponent.TubeCoupler;
+import net.sf.openrocket.util.Prefs;
+
+/**
+ * A component that contains addition buttons to add different types of rocket components
+ * to a rocket. It enables and disables buttons according to the current selection of a
+ * TreeSelectionModel.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class ComponentAddButtons extends JPanel implements Scrollable {
+
+ private static final int ROWS = 3;
+ private static final int MAXCOLS = 6;
+ private static final String BUTTONPARAM = "grow, sizegroup buttons";
+
+ private static final int GAP = 5;
+ private static final int EXTRASPACE = 0;
+
+ private final ComponentButton[][] buttons;
+
+ private final OpenRocketDocument document;
+ private final TreeSelectionModel selectionModel;
+ private final JViewport viewport;
+ private final MigLayout layout;
+
+ private final int width, height;
+
+
+ public ComponentAddButtons(OpenRocketDocument document, TreeSelectionModel model,
+ JViewport viewport) {
+
+ super();
+ String constaint = "[min!]";
+ for (int i=1; i<MAXCOLS; i++)
+ constaint = constaint + GAP + "[min!]";
+
+ layout = new MigLayout("fill",constaint);
+ setLayout(layout);
+ this.document = document;
+ this.selectionModel = model;
+ this.viewport = viewport;
+
+ buttons = new ComponentButton[ROWS][];
+ int row = 0;
+
+ ////////////////////////////////////////////
+
+// addButtonRow("Body components",row,
+// new ComponentButton(NoseCone.class,"Nose cone") {
+// @Override
+// public boolean isAddable(RocketComponent c) {
+// if (!(c instanceof ComponentAssembly))
+// return false;
+// if (c.getSiblingCount() == 0)
+// return true;
+// return false;
+// }
+// },
+// new BodyComponentButton(BodyTube.class,"Body tube"),
+// new BodyComponentButton(null,"Transition"));
+
+
+ addButtonRow("Body components and fin sets",row,
+ new BodyComponentButton(NoseCone.class,"Nose cone"),
+ new BodyComponentButton(BodyTube.class,"Body tube"),
+ new BodyComponentButton(Transition.class,"Transition"),
+ new FinButton(TrapezoidFinSet.class,"Trapezoidal"), // TODO: MEDIUM: freer fin placing
+ new FinButton(EllipticalFinSet.class,"Elliptical"),
+ new FinButton(FreeformFinSet.class,"Freeform"),
+ new FinButton(LaunchLug.class,"Launch lug")
+ );
+
+ row++;
+/////
+
+
+ /////////////////////////////////////////////
+
+ addButtonRow("Inner component",row,
+ new ComponentButton(InnerTube.class, "Inner tube"),
+ new ComponentButton(TubeCoupler.class, "Coupler"),
+ new ComponentButton(CenteringRing.class, "Centering\nring"),
+ new ComponentButton(Bulkhead.class, "Bulkhead"),
+ new ComponentButton(EngineBlock.class, "Engine\nblock"));
+
+ row++;
+
+ ////////////////////////////////////////////
+
+ addButtonRow("Mass objects",row,
+ new ComponentButton(Parachute.class, "Parachute"),
+ new ComponentButton(Streamer.class, "Streamer"),
+ new ComponentButton(ShockCord.class, "Shock cord"),
+// new ComponentButton("Motor clip"),
+// new ComponentButton("Payload"),
+ new ComponentButton(MassComponent.class,"Mass\ncomponent")
+ );
+
+
+ // Get maximum button size
+ int w=0, h=0;
+
+ for (row=0; row < buttons.length; row++) {
+ for (int col=0; col < buttons[row].length; col++) {
+ Dimension d = buttons[row][col].getPreferredSize();
+ if (d.width > w)
+ w = d.width;
+ if (d.height > h)
+ h = d.height;
+ }
+ }
+
+ // Set all buttons to maximum size
+ System.out.println("Setting w="+w+" h="+h);
+ width=w;
+ height=h;
+ Dimension d = new Dimension(width,height);
+ for (row=0; row < buttons.length; row++) {
+ for (int col=0; col < buttons[row].length; col++) {
+ buttons[row][col].setMinimumSize(d);
+ buttons[row][col].setPreferredSize(d);
+ buttons[row][col].getComponent(0).validate();
+ }
+ }
+
+ // Add viewport listener if viewport provided
+ if (viewport != null) {
+ viewport.addChangeListener(new ChangeListener() {
+ private int oldWidth = -1;
+ public void stateChanged(ChangeEvent e) {
+ Dimension d = ComponentAddButtons.this.viewport.getExtentSize();
+ if (d.width != oldWidth) {
+ oldWidth = d.width;
+ flowButtons();
+ }
+ }
+ });
+ }
+
+ add(new JPanel(),"grow");
+ }
+
+
+ /**
+ * Adds a row of buttons to the panel.
+ * @param label Label placed before the row
+ * @param row Row number
+ * @param b List of ComponentButtons to place on the row
+ */
+ private void addButtonRow(String label, int row, ComponentButton ... b) {
+ if (row>0)
+ add(new JLabel(label),"span, gaptop unrel, wrap");
+ else
+ add(new JLabel(label),"span, gaptop 0, wrap");
+
+ int col=0;
+ buttons[row] = new ComponentButton[b.length];
+
+ for (int i=0; i<b.length; i++) {
+ buttons[row][col] = b[i];
+ if (i < b.length-1)
+ add(b[i],BUTTONPARAM);
+ else
+ add(b[i],BUTTONPARAM+", wrap");
+ col++;
+ }
+ }
+
+
+ /**
+ * Flows the buttons in all rows of the panel. If a button would come too close
+ * to the right edge of the viewport, "newline" is added to its constraints flowing
+ * it to the next line.
+ */
+ private void flowButtons() {
+ if (viewport==null)
+ return;
+
+ int w;
+
+ Dimension d = viewport.getExtentSize();
+
+ for (int row=0; row < buttons.length; row++) {
+ w=0;
+ for (int col=0; col < buttons[row].length; col++) {
+ w += GAP+width;
+ String param = BUTTONPARAM+",width "+width+"!,height "+height+"!";
+
+ if (w+EXTRASPACE > d.width) {
+ param = param + ",newline";
+ w = GAP+width;
+ }
+ if (col == buttons[row].length-1)
+ param = param + ",wrap";
+ layout.setComponentConstraints(buttons[row][col], param);
+ }
+ }
+ revalidate();
+ }
+
+
+
+ /**
+ * Class for a component button.
+ */
+ private class ComponentButton extends JButton implements TreeSelectionListener {
+ protected Class<? extends RocketComponent> componentClass = null;
+ private Constructor<? extends RocketComponent> constructor = null;
+
+ /** Only label, no icon. */
+ public ComponentButton(String text) {
+ this(text,null,null);
+ }
+
+ /**
+ * Constructor with icon and label. The icon and label are placed into the button.
+ * The label may contain "\n" as a newline.
+ */
+ public ComponentButton(String text, Icon enabled, Icon disabled) {
+ super();
+ setLayout(new MigLayout("fill, flowy, insets 0, gap 0","",""));
+
+ add(new JLabel(),"push, sizegroup spacing");
+
+ // Add Icon
+ if (enabled != null) {
+ JLabel label = new JLabel(enabled);
+ if (disabled != null)
+ label.setDisabledIcon(disabled);
+ add(label,"growx");
+ }
+
+ // Add labels
+ String[] l = text.split("\n");
+ for (int i=0; i<l.length; i++) {
+ add(new ResizeLabel(l[i],SwingConstants.CENTER,-3.0f),"growx");
+ }
+
+ add(new JLabel(),"push, sizegroup spacing");
+
+ valueChanged(null); // Update enabled status
+ selectionModel.addTreeSelectionListener(this);
+ }
+
+
+ /**
+ * Main constructor that should be used. The generated component type is specified
+ * and the text. The icons are fetched based on the component type.
+ */
+ public ComponentButton(Class<? extends RocketComponent> c, String text) {
+ this(text,ComponentIcons.getLargeIcon(c),ComponentIcons.getLargeDisabledIcon(c));
+
+ if (c==null)
+ return;
+
+ componentClass = c;
+
+ try {
+ constructor = c.getConstructor();
+ } catch (NoSuchMethodException e) {
+ throw new IllegalArgumentException("Unable to get default "+
+ "constructor for class "+c,e);
+ }
+ }
+
+
+ /**
+ * Return whether the current component is addable when the component c is selected.
+ * c is null if there is no selection. The default is to use c.isCompatible(class).
+ */
+ public boolean isAddable(RocketComponent c) {
+ if (c==null)
+ return false;
+ if (componentClass==null)
+ return false;
+ return c.isCompatible(componentClass);
+ }
+
+ /**
+ * Return the position to add the component if component c is selected currently.
+ * The first element of the returned array is the RocketComponent to add the component
+ * to, and the second (in any) an Integer telling the position of the component.
+ * A return value of null means that the user cancelled addition of the component.
+ * If the array has only one element, the component is added at the end of the sibling
+ * list. By default returns the end of the currently selected component.
+ *
+ * @param c The component currently selected
+ * @return The position to add the new component to, or null if should not add.
+ */
+ public Object[] getAdditionPosition(RocketComponent c) {
+ return new Object[] { c };
+ }
+
+ /**
+ * Updates the enabled status of the button.
+ * TODO: LOW: What about updates to the rocket tree?
+ */
+ public void valueChanged(TreeSelectionEvent e) {
+ updateEnabled();
+ }
+
+ /**
+ * Sets the enabled status of the button and all subcomponents.
+ */
+ @Override
+ public void setEnabled(boolean enabled) {
+ super.setEnabled(enabled);
+ Component[] c = getComponents();
+ for (int i=0; i<c.length; i++)
+ c[i].setEnabled(enabled);
+ }
+
+
+ /**
+ * Update the enabled status of the button.
+ */
+ private void updateEnabled() {
+ RocketComponent c=null;
+ TreePath p = selectionModel.getSelectionPath();
+ if (p!=null)
+ c = (RocketComponent)p.getLastPathComponent();
+ setEnabled(isAddable(c));
+ }
+
+
+ @Override
+ protected void fireActionPerformed(ActionEvent event) {
+ super.fireActionPerformed(event);
+ RocketComponent c = null;
+ Integer position = null;
+
+ TreePath p = selectionModel.getSelectionPath();
+ if (p!= null)
+ c = (RocketComponent)p.getLastPathComponent();
+ if (c != null) {
+ Object[] pos = getAdditionPosition(c);
+ if (pos==null || pos.length==0) {
+ // Cancel addition
+ return;
+ }
+
+ c = (RocketComponent)pos[0];
+ if (pos.length>1)
+ position = (Integer)pos[1];
+ }
+
+ if (c == null) {
+ // Should not occur
+ System.err.println("ERROR: Could not place new component.");
+ Thread.dumpStack();
+ updateEnabled();
+ return;
+ }
+
+ if (constructor == null) {
+ System.err.println("ERROR: Construction of type not supported yet.");
+ return;
+ }
+
+ RocketComponent component;
+ try {
+ component = (RocketComponent)constructor.newInstance();
+ } catch (Exception e) {
+ throw new RuntimeException("Could not construct new instance of class "+
+ constructor,e);
+ }
+
+ // Next undo position is set by opening the configuration dialog
+ document.addUndoPosition("Add " + component.getComponentName());
+
+
+ if (position == null)
+ c.addChild(component);
+ else
+ c.addChild(component, position);
+
+ // Select new component and open config dialog
+ selectionModel.setSelectionPath(ComponentTreeModel.makeTreePath(component));
+
+ JFrame parent = null;
+ for (Component comp = ComponentAddButtons.this; comp != null;
+ comp = comp.getParent()) {
+ if (comp instanceof JFrame) {
+ parent = (JFrame) comp;
+ break;
+ }
+ }
+
+ ComponentConfigDialog.showDialog(parent, document, component);
+ }
+ }
+
+ /**
+ * A class suitable for BodyComponents. Addition is allowed ...
+ */
+ private class BodyComponentButton extends ComponentButton {
+
+ public BodyComponentButton(Class<? extends RocketComponent> c, String text) {
+ super(c, text);
+ }
+
+ public BodyComponentButton(String text, Icon enabled, Icon disabled) {
+ super(text, enabled, disabled);
+ }
+
+ public BodyComponentButton(String text) {
+ super(text);
+ }
+
+ @Override
+ public boolean isAddable(RocketComponent c) {
+ if (super.isAddable(c))
+ return true;
+ if (c instanceof BodyComponent) // Handled separately
+ return true;
+ return false;
+ }
+
+ @Override
+ public Object[] getAdditionPosition(RocketComponent c) {
+ if (super.isAddable(c)) // Handled automatically
+ return super.getAdditionPosition(c);
+
+ // Handle BodyComponent separately
+ if (!(c instanceof BodyComponent))
+ return null;
+ RocketComponent parent = c.getParent();
+ assert(parent != null);
+
+ // Check whether to insert between or at the end.
+ // 0 = ask, 1 = in between, 2 = at the end
+ int pos = Prefs.getChoise(Prefs.BODY_COMPONENT_INSERT_POSITION_KEY, 2, 0);
+ if (pos==0) {
+ if (parent.getChildPosition(c) == parent.getChildCount()-1)
+ pos = 2; // Selected component is the last component
+ else
+ pos = askPosition();
+ }
+
+ switch (pos) {
+ case 0:
+ // Cancel
+ return null;
+ case 1:
+ // Insert after current position
+ return new Object[] { parent, new Integer(parent.getChildPosition(c)+1) };
+ case 2:
+ // Insert at the end of the parent
+ return new Object[] { parent };
+ default:
+ System.err.println("ERROR: Bad position type: "+pos);
+ Thread.dumpStack();
+ return null;
+ }
+ }
+
+ private int askPosition() {
+ Object[] options = { "Insert here", "Add to the end", "Cancel" };
+
+ JPanel panel = new JPanel(new MigLayout());
+ JCheckBox check = new JCheckBox("Do not ask me again");
+ panel.add(check,"wrap");
+ panel.add(new ResizeLabel("You can change the default operation in the " +
+ "preferences.",-2));
+
+ int sel = JOptionPane.showOptionDialog(null, // parent component
+ new Object[] {
+ "Insert the component after the current component or as the last " +
+ "component?",
+ panel },
+ "Select component position", // title
+ JOptionPane.DEFAULT_OPTION, // default selections
+ JOptionPane.QUESTION_MESSAGE, // dialog type
+ null, // icon
+ options, // options
+ options[0]); // initial value
+
+ switch (sel) {
+ case JOptionPane.CLOSED_OPTION:
+ case 2:
+ // Cancel
+ return 0;
+ case 0:
+ // Insert
+ sel = 1;
+ break;
+ case 1:
+ // Add
+ sel = 2;
+ break;
+ default:
+ System.err.println("ERROR: JOptionPane returned "+sel);
+ Thread.dumpStack();
+ return 0;
+ }
+
+ if (check.isSelected()) {
+ // Save the preference
+ Prefs.NODE.putInt(Prefs.BODY_COMPONENT_INSERT_POSITION_KEY, sel);
+ }
+ return sel;
+ }
+
+ }
+
+
+
+ /**
+ * Class for fin sets, that attach only to BodyTubes.
+ */
+ private class FinButton extends ComponentButton {
+ public FinButton(Class<? extends RocketComponent> c, String text) {
+ super(c, text);
+ }
+
+ public FinButton(String text, Icon enabled, Icon disabled) {
+ super(text, enabled, disabled);
+ }
+
+ public FinButton(String text) {
+ super(text);
+ }
+
+ @Override
+ public boolean isAddable(RocketComponent c) {
+ if (c==null)
+ return false;
+ return (c.getClass().equals(BodyTube.class));
+ }
+ }
+
+
+
+ ///////// Scrolling functionality
+
+ @Override
+ public Dimension getPreferredScrollableViewportSize() {
+ return getPreferredSize();
+ }
+
+
+ @Override
+ public int getScrollableBlockIncrement(Rectangle visibleRect,
+ int orientation, int direction) {
+ if (orientation == SwingConstants.VERTICAL)
+ return visibleRect.height * 8 / 10;
+ return 10;
+ }
+
+
+ @Override
+ public boolean getScrollableTracksViewportHeight() {
+ return false;
+ }
+
+
+ @Override
+ public boolean getScrollableTracksViewportWidth() {
+ return true;
+ }
+
+
+ @Override
+ public int getScrollableUnitIncrement(Rectangle visibleRect,
+ int orientation, int direction) {
+ return 10;
+ }
+
+}
+
--- /dev/null
+package net.sf.openrocket.gui.main;
+
+import java.awt.image.BufferedImage;
+import java.io.IOException;
+import java.net.URL;
+import java.util.HashMap;
+
+import javax.imageio.ImageIO;
+import javax.swing.Icon;
+import javax.swing.ImageIcon;
+
+import net.sf.openrocket.rocketcomponent.BodyTube;
+import net.sf.openrocket.rocketcomponent.Bulkhead;
+import net.sf.openrocket.rocketcomponent.CenteringRing;
+import net.sf.openrocket.rocketcomponent.EllipticalFinSet;
+import net.sf.openrocket.rocketcomponent.EngineBlock;
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.InnerTube;
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+import net.sf.openrocket.rocketcomponent.MassComponent;
+import net.sf.openrocket.rocketcomponent.NoseCone;
+import net.sf.openrocket.rocketcomponent.Parachute;
+import net.sf.openrocket.rocketcomponent.ShockCord;
+import net.sf.openrocket.rocketcomponent.Streamer;
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
+import net.sf.openrocket.rocketcomponent.TubeCoupler;
+
+
+public class ComponentIcons {
+
+ private static final String ICON_DIRECTORY = "pix/componenticons/";
+ private static final String SMALL_SUFFIX = "-small.png";
+ private static final String LARGE_SUFFIX = "-large.png";
+
+ private static final HashMap<Class<?>,ImageIcon> SMALL_ICONS =
+ new HashMap<Class<?>,ImageIcon>();
+ private static final HashMap<Class<?>,ImageIcon> LARGE_ICONS =
+ new HashMap<Class<?>,ImageIcon>();
+ private static final HashMap<Class<?>,ImageIcon> DISABLED_ICONS =
+ new HashMap<Class<?>,ImageIcon>();
+
+ static {
+ load("nosecone", "Nose cone", NoseCone.class);
+ load("bodytube", "Body tube", BodyTube.class);
+ load("transition", "Transition", Transition.class);
+ load("trapezoidfin", "Trapezoidal fin set", TrapezoidFinSet.class);
+ load("ellipticalfin", "Elliptical fin set", EllipticalFinSet.class);
+ load("freeformfin", "Freeform fin set", FreeformFinSet.class);
+ load("launchlug", "Launch lug", LaunchLug.class);
+ load("innertube", "Inner tube", InnerTube.class);
+ load("tubecoupler", "Tube coupler", TubeCoupler.class);
+ load("centeringring", "Centering ring", CenteringRing.class);
+ load("bulkhead", "Bulk head", Bulkhead.class);
+ load("engineblock", "Engine block", EngineBlock.class);
+ load("parachute", "Parachute", Parachute.class);
+ load("streamer", "Streamer", Streamer.class);
+ load("shockcord", "Shock cord", ShockCord.class);
+ load("mass", "Mass component", MassComponent.class);
+ }
+
+ private static void load(String filename, String name, Class<?> componentClass) {
+ ImageIcon icon = loadSmall(ICON_DIRECTORY + filename + SMALL_SUFFIX, name);
+ SMALL_ICONS.put(componentClass, icon);
+
+ ImageIcon[] icons = loadLarge(ICON_DIRECTORY + filename + LARGE_SUFFIX, name);
+ LARGE_ICONS.put(componentClass, icons[0]);
+ DISABLED_ICONS.put(componentClass, icons[1]);
+ }
+
+
+
+ public static Icon getSmallIcon(Class<?> c) {
+ return SMALL_ICONS.get(c);
+ }
+ public static Icon getLargeIcon(Class<?> c) {
+ return LARGE_ICONS.get(c);
+ }
+ public static Icon getLargeDisabledIcon(Class<?> c) {
+ return DISABLED_ICONS.get(c);
+ }
+
+
+
+
+ private static ImageIcon loadSmall(String file, String desc) {
+ URL url = ClassLoader.getSystemResource(file);
+ if (url==null) {
+ System.err.println("ERROR: Couldn't find file: " + file);
+ return null;
+ }
+ return new ImageIcon(url, desc);
+ }
+
+
+ private static ImageIcon[] loadLarge(String file, String desc) {
+ ImageIcon[] icons = new ImageIcon[2];
+
+ URL url = ClassLoader.getSystemResource(file);
+ if (url != null) {
+ BufferedImage bi,bi2;
+ try {
+ bi = ImageIO.read(url);
+ bi2 = ImageIO.read(url); // How the fsck can one duplicate a BufferedImage???
+ } catch (IOException e) {
+ System.err.println("ERROR: Couldn't read file: "+file);
+ e.printStackTrace();
+ return new ImageIcon[]{null,null};
+ }
+
+ icons[0] = new ImageIcon(bi,desc);
+
+ // Create disabled icon
+ if (false) { // Fade using alpha
+
+ int rgb[] = bi2.getRGB(0,0,bi2.getWidth(),bi2.getHeight(),null,0,bi2.getWidth());
+ for (int i=0; i<rgb.length; i++) {
+ final int alpha = (rgb[i]>>24)&0xFF;
+ rgb[i] = (rgb[i]&0xFFFFFF) | (alpha/3)<<24;
+
+ //rgb[i] = (rgb[i]&0xFFFFFF) | ((rgb[i]>>1)&0x3F000000);
+ }
+ bi2.setRGB(0, 0, bi2.getWidth(), bi2.getHeight(), rgb, 0, bi2.getWidth());
+
+ } else { // Raster alpha
+
+ for (int x=0; x < bi.getWidth(); x++) {
+ for (int y=0; y < bi.getHeight(); y++) {
+ if ((x+y)%2 == 0) {
+ bi2.setRGB(x, y, 0);
+ }
+ }
+ }
+
+ }
+
+ icons[1] = new ImageIcon(bi2,desc + " (disabled)");
+
+ return icons;
+ } else {
+ System.err.println("ERROR: Couldn't find file: " + file);
+ return new ImageIcon[]{null,null};
+ }
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.main;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+
+import javax.swing.Icon;
+import javax.swing.JTree;
+
+import net.sf.openrocket.rocketcomponent.*;
+
+
+public class ComponentTree extends JTree {
+ private static final long serialVersionUID = 1L;
+
+ public ComponentTree(RocketComponent root) {
+ super();
+ this.setModel(new ComponentTreeModel(root,this));
+// this.setModel(new BareComponentTreeModel(root,this));
+ setToggleClickCount(0);
+
+ javax.swing.plaf.basic.BasicTreeUI ui = new javax.swing.plaf.basic.BasicTreeUI();
+ this.setUI(ui);
+
+ ui.setExpandedIcon(TreeIcon.MINUS);
+ ui.setCollapsedIcon(TreeIcon.PLUS);
+
+ ui.setLeftChildIndent(15);
+
+
+ setBackground(Color.WHITE);
+ setShowsRootHandles(false);
+
+ setCellRenderer(new ComponentTreeRenderer());
+
+ // Expand whole tree by default
+ expandTree();
+ }
+
+
+ public void expandTree() {
+ for (int i=0; i<getRowCount(); i++)
+ expandRow(i);
+
+ }
+
+ private static class TreeIcon implements Icon{
+ public static final Icon PLUS = new TreeIcon(true);
+ public static final Icon MINUS = new TreeIcon(false);
+
+ // Implementation:
+
+ private final static int width = 9;
+ private final static int height = 9;
+ private final static BasicStroke stroke = new BasicStroke(2);
+ private boolean plus;
+
+ private TreeIcon(boolean plus) {
+ this.plus = plus;
+ }
+
+ public void paintIcon(Component c, Graphics g, int x, int y) {
+ Graphics2D g2 = (Graphics2D)g.create();
+
+ g2.setColor(Color.WHITE);
+ g2.fillRect(x,y,width,height);
+
+ g2.setColor(Color.DARK_GRAY);
+ g2.drawRect(x,y,width,height);
+
+ g2.setStroke(stroke);
+ g2.drawLine(x+3, y+(height+1)/2, x+width-2, y+(height+1)/2);
+ if (plus)
+ g2.drawLine(x+(width+1)/2, y+3, x+(width+1)/2, y+height-2);
+
+ g2.dispose();
+ }
+
+ public int getIconWidth() {
+ return width;
+ }
+
+ public int getIconHeight() {
+ return height;
+ }
+ }
+
+}
+
+
--- /dev/null
+package net.sf.openrocket.gui.main;
+
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.Iterator;
+import java.util.List;
+
+import javax.swing.JTree;
+import javax.swing.event.TreeModelEvent;
+import javax.swing.event.TreeModelListener;
+import javax.swing.tree.TreeModel;
+import javax.swing.tree.TreePath;
+
+import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
+import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+
+/**
+ * A TreeModel that implements viewing of the rocket tree structure.
+ * This transforms the internal view (which has nested Stages) into the user-view
+ * (which has parallel Stages).
+ *
+ * To view with the internal structure, switch to using BareComponentTreeModel in
+ * ComponentTree.java. NOTE: This class's makeTreePath will still be used, which
+ * will create illegal paths, which results in problems with selections.
+ *
+ * TODO: MEDIUM: When converting a component to another component this model given
+ * outdated information, since it uses the components themselves as the nodes.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class ComponentTreeModel implements TreeModel, ComponentChangeListener {
+ ArrayList<TreeModelListener> listeners = new ArrayList<TreeModelListener>();
+
+ private final RocketComponent root;
+ private final JTree tree;
+
+ public ComponentTreeModel(RocketComponent root, JTree tree) {
+ this.root = root;
+ this.tree = tree;
+ root.addComponentChangeListener(this);
+ }
+
+
+ public Object getChild(Object parent, int index) {
+ RocketComponent component = (RocketComponent)parent;
+
+ try {
+ return component.getChild(index);
+ } catch (IndexOutOfBoundsException e) {
+ return null;
+ }
+ }
+
+
+ public int getChildCount(Object parent) {
+ RocketComponent c = (RocketComponent)parent;
+
+ return c.getChildCount();
+ }
+
+
+ public int getIndexOfChild(Object parent, Object child) {
+ if (parent==null || child==null)
+ return -1;
+
+ RocketComponent p = (RocketComponent)parent;
+ RocketComponent c = (RocketComponent)child;
+
+ return p.getChildPosition(c);
+ }
+
+ public Object getRoot() {
+ return root;
+ }
+
+ public boolean isLeaf(Object node) {
+ RocketComponent c = (RocketComponent)node;
+
+ return (c.getChildCount() == 0);
+ }
+
+ public void addTreeModelListener(TreeModelListener l) {
+ listeners.add(l);
+ }
+
+ public void removeTreeModelListener(TreeModelListener l) {
+ listeners.remove(l);
+ }
+
+ private void fireTreeNodesChanged() {
+ Object[] path = { root };
+ TreeModelEvent e = new TreeModelEvent(this,path);
+ Object[] l = listeners.toArray();
+ for (int i=0; i<l.length; i++)
+ ((TreeModelListener)l[i]).treeNodesChanged(e);
+ }
+
+
+ @SuppressWarnings("unused")
+ private void printStructure(TreePath p, int level) {
+ String indent="";
+ for (int i=0; i<level; i++)
+ indent += " ";
+ System.out.println(indent+p+
+ ": isVisible:"+tree.isVisible(p)+
+ " isCollapsed:"+tree.isCollapsed(p)+
+ " isExpanded:"+tree.isExpanded(p));
+ Object parent = p.getLastPathComponent();
+ for (int i=0; i<getChildCount(parent); i++) {
+ Object child = getChild(parent,i);
+ TreePath path = makeTreePath((RocketComponent)child);
+ printStructure(path,level+1);
+ }
+ }
+
+
+ private void fireTreeStructureChanged(RocketComponent source) {
+ Object[] path = { root };
+
+
+ // Get currently expanded path IDs
+ Enumeration<TreePath> enumer = tree.getExpandedDescendants(new TreePath(path));
+ ArrayList<String> expanded = new ArrayList<String>();
+ if (enumer != null) {
+ while (enumer.hasMoreElements()) {
+ TreePath p = enumer.nextElement();
+ expanded.add(((RocketComponent)p.getLastPathComponent()).getID());
+ }
+ }
+
+ // Send structure change event
+ TreeModelEvent e = new TreeModelEvent(this,path);
+ Object[] l = listeners.toArray();
+ for (int i=0; i<l.length; i++)
+ ((TreeModelListener)l[i]).treeStructureChanged(e);
+
+ // Re-expand the paths
+ for (String id: expanded) {
+ RocketComponent c = root.findComponent(id);
+ if (c==null)
+ continue;
+ tree.expandPath(makeTreePath(c));
+ }
+ if (source != null) {
+ TreePath p = makeTreePath(source);
+ tree.makeVisible(p);
+ tree.expandPath(p);
+ }
+ }
+
+ public void valueForPathChanged(TreePath path, Object newValue) {
+ System.err.println("ERROR: valueForPathChanged called?!");
+ }
+
+
+ public void componentChanged(ComponentChangeEvent e) {
+ if (e.isTreeChange() || e.isUndoChange()) {
+ // Tree must be fully updated also in case of an undo change
+ fireTreeStructureChanged((RocketComponent)e.getSource());
+ if (e.isTreeChange() && e.isUndoChange()) {
+ // If the undo has changed the tree structure, some elements may be hidden
+ // unnecessarily
+ // TODO: LOW: Could this be performed better?
+ expandAll();
+ }
+ } else if (e.isOtherChange()) {
+ fireTreeNodesChanged();
+ }
+ }
+
+ public void expandAll() {
+ Iterator<RocketComponent> iterator = root.deepIterator();
+ while (iterator.hasNext()) {
+ tree.makeVisible(makeTreePath(iterator.next()));
+ }
+ }
+
+
+ public static TreePath makeTreePath(RocketComponent component) {
+ RocketComponent c = component;
+
+ List<RocketComponent> list = new ArrayList<RocketComponent>();
+
+ while (c != null) {
+ list.add(0,c);
+ c = c.getParent();
+ }
+
+ return new TreePath(list.toArray());
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.main;
+
+
+
+import java.awt.Component;
+
+import javax.swing.JTree;
+import javax.swing.tree.DefaultTreeCellRenderer;
+
+public class ComponentTreeRenderer extends DefaultTreeCellRenderer {
+
+ @Override
+ public Component getTreeCellRendererComponent(
+ JTree tree,
+ Object value,
+ boolean sel,
+ boolean expanded,
+ boolean leaf,
+ int row,
+ boolean hasFocus) {
+
+ super.getTreeCellRendererComponent(tree, value, sel, expanded, leaf, row, hasFocus);
+
+ setIcon(ComponentIcons.getSmallIcon(value.getClass()));
+
+ return this;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.main;
+
+import java.awt.Font;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.io.BufferedReader;
+import java.io.InputStreamReader;
+
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JFrame;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTextArea;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.gui.ResizeLabel;
+import net.sf.openrocket.util.GUIUtil;
+
+public class LicenseDialog extends JDialog {
+ private static final String LICENSE_FILENAME = "LICENSE.TXT";
+
+ private static final String DEFAULT_LICENSE_TEXT =
+ "\n" +
+ "Error: Unable to load " + LICENSE_FILENAME + "!\n" +
+ "\n" +
+ "OpenRocket is licensed under the GNU GPL version 3, with additional permissions.\n" +
+ "See http://openrocket.sourceforge.net/ for details.";
+
+ public LicenseDialog(JFrame parent) {
+ super(parent, true);
+
+ JPanel panel = new JPanel(new MigLayout("fill"));
+
+ panel.add(new ResizeLabel("OpenRocket license", 10), "ax 50%, wrap para");
+
+ String licenseText;
+ try {
+
+ BufferedReader reader = new BufferedReader(
+ new InputStreamReader(ClassLoader.getSystemResourceAsStream(LICENSE_FILENAME)));
+ StringBuffer sb = new StringBuffer();
+ for (String s = reader.readLine(); s != null; s = reader.readLine()) {
+ sb.append(s);
+ sb.append('\n');
+ }
+ licenseText = sb.toString();
+
+ } catch (Exception e) {
+
+ licenseText = DEFAULT_LICENSE_TEXT;
+
+ }
+
+ JTextArea text = new JTextArea(licenseText);
+ text.setFont(new Font(Font.MONOSPACED, Font.PLAIN, 12));
+ text.setRows(20);
+ text.setColumns(80);
+ text.setEditable(false);
+ panel.add(new JScrollPane(text),"grow, wrap para");
+
+ JButton close = new JButton("Close");
+ close.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ LicenseDialog.this.dispose();
+ }
+ });
+ panel.add(close, "right");
+
+ this.add(panel);
+ this.setTitle("OpenRocket license");
+ this.pack();
+ this.setLocationByPlatform(true);
+ GUIUtil.setDefaultButton(close);
+ GUIUtil.installEscapeCloseOperation(this);
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.main;
+
+
+import java.awt.Font;
+import java.awt.Frame;
+import java.awt.Rectangle;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.text.Collator;
+import java.util.Comparator;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.swing.DefaultComboBoxModel;
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.ListSelectionModel;
+import javax.swing.RowFilter;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.table.AbstractTableModel;
+import javax.swing.table.TableModel;
+import javax.swing.table.TableRowSorter;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.database.Databases;
+import net.sf.openrocket.gui.ResizeLabel;
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.GUIUtil;
+import net.sf.openrocket.util.Prefs;
+
+public class MotorChooserDialog extends JDialog {
+
+ private static final int SHOW_ALL = 0;
+ private static final int SHOW_SMALLER = 1;
+ private static final int SHOW_EXACT = 2;
+ private static final String[] SHOW_DESCRIPTIONS = {
+ "Show all motors",
+ "Show motors with diameter less than that of the motor mount",
+ "Show motors with diameter equal to that of the motor mount"
+ };
+ private static final int SHOW_MAX = 2;
+
+
+ private final double diameter;
+
+ private Motor selectedMotor = null;
+ private double selectedDelay = 0;
+
+ private JTable table;
+ private TableRowSorter<TableModel> sorter;
+ private JComboBox delayBox;
+ private MotorDatabaseModel model;
+
+ private boolean okClicked = false;
+
+
+ public MotorChooserDialog(double diameter) {
+ this(null,5,diameter,null);
+ }
+
+ public MotorChooserDialog(Motor current, double delay, double diameter) {
+ this(current,delay,diameter,null);
+ }
+
+ public MotorChooserDialog(Motor current, double delay, double diameter, Frame owner) {
+ super(owner, "Select a rocket motor", true);
+
+ JButton button;
+
+ this.selectedMotor = current;
+ this.selectedDelay = delay;
+ this.diameter = diameter;
+
+ JPanel panel = new JPanel(new MigLayout("fill"));
+
+ // Label
+ JLabel label = new JLabel("Select a rocket motor:");
+ label.setFont(label.getFont().deriveFont(Font.BOLD));
+ panel.add(label,"split 2, growx");
+
+ label = new JLabel("Motor mount diameter: " +
+ UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(diameter));
+ panel.add(label,"alignx 100%, wrap paragraph");
+
+
+ // Diameter selection
+ JComboBox combo = new JComboBox(SHOW_DESCRIPTIONS);
+ combo.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ JComboBox cb = (JComboBox) e.getSource();
+ int sel = cb.getSelectedIndex();
+ if ((sel < 0) || (sel > SHOW_MAX))
+ sel = SHOW_ALL;
+ switch (sel) {
+ case SHOW_ALL:
+ System.out.println("Setting filter: all");
+ sorter.setRowFilter(new MotorRowFilterAll());
+ break;
+
+ case SHOW_SMALLER:
+ System.out.println("Setting filter: smaller");
+ sorter.setRowFilter(new MotorRowFilterSmaller());
+ break;
+
+ case SHOW_EXACT:
+ System.out.println("Setting filter: exact");
+ sorter.setRowFilter(new MotorRowFilterExact());
+ break;
+
+ default:
+ assert(false) : "Should not occur.";
+ }
+ Prefs.putChoise("MotorDiameterMatch", sel);
+ setSelectionVisible();
+ }
+ });
+ panel.add(combo,"growx, wrap");
+
+
+ // Table, overridden to show meaningful tooltip texts
+ model = new MotorDatabaseModel(current);
+ table = new JTable(model) {
+ @Override
+ public String getToolTipText(MouseEvent e) {
+ java.awt.Point p = e.getPoint();
+ int colIndex = columnAtPoint(p);
+ int viewRow = rowAtPoint(p);
+ if (viewRow < 0)
+ return null;
+ int rowIndex = convertRowIndexToModel(viewRow);
+ Motor motor = model.getMotor(rowIndex);
+
+ if (colIndex < 0 || colIndex >= MotorColumns.values().length)
+ return null;
+
+ return MotorColumns.values()[colIndex].getToolTipText(motor);
+ }
+ };
+
+ // Set comparators and widths
+ table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
+ sorter = new TableRowSorter<TableModel>(model);
+ for (int i=0; i < MotorColumns.values().length; i++) {
+ MotorColumns column = MotorColumns.values()[i];
+ sorter.setComparator(i, column.getComparator());
+ table.getColumnModel().getColumn(i).setPreferredWidth(column.getWidth());
+ }
+ table.setRowSorter(sorter);
+
+ // Set selection and double-click listeners
+ table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
+ @Override
+ public void valueChanged(ListSelectionEvent e) {
+ int row = table.getSelectedRow();
+ if (row >= 0) {
+ row = table.convertRowIndexToModel(row);
+ Motor m = model.getMotor(row);
+ if (!m.equals(selectedMotor)) {
+ selectedMotor = model.getMotor(row);
+ setDelays(true); // Reset delay times
+ }
+ }
+ }
+ });
+ table.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
+ okClicked = true;
+ MotorChooserDialog.this.setVisible(false);
+ }
+ }
+ });
+ // (Current selection and scrolling performed later)
+
+ JScrollPane scrollpane = new JScrollPane();
+ scrollpane.setViewportView(table);
+ panel.add(scrollpane,"grow, width :700:, height :300:, wrap paragraph");
+
+
+ // Ejection delay
+ panel.add(new JLabel("Select ejection charge delay:"), "split 3, gap rel");
+
+ delayBox = new JComboBox();
+ delayBox.setEditable(true);
+ delayBox.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ JComboBox cb = (JComboBox) e.getSource();
+ String sel = (String)cb.getSelectedItem();
+ if (sel.equalsIgnoreCase("None")) {
+ selectedDelay = Motor.PLUGGED;
+ } else {
+ try {
+ selectedDelay = Double.parseDouble(sel);
+ } catch (NumberFormatException ignore) { }
+ }
+ setDelays(false);
+ }
+ });
+ panel.add(delayBox,"gapright unrel");
+ panel.add(new ResizeLabel("(Number of seconds or \"None\")", -1), "wrap para");
+ setDelays(false);
+
+
+ JButton okButton = new JButton("OK");
+ okButton.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ okClicked = true;
+ MotorChooserDialog.this.setVisible(false);
+ }
+ });
+ panel.add(okButton,"split, tag ok");
+
+ button = new JButton("Cancel");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ MotorChooserDialog.this.setVisible(false);
+ }
+ });
+ panel.add(button,"tag cancel");
+
+
+ // Sets the filter:
+ int showMode = Prefs.getChoise("MotorDiameterMatch", SHOW_MAX, SHOW_EXACT);
+ combo.setSelectedIndex(showMode);
+
+
+ this.add(panel);
+ this.pack();
+// this.setAlwaysOnTop(true);
+
+ GUIUtil.setDefaultButton(okButton);
+ GUIUtil.installEscapeCloseOperation(this);
+
+ // Table can be scrolled only after pack() has been called
+ setSelectionVisible();
+ }
+
+ private void setSelectionVisible() {
+ if (selectedMotor != null) {
+ int index = table.convertRowIndexToView(model.getIndex(selectedMotor));
+ table.getSelectionModel().setSelectionInterval(index, index);
+ Rectangle rect = table.getCellRect(index, 0, true);
+ rect = new Rectangle(rect.x,rect.y-100,rect.width,rect.height+200);
+ table.scrollRectToVisible(rect);
+ }
+ }
+
+
+ /**
+ * Set the values in the delay combo box. If <code>reset</code> is <code>true</code>
+ * then sets the selected value as the value closest to selectedDelay, otherwise
+ * leaves selection alone.
+ */
+ private void setDelays(boolean reset) {
+ if (selectedMotor == null) {
+
+ delayBox.setModel(new DefaultComboBoxModel(new String[] { "None" }));
+ delayBox.setSelectedIndex(0);
+
+ } else {
+
+ double[] delays = selectedMotor.getStandardDelays();
+ String[] delayStrings = new String[delays.length];
+ double currentDelay = selectedDelay; // Store current setting locally
+
+ for (int i=0; i < delays.length; i++) {
+ delayStrings[i] = Motor.getDelayString(delays[i], "None");
+ }
+ delayBox.setModel(new DefaultComboBoxModel(delayStrings));
+
+ if (reset) {
+
+ // Find and set the closest value
+ double closest = Double.NaN;
+ for (int i=0; i < delays.length; i++) {
+ // if-condition to always become true for NaN
+ if (!(Math.abs(delays[i] - currentDelay) >
+ Math.abs(closest - currentDelay))) {
+ closest = delays[i];
+ }
+ }
+ if (!Double.isNaN(closest)) {
+ selectedDelay = closest;
+ delayBox.setSelectedItem(Motor.getDelayString(closest, "None"));
+ } else {
+ delayBox.setSelectedItem("None");
+ }
+
+ } else {
+
+ selectedDelay = currentDelay;
+ delayBox.setSelectedItem(Motor.getDelayString(currentDelay, "None"));
+
+ }
+
+ }
+ }
+
+
+
+ public Motor getSelectedMotor() {
+ if (!okClicked)
+ return null;
+ return selectedMotor;
+ }
+
+
+ public double getSelectedDelay() {
+ return selectedDelay;
+ }
+
+
+
+
+ //////////////// JTable elements ////////////////
+
+
+ /**
+ * Enum defining the table columns.
+ */
+ private enum MotorColumns {
+ MANUFACTURER("Manufacturer",100) {
+ @Override
+ public String getValue(Motor m) {
+ return m.getManufacturer();
+ }
+// @Override
+// public String getToolTipText(Motor m) {
+// return "<html>" + m.getDescription().replace((CharSequence)"\n", "<br>");
+// }
+ @Override
+ public Comparator<?> getComparator() {
+ return Collator.getInstance();
+ }
+ },
+ DESIGNATION("Designation") {
+ @Override
+ public String getValue(Motor m) {
+ return m.getDesignation();
+ }
+// @Override
+// public String getToolTipText(Motor m) {
+// return "<html>" + m.getDescription().replace((CharSequence)"\n", "<br>");
+// }
+ @Override
+ public Comparator<?> getComparator() {
+ return Motor.getDesignationComparator();
+ }
+ },
+ TYPE("Type") {
+ @Override
+ public String getValue(Motor m) {
+ return m.getMotorType().getName();
+ }
+// @Override
+// public String getToolTipText(Motor m) {
+// return m.getMotorType().getDescription();
+// }
+ @Override
+ public Comparator<?> getComparator() {
+ return Collator.getInstance();
+ }
+ },
+ DIAMETER("Diameter") {
+ @Override
+ public String getValue(Motor m) {
+ return UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(
+ m.getDiameter());
+ }
+ @Override
+ public Comparator<?> getComparator() {
+ return getNumericalComparator();
+ }
+ },
+ LENGTH("Length") {
+ @Override
+ public String getValue(Motor m) {
+ return UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(
+ m.getLength());
+ }
+ @Override
+ public Comparator<?> getComparator() {
+ return getNumericalComparator();
+ }
+ },
+ IMPULSE("Impulse") {
+ @Override
+ public String getValue(Motor m) {
+ return UnitGroup.UNITS_IMPULSE.getDefaultUnit().toStringUnit(
+ m.getTotalImpulse());
+ }
+ @Override
+ public Comparator<?> getComparator() {
+ return getNumericalComparator();
+ }
+ },
+ TIME("Burn time") {
+ @Override
+ public String getValue(Motor m) {
+ return UnitGroup.UNITS_SHORT_TIME.getDefaultUnit().toStringUnit(
+ m.getAverageTime());
+ }
+ @Override
+ public Comparator<?> getComparator() {
+ return getNumericalComparator();
+ }
+ };
+
+
+ private final String title;
+ private final int width;
+
+ MotorColumns(String title) {
+ this(title, 50);
+ }
+
+ MotorColumns(String title, int width) {
+ this.title = title;
+ this.width = width;
+ }
+
+
+ public abstract String getValue(Motor m);
+ public abstract Comparator<?> getComparator();
+
+ public String getTitle() {
+ return title;
+ }
+
+ public int getWidth() {
+ return width;
+ }
+
+ public String getToolTipText(Motor m) {
+ String tip = "<html>";
+ tip += "<b>" + m.toString() + "</b>";
+ tip += " (" + m.getMotorType().getDescription() + ")<br><hr>";
+
+ String desc = m.getDescription().trim();
+ if (desc.length() > 0) {
+ tip += "<i>" + desc.replace("\n", "<br>") + "</i><br><hr>";
+ }
+
+ tip += ("Diameter: " +
+ UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(m.getDiameter()) +
+ "<br>");
+ tip += ("Length: " +
+ UnitGroup.UNITS_MOTOR_DIMENSIONS.getDefaultUnit().toStringUnit(m.getLength()) +
+ "<br>");
+ tip += ("Maximum thrust: " +
+ UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(m.getMaxThrust()) +
+ "<br>");
+ tip += ("Average thrust: " +
+ UnitGroup.UNITS_FORCE.getDefaultUnit().toStringUnit(m.getAverageThrust()) +
+ "<br>");
+ tip += ("Burn time: " +
+ UnitGroup.UNITS_SHORT_TIME.getDefaultUnit()
+ .toStringUnit(m.getAverageTime()) + "<br>");
+ tip += ("Total impulse: " +
+ UnitGroup.UNITS_IMPULSE.getDefaultUnit()
+ .toStringUnit(m.getTotalImpulse()) + "<br>");
+ tip += ("Launch mass: " +
+ UnitGroup.UNITS_MASS.getDefaultUnit().toStringUnit(m.getMass(0)) +
+ "<br>");
+ tip += ("Empty mass: " +
+ UnitGroup.UNITS_MASS.getDefaultUnit()
+ .toStringUnit(m.getMass(Double.MAX_VALUE)));
+ return tip;
+ }
+
+ }
+
+
+ /**
+ * The JTable model. Includes an extra motor, given in the constructor,
+ * if it is not already in the database.
+ */
+ private class MotorDatabaseModel extends AbstractTableModel {
+ private final Motor extra;
+
+ public MotorDatabaseModel(Motor current) {
+ if (Databases.MOTOR.contains(current))
+ extra = null;
+ else
+ extra = current;
+ }
+
+ @Override
+ public int getColumnCount() {
+ return MotorColumns.values().length;
+ }
+
+ @Override
+ public int getRowCount() {
+ if (extra == null)
+ return Databases.MOTOR.size();
+ else
+ return Databases.MOTOR.size()+1;
+ }
+
+ @Override
+ public Object getValueAt(int rowIndex, int columnIndex) {
+ MotorColumns column = getColumn(columnIndex);
+ if (extra == null) {
+ return column.getValue(Databases.MOTOR.get(rowIndex));
+ } else {
+ if (rowIndex == 0)
+ return column.getValue(extra);
+ else
+ return column.getValue(Databases.MOTOR.get(rowIndex - 1));
+ }
+ }
+
+ @Override
+ public String getColumnName(int columnIndex) {
+ return getColumn(columnIndex).getTitle();
+ }
+
+
+ public Motor getMotor(int rowIndex) {
+ if (extra == null) {
+ return Databases.MOTOR.get(rowIndex);
+ } else {
+ if (rowIndex == 0)
+ return extra;
+ else
+ return Databases.MOTOR.get(rowIndex-1);
+ }
+ }
+
+ public int getIndex(Motor m) {
+ if (extra == null) {
+ return Databases.MOTOR.indexOf(m);
+ } else {
+ if (extra.equals(m))
+ return 0;
+ else
+ return Databases.MOTOR.indexOf(m)+1;
+ }
+ }
+
+ private MotorColumns getColumn(int index) {
+ return MotorColumns.values()[index];
+ }
+ }
+
+
+ //////// Row filters
+
+ /**
+ * Abstract adapter class.
+ */
+ private abstract class MotorRowFilter extends RowFilter<TableModel,Integer> {
+ @Override
+ public boolean include(
+ RowFilter.Entry<? extends TableModel, ? extends Integer> entry) {
+ int index = entry.getIdentifier();
+ Motor m = model.getMotor(index);
+ return include(m);
+ }
+
+ public abstract boolean include(Motor m);
+ }
+
+ /**
+ * Show all motors.
+ */
+ private class MotorRowFilterAll extends MotorRowFilter {
+ @Override
+ public boolean include(Motor m) {
+ return true;
+ }
+ }
+
+ /**
+ * Show motors smaller than the mount.
+ */
+ private class MotorRowFilterSmaller extends MotorRowFilter {
+ @Override
+ public boolean include(Motor m) {
+ return (m.getDiameter() <= diameter + 0.0004);
+ }
+ }
+
+ /**
+ * Show motors that fit the mount.
+ */
+ private class MotorRowFilterExact extends MotorRowFilter {
+ @Override
+ public boolean include(Motor m) {
+ return ((m.getDiameter() <= diameter + 0.0004) &&
+ (m.getDiameter() >= diameter - 0.0015));
+ }
+ }
+
+
+ private static Comparator<String> numericalComparator = null;
+ private static Comparator<String> getNumericalComparator() {
+ if (numericalComparator == null)
+ numericalComparator = new NumericalComparator();
+ return numericalComparator;
+ }
+
+ private static class NumericalComparator implements Comparator<String> {
+ private Pattern pattern =
+ Pattern.compile("^\\s*([0-9]*[.,][0-9]+|[0-9]+[.,]?[0-9]*).*?$");
+ private Collator collator = null;
+ @Override
+ public int compare(String s1, String s2) {
+ Matcher m1, m2;
+
+ m1 = pattern.matcher(s1);
+ m2 = pattern.matcher(s2);
+ if (m1.find() && m2.find()) {
+ double d1 = Double.parseDouble(m1.group(1));
+ double d2 = Double.parseDouble(m2.group(1));
+
+ return (int)((d1-d2)*1000);
+ }
+
+ if (collator == null)
+ collator = Collator.getInstance(Locale.US);
+ return collator.compare(s1, s2);
+ }
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.main;
+
+
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.JFrame;
+import javax.swing.KeyStroke;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.tree.TreePath;
+import javax.swing.tree.TreeSelectionModel;
+
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
+import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
+import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.Stage;
+import net.sf.openrocket.util.Icons;
+import net.sf.openrocket.util.Pair;
+
+
+
+/**
+ * A class that holds Actions for common rocket operations such as
+ * cut/copy/paste/delete etc.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class RocketActions {
+
+ private static RocketComponent clipboard = null;
+ private static List<RocketAction> clipboardListeners = new ArrayList<RocketAction>();
+
+ private final OpenRocketDocument document;
+ private final Rocket rocket;
+ private final JFrame parentFrame;
+ private final TreeSelectionModel selectionModel;
+
+
+ private final RocketAction deleteAction;
+ private final RocketAction cutAction;
+ private final RocketAction copyAction;
+ private final RocketAction pasteAction;
+ private final RocketAction editAction;
+ private final RocketAction newStageAction;
+ private final RocketAction moveUpAction;
+ private final RocketAction moveDownAction;
+
+
+ public RocketActions(OpenRocketDocument document, TreeSelectionModel selectionModel,
+ JFrame parentFrame) {
+ this.document = document;
+ this.rocket = document.getRocket();
+ this.selectionModel = selectionModel;
+ this.parentFrame = parentFrame;
+
+ // Add action also to updateActions()
+ this.deleteAction = new DeleteAction();
+ this.cutAction = new CutAction();
+ this.copyAction = new CopyAction();
+ this.pasteAction = new PasteAction();
+ this.editAction = new EditAction();
+ this.newStageAction = new NewStageAction();
+ this.moveUpAction = new MoveUpAction();
+ this.moveDownAction = new MoveDownAction();
+
+ updateActions();
+
+ // Update all actions when tree selection or rocket changes
+
+ selectionModel.addTreeSelectionListener(new TreeSelectionListener() {
+ @Override
+ public void valueChanged(TreeSelectionEvent e) {
+ updateActions();
+ }
+ });
+ document.getRocket().addComponentChangeListener(new ComponentChangeListener() {
+ @Override
+ public void componentChanged(ComponentChangeEvent e) {
+ updateActions();
+ }
+ });
+ }
+
+ /**
+ * Update the state of all of the actions.
+ */
+ private void updateActions() {
+ deleteAction.update();
+ cutAction.update();
+ copyAction.update();
+ pasteAction.update();
+ editAction.update();
+ newStageAction.update();
+ moveUpAction.update();
+ moveDownAction.update();
+ }
+
+
+ /**
+ * Update the state of all actions that depend on the clipboard.
+ */
+ private void updateClipboardActions() {
+ RocketAction[] array = clipboardListeners.toArray(new RocketAction[0]);
+ for (RocketAction a: array) {
+ a.update();
+ }
+ }
+
+
+
+ public Action getDeleteAction() {
+ return deleteAction;
+ }
+
+ public Action getCutAction() {
+ return cutAction;
+ }
+
+ public Action getCopyAction() {
+ return copyAction;
+ }
+
+ public Action getPasteAction() {
+ return pasteAction;
+ }
+
+ public Action getEditAction() {
+ return editAction;
+ }
+
+ public Action getNewStageAction() {
+ return newStageAction;
+ }
+
+ public Action getMoveUpAction() {
+ return moveUpAction;
+ }
+
+ public Action getMoveDownAction() {
+ return moveDownAction;
+ }
+
+
+
+ //////// Helper methods for the actions
+
+ /**
+ * Return the currently selected rocket component, or null if none selected.
+ *
+ * @return the currently selected component.
+ */
+ private RocketComponent getSelectedComponent() {
+ RocketComponent c = null;
+ TreePath p = selectionModel.getSelectionPath();
+ if (p != null)
+ c = (RocketComponent) p.getLastPathComponent();
+
+ if (c != null && c.getRocket() != rocket) {
+ throw new IllegalStateException("Selection not same as document rocket, "
+ + "report bug!");
+ }
+ return c;
+ }
+
+ private void setSelectedComponent(RocketComponent component) {
+ TreePath path = ComponentTreeModel.makeTreePath(component);
+ selectionModel.setSelectionPath(path);
+ }
+
+
+ private boolean isDeletable(RocketComponent c) {
+ // Sanity check
+ if (c == null || c.getParent() == null)
+ return false;
+
+ // Cannot remove Rocket
+ if (c instanceof Rocket)
+ return false;
+
+ // Cannot remove last stage
+ if ((c instanceof Stage) && (c.getParent().getChildCount() == 1)) {
+ return false;
+ }
+
+ return true;
+ }
+
+ private void delete(RocketComponent c) {
+ if (!isDeletable(c)) {
+ throw new IllegalArgumentException("Report bug! Component " + c + " not deletable.");
+ }
+
+ RocketComponent parent = c.getParent();
+ parent.removeChild(c);
+ }
+
+ private boolean isCopyable(RocketComponent c) {
+ if (c==null)
+ return false;
+ if (c instanceof Rocket)
+ return false;
+ return true;
+ }
+
+
+
+
+ /**
+ * Return the component and position to which the current clipboard
+ * should be pasted. Returns null if the clipboard is empty or if the
+ * clipboard cannot be pasted to the current selection.
+ *
+ * @return a Pair with both components defined, or null.
+ */
+ private Pair<RocketComponent, Integer> getPastePosition() {
+ RocketComponent selected = getSelectedComponent();
+ if (selected == null)
+ return null;
+
+ if (clipboard == null)
+ return null;
+
+ if (selected.isCompatible(clipboard))
+ return new Pair<RocketComponent, Integer>(selected, selected.getChildCount());
+
+ RocketComponent parent = selected.getParent();
+ if (parent != null && parent.isCompatible(clipboard)) {
+ int index = parent.getChildPosition(selected) + 1;
+ return new Pair<RocketComponent, Integer>(parent, index);
+ }
+
+ return null;
+ }
+
+
+
+
+
+ /////// Action classes
+
+ private abstract class RocketAction extends AbstractAction {
+ public abstract void update();
+ }
+
+
+ /**
+ * Action that deletes the selected component.
+ */
+ private class DeleteAction extends RocketAction {
+ public DeleteAction() {
+ this.putValue(NAME, "Delete");
+ this.putValue(SHORT_DESCRIPTION, "Delete the selected component and subcomponents.");
+ this.putValue(MNEMONIC_KEY, KeyEvent.VK_D);
+ this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_DELETE, 0));
+ this.putValue(SMALL_ICON, Icons.EDIT_DELETE);
+ update();
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ RocketComponent c = getSelectedComponent();
+ if (!isDeletable(c))
+ return;
+
+ ComponentConfigDialog.hideDialog();
+
+ document.addUndoPosition("Delete " + c.getComponentName());
+ delete(c);
+ }
+
+ @Override
+ public void update() {
+ this.setEnabled(isDeletable(getSelectedComponent()));
+ }
+ }
+
+
+
+ /**
+ * Action the cuts the selected component (copies to clipboard and deletes).
+ */
+ private class CutAction extends RocketAction {
+ public CutAction() {
+ this.putValue(NAME, "Cut");
+ this.putValue(MNEMONIC_KEY, KeyEvent.VK_T);
+ this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_X,
+ ActionEvent.CTRL_MASK));
+ this.putValue(SHORT_DESCRIPTION, "Cut this component (and subcomponents) to "
+ + "the clipboard and remove from this design");
+ this.putValue(SMALL_ICON, Icons.EDIT_CUT);
+ update();
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ RocketComponent c = getSelectedComponent();
+ if (!isDeletable(c) || !isCopyable(c))
+ return;
+
+ ComponentConfigDialog.hideDialog();
+
+ document.addUndoPosition("Cut " + c.getComponentName());
+ clipboard = c.copy();
+ delete(c);
+ updateClipboardActions();
+ }
+
+ @Override
+ public void update() {
+ RocketComponent c = getSelectedComponent();
+ this.setEnabled(isDeletable(c) && isCopyable(c));
+ }
+ }
+
+
+
+ /**
+ * Action that copies the selected component to the clipboard.
+ */
+ private class CopyAction extends RocketAction {
+ public CopyAction() {
+ this.putValue(NAME, "Copy");
+ this.putValue(MNEMONIC_KEY, KeyEvent.VK_C);
+ this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_C,
+ ActionEvent.CTRL_MASK));
+ this.putValue(SHORT_DESCRIPTION, "Copy this component (and subcomponents) to "
+ + "the clipboard.");
+ this.putValue(SMALL_ICON, Icons.EDIT_COPY);
+ update();
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ RocketComponent c = getSelectedComponent();
+ if (!isCopyable(c))
+ return;
+
+ clipboard = c.copy();
+ updateClipboardActions();
+ }
+
+ @Override
+ public void update() {
+ this.setEnabled(isCopyable(getSelectedComponent()));
+ }
+
+ }
+
+
+
+ /**
+ * Action that pastes the current clipboard to the selected position.
+ * It first tries to paste the component to the end of the selected component
+ * as a child, and after that as a sibling after the selected component.
+ */
+ private class PasteAction extends RocketAction {
+ public PasteAction() {
+ this.putValue(NAME, "Paste");
+ this.putValue(MNEMONIC_KEY, KeyEvent.VK_P);
+ this.putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(KeyEvent.VK_V,
+ ActionEvent.CTRL_MASK));
+ this.putValue(SHORT_DESCRIPTION, "Paste the component (and subcomponents) on "
+ + "the clipboard to the design.");
+ this.putValue(SMALL_ICON, Icons.EDIT_PASTE);
+ update();
+
+ // Listen to when the clipboard changes
+ clipboardListeners.add(this);
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ Pair<RocketComponent, Integer> position = getPastePosition();
+ if (position == null)
+ return;
+
+ ComponentConfigDialog.hideDialog();
+
+ RocketComponent pasted = clipboard.copy();
+ document.addUndoPosition("Paste " + pasted.getComponentName());
+ position.getU().addChild(pasted, position.getV());
+ setSelectedComponent(pasted);
+ }
+
+ @Override
+ public void update() {
+ this.setEnabled(getPastePosition() != null);
+ }
+ }
+
+
+
+
+
+
+ /**
+ * Action to edit the currently selected component.
+ */
+ private class EditAction extends RocketAction {
+ public EditAction() {
+ this.putValue(NAME, "Edit");
+ this.putValue(SHORT_DESCRIPTION, "Edit the selected component.");
+ update();
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ RocketComponent c = getSelectedComponent();
+ if (c == null)
+ return;
+
+ ComponentConfigDialog.showDialog(parentFrame, document, c);
+ }
+
+ @Override
+ public void update() {
+ this.setEnabled(getSelectedComponent() != null);
+ }
+ }
+
+
+
+
+
+
+
+ /**
+ * Action to add a new stage to the rocket.
+ */
+ private class NewStageAction extends RocketAction {
+ public NewStageAction() {
+ this.putValue(NAME, "New stage");
+ this.putValue(SHORT_DESCRIPTION, "Add a new stage to the rocket design.");
+ update();
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+
+ ComponentConfigDialog.hideDialog();
+
+ RocketComponent stage = new Stage();
+ stage.setName("Booster stage");
+ document.addUndoPosition("Add stage");
+ rocket.addChild(stage);
+ rocket.getDefaultConfiguration().setAllStages();
+ setSelectedComponent(stage);
+ ComponentConfigDialog.showDialog(parentFrame, document, stage);
+
+ }
+
+ @Override
+ public void update() {
+ this.setEnabled(true);
+ }
+ }
+
+
+
+
+ /**
+ * Action to move the selected component upwards in the parent's child list.
+ */
+ private class MoveUpAction extends RocketAction {
+ public MoveUpAction() {
+ this.putValue(NAME, "Move up");
+ this.putValue(SHORT_DESCRIPTION, "Move this component upwards.");
+ update();
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ RocketComponent selected = getSelectedComponent();
+ if (!canMove(selected))
+ return;
+
+ ComponentConfigDialog.hideDialog();
+
+ RocketComponent parent = selected.getParent();
+ document.addUndoPosition("Move "+selected.getComponentName());
+ parent.moveChild(selected, parent.getChildPosition(selected)-1);
+ setSelectedComponent(selected);
+ }
+
+ @Override
+ public void update() {
+ this.setEnabled(canMove(getSelectedComponent()));
+ }
+
+ private boolean canMove(RocketComponent c) {
+ if (c == null || c.getParent() == null)
+ return false;
+ RocketComponent parent = c.getParent();
+ if (parent.getChildPosition(c) > 0)
+ return true;
+ return false;
+ }
+ }
+
+
+
+ /**
+ * Action to move the selected component down in the parent's child list.
+ */
+ private class MoveDownAction extends RocketAction {
+ public MoveDownAction() {
+ this.putValue(NAME, "Move down");
+ this.putValue(SHORT_DESCRIPTION, "Move this component downwards.");
+ update();
+ }
+
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ RocketComponent selected = getSelectedComponent();
+ if (!canMove(selected))
+ return;
+
+ ComponentConfigDialog.hideDialog();
+
+ RocketComponent parent = selected.getParent();
+ document.addUndoPosition("Move "+selected.getComponentName());
+ parent.moveChild(selected, parent.getChildPosition(selected)+1);
+ setSelectedComponent(selected);
+ }
+
+ @Override
+ public void update() {
+ this.setEnabled(canMove(getSelectedComponent()));
+ }
+
+ private boolean canMove(RocketComponent c) {
+ if (c == null || c.getParent() == null)
+ return false;
+ RocketComponent parent = c.getParent();
+ if (parent.getChildPosition(c) < parent.getChildCount()-1)
+ return true;
+ return false;
+ }
+ }
+
+
+
+}
--- /dev/null
+package net.sf.openrocket.gui.main;
+
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.Arrays;
+import java.util.List;
+
+import javax.swing.AbstractListModel;
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComboBox;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JSpinner;
+import javax.swing.JTabbedPane;
+import javax.swing.JTextField;
+import javax.swing.ListCellRenderer;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.DocumentEvent;
+import javax.swing.event.DocumentListener;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.aerodynamics.ExtendedISAModel;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.DescriptionArea;
+import net.sf.openrocket.gui.SpinnerEditor;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.BooleanModel;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.MotorConfigurationModel;
+import net.sf.openrocket.gui.plot.Axis;
+import net.sf.openrocket.gui.plot.PlotConfiguration;
+import net.sf.openrocket.gui.plot.PlotPanel;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.simulation.FlightDataBranch;
+import net.sf.openrocket.simulation.RK4Simulator;
+import net.sf.openrocket.simulation.SimulationConditions;
+import net.sf.openrocket.simulation.SimulationListener;
+import net.sf.openrocket.simulation.listeners.CSVSaveListener;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.GUIUtil;
+import net.sf.openrocket.util.Icons;
+import net.sf.openrocket.util.Prefs;
+
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.ChartPanel;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.plot.ValueMarker;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.StandardXYItemRenderer;
+import org.jfree.data.xy.XYSeries;
+import org.jfree.data.xy.XYSeriesCollection;
+
+
+public class SimulationEditDialog extends JDialog {
+
+ public static final int DEFAULT = -1;
+ public static final int EDIT = 1;
+ public static final int PLOT = 2;
+
+
+ private final Window parentWindow;
+ private final Simulation simulation;
+ private final SimulationConditions conditions;
+ private final Configuration configuration;
+
+
+ public SimulationEditDialog(Window parent, Simulation s) {
+ this(parent, s, 0);
+ }
+
+ public SimulationEditDialog(Window parent, Simulation s, int tab) {
+ super(parent, "Edit simulation", JDialog.ModalityType.DOCUMENT_MODAL);
+
+ this.parentWindow = parent;
+ this.simulation = s;
+ this.conditions = simulation.getConditions();
+ configuration = simulation.getConfiguration();
+
+ JPanel mainPanel = new JPanel(new MigLayout("fill","[grow, fill]"));
+
+ mainPanel.add(new JLabel("Simulation name: "), "span, split 2, shrink");
+ final JTextField field = new JTextField(simulation.getName());
+ field.getDocument().addDocumentListener(new DocumentListener() {
+ @Override
+ public void changedUpdate(DocumentEvent e) {
+ setText();
+ }
+ @Override
+ public void insertUpdate(DocumentEvent e) {
+ setText();
+ }
+ @Override
+ public void removeUpdate(DocumentEvent e) {
+ setText();
+ }
+ private void setText() {
+ String name = field.getText();
+ if (name == null || name.equals(""))
+ return;
+ System.out.println("Setting name:"+name);
+ simulation.setName(name);
+
+ }
+ });
+ mainPanel.add(field, "shrinky, growx, wrap");
+
+ JTabbedPane tabbedPane = new JTabbedPane();
+
+
+ tabbedPane.addTab("Launch conditions", flightConditionsTab());
+ tabbedPane.addTab("Simulation options", simulationOptionsTab());
+ tabbedPane.addTab("Plot data", plotTab());
+// tabbedPane.addTab("Export data", exportTab());
+
+ // Select the initial tab
+ if (tab == EDIT) {
+ tabbedPane.setSelectedIndex(0);
+ } else if (tab == PLOT) {
+ tabbedPane.setSelectedIndex(2);
+ } else {
+ FlightData data = s.getSimulatedData();
+ if (data == null || data.getBranchCount() == 0)
+ tabbedPane.setSelectedIndex(0);
+ else
+ tabbedPane.setSelectedIndex(2);
+ }
+
+ mainPanel.add(tabbedPane, "spanx, grow, wrap");
+
+
+ // Buttons
+ mainPanel.add(new JPanel(), "spanx, split, growx");
+
+ JButton button;
+ button = new JButton("Run simulation");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ SimulationEditDialog.this.dispose();
+ SimulationRunDialog.runSimulations(parentWindow, simulation);
+ }
+ });
+ mainPanel.add(button, "gapright para");
+
+
+ JButton close = new JButton("Close");
+ close.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ SimulationEditDialog.this.dispose();
+ }
+ });
+ mainPanel.add(close, "");
+
+
+ this.add(mainPanel);
+ this.validate();
+ this.pack();
+ this.setLocationByPlatform(true);
+ GUIUtil.setDefaultButton(button);
+ GUIUtil.installEscapeCloseOperation(this);
+ }
+
+
+
+
+
+ private JPanel flightConditionsTab() {
+ JPanel panel = new JPanel(new MigLayout("fill"));
+ JPanel sub;
+ String tip;
+ UnitSelector unit;
+ BasicSlider slider;
+ DoubleModel m;
+ JSpinner spin;
+
+ //// Motor selector
+ JLabel label = new JLabel("Motor configuration:");
+ label.setToolTipText("Select the motor configuration to use.");
+ panel.add(label, "shrinkx, spanx, split 2");
+
+ JComboBox combo = new JComboBox(new MotorConfigurationModel(configuration));
+ combo.setToolTipText("Select the motor configuration to use.");
+ combo.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ conditions.setMotorConfigurationID(configuration.getMotorConfigurationID());
+ }
+ });
+ panel.add(combo, "growx, wrap para");
+
+
+ //// Wind settings: Average wind speed, turbulence intensity, std. deviation
+ sub = new JPanel(new MigLayout("fill, gap rel unrel",
+ "[grow][65lp!][30lp!][75lp!]",""));
+ sub.setBorder(BorderFactory.createTitledBorder("Wind"));
+ panel.add(sub, "growx, split 2, aligny 0, flowy, gapright para");
+
+
+ // Wind average
+ label = new JLabel("Average windspeed:");
+ tip = "The average windspeed relative to the ground.";
+ label.setToolTipText(tip);
+ sub.add(label);
+
+ m = new DoubleModel(conditions,"WindSpeedAverage", UnitGroup.UNITS_VELOCITY,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ spin.setToolTipText(tip);
+ sub.add(spin,"w 65lp!");
+
+ unit = new UnitSelector(m);
+ unit.setToolTipText(tip);
+ sub.add(unit,"growx");
+ slider = new BasicSlider(m.getSliderModel(0, 10.0));
+ slider.setToolTipText(tip);
+ sub.add(slider,"w 75lp, wrap");
+
+
+
+ // Wind std. deviation
+ label = new JLabel("Standard deviation:");
+ tip = "<html>The standard deviation of the windspeed.<br>" +
+ "The windspeed is within twice the standard deviation from the average for " +
+ "95% of the time.";
+ label.setToolTipText(tip);
+ sub.add(label);
+
+ m = new DoubleModel(conditions,"WindSpeedDeviation", UnitGroup.UNITS_VELOCITY,0);
+ DoubleModel m2 = new DoubleModel(conditions,"WindSpeedAverage", 0.25,
+ UnitGroup.UNITS_COEFFICIENT,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ spin.setToolTipText(tip);
+ sub.add(spin,"w 65lp!");
+
+ unit = new UnitSelector(m);
+ unit.setToolTipText(tip);
+ sub.add(unit,"growx");
+ slider = new BasicSlider(m.getSliderModel(new DoubleModel(0), m2));
+ slider.setToolTipText(tip);
+ sub.add(slider,"w 75lp, wrap");
+
+
+ // Wind turbulence intensity
+ label = new JLabel("Turbulence intensity:");
+ tip = "<html>The turbulence intensity is the standard deviation " +
+ "divided by the average windspeed.<br>" +
+ "Typical values range from "+
+ UnitGroup.UNITS_RELATIVE.getDefaultUnit().toStringUnit(0.05) +
+ " to " +
+ UnitGroup.UNITS_RELATIVE.getDefaultUnit().toStringUnit(0.20) + ".";
+ label.setToolTipText(tip);
+ sub.add(label);
+
+ m = new DoubleModel(conditions,"WindTurbulenceIntensity", UnitGroup.UNITS_RELATIVE,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ spin.setToolTipText(tip);
+ sub.add(spin,"w 65lp!");
+
+ unit = new UnitSelector(m);
+ unit.setToolTipText(tip);
+ sub.add(unit,"growx");
+
+ final JLabel intensityLabel = new JLabel(
+ getIntensityDescription(conditions.getWindTurbulenceIntensity()));
+ intensityLabel.setToolTipText(tip);
+ sub.add(intensityLabel,"w 75lp, wrap");
+ m.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ intensityLabel.setText(
+ getIntensityDescription(conditions.getWindTurbulenceIntensity()));
+ }
+ });
+
+
+
+
+
+ //// Temperature and pressure
+ sub = new JPanel(new MigLayout("fill, gap rel unrel",
+ "[grow][65lp!][30lp!][75lp!]",""));
+ sub.setBorder(BorderFactory.createTitledBorder("Atmospheric conditions"));
+ panel.add(sub, "growx, aligny 0, gapright para");
+
+
+ BooleanModel isa = new BooleanModel(conditions, "ISAAtmosphere");
+ JCheckBox check = new JCheckBox(isa);
+ check.setText("Use International Standard Atmosphere");
+ check.setToolTipText("<html>Select to use the International Standard Atmosphere model."+
+ "<br>This model has a temperature of " +
+ UnitGroup.UNITS_TEMPERATURE.toStringUnit(ExtendedISAModel.STANDARD_TEMPERATURE)+
+ " and a pressure of " +
+ UnitGroup.UNITS_PRESSURE.toStringUnit(ExtendedISAModel.STANDARD_PRESSURE) +
+ " at sea level.");
+ sub.add(check, "spanx, wrap unrel");
+
+ // Temperature
+ label = new JLabel("Temperature:");
+ tip = "The temperature at the launch site.";
+ label.setToolTipText(tip);
+ isa.addEnableComponent(label, false);
+ sub.add(label);
+
+ m = new DoubleModel(conditions,"LaunchTemperature", UnitGroup.UNITS_TEMPERATURE,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ spin.setToolTipText(tip);
+ isa.addEnableComponent(spin, false);
+ sub.add(spin,"w 65lp!");
+
+ unit = new UnitSelector(m);
+ unit.setToolTipText(tip);
+ isa.addEnableComponent(unit, false);
+ sub.add(unit,"growx");
+ slider = new BasicSlider(m.getSliderModel(253.15, 308.15)); // -20 ... 35
+ slider.setToolTipText(tip);
+ isa.addEnableComponent(slider, false);
+ sub.add(slider,"w 75lp, wrap");
+
+
+
+ // Pressure
+ label = new JLabel("Pressure:");
+ tip = "The atmospheric pressure at the launch site.";
+ label.setToolTipText(tip);
+ isa.addEnableComponent(label, false);
+ sub.add(label);
+
+ m = new DoubleModel(conditions,"LaunchPressure", UnitGroup.UNITS_PRESSURE,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ spin.setToolTipText(tip);
+ isa.addEnableComponent(spin, false);
+ sub.add(spin,"w 65lp!");
+
+ unit = new UnitSelector(m);
+ unit.setToolTipText(tip);
+ isa.addEnableComponent(unit, false);
+ sub.add(unit,"growx");
+ slider = new BasicSlider(m.getSliderModel(0.950e5, 1.050e5));
+ slider.setToolTipText(tip);
+ isa.addEnableComponent(slider, false);
+ sub.add(slider,"w 75lp, wrap");
+
+
+
+
+
+ //// Launch site conditions
+ sub = new JPanel(new MigLayout("fill, gap rel unrel",
+ "[grow][65lp!][30lp!][75lp!]",""));
+ sub.setBorder(BorderFactory.createTitledBorder("Launch site"));
+ panel.add(sub, "growx, split 2, aligny 0, flowy");
+
+
+ // Latitude
+ label = new JLabel("Latitude:");
+ tip = "<html>The launch site latitude affects the gravitational pull of Earth.<br>" +
+ "Positive values are on the Northern hemisphere, negative values on the " +
+ "Southern hemisphere.";
+ label.setToolTipText(tip);
+ sub.add(label);
+
+ m = new DoubleModel(conditions,"LaunchLatitude", UnitGroup.UNITS_NONE, -90, 90);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ spin.setToolTipText(tip);
+ sub.add(spin,"w 65lp!");
+
+ label = new JLabel("\u00b0 N");
+ label.setToolTipText(tip);
+ sub.add(label,"growx");
+ slider = new BasicSlider(m.getSliderModel(-90, 90));
+ slider.setToolTipText(tip);
+ sub.add(slider,"w 75lp, wrap");
+
+
+
+ // Altitude
+ label = new JLabel("Altitude:");
+ tip = "<html>The launch altitude above mean sea level.<br>" +
+ "This affects the position of the rocket in the atmospheric model.";
+ label.setToolTipText(tip);
+ sub.add(label);
+
+ m = new DoubleModel(conditions,"LaunchAltitude", UnitGroup.UNITS_DISTANCE,0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ spin.setToolTipText(tip);
+ sub.add(spin,"w 65lp!");
+
+ unit = new UnitSelector(m);
+ unit.setToolTipText(tip);
+ sub.add(unit,"growx");
+ slider = new BasicSlider(m.getSliderModel(0, 250, 1000));
+ slider.setToolTipText(tip);
+ sub.add(slider,"w 75lp, wrap");
+
+
+
+
+
+ //// Launch rod
+ sub = new JPanel(new MigLayout("fill, gap rel unrel",
+ "[grow][65lp!][30lp!][75lp!]",""));
+ sub.setBorder(BorderFactory.createTitledBorder("Launch rod"));
+ panel.add(sub, "growx, aligny 0, wrap");
+
+
+ // Length
+ label = new JLabel("Length:");
+ tip = "The length of the launch rod.";
+ label.setToolTipText(tip);
+ sub.add(label);
+
+ m = new DoubleModel(conditions,"LaunchRodLength", UnitGroup.UNITS_LENGTH, 0);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ spin.setToolTipText(tip);
+ sub.add(spin,"w 65lp!");
+
+ unit = new UnitSelector(m);
+ unit.setToolTipText(tip);
+ sub.add(unit,"growx");
+ slider = new BasicSlider(m.getSliderModel(0, 1, 5));
+ slider.setToolTipText(tip);
+ sub.add(slider,"w 75lp, wrap");
+
+
+
+ // Angle
+ label = new JLabel("Angle:");
+ tip = "The angle of the launch rod from vertical.";
+ label.setToolTipText(tip);
+ sub.add(label);
+
+ m = new DoubleModel(conditions,"LaunchRodAngle", UnitGroup.UNITS_ANGLE,
+ 0, SimulationConditions.MAX_LAUNCH_ROD_ANGLE);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ spin.setToolTipText(tip);
+ sub.add(spin,"w 65lp!");
+
+ unit = new UnitSelector(m);
+ unit.setToolTipText(tip);
+ sub.add(unit,"growx");
+ slider = new BasicSlider(m.getSliderModel(0, Math.PI/9,
+ SimulationConditions.MAX_LAUNCH_ROD_ANGLE));
+ slider.setToolTipText(tip);
+ sub.add(slider,"w 75lp, wrap");
+
+
+
+ // Direction
+ label = new JLabel("Direction:");
+ tip = "<html>Direction of the launch rod relative to the wind.<br>" +
+ UnitGroup.UNITS_ANGLE.toStringUnit(0) +
+ " = towards the wind, "+
+ UnitGroup.UNITS_ANGLE.toStringUnit(Math.PI) +
+ " = downwind.";
+ label.setToolTipText(tip);
+ sub.add(label);
+
+ m = new DoubleModel(conditions,"LaunchRodDirection", UnitGroup.UNITS_ANGLE,
+ -Math.PI, Math.PI);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ spin.setToolTipText(tip);
+ sub.add(spin,"w 65lp!");
+
+ unit = new UnitSelector(m);
+ unit.setToolTipText(tip);
+ sub.add(unit,"growx");
+ slider = new BasicSlider(m.getSliderModel(-Math.PI, Math.PI));
+ slider.setToolTipText(tip);
+ sub.add(slider,"w 75lp, wrap");
+
+ return panel;
+ }
+
+
+ private String getIntensityDescription(double i) {
+ if (i < 0.001)
+ return "None";
+ if (i < 0.05)
+ return "Very low";
+ if (i < 0.10)
+ return "Low";
+ if (i < 0.15)
+ return "Medium";
+ if (i < 0.20)
+ return "High";
+ if (i < 0.25)
+ return "Very high";
+ return "Extreme";
+ }
+
+
+
+ private JPanel simulationOptionsTab() {
+ JPanel panel = new JPanel(new MigLayout("fill"));
+ JPanel sub;
+ String tip;
+ JLabel label;
+ DoubleModel m;
+ JSpinner spin;
+ UnitSelector unit;
+ BasicSlider slider;
+
+
+ //// Simulation options
+ sub = new JPanel(new MigLayout("fill, gap rel unrel",
+ "[grow][65lp!][30lp!][75lp!]",""));
+ sub.setBorder(BorderFactory.createTitledBorder("Simulator options"));
+ panel.add(sub, "w 330lp!, growy, aligny 0");
+
+
+ // Calculation method
+ tip = "<html>" +
+ "The Extended Barrowman method calculates aerodynamic forces according <br>" +
+ "to the Barrowman equations extended to accommodate more components.";
+
+ label = new JLabel("Calculation method:");
+ label.setToolTipText(tip);
+ sub.add(label, "gaptop unrel, gapright para, spanx, split 2, w 150lp!");
+
+ label = new JLabel("Extended Barrowman");
+ label.setToolTipText(tip);
+ sub.add(label, "growx, wrap para");
+
+
+ // Simulation method
+ tip = "<html>" +
+ "The six degree-of-freedom simulator allows the rocket total freedom during " +
+ "flight.<br>" +
+ "Integration is performed using a 4<sup>th</sup> order Runge-Kutta 4 " +
+ "numerical integration.";
+
+ label = new JLabel("Simulation method:");
+ label.setToolTipText(tip);
+ sub.add(label, "gaptop unrel, gapright para, spanx, split 2, w 150lp!");
+
+ label = new JLabel("6-DOF Runge-Kutta 4");
+ label.setToolTipText(tip);
+ sub.add(label, "growx, wrap 35lp");
+
+
+ // Wind average
+ label = new JLabel("Time step:");
+ tip = "<html>The time between simulation steps.<br>" +
+ "A smaller time step results in a more accurate but slower simulation.<br>" +
+ "The 4<sup>th</sup> order simulation method is quite accurate with a time " +
+ "step of " +
+ UnitGroup.UNITS_TIME_STEP.toStringUnit(RK4Simulator.RECOMMENDED_TIME_STEP) +
+ ".";
+ label.setToolTipText(tip);
+ sub.add(label);
+
+ m = new DoubleModel(conditions,"TimeStep", UnitGroup.UNITS_TIME_STEP, 0, 1);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ spin.setToolTipText(tip);
+ sub.add(spin,"w 65lp!");
+
+ unit = new UnitSelector(m);
+ unit.setToolTipText(tip);
+ sub.add(unit,"growx");
+ slider = new BasicSlider(m.getSliderModel(0, 0.2));
+ slider.setToolTipText(tip);
+ sub.add(slider,"w 75lp, wrap");
+
+
+
+ // Maximum angle step
+ /*
+ label = new JLabel("Max. angle step:");
+ tip = "<html>" +
+ "This defines the maximum angle the rocket will turn during one time step.<br>"+
+ "Smaller values result in a more accurate but possibly slower simulation.<br>"+
+ "A recommended value is " +
+ UnitGroup.UNITS_ANGLE.toStringUnit(RK4Simulator.RECOMMENDED_ANGLE_STEP) + ".";
+ label.setToolTipText(tip);
+ sub.add(label);
+
+ m = new DoubleModel(conditions,"MaximumStepAngle", UnitGroup.UNITS_ANGLE,
+ 1*Math.PI/180, Math.PI/9);
+
+ spin = new JSpinner(m.getSpinnerModel());
+ spin.setEditor(new SpinnerEditor(spin));
+ spin.setToolTipText(tip);
+ sub.add(spin,"w 65lp!");
+
+ unit = new UnitSelector(m);
+ unit.setToolTipText(tip);
+ sub.add(unit,"growx");
+ slider = new BasicSlider(m.getSliderModel(0, Math.toRadians(10)));
+ slider.setToolTipText(tip);
+ sub.add(slider,"w 75lp, wrap para");
+ */
+
+ JButton button = new JButton("Reset to default");
+ button.setToolTipText("Reset the time step to its default value (" +
+ UnitGroup.UNITS_SHORT_TIME.toStringUnit(RK4Simulator.RECOMMENDED_TIME_STEP) +
+ ").");
+
+// button.setToolTipText("<html>Reset the step value to its default:<br>" +
+// "Time step " +
+// UnitGroup.UNITS_SHORT_TIME.toStringUnit(RK4Simulator.RECOMMENDED_TIME_STEP) +
+// "; maximum angle step " +
+// UnitGroup.UNITS_ANGLE.toStringUnit(RK4Simulator.RECOMMENDED_ANGLE_STEP) + ".");
+ sub.add(button, "spanx, tag right, wrap para");
+
+
+
+
+ //// Simulation listeners
+ sub = new JPanel(new MigLayout("fill, gap 0 0"));
+ sub.setBorder(BorderFactory.createTitledBorder("Simulator listeners"));
+ panel.add(sub, "growx, growy");
+
+
+ DescriptionArea desc = new DescriptionArea(5, -1);
+ desc.setText("<html><p>" +
+ "<i>Simulation listeners</i> is an advanced feature that allows "+
+ "user-written code to listen to and interact with the simulation. " +
+ "For details on writing simulation listeners, see the OpenRocket " +
+ "technical documentation.</p>");
+ sub.add(desc, "aligny 0, growx, wrap para");
+
+
+ label = new JLabel("Current listeners:");
+ sub.add(label, "spanx, wrap rel");
+
+ final ListenerListModel listenerModel = new ListenerListModel();
+ final JList list = new JList(listenerModel);
+ list.setCellRenderer(new ListenerCellRenderer());
+ JScrollPane scroll = new JScrollPane(list);
+// scroll.setPreferredSize(new Dimension(1,1));
+ sub.add(scroll, "height 1px, grow, wrap rel");
+
+
+ button = new JButton("Add");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ String previous = Prefs.NODE.get("previousListenerName", "");
+ String input = (String)JOptionPane.showInputDialog(SimulationEditDialog.this,
+ new Object[] {
+ "Type the full Java class name of the simulation listener, for example:",
+ "<html><tt>" + CSVSaveListener.class.getName() + "</tt>" },
+ "Add simulation listener",
+ JOptionPane.QUESTION_MESSAGE,
+ null, null,
+ previous
+ );
+ if (input == null || input.equals(""))
+ return;
+
+ Prefs.NODE.put("previousListenerName", input);
+ simulation.getSimulationListeners().add(input);
+ listenerModel.fireContentsChanged();
+ }
+ });
+ sub.add(button, "split 2, sizegroup buttons, alignx 50%, gapright para");
+
+ button = new JButton("Remove");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ int[] selected = list.getSelectedIndices();
+ Arrays.sort(selected);
+ for (int i=selected.length-1; i>=0; i--) {
+ simulation.getSimulationListeners().remove(selected[i]);
+ }
+ listenerModel.fireContentsChanged();
+ }
+ });
+ sub.add(button, "sizegroup buttons, alignx 50%");
+
+
+ return panel;
+ }
+
+
+ private class ListenerListModel extends AbstractListModel {
+ @Override
+ public String getElementAt(int index) {
+ if (index < 0 || index >= getSize())
+ return null;
+ return simulation.getSimulationListeners().get(index);
+ }
+ @Override
+ public int getSize() {
+ return simulation.getSimulationListeners().size();
+ }
+ public void fireContentsChanged() {
+ super.fireContentsChanged(this, 0, getSize());
+ }
+ }
+
+
+
+
+ /**
+ * A panel for plotting the previously calculated data.
+ */
+ private JPanel plotTab() {
+
+ // Check that data exists
+ if (simulation.getSimulatedData() == null ||
+ simulation.getSimulatedData().getBranchCount() == 0) {
+ return noDataPanel();
+ }
+
+
+ if (true)
+ return new PlotPanel(simulation);
+
+ JPanel panel = new JPanel(new MigLayout("fill"));
+
+
+
+
+ JButton button = new JButton("test");
+
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ PlotConfiguration config = new PlotConfiguration();
+ config.addPlotDataType(FlightDataBranch.TYPE_ALTITUDE);
+ config.addPlotDataType(FlightDataBranch.TYPE_VELOCITY_Z);
+ config.addPlotDataType(FlightDataBranch.TYPE_ACCELERATION_Z);
+ config.addPlotDataType(FlightDataBranch.TYPE_ACCELERATION_TOTAL);
+
+ config.setDomainAxisType(FlightDataBranch.TYPE_TIME);
+
+ performPlot(config);
+ }
+ });
+ panel.add(button);
+
+ return panel;
+ }
+
+
+
+ /**
+ * A panel for exporting the data.
+ */
+ private JPanel exportTab() {
+
+ // Check that data exists
+ if (simulation.getSimulatedData() == null ||
+ simulation.getSimulatedData().getBranchCount() == 0) {
+ return noDataPanel();
+ }
+
+
+ JPanel panel = new JPanel(new MigLayout("fill"));
+
+ panel.add(new JLabel("Not implemented yet.")); // TODO: HIGH: Implement export
+
+ return panel;
+ }
+
+
+
+
+
+ /**
+ * Return a panel stating that there is no data available, and that the user
+ * should run the simulation first.
+ */
+ private JPanel noDataPanel() {
+ JPanel panel = new JPanel(new MigLayout("fill"));
+
+ // No data available
+ panel.add(new JLabel("No flight data available."),
+ "alignx 50%, aligny 100%, wrap para");
+ panel.add(new JLabel("Please run the simulation first."),
+ "alignx 50%, aligny 0%, wrap");
+ return panel;
+ }
+
+
+ private void performPlot(PlotConfiguration config) {
+
+ // Fill the auto-selections
+ FlightDataBranch branch = simulation.getSimulatedData().getBranch(0);
+ PlotConfiguration filled = config.fillAutoAxes(branch);
+ List<Axis> axes = filled.getAllAxes();
+
+
+ // Create the data series for both axes
+ XYSeriesCollection[] data = new XYSeriesCollection[2];
+ data[0] = new XYSeriesCollection();
+ data[1] = new XYSeriesCollection();
+
+
+ // Get the domain axis type
+ final FlightDataBranch.Type domainType = filled.getDomainAxisType();
+ final Unit domainUnit = filled.getDomainAxisUnit();
+ if (domainType == null) {
+ throw new IllegalArgumentException("Domain axis type not specified.");
+ }
+ List<Double> x = branch.get(domainType);
+
+
+ // Create the XYSeries objects from the flight data and store into the collections
+ int length = filled.getTypeCount();
+ String[] axisLabel = new String[2];
+ for (int i = 0; i < length; i++) {
+ // Get info
+ FlightDataBranch.Type type = filled.getType(i);
+ Unit unit = filled.getUnit(i);
+ int axis = filled.getAxis(i);
+ String name = getLabel(type, unit);
+
+ // Store data in provided units
+ List<Double> y = branch.get(type);
+ XYSeries series = new XYSeries(name, false, true);
+ for (int j=0; j<x.size(); j++) {
+ series.add(domainUnit.toUnit(x.get(j)), unit.toUnit(y.get(j)));
+ }
+ data[axis].addSeries(series);
+
+ // Update axis label
+ if (axisLabel[axis] == null)
+ axisLabel[axis] = type.getName();
+ else
+ axisLabel[axis] += "; " + type.getName();
+ }
+
+
+ // Create the chart using the factory to get all default settings
+ JFreeChart chart = ChartFactory.createXYLineChart(
+ "Simulated flight",
+ null,
+ null,
+ null,
+ PlotOrientation.VERTICAL,
+ true,
+ true,
+ false
+ );
+
+
+ // Add the data and formatting to the plot
+ XYPlot plot = chart.getXYPlot();
+ int axisno = 0;
+ for (int i=0; i<2; i++) {
+ // Check whether axis has any data
+ if (data[i].getSeriesCount() > 0) {
+ // Create and set axis
+ double min = axes.get(i).getMinValue();
+ double max = axes.get(i).getMaxValue();
+ NumberAxis axis = new PresetNumberAxis(min, max);
+ axis.setLabel(axisLabel[i]);
+// axis.setRange(axes.get(i).getMinValue(), axes.get(i).getMaxValue());
+ plot.setRangeAxis(axisno, axis);
+
+ // Add data and map to the axis
+ plot.setDataset(axisno, data[i]);
+ plot.setRenderer(axisno, new StandardXYItemRenderer());
+ plot.mapDatasetToRangeAxis(axisno, axisno);
+ axisno++;
+ }
+ }
+
+ plot.getDomainAxis().setLabel(getLabel(domainType,domainUnit));
+ plot.addDomainMarker(new ValueMarker(0));
+ plot.addRangeMarker(new ValueMarker(0));
+
+
+ // Create the dialog
+ final JDialog dialog = new JDialog(this, "Simulation results");
+ dialog.setModalityType(ModalityType.DOCUMENT_MODAL);
+
+ JPanel panel = new JPanel(new MigLayout("fill"));
+ dialog.add(panel);
+
+ ChartPanel chartPanel = new ChartPanel(chart,
+ false, // properties
+ true, // save
+ false, // print
+ true, // zoom
+ true); // tooltips
+ chartPanel.setMouseWheelEnabled(true);
+ chartPanel.setEnforceFileExtensions(true);
+ chartPanel.setInitialDelay(500);
+
+ chartPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
+
+ panel.add(chartPanel, "grow, wrap 20lp");
+
+ JButton button = new JButton("Close");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ dialog.setVisible(false);
+ }
+ });
+ panel.add(button, "right");
+
+ dialog.setLocationByPlatform(true);
+ dialog.pack();
+ GUIUtil.installEscapeCloseOperation(dialog);
+ GUIUtil.setDefaultButton(button);
+
+ dialog.setVisible(true);
+ }
+
+
+ private class PresetNumberAxis extends NumberAxis {
+ private final double min;
+ private final double max;
+
+ public PresetNumberAxis(double min, double max) {
+ this.min = min;
+ this.max = max;
+ autoAdjustRange();
+ }
+
+ @Override
+ protected void autoAdjustRange() {
+ this.setRange(min, max);
+ }
+ }
+
+
+ private String getLabel(FlightDataBranch.Type type, Unit unit) {
+ String name = type.getName();
+ if (unit != null && !UnitGroup.UNITS_NONE.contains(unit) &&
+ !UnitGroup.UNITS_COEFFICIENT.contains(unit) && unit.getUnit().length() > 0)
+ name += " ("+unit.getUnit() + ")";
+ return name;
+ }
+
+
+
+ private class ListenerCellRenderer extends JLabel implements ListCellRenderer {
+
+ public Component getListCellRendererComponent(JList list, Object value,
+ int index, boolean isSelected, boolean cellHasFocus) {
+ String s = value.toString();
+ setText(s);
+
+ // Attempt instantiating, catch any exceptions
+ Exception ex = null;
+ try {
+ Class<?> c = Class.forName(s);
+ @SuppressWarnings("unused")
+ SimulationListener l = (SimulationListener)c.newInstance();
+ } catch (Exception e) {
+ ex = e;
+ }
+
+ if (ex == null) {
+ setIcon(Icons.SIMULATION_LISTENER_OK);
+ setToolTipText("Listener instantiated successfully.");
+ } else {
+ setIcon(Icons.SIMULATION_LISTENER_ERROR);
+ setToolTipText("<html>Unable to instantiate listener due to exception:<br>" +
+ ex.toString());
+ }
+
+ if (isSelected) {
+ setBackground(list.getSelectionBackground());
+ setForeground(list.getSelectionForeground());
+ } else {
+ setBackground(list.getBackground());
+ setForeground(list.getForeground());
+ }
+ setOpaque(true);
+ return this;
+ }
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.main;
+
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.util.Arrays;
+
+import javax.swing.JButton;
+import javax.swing.JCheckBox;
+import javax.swing.JComponent;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JTable;
+import javax.swing.SwingUtilities;
+import javax.swing.table.DefaultTableCellRenderer;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.aerodynamics.Warning;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.gui.ResizeLabel;
+import net.sf.openrocket.gui.adaptors.Column;
+import net.sf.openrocket.gui.adaptors.ColumnTableModel;
+import net.sf.openrocket.rocketcomponent.ComponentChangeEvent;
+import net.sf.openrocket.rocketcomponent.ComponentChangeListener;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.Icons;
+import net.sf.openrocket.util.Prefs;
+
+public class SimulationPanel extends JPanel {
+
+ private static final Color WARNING_COLOR = Color.RED;
+ private static final String WARNING_TEXT = "\uFF01"; // Fullwidth exclamation mark
+
+ private static final Color OK_COLOR = new Color(60,150,0);
+ private static final String OK_TEXT = "\u2714"; // Heavy check mark
+
+ private static final String NAME_PREFIX = "Simulation ";
+
+
+
+ private final OpenRocketDocument document;
+
+ private final ColumnTableModel simulationTableModel;
+ private final JTable simulationTable;
+
+
+ public SimulationPanel(OpenRocketDocument doc) {
+ super(new MigLayout("fill","[grow][][][][][][grow]"));
+
+ JButton button;
+
+
+ this.document = doc;
+
+
+
+ //////// The simulation action buttons
+
+ button = new JButton("New simulation");
+ button.setToolTipText("Add a new simulation");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+
+ // Generate unique name for the simulation
+ int maxValue = 0;
+ for (Simulation s: document.getSimulations()) {
+ String name = s.getName();
+ if (name.startsWith(NAME_PREFIX)) {
+ try {
+ maxValue = Math.max(maxValue,
+ Integer.parseInt(name.substring(NAME_PREFIX.length())));
+ } catch (NumberFormatException ignore) { }
+ }
+ }
+
+ Simulation sim = new Simulation(document.getRocket());
+ sim.setName(NAME_PREFIX + (maxValue+1));
+
+ int n = document.getSimulationCount();
+ document.addSimulation(sim);
+ simulationTableModel.fireTableDataChanged();
+ simulationTable.clearSelection();
+ simulationTable.addRowSelectionInterval(n, n);
+
+ openDialog(sim, SimulationEditDialog.EDIT);
+ }
+ });
+ this.add(button,"skip 1, gapright para");
+
+ button = new JButton("Edit simulation");
+ button.setToolTipText("Edit the selected simulation");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ int selected = simulationTable.getSelectedRow();
+ if (selected < 0)
+ return; // TODO: MEDIUM: "None selected" dialog
+
+ selected = simulationTable.convertRowIndexToModel(selected);
+ simulationTable.clearSelection();
+ simulationTable.addRowSelectionInterval(selected, selected);
+
+ openDialog(document.getSimulations().get(selected), SimulationEditDialog.EDIT);
+ }
+ });
+ this.add(button,"gapright para");
+
+ button = new JButton("Run simulations");
+ button.setToolTipText("Re-run the selected simulations");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ int[] selection = simulationTable.getSelectedRows();
+ if (selection.length == 0)
+ return; // TODO: LOW: "None selected" dialog
+
+ Simulation[] sims = new Simulation[selection.length];
+ for (int i=0; i < selection.length; i++) {
+ selection[i] = simulationTable.convertRowIndexToModel(selection[i]);
+ sims[i] = document.getSimulation(selection[i]);
+ }
+
+ long t = System.currentTimeMillis();
+ new SimulationRunDialog(SwingUtilities.getWindowAncestor(
+ SimulationPanel.this), sims).setVisible(true);
+ System.err.println("Running took "+(System.currentTimeMillis()-t) + " ms");
+ fireMaintainSelection();
+ }
+ });
+ this.add(button,"gapright para");
+
+ button = new JButton("Delete simulations");
+ button.setToolTipText("Delete the selected simulations");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ int[] selection = simulationTable.getSelectedRows();
+ if (selection.length == 0)
+ return; // TODO: LOW: "None selected" dialog
+
+ // Verify deletion
+ boolean verify = Prefs.NODE.getBoolean(Prefs.CONFIRM_DELETE_SIMULATION, true);
+ if (verify) {
+
+ JPanel panel = new JPanel(new MigLayout());
+ JCheckBox dontAsk = new JCheckBox("Do not ask me again");
+ panel.add(dontAsk,"wrap");
+ panel.add(new ResizeLabel("You can change the default operation in the " +
+ "preferences.",-2));
+
+ int ret = JOptionPane.showConfirmDialog(SimulationPanel.this,
+ new Object[] {
+ "Delete the selected simulations?",
+ "<html><i>This operation cannot be undone.</i>",
+ "",
+ panel },
+ "Delete simulations",
+ JOptionPane.OK_CANCEL_OPTION,
+ JOptionPane.WARNING_MESSAGE);
+ if (ret != JOptionPane.OK_OPTION)
+ return;
+
+ if (dontAsk.isSelected()) {
+ Prefs.NODE.putBoolean(Prefs.CONFIRM_DELETE_SIMULATION, false);
+ }
+ }
+
+ // Delete simulations
+ for (int i=0; i < selection.length; i++) {
+ selection[i] = simulationTable.convertRowIndexToModel(selection[i]);
+ }
+ Arrays.sort(selection);
+ for (int i=selection.length-1; i>=0; i--) {
+ document.removeSimulation(selection[i]);
+ }
+ simulationTableModel.fireTableDataChanged();
+ }
+ });
+ this.add(button,"gapright para");
+
+
+// button = new JButton("Plot / export");
+ button = new JButton("Plot flight");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ int selected = simulationTable.getSelectedRow();
+ if (selected < 0)
+ return; // TODO: MEDIUM: "None selected" dialog
+
+ selected = simulationTable.convertRowIndexToModel(selected);
+ simulationTable.clearSelection();
+ simulationTable.addRowSelectionInterval(selected, selected);
+
+ openDialog(document.getSimulations().get(selected), SimulationEditDialog.PLOT);
+ }
+ });
+ this.add(button, "wrap para");
+
+
+
+
+ //////// The simulation table
+
+ simulationTableModel = new ColumnTableModel(
+
+ //// Status and warning column
+ new Column("") {
+ private JLabel label = null;
+ @Override
+ public Object getValueAt(int row) {
+ if (row < 0 || row >= document.getSimulationCount())
+ return null;
+
+ // Initialize the label
+ if (label == null) {
+ label = new ResizeLabel(2f);
+ label.setIconTextGap(1);
+// label.setFont(label.getFont().deriveFont(Font.BOLD));
+ }
+
+ // Set simulation status icon
+ Simulation.Status status = document.getSimulation(row).getStatus();
+ label.setIcon(Icons.SIMULATION_STATUS_ICON_MAP.get(status));
+
+
+ // Set warning marker
+ if (status == Simulation.Status.NOT_SIMULATED ||
+ status == Simulation.Status.EXTERNAL) {
+
+ label.setText("");
+
+ } else {
+
+ WarningSet w = document.getSimulation(row).getSimulatedWarnings();
+ if (w == null) {
+ label.setText("");
+ } else if (w.isEmpty()) {
+ label.setForeground(OK_COLOR);
+ label.setText(OK_TEXT);
+ } else {
+ label.setForeground(WARNING_COLOR);
+ label.setText(WARNING_TEXT);
+ }
+ }
+
+ return label;
+ }
+ @Override public int getExactWidth() {
+ return 32;
+ }
+ @Override public Class<?> getColumnClass() {
+ return JLabel.class;
+ }
+ },
+
+ //// Simulation name
+ new Column("Name") {
+ @Override public Object getValueAt(int row) {
+ if (row < 0 || row >= document.getSimulationCount())
+ return null;
+ return document.getSimulation(row).getName();
+ }
+ @Override
+ public int getDefaultWidth() {
+ return 125;
+ }
+ },
+
+ //// Simulation motors
+ new Column("Motors") {
+ @Override public Object getValueAt(int row) {
+ if (row < 0 || row >= document.getSimulationCount())
+ return null;
+ return document.getSimulation(row).getConfiguration()
+ .getMotorConfigurationDescription();
+ }
+ @Override
+ public int getDefaultWidth() {
+ return 125;
+ }
+ },
+
+ //// Apogee
+ new Column("Apogee") {
+ @Override public Object getValueAt(int row) {
+ if (row < 0 || row >= document.getSimulationCount())
+ return null;
+
+ FlightData data = document.getSimulation(row).getSimulatedData();
+ if (data==null)
+ return null;
+
+ return UnitGroup.UNITS_DISTANCE.getDefaultUnit().toStringUnit(
+ data.getMaxAltitude());
+ }
+ },
+
+ //// Maximum velocity
+ new Column("Max. velocity") {
+ @Override public Object getValueAt(int row) {
+ if (row < 0 || row >= document.getSimulationCount())
+ return null;
+
+ FlightData data = document.getSimulation(row).getSimulatedData();
+ if (data==null)
+ return null;
+
+ return UnitGroup.UNITS_VELOCITY.getDefaultUnit().toStringUnit(
+ data.getMaxVelocity());
+ }
+ },
+
+ //// Maximum acceleration
+ new Column("Max. acceleration") {
+ @Override public Object getValueAt(int row) {
+ if (row < 0 || row >= document.getSimulationCount())
+ return null;
+
+ FlightData data = document.getSimulation(row).getSimulatedData();
+ if (data==null)
+ return null;
+
+ return UnitGroup.UNITS_ACCELERATION.getDefaultUnit().toStringUnit(
+ data.getMaxAcceleration());
+ }
+ },
+
+ //// Time to apogee
+ new Column("Time to apogee") {
+ @Override public Object getValueAt(int row) {
+ if (row < 0 || row >= document.getSimulationCount())
+ return null;
+
+ FlightData data = document.getSimulation(row).getSimulatedData();
+ if (data==null)
+ return null;
+
+ return UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit().toStringUnit(
+ data.getTimeToApogee());
+ }
+ },
+
+ //// Flight time
+ new Column("Flight time") {
+ @Override public Object getValueAt(int row) {
+ if (row < 0 || row >= document.getSimulationCount())
+ return null;
+
+ FlightData data = document.getSimulation(row).getSimulatedData();
+ if (data==null)
+ return null;
+
+ return UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit().toStringUnit(
+ data.getFlightTime());
+ }
+ },
+
+ //// Ground hit velocity
+ new Column("Ground hit velocity") {
+ @Override public Object getValueAt(int row) {
+ if (row < 0 || row >= document.getSimulationCount())
+ return null;
+
+ FlightData data = document.getSimulation(row).getSimulatedData();
+ if (data==null)
+ return null;
+
+ return UnitGroup.UNITS_VELOCITY.getDefaultUnit().toStringUnit(
+ data.getGroundHitVelocity());
+ }
+ }
+
+ ) {
+ @Override
+ public int getRowCount() {
+ return document.getSimulationCount();
+ }
+ };
+
+ simulationTable = new JTable(simulationTableModel);
+ simulationTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
+ simulationTable.setDefaultRenderer(Object.class, new JLabelRenderer());
+ simulationTableModel.setColumnWidths(simulationTable.getColumnModel());
+
+ // Mouse listener to act on double-clicks
+ simulationTable.addMouseListener(new MouseAdapter() {
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ if (e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2) {
+ int selected = simulationTable.getSelectedRow();
+ if (selected < 0)
+ return;
+
+ selected = simulationTable.convertRowIndexToModel(selected);
+ simulationTable.clearSelection();
+ simulationTable.addRowSelectionInterval(selected, selected);
+
+ openDialog(document.getSimulations().get(selected),
+ SimulationEditDialog.DEFAULT);
+ }
+ }
+ });
+
+
+
+
+ // Fire table change event when the rocket changes
+ document.getRocket().addComponentChangeListener(new ComponentChangeListener() {
+ @Override
+ public void componentChanged(ComponentChangeEvent e) {
+ fireMaintainSelection();
+ }
+ });
+
+
+ JScrollPane scrollpane = new JScrollPane(simulationTable);
+ this.add(scrollpane,"spanx, grow, wrap rel");
+
+
+ }
+
+
+ private void openDialog(final Simulation sim, int position) {
+ new SimulationEditDialog(SwingUtilities.getWindowAncestor(this), sim, position)
+ .setVisible(true);
+ fireMaintainSelection();
+ }
+
+ private void fireMaintainSelection() {
+ int[] selection = simulationTable.getSelectedRows();
+ simulationTableModel.fireTableDataChanged();
+ for (int row: selection) {
+ simulationTable.addRowSelectionInterval(row, row);
+ }
+ }
+
+
+ private class JLabelRenderer extends DefaultTableCellRenderer {
+
+ @Override
+ public Component getTableCellRendererComponent(JTable table,
+ Object value, boolean isSelected, boolean hasFocus, int row,
+ int column) {
+
+ if (row < 0 || row >= document.getSimulationCount())
+ return super.getTableCellRendererComponent(table, value,
+ isSelected, hasFocus, row, column);
+
+ // A JLabel is self-contained and has set its own tool tip
+ if (value instanceof JLabel) {
+ JLabel label = (JLabel)value;
+ if (isSelected)
+ label.setBackground(table.getSelectionBackground());
+ else
+ label.setBackground(table.getBackground());
+ label.setOpaque(true);
+
+ label.setToolTipText(getSimulationToolTip(document.getSimulation(row)));
+ return label;
+ }
+
+ Component component = super.getTableCellRendererComponent(table, value,
+ isSelected, hasFocus, row, column);
+
+ if (component instanceof JComponent) {
+ ((JComponent)component).setToolTipText(getSimulationToolTip(
+ document.getSimulation(row)));
+ }
+ return component;
+ }
+
+ private String getSimulationToolTip(Simulation sim) {
+ String tip;
+ FlightData data = sim.getSimulatedData();
+
+ tip = "<html><b>" + sim.getName() + "</b><br>";
+ switch (sim.getStatus()) {
+ case UPTODATE:
+ tip += "<i>Up to date</i><br>";
+ break;
+
+ case LOADED:
+ tip += "<i>Data loaded from a file</i><br>";
+ break;
+
+ case OUTDATED:
+ tip += "<i><font color=\"red\">Data is out of date</font></i><br>";
+ tip += "Click <i><b>Run simulations</b></i> to simulate.<br>";
+ break;
+
+ case EXTERNAL:
+ tip += "<i>Imported data</i><br>";
+ return tip;
+
+ case NOT_SIMULATED:
+ tip += "<i>Not simulated yet</i><br>";
+ tip += "Click <i><b>Run simulations</b></i> to simulate.";
+ return tip;
+ }
+
+ if (data == null) {
+ tip += "No simulation data available.";
+ return tip;
+ }
+ WarningSet warnings = data.getWarningSet();
+
+ if (warnings.isEmpty()) {
+ tip += "<font color=\"gray\">No warnings.</font>";
+ return tip;
+ }
+
+ tip += "<font color=\"red\">Warnings:</font>";
+ for (Warning w: warnings) {
+ tip += "<br>" + w.toString();
+ }
+
+ return tip;
+ }
+
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.main;
+
+
+import java.awt.Dialog;
+import java.awt.Dimension;
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.WindowAdapter;
+import java.awt.event.WindowEvent;
+import java.io.CharArrayWriter;
+import java.io.PrintWriter;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JProgressBar;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.gui.DetailDialog;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.MotorMount.IgnitionEvent;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationListener;
+import net.sf.openrocket.simulation.SimulationStatus;
+import net.sf.openrocket.simulation.exception.SimulationCancelledException;
+import net.sf.openrocket.simulation.exception.SimulationException;
+import net.sf.openrocket.simulation.exception.SimulationLaunchException;
+import net.sf.openrocket.simulation.listeners.AbstractSimulationListener;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.GUIUtil;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Prefs;
+
+
+public class SimulationRunDialog extends JDialog {
+ /** Update the dialog status every this many ms */
+ private static final long UPDATE_MS = 200;
+
+ /** Flight progress at motor burnout */
+ private static final double BURNOUT_PROGRESS = 0.4;
+
+ /** Flight progress at apogee */
+ private static final double APOGEE_PROGRESS = 0.7;
+
+
+ /*
+ * The executor service is not static since we want concurrent simulation
+ * dialogs to run in parallel, ie. they both have their own executor service.
+ */
+ private final ExecutorService executor = Executors.newFixedThreadPool(
+ Prefs.getMaxThreadCount());
+
+
+ private final JLabel simLabel, timeLabel, altLabel, velLabel;
+ private final JProgressBar progressBar;
+
+
+ private final Simulation[] simulations;
+ private final SimulationWorker[] simulationWorkers;
+ private final SimulationStatus[] simulationStatuses;
+ private final double[] simulationMaxAltitude;
+ private final double[] simulationMaxVelocity;
+ private final boolean[] simulationDone;
+
+ public SimulationRunDialog(Window window, Simulation ... simulations) {
+ super(window, "Running simulations...", Dialog.ModalityType.DOCUMENT_MODAL);
+
+ if (simulations.length == 0) {
+ throw new IllegalArgumentException("Called with no simulations to run");
+ }
+
+ this.simulations = simulations;
+
+ // Initialize the simulations
+ int n = simulations.length;
+ simulationWorkers = new SimulationWorker[n];
+ simulationStatuses = new SimulationStatus[n];
+ simulationMaxAltitude = new double[n];
+ simulationMaxVelocity = new double[n];
+ simulationDone = new boolean[n];
+
+ for (int i=0; i<n; i++) {
+ simulationWorkers[i] = new InteractiveSimulationWorker(simulations[i], i);
+ executor.execute(simulationWorkers[i]);
+ }
+
+ // Build the dialog
+ JPanel panel = new JPanel(new MigLayout("fill", "[][grow]"));
+
+ simLabel = new JLabel("Running ...");
+ panel.add(simLabel, "spanx, wrap para");
+
+ panel.add(new JLabel("Simulation time: "), "gapright para");
+ timeLabel = new JLabel("");
+ panel.add(timeLabel, "growx, wrap rel");
+
+ panel.add(new JLabel("Altitude: "));
+ altLabel = new JLabel("");
+ panel.add(altLabel, "growx, wrap rel");
+
+ panel.add(new JLabel("Velocity: "));
+ velLabel = new JLabel("");
+ panel.add(velLabel, "growx, wrap para");
+
+ progressBar = new JProgressBar();
+ panel.add(progressBar, "spanx, growx, wrap para");
+
+
+ // Add cancel button
+ JButton cancel = new JButton("Cancel");
+ cancel.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ cancelSimulations();
+ }
+ });
+ panel.add(cancel, "spanx, tag cancel");
+
+
+ // Cancel simulations when user closes the window
+ this.addWindowListener(new WindowAdapter() {
+ @Override
+ public void windowClosing(WindowEvent e) {
+ cancelSimulations();
+ }
+ });
+
+
+ this.add(panel);
+ this.setMinimumSize(new Dimension(300, 0));
+ this.setLocationByPlatform(true);
+ this.validate();
+ this.pack();
+ GUIUtil.installEscapeCloseOperation(this);
+
+ updateProgress();
+ }
+
+
+ /**
+ * Cancel the currently running simulations. This is equivalent to clicking
+ * the Cancel button on the dialog.
+ */
+ public void cancelSimulations() {
+ executor.shutdownNow();
+ for (SimulationWorker w: simulationWorkers) {
+ w.cancel(true);
+ }
+ }
+
+
+ /**
+ * Static helper method to run simulations.
+ *
+ * @param parent the parent Window of the dialog to use.
+ * @param simulations the simulations to run.
+ */
+ public static void runSimulations(Window parent, Simulation ... simulations) {
+ new SimulationRunDialog(parent, simulations).setVisible(true);
+ }
+
+
+
+
+ private void updateProgress() {
+ System.out.println("updateProgress() called");
+ int index;
+ for (index=0; index < simulations.length; index++) {
+ if (!simulationDone[index])
+ break;
+ }
+
+ if (index >= simulations.length) {
+ // Everything is done, close the dialog
+ System.out.println("Everything done.");
+ this.dispose();
+ return;
+ }
+
+ // Update the progress bar status
+ int progress = 0;
+ for (SimulationWorker s: simulationWorkers) {
+ progress += s.getProgress();
+ }
+ progress /= simulationWorkers.length;
+ progressBar.setValue(progress);
+ System.out.println("Progressbar value "+progress);
+
+ // Update the simulation fields
+ simLabel.setText("Running " + simulations[index].getName());
+ if (simulationStatuses[index] == null) {
+ timeLabel.setText("");
+ altLabel.setText("");
+ velLabel.setText("");
+ System.out.println("Empty labels, how sad.");
+ return;
+ }
+
+ Unit u = UnitGroup.UNITS_FLIGHT_TIME.getDefaultUnit();
+ timeLabel.setText(u.toStringUnit(simulationStatuses[index].time));
+
+ u = UnitGroup.UNITS_DISTANCE.getDefaultUnit();
+ altLabel.setText(u.toStringUnit(simulationStatuses[index].position.z) + " (max. " +
+ u.toStringUnit(simulationMaxAltitude[index]) + ")");
+
+ u = UnitGroup.UNITS_VELOCITY.getDefaultUnit();
+ velLabel.setText(u.toStringUnit(simulationStatuses[index].velocity.z) + " (max. " +
+ u.toStringUnit(simulationMaxVelocity[index]) + ")");
+ System.out.println("Set interesting labels.");
+ }
+
+
+
+ /**
+ * A SwingWorker that performs a flight simulation. It periodically updates the
+ * simulation statuses of the parent class and calls updateProgress().
+ * The progress of the simulation is stored in the progress property of the
+ * SwingWorker.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+ private class InteractiveSimulationWorker extends SimulationWorker {
+
+ private final int index;
+ private final double burnoutTimeEstimate;
+ private volatile double burnoutVelocity;
+ private volatile double apogeeAltitude;
+
+ /*
+ * -2 = time from 0 ... burnoutTimeEstimate
+ * -1 = velocity from v(burnoutTimeEstimate) ... 0
+ * 0 ... n = stages from alt(max) ... 0
+ */
+ private volatile int simulationStage = -2;
+
+ private int progress = 0;
+
+
+ public InteractiveSimulationWorker(Simulation sim, int index) {
+ super(sim);
+ this.index = index;
+
+ // Calculate estimate of motor burn time
+ double launchBurn = 0;
+ double otherBurn = 0;
+ Configuration config = simulation.getConfiguration();
+ String id = simulation.getConditions().getMotorConfigurationID();
+ Iterator<MotorMount> iterator = config.motorIterator();
+ while (iterator.hasNext()) {
+ MotorMount m = iterator.next();
+ if (m.getIgnitionEvent() == IgnitionEvent.LAUNCH)
+ launchBurn = MathUtil.max(launchBurn, m.getMotor(id).getTotalTime());
+ else
+ otherBurn = otherBurn + m.getMotor(id).getTotalTime();
+ }
+ burnoutTimeEstimate = Math.max(launchBurn + otherBurn, 0.1);
+
+ }
+
+
+ /**
+ * Return the extra listeners to use, a progress listener and cancel listener.
+ */
+ @Override
+ protected SimulationListener[] getExtraListeners() {
+ return new SimulationListener[] {
+ new SimulationProgressListener()
+ };
+ }
+
+
+ /**
+ * Processes simulation statuses published by the simulation listener.
+ * The statuses of the parent class and the progress property are updated.
+ */
+ @Override
+ protected void process(List<SimulationStatus> chunks) {
+
+ // Update max. altitude and velocity
+ for (SimulationStatus s: chunks) {
+ simulationMaxAltitude[index] = Math.max(simulationMaxAltitude[index],
+ s.position.z);
+ simulationMaxVelocity[index] = Math.max(simulationMaxVelocity[index],
+ s.velocity.length());
+ }
+
+ // Calculate the progress
+ SimulationStatus status = chunks.get(chunks.size()-1);
+ simulationStatuses[index] = status;
+
+ // 1. time = 0 ... burnoutTimeEstimate
+ if (simulationStage == -2 && status.time < burnoutTimeEstimate) {
+ System.out.println("Method 1: t="+status.time + " est="+burnoutTimeEstimate);
+ setSimulationProgress(MathUtil.map(status.time, 0, burnoutTimeEstimate,
+ 0.0, BURNOUT_PROGRESS));
+ updateProgress();
+ return;
+ }
+
+ if (simulationStage == -2) {
+ simulationStage++;
+ burnoutVelocity = MathUtil.max(status.velocity.z, 0.1);
+ System.out.println("CHANGING to Method 2, vel="+burnoutVelocity);
+ }
+
+ // 2. z-velocity from burnout velocity to zero
+ if (simulationStage == -1 && status.velocity.z >= 0) {
+ System.out.println("Method 2: vel="+status.velocity.z + " burnout=" +
+ burnoutVelocity);
+ setSimulationProgress(MathUtil.map(status.velocity.z, burnoutVelocity, 0,
+ BURNOUT_PROGRESS, APOGEE_PROGRESS));
+ updateProgress();
+ return;
+ }
+
+ if (simulationStage == -1 && status.velocity.z < 0) {
+ simulationStage++;
+ apogeeAltitude = status.position.z;
+ }
+
+ // 3. z-position from apogee to zero
+ // TODO: MEDIUM: several stages
+ System.out.println("Method 3: alt="+status.position.z +" apogee="+apogeeAltitude);
+ setSimulationProgress(MathUtil.map(status.position.z,
+ apogeeAltitude, 0, APOGEE_PROGRESS, 1.0));
+ updateProgress();
+ }
+
+ /**
+ * Marks this simulation as done and calls the progress update.
+ */
+ @Override
+ protected void simulationDone() {
+ simulationDone[index] = true;
+ System.out.println("DONE, setting progress");
+ setSimulationProgress(1.0);
+ updateProgress();
+ }
+
+
+ /**
+ * Marks the simulation as done and shows a dialog presenting
+ * the error, unless the simulation was cancelled.
+ */
+ @Override
+ protected void simulationInterrupted(Throwable t) {
+
+ if (t instanceof SimulationCancelledException) {
+ simulationDone();
+ return; // Ignore cancellations
+ }
+
+ // Retrieve the stack trace in a textual form
+ CharArrayWriter arrayWriter = new CharArrayWriter();
+ arrayWriter.append(t.toString() + "\n" + "\n");
+ t.printStackTrace(new PrintWriter(arrayWriter));
+ String stackTrace = arrayWriter.toString();
+
+ // Analyze the exception type
+ if (t instanceof SimulationLaunchException) {
+
+ DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this,
+ new Object[] {
+ "Unable to simulate:",
+ t.getMessage()
+ },
+ null, simulation.getName(), JOptionPane.ERROR_MESSAGE);
+
+ } else if (t instanceof SimulationException) {
+
+ DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this,
+ new Object[] {
+ "A error occurred during the simulation:",
+ t.getMessage()
+ },
+ stackTrace, simulation.getName(), JOptionPane.ERROR_MESSAGE);
+
+ } else if (t instanceof Exception) {
+
+ DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this,
+ new Object[] {
+ "An exception occurred during the simulation:",
+ t.getMessage(),
+ simulation.getSimulationListeners().isEmpty() ?
+ "Please report this as a bug along with the details below." : ""
+ },
+ stackTrace, simulation.getName(), JOptionPane.ERROR_MESSAGE);
+
+ } else if (t instanceof AssertionError) {
+
+ DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this,
+ new Object[] {
+ "A computation error occurred during the simulation.",
+ "Please report this as a bug along with the details below."
+ },
+ stackTrace, simulation.getName(), JOptionPane.ERROR_MESSAGE);
+
+ } else {
+
+ // Probably an Error
+ DetailDialog.showDetailedMessageDialog(SimulationRunDialog.this,
+ new Object[] {
+ "An unknown error was encountered during the simulation.",
+ "The program may be unstable, you should save all your designs " +
+ "and restart OpenRocket now!"
+ },
+ stackTrace, simulation.getName(), JOptionPane.ERROR_MESSAGE);
+
+ }
+ simulationDone();
+ }
+
+
+
+ private void setSimulationProgress(double p) {
+ progress = Math.max(progress, (int)(100*p+0.5));
+ progress = MathUtil.clamp(progress, 0, 100);
+ System.out.println("Setting progress to "+progress+ " (real " +
+ ((int)(100*p+0.5)) + ")");
+ super.setProgress(progress);
+ }
+
+
+ /**
+ * A simulation listener that regularly updates the progress property of the
+ * SimulationWorker and publishes the simulation status for the run dialog to process.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+ private class SimulationProgressListener extends AbstractSimulationListener {
+ private long time = 0;
+
+ @Override
+ public Collection<FlightEvent> handleEvent(FlightEvent event,
+ SimulationStatus status) {
+
+ switch (event.getType()) {
+ case APOGEE:
+ simulationStage = 0;
+ apogeeAltitude = status.position.z;
+ System.out.println("APOGEE, setting progress");
+ setSimulationProgress(APOGEE_PROGRESS);
+ publish(status);
+ break;
+
+ case LAUNCH:
+ publish(status);
+ break;
+
+ case SIMULATION_END:
+ System.out.println("END, setting progress");
+ setSimulationProgress(1.0);
+ break;
+ }
+ return null;
+ }
+
+ @Override
+ public Collection<FlightEvent> stepTaken(SimulationStatus status) {
+ if (System.currentTimeMillis() >= time + UPDATE_MS) {
+ time = System.currentTimeMillis();
+ publish(status);
+ }
+ return null;
+ }
+ }
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.main;
+
+import java.util.Arrays;
+import java.util.Collection;
+
+import javax.swing.SwingWorker;
+
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationListener;
+import net.sf.openrocket.simulation.SimulationStatus;
+import net.sf.openrocket.simulation.exception.SimulationCancelledException;
+import net.sf.openrocket.simulation.listeners.AbstractSimulationListener;
+
+
+
+/**
+ * A SwingWorker that runs a simulation in a background thread. The simulation
+ * always includes a listener that checks whether this SwingWorked has been cancelled,
+ * and throws a {@link SimulationCancelledException} if it has. This allows the
+ * {@link #cancel(boolean)} method to be used to cancel the simulation.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class SimulationWorker extends SwingWorker<FlightData, SimulationStatus> {
+
+ protected final Simulation simulation;
+ private Throwable throwable = null;
+
+ public SimulationWorker(Simulation sim) {
+ this.simulation = sim;
+ }
+
+
+ /**
+ * Runs the simulation.
+ */
+ @Override
+ protected FlightData doInBackground() {
+ if (isCancelled()) {
+ throwable = new SimulationCancelledException("The simulation was interrupted.");
+ return null;
+ }
+
+ SimulationListener[] listeners = getExtraListeners();
+
+ if (listeners != null) {
+ listeners = Arrays.copyOf(listeners, listeners.length+1);
+ } else {
+ listeners = new SimulationListener[1];
+ }
+
+ listeners[listeners.length-1] = new CancelListener();
+
+ try {
+ simulation.simulate(listeners);
+ } catch (Throwable e) {
+// System.out.println("Simulation interrupted:");
+// e.printStackTrace();
+ throwable = e;
+ return null;
+ }
+ return simulation.getSimulatedData();
+ }
+
+
+ /**
+ * Return additional listeners to use during the simulation. The default
+ * implementation returns an empty array.
+ *
+ * @return additional listeners to use, or <code>null</code>.
+ */
+ protected SimulationListener[] getExtraListeners() {
+ return new SimulationListener[0];
+ }
+
+
+ /**
+ * Called after a simulation is successfully simulated. This method is not
+ * called if the simulation ends in an exception.
+ *
+ * @param sim the simulation including the flight data
+ */
+ protected abstract void simulationDone();
+
+ /**
+ * Called if the simulation is interrupted due to an exception.
+ *
+ * @param t the Throwable that caused the interruption
+ */
+ protected abstract void simulationInterrupted(Throwable t);
+
+
+
+ /**
+ * Marks this simulation as done and calls the progress update.
+ */
+ @Override
+ protected final void done() {
+ if (throwable == null)
+ simulationDone();
+ else
+ simulationInterrupted(throwable);
+ }
+
+
+
+ /**
+ * A simulation listener that throws a {@link SimulationCancelledException} if
+ * this SwingWorker has been cancelled. The conditions is checked every time a step
+ * is taken.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+ private class CancelListener extends AbstractSimulationListener {
+
+ @Override
+ public Collection<FlightEvent> stepTaken(SimulationStatus status)
+ throws SimulationCancelledException {
+
+ if (isCancelled()) {
+ throw new SimulationCancelledException("The simulation was interrupted.");
+ }
+
+ return null;
+ }
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.plot;
+
+public class Axis implements Cloneable {
+
+ private double minValue = Double.NaN;
+ private double maxValue = Double.NaN;
+
+
+
+ public void addBound(double value) {
+
+ if (value < minValue || Double.isNaN(minValue)) {
+ minValue = value;
+ }
+ if (value > maxValue || Double.isNaN(maxValue)) {
+ maxValue = value;
+ }
+
+ }
+
+
+ public double getMinValue() {
+ return minValue;
+ }
+
+ public double getMaxValue() {
+ return maxValue;
+ }
+
+ public double getRangeLength() {
+ return maxValue - minValue;
+ }
+
+ public void reset() {
+ minValue = Double.NaN;
+ maxValue = Double.NaN;
+ }
+
+
+
+ @Override
+ public Axis clone() {
+ try {
+
+ return (Axis) super.clone();
+
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException("BUG! Could not clone().");
+ }
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.plot;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.simulation.FlightDataBranch;
+import net.sf.openrocket.simulation.FlightDataBranch.Type;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Pair;
+
+
+public class PlotConfiguration implements Cloneable {
+
+ public static final PlotConfiguration[] DEFAULT_CONFIGURATIONS;
+ static {
+ ArrayList<PlotConfiguration> configs = new ArrayList<PlotConfiguration>();
+ PlotConfiguration config;
+
+ config = new PlotConfiguration("Vertical motion vs. time");
+ config.addPlotDataType(FlightDataBranch.TYPE_ALTITUDE, 0);
+ config.addPlotDataType(FlightDataBranch.TYPE_VELOCITY_Z);
+ config.addPlotDataType(FlightDataBranch.TYPE_ACCELERATION_Z);
+ configs.add(config);
+
+ config = new PlotConfiguration("Total motion vs. time");
+ config.addPlotDataType(FlightDataBranch.TYPE_ALTITUDE, 0);
+ config.addPlotDataType(FlightDataBranch.TYPE_VELOCITY_TOTAL);
+ config.addPlotDataType(FlightDataBranch.TYPE_ACCELERATION_TOTAL);
+ configs.add(config);
+
+ config = new PlotConfiguration("Flight side profile", FlightDataBranch.TYPE_POSITION_X);
+ config.addPlotDataType(FlightDataBranch.TYPE_ALTITUDE);
+ configs.add(config);
+
+ config = new PlotConfiguration("Stability vs. time");
+ config.addPlotDataType(FlightDataBranch.TYPE_STABILITY, 0);
+ config.addPlotDataType(FlightDataBranch.TYPE_CP_LOCATION, 1);
+ config.addPlotDataType(FlightDataBranch.TYPE_CG_LOCATION, 1);
+ configs.add(config);
+
+ config = new PlotConfiguration("Drag coefficients vs. Mach number",
+ FlightDataBranch.TYPE_MACH_NUMBER);
+ config.addPlotDataType(FlightDataBranch.TYPE_DRAG_COEFF, 0);
+ config.addPlotDataType(FlightDataBranch.TYPE_FRICTION_DRAG_COEFF, 0);
+ config.addPlotDataType(FlightDataBranch.TYPE_BASE_DRAG_COEFF, 0);
+ config.addPlotDataType(FlightDataBranch.TYPE_PRESSURE_DRAG_COEFF, 0);
+ configs.add(config);
+
+ config = new PlotConfiguration("Roll characteristics");
+ config.addPlotDataType(FlightDataBranch.TYPE_ROLL_RATE, 0);
+ config.addPlotDataType(FlightDataBranch.TYPE_ROLL_MOMENT_COEFF, 1);
+ config.addPlotDataType(FlightDataBranch.TYPE_ROLL_FORCING_COEFF, 1);
+ config.addPlotDataType(FlightDataBranch.TYPE_ROLL_DAMPING_COEFF, 1);
+ configs.add(config);
+
+ config = new PlotConfiguration("Simulation time step and computation time");
+ config.addPlotDataType(FlightDataBranch.TYPE_TIME_STEP);
+ config.addPlotDataType(FlightDataBranch.TYPE_COMPUTATION_TIME);
+ configs.add(config);
+
+ DEFAULT_CONFIGURATIONS = configs.toArray(new PlotConfiguration[0]);
+ }
+
+
+
+ /** Bonus given for the first type being on the first axis */
+ private static final double BONUS_FIRST_TYPE_ON_FIRST_AXIS = 1.0;
+
+ /**
+ * Bonus given if the first axis includes zero (to prefer first axis having zero over
+ * the others)
+ */
+ private static final double BONUS_FIRST_AXIS_HAS_ZERO = 2.0;
+
+ /** Bonus given for a common zero point on left and right axes. */
+ private static final double BONUS_COMMON_ZERO = 40.0;
+
+ /** Bonus given for only using a single axis. */
+ private static final double BONUS_ONLY_ONE_AXIS = 50.0;
+
+
+ private static final double INCLUDE_ZERO_DISTANCE = 0.3; // 30% of total range
+
+
+
+ /** The data types to be plotted. */
+ private ArrayList<FlightDataBranch.Type> plotDataTypes = new ArrayList<FlightDataBranch.Type>();
+
+ private ArrayList<Unit> plotDataUnits = new ArrayList<Unit>();
+
+ /** The corresponding Axis on which they will be plotted, or null to auto-select. */
+ private ArrayList<Integer> plotDataAxes = new ArrayList<Integer>();
+
+
+ /** The domain (x) axis. */
+ private FlightDataBranch.Type domainAxisType = null;
+ private Unit domainAxisUnit = null;
+
+
+ /** All available axes. */
+ private final int axesCount;
+ private ArrayList<Axis> allAxes = new ArrayList<Axis>();
+
+
+
+ private String name = null;
+
+
+
+ public PlotConfiguration() {
+ this(null, FlightDataBranch.TYPE_TIME);
+ }
+
+ public PlotConfiguration(String name) {
+ this(name, FlightDataBranch.TYPE_TIME);
+ }
+
+ public PlotConfiguration(String name, FlightDataBranch.Type domainType) {
+ this.name = name;
+ // Two axes
+ allAxes.add(new Axis());
+ allAxes.add(new Axis());
+ axesCount = 2;
+
+ setDomainAxisType(domainType);
+ }
+
+
+
+
+
+ public FlightDataBranch.Type getDomainAxisType() {
+ return domainAxisType;
+ }
+
+ public void setDomainAxisType(FlightDataBranch.Type type) {
+ boolean setUnit;
+
+ if (domainAxisType != null && domainAxisType.getUnitGroup() == type.getUnitGroup())
+ setUnit = false;
+ else
+ setUnit = true;
+
+ domainAxisType = type;
+ if (setUnit)
+ domainAxisUnit = domainAxisType.getUnitGroup().getDefaultUnit();
+ }
+
+ public Unit getDomainAxisUnit() {
+ return domainAxisUnit;
+ }
+
+ public void setDomainAxisUnit(Unit u) {
+ if (!domainAxisType.getUnitGroup().contains(u)) {
+ throw new IllegalArgumentException("Setting unit "+u+" to type "+domainAxisType);
+ }
+ domainAxisUnit = u;
+ }
+
+
+
+ public void addPlotDataType(FlightDataBranch.Type type) {
+ plotDataTypes.add(type);
+ plotDataUnits.add(type.getUnitGroup().getDefaultUnit());
+ plotDataAxes.add(-1);
+ }
+
+ public void addPlotDataType(FlightDataBranch.Type type, int axis) {
+ if (axis >= axesCount) {
+ throw new IllegalArgumentException("Axis index too large");
+ }
+ plotDataTypes.add(type);
+ plotDataUnits.add(type.getUnitGroup().getDefaultUnit());
+ plotDataAxes.add(axis);
+ }
+
+
+
+
+ public void setPlotDataType(int index, FlightDataBranch.Type type) {
+ FlightDataBranch.Type origType = plotDataTypes.get(index);
+ plotDataTypes.set(index, type);
+
+ if (origType.getUnitGroup() != type.getUnitGroup()) {
+ plotDataUnits.set(index, type.getUnitGroup().getDefaultUnit());
+ }
+ }
+
+ public void setPlotDataUnit(int index, Unit unit) {
+ if (!plotDataTypes.get(index).getUnitGroup().contains(unit)) {
+ throw new IllegalArgumentException("Attempting to set unit "+unit+" to group "
+ + plotDataTypes.get(index).getUnitGroup());
+ }
+ plotDataUnits.set(index, unit);
+ }
+
+ public void setPlotDataAxis(int index, int axis) {
+ if (axis >= axesCount) {
+ throw new IllegalArgumentException("Axis index too large");
+ }
+ plotDataAxes.set(index, axis);
+ }
+
+
+ public void setPlotDataType(int index, FlightDataBranch.Type type, Unit unit, int axis) {
+ if (axis >= axesCount) {
+ throw new IllegalArgumentException("Axis index too large");
+ }
+ plotDataTypes.set(index, type);
+ plotDataUnits.set(index, unit);
+ plotDataAxes.set(index, axis);
+ }
+
+ public void removePlotDataType(int index) {
+ plotDataTypes.remove(index);
+ plotDataUnits.remove(index);
+ plotDataAxes.remove(index);
+ }
+
+
+
+ public FlightDataBranch.Type getType (int index) {
+ return plotDataTypes.get(index);
+ }
+ public Unit getUnit(int index) {
+ return plotDataUnits.get(index);
+ }
+ public int getAxis(int index) {
+ return plotDataAxes.get(index);
+ }
+
+ public int getTypeCount() {
+ return plotDataTypes.size();
+ }
+
+
+
+ public List<Axis> getAllAxes() {
+ List<Axis> list = new ArrayList<Axis>();
+ list.addAll(allAxes);
+ return list;
+ }
+
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ /**
+ * Returns the name of this PlotConfiguration.
+ */
+ @Override
+ public String toString() {
+ return name;
+ }
+
+
+
+ /**
+ * Find the best combination of the auto-selectable axes.
+ *
+ * @return a new PlotConfiguration with the best fitting auto-selected axes and
+ * axes ranges selected.
+ */
+ public PlotConfiguration fillAutoAxes(FlightDataBranch data) {
+ PlotConfiguration config = recursiveFillAutoAxes(data).getU();
+ System.out.println("BEST FOUND, fitting");
+ config.fitAxes(data);
+ return config;
+ }
+
+
+
+
+ /**
+ * Recursively search for the best combination of the auto-selectable axes.
+ * This is a brute-force search method.
+ *
+ * @return a new PlotConfiguration with the best fitting auto-selected axes and
+ * axes ranges selected, and the goodness value
+ */
+ private Pair<PlotConfiguration, Double> recursiveFillAutoAxes(FlightDataBranch data) {
+
+ // Create copy to fill in
+ PlotConfiguration copy = this.clone();
+
+ int autoindex;
+ for (autoindex=0; autoindex < plotDataAxes.size(); autoindex++) {
+ if (plotDataAxes.get(autoindex) < 0)
+ break;
+ }
+
+
+ if (autoindex >= plotDataAxes.size()) {
+ // All axes have been assigned, just return since we are already the best
+ return new Pair<PlotConfiguration, Double>(copy, copy.getGoodnessValue(data));
+ }
+
+
+ // Set the auto-selected index one at a time and choose the best one
+ PlotConfiguration best = null;
+ double bestValue = Double.NEGATIVE_INFINITY;
+ for (int i=0; i < axesCount; i++) {
+ copy.plotDataAxes.set(autoindex, i);
+ Pair<PlotConfiguration, Double> result = copy.recursiveFillAutoAxes(data);
+ if (result.getV() > bestValue) {
+ best = result.getU();
+ bestValue = result.getV();
+ }
+ }
+
+ return new Pair<PlotConfiguration, Double>(best, bestValue);
+ }
+
+
+
+
+
+ /**
+ * Fit the axes to hold the provided data. All of the plotDataAxis elements must
+ * be non-negative.
+ * <p>
+ * NOTE: This method assumes that only two axes are used.
+ */
+ protected void fitAxes(FlightDataBranch data) {
+
+ // Reset axes
+ for (Axis a: allAxes) {
+ a.reset();
+ }
+
+ // Add full range to the axes
+ int length = plotDataTypes.size();
+ for (int i=0; i<length; i++) {
+ FlightDataBranch.Type type = plotDataTypes.get(i);
+ Unit unit = plotDataUnits.get(i);
+ int index = plotDataAxes.get(i);
+ if (index < 0) {
+ throw new IllegalStateException("fitAxes called with auto-selected axis");
+ }
+ Axis axis = allAxes.get(index);
+
+ double min = unit.toUnit(data.getMinimum(type));
+ double max = unit.toUnit(data.getMaximum(type));
+
+ axis.addBound(min);
+ axis.addBound(max);
+ }
+
+ // Ensure non-zero (or NaN) range, add a few percent range, include zero if it is close
+ for (Axis a: allAxes) {
+ if (MathUtil.equals(a.getMinValue(), a.getMaxValue())) {
+ a.addBound(a.getMinValue()-1);
+ a.addBound(a.getMaxValue()+1);
+ }
+
+ double addition = a.getRangeLength() * 0.03;
+ a.addBound(a.getMinValue() - addition);
+ a.addBound(a.getMaxValue() + addition);
+
+ double dist;
+ dist = Math.min(Math.abs(a.getMinValue()), Math.abs(a.getMaxValue()));
+ if (dist <= a.getRangeLength() * INCLUDE_ZERO_DISTANCE) {
+ a.addBound(0);
+ }
+ }
+
+
+ // Check whether to use a common zero
+ Axis left = allAxes.get(0);
+ Axis right = allAxes.get(1);
+
+ if (left.getMinValue() > 0 || left.getMaxValue() < 0 ||
+ right.getMinValue() > 0 || right.getMaxValue() < 0 ||
+ Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue()))
+ return;
+
+
+
+ //// Compute common zero
+ // TODO: MEDIUM: This algorithm may require tweaking
+
+ double min1 = left.getMinValue();
+ double max1 = left.getMaxValue();
+ double min2 = right.getMinValue();
+ double max2 = right.getMaxValue();
+
+ // Calculate and round scaling factor
+ double scale = Math.max(left.getRangeLength(), right.getRangeLength()) /
+ Math.min(left.getRangeLength(), right.getRangeLength());
+
+ System.out.println("Scale: "+scale);
+
+ scale = roundScale(scale);
+ if (right.getRangeLength() > left.getRangeLength()) {
+ scale = 1/scale;
+ }
+ System.out.println("Rounded scale: " + scale);
+
+ // Scale right axis, enlarge axes if necessary and scale back
+ min2 *= scale;
+ max2 *= scale;
+ min1 = Math.min(min1, min2);
+ min2 = min1;
+ max1 = Math.max(max1, max2);
+ max2 = max1;
+ min2 /= scale;
+ max2 /= scale;
+
+
+
+ // Scale to unit length
+// double scale1 = left.getRangeLength();
+// double scale2 = right.getRangeLength();
+//
+// double min1 = left.getMinValue() / scale1;
+// double max1 = left.getMaxValue() / scale1;
+// double min2 = right.getMinValue() / scale2;
+// double max2 = right.getMaxValue() / scale2;
+//
+// // Combine unit ranges
+// min1 = MathUtil.min(min1, min2);
+// min2 = min1;
+// max1 = MathUtil.max(max1, max2);
+// max2 = max1;
+//
+// // Scale up
+// min1 *= scale1;
+// max1 *= scale1;
+// min2 *= scale2;
+// max2 *= scale2;
+//
+// // Compute common scale
+// double range1 = max1-min1;
+// double range2 = max2-min2;
+//
+// double scale = MathUtil.max(range1, range2) / MathUtil.min(range1, range2);
+// double roundScale = roundScale(scale);
+//
+// if (range2 < range1) {
+// if (roundScale < scale) {
+// min2 = min1 / roundScale;
+// max2 = max1 / roundScale;
+// } else {
+// min1 = min2 * roundScale;
+// max1 = max2 * roundScale;
+// }
+// } else {
+// if (roundScale > scale) {
+// min2 = min1 * roundScale;
+// max2 = max1 * roundScale;
+// } else {
+// min1 = min2 / roundScale;
+// max1 = max2 / roundScale;
+// }
+// }
+
+ // Apply scale
+ left.addBound(min1);
+ left.addBound(max1);
+ right.addBound(min2);
+ right.addBound(max2);
+
+ }
+
+
+
+ private double roundScale(double scale) {
+ double mul = 1;
+ while (scale >= 10) {
+ scale /= 10;
+ mul *= 10;
+ }
+ while (scale < 1) {
+ scale *= 10;
+ mul /= 10;
+ }
+
+ // 1 2 4 5 10
+
+ if (scale > 7.5) {
+ scale = 10;
+ } else if (scale > 4.5) {
+ scale = 5;
+ } else if (scale > 3) {
+ scale = 4;
+ } else if (scale > 1.5) {
+ scale = 2;
+ } else {
+ scale = 1;
+ }
+ return scale*mul;
+ }
+
+
+
+ private double roundScaleUp(double scale) {
+ double mul = 1;
+ while (scale >= 10) {
+ scale /= 10;
+ mul *= 10;
+ }
+ while (scale < 1) {
+ scale *= 10;
+ mul /= 10;
+ }
+
+ if (scale > 5) {
+ scale = 10;
+ } else if (scale > 4) {
+ scale = 5;
+ } else if (scale > 2) {
+ scale = 4;
+ } else if (scale > 1) {
+ scale = 2;
+ } else {
+ scale = 1;
+ }
+ return scale*mul;
+ }
+
+
+ private double roundScaleDown(double scale) {
+ double mul = 1;
+ while (scale >= 10) {
+ scale /= 10;
+ mul *= 10;
+ }
+ while (scale < 1) {
+ scale *= 10;
+ mul /= 10;
+ }
+
+ if (scale > 5) {
+ scale = 5;
+ } else if (scale > 4) {
+ scale = 4;
+ } else if (scale > 2) {
+ scale = 2;
+ } else {
+ scale = 1;
+ }
+ return scale*mul;
+ }
+
+
+
+ /**
+ * Fits the axis ranges to the data and returns the "goodness value" of this
+ * selection of axes. All plotDataAxis elements must be non-null.
+ * <p>
+ * NOTE: This method assumes that all data can fit into the axes ranges and
+ * that only two axes are used.
+ *
+ * @return a "goodness value", the larger the better.
+ */
+ protected double getGoodnessValue(FlightDataBranch data) {
+ double goodness = 0;
+ int length = plotDataTypes.size();
+
+ // Fit the axes ranges to the data
+ fitAxes(data);
+
+ /*
+ * Calculate goodness of ranges. 100 points is given if the values fill the
+ * entire range, 0 if they fill none of it.
+ */
+ for (int i = 0; i < length; i++) {
+ FlightDataBranch.Type type = plotDataTypes.get(i);
+ Unit unit = plotDataUnits.get(i);
+ int index = plotDataAxes.get(i);
+ if (index < 0) {
+ throw new IllegalStateException("getGoodnessValue called with auto-selected axis");
+ }
+ Axis axis = allAxes.get(index);
+
+ double min = unit.toUnit(data.getMinimum(type));
+ double max = unit.toUnit(data.getMaximum(type));
+ if (Double.isNaN(min) || Double.isNaN(max))
+ continue;
+ if (MathUtil.equals(min, max))
+ continue;
+
+ double d = (max-min) / axis.getRangeLength();
+ d = Math.sqrt(d); // Prioritize small ranges
+ goodness += d * 100.0;
+ }
+
+
+ /*
+ * Add extra points for specific things.
+ */
+
+ // A little for the first type being on the first axis
+ if (plotDataAxes.get(0) == 0)
+ goodness += BONUS_FIRST_TYPE_ON_FIRST_AXIS;
+
+ // A little bonus if the first axis contains zero
+ Axis left = allAxes.get(0);
+ if (left.getMinValue() <= 0 && left.getMaxValue() >= 0)
+ goodness += BONUS_FIRST_AXIS_HAS_ZERO;
+
+ // A boost if a common zero was used in the ranging
+ Axis right = allAxes.get(1);
+ if (left.getMinValue() <= 0 && left.getMaxValue() >= 0 &&
+ right.getMinValue() <= 0 && right.getMaxValue() >= 0)
+ goodness += BONUS_COMMON_ZERO;
+
+ // A boost if only one axis is used
+ if (Double.isNaN(left.getMinValue()) || Double.isNaN(right.getMinValue()))
+ goodness += BONUS_ONLY_ONE_AXIS;
+
+ return goodness;
+ }
+
+
+
+ /**
+ * Reset the units of this configuration to the default units. Returns this
+ * PlotConfiguration.
+ *
+ * @return this PlotConfiguration.
+ */
+ public PlotConfiguration resetUnits() {
+ for (int i=0; i < plotDataTypes.size(); i++) {
+ plotDataUnits.set(i, plotDataTypes.get(i).getUnitGroup().getDefaultUnit());
+ }
+ return this;
+ }
+
+
+
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public PlotConfiguration clone() {
+ try {
+
+ PlotConfiguration copy = (PlotConfiguration) super.clone();
+
+ // Shallow-clone all immutable lists
+ copy.plotDataTypes = (ArrayList<Type>) this.plotDataTypes.clone();
+ copy.plotDataAxes = (ArrayList<Integer>) this.plotDataAxes.clone();
+ copy.plotDataUnits = (ArrayList<Unit>) this.plotDataUnits.clone();
+
+ // Deep-clone all Axis since they are mutable
+ copy.allAxes = new ArrayList<Axis>();
+ for (Axis a: this.allAxes) {
+ copy.allAxes.add(a.clone());
+ }
+
+ return copy;
+
+
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException("BUG! Could not clone().");
+ }
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.plot;
+
+import java.awt.Color;
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.util.List;
+
+import javax.swing.BorderFactory;
+import javax.swing.JButton;
+import javax.swing.JDialog;
+import javax.swing.JPanel;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.simulation.FlightDataBranch;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.GUIUtil;
+
+import org.jfree.chart.ChartFactory;
+import org.jfree.chart.ChartPanel;
+import org.jfree.chart.JFreeChart;
+import org.jfree.chart.axis.NumberAxis;
+import org.jfree.chart.plot.PlotOrientation;
+import org.jfree.chart.plot.ValueMarker;
+import org.jfree.chart.plot.XYPlot;
+import org.jfree.chart.renderer.xy.StandardXYItemRenderer;
+import org.jfree.chart.title.TextTitle;
+import org.jfree.data.xy.XYSeries;
+import org.jfree.data.xy.XYSeriesCollection;
+
+public class PlotDialog extends JDialog {
+
+ private PlotDialog(Window parent, Simulation simulation, PlotConfiguration config) {
+ super(parent, "Flight data plot");
+ this.setModalityType(ModalityType.DOCUMENT_MODAL);
+
+
+ // Fill the auto-selections
+ FlightDataBranch branch = simulation.getSimulatedData().getBranch(0);
+ PlotConfiguration filled = config.fillAutoAxes(branch);
+ List<Axis> axes = filled.getAllAxes();
+
+
+ // Create the data series for both axes
+ XYSeriesCollection[] data = new XYSeriesCollection[2];
+ data[0] = new XYSeriesCollection();
+ data[1] = new XYSeriesCollection();
+
+
+ // Get the domain axis type
+ final FlightDataBranch.Type domainType = filled.getDomainAxisType();
+ final Unit domainUnit = filled.getDomainAxisUnit();
+ if (domainType == null) {
+ throw new IllegalArgumentException("Domain axis type not specified.");
+ }
+ List<Double> x = branch.get(domainType);
+
+
+ // Create the XYSeries objects from the flight data and store into the collections
+ int length = filled.getTypeCount();
+ String[] axisLabel = new String[2];
+ for (int i = 0; i < length; i++) {
+ // Get info
+ FlightDataBranch.Type type = filled.getType(i);
+ Unit unit = filled.getUnit(i);
+ int axis = filled.getAxis(i);
+ String name = getLabel(type, unit);
+
+ // Store data in provided units
+ List<Double> y = branch.get(type);
+ XYSeries series = new XYSeries(name, false, true);
+ for (int j=0; j<x.size(); j++) {
+ series.add(domainUnit.toUnit(x.get(j)), unit.toUnit(y.get(j)));
+ }
+ data[axis].addSeries(series);
+
+ // Update axis label
+ if (axisLabel[axis] == null)
+ axisLabel[axis] = type.getName();
+ else
+ axisLabel[axis] += "; " + type.getName();
+ }
+
+
+ // Create the chart using the factory to get all default settings
+ JFreeChart chart = ChartFactory.createXYLineChart(
+ "Simulated flight",
+ null,
+ null,
+ null,
+ PlotOrientation.VERTICAL,
+ true,
+ true,
+ false
+ );
+
+ chart.addSubtitle(new TextTitle(config.getName()));
+
+ // Add the data and formatting to the plot
+ XYPlot plot = chart.getXYPlot();
+ int axisno = 0;
+ for (int i=0; i<2; i++) {
+ // Check whether axis has any data
+ if (data[i].getSeriesCount() > 0) {
+ // Create and set axis
+ double min = axes.get(i).getMinValue();
+ double max = axes.get(i).getMaxValue();
+ NumberAxis axis = new PresetNumberAxis(min, max);
+ axis.setLabel(axisLabel[i]);
+// axis.setRange(axes.get(i).getMinValue(), axes.get(i).getMaxValue());
+ plot.setRangeAxis(axisno, axis);
+
+ // Add data and map to the axis
+ plot.setDataset(axisno, data[i]);
+ plot.setRenderer(axisno, new StandardXYItemRenderer());
+ plot.mapDatasetToRangeAxis(axisno, axisno);
+ axisno++;
+ }
+ }
+
+ plot.getDomainAxis().setLabel(getLabel(domainType,domainUnit));
+ plot.addDomainMarker(new ValueMarker(0));
+ plot.addRangeMarker(new ValueMarker(0));
+
+
+ // Create the dialog
+
+ JPanel panel = new JPanel(new MigLayout("fill"));
+ this.add(panel);
+
+ ChartPanel chartPanel = new ChartPanel(chart,
+ false, // properties
+ true, // save
+ false, // print
+ true, // zoom
+ true); // tooltips
+ chartPanel.setMouseWheelEnabled(true);
+ chartPanel.setEnforceFileExtensions(true);
+ chartPanel.setInitialDelay(500);
+
+ chartPanel.setBorder(BorderFactory.createLineBorder(Color.GRAY, 1));
+
+ panel.add(chartPanel, "grow, wrap 20lp");
+
+ JButton button = new JButton("Close");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ PlotDialog.this.dispose();
+ }
+ });
+ panel.add(button, "right");
+
+ this.setLocationByPlatform(true);
+ this.pack();
+ GUIUtil.installEscapeCloseOperation(this);
+ GUIUtil.setDefaultButton(button);
+ }
+
+
+ private String getLabel(FlightDataBranch.Type type, Unit unit) {
+ String name = type.getName();
+ if (unit != null && !UnitGroup.UNITS_NONE.contains(unit) &&
+ !UnitGroup.UNITS_COEFFICIENT.contains(unit) && unit.getUnit().length() > 0)
+ name += " ("+unit.getUnit() + ")";
+ return name;
+ }
+
+
+
+ private class PresetNumberAxis extends NumberAxis {
+ private final double min;
+ private final double max;
+
+ public PresetNumberAxis(double min, double max) {
+ this.min = min;
+ this.max = max;
+ autoAdjustRange();
+ }
+
+ @Override
+ protected void autoAdjustRange() {
+ this.setRange(min, max);
+ }
+ }
+
+
+ /**
+ * Static method that shows a plot with the specified parameters.
+ *
+ * @param parent the parent window, which will be blocked.
+ * @param simulation the simulation to plot.
+ * @param config the configuration of the plot.
+ */
+ public static void showPlot(Window parent, Simulation simulation, PlotConfiguration config) {
+ new PlotDialog(parent, simulation, config).setVisible(true);
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.plot;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.ItemEvent;
+import java.awt.event.ItemListener;
+import java.util.Arrays;
+
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.SwingUtilities;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.gui.ResizeLabel;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.simulation.FlightDataBranch;
+import net.sf.openrocket.simulation.FlightDataBranch.Type;
+import net.sf.openrocket.unit.Unit;
+
+public class PlotPanel extends JPanel {
+
+ // TODO: LOW: Should these be somewhere else?
+ public static final int AUTO = -1;
+ public static final int LEFT = 0;
+ public static final int RIGHT = 1;
+
+ public static final String AUTO_NAME = "Auto";
+ public static final String LEFT_NAME = "Left";
+ public static final String RIGHT_NAME = "Right";
+
+ private static final String CUSTOM = "Custom";
+
+ /** The "Custom" configuration - not to be used for anything other than the title. */
+ private static final PlotConfiguration CUSTOM_CONFIGURATION;
+ static {
+ CUSTOM_CONFIGURATION = new PlotConfiguration(CUSTOM);
+ }
+
+ /** The array of presets for the combo box. */
+ private static final PlotConfiguration[] PRESET_ARRAY;
+ static {
+ PRESET_ARRAY = Arrays.copyOf(PlotConfiguration.DEFAULT_CONFIGURATIONS,
+ PlotConfiguration.DEFAULT_CONFIGURATIONS.length + 1);
+ PRESET_ARRAY[PRESET_ARRAY.length-1] = CUSTOM_CONFIGURATION;
+ }
+
+
+
+ /** The current default configuration, set each time a plot is made. */
+ private static PlotConfiguration defaultConfiguration =
+ PlotConfiguration.DEFAULT_CONFIGURATIONS[0].resetUnits();
+
+
+ private final Simulation simulation;
+ private final FlightDataBranch.Type[] types;
+ private PlotConfiguration configuration;
+
+
+ private JComboBox configurationSelector;
+
+ private JComboBox domainTypeSelector;
+ private UnitSelector domainUnitSelector;
+
+ private JPanel typeSelectorPanel;
+
+
+ private int modifying = 0;
+
+
+ public PlotPanel(final Simulation simulation) {
+ super(new MigLayout("fill"));
+
+ this.simulation = simulation;
+ if (simulation.getSimulatedData() == null ||
+ simulation.getSimulatedData().getBranchCount()==0) {
+ throw new IllegalArgumentException("Simulation contains no data.");
+ }
+ FlightDataBranch branch = simulation.getSimulatedData().getBranch(0);
+ types = branch.getTypes();
+
+ // TODO: LOW: Revert to custom if data type is not available.
+ configuration = defaultConfiguration.clone();
+
+
+
+
+ // Setup the combo box
+ configurationSelector = new JComboBox(PRESET_ARRAY);
+ for (PlotConfiguration config: PRESET_ARRAY) {
+ if (config.getName().equals(configuration.getName())) {
+ configurationSelector.setSelectedItem(config);
+ }
+ }
+ configurationSelector.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ if (modifying > 0)
+ return;
+ PlotConfiguration conf = (PlotConfiguration)configurationSelector.getSelectedItem();
+ if (conf == CUSTOM_CONFIGURATION)
+ return;
+ modifying++;
+ configuration = conf.clone().resetUnits();
+ updatePlots();
+ modifying--;
+ }
+ });
+ this.add(new JLabel("Preset plot configurations: "), "spanx, split");
+ this.add(configurationSelector,"growx, wrap 30lp");
+
+
+
+ this.add(new JLabel("X axis type:"), "spanx, split");
+ domainTypeSelector = new JComboBox(types);
+ domainTypeSelector.setSelectedItem(configuration.getDomainAxisType());
+ domainTypeSelector.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ if (modifying > 0)
+ return;
+ FlightDataBranch.Type type = (Type) domainTypeSelector.getSelectedItem();
+ configuration.setDomainAxisType(type);
+ domainUnitSelector.setUnitGroup(type.getUnitGroup());
+ domainUnitSelector.setSelectedUnit(configuration.getDomainAxisUnit());
+ setToCustom();
+ }
+ });
+ this.add(domainTypeSelector, "gapright para");
+
+
+ this.add(new JLabel("Unit:"));
+ domainUnitSelector = new UnitSelector(configuration.getDomainAxisType().getUnitGroup());
+ domainUnitSelector.setSelectedUnit(configuration.getDomainAxisUnit());
+ domainUnitSelector.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ if (modifying > 0)
+ return;
+ configuration.setDomainAxisUnit(domainUnitSelector.getSelectedUnit());
+ }
+ });
+ this.add(domainUnitSelector, "width 40lp, gapright para");
+
+
+ ResizeLabel desc = new ResizeLabel("<html><p>The data will be plotted in time order " +
+ "even if the X axis type is not time.", -2);
+ this.add(desc, "width :0px:, growx, wrap para");
+
+
+
+ this.add(new JLabel("Y axis types:"), "spanx, wrap rel");
+
+ typeSelectorPanel = new JPanel(new MigLayout("gapy rel"));
+ JScrollPane scroll = new JScrollPane(typeSelectorPanel);
+ this.add(scroll, "spanx, height :0:, grow, wrap para");
+
+
+ JButton button = new JButton("New Y axis plot type");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ if (configuration.getTypeCount() >= 15) {
+ JOptionPane.showMessageDialog(PlotPanel.this,
+ "A maximum of 15 plots is allowed.", "Cannot add plot",
+ JOptionPane.ERROR_MESSAGE);
+ return;
+ }
+
+ // Select new type smartly
+ FlightDataBranch.Type type = null;
+ for (FlightDataBranch.Type t:
+ simulation.getSimulatedData().getBranch(0).getTypes()) {
+
+ boolean used = false;
+ if (configuration.getDomainAxisType().equals(t)) {
+ used = true;
+ } else {
+ for (int i=0; i < configuration.getTypeCount(); i++) {
+ if (configuration.getType(i).equals(t)) {
+ used = true;
+ break;
+ }
+ }
+ }
+
+ if (!used) {
+ type = t;
+ break;
+ }
+ }
+ if (type == null) {
+ type = simulation.getSimulatedData().getBranch(0).getTypes()[0];
+ }
+
+ // Add new type
+ configuration.addPlotDataType(type);
+ setToCustom();
+ updatePlots();
+ }
+ });
+ this.add(button, "spanx, split");
+
+ this.add(new JPanel(), "growx");
+
+ button = new JButton("Plot flight");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ defaultConfiguration = configuration.clone();
+ PlotDialog.showPlot(SwingUtilities.getWindowAncestor(PlotPanel.this),
+ simulation, configuration);
+ }
+ });
+ this.add(button, "right");
+
+
+ updatePlots();
+ }
+
+
+ private void setToCustom() {
+ configuration.setName(CUSTOM);
+ configurationSelector.setSelectedItem(CUSTOM_CONFIGURATION);
+ }
+
+
+ private void updatePlots() {
+ domainTypeSelector.setSelectedItem(configuration.getDomainAxisType());
+ domainUnitSelector.setUnitGroup(configuration.getDomainAxisType().getUnitGroup());
+ domainUnitSelector.setSelectedUnit(configuration.getDomainAxisUnit());
+
+ typeSelectorPanel.removeAll();
+ for (int i=0; i < configuration.getTypeCount(); i++) {
+ FlightDataBranch.Type type = configuration.getType(i);
+ Unit unit = configuration.getUnit(i);
+ int axis = configuration.getAxis(i);
+
+ typeSelectorPanel.add(new PlotTypeSelector(i, type, unit, axis), "wrap");
+ }
+
+ typeSelectorPanel.repaint();
+ }
+
+
+
+
+ /**
+ * A JPanel which configures a single plot of a PlotConfiguration.
+ */
+ private class PlotTypeSelector extends JPanel {
+ private final String[] POSITIONS = { AUTO_NAME, LEFT_NAME, RIGHT_NAME };
+
+ private final int index;
+ private JComboBox typeSelector;
+ private UnitSelector unitSelector;
+ private JComboBox axisSelector;
+
+
+ public PlotTypeSelector(int index, FlightDataBranch.Type type) {
+ this (index, type, null, -1);
+ }
+
+ public PlotTypeSelector(int plotIndex, FlightDataBranch.Type type, Unit unit, int position) {
+ super(new MigLayout(""));
+
+ this.index = plotIndex;
+
+ typeSelector = new JComboBox(types);
+ typeSelector.setSelectedItem(type);
+ typeSelector.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ if (modifying > 0)
+ return;
+ FlightDataBranch.Type type = (Type) typeSelector.getSelectedItem();
+ configuration.setPlotDataType(index, type);
+ unitSelector.setUnitGroup(type.getUnitGroup());
+ unitSelector.setSelectedUnit(configuration.getUnit(index));
+ setToCustom();
+ }
+ });
+ this.add(typeSelector, "gapright para");
+
+ this.add(new JLabel("Unit:"));
+ unitSelector = new UnitSelector(type.getUnitGroup());
+ if (unit != null)
+ unitSelector.setSelectedUnit(unit);
+ unitSelector.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ if (modifying > 0)
+ return;
+ Unit unit = (Unit) unitSelector.getSelectedUnit();
+ configuration.setPlotDataUnit(index, unit);
+ }
+ });
+ this.add(unitSelector, "width 40lp, gapright para");
+
+ this.add(new JLabel("Axis:"));
+ axisSelector = new JComboBox(POSITIONS);
+ if (position == LEFT)
+ axisSelector.setSelectedIndex(1);
+ else if (position == RIGHT)
+ axisSelector.setSelectedIndex(2);
+ else
+ axisSelector.setSelectedIndex(0);
+ axisSelector.addItemListener(new ItemListener() {
+ @Override
+ public void itemStateChanged(ItemEvent e) {
+ if (modifying > 0)
+ return;
+ int axis = axisSelector.getSelectedIndex() - 1;
+ configuration.setPlotDataAxis(index, axis);
+ }
+ });
+ this.add(axisSelector, "gapright para");
+
+
+ JButton button = new JButton("Remove");
+ button.addActionListener(new ActionListener() {
+ @Override
+ public void actionPerformed(ActionEvent e) {
+ configuration.removePlotDataType(index);
+ setToCustom();
+ updatePlots();
+ }
+ });
+ this.add(button);
+ }
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.rocketfigure;
+
+import java.awt.Shape;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Rectangle2D;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.Transformation;
+
+
+public class BodyTubeShapes extends RocketComponentShapes {
+
+ public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component,
+ Transformation transformation) {
+ net.sf.openrocket.rocketcomponent.BodyTube tube = (net.sf.openrocket.rocketcomponent.BodyTube)component;
+
+ double length = tube.getLength();
+ double radius = tube.getRadius();
+ Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0)));
+
+ Shape[] s = new Shape[start.length];
+ for (int i=0; i < start.length; i++) {
+ s[i] = new Rectangle2D.Double(start[i].x*S,(start[i].y-radius)*S,
+ length*S,2*radius*S);
+ }
+ return s;
+ }
+
+
+ public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component,
+ Transformation transformation) {
+ net.sf.openrocket.rocketcomponent.BodyTube tube = (net.sf.openrocket.rocketcomponent.BodyTube)component;
+
+ double or = tube.getRadius();
+
+ Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0)));
+
+ Shape[] s = new Shape[start.length];
+ for (int i=0; i < start.length; i++) {
+ s[i] = new Ellipse2D.Double((start[i].z-or)*S,(start[i].y-or)*S,2*or*S,2*or*S);
+ }
+ return s;
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.gui.rocketfigure;
+
+import java.awt.Shape;
+import java.awt.geom.Path2D;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Transformation;
+
+
+public class FinSetShapes extends RocketComponentShapes {
+
+ // TODO: LOW: Clustering is ignored (FinSet cannot currently be clustered)
+
+ public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component,
+ Transformation transformation) {
+ net.sf.openrocket.rocketcomponent.FinSet finset = (net.sf.openrocket.rocketcomponent.FinSet)component;
+
+
+ int fins = finset.getFinCount();
+ Transformation cantRotation = finset.getCantRotation();
+ Transformation baseRotation = finset.getBaseRotationTransformation();
+ Transformation finRotation = finset.getFinRotationTransformation();
+
+ Coordinate c[] = finset.getFinPoints();
+
+
+ // TODO: MEDIUM: sloping radius
+ double radius = finset.getBodyRadius();
+
+ // Translate & rotate the coordinates
+ for (int i=0; i<c.length; i++) {
+ c[i] = cantRotation.transform(c[i]);
+ c[i] = baseRotation.transform(c[i].add(0,radius,0));
+ }
+
+
+ // Generate shapes
+ Shape[] s = new Shape[fins];
+ for (int fin=0; fin<fins; fin++) {
+ Coordinate a;
+ Path2D.Float p;
+
+ // Make polygon
+ p = new Path2D.Float();
+ for (int i=0; i<c.length; i++) {
+ a = transformation.transform(finset.toAbsolute(c[i])[0]);
+ if (i==0)
+ p.moveTo(a.x*S, a.y*S);
+ else
+ p.lineTo(a.x*S, a.y*S);
+ }
+ p.closePath();
+ s[fin] = p;
+
+ // Rotate fin coordinates
+ for (int i=0; i<c.length; i++)
+ c[i] = finRotation.transform(c[i]);
+ }
+
+ return s;
+ }
+
+ public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component,
+ Transformation transformation) {
+
+ net.sf.openrocket.rocketcomponent.FinSet finset = (net.sf.openrocket.rocketcomponent.FinSet)component;
+
+ if (MathUtil.equals(finset.getCantAngle(),0))
+ return uncantedShapesBack(finset, transformation);
+ else
+ return cantedShapesBack(finset, transformation);
+
+ }
+
+
+ private static Shape[] uncantedShapesBack(net.sf.openrocket.rocketcomponent.FinSet finset,
+ Transformation transformation) {
+
+ int fins = finset.getFinCount();
+ double radius = finset.getBodyRadius();
+ double thickness = finset.getThickness();
+ double height = finset.getSpan();
+
+ Transformation baseRotation = finset.getBaseRotationTransformation();
+ Transformation finRotation = finset.getFinRotationTransformation();
+
+
+ // Generate base coordinates for a single fin
+ Coordinate c[] = new Coordinate[4];
+ c[0]=new Coordinate(0,radius,-thickness/2);
+ c[1]=new Coordinate(0,radius,thickness/2);
+ c[2]=new Coordinate(0,height+radius,thickness/2);
+ c[3]=new Coordinate(0,height+radius,-thickness/2);
+
+ // Apply base rotation
+ transformPoints(c,baseRotation);
+
+ // Generate shapes
+ Shape[] s = new Shape[fins];
+ for (int fin=0; fin<fins; fin++) {
+ Coordinate a;
+ Path2D.Double p;
+
+ // Make polygon
+ p = new Path2D.Double();
+ a = transformation.transform(finset.toAbsolute(c[0])[0]);
+ p.moveTo(a.z*S, a.y*S);
+ a = transformation.transform(finset.toAbsolute(c[1])[0]);
+ p.lineTo(a.z*S, a.y*S);
+ a = transformation.transform(finset.toAbsolute(c[2])[0]);
+ p.lineTo(a.z*S, a.y*S);
+ a = transformation.transform(finset.toAbsolute(c[3])[0]);
+ p.lineTo(a.z*S, a.y*S);
+ p.closePath();
+ s[fin] = p;
+
+ // Rotate fin coordinates
+ transformPoints(c,finRotation);
+ }
+
+ return s;
+ }
+
+
+ // TODO: LOW: Jagged shapes from back draw incorrectly.
+ private static Shape[] cantedShapesBack(net.sf.openrocket.rocketcomponent.FinSet finset,
+ Transformation transformation) {
+ int i;
+ int fins = finset.getFinCount();
+ double radius = finset.getBodyRadius();
+ double thickness = finset.getThickness();
+
+ Transformation baseRotation = finset.getBaseRotationTransformation();
+ Transformation finRotation = finset.getFinRotationTransformation();
+ Transformation cantRotation = finset.getCantRotation();
+
+ Coordinate[] sidePoints;
+ Coordinate[] backPoints;
+ int maxIndex;
+
+ Coordinate[] points = finset.getFinPoints();
+ for (maxIndex = points.length-1; maxIndex > 0; maxIndex--) {
+ if (points[maxIndex-1].y < points[maxIndex].y)
+ break;
+ }
+
+ transformPoints(points,cantRotation);
+ transformPoints(points,new Transformation(0,radius,0));
+ transformPoints(points,baseRotation);
+
+
+ sidePoints = new Coordinate[points.length];
+ backPoints = new Coordinate[2*(points.length-maxIndex)];
+ double sign;
+ if (finset.getCantAngle() > 0) {
+ sign = 1.0;
+ } else {
+ sign = -1.0;
+ }
+
+ // Calculate points for the side panel
+ for (i=0; i < points.length; i++) {
+ sidePoints[i] = points[i].add(0,0,sign*thickness/2);
+ }
+
+ // Calculate points for the back portion
+ i=0;
+ for (int j=points.length-1; j >= maxIndex; j--, i++) {
+ backPoints[i] = points[j].add(0,0,sign*thickness/2);
+ }
+ for (int j=maxIndex; j <= points.length-1; j++, i++) {
+ backPoints[i] = points[j].add(0,0,-sign*thickness/2);
+ }
+
+ // Generate shapes
+ Shape[] s;
+ if (thickness > 0.0005) {
+
+ s = new Shape[fins*2];
+ for (int fin=0; fin<fins; fin++) {
+
+ s[2*fin] = makePolygonBack(sidePoints,finset,transformation);
+ s[2*fin+1] = makePolygonBack(backPoints,finset,transformation);
+
+ // Rotate fin coordinates
+ transformPoints(sidePoints,finRotation);
+ transformPoints(backPoints,finRotation);
+ }
+
+ } else {
+
+ s = new Shape[fins];
+ for (int fin=0; fin<fins; fin++) {
+ s[fin] = makePolygonBack(sidePoints,finset,transformation);
+ transformPoints(sidePoints,finRotation);
+ }
+
+ }
+
+ return s;
+ }
+
+
+
+ private static void transformPoints(Coordinate[] array, Transformation t) {
+ for (int i=0; i < array.length; i++) {
+ array[i] = t.transform(array[i]);
+ }
+ }
+
+ private static Shape makePolygonBack(Coordinate[] array, net.sf.openrocket.rocketcomponent.FinSet finset,
+ Transformation t) {
+ Path2D.Float p;
+
+ // Make polygon
+ p = new Path2D.Float();
+ for (int i=0; i < array.length; i++) {
+ Coordinate a = t.transform(finset.toAbsolute(array[i])[0]);
+ if (i==0)
+ p.moveTo(a.z*S, a.y*S);
+ else
+ p.lineTo(a.z*S, a.y*S);
+ }
+ p.closePath();
+ return p;
+ }
+
+
+ /* Side painting with thickness:
+
+ Coordinate c[] = new Coordinate[8];
+
+ c[0]=new Coordinate(0-position*rootChord,radius,thickness/2);
+ c[1]=new Coordinate(rootChord-position*rootChord,radius,thickness/2);
+ c[2]=new Coordinate(sweep+tipChord-position*rootChord,height+radius,thickness/2);
+ c[3]=new Coordinate(sweep-position*rootChord,height+radius,thickness/2);
+
+ c[4]=new Coordinate(0-position*rootChord,radius,-thickness/2);
+ c[5]=new Coordinate(rootChord-position*rootChord,radius,-thickness/2);
+ c[6]=new Coordinate(sweep+tipChord-position*rootChord,height+radius,-thickness/2);
+ c[7]=new Coordinate(sweep-position*rootChord,height+radius,-thickness/2);
+
+ if (rotation != 0) {
+ rot = Transformation.rotate_x(rotation);
+ for (int i=0; i<8; i++)
+ c[i] = rot.transform(c[i]);
+ }
+
+ Shape[] s = new Shape[fins*6];
+ rot = Transformation.rotate_x(2*Math.PI/fins);
+
+ for (int fin=0; fin<fins; fin++) {
+ Coordinate a,b;
+ Path2D.Float p;
+
+ // First polygon
+ p = new Path2D.Float();
+ a = finset.toAbsolute(c[0]);
+ p.moveTo(a.x(), a.y());
+ a = finset.toAbsolute(c[1]);
+ p.lineTo(a.x(), a.y());
+ a = finset.toAbsolute(c[2]);
+ p.lineTo(a.x(), a.y());
+ a = finset.toAbsolute(c[3]);
+ p.lineTo(a.x(), a.y());
+ p.closePath();
+ s[fin*6] = p;
+
+ // Second polygon
+ p = new Path2D.Float();
+ a = finset.toAbsolute(c[4]);
+ p.moveTo(a.x(), a.y());
+ a = finset.toAbsolute(c[5]);
+ p.lineTo(a.x(), a.y());
+ a = finset.toAbsolute(c[6]);
+ p.lineTo(a.x(), a.y());
+ a = finset.toAbsolute(c[7]);
+ p.lineTo(a.x(), a.y());
+ p.closePath();
+ s[fin*6+1] = p;
+
+ // Single lines
+ for (int i=0; i<4; i++) {
+ a = finset.toAbsolute(c[i]);
+ b = finset.toAbsolute(c[i+4]);
+ s[fin*6+2+i] = new Line2D.Float((float)a.x(),(float)a.y(),(float)b.x(),(float)b.y());
+ }
+
+ // Rotate fin coordinates
+ for (int i=0; i<8; i++)
+ c[i] = rot.transform(c[i]);
+ }
+
+ */
+}
--- /dev/null
+package net.sf.openrocket.gui.rocketfigure;
+
+import java.awt.Shape;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Rectangle2D;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.Transformation;
+
+
+public class LaunchLugShapes extends RocketComponentShapes {
+
+ public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component,
+ Transformation transformation) {
+ net.sf.openrocket.rocketcomponent.LaunchLug lug = (net.sf.openrocket.rocketcomponent.LaunchLug)component;
+
+ double length = lug.getLength();
+ double radius = lug.getRadius();
+ Coordinate[] start = transformation.transform(lug.toAbsolute(new Coordinate(0,0,0)));
+
+ Shape[] s = new Shape[start.length];
+ for (int i=0; i < start.length; i++) {
+ s[i] = new Rectangle2D.Double(start[i].x*S,(start[i].y-radius)*S,
+ length*S,2*radius*S);
+ }
+ return s;
+ }
+
+
+ public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component,
+ Transformation transformation) {
+ net.sf.openrocket.rocketcomponent.LaunchLug lug = (net.sf.openrocket.rocketcomponent.LaunchLug)component;
+
+ double or = lug.getRadius();
+
+ Coordinate[] start = transformation.transform(lug.toAbsolute(new Coordinate(0,0,0)));
+
+ Shape[] s = new Shape[start.length];
+ for (int i=0; i < start.length; i++) {
+ s[i] = new Ellipse2D.Double((start[i].z-or)*S,(start[i].y-or)*S,2*or*S,2*or*S);
+ }
+ return s;
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.rocketfigure;
+
+import java.awt.Shape;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.RoundRectangle2D;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.Transformation;
+
+
+public class MassObjectShapes extends RocketComponentShapes {
+
+ public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component,
+ Transformation transformation) {
+ net.sf.openrocket.rocketcomponent.MassObject tube = (net.sf.openrocket.rocketcomponent.MassObject)component;
+
+ double length = tube.getLength();
+ double radius = tube.getRadius();
+ double arc = Math.min(length, 2*radius) * 0.7;
+ Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0)));
+
+ Shape[] s = new Shape[start.length];
+ for (int i=0; i < start.length; i++) {
+ s[i] = new RoundRectangle2D.Double(start[i].x*S,(start[i].y-radius)*S,
+ length*S,2*radius*S,arc*S,arc*S);
+ }
+ return s;
+ }
+
+
+ public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component,
+ Transformation transformation) {
+ net.sf.openrocket.rocketcomponent.MassObject tube = (net.sf.openrocket.rocketcomponent.MassObject)component;
+
+ double or = tube.getRadius();
+
+ Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0)));
+
+ Shape[] s = new Shape[start.length];
+ for (int i=0; i < start.length; i++) {
+ s[i] = new Ellipse2D.Double((start[i].z-or)*S,(start[i].y-or)*S,2*or*S,2*or*S);
+ }
+ return s;
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.gui.rocketfigure;\r
+\r
+\r
+import java.awt.Shape;\r
+import java.awt.geom.Ellipse2D;\r
+import java.awt.geom.Rectangle2D;\r
+\r
+import net.sf.openrocket.util.Coordinate;\r
+import net.sf.openrocket.util.Transformation;\r
+\r
+\r
+public class RingComponentShapes extends RocketComponentShapes {\r
+\r
+ public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component, \r
+ Transformation transformation) {\r
+ net.sf.openrocket.rocketcomponent.RingComponent tube = (net.sf.openrocket.rocketcomponent.RingComponent)component;\r
+ Shape[] s;\r
+ \r
+ double length = tube.getLength();\r
+ double or = tube.getOuterRadius();\r
+ double ir = tube.getInnerRadius();\r
+ \r
+\r
+ Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0)));\r
+\r
+ if ((or-ir >= 0.0012) && (ir > 0)) {\r
+ // Draw outer and inner\r
+ s = new Shape[start.length*2];\r
+ for (int i=0; i < start.length; i++) {\r
+ s[2*i] = new Rectangle2D.Double(start[i].x*S,(start[i].y-or)*S,\r
+ length*S,2*or*S);\r
+ s[2*i+1] = new Rectangle2D.Double(start[i].x*S,(start[i].y-ir)*S,\r
+ length*S,2*ir*S);\r
+ }\r
+ } else {\r
+ // Draw only outer\r
+ s = new Shape[start.length];\r
+ for (int i=0; i < start.length; i++) {\r
+ s[i] = new Rectangle2D.Double(start[i].x*S,(start[i].y-or)*S,\r
+ length*S,2*or*S);\r
+ }\r
+ }\r
+ return s;\r
+ }\r
+ \r
+\r
+ public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component, \r
+ Transformation transformation) {\r
+ net.sf.openrocket.rocketcomponent.RingComponent tube = (net.sf.openrocket.rocketcomponent.RingComponent)component;\r
+ Shape[] s;\r
+ \r
+ double or = tube.getOuterRadius();\r
+ double ir = tube.getInnerRadius();\r
+ \r
+\r
+ Coordinate[] start = transformation.transform(tube.toAbsolute(new Coordinate(0,0,0)));\r
+\r
+ if ((ir < or) && (ir > 0)) {\r
+ // Draw inner and outer\r
+ s = new Shape[start.length*2];\r
+ for (int i=0; i < start.length; i++) {\r
+ s[2*i] = new Ellipse2D.Double((start[i].z-or)*S, (start[i].y-or)*S,\r
+ 2*or*S, 2*or*S);\r
+ s[2*i+1] = new Ellipse2D.Double((start[i].z-ir)*S, (start[i].y-ir)*S,\r
+ 2*ir*S, 2*ir*S);\r
+ }\r
+ } else {\r
+ // Draw only outer\r
+ s = new Shape[start.length];\r
+ for (int i=0; i < start.length; i++) {\r
+ s[i] = new Ellipse2D.Double((start[i].z-or)*S,(start[i].y-or)*S,2*or*S,2*or*S);\r
+ }\r
+ }\r
+ return s;\r
+ }\r
+ \r
+}\r
--- /dev/null
+package net.sf.openrocket.gui.rocketfigure;
+
+
+import java.awt.Shape;
+
+import net.sf.openrocket.gui.scalefigure.RocketFigure;
+import net.sf.openrocket.util.Transformation;
+
+
+/**
+ * A catch-all, no-operation drawing component.
+ */
+public class RocketComponentShapes {
+
+ protected static final double S = RocketFigure.EXTRA_SCALE;
+
+ public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component,
+ Transformation t) {
+ // no-op
+ System.err.println("ERROR: RocketComponent.getShapesSide called with "+component);
+ return new Shape[0];
+ }
+
+ public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component,
+ Transformation t) {
+ // no-op
+ System.err.println("ERROR: RocketComponent.getShapesBack called with "+component);
+ return new Shape[0];
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.gui.rocketfigure;
+
+import java.awt.Shape;
+import java.awt.geom.Path2D;
+import java.util.ArrayList;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.Transformation;
+
+
+public class SymmetricComponentShapes extends RocketComponentShapes {
+ private static final int MINPOINTS = 91;
+ private static final double ACCEPTABLE_ANGLE = Math.cos(7.0*Math.PI/180.0);
+
+ // TODO: HIGH: adaptiveness sucks, remove it.
+
+ // TODO: LOW: Uses only first component of cluster (not currently clusterable)
+
+ public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component,
+ Transformation transformation) {
+ net.sf.openrocket.rocketcomponent.SymmetricComponent c = (net.sf.openrocket.rocketcomponent.SymmetricComponent)component;
+ int i;
+
+ final double delta = 0.0000001;
+ double x;
+
+ ArrayList<Coordinate> points = new ArrayList<Coordinate>();
+ x = delta;
+ points.add(new Coordinate(x,c.getRadius(x),0));
+ for (i=1; i < MINPOINTS-1; i++) {
+ x = c.getLength()*i/(MINPOINTS-1);
+ points.add(new Coordinate(x,c.getRadius(x),0));
+ //System.out.println("Starting with x="+x);
+ }
+ x = c.getLength() - delta;
+ points.add(new Coordinate(x,c.getRadius(x),0));
+
+
+ i=0;
+ while (i < points.size()-2) {
+ if (angleAcceptable(points.get(i),points.get(i+1),points.get(i+2)) ||
+ points.get(i+1).x - points.get(i).x < 0.001) { // 1mm
+ i++;
+ continue;
+ }
+
+ // Split the longer of the areas
+ int n;
+ if (points.get(i+2).x-points.get(i+1).x > points.get(i+1).x-points.get(i).x)
+ n = i+1;
+ else
+ n = i;
+
+ x = (points.get(n).x + points.get(n+1).x)/2;
+ points.add(n+1,new Coordinate(x,c.getRadius(x),0));
+ }
+
+
+ //System.out.println("Final points: "+points.size());
+
+ final int len = points.size();
+
+ for (i=0; i < len; i++) {
+ points.set(i, c.toAbsolute(points.get(i))[0]);
+ }
+
+ /* Show points:
+ Shape[] s = new Shape[len+1];
+ final double d=0.001;
+ for (i=0; i<len; i++) {
+ s[i] = new Ellipse2D.Double(points.get(i).x()-d/2,points.get(i).y()-d/2,d,d);
+ }
+ */
+
+ //System.out.println("here");
+
+ // TODO: LOW: curved path instead of linear
+ Path2D.Double path = new Path2D.Double();
+ path.moveTo(points.get(len-1).x*S, points.get(len-1).y*S);
+ for (i=len-2; i>=0; i--) {
+ path.lineTo(points.get(i).x*S, points.get(i).y*S);
+ }
+ for (i=0; i<len; i++) {
+ path.lineTo(points.get(i).x*S, -points.get(i).y*S);
+ }
+ path.lineTo(points.get(len-1).x*S, points.get(len-1).y*S);
+ path.closePath();
+
+ //s[len] = path;
+ //return s;
+ return new Shape[]{ path };
+ }
+
+ private static boolean angleAcceptable(Coordinate v1, Coordinate v2, Coordinate v3) {
+ return (cosAngle(v1,v2,v3) > ACCEPTABLE_ANGLE);
+ }
+ /*
+ * cosAngle = v1.v2 / |v1|*|v2| = v1.v2 / sqrt(v1.v1*v2.v2)
+ */
+ private static double cosAngle(Coordinate v1, Coordinate v2, Coordinate v3) {
+ double cos;
+ double len;
+ cos = Coordinate.dot(v1.sub(v2), v2.sub(v3));
+ len = Math.sqrt(v1.sub(v2).length2() * v2.sub(v3).length2());
+ return cos/len;
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.rocketfigure;
+
+import java.awt.Shape;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.Path2D;
+import java.awt.geom.Rectangle2D;
+
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.Transformation;
+
+
+public class TransitionShapes extends RocketComponentShapes {
+
+ // TODO: LOW: Uses only first component of cluster (not currently clusterable).
+
+ public static Shape[] getShapesSide(net.sf.openrocket.rocketcomponent.RocketComponent component,
+ Transformation transformation) {
+ net.sf.openrocket.rocketcomponent.Transition transition = (net.sf.openrocket.rocketcomponent.Transition)component;
+
+ Shape[] mainShapes;
+
+ // Simpler shape for conical transition, others use the method from SymmetricComponent
+ if (transition.getType() == Transition.Shape.CONICAL) {
+ double length = transition.getLength();
+ double r1 = transition.getForeRadius();
+ double r2 = transition.getAftRadius();
+ Coordinate start = transformation.transform(transition.
+ toAbsolute(Coordinate.NUL)[0]);
+
+ Path2D.Float path = new Path2D.Float();
+ path.moveTo(start.x*S, r1*S);
+ path.lineTo((start.x+length)*S, r2*S);
+ path.lineTo((start.x+length)*S, -r2*S);
+ path.lineTo(start.x*S, -r1*S);
+ path.closePath();
+
+ mainShapes = new Shape[] { path };
+ } else {
+ mainShapes = SymmetricComponentShapes.getShapesSide(component, transformation);
+ }
+
+ Rectangle2D.Double shoulder1=null, shoulder2=null;
+ int arrayLength = mainShapes.length;
+
+ if (transition.getForeShoulderLength() > 0.0005) {
+ Coordinate start = transformation.transform(transition.
+ toAbsolute(Coordinate.NUL)[0]);
+ double r = transition.getForeShoulderRadius();
+ double l = transition.getForeShoulderLength();
+ shoulder1 = new Rectangle2D.Double((start.x-l)*S, -r*S, l*S, 2*r*S);
+ arrayLength++;
+ }
+ if (transition.getAftShoulderLength() > 0.0005) {
+ Coordinate start = transformation.transform(transition.
+ toAbsolute(new Coordinate(transition.getLength()))[0]);
+ double r = transition.getAftShoulderRadius();
+ double l = transition.getAftShoulderLength();
+ shoulder2 = new Rectangle2D.Double(start.x*S, -r*S, l*S, 2*r*S);
+ arrayLength++;
+ }
+ if (shoulder1==null && shoulder2==null)
+ return mainShapes;
+
+ Shape[] shapes = new Shape[arrayLength];
+ int i;
+
+ for (i=0; i < mainShapes.length; i++) {
+ shapes[i] = mainShapes[i];
+ }
+ if (shoulder1 != null) {
+ shapes[i] = shoulder1;
+ i++;
+ }
+ if (shoulder2 != null) {
+ shapes[i] = shoulder2;
+ }
+ return shapes;
+ }
+
+
+ public static Shape[] getShapesBack(net.sf.openrocket.rocketcomponent.RocketComponent component,
+ Transformation transformation) {
+ net.sf.openrocket.rocketcomponent.Transition transition = (net.sf.openrocket.rocketcomponent.Transition)component;
+
+ double r1 = transition.getForeRadius();
+ double r2 = transition.getAftRadius();
+
+ Shape[] s = new Shape[2];
+ s[0] = new Ellipse2D.Double(-r1*S,-r1*S,2*r1*S,2*r1*S);
+ s[1] = new Ellipse2D.Double(-r2*S,-r2*S,2*r2*S,2*r2*S);
+ return s;
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.gui.scalefigure;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.util.LinkedList;
+import java.util.List;
+
+import javax.swing.JPanel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.util.Prefs;
+
+
+public abstract class AbstractScaleFigure extends JPanel implements ScaleFigure {
+
+ // Number of pixels to leave at edges when fitting figure
+ public static final int BORDER_PIXELS_WIDTH=30;
+ public static final int BORDER_PIXELS_HEIGHT=20;
+
+
+ protected final double dpi;
+
+ protected double scale = 1.0;
+ protected double scaling = 1.0;
+
+ protected final List<ChangeListener> listeners = new LinkedList<ChangeListener>();
+
+
+ public AbstractScaleFigure() {
+ this.dpi = Prefs.getDPI();
+ this.scaling = 1.0;
+ this.scale = dpi/0.0254*scaling;
+
+ setBackground(Color.WHITE);
+ setOpaque(true);
+ }
+
+
+
+ public abstract void updateFigure();
+ public abstract double getFigureWidth();
+ public abstract double getFigureHeight();
+
+
+ @Override
+ public double getScaling() {
+ return scaling;
+ }
+
+ @Override
+ public double getAbsoluteScale() {
+ return scale;
+ }
+
+ @Override
+ public void setScaling(double scaling) {
+ if (Double.isInfinite(scaling) || Double.isNaN(scaling))
+ scaling = 1.0;
+ if (scaling < 0.001)
+ scaling = 0.001;
+ if (scaling > 1000)
+ scaling = 1000;
+ if (Math.abs(this.scaling - scaling) < 0.01)
+ return;
+ this.scaling = scaling;
+ this.scale = dpi/0.0254*scaling;
+ updateFigure();
+ }
+
+ @Override
+ public void setScaling(Dimension bounds) {
+ double zh = 1, zv = 1;
+ int w = bounds.width - 2*BORDER_PIXELS_WIDTH -20;
+ int h = bounds.height - 2*BORDER_PIXELS_HEIGHT -20;
+
+ if (w < 10)
+ w = 10;
+ if (h < 10)
+ h = 10;
+
+ zh = ((double)w) / getFigureWidth();
+ zv = ((double)h) / getFigureHeight();
+
+ double s = Math.min(zh, zv)/dpi*0.0254 - 0.001;
+
+ setScaling(s);
+ }
+
+
+
+ @Override
+ public void addChangeListener(ChangeListener listener) {
+ listeners.add(0,listener);
+ }
+
+ @Override
+ public void removeChangeListener(ChangeListener listener) {
+ listeners.remove(listener);
+ }
+
+ private ChangeEvent changeEvent = null;
+ protected void fireChangeEvent() {
+ ChangeListener[] list = listeners.toArray(new ChangeListener[0]);
+ for (ChangeListener l: list) {
+ if (changeEvent == null)
+ changeEvent = new ChangeEvent(this);
+ l.stateChanged(changeEvent);
+ }
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.scalefigure;
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Line2D;
+import java.awt.geom.NoninvertibleTransformException;
+import java.awt.geom.Path2D;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.unit.Tick;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+// TODO: MEDIUM: the figure jumps and bugs when using automatic fitting
+
+public class FinPointFigure extends AbstractScaleFigure {
+
+ private static final int BOX_SIZE = 4;
+
+ private final FreeformFinSet finset;
+ private int modID = -1;
+
+ private double minX, maxX, maxY;
+ private double figureWidth = 0;
+ private double figureHeight = 0;
+ private double translateX = 0;
+ private double translateY = 0;
+
+ private AffineTransform transform;
+ private Rectangle2D.Double[] handles = null;
+
+
+ public FinPointFigure(FreeformFinSet finset) {
+ this.finset = finset;
+ }
+
+
+ @Override
+ public void paintComponent(Graphics g) {
+ super.paintComponent(g);
+ Graphics2D g2 = (Graphics2D) g;
+
+ if (modID != finset.getRocket().getAerodynamicModID()) {
+ modID = finset.getRocket().getAerodynamicModID();
+ calculateDimensions();
+ }
+
+
+ double tx, ty;
+ // Calculate translation for figure centering
+ if (figureWidth*scale + 2*BORDER_PIXELS_WIDTH < getWidth()) {
+
+ // Figure fits in the viewport
+ tx = (getWidth()-figureWidth*scale)/2 - minX*scale;
+
+ } else {
+
+ // Figure does not fit in viewport
+ tx = BORDER_PIXELS_WIDTH - minX*scale;
+
+ }
+
+
+ if (figureHeight*scale + 2*BORDER_PIXELS_HEIGHT < getHeight()) {
+ ty = getHeight() - BORDER_PIXELS_HEIGHT;
+ } else {
+ ty = BORDER_PIXELS_HEIGHT + figureHeight*scale;
+ }
+
+ if (Math.abs(translateX - tx)>1 || Math.abs(translateY - ty)>1) {
+ // Origin has changed, fire event
+ translateX = tx;
+ translateY = ty;
+ fireChangeEvent();
+ }
+
+
+ if (Math.abs(translateX - tx)>1 || Math.abs(translateY - ty)>1) {
+ // Origin has changed, fire event
+ translateX = tx;
+ translateY = ty;
+ fireChangeEvent();
+ }
+
+
+ // Calculate and store the transformation used
+ transform = new AffineTransform();
+ transform.translate(translateX, translateY);
+ transform.scale(scale/EXTRA_SCALE, -scale/EXTRA_SCALE);
+
+ // TODO: HIGH: border Y-scale upwards
+
+ g2.transform(transform);
+
+ // Set rendering hints appropriately
+ g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
+ RenderingHints.VALUE_STROKE_NORMALIZE);
+ g2.setRenderingHint(RenderingHints.KEY_RENDERING,
+ RenderingHints.VALUE_RENDER_QUALITY);
+ g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON);
+
+
+
+ Rectangle visible = g2.getClipBounds();
+ double x0 = ((double)visible.x-3)/EXTRA_SCALE;
+ double x1 = ((double)visible.x+visible.width+4)/EXTRA_SCALE;
+ double y0 = ((double)visible.y-3)/EXTRA_SCALE;
+ double y1 = ((double)visible.y+visible.height+4)/EXTRA_SCALE;
+
+
+ // Background grid
+
+ g2.setStroke(new BasicStroke((float)(1.0*EXTRA_SCALE/scale),
+ BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL));
+ g2.setColor(new Color(0,0,255,30));
+
+ Unit unit;
+ if (this.getParent() != null &&
+ this.getParent().getParent() instanceof ScaleScrollPane) {
+ unit = ((ScaleScrollPane)this.getParent().getParent()).getCurrentUnit();
+ } else {
+ unit = UnitGroup.UNITS_LENGTH.getDefaultUnit();
+ }
+
+ // vertical
+ Tick[] ticks = unit.getTicks(x0, x1,
+ ScaleScrollPane.MINOR_TICKS/scale,
+ ScaleScrollPane.MAJOR_TICKS/scale);
+ Line2D.Double line = new Line2D.Double();
+ for (Tick t: ticks) {
+ if (t.major) {
+ line.setLine(t.value*EXTRA_SCALE, y0*EXTRA_SCALE,
+ t.value*EXTRA_SCALE, y1*EXTRA_SCALE);
+ g2.draw(line);
+ }
+ }
+
+ // horizontal
+ ticks = unit.getTicks(y0, y1,
+ ScaleScrollPane.MINOR_TICKS/scale,
+ ScaleScrollPane.MAJOR_TICKS/scale);
+ for (Tick t: ticks) {
+ if (t.major) {
+ line.setLine(x0*EXTRA_SCALE, t.value*EXTRA_SCALE,
+ x1*EXTRA_SCALE, t.value*EXTRA_SCALE);
+ g2.draw(line);
+ }
+ }
+
+
+
+
+
+ // Base rocket line
+ g2.setStroke(new BasicStroke((float)(3.0*EXTRA_SCALE/scale),
+ BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL));
+ g2.setColor(Color.GRAY);
+
+ g2.drawLine((int)(x0*EXTRA_SCALE), 0, (int)(x1*EXTRA_SCALE), 0);
+
+
+ // Fin shape
+ Coordinate[] points = finset.getFinPoints();
+ Path2D.Double shape = new Path2D.Double();
+ shape.moveTo(0, 0);
+ for (int i=1; i < points.length; i++) {
+ shape.lineTo(points[i].x*EXTRA_SCALE, points[i].y*EXTRA_SCALE);
+ }
+
+ g2.setStroke(new BasicStroke((float)(1.0*EXTRA_SCALE/scale),
+ BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL));
+ g2.setColor(Color.BLACK);
+ g2.draw(shape);
+
+
+ // Fin point boxes
+ g2.setColor(new Color(150,0,0));
+ double s = BOX_SIZE*EXTRA_SCALE/scale;
+ handles = new Rectangle2D.Double[points.length];
+ for (int i=0; i < points.length; i++) {
+ Coordinate c = points[i];
+ handles[i] = new Rectangle2D.Double(c.x*EXTRA_SCALE-s, c.y*EXTRA_SCALE-s, 2*s, 2*s);
+ g2.draw(handles[i]);
+ }
+
+ }
+
+
+
+ public int getIndexByPoint(double x, double y) {
+ if (handles == null)
+ return -1;
+
+ // Calculate point in shapes' coordinates
+ Point2D.Double p = new Point2D.Double(x,y);
+ try {
+ transform.inverseTransform(p,p);
+ } catch (NoninvertibleTransformException e) {
+ return -1;
+ }
+
+ for (int i=0; i < handles.length; i++) {
+ if (handles[i].contains(p))
+ return i;
+ }
+ return -1;
+ }
+
+
+ public int getSegmentByPoint(double x, double y) {
+ if (handles == null)
+ return -1;
+
+ // Calculate point in shapes' coordinates
+ Point2D.Double p = new Point2D.Double(x,y);
+ try {
+ transform.inverseTransform(p,p);
+ } catch (NoninvertibleTransformException e) {
+ return -1;
+ }
+
+ double x0 = p.x / EXTRA_SCALE;
+ double y0 = p.y / EXTRA_SCALE;
+ double delta = BOX_SIZE / scale;
+
+ System.out.println("Point: "+x0+","+y0);
+ System.out.println("delta: "+(BOX_SIZE/scale));
+
+ Coordinate[] points = finset.getFinPoints();
+ for (int i=1; i < points.length; i++) {
+ double x1 = points[i-1].x;
+ double y1 = points[i-1].y;
+ double x2 = points[i].x;
+ double y2 = points[i].y;
+
+// System.out.println("point1:"+x1+","+y1+" point2:"+x2+","+y2);
+
+ double u = Math.abs((x2-x1)*(y1-y0) - (x1-x0)*(y2-y1)) /
+ MathUtil.hypot(x2-x1, y2-y1);
+ System.out.println("Distance of segment "+i+" is "+u);
+ if (u < delta)
+ return i;
+ }
+
+ return -1;
+ }
+
+
+ public Point2D.Double convertPoint(double x, double y) {
+ Point2D.Double p = new Point2D.Double(x,y);
+ try {
+ transform.inverseTransform(p,p);
+ } catch (NoninvertibleTransformException e) {
+ assert(false): "Should not occur";
+ return new Point2D.Double(0,0);
+ }
+
+ p.setLocation(p.x / EXTRA_SCALE, p.y / EXTRA_SCALE);
+ return p;
+ }
+
+
+
+ @Override
+ public Dimension getOrigin() {
+ if (modID != finset.getRocket().getAerodynamicModID()) {
+ modID = finset.getRocket().getAerodynamicModID();
+ calculateDimensions();
+ }
+ return new Dimension((int)translateX, (int)translateY);
+ }
+
+ @Override
+ public double getFigureWidth() {
+ if (modID != finset.getRocket().getAerodynamicModID()) {
+ modID = finset.getRocket().getAerodynamicModID();
+ calculateDimensions();
+ }
+ return figureWidth;
+ }
+
+ @Override
+ public double getFigureHeight() {
+ if (modID != finset.getRocket().getAerodynamicModID()) {
+ modID = finset.getRocket().getAerodynamicModID();
+ calculateDimensions();
+ }
+ return figureHeight;
+ }
+
+
+ private void calculateDimensions() {
+ minX = 0;
+ maxX = 0;
+ maxY = 0;
+
+ for (Coordinate c: finset.getFinPoints()) {
+ if (c.x < minX)
+ minX = c.x;
+ if (c.x > maxX)
+ maxX = c.x;
+ if (c.y > maxY)
+ maxY = c.y;
+ }
+
+ if (maxX < 0.01)
+ maxX = 0.01;
+
+ figureWidth = maxX - minX;
+ figureHeight = maxY;
+
+
+ Dimension d = new Dimension((int)(figureWidth*scale+2*BORDER_PIXELS_WIDTH),
+ (int)(figureHeight*scale+2*BORDER_PIXELS_HEIGHT));
+
+ if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) {
+ setPreferredSize(d);
+ setMinimumSize(d);
+ revalidate();
+ }
+ }
+
+
+
+ @Override
+ public void updateFigure() {
+ repaint();
+ }
+
+
+
+}
--- /dev/null
+package net.sf.openrocket.gui.scalefigure;
+
+
+import java.awt.BasicStroke;
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.Shape;
+import java.awt.geom.AffineTransform;
+import java.awt.geom.Ellipse2D;
+import java.awt.geom.NoninvertibleTransformException;
+import java.awt.geom.Point2D;
+import java.awt.geom.Rectangle2D;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+
+import net.sf.openrocket.gui.figureelements.FigureElement;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.LineStyle;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Prefs;
+import net.sf.openrocket.util.Reflection;
+import net.sf.openrocket.util.Transformation;
+
+/**
+ * A <code>ScaleFigure</code> that draws a complete rocket. Extra information can
+ * be added to the figure by the methods {@link #addRelativeExtra(FigureElement)},
+ * {@link #clearRelativeExtra()}.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class RocketFigure extends AbstractScaleFigure {
+ private static final long serialVersionUID = 1L;
+
+ private static final String ROCKET_FIGURE_PACKAGE = "net.sf.openrocket.gui.rocketfigure";
+ private static final String ROCKET_FIGURE_SUFFIX = "Shapes";
+
+ public static final int TYPE_SIDE = 1;
+ public static final int TYPE_BACK = 2;
+
+ // Width for drawing normal and selected components
+ public static final double NORMAL_WIDTH = 1.0;
+ public static final double SELECTED_WIDTH = 2.0;
+
+
+ private final Configuration configuration;
+ private RocketComponent[] selection = new RocketComponent[0];
+
+ private int type = TYPE_SIDE;
+
+ private double rotation;
+ private Transformation transformation;
+
+ private double translateX, translateY;
+
+
+
+ /*
+ * figureComponents contains the corresponding RocketComponents of the figureShapes
+ */
+ private final ArrayList<Shape> figureShapes = new ArrayList<Shape>();
+ private final ArrayList<RocketComponent> figureComponents =
+ new ArrayList<RocketComponent>();
+
+ private double minX=0, maxX=0, maxR=0;
+ // Figure width and height in SI-units and pixels
+ private double figureWidth=0, figureHeight=0;
+ private int figureWidthPx=0, figureHeightPx=0;
+
+ private AffineTransform g2transformation = null;
+
+ private final ArrayList<FigureElement> relativeExtra = new ArrayList<FigureElement>();
+ private final ArrayList<FigureElement> absoluteExtra = new ArrayList<FigureElement>();
+
+
+ /**
+ * Creates a new rocket figure.
+ */
+ public RocketFigure(Configuration configuration) {
+ super();
+
+ this.configuration = configuration;
+
+ this.rotation = 0.0;
+ this.transformation = Transformation.rotate_x(0.0);
+
+ calculateSize();
+ updateFigure();
+ }
+
+
+
+ public Dimension getOrigin() {
+ return new Dimension((int)translateX, (int)translateY);
+ }
+
+ @Override
+ public double getFigureHeight() {
+ return figureHeight;
+ }
+
+ @Override
+ public double getFigureWidth() {
+ return figureWidth;
+ }
+
+
+ public RocketComponent[] getSelection() {
+ return selection;
+ }
+
+ public void setSelection(RocketComponent[] selection) {
+ if (selection == null) {
+ selection = new RocketComponent[0];
+ } else {
+ this.selection = selection;
+ }
+ updateFigure();
+ }
+
+
+ public double getRotation() {
+ return rotation;
+ }
+
+ public Transformation getRotateTransformation() {
+ return transformation;
+ }
+
+ public void setRotation(double rot) {
+ if (MathUtil.equals(rotation, rot))
+ return;
+ this.rotation = rot;
+ this.transformation = Transformation.rotate_x(rotation);
+ updateFigure();
+ }
+
+
+ public int getType() {
+ return type;
+ }
+
+ public void setType(int type) {
+ if (type != TYPE_BACK && type != TYPE_SIDE) {
+ throw new IllegalArgumentException("Illegal type: "+type);
+ }
+ if (this.type == type)
+ return;
+ this.type = type;
+ updateFigure();
+ }
+
+
+
+
+
+
+ /**
+ * Updates the figure shapes and figure size.
+ */
+ @Override
+ public void updateFigure() {
+ figureShapes.clear();
+ figureComponents.clear();
+
+ calculateSize();
+
+ // Get shapes for all active components
+ for (RocketComponent c: configuration) {
+ Shape[] s = getShapes(c);
+ for (int i=0; i < s.length; i++) {
+ figureShapes.add(s[i]);
+ figureComponents.add(c);
+ }
+ }
+
+ repaint();
+ fireChangeEvent();
+ }
+
+
+ public void addRelativeExtra(FigureElement p) {
+ relativeExtra.add(p);
+ }
+
+ public void removeRelativeExtra(FigureElement p) {
+ relativeExtra.remove(p);
+ }
+
+ public void clearRelativeExtra() {
+ relativeExtra.clear();
+ }
+
+
+ public void addAbsoluteExtra(FigureElement p) {
+ absoluteExtra.add(p);
+ }
+
+ public void removeAbsoluteExtra(FigureElement p) {
+ absoluteExtra.remove(p);
+ }
+
+ public void clearAbsoluteExtra() {
+ absoluteExtra.clear();
+ }
+
+
+ /**
+ * Paints the rocket on to the Graphics element.
+ * <p>
+ * Warning: If paintComponent is used outside the normal Swing usage, some Swing
+ * dependent parameters may be left wrong (mainly transformation). If it is used,
+ * the RocketFigure should be repainted immediately afterwards.
+ */
+ @Override
+ public void paintComponent(Graphics g) {
+ super.paintComponent(g);
+ Graphics2D g2 = (Graphics2D)g;
+
+
+ AffineTransform baseTransform = g2.getTransform();
+
+ // Update figure shapes if necessary
+ if (figureShapes == null)
+ updateFigure();
+
+
+ double tx, ty;
+ // Calculate translation for figure centering
+ if (figureWidthPx + 2*BORDER_PIXELS_WIDTH < getWidth()) {
+
+ // Figure fits in the viewport
+ if (type == TYPE_BACK)
+ tx = getWidth()/2;
+ else
+ tx = (getWidth()-figureWidthPx)/2 - minX*scale;
+
+ } else {
+
+ // Figure does not fit in viewport
+ if (type == TYPE_BACK)
+ tx = BORDER_PIXELS_WIDTH + figureWidthPx/2;
+ else
+ tx = BORDER_PIXELS_WIDTH - minX*scale;
+
+ }
+
+ if (figureHeightPx + 2*BORDER_PIXELS_HEIGHT < getHeight()) {
+ ty = getHeight()/2;
+ } else {
+ ty = BORDER_PIXELS_HEIGHT + figureHeightPx/2;
+ }
+
+ if (Math.abs(translateX - tx)>1 || Math.abs(translateY - ty)>1) {
+ // Origin has changed, fire event
+ translateX = tx;
+ translateY = ty;
+ fireChangeEvent();
+ }
+
+
+ // Calculate and store the transformation used
+ // (inverse is used in detecting clicks on objects)
+ g2transformation = new AffineTransform();
+ g2transformation.translate(translateX, translateY);
+ // Mirror position Y-axis upwards
+ g2transformation.scale(scale/EXTRA_SCALE, -scale/EXTRA_SCALE);
+
+ g2.transform(g2transformation);
+
+ // Set rendering hints appropriately
+ g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
+ RenderingHints.VALUE_STROKE_NORMALIZE);
+ g2.setRenderingHint(RenderingHints.KEY_RENDERING,
+ RenderingHints.VALUE_RENDER_QUALITY);
+ g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
+ RenderingHints.VALUE_ANTIALIAS_ON);
+
+
+ // Draw all shapes
+
+ for (int i=0; i < figureShapes.size(); i++) {
+ RocketComponent c = figureComponents.get(i);
+ Shape s = figureShapes.get(i);
+ boolean selected = false;
+
+ // Check if component is in the selection
+ for (int j=0; j < selection.length; j++) {
+ if (c == selection[j]) {
+ selected = true;
+ break;
+ }
+ }
+
+ // Set component color and line style
+ Color color = c.getColor();
+ if (color == null) {
+ color = Prefs.getDefaultColor(c.getClass());
+ }
+ g2.setColor(color);
+
+ LineStyle style = c.getLineStyle();
+ if (style == null)
+ style = Prefs.getDefaultLineStyle(c.getClass());
+
+ float[] dashes = style.getDashes();
+ for (int j=0; j<dashes.length; j++) {
+ dashes[j] *= EXTRA_SCALE / scale;
+ }
+
+ if (selected) {
+ g2.setStroke(new BasicStroke((float)(SELECTED_WIDTH*EXTRA_SCALE/scale),
+ BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL, 0, dashes, 0));
+ g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
+ RenderingHints.VALUE_STROKE_PURE);
+ } else {
+ g2.setStroke(new BasicStroke((float)(NORMAL_WIDTH*EXTRA_SCALE/scale),
+ BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL, 0, dashes, 0));
+ g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
+ RenderingHints.VALUE_STROKE_NORMALIZE);
+ }
+ g2.draw(s);
+
+ }
+
+ g2.setStroke(new BasicStroke((float)(NORMAL_WIDTH*EXTRA_SCALE/scale),
+ BasicStroke.CAP_BUTT,BasicStroke.JOIN_BEVEL));
+ g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
+ RenderingHints.VALUE_STROKE_NORMALIZE);
+
+
+ // Draw motors
+ String motorID = configuration.getMotorConfigurationID();
+ Color fillColor = Prefs.getMotorFillColor();
+ Color borderColor = Prefs.getMotorBorderColor();
+ Iterator<MotorMount> iterator = configuration.motorIterator();
+ while (iterator.hasNext()) {
+ MotorMount mount = iterator.next();
+ Motor motor = mount.getMotor(motorID);
+ double length = motor.getLength();
+ double radius = motor.getDiameter() / 2;
+
+ Coordinate[] position = ((RocketComponent)mount).toAbsolute(
+ new Coordinate(((RocketComponent)mount).getLength() +
+ mount.getMotorOverhang() - length));
+
+ for (int i=0; i < position.length; i++) {
+ position[i] = transformation.transform(position[i]);
+ }
+
+ for (Coordinate coord: position) {
+ Shape s;
+ if (type == TYPE_SIDE) {
+ s = new Rectangle2D.Double(EXTRA_SCALE*coord.x,
+ EXTRA_SCALE*(coord.y - radius), EXTRA_SCALE*length,
+ EXTRA_SCALE*2*radius);
+ } else {
+ s = new Ellipse2D.Double(EXTRA_SCALE*(coord.z-radius),
+ EXTRA_SCALE*(coord.y-radius), EXTRA_SCALE*2*radius,
+ EXTRA_SCALE*2*radius);
+ }
+ g2.setColor(fillColor);
+ g2.fill(s);
+ g2.setColor(borderColor);
+ g2.draw(s);
+ }
+ }
+
+
+
+ // Draw relative extras
+ for (FigureElement e: relativeExtra) {
+ e.paint(g2, scale/EXTRA_SCALE);
+ }
+
+ // Draw absolute extras
+ g2.setTransform(baseTransform);
+ Rectangle rect = this.getVisibleRect();
+
+ for (FigureElement e: absoluteExtra) {
+ e.paint(g2, 1.0, rect);
+ }
+
+ }
+
+
+ public RocketComponent[] getComponentsByPoint(double x, double y) {
+ // Calculate point in shapes' coordinates
+ Point2D.Double p = new Point2D.Double(x,y);
+ try {
+ g2transformation.inverseTransform(p,p);
+ } catch (NoninvertibleTransformException e) {
+ return new RocketComponent[0];
+ }
+
+ LinkedHashSet<RocketComponent> l = new LinkedHashSet<RocketComponent>();
+
+ for (int i=0; i<figureShapes.size(); i++) {
+ if (figureShapes.get(i).contains(p))
+ l.add(figureComponents.get(i));
+ }
+ return l.toArray(new RocketComponent[0]);
+ }
+
+
+
+ /**
+ * Gets the shapes required to draw the component.
+ *
+ * @param component
+ * @param params
+ * @return
+ */
+ private Shape[] getShapes(RocketComponent component) {
+ Reflection.Method m;
+
+ // Find the appropriate method
+ switch (type) {
+ case TYPE_SIDE:
+ m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesSide",
+ RocketComponent.class, Transformation.class);
+ break;
+
+ case TYPE_BACK:
+ m = Reflection.findMethod(ROCKET_FIGURE_PACKAGE, component, ROCKET_FIGURE_SUFFIX, "getShapesBack",
+ RocketComponent.class, Transformation.class);
+ break;
+
+ default:
+ throw new RuntimeException("Unknown figure type = "+type);
+ }
+
+ if (m == null) {
+ System.err.println("ERROR: Rocket figure paint method not found for " + component);
+ return new Shape[0];
+ }
+
+ return (Shape[])m.invokeStatic(component,transformation);
+ }
+
+
+
+ /**
+ * Gets the bounds of the figure, i.e. the maximum extents in the selected dimensions.
+ * The bounds are stored in the variables minX, maxX and maxR.
+ */
+ private void calculateFigureBounds() {
+ Collection<Coordinate> bounds = configuration.getBounds();
+
+ if (bounds.isEmpty()) {
+ minX = 0;
+ maxX = 0;
+ maxR = 0;
+ return;
+ }
+
+ minX = Double.MAX_VALUE;
+ maxX = Double.MIN_VALUE;
+ maxR = 0;
+ for (Coordinate c: bounds) {
+ double x = c.x, r = MathUtil.hypot(c.y, c.z);
+ if (x < minX)
+ minX = x;
+ if (x > maxX)
+ maxX = x;
+ if (r > maxR)
+ maxR = r;
+ }
+ }
+
+
+ public double getBestZoom(Rectangle2D bounds) {
+ double zh=1, zv=1;
+ if (bounds.getWidth() > 0.0001)
+ zh = (getWidth()-2*BORDER_PIXELS_WIDTH)/bounds.getWidth();
+ if (bounds.getHeight() > 0.0001)
+ zv = (getHeight()-2*BORDER_PIXELS_HEIGHT)/bounds.getHeight();
+ return Math.min(zh, zv);
+ }
+
+
+ /**
+ * Calculates the necessary size of the figure and set the PreferredSize
+ * property accordingly.
+ */
+ private void calculateSize() {
+ calculateFigureBounds();
+
+ switch (type) {
+ case TYPE_SIDE:
+ figureWidth = maxX-minX;
+ figureHeight = 2*maxR;
+ break;
+
+ case TYPE_BACK:
+ figureWidth = 2*maxR;
+ figureHeight = 2*maxR;
+ break;
+
+ default:
+ assert(false): "Should not occur, type="+type;
+ figureWidth = 0;
+ figureHeight = 0;
+ }
+
+ figureWidthPx = (int)(figureWidth * scale);
+ figureHeightPx = (int)(figureHeight * scale);
+
+ Dimension d = new Dimension(figureWidthPx+2*BORDER_PIXELS_WIDTH,
+ figureHeightPx+2*BORDER_PIXELS_HEIGHT);
+
+ if (!d.equals(getPreferredSize()) || !d.equals(getMinimumSize())) {
+ setPreferredSize(d);
+ setMinimumSize(d);
+ revalidate();
+ }
+ }
+
+ public Rectangle2D getDimensions() {
+ switch (type) {
+ case TYPE_SIDE:
+ return new Rectangle2D.Double(minX,-maxR,maxX-minX,2*maxR);
+
+ case TYPE_BACK:
+ return new Rectangle2D.Double(-maxR,-maxR,2*maxR,2*maxR);
+
+ default:
+ throw new RuntimeException("Illegal figure type = "+type);
+ }
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.scalefigure;
+
+
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Point;
+import java.awt.event.ActionEvent;
+import java.awt.event.InputEvent;
+import java.awt.event.MouseEvent;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.JComboBox;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+import javax.swing.JSlider;
+import javax.swing.JToggleButton;
+import javax.swing.JViewport;
+import javax.swing.SwingUtilities;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.TreeSelectionEvent;
+import javax.swing.event.TreeSelectionListener;
+import javax.swing.tree.TreePath;
+import javax.swing.tree.TreeSelectionModel;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
+import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.gui.BasicSlider;
+import net.sf.openrocket.gui.StageSelector;
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.gui.adaptors.MotorConfigurationModel;
+import net.sf.openrocket.gui.configdialog.ComponentConfigDialog;
+import net.sf.openrocket.gui.figureelements.CGCaret;
+import net.sf.openrocket.gui.figureelements.CPCaret;
+import net.sf.openrocket.gui.figureelements.Caret;
+import net.sf.openrocket.gui.figureelements.RocketInfo;
+import net.sf.openrocket.gui.main.ComponentTreeModel;
+import net.sf.openrocket.gui.main.SimulationWorker;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.SymmetricComponent;
+import net.sf.openrocket.simulation.FlightData;
+import net.sf.openrocket.simulation.SimulationListener;
+import net.sf.openrocket.simulation.listeners.ApogeeEndListener;
+import net.sf.openrocket.simulation.listeners.InterruptListener;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.ChangeSource;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Prefs;
+
+/**
+ * A JPanel that contains a RocketFigure and buttons to manipulate the figure.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class RocketPanel extends JPanel implements TreeSelectionListener, ChangeSource {
+
+ private final RocketFigure figure;
+ private final ScaleScrollPane scrollPane;
+
+ private JLabel infoMessage;
+
+ private TreeSelectionModel selectionModel = null;
+
+
+ /* Calculation of CP and CG */
+ private AerodynamicCalculator calculator;
+
+
+ private final OpenRocketDocument document;
+ private final Configuration configuration;
+
+ private Caret extraCP = null;
+ private Caret extraCG = null;
+ private RocketInfo extraText = null;
+
+
+ private double cpAOA = Double.NaN;
+ private double cpTheta = Double.NaN;
+ private double cpMach = Double.NaN;
+ private double cpRoll = Double.NaN;
+
+ // The functional ID of the rocket that was simulated
+ private int flightDataFunctionalID = -1;
+ private String flightDataMotorID = null;
+
+
+ private SimulationWorker backgroundSimulationWorker = null;
+
+
+ private List<ChangeListener> listeners = new ArrayList<ChangeListener>();
+
+
+ /**
+ * The executor service used for running the background simulations.
+ * This uses a fixed-sized thread pool for all background simulations
+ * with all threads in daemon mode and with minimum priority.
+ */
+ private static final Executor backgroundSimulationExecutor;
+ static {
+ backgroundSimulationExecutor = Executors.newFixedThreadPool(Prefs.getMaxThreadCount(),
+ new ThreadFactory() {
+ private ThreadFactory factory = Executors.defaultThreadFactory();
+ @Override
+ public Thread newThread(Runnable r) {
+ Thread t = factory.newThread(r);
+ t.setDaemon(true);
+ t.setPriority(Thread.MIN_PRIORITY);
+ return t;
+ }
+ });
+ }
+
+
+ public RocketPanel(OpenRocketDocument document) {
+
+ this.document = document;
+ configuration = document.getDefaultConfiguration();
+
+ // TODO: FUTURE: calculator selection
+ calculator = new BarrowmanCalculator(configuration);
+
+ // Create figure and custom scroll pane
+ figure = new RocketFigure(configuration);
+
+ scrollPane = new ScaleScrollPane(figure) {
+ @Override
+ public void mouseClicked(MouseEvent event) {
+ handleMouseClick(event);
+ }
+ };
+ scrollPane.getViewport().setScrollMode(JViewport.SIMPLE_SCROLL_MODE);
+ scrollPane.setFitting(true);
+
+ createPanel();
+
+ configuration.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ System.out.println("Configuration changed, calling updateFigure");
+ updateExtras();
+ figure.updateFigure();
+ }
+ });
+ }
+
+
+ /**
+ * Creates the layout and components of the panel.
+ */
+ private void createPanel() {
+ setLayout(new MigLayout("","[shrink][grow]","[shrink][shrink][grow][shrink]"));
+
+ setPreferredSize(new Dimension(800,300));
+
+
+ //// Create toolbar
+
+ // Side/back buttons
+ FigureTypeAction action = new FigureTypeAction(RocketFigure.TYPE_SIDE);
+ action.putValue(Action.NAME, "Side view");
+ action.putValue(Action.SHORT_DESCRIPTION, "Side view");
+ JToggleButton toggle = new JToggleButton(action);
+ add(toggle,"spanx, split");
+
+ action = new FigureTypeAction(RocketFigure.TYPE_BACK);
+ action.putValue(Action.NAME, "Back view");
+ action.putValue(Action.SHORT_DESCRIPTION, "Rear view");
+ toggle = new JToggleButton(action);
+ add(toggle,"gap rel");
+
+
+ // Zoom level selector
+ ScaleSelector scaleSelector = new ScaleSelector(scrollPane);
+ add(scaleSelector);
+
+
+
+ // Stage selector
+ StageSelector stageSelector = new StageSelector(configuration);
+ add(stageSelector,"");
+
+
+
+ // Motor configuration selector
+
+ JLabel label = new JLabel("Motor configuration:");
+ label.setHorizontalAlignment(JLabel.RIGHT);
+ add(label,"growx, right");
+ add(new JComboBox(new MotorConfigurationModel(configuration)),"wrap");
+
+
+
+
+
+ // Create slider and scroll pane
+
+ DoubleModel theta = new DoubleModel(figure,"Rotation",
+ UnitGroup.UNITS_ANGLE,0,2*Math.PI);
+ UnitSelector us = new UnitSelector(theta,true);
+ us.setHorizontalAlignment(JLabel.CENTER);
+ add(us,"alignx 50%, growx");
+
+ // Add the rocket figure
+ add(scrollPane,"grow, spany 2, wmin 300lp, hmin 100lp, wrap");
+
+
+ // Add rotation slider
+ // Minimum size to fit "360deg"
+ JLabel l = new JLabel("360\u00b0");
+ Dimension d = l.getPreferredSize();
+
+ add(new BasicSlider(theta.getSliderModel(0,2*Math.PI),JSlider.VERTICAL,true),
+ "ax 50%, wrap, width "+(d.width+6)+"px:null:null, growy");
+
+
+ infoMessage = new JLabel("<html>" +
+ "Click to select " +
+ "Shift+click to select other " +
+ "Double-click to edit " +
+ "Click+drag to move");
+ infoMessage.setFont(new Font("Sans Serif", Font.PLAIN, 9));
+ add(infoMessage,"skip, span, gapleft 25, wrap");
+
+ addExtras();
+ }
+
+
+
+ public RocketFigure getFigure() {
+ return figure;
+ }
+
+ public AerodynamicCalculator getCalculator() {
+ return calculator;
+ }
+
+ public Configuration getConfiguration() {
+ return configuration;
+ }
+
+ public void setSelectionModel(TreeSelectionModel m) {
+ if (selectionModel != null) {
+ selectionModel.removeTreeSelectionListener(this);
+ }
+ selectionModel = m;
+ selectionModel.addTreeSelectionListener(this);
+ valueChanged((TreeSelectionEvent)null); // updates FigureParameters
+ }
+
+
+
+ /**
+ * Return the angle of attack used in CP calculation. NaN signifies the default value
+ * of zero.
+ * @return the angle of attack used, or NaN.
+ */
+ public double getCPAOA() {
+ return cpAOA;
+ }
+
+ /**
+ * Set the angle of attack to be used in CP calculation. A value of NaN signifies that
+ * the default AOA (zero) should be used.
+ * @param aoa the angle of attack to use, or NaN
+ */
+ public void setCPAOA(double aoa) {
+ if (MathUtil.equals(aoa, cpAOA) ||
+ (Double.isNaN(aoa) && Double.isNaN(cpAOA)))
+ return;
+ cpAOA = aoa;
+ updateExtras();
+ figure.updateFigure();
+ fireChangeEvent();
+ }
+
+ public double getCPTheta() {
+ return cpTheta;
+ }
+
+ public void setCPTheta(double theta) {
+ if (MathUtil.equals(theta, cpTheta) ||
+ (Double.isNaN(theta) && Double.isNaN(cpTheta)))
+ return;
+ cpTheta = theta;
+ if (!Double.isNaN(theta))
+ figure.setRotation(theta);
+ updateExtras();
+ figure.updateFigure();
+ fireChangeEvent();
+ }
+
+ public double getCPMach() {
+ return cpMach;
+ }
+
+ public void setCPMach(double mach) {
+ if (MathUtil.equals(mach, cpMach) ||
+ (Double.isNaN(mach) && Double.isNaN(cpMach)))
+ return;
+ cpMach = mach;
+ updateExtras();
+ figure.updateFigure();
+ fireChangeEvent();
+ }
+
+ public double getCPRoll() {
+ return cpRoll;
+ }
+
+ public void setCPRoll(double roll) {
+ if (MathUtil.equals(roll, cpRoll) ||
+ (Double.isNaN(roll) && Double.isNaN(cpRoll)))
+ return;
+ cpRoll = roll;
+ updateExtras();
+ figure.updateFigure();
+ fireChangeEvent();
+ }
+
+
+
+ @Override
+ public void addChangeListener(ChangeListener listener) {
+ listeners.add(0,listener);
+ }
+ @Override
+ public void removeChangeListener(ChangeListener listener) {
+ listeners.remove(listener);
+ }
+
+ protected void fireChangeEvent() {
+ ChangeEvent e = new ChangeEvent(this);
+ ChangeListener[] list = listeners.toArray(new ChangeListener[0]);
+ for (ChangeListener l: list) {
+ l.stateChanged(e);
+ }
+ }
+
+
+
+
+ /**
+ * Handle clicking on figure shapes. The functioning is the following:
+ *
+ * Get the components clicked.
+ * If no component is clicked, do nothing.
+ * If the primary currently selected component is in the set, keep it,
+ * unless the selector specified is pressed. If it is pressed, cycle to
+ * the next component. Otherwise select the first component in the list.
+ */
+ public static final int CYCLE_SELECTION_MODIFIER = InputEvent.SHIFT_DOWN_MASK;
+
+ private void handleMouseClick(MouseEvent event) {
+ if (event.getButton() != MouseEvent.BUTTON1)
+ return;
+ Point p0 = event.getPoint();
+ Point p1 = scrollPane.getViewport().getViewPosition();
+ int x = p0.x + p1.x;
+ int y = p0.y + p1.y;
+
+ RocketComponent[] clicked = figure.getComponentsByPoint(x, y);
+
+ // If no component is clicked, do nothing
+ if (clicked.length == 0)
+ return;
+
+ // Check whether the currently selected component is in the clicked components.
+ TreePath path = selectionModel.getSelectionPath();
+ if (path != null) {
+ RocketComponent current = (RocketComponent)path.getLastPathComponent();
+ path = null;
+ for (int i=0; i<clicked.length; i++) {
+ if (clicked[i] == current) {
+ if (event.isShiftDown() && (event.getClickCount()==1)) {
+ path = ComponentTreeModel.makeTreePath(clicked[(i+1)%clicked.length]);
+ } else {
+ path = ComponentTreeModel.makeTreePath(clicked[i]);
+ }
+ break;
+ }
+ }
+ }
+
+ // Currently selected component not clicked
+ if (path == null) {
+ path = ComponentTreeModel.makeTreePath(clicked[0]);
+ }
+
+ // Set selection and check for double-click
+ selectionModel.setSelectionPath(path);
+ if (event.getClickCount() == 2) {
+ RocketComponent component = (RocketComponent)path.getLastPathComponent();
+
+ ComponentConfigDialog.showDialog(SwingUtilities.getWindowAncestor(this),
+ document, component);
+ }
+ }
+
+
+
+
+ /**
+ * Updates the extra data included in the figure. Currently this includes
+ * the CP and CG carets.
+ */
+ private WarningSet warnings = new WarningSet();
+ private void updateExtras() {
+ Coordinate cp,cg;
+ double cpx, cgx;
+
+ // TODO: MEDIUM: User-definable conditions
+ FlightConditions conditions = new FlightConditions(configuration);
+ warnings.clear();
+
+ if (!Double.isNaN(cpMach)) {
+ conditions.setMach(cpMach);
+ extraText.setMach(cpMach);
+ } else {
+ conditions.setMach(Prefs.getDefaultMach());
+ extraText.setMach(Prefs.getDefaultMach());
+ }
+
+ if (!Double.isNaN(cpAOA)) {
+ conditions.setAOA(cpAOA);
+ } else {
+ conditions.setAOA(0);
+ }
+ extraText.setAOA(cpAOA);
+
+ if (!Double.isNaN(cpRoll)) {
+ conditions.setRollRate(cpRoll);
+ } else {
+ conditions.setRollRate(0);
+ }
+
+ if (!Double.isNaN(cpTheta)) {
+ conditions.setTheta(cpTheta);
+ cp = calculator.getCP(conditions, warnings);
+ } else {
+ cp = calculator.getWorstCP(conditions, warnings);
+ }
+ extraText.setTheta(cpTheta);
+
+
+ cg = calculator.getCG(0);
+// System.out.println("CG computed as "+cg+ " CP as "+cp);
+
+ if (cp.weight > 0.000001)
+ cpx = cp.x;
+ else
+ cpx = Double.NaN;
+
+ if (cg.weight > 0.000001)
+ cgx = cg.x;
+ else
+ cgx = Double.NaN;
+
+ // Length bound is assumed to be tight
+ double length = 0, diameter = 0;
+ Collection<Coordinate> bounds = configuration.getBounds();
+ if (!bounds.isEmpty()) {
+ double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
+ for (Coordinate c: bounds) {
+ if (c.x < minX)
+ minX = c.x;
+ if (c.x > maxX)
+ maxX = c.x;
+ }
+ length = maxX - minX;
+ }
+
+ for (RocketComponent c: configuration) {
+ if (c instanceof SymmetricComponent) {
+ double d1 = ((SymmetricComponent)c).getForeRadius() * 2;
+ double d2 = ((SymmetricComponent)c).getAftRadius() * 2;
+ diameter = MathUtil.max(diameter, d1, d2);
+ }
+ }
+
+ extraText.setCG(cgx);
+ extraText.setCP(cpx);
+ extraText.setLength(length);
+ extraText.setDiameter(diameter);
+ extraText.setMass(cg.weight);
+ extraText.setWarnings(warnings);
+
+
+ if (figure.getType() == RocketFigure.TYPE_SIDE && length > 0) {
+
+ // TODO: LOW: Y-coordinate and rotation
+ extraCP.setPosition(cpx * RocketFigure.EXTRA_SCALE, 0);
+ extraCG.setPosition(cgx * RocketFigure.EXTRA_SCALE, 0);
+
+ } else {
+
+ extraCP.setPosition(Double.NaN, Double.NaN);
+ extraCG.setPosition(Double.NaN, Double.NaN);
+
+ }
+
+
+ //////// Flight simulation in background
+
+ // Check whether to compute or not
+ if (!Prefs.computeFlightInBackground()) {
+ extraText.setFlightData(null);
+ extraText.setCalculatingData(false);
+ stopBackgroundSimulation();
+ return;
+ }
+
+ // Check whether data is already up to date
+ if (flightDataFunctionalID == configuration.getRocket().getFunctionalModID() &&
+ flightDataMotorID == configuration.getMotorConfigurationID()) {
+ return;
+ }
+
+ flightDataFunctionalID = configuration.getRocket().getFunctionalModID();
+ flightDataMotorID = configuration.getMotorConfigurationID();
+
+ // Stop previous computation (if any)
+ stopBackgroundSimulation();
+
+ // Check that configuration has motors
+ if (!configuration.hasMotors()) {
+ extraText.setFlightData(FlightData.NaN_DATA);
+ extraText.setCalculatingData(false);
+ return;
+ }
+
+ // Start calculation process
+ extraText.setCalculatingData(true);
+
+ Rocket duplicate = configuration.getRocket().copy();
+ Simulation simulation = Prefs.getBackgroundSimulation(duplicate);
+ simulation.getConditions().setMotorConfigurationID(
+ configuration.getMotorConfigurationID());
+
+ backgroundSimulationWorker = new BackgroundSimulationWorker(simulation);
+ backgroundSimulationExecutor.execute(backgroundSimulationWorker);
+ }
+
+ /**
+ * Cancels the current background simulation worker, if any.
+ */
+ private void stopBackgroundSimulation() {
+ if (backgroundSimulationWorker != null) {
+ backgroundSimulationWorker.cancel(true);
+ backgroundSimulationWorker = null;
+ }
+ }
+
+
+ /**
+ * A SimulationWorker that simulates the rocket flight in the background and
+ * sets the results to the extra text when finished. The worker can be cancelled
+ * if necessary.
+ */
+ private class BackgroundSimulationWorker extends SimulationWorker {
+
+ public BackgroundSimulationWorker(Simulation sim) {
+ super(sim);
+ }
+
+ @Override
+ protected FlightData doInBackground() {
+
+ // Pause a little while to allow faster UI reaction
+ try {
+ Thread.sleep(300);
+ } catch (InterruptedException ignore) { }
+ if (isCancelled() || backgroundSimulationWorker != this)
+ return null;
+
+ return super.doInBackground();
+ }
+
+ @Override
+ protected void simulationDone() {
+ // Do nothing if cancelled
+ if (isCancelled() || backgroundSimulationWorker != this) // Double-check
+ return;
+
+ backgroundSimulationWorker = null;
+ extraText.setFlightData(simulation.getSimulatedData());
+ extraText.setCalculatingData(false);
+ figure.repaint();
+ }
+
+ @Override
+ protected SimulationListener[] getExtraListeners() {
+ return new SimulationListener[] {
+ InterruptListener.INSTANCE,
+ ApogeeEndListener.INSTANCE
+ };
+ }
+
+ @Override
+ protected void simulationInterrupted(Throwable t) {
+ // Do nothing on cancel, set N/A data otherwise
+ if (isCancelled() || backgroundSimulationWorker != this) // Double-check
+ return;
+
+ backgroundSimulationWorker = null;
+ extraText.setFlightData(FlightData.NaN_DATA);
+ extraText.setCalculatingData(false);
+ figure.repaint();
+ }
+ }
+
+
+
+ /**
+ * Adds the extra data to the figure. Currently this includes the CP and CG carets.
+ */
+ private void addExtras() {
+ figure.clearRelativeExtra();
+ extraCG = new CGCaret(0,0);
+ extraCP = new CPCaret(0,0);
+ extraText = new RocketInfo(configuration);
+ updateExtras();
+ figure.addRelativeExtra(extraCP);
+ figure.addRelativeExtra(extraCG);
+ figure.addAbsoluteExtra(extraText);
+ }
+
+
+ /**
+ * Updates the selection in the FigureParameters and repaints the figure.
+ * Ignores the event itself.
+ */
+ public void valueChanged(TreeSelectionEvent e) {
+ TreePath[] paths = selectionModel.getSelectionPaths();
+ if (paths==null) {
+ figure.setSelection(null);
+ return;
+ }
+
+ RocketComponent[] components = new RocketComponent[paths.length];
+ for (int i=0; i<paths.length; i++)
+ components[i] = (RocketComponent)paths[i].getLastPathComponent();
+ figure.setSelection(components);
+ }
+
+
+
+ /**
+ * An <code>Action</code> that shows whether the figure type is the type
+ * given in the constructor.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+ private class FigureTypeAction extends AbstractAction implements ChangeListener {
+ private final int type;
+
+ public FigureTypeAction(int type) {
+ this.type = type;
+ stateChanged(null);
+ figure.addChangeListener(this);
+ }
+
+ public void actionPerformed(ActionEvent e) {
+ boolean state = (Boolean)getValue(Action.SELECTED_KEY);
+ if (state == true) {
+ // This view has been selected
+ figure.setType(type);
+ updateExtras();
+ }
+ stateChanged(null);
+ }
+
+ public void stateChanged(ChangeEvent e) {
+ putValue(Action.SELECTED_KEY,figure.getType() == type);
+ }
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.gui.scalefigure;
+
+import java.awt.Dimension;
+
+import net.sf.openrocket.util.ChangeSource;
+
+
+public interface ScaleFigure extends ChangeSource {
+
+ /**
+ * Extra scaling applied to the figure. The f***ing Java JRE doesn't know
+ * how to draw shapes when using very large scaling factors, so this must
+ * be manually applied to every single shape used.
+ * <p>
+ * The scaling factor used is divided by this value, and every coordinate used
+ * in the figures must be multiplied by this factor.
+ */
+ public static final double EXTRA_SCALE = 1000;
+
+ /**
+ * Shorthand for {@link #EXTRA_SCALE}.
+ */
+ public static final double S = EXTRA_SCALE;
+
+
+ /**
+ * Set the scale level of the figure. A scale value of 1.0 indicates an original
+ * size when using the current DPI level.
+ *
+ * @param scale the scale level.
+ */
+ public void setScaling(double scale);
+
+
+ /**
+ * Set the scale level so that the figure fits into the given bounds.
+ *
+ * @param bounds the bounds of the figure.
+ */
+ public void setScaling(Dimension bounds);
+
+
+ /**
+ * Return the scale level of the figure. A scale value of 1.0 indicates an original
+ * size when using the current DPI level.
+ *
+ * @return the current scale level.
+ */
+ public double getScaling();
+
+
+ /**
+ * Return the scale of the figure on px/m.
+ *
+ * @return the current scale value.
+ */
+ public double getAbsoluteScale();
+
+
+ /**
+ * Return the pixel coordinates of the figure origin.
+ *
+ * @return the pixel coordinates of the figure origin.
+ */
+ public Dimension getOrigin();
+
+}
--- /dev/null
+package net.sf.openrocket.gui.scalefigure;
+
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.Graphics;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.awt.RenderingHints;
+import java.awt.event.ComponentAdapter;
+import java.awt.event.ComponentEvent;
+import java.awt.event.MouseEvent;
+import java.awt.event.MouseListener;
+import java.awt.event.MouseMotionListener;
+
+import javax.swing.BorderFactory;
+import javax.swing.JComponent;
+import javax.swing.JPanel;
+import javax.swing.JScrollPane;
+import javax.swing.JViewport;
+import javax.swing.ScrollPaneConstants;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.gui.UnitSelector;
+import net.sf.openrocket.gui.adaptors.DoubleModel;
+import net.sf.openrocket.unit.Tick;
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+
+
+
+/**
+ * A scroll pane that holds a {@link ScaleFigure} and includes rulers that show
+ * natural units. The figure can be moved by dragging on the figure.
+ * <p>
+ * This class implements both <code>MouseListener</code> and
+ * <code>MouseMotionListener</code>. If subclasses require extra functionality
+ * (e.g. checking for clicks) then these methods may be overridden, and only unhandled
+ * events passed to this class.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class ScaleScrollPane extends JScrollPane
+ implements MouseListener, MouseMotionListener {
+
+ public static final int RULER_SIZE = 20;
+ public static final int MINOR_TICKS = 3;
+ public static final int MAJOR_TICKS = 30;
+
+
+ private JComponent component;
+ private ScaleFigure figure;
+ private JViewport viewport;
+
+ private DoubleModel rulerUnit;
+ private Ruler horizontalRuler;
+ private Ruler verticalRuler;
+
+ private final boolean allowFit;
+
+ private boolean fit = false;
+
+
+ public ScaleScrollPane(JComponent component) {
+ this(component, true);
+ }
+
+ public ScaleScrollPane(JComponent component, boolean allowFit) {
+ super(component);
+// super(component, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
+// JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);
+
+ if (!(component instanceof ScaleFigure)) {
+ throw new IllegalArgumentException("component must implement ScaleFigure");
+ }
+
+ this.component = component;
+ this.figure = (ScaleFigure)component;
+ this.allowFit = allowFit;
+
+
+ rulerUnit = new DoubleModel(0.0,UnitGroup.UNITS_LENGTH);
+ rulerUnit.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ ScaleScrollPane.this.component.repaint();
+ }
+ });
+ horizontalRuler = new Ruler(Ruler.HORIZONTAL);
+ verticalRuler = new Ruler(Ruler.VERTICAL);
+ this.setColumnHeaderView(horizontalRuler);
+ this.setRowHeaderView(verticalRuler);
+
+ UnitSelector selector = new UnitSelector(rulerUnit);
+ selector.setFont(new Font("SansSerif", Font.PLAIN, 8));
+ this.setCorner(JScrollPane.UPPER_LEFT_CORNER, selector);
+ this.setCorner(JScrollPane.UPPER_RIGHT_CORNER, new JPanel());
+ this.setCorner(JScrollPane.LOWER_LEFT_CORNER, new JPanel());
+ this.setCorner(JScrollPane.LOWER_RIGHT_CORNER, new JPanel());
+
+ this.setBorder(BorderFactory.createLineBorder(Color.LIGHT_GRAY));
+
+
+ viewport = this.getViewport();
+ viewport.addMouseListener(this);
+ viewport.addMouseMotionListener(this);
+
+ figure.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ horizontalRuler.updateSize();
+ verticalRuler.updateSize();
+ if (fit) {
+ setFitting(true);
+ }
+ }
+ });
+
+ viewport.addComponentListener(new ComponentAdapter() {
+ @Override
+ public void componentResized(ComponentEvent e) {
+ if (fit) {
+ setFitting(true);
+ }
+ }
+ });
+
+ }
+
+ public ScaleFigure getFigure() {
+ return figure;
+ }
+
+
+ public boolean isFittingAllowed() {
+ return allowFit;
+ }
+
+ public boolean isFitting() {
+ return fit;
+ }
+
+ public void setFitting(boolean fit) {
+ if (fit && !allowFit) {
+ throw new RuntimeException("Attempting to fit figure not allowing fit.");
+ }
+ this.fit = fit;
+ if (fit) {
+ setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
+ setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
+ validate();
+ Dimension view = viewport.getExtentSize();
+ figure.setScaling(view);
+ } else {
+ setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
+ setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED);
+ }
+ }
+
+
+
+ public double getScaling() {
+ return figure.getScaling();
+ }
+
+ public double getScale() {
+ return figure.getAbsoluteScale();
+ }
+
+ public void setScaling(double scale) {
+ if (fit) {
+ setFitting(false);
+ }
+ figure.setScaling(scale);
+ horizontalRuler.repaint();
+ verticalRuler.repaint();
+ }
+
+
+ public Unit getCurrentUnit() {
+ return rulerUnit.getCurrentUnit();
+ }
+
+
+ //////////////// Mouse handlers ////////////////
+
+
+ private int dragStartX=0;
+ private int dragStartY=0;
+ private Rectangle dragRectangle = null;
+
+ @Override
+ public void mousePressed(MouseEvent e) {
+ dragStartX = e.getX();
+ dragStartY = e.getY();
+ dragRectangle = viewport.getViewRect();
+ }
+
+ @Override
+ public void mouseReleased(MouseEvent e) {
+ dragRectangle = null;
+ }
+
+ @Override
+ public void mouseDragged(MouseEvent e) {
+ if (dragRectangle==null) {
+ return;
+ }
+
+ dragRectangle.setLocation(dragStartX-e.getX(),dragStartY-e.getY());
+
+ dragStartX = e.getX();
+ dragStartY = e.getY();
+
+ viewport.scrollRectToVisible(dragRectangle);
+ }
+
+ @Override
+ public void mouseClicked(MouseEvent e) {
+ }
+
+ @Override
+ public void mouseEntered(MouseEvent e) {
+ }
+
+ @Override
+ public void mouseExited(MouseEvent e) {
+ }
+
+ @Override
+ public void mouseMoved(MouseEvent e) {
+ }
+
+
+
+ //////////////// The view port rulers ////////////////
+
+
+ private class Ruler extends JComponent {
+ public static final int HORIZONTAL = 0;
+ public static final int VERTICAL = 1;
+
+ private final int orientation;
+
+ public Ruler(int orientation) {
+ this.orientation = orientation;
+ updateSize();
+
+ rulerUnit.addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ Ruler.this.repaint();
+ }
+ });
+ }
+
+
+ public void updateSize() {
+ Dimension d = component.getPreferredSize();
+ if (orientation == HORIZONTAL) {
+ setPreferredSize(new Dimension(d.width+10,RULER_SIZE));
+ } else {
+ setPreferredSize(new Dimension(RULER_SIZE,d.height+10));
+ }
+ revalidate();
+ repaint();
+ }
+
+ private double fromPx(int px) {
+ Dimension origin = figure.getOrigin();
+ if (orientation == HORIZONTAL) {
+ px -= origin.width;
+ } else {
+// px = -(px - origin.height);
+ px -= origin.height;
+ }
+ return px/figure.getAbsoluteScale();
+ }
+
+ private int toPx(double l) {
+ Dimension origin = figure.getOrigin();
+ int px = (int)(l * figure.getAbsoluteScale() + 0.5);
+ if (orientation == HORIZONTAL) {
+ px += origin.width;
+ } else {
+ px = px + origin.height;
+// px += origin.height;
+ }
+ return px;
+ }
+
+
+ @Override
+ protected void paintComponent(Graphics g) {
+ super.paintComponent(g);
+ Graphics2D g2 = (Graphics2D)g;
+
+ Rectangle area = g2.getClipBounds();
+
+ // Fill area with background color
+ g2.setColor(getBackground());
+ g2.fillRect(area.x, area.y, area.width, area.height+100);
+
+
+ int startpx,endpx;
+ if (orientation == HORIZONTAL) {
+ startpx = area.x;
+ endpx = area.x+area.width;
+ } else {
+ startpx = area.y;
+ endpx = area.y+area.height;
+ }
+
+ Unit unit = rulerUnit.getCurrentUnit();
+ double start,end,minor,major;
+ start = fromPx(startpx);
+ end = fromPx(endpx);
+ minor = MINOR_TICKS/figure.getAbsoluteScale();
+ major = MAJOR_TICKS/figure.getAbsoluteScale();
+
+ Tick[] ticks = unit.getTicks(start, end, minor, major);
+
+
+ // Set color & hints
+ g2.setColor(Color.BLACK);
+ g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,
+ RenderingHints.VALUE_STROKE_NORMALIZE);
+ g2.setRenderingHint(RenderingHints.KEY_RENDERING,
+ RenderingHints.VALUE_RENDER_QUALITY);
+ g2.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
+ RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
+
+ for (Tick t: ticks) {
+ int position = toPx(t.value);
+ drawTick(g2,position,t);
+ }
+ }
+
+ private void drawTick(Graphics g, int position, Tick t) {
+ int length;
+ String str = null;
+ if (t.major) {
+ length = RULER_SIZE/2;
+ } else {
+ if (t.notable)
+ length = RULER_SIZE/3;
+ else
+ length = RULER_SIZE/6;
+ }
+
+ // Set font
+ if (t.major) {
+ str = rulerUnit.getCurrentUnit().toString(t.value);
+ if (t.notable)
+ g.setFont(new Font("SansSerif", Font.BOLD, 9));
+ else
+ g.setFont(new Font("SansSerif", Font.PLAIN, 9));
+ }
+
+ // Draw tick & text
+ if (orientation == HORIZONTAL) {
+ g.drawLine(position, RULER_SIZE-length, position, RULER_SIZE);
+ if (str != null)
+ g.drawString(str, position, RULER_SIZE-length-1);
+ } else {
+ g.drawLine(RULER_SIZE-length, position, RULER_SIZE, position);
+ if (str != null)
+ g.drawString(str, 1, position-1);
+ }
+ }
+ }
+}
--- /dev/null
+package net.sf.openrocket.gui.scalefigure;
+
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.text.DecimalFormat;
+import java.util.Arrays;
+
+import javax.swing.JButton;
+import javax.swing.JComboBox;
+import javax.swing.JPanel;
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.miginfocom.swing.MigLayout;
+import net.sf.openrocket.util.Icons;
+
+public class ScaleSelector extends JPanel {
+
+ // Ready zoom settings
+ private static final DecimalFormat PERCENT_FORMAT = new DecimalFormat("0.#%");
+
+ private static final double[] ZOOM_LEVELS = { 0.15, 0.25, 0.5, 0.75, 1.0, 1.5, 2.0 };
+ private static final String ZOOM_FIT = "Fit";
+ private static final String[] ZOOM_SETTINGS;
+ static {
+ ZOOM_SETTINGS = new String[ZOOM_LEVELS.length+1];
+ for (int i=0; i<ZOOM_LEVELS.length; i++)
+ ZOOM_SETTINGS[i] = PERCENT_FORMAT.format(ZOOM_LEVELS[i]);
+ ZOOM_SETTINGS[ZOOM_SETTINGS.length-1] = ZOOM_FIT;
+ }
+
+
+ private final ScaleScrollPane scrollPane;
+ private JComboBox zoomSelector;
+
+
+ public ScaleSelector(ScaleScrollPane scroll) {
+ super(new MigLayout());
+
+ this.scrollPane = scroll;
+
+ // Zoom out button
+ JButton button = new JButton(Icons.ZOOM_OUT);
+ button.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ double scale = scrollPane.getScaling();
+ scale = getPreviousScale(scale);
+ scrollPane.setScaling(scale);
+ }
+ });
+ add(button, "gap");
+
+ // Zoom level selector
+ String[] settings = ZOOM_SETTINGS;
+ if (!scrollPane.isFittingAllowed()) {
+ settings = Arrays.copyOf(settings, settings.length-1);
+ }
+
+ zoomSelector = new JComboBox(settings);
+ zoomSelector.setEditable(true);
+ setZoomText();
+ zoomSelector.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ try {
+ String text = (String)zoomSelector.getSelectedItem();
+ text = text.replaceAll("%", "").trim();
+
+ if (text.toLowerCase().startsWith(ZOOM_FIT.toLowerCase()) &&
+ scrollPane.isFittingAllowed()) {
+ scrollPane.setFitting(true);
+ setZoomText();
+ return;
+ }
+
+ double n = Double.parseDouble(text);
+ n /= 100;
+ if (n <= 0.005)
+ n = 0.005;
+
+ scrollPane.setScaling(n);
+ setZoomText();
+ } catch (NumberFormatException ignore) {
+ } finally {
+ setZoomText();
+ }
+ }
+ });
+ scrollPane.getFigure().addChangeListener(new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ setZoomText();
+ }
+ });
+ add(zoomSelector,"gap rel");
+
+
+ // Zoom in button
+ button = new JButton(Icons.ZOOM_IN);
+ button.addActionListener(new ActionListener() {
+ public void actionPerformed(ActionEvent e) {
+ double scale = scrollPane.getScaling();
+ scale = getNextScale(scale);
+ scrollPane.setScaling(scale);
+ }
+ });
+ add(button,"gapleft rel");
+
+ }
+
+
+
+ private void setZoomText() {
+ String text;
+ double zoom = scrollPane.getScaling();
+ text = PERCENT_FORMAT.format(zoom);
+ if (scrollPane.isFitting()) {
+ text = "Fit ("+text+")";
+ }
+ if (!text.equals(zoomSelector.getSelectedItem()))
+ zoomSelector.setSelectedItem(text);
+ }
+
+
+
+ private double getPreviousScale(double scale) {
+ int i;
+ for (i=0; i<ZOOM_LEVELS.length-1; i++) {
+ if (scale > ZOOM_LEVELS[i]+0.05 && scale < ZOOM_LEVELS[i+1]+0.05)
+ return ZOOM_LEVELS[i];
+ }
+ if (scale > ZOOM_LEVELS[ZOOM_LEVELS.length/2]) {
+ // scale is large, drop to next lowest full 100%
+ scale = Math.ceil(scale-1.05);
+ return Math.max(scale, ZOOM_LEVELS[i]);
+ }
+ // scale is small
+ return scale/1.5;
+ }
+
+
+ private double getNextScale(double scale) {
+ int i;
+ for (i=0; i<ZOOM_LEVELS.length-1; i++) {
+ if (scale > ZOOM_LEVELS[i]-0.05 && scale < ZOOM_LEVELS[i+1]-0.05)
+ return ZOOM_LEVELS[i+1];
+ }
+ if (scale > ZOOM_LEVELS[ZOOM_LEVELS.length/2]) {
+ // scale is large, give next full 100%
+ scale = Math.floor(scale+1.05);
+ return scale;
+ }
+ return scale*1.5;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.material;
+
+import net.sf.openrocket.unit.Unit;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.MathUtil;
+
+/**
+ * A class for different material types. Each material has a name and density.
+ * The interpretation of the density depends on the material type. For
+ * {@link Type#BULK} it is kg/m^3, for {@link Type#SURFACE} km/m^2.
+ * <p>
+ * Objects of this type are immutable.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public abstract class Material implements Comparable<Material> {
+
+ public enum Type {
+ LINE,
+ SURFACE,
+ BULK
+ }
+
+ public static class Line extends Material {
+ public Line(String name, double density) {
+ super(name, density);
+ }
+
+ @Override
+ public UnitGroup getUnitGroup() {
+ return UnitGroup.UNITS_DENSITY_LINE;
+ }
+
+ @Override
+ public Type getType() {
+ return Type.LINE;
+ }
+ }
+
+ public static class Surface extends Material {
+
+ public Surface(String name, double density) {
+ super(name, density);
+ }
+
+ @Override
+ public UnitGroup getUnitGroup() {
+ return UnitGroup.UNITS_DENSITY_SURFACE;
+ }
+
+ @Override
+ public Type getType() {
+ return Type.SURFACE;
+ }
+
+ @Override
+ public String toStorableString() {
+ return super.toStorableString();
+ }
+ }
+
+ public static class Bulk extends Material {
+ public Bulk(String name, double density) {
+ super(name, density);
+ }
+
+ @Override
+ public UnitGroup getUnitGroup() {
+ return UnitGroup.UNITS_DENSITY_BULK;
+ }
+
+ @Override
+ public Type getType() {
+ return Type.BULK;
+ }
+ }
+
+
+
+ private final String name;
+ private final double density;
+
+
+ public Material(String name, double density) {
+ this.name = name;
+ this.density = density;
+ }
+
+
+
+ public double getDensity() {
+ return density;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public String getName(Unit u) {
+ return name + " (" + u.toStringUnit(density) + ")";
+ }
+
+ public abstract UnitGroup getUnitGroup();
+ public abstract Type getType();
+
+ @Override
+ public String toString() {
+ return getName(getUnitGroup().getDefaultUnit());
+ }
+
+
+ /**
+ * Compares this object to another object. Material objects are equal if and only if
+ * their types, names and densities are identical.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (o == null)
+ return false;
+ if (this.getClass() != o.getClass())
+ return false;
+ Material m = (Material)o;
+ return ((m.name.equals(this.name)) &&
+ MathUtil.equals(m.density, this.density));
+ }
+
+
+ /**
+ * A hashCode() method giving a hash code compatible with the equals() method.
+ */
+ @Override
+ public int hashCode() {
+ return name.hashCode() + (int)(density*1000);
+ }
+
+
+ /**
+ * Order the materials according to their name, secondarily according to density.
+ */
+ public int compareTo(Material o) {
+ int c = this.name.compareTo(o.name);
+ if (c != 0) {
+ return c;
+ } else {
+ return (int)((this.density - o.density)*1000);
+ }
+ }
+
+
+
+ public static Material newMaterial(Type type, String name, double density) {
+ switch (type) {
+ case LINE:
+ return new Material.Line(name, density);
+
+ case SURFACE:
+ return new Material.Surface(name, density);
+
+ case BULK:
+ return new Material.Bulk(name, density);
+
+ default:
+ throw new IllegalArgumentException("Unknown material type: "+type);
+ }
+ }
+
+
+ public String toStorableString() {
+ return getType().name() + "|" + name.replace('|', ' ') + '|' + density;
+ }
+
+ public static Material fromStorableString(String str) {
+ String[] split = str.split("\\|",3);
+ if (split.length < 3)
+ throw new IllegalArgumentException("Illegal material string: "+str);
+
+ Type type = null;
+ String name;
+ double density;
+
+ try {
+ type = Type.valueOf(split[0]);
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Illegal material string: "+str, e);
+ }
+
+ name = split[1];
+
+ try {
+ density = Double.parseDouble(split[2]);
+ } catch (NumberFormatException e) {
+ throw new IllegalArgumentException("Illegal material string: "+str, e);
+ }
+
+ switch (type) {
+ case BULK:
+ return new Material.Bulk(name, density);
+
+ case SURFACE:
+ return new Material.Surface(name, density);
+
+ case LINE:
+ return new Material.Line(name, density);
+
+ default:
+ throw new IllegalArgumentException("Illegal material string: "+str);
+ }
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+
+/**
+ * Class to represent a body object. The object can be described as a function of
+ * the cylindrical coordinates x and angle theta as r = f(x,theta). The component
+ * need not be symmetrical in any way (e.g. square tube, slanted cone etc).
+ *
+ * It defines the methods getRadius(x,theta) and getInnerRadius(x,theta), as well
+ * as get/setLength().
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public abstract class BodyComponent extends ExternalComponent {
+
+ /**
+ * Default constructor. Sets the relative position to POSITION_RELATIVE_AFTER,
+ * i.e. body components come after one another.
+ */
+ public BodyComponent() {
+ super(RocketComponent.Position.AFTER);
+ }
+
+
+
+ /**
+ * Get the outer radius of the component at cylindrical coordinate (x,theta).
+ *
+ * Note that the return value may be negative for a slanted object.
+ *
+ * @param x Distance in x direction
+ * @param theta Angle about the x-axis
+ * @return Distance to the outer edge of the object
+ */
+ public abstract double getRadius(double x, double theta);
+
+
+ /**
+ * Get the inner radius of the component at cylindrical coordinate (x,theta).
+ *
+ * Note that the return value may be negative for a slanted object.
+ *
+ * @param x Distance in x direction
+ * @param theta Angle about the x-axis
+ * @return Distance to the inner edge of the object
+ */
+ public abstract double getInnerRadius(double x, double theta);
+
+
+
+ /**
+ * Sets the length of the body component.
+ */
+ public void setLength(double length) {
+ if (this.length == length)
+ return;
+ this.length = Math.max(length,0);
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+ /**
+ * Check whether the given type can be added to this component. BodyComponents allow any
+ * InternalComponents or ExternalComponents, excluding BodyComponents, to be added.
+ *
+ * @param type The RocketComponent class type to add.
+ * @return Whether such a component can be added.
+ */
+ @Override
+ public boolean isCompatible(Class<? extends RocketComponent> type) {
+ if (InternalComponent.class.isAssignableFrom(type))
+ return true;
+ if (ExternalComponent.class.isAssignableFrom(type) &&
+ !BodyComponent.class.isAssignableFrom(type))
+ return true;
+ return false;
+ }
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+/**
+ * Rocket body tube component. Has only two parameters, a radius and length.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class BodyTube extends SymmetricComponent implements MotorMount {
+
+ private double radius=0;
+ private boolean autoRadius = false; // Radius chosen automatically based on parent component
+
+ // When changing the inner radius, thickness is modified
+
+ private boolean motorMount = false;
+ private HashMap<String, Double> ejectionDelays = new HashMap<String, Double>();
+ private HashMap<String, Motor> motors = new HashMap<String, Motor>();
+ private IgnitionEvent ignitionEvent = IgnitionEvent.AUTOMATIC;
+ private double ignitionDelay = 0;
+ private double overhang = 0;
+
+
+
+ public BodyTube() {
+ super();
+ this.length = 8*DEFAULT_RADIUS;
+ this.radius = DEFAULT_RADIUS;
+ this.autoRadius = true;
+ }
+
+ public BodyTube(double length, double radius) {
+ super();
+ this.radius = Math.max(radius,0);
+ this.length = Math.max(length,0);
+ }
+
+
+ public BodyTube(double length, double radius, boolean filled) {
+ this(length,radius);
+ this.filled = filled;
+ }
+
+ public BodyTube(double length, double radius, double thickness) {
+ this(length,radius);
+ this.filled = false;
+ this.thickness = thickness;
+ }
+
+
+ /************ Get/set component parameter methods ************/
+
+ /**
+ * Return the outer radius of the body tube.
+ */
+ public double getRadius() {
+ if (autoRadius) {
+ // Return auto radius from front or rear
+ double r = -1;
+ SymmetricComponent c = this.getPreviousSymmetricComponent();
+ if (c != null) {
+ r = c.getFrontAutoRadius();
+ }
+ if (r < 0) {
+ c = this.getNextSymmetricComponent();
+ if (c != null) {
+ r = c.getRearAutoRadius();
+ }
+ }
+ if (r < 0)
+ r = DEFAULT_RADIUS;
+ return r;
+ }
+ return radius;
+ }
+
+
+ /**
+ * Set the outer radius of the body tube. If the radius is less than the wall thickness,
+ * the wall thickness is decreased accordingly of the value of the radius.
+ * This method sets the automatic radius off.
+ */
+ public void setRadius(double radius) {
+ if ((this.radius == radius) && (autoRadius==false))
+ return;
+
+ this.autoRadius = false;
+ this.radius = Math.max(radius,0);
+
+ if (this.thickness > this.radius)
+ this.thickness = this.radius;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+ /**
+ * Returns whether the radius is selected automatically or not.
+ * Returns false also in case automatic radius selection is not possible.
+ */
+ public boolean isRadiusAutomatic() {
+ return autoRadius;
+ }
+
+ /**
+ * Sets whether the radius is selected automatically or not.
+ */
+ public void setRadiusAutomatic(boolean auto) {
+ if (autoRadius == auto)
+ return;
+
+ autoRadius = auto;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+ @Override
+ public double getAftRadius() { return getRadius(); }
+ @Override
+ public double getForeRadius() { return getRadius(); }
+ @Override
+ public boolean isAftRadiusAutomatic() { return isRadiusAutomatic(); }
+ @Override
+ public boolean isForeRadiusAutomatic() { return isRadiusAutomatic(); }
+
+
+
+ @Override
+ protected double getFrontAutoRadius() {
+ if (isRadiusAutomatic()) {
+ // Search for previous SymmetricComponent
+ SymmetricComponent c = this.getPreviousSymmetricComponent();
+ if (c != null) {
+ return c.getFrontAutoRadius();
+ } else {
+ return -1;
+ }
+ }
+ return getRadius();
+ }
+
+ @Override
+ protected double getRearAutoRadius() {
+ if (isRadiusAutomatic()) {
+ // Search for next SymmetricComponent
+ SymmetricComponent c = this.getNextSymmetricComponent();
+ if (c != null) {
+ return c.getRearAutoRadius();
+ } else {
+ return -1;
+ }
+ }
+ return getRadius();
+ }
+
+
+
+
+
+
+
+ public double getInnerRadius() {
+ if (filled)
+ return 0;
+ return Math.max(getRadius()-thickness, 0);
+ }
+
+ public void setInnerRadius(double r) {
+ setThickness(getRadius()-r);
+ }
+
+
+
+
+ /**
+ * Return the component name.
+ */
+ @Override
+ public String getComponentName() {
+ return "Body tube";
+ }
+
+
+ /************ Component calculations ***********/
+
+ // From SymmetricComponent
+ /**
+ * Returns the outer radius at the position x. This returns the same value as getRadius().
+ */
+ @Override
+ public double getRadius(double x) {
+ return getRadius();
+ }
+
+ /**
+ * Returns the inner radius at the position x. If the tube is filled, returns always zero.
+ */
+ @Override
+ public double getInnerRadius(double x) {
+ if (filled)
+ return 0.0;
+ else
+ return Math.max(getRadius()-thickness,0);
+ }
+
+
+ /**
+ * Returns the body tube's center of gravity.
+ */
+ @Override
+ public Coordinate getComponentCG() {
+ return new Coordinate(length/2,0,0,getComponentMass());
+ }
+
+ /**
+ * Returns the body tube's volume.
+ */
+ @Override
+ public double getComponentVolume() {
+ double r = getRadius();
+ if (filled)
+ return getFilledVolume(r,length);
+ else
+ return getFilledVolume(r,length) - getFilledVolume(getInnerRadius(0),length);
+ }
+
+
+ @Override
+ public double getLongitudalUnitInertia() {
+ // 1/12 * (3 * (r1^2 + r2^2) + h^2)
+ return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getRadius()) +
+ MathUtil.pow2(getLength())) / 12;
+ }
+
+ @Override
+ public double getRotationalUnitInertia() {
+ // 1/2 * (r1^2 + r2^2)
+ return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getRadius()))/2;
+ }
+
+
+
+
+ /**
+ * Helper function for cylinder volume.
+ */
+ private static double getFilledVolume(double r, double l) {
+ return Math.PI * r*r * l;
+ }
+
+
+ /**
+ * Adds bounding coordinates to the given set. The body tube will fit within the
+ * convex hull of the points.
+ *
+ * Currently the points are simply a rectangular box around the body tube.
+ */
+ @Override
+ public Collection<Coordinate> getComponentBounds() {
+ Collection<Coordinate> bounds = new ArrayList<Coordinate>(8);
+ double r = getRadius();
+ addBound(bounds,0,r);
+ addBound(bounds,length,r);
+ return bounds;
+ }
+
+
+ //////////////// Motor mount /////////////////
+
+ @Override
+ public boolean isMotorMount() {
+ return motorMount;
+ }
+
+ @Override
+ public void setMotorMount(boolean mount) {
+ if (motorMount == mount)
+ return;
+ motorMount = mount;
+ fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
+ }
+
+ @Override
+ public Motor getMotor(String id) {
+ return motors.get(id);
+ }
+
+ @Override
+ public void setMotor(String id, Motor motor) {
+ Motor current = motors.get(id);
+ if ((motor == null && current == null) ||
+ (motor != null && motor.equals(current)))
+ return;
+ motors.put(id, motor);
+ fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
+ }
+
+ @Override
+ public double getMotorDelay(String id) {
+ Double delay = ejectionDelays.get(id);
+ if (delay == null)
+ return Motor.PLUGGED;
+ return delay;
+ }
+
+ @Override
+ public void setMotorDelay(String id, double delay) {
+ ejectionDelays.put(id, delay);
+ fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
+ }
+
+ @Override
+ public int getMotorCount() {
+ return 1;
+ }
+
+ @Override
+ public double getMotorMountDiameter() {
+ return getInnerRadius()*2;
+ }
+
+ @Override
+ public IgnitionEvent getIgnitionEvent() {
+ return ignitionEvent;
+ }
+
+ @Override
+ public void setIgnitionEvent(IgnitionEvent event) {
+ if (ignitionEvent == event)
+ return;
+ ignitionEvent = event;
+ fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
+ }
+
+
+ @Override
+ public double getIgnitionDelay() {
+ return ignitionDelay;
+ }
+
+ @Override
+ public void setIgnitionDelay(double delay) {
+ if (MathUtil.equals(delay, ignitionDelay))
+ return;
+ ignitionDelay = delay;
+ fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
+ }
+
+
+ @Override
+ public double getMotorOverhang() {
+ return overhang;
+ }
+
+ @Override
+ public void setMotorOverhang(double overhang) {
+ if (MathUtil.equals(this.overhang, overhang))
+ return;
+ this.overhang = overhang;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+
+
+ /*
+ * (non-Javadoc)
+ * Copy the motor and ejection delay HashMaps.
+ *
+ * @see rocketcomponent.RocketComponent#copy()
+ */
+ @SuppressWarnings("unchecked")
+ @Override
+ public RocketComponent copy() {
+ RocketComponent c = super.copy();
+ ((BodyTube)c).motors = (HashMap<String,Motor>) motors.clone();
+ ((BodyTube)c).ejectionDelays = (HashMap<String,Double>) ejectionDelays.clone();
+ return c;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+
+public class Bulkhead extends RadiusRingComponent {
+
+ public Bulkhead() {
+ setOuterRadiusAutomatic(true);
+ setLength(0.002);
+ }
+
+ @Override
+ public double getInnerRadius() {
+ return 0;
+ }
+
+ @Override
+ public void setInnerRadius(double r) {
+ // No-op
+ }
+
+ @Override
+ public void setOuterRadiusAutomatic(boolean auto) {
+ super.setOuterRadiusAutomatic(auto);
+ }
+
+ @Override
+ public String getComponentName() {
+ return "Bulkhead";
+ }
+
+ @Override
+ public boolean isCompatible(Class<? extends RocketComponent> type) {
+ return false;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.util.Coordinate;
+
+
+public class CenteringRing extends RadiusRingComponent {
+
+ public CenteringRing() {
+ setOuterRadiusAutomatic(true);
+ setInnerRadiusAutomatic(true);
+ setLength(0.002);
+ }
+
+
+ @Override
+ public double getInnerRadius() {
+ // Implement sibling inner radius automation
+ if (isInnerRadiusAutomatic()) {
+ innerRadius = 0;
+ for (RocketComponent sibling: this.getParent().getChildren()) {
+ if (!(sibling instanceof RadialParent)) // Excludes itself
+ continue;
+
+ double pos1 = this.toRelative(Coordinate.NUL, sibling)[0].x;
+ double pos2 = this.toRelative(new Coordinate(getLength()), sibling)[0].x;
+ if (pos2 < 0 || pos1 > sibling.getLength())
+ continue;
+
+ innerRadius = Math.max(innerRadius, ((InnerTube)sibling).getOuterRadius());
+ }
+ innerRadius = Math.min(innerRadius, getOuterRadius());
+ }
+
+ return super.getInnerRadius();
+ }
+
+
+ @Override
+ public void setOuterRadiusAutomatic(boolean auto) {
+ super.setOuterRadiusAutomatic(auto);
+ }
+
+ @Override
+ public void setInnerRadiusAutomatic(boolean auto) {
+ super.setInnerRadiusAutomatic(auto);
+ }
+
+ @Override
+ public String getComponentName() {
+ return "Centering ring";
+ }
+
+ @Override
+ public boolean isCompatible(Class<? extends RocketComponent> type) {
+ return false;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+
+/**
+ * Class that defines different cluster configurations available for the InnerTube.
+ * The class is immutable, and all the constructors are private. Therefore the only
+ * available cluster configurations are those available in the static fields.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class ClusterConfiguration {
+ // Helper vars
+ private static final double R5 = 1.0/(2*Math.sin(2*Math.PI/10));
+ private static final double SQRT2 = Math.sqrt(2);
+ private static final double SQRT3 = Math.sqrt(3);
+
+ /** A single motor */
+ public static final ClusterConfiguration SINGLE = new ClusterConfiguration("single", 0,0);
+
+ /** Definitions of cluster configurations. Do not modify array. */
+ public static final ClusterConfiguration[] CONFIGURATIONS = {
+ // Single row
+ SINGLE,
+ new ClusterConfiguration("double", -0.5,0, 0.5,0),
+ new ClusterConfiguration("3-row", -1.0,0, 0.0,0, 1.0,0),
+ new ClusterConfiguration("4-row", -1.5,0, -0.5,0, 0.5,0, 1.5,0),
+
+ // Ring of tubes
+ new ClusterConfiguration("3-ring", -0.5,-1.0/(2*SQRT3),
+ 0.5,-1.0/(2*SQRT3),
+ 0, 1.0/SQRT3),
+ new ClusterConfiguration("4-ring", -0.5,0.5, 0.5,0.5, 0.5,-0.5, -0.5,-0.5),
+ new ClusterConfiguration("5-ring", 0,R5,
+ R5*Math.sin(2*Math.PI/5),R5*Math.cos(2*Math.PI/5),
+ R5*Math.sin(2*Math.PI*2/5),R5*Math.cos(2*Math.PI*2/5),
+ R5*Math.sin(2*Math.PI*3/5),R5*Math.cos(2*Math.PI*3/5),
+ R5*Math.sin(2*Math.PI*4/5),R5*Math.cos(2*Math.PI*4/5)),
+ new ClusterConfiguration("6-ring", 0,1, SQRT3/2,0.5, SQRT3/2,-0.5,
+ 0,-1, -SQRT3/2,-0.5, -SQRT3/2,0.5),
+
+ // Centered with ring
+ new ClusterConfiguration("3-star", 0,0, 0,1, SQRT3/2,-0.5, -SQRT3/2,-0.5),
+ new ClusterConfiguration("4-star", 0,0, -1/SQRT2,1/SQRT2, 1/SQRT2,1/SQRT2,
+ 1/SQRT2,-1/SQRT2, -1/SQRT2,-1/SQRT2),
+ new ClusterConfiguration("5-star", 0,0, 0,1,
+ Math.sin(2*Math.PI/5),Math.cos(2*Math.PI/5),
+ Math.sin(2*Math.PI*2/5),Math.cos(2*Math.PI*2/5),
+ Math.sin(2*Math.PI*3/5),Math.cos(2*Math.PI*3/5),
+ Math.sin(2*Math.PI*4/5),Math.cos(2*Math.PI*4/5)),
+ new ClusterConfiguration("6-star", 0,0, 0,1, SQRT3/2,0.5, SQRT3/2,-0.5,
+ 0,-1, -SQRT3/2,-0.5, -SQRT3/2,0.5)
+ };
+
+
+ private final List<Double> points;
+ private final String xmlName;
+
+ private ClusterConfiguration(String xmlName, double... points) {
+ this.xmlName = xmlName;
+ if (points.length == 0 || points.length%2 == 1) {
+ throw new IllegalArgumentException("Illegal number of points specified: "+
+ points.length);
+ }
+ List<Double> l = new ArrayList<Double>(points.length);
+ for (double d: points)
+ l.add(d);
+
+ this.points = Collections.unmodifiableList(l);
+ }
+
+ public String getXMLName() {
+ return xmlName;
+ }
+
+ public int getClusterCount() {
+ return points.size()/2;
+ }
+
+ public List<Double> getPoints() {
+ return points; // Unmodifiable
+ }
+
+ /**
+ * Return the points rotated by <code>rotation</code> radians.
+ * @param rotation Rotation amount.
+ */
+ public List<Double> getPoints(double rotation) {
+ double cos = Math.cos(rotation);
+ double sin = Math.sin(rotation);
+ List<Double> ret = new ArrayList<Double>(points.size());
+ for (int i=0; i<points.size()/2; i++) {
+ double x = points.get(2*i);
+ double y = points.get(2*i+1);
+ ret.add( x*cos + y*sin);
+ ret.add(-x*sin + y*cos);
+ }
+ return ret;
+ }
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.util.ChangeSource;
+
+public interface Clusterable extends ChangeSource {
+
+ public ClusterConfiguration getClusterConfiguration();
+ public void setClusterConfiguration(ClusterConfiguration cluster);
+ public double getClusterSeparation();
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import net.sf.openrocket.util.Coordinate;
+
+
+
+/**
+ * A base of component assemblies.
+ * <p>
+ * Note that the mass and CG overrides of the <code>ComponentAssembly</code> class
+ * overrides all sibling mass/CG as well as its own.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class ComponentAssembly extends RocketComponent {
+
+ /**
+ * Sets the position of the components to POSITION_RELATIVE_AFTER.
+ * (Should have no effect.)
+ */
+ public ComponentAssembly() {
+ super(RocketComponent.Position.AFTER);
+ }
+
+ /**
+ * Null method (ComponentAssembly has no bounds of itself).
+ */
+ @Override
+ public Collection<Coordinate> getComponentBounds() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * Null method (ComponentAssembly has no mass of itself).
+ */
+ @Override
+ public Coordinate getComponentCG() {
+ return Coordinate.NUL;
+ }
+
+ /**
+ * Null method (ComponentAssembly has no mass of itself).
+ */
+ @Override
+ public double getComponentMass() {
+ return 0;
+ }
+
+ /**
+ * Null method (ComponentAssembly has no mass of itself).
+ */
+ @Override
+ public double getLongitudalUnitInertia() {
+ return 0;
+ }
+
+ /**
+ * Null method (ComponentAssembly has no mass of itself).
+ */
+ @Override
+ public double getRotationalUnitInertia() {
+ return 0;
+ }
+
+ /**
+ * Components have no aerodynamic effect, so return false.
+ */
+ @Override
+ public boolean isAerodynamic() {
+ return false;
+ }
+
+ /**
+ * Component have no effect on mass, so return false (even though the override values
+ * may have an effect).
+ */
+ @Override
+ public boolean isMassive() {
+ return false;
+ }
+
+ @Override
+ public boolean getOverrideSubcomponents() {
+ return true;
+ }
+
+ @Override
+ public void setOverrideSubcomponents(boolean override) {
+ // No-op
+ }
+
+ @Override
+ public boolean isOverrideSubcomponentsEnabled() {
+ return false;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import javax.swing.event.ChangeEvent;
+
+public class ComponentChangeEvent extends ChangeEvent {
+ private static final long serialVersionUID = 1L;
+
+
+ /** A change that does not affect simulation results in any way (name, color, etc.) */
+ public static final int NONFUNCTIONAL_CHANGE = 1;
+ /** A change that affects the mass properties of the rocket */
+ public static final int MASS_CHANGE = 2;
+ /** A change that affects the aerodynamic properties of the rocket */
+ public static final int AERODYNAMIC_CHANGE = 4;
+ /** A change that affects the mass and aerodynamic properties of the rocket */
+ public static final int BOTH_CHANGE = MASS_CHANGE|AERODYNAMIC_CHANGE; // Mass & Aerodynamic
+
+ /** A change that affects the rocket tree structure */
+ public static final int TREE_CHANGE = 8;
+ /** A change caused by undo/redo. */
+ public static final int UNDO_CHANGE = 16;
+ /** A change in the motor configurations or names */
+ public static final int MOTOR_CHANGE = 32;
+ /** A change in the events occurring during flight. */
+ public static final int EVENT_CHANGE = 64;
+
+ /** A bit-field that contains all possible change types. */
+ public static final int ALL_CHANGE = 0xFFFFFFFF;
+
+ private final int type;
+
+
+ public ComponentChangeEvent(RocketComponent component, int type) {
+ super(component);
+ if (type == 0) {
+ throw new IllegalArgumentException("no event type provided");
+ }
+ this.type = type;
+ }
+
+
+ public boolean isAerodynamicChange() {
+ return (type & AERODYNAMIC_CHANGE) != 0;
+ }
+
+ public boolean isMassChange() {
+ return (type & MASS_CHANGE) != 0;
+ }
+
+ public boolean isOtherChange() {
+ return (type & BOTH_CHANGE) == 0;
+ }
+
+ public boolean isTreeChange() {
+ return (type & TREE_CHANGE) != 0;
+ }
+
+ public boolean isUndoChange() {
+ return (type & UNDO_CHANGE) != 0;
+ }
+
+ public boolean isMotorChange() {
+ return (type & MOTOR_CHANGE) != 0;
+ }
+
+ public int getType() {
+ return type;
+ }
+
+ @Override
+ public String toString() {
+ String s = "";
+
+ if ((type & NONFUNCTIONAL_CHANGE) != 0)
+ s += ",nonfunc";
+ if (isMassChange())
+ s += ",mass";
+ if (isAerodynamicChange())
+ s += ",aero";
+ if (isTreeChange())
+ s += ",tree";
+ if (isUndoChange())
+ s += ",undo";
+ if (isMotorChange())
+ s += ",motor";
+ if ((type & EVENT_CHANGE) != 0)
+ s += ",event";
+
+ if (s.length() > 0)
+ s = s.substring(1);
+
+ return "ComponentChangeEvent[" + s + "]";
+ }
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.EventListener;
+
+public interface ComponentChangeListener extends EventListener {
+
+ public void componentChanged(ComponentChangeEvent e);
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+import javax.swing.event.EventListenerList;
+
+import net.sf.openrocket.util.ChangeSource;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+public class Configuration implements Cloneable, ChangeSource, ComponentChangeListener,
+ Iterable<RocketComponent> {
+
+ public static final double DEFAULT_IGNITION_TIME = Double.MAX_VALUE;
+
+
+ private Rocket rocket;
+ private BitSet stages = new BitSet();
+
+ private String motorConfiguration = null;
+
+ private EventListenerList listenerList = new EventListenerList();
+
+ private final HashMap<MotorMount, Double> ignitionTimes =
+ new HashMap<MotorMount, Double>();
+
+
+ /* Cached data */
+ private int boundsModID = -1;
+ private ArrayList<Coordinate> cachedBounds = new ArrayList<Coordinate>();
+ private double cachedLength = -1;
+
+ private int refLengthModID = -1;
+ private double cachedRefLength = -1;
+
+
+
+ /**
+ * Create a new configuration with the specified <code>Rocket</code> with
+ * <code>null</code> motor configuration.
+ *
+ * @param rocket the rocket
+ */
+ public Configuration(Rocket rocket) {
+ this.rocket = rocket;
+ setAllStages();
+ rocket.addComponentChangeListener(this);
+ }
+
+
+ /**
+ * Create a new configuration with the specified <code>Rocket</code> and motor
+ * configuration.
+ *
+ * @param rocket the rocket.
+ * @param motorID the motor configuration ID to use.
+ */
+ public Configuration(Rocket rocket, String motorID) {
+ this.rocket = rocket;
+ this.motorConfiguration = motorID;
+ setAllStages();
+ rocket.addComponentChangeListener(this);
+ }
+
+
+
+
+ public Rocket getRocket() {
+ return rocket;
+ }
+
+
+ public void setAllStages() {
+ stages.clear();
+ stages.set(0,rocket.getStageCount());
+ fireChangeEvent();
+ }
+
+
+ /**
+ * Set all stages up to and including the given stage number. For example,
+ * <code>setToStage(0)</code> will set only the first stage active.
+ *
+ * @param stage the stage number.
+ */
+ public void setToStage(int stage) {
+ stages.clear();
+ stages.set(0, stage+1, true);
+// stages.set(stage+1, rocket.getStageCount(), false);
+ fireChangeEvent();
+ }
+
+
+ /**
+ * Check whether the up-most stage of the rocket is in this configuration.
+ *
+ * @return <code>true</code> if the first stage is active in this configuration.
+ */
+ public boolean isHead() {
+ return isStageActive(0);
+ }
+
+ public boolean isStageActive(RocketComponent stage) {
+ if (!(stage instanceof Stage)) {
+ throw new IllegalArgumentException("called with component "+stage);
+ }
+ return stages.get(stage.getParent().getChildPosition(stage));
+ }
+
+
+ public boolean isStageActive(int stage) {
+ if (stage >= rocket.getStageCount())
+ return false;
+ return stages.get(stage);
+ }
+
+ public int getStageCount() {
+ return rocket.getStageCount();
+ }
+
+ public int getActiveStageCount() {
+ int count = 0;
+ int s = rocket.getStageCount();
+
+ for (int i=0; i < s; i++) {
+ if (stages.get(i))
+ count++;
+ }
+ return count;
+ }
+
+ public int[] getActiveStages() {
+ int stageCount = rocket.getStageCount();
+ List<Integer> active = new ArrayList<Integer>();
+ int[] ret;
+
+ for (int i=0; i < stageCount; i++) {
+ if (stages.get(i)) {
+ active.add(i);
+ }
+ }
+
+ ret = new int[active.size()];
+ for (int i=0; i < ret.length; i++) {
+ ret[i] = active.get(i);
+ }
+
+ return ret;
+ }
+
+
+ /**
+ * Return the reference length associated with the current configuration. The
+ * reference length type is retrieved from the <code>Rocket</code>.
+ *
+ * @return the reference length for this configuration.
+ */
+ public double getReferenceLength() {
+ if (rocket.getModID() != refLengthModID) {
+ refLengthModID = rocket.getModID();
+ cachedRefLength = rocket.getReferenceType().getReferenceLength(this);
+ }
+ return cachedRefLength;
+ }
+
+
+ public double getReferenceArea() {
+ return Math.PI * MathUtil.pow2(getReferenceLength()/2);
+ }
+
+
+ public String getMotorConfigurationID() {
+ return motorConfiguration;
+ }
+
+ public void setMotorConfigurationID(String id) {
+ if ((motorConfiguration == null && id == null) ||
+ (id != null && id.equals(motorConfiguration)))
+ return;
+
+ motorConfiguration = id;
+ fireChangeEvent();
+ }
+
+ public String getMotorConfigurationDescription() {
+ return rocket.getMotorConfigurationDescription(motorConfiguration);
+ }
+
+
+
+ /**
+ * Clear all motor ignition times. All values are reset to their default of
+ * {@link #DEFAULT_IGNITION_TIME}.
+ */
+ public void resetIgnitionTimes() {
+ ignitionTimes.clear();
+ }
+
+ /**
+ * Set the ignition time of the motor in the specified motor mount. Negative or NaN
+ * time values will cause an <code>IllegalArgumentException</code>.
+ *
+ * @param mount the motor mount to specify.
+ * @param time the time at which to ignite the motors.
+ * @throws IllegalArgumentException if <code>time</code> is negative of NaN.
+ */
+ public void setIgnitionTime(MotorMount mount, double time) {
+ if (time < 0) {
+ throw new IllegalArgumentException("time is negative: "+time);
+ }
+ ignitionTimes.put(mount, time);
+ // TODO: MEDIUM: Should this fire events?
+ }
+
+ /**
+ * Return the ignition time of the motor in the specified motor mount. If no time
+ * has been specified, returns {@link #DEFAULT_IGNITION_TIME} as the default.
+ *
+ * @param mount the motor mount.
+ * @return ignition time of the motors in the mount.
+ */
+ public double getIgnitionTime(MotorMount mount) {
+ Double d = ignitionTimes.get(mount);
+ if (d == null)
+ return DEFAULT_IGNITION_TIME;
+ return d;
+ }
+
+
+
+
+ /**
+ * Removes the listener connection to the rocket and listeners of this object.
+ * This configuration may not be used after a call to this method!
+ */
+ public void release() {
+ rocket.removeComponentChangeListener(this);
+ listenerList = null;
+ rocket = null;
+ }
+
+
+ //////////////// Listeners ////////////////
+
+ @Override
+ public void addChangeListener(ChangeListener listener) {
+ listenerList.add(ChangeListener.class, listener);
+ }
+
+ @Override
+ public void removeChangeListener(ChangeListener listener) {
+ listenerList.remove(ChangeListener.class, listener);
+ }
+
+ protected void fireChangeEvent() {
+ Object[] listeners = listenerList.getListenerList();
+ ChangeEvent e = new ChangeEvent(this);
+
+ boundsModID = -1;
+ refLengthModID = -1;
+
+ for (int i = listeners.length-2; i>=0; i-=2) {
+ if (listeners[i] == ChangeListener.class) {
+ ((ChangeListener) listeners[i+1]).stateChanged(e);
+ }
+ }
+ }
+
+
+ @Override
+ public void componentChanged(ComponentChangeEvent e) {
+ fireChangeEvent();
+ }
+
+
+ /////////////// Helper methods ///////////////
+
+ /**
+ * Return whether this configuration has any motors defined to it.
+ *
+ * @return true if this configuration has active motor mounts with motors defined to them.
+ */
+ public boolean hasMotors() {
+ for (RocketComponent c: this) {
+ if (c instanceof MotorMount) {
+ MotorMount mount = (MotorMount) c;
+ if (!mount.isMotorMount())
+ continue;
+ if (mount.getMotor(this.motorConfiguration) != null) {
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+
+ /**
+ * Return the bounds of the current configuration. The bounds are cached.
+ *
+ * @return a <code>Collection</code> containing coordinates bouding the rocket.
+ */
+ @SuppressWarnings("unchecked")
+ public Collection<Coordinate> getBounds() {
+ if (rocket.getModID() != boundsModID) {
+ boundsModID = rocket.getModID();
+ cachedBounds.clear();
+
+ double minX = Double.POSITIVE_INFINITY, maxX = Double.NEGATIVE_INFINITY;
+ for (RocketComponent component: this) {
+ for (Coordinate c: component.getComponentBounds()) {
+ for (Coordinate coord: component.toAbsolute(c)) {
+ cachedBounds.add(coord);
+ if (coord.x < minX)
+ minX = coord.x;
+ if (coord.x > maxX)
+ maxX = coord.x;
+ }
+ }
+ }
+
+ if (Double.isInfinite(minX) || Double.isInfinite(maxX)) {
+ cachedLength = 0;
+ } else {
+ cachedLength = maxX - minX;
+ }
+ }
+ return (ArrayList<Coordinate>) cachedBounds.clone();
+ }
+
+
+ /**
+ * Returns the length of the rocket configuration, from the foremost bound X-coordinate
+ * to the aft-most X-coordinate. The value is cached.
+ *
+ * @return the length of the rocket in the X-direction.
+ */
+ public double getLength() {
+ if (rocket.getModID() != boundsModID)
+ getBounds(); // Calculates the length
+
+ return cachedLength;
+ }
+
+
+
+
+ /**
+ * Return an iterator that iterates over the currently active components.
+ * The <code>Rocket</code> and <code>Stage</code> components are not returned,
+ * but instead all components that are within currently active stages.
+ */
+ @Override
+ public Iterator<RocketComponent> iterator() {
+ return new ConfigurationIterator();
+ }
+
+
+ /**
+ * Return an iterator that iterates over all <code>MotorMount</code>s within the
+ * current configuration that have an active motor.
+ *
+ * @return an iterator over active motor mounts.
+ */
+ public Iterator<MotorMount> motorIterator() {
+ return new MotorIterator();
+ }
+
+
+ /**
+ * Perform a deep-clone. The object references are also cloned and no
+ * listeners are listening on the cloned object.
+ */
+ @Override
+ public Configuration clone() {
+ try {
+ Configuration config = (Configuration) super.clone();
+ config.listenerList = new EventListenerList();
+ config.stages = (BitSet) this.stages.clone();
+ config.cachedBounds = new ArrayList<Coordinate>();
+ config.boundsModID = -1;
+ config.refLengthModID = -1;
+ rocket.addComponentChangeListener(config);
+ return config;
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException("BUG: clone not supported!",e);
+ }
+ }
+
+
+
+ /**
+ * A class that iterates over all currently active components.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+ private class ConfigurationIterator implements Iterator<RocketComponent> {
+ Iterator<Iterator<RocketComponent>> iterators;
+ Iterator<RocketComponent> current = null;
+
+ public ConfigurationIterator() {
+ List<Iterator<RocketComponent>> list = new ArrayList<Iterator<RocketComponent>>();
+
+ for (RocketComponent stage: rocket.getChildren()) {
+ if (isStageActive(stage)) {
+ list.add(stage.deepIterator());
+ }
+ }
+
+ // Get iterators and initialize current
+ iterators = list.iterator();
+ if (iterators.hasNext()) {
+ current = iterators.next();
+ } else {
+ List<RocketComponent> l = Collections.emptyList();
+ current = l.iterator();
+ }
+ }
+
+
+ @Override
+ public boolean hasNext() {
+ if (!current.hasNext())
+ getNextIterator();
+
+ return current.hasNext();
+ }
+
+ @Override
+ public RocketComponent next() {
+ if (!current.hasNext())
+ getNextIterator();
+
+ return current.next();
+ }
+
+ /**
+ * Get the next iterator that has items. If such an iterator does
+ * not exist, current is left to an empty iterator.
+ */
+ private void getNextIterator() {
+ while ((!current.hasNext()) && iterators.hasNext()) {
+ current = iterators.next();
+ }
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException("remove unsupported");
+ }
+ }
+
+ private class MotorIterator implements Iterator<MotorMount> {
+ private final Iterator<RocketComponent> iterator;
+ private MotorMount next = null;
+
+ public MotorIterator() {
+ this.iterator = iterator();
+ }
+
+ @Override
+ public boolean hasNext() {
+ getNext();
+ return (next != null);
+ }
+
+ @Override
+ public MotorMount next() {
+ getNext();
+ if (next == null) {
+ throw new NoSuchElementException("iterator called for too long");
+ }
+
+ MotorMount ret = next;
+ next = null;
+ return ret;
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException("remove unsupported");
+ }
+
+ private void getNext() {
+ if (next != null)
+ return;
+ while (iterator.hasNext()) {
+ RocketComponent c = iterator.next();
+ if (c instanceof MotorMount) {
+ MotorMount mount = (MotorMount) c;
+ if (mount.isMotorMount() && mount.getMotor(motorConfiguration) != null) {
+ next = mount;
+ return;
+ }
+ }
+ }
+ }
+ }
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+public class EllipticalFinSet extends FinSet {
+ public static final int POINTS = 21;
+
+ private static final double[] POINT_X = new double[POINTS];
+ private static final double[] POINT_Y = new double[POINTS];
+ static {
+ for (int i=0; i < POINTS; i++) {
+ double a = Math.PI * (POINTS-1-i)/(POINTS-1);
+ POINT_X[i] = (Math.cos(a)+1)/2;
+ POINT_Y[i] = Math.sin(a);
+ }
+ POINT_X[0] = 0;
+ POINT_Y[0] = 0;
+ POINT_X[POINTS-1] = 1;
+ POINT_Y[POINTS-1] = 0;
+ }
+
+
+ private double height = 0.05;
+
+ public EllipticalFinSet() {
+ this.length = 0.05;
+ }
+
+
+ @Override
+ public Coordinate[] getFinPoints() {
+ Coordinate[] points = new Coordinate[POINTS];
+ for (int i=0; i < POINTS; i++) {
+ points[i] = new Coordinate(POINT_X[i]*length, POINT_Y[i]*height);
+ }
+ return points;
+ }
+
+ @Override
+ public double getSpan() {
+ return height;
+ }
+
+ @Override
+ public String getComponentName() {
+ return "Elliptical fin set";
+ }
+
+
+ public double getHeight() {
+ return height;
+ }
+
+ public void setHeight(double height) {
+ if (MathUtil.equals(this.height, height))
+ return;
+ this.height = height;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+ public void setLength(double length) {
+ if (MathUtil.equals(this.length, length))
+ return;
+ this.length = length;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+
+public class EngineBlock extends ThicknessRingComponent {
+
+ public EngineBlock() {
+ super();
+ setOuterRadiusAutomatic(true);
+ setThickness(0.005);
+ setLength(0.005);
+ }
+
+ @Override
+ public void setOuterRadiusAutomatic(boolean auto) {
+ super.setOuterRadiusAutomatic(auto);
+ }
+
+ @Override
+ public String getComponentName() {
+ return "Engine block";
+ }
+
+ @Override
+ public boolean isCompatible(Class<? extends RocketComponent> type) {
+ return false;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.Prefs;
+
+/**
+ * Class of components with well-defined physical appearance and which have an effect on
+ * aerodynamic simulation. They have material defined for them, and the mass of the component
+ * is calculated using the component's volume.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public abstract class ExternalComponent extends RocketComponent {
+
+ public enum Finish {
+ ROUGH("Rough", 500e-6),
+ UNFINISHED("Unfinished", 150e-6),
+ NORMAL("Regular paint", 60e-6),
+ SMOOTH("Smooth paint", 20e-6),
+ POLISHED("Polished", 2e-6);
+
+ private final String name;
+ private final double roughnessSize;
+
+ Finish(String name, double roughness) {
+ this.name = name;
+ this.roughnessSize = roughness;
+ }
+
+ public double getRoughnessSize() {
+ return roughnessSize;
+ }
+
+ @Override
+ public String toString() {
+ return name + " (" + UnitGroup.UNITS_ROUGHNESS.toStringUnit(roughnessSize) + ")";
+ }
+ }
+
+
+ /**
+ * The material of the component.
+ */
+ protected Material material=null;
+
+ protected Finish finish = Finish.NORMAL;
+
+
+
+ /**
+ * Constructor that sets the relative position of the component.
+ */
+ public ExternalComponent(RocketComponent.Position relativePosition) {
+ super(relativePosition);
+ this.material = Prefs.getDefaultComponentMaterial(this.getClass(), Material.Type.BULK);
+ }
+
+ /**
+ * Returns the volume of the component. This value is used in calculating the mass
+ * of the object.
+ */
+ public abstract double getComponentVolume();
+
+ /**
+ * Calculates the mass of the component as the product of the volume and interior density.
+ */
+ @Override
+ public double getComponentMass() {
+ return material.getDensity() * getComponentVolume();
+ }
+
+ /**
+ * ExternalComponent has aerodynamic effect, so return true.
+ */
+ @Override
+ public boolean isAerodynamic() {
+ return true;
+ }
+
+ /**
+ * ExternalComponent has effect on the mass, so return true.
+ */
+ @Override
+ public boolean isMassive() {
+ return true;
+ }
+
+
+ public Material getMaterial() {
+ return material;
+ }
+
+ public void setMaterial(Material mat) {
+ if (mat.getType() != Material.Type.BULK) {
+ throw new IllegalArgumentException("ExternalComponent requires a bulk material" +
+ " type="+mat.getType());
+ }
+
+ if (material.equals(mat))
+ return;
+ material = mat;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+ public Finish getFinish() {
+ return finish;
+ }
+
+ public void setFinish(Finish finish) {
+ if (this.finish == finish)
+ return;
+ this.finish = finish;
+ fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE);
+ }
+
+
+
+ @Override
+ protected void copyFrom(RocketComponent c) {
+ super.copyFrom(c);
+
+ ExternalComponent src = (ExternalComponent)c;
+ this.material = src.material;
+ this.finish = src.finish;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Transformation;
+
+
+public abstract class FinSet extends ExternalComponent {
+
+ /**
+ * Maximum allowed cant of fins.
+ */
+ public static final double MAX_CANT = (15.0 * Math.PI / 180);
+
+
+ public enum CrossSection {
+ SQUARE("Square", 1.00),
+ ROUNDED("Rounded", 0.99),
+ AIRFOIL("Airfoil", 0.85);
+
+ private final String name;
+ private final double volume;
+ CrossSection(String name, double volume) {
+ this.name = name;
+ this.volume = volume;
+ }
+
+ public double getRelativeVolume() {
+ return volume;
+ }
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+
+ /**
+ * Number of fins.
+ */
+ protected int fins = 3;
+
+ /**
+ * Rotation about the x-axis by 2*PI/fins.
+ */
+ protected Transformation finRotation = Transformation.rotate_x(2*Math.PI/fins);
+
+ /**
+ * Rotation angle of the first fin. Zero corresponds to the positive y-axis.
+ */
+ protected double rotation = 0;
+
+ /**
+ * Rotation about the x-axis by angle this.rotation.
+ */
+ protected Transformation baseRotation = Transformation.rotate_x(rotation);
+
+
+ /**
+ * Cant angle of fins.
+ */
+ protected double cantAngle = 0;
+
+ /* Cached value: */
+ private Transformation cantRotation = null;
+
+
+ /**
+ * Thickness of the fins.
+ */
+ protected double thickness = 0;
+
+
+ /**
+ * The cross-section shape of the fins.
+ */
+ protected CrossSection crossSection = CrossSection.SQUARE;
+
+
+ // Cached fin area & CG. Validity of both must be checked using finArea!
+ private double finArea = -1;
+ private double finCGx = -1;
+ private double finCGy = -1;
+
+
+ /**
+ * New FinSet with given number of fins and given base rotation angle.
+ * Sets the component relative position to POSITION_RELATIVE_BOTTOM,
+ * i.e. fins are positioned at the bottom of the parent component.
+ */
+ public FinSet() {
+ super(RocketComponent.Position.BOTTOM);
+ }
+
+
+
+ /**
+ * Return the number of fins in the set.
+ * @return The number of fins.
+ */
+ public int getFinCount() {
+ return fins;
+ }
+
+ /**
+ * Sets the number of fins in the set.
+ * @param n The number of fins, greater of equal to one.
+ */
+ public void setFinCount(int n) {
+ if (fins == n)
+ return;
+ if (n < 1)
+ n = 1;
+ if (n > 8)
+ n = 8;
+ fins = n;
+ finRotation = Transformation.rotate_x(2*Math.PI/fins);
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+ public Transformation getFinRotationTransformation() {
+ return finRotation;
+ }
+
+ /**
+ * Gets the base rotation amount of the first fin.
+ * @return The base rotation amount.
+ */
+ public double getBaseRotation() {
+ return rotation;
+ }
+
+ /**
+ * Sets the base rotation amount of the first fin.
+ * @param r The base rotation amount.
+ */
+ public void setBaseRotation(double r) {
+ r = MathUtil.reduce180(r);
+ if (MathUtil.equals(r, rotation))
+ return;
+ rotation = r;
+ baseRotation = Transformation.rotate_x(rotation);
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+ public Transformation getBaseRotationTransformation() {
+ return baseRotation;
+ }
+
+
+
+ public double getCantAngle() {
+ return cantAngle;
+ }
+
+ public void setCantAngle(double cant) {
+ cant = MathUtil.clamp(cant, -MAX_CANT, MAX_CANT);
+ if (MathUtil.equals(cant, cantAngle))
+ return;
+ this.cantAngle = cant;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+ public Transformation getCantRotation() {
+ if (cantRotation == null) {
+ if (MathUtil.equals(cantAngle,0)) {
+ cantRotation = Transformation.IDENTITY;
+ } else {
+ Transformation t = new Transformation(-length/2,0,0);
+ t = Transformation.rotate_y(cantAngle).applyTransformation(t);
+ t = new Transformation(length/2,0,0).applyTransformation(t);
+ cantRotation = t;
+ }
+ }
+ return cantRotation;
+ }
+
+
+
+ public double getThickness() {
+ return thickness;
+ }
+
+ public void setThickness(double r) {
+ if (thickness == r)
+ return;
+ thickness = Math.max(r,0);
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+ public CrossSection getCrossSection() {
+ return crossSection;
+ }
+
+ public void setCrossSection(CrossSection cs) {
+ if (crossSection == cs)
+ return;
+ crossSection = cs;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+
+
+
+ @Override
+ public void setRelativePosition(RocketComponent.Position position) {
+ super.setRelativePosition(position);
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+ @Override
+ public void setPositionValue(double value) {
+ super.setPositionValue(value);
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+
+
+
+
+ /////////// Calculation methods ///////////
+
+ /**
+ * Return the area of one side of one fin.
+ *
+ * @return the area of one side of one fin.
+ */
+ public double getFinArea() {
+ if (finArea < 0)
+ calculateAreaCG();
+
+ return finArea;
+ }
+
+ /**
+ * Return the unweighted CG of a single fin. The X-coordinate is relative to
+ * the root chord trailing edge and the Y-coordinate to the fin root chord.
+ *
+ * @return the unweighted CG coordinate of a single fin.
+ */
+ public Coordinate getFinCG() {
+ if (finArea < 0)
+ calculateAreaCG();
+
+ return new Coordinate(finCGx,finCGy,0);
+ }
+
+
+
+ @Override
+ public double getComponentVolume() {
+ return fins * getFinArea() * thickness * crossSection.getRelativeVolume();
+ }
+
+
+ @Override
+ public Coordinate getComponentCG() {
+ if (finArea < 0)
+ calculateAreaCG();
+
+ double mass = getComponentMass(); // safe
+
+ if (fins == 1) {
+ return baseRotation.transform(
+ new Coordinate(finCGx,finCGy + getBodyRadius(), 0, mass));
+ } else {
+ return new Coordinate(finCGx, 0, 0, mass);
+ }
+ }
+
+
+ private void calculateAreaCG() {
+ Coordinate[] points = this.getFinPoints();
+ finArea = 0;
+ finCGx = 0;
+ finCGy = 0;
+
+ for (int i=0; i < points.length-1; i++) {
+ final double x0 = points[i].x;
+ final double x1 = points[i+1].x;
+ final double y0 = points[i].y;
+ final double y1 = points[i+1].y;
+
+ double da = (y0+y1)*(x1-x0) / 2;
+ finArea += da;
+ if (Math.abs(y0-y1) < 0.00001) {
+ finCGx += (x0+x1)/2 * da;
+ finCGy += y0/2 * da;
+ } else {
+ finCGx += (x0*(2*y0 + y1) + x1*(y0 + 2*y1)) / (3*(y0 + y1)) * da;
+ finCGy += (y1 + y0*y0/(y0 + y1))/3 * da;
+ }
+ }
+
+ if (finArea < 0)
+ finArea = 0;
+
+ if (finArea > 0) {
+ finCGx /= finArea;
+ finCGy /= finArea;
+ } else {
+ finCGx = (points[0].x + points[points.length-1].x)/2;
+ finCGy = 0;
+ }
+ }
+
+
+ /*
+ * Return an approximation of the longitudal unitary inertia of the fin set.
+ * The process is the following:
+ *
+ * 1. Approximate the fin with a rectangular fin
+ *
+ * 2. The inertia of one fin is taken as the average of the moments of inertia
+ * through its center perpendicular to the plane, and the inertia through
+ * its center parallel to the plane
+ *
+ * 3. If there are multiple fins, the inertia is shifted to the center of the fin
+ * set and multiplied by the number of fins.
+ */
+ @Override
+ public double getLongitudalUnitInertia() {
+ double area = getFinArea();
+ if (MathUtil.equals(area, 0))
+ return 0;
+
+ // Approximate fin with a rectangular fin
+ // w2 and h2 are squares of the fin width and height
+ double w = getLength();
+ double h = getSpan();
+ double w2,h2;
+
+ if (MathUtil.equals(w*h,0)) {
+ w2 = area;
+ h2 = area;
+ } else {
+ w2 = w*area/h;
+ h2 = h*area/w;
+ }
+
+ double inertia = (h2 + 2*w2)/24;
+
+ if (fins == 1)
+ return inertia;
+
+ double radius = getBodyRadius();
+
+ return fins * (inertia + MathUtil.pow2(Math.sqrt(h2) + radius));
+ }
+
+
+ /*
+ * Return an approximation of the rotational unitary inertia of the fin set.
+ * The process is the following:
+ *
+ * 1. Approximate the fin with a rectangular fin and calculate the inertia of the
+ * rectangular approximate
+ *
+ * 2. If there are multiple fins, shift the inertia center to the fin set center
+ * and multiply with the number of fins.
+ */
+ @Override
+ public double getRotationalUnitInertia() {
+ double area = getFinArea();
+ if (MathUtil.equals(area, 0))
+ return 0;
+
+ // Approximate fin with a rectangular fin
+ double w = getLength();
+ double h = getSpan();
+
+ if (MathUtil.equals(w*h,0)) {
+ h = Math.sqrt(area);
+ } else {
+ h = Math.sqrt(h*area/w);
+ }
+
+ if (fins == 1)
+ return h*h / 12;
+
+ double radius = getBodyRadius();
+
+ return fins * (h*h/12 + MathUtil.pow2(h/2 + radius));
+ }
+
+
+ /**
+ * Adds the fin set's bounds to the collection.
+ */
+ @Override
+ public Collection<Coordinate> getComponentBounds() {
+ List<Coordinate> bounds = new ArrayList<Coordinate>();
+ double r = getBodyRadius();
+
+ for (Coordinate point: getFinPoints()) {
+ addFinBound(bounds, point.x, point.y + r);
+ }
+
+ return bounds;
+ }
+
+
+ /**
+ * Adds the 2d-coordinate bound (x,y) to the collection for both z-components and for
+ * all fin rotations.
+ */
+ private void addFinBound(Collection<Coordinate> set, double x, double y) {
+ Coordinate c;
+ int i;
+
+ c = new Coordinate(x,y,thickness/2);
+ c = baseRotation.transform(c);
+ set.add(c);
+ for (i=1; i<fins; i++) {
+ c = finRotation.transform(c);
+ set.add(c);
+ }
+
+ c = new Coordinate(x,y,-thickness/2);
+ c = baseRotation.transform(c);
+ set.add(c);
+ for (i=1; i<fins; i++) {
+ c = finRotation.transform(c);
+ set.add(c);
+ }
+ }
+
+
+
+ @Override
+ public void componentChanged(ComponentChangeEvent e) {
+ if (e.isAerodynamicChange()) {
+ finArea = -1;
+ cantRotation = null;
+ }
+ }
+
+
+ /**
+ * Return the radius of the BodyComponent the fin set is situated on. Currently
+ * only supports SymmetricComponents and returns the radius at the starting point of the
+ * root chord.
+ *
+ * @return radius of the underlying BodyComponent or 0 if none exists.
+ */
+ public double getBodyRadius() {
+ RocketComponent s;
+
+ s = this.getParent();
+ while (s!=null) {
+ if (s instanceof SymmetricComponent) {
+ double x = this.toRelative(new Coordinate(0,0,0), s)[0].x;
+ return ((SymmetricComponent)s).getRadius(x);
+ }
+ s = s.getParent();
+ }
+ return 0;
+ }
+
+ /**
+ * Allows nothing to be attached to a FinSet.
+ *
+ * @return <code>false</code>
+ */
+ @Override
+ public boolean isCompatible(Class<? extends RocketComponent> type) {
+ return false;
+ }
+
+
+
+
+ /**
+ * Return a list of coordinates defining the geometry of a single fin.
+ * The coordinates are the XY-coordinates of points defining the shape of a single fin,
+ * where the origin is the leading root edge. Therefore, the first point must be (0,0,0).
+ * All Z-coordinates must be zero, and the last coordinate must have Y=0.
+ *
+ * @return List of XY-coordinates.
+ */
+ public abstract Coordinate[] getFinPoints();
+
+ /**
+ * Get the span of a single fin. That is, the length from the root to the tip of the fin.
+ * @return Span of a single fin.
+ */
+ public abstract double getSpan();
+
+
+ @Override
+ protected void copyFrom(RocketComponent c) {
+ super.copyFrom(c);
+
+ FinSet src = (FinSet)c;
+ this.fins = src.fins;
+ this.finRotation = src.finRotation;
+ this.rotation = src.rotation;
+ this.baseRotation = src.baseRotation;
+ this.cantAngle = src.cantAngle;
+ this.cantRotation = src.cantRotation;
+ this.thickness = src.thickness;
+ this.crossSection = src.crossSection;
+ }
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.util.Coordinate;
+
+
+public class FreeformFinSet extends FinSet {
+
+ private final List<Coordinate> points = new ArrayList<Coordinate>();
+
+ public FreeformFinSet() {
+ points.add(Coordinate.NUL);
+ points.add(new Coordinate(0.025,0.05));
+ points.add(new Coordinate(0.075,0.05));
+ points.add(new Coordinate(0.05,0));
+
+ this.length = 0.05;
+ }
+
+
+ public FreeformFinSet(FinSet finset) {
+ Coordinate[] finpoints = finset.getFinPoints();
+ this.copyFrom(finset);
+
+ points.clear();
+ for (Coordinate c: finpoints) {
+ points.add(c);
+ }
+ this.length = points.get(points.size()-1).x - points.get(0).x;
+ }
+
+
+
+ /**
+ * Add a fin point between indices <code>index-1</code> and <code>index</code>.
+ * The point is placed at the midpoint of the current segment.
+ *
+ * @param index the fin point before which to add the new point.
+ */
+ public void addPoint(int index) {
+ double x0, y0, x1, y1;
+
+ x0 = points.get(index-1).x;
+ y0 = points.get(index-1).y;
+ x1 = points.get(index).x;
+ y1 = points.get(index).y;
+
+ points.add(index, new Coordinate((x0+x1)/2, (y0+y1)/2));
+ // adding a point within the segment affects neither mass nor aerodynamics
+ fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+ }
+
+
+ /**
+ * Remove the fin point with the given index. The first and last fin points
+ * cannot be removed, and will cause an <code>IllegalArgumentException</code>
+ * if attempted.
+ *
+ * @param index the fin point index to remove
+ */
+ public void removePoint(int index) {
+ if (index == 0 || index == points.size()-1) {
+ throw new IllegalArgumentException("cannot remove first or last point");
+ }
+ points.remove(index);
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+ public int getPointCount() {
+ return points.size();
+ }
+
+ public void setPoints(Coordinate[] p) {
+ if (p[0].x != 0 || p[0].y != 0 || p[p.length-1].y != 0) {
+ throw new IllegalArgumentException("Start or end point illegal.");
+ }
+ for (int i=0; i < p.length-1; i++) {
+ for (int j=i+2; j < p.length-1; j++) {
+ if (intersects(p[i].x, p[i].y, p[i+1].x, p[i+1].y,
+ p[j].x, p[j].y, p[j+1].x, p[j+1].y)) {
+ throw new IllegalArgumentException("segments intersect");
+ }
+ }
+ if (p[i].z != 0) {
+ throw new IllegalArgumentException("z-coordinate not zero");
+ }
+ }
+
+ points.clear();
+ for (Coordinate c: p) {
+ points.add(c);
+ }
+ this.length = p[p.length-1].x;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+ /**
+ * Set the point at position <code>i</code> to coordinates (x,y).
+ * <p>
+ * Note that this method enforces basic fin shape restrictions (non-negative y,
+ * first and last point locations) silently, but throws an
+ * <code>IllegalArgumentException</code> if the point causes fin segments to
+ * intersect. The calling method should always catch this exception.
+ * <p>
+ * Moving of the first point in the X-axis is allowed, but this actually moves
+ * all of the other points the corresponding distance back.
+ *
+ * @param index the point index to modify.
+ * @param x the x-coordinate.
+ * @param y the y-coordinate.
+ */
+ public void setPoint(int index, double x, double y) {
+ if (y < 0)
+ y = 0;
+
+ double x0,y0,x1,y1;
+
+ if (index == 0) {
+
+ // Restrict point
+ x = Math.min(x, points.get(points.size()-1).x);
+ y = 0;
+ x0 = Double.NaN;
+ y0 = Double.NaN;
+ x1 = points.get(1).x;
+ y1 = points.get(1).y;
+
+ } else if (index == points.size()-1) {
+
+ // Restrict point
+ x = Math.max(x, 0);
+ y = 0;
+ x0 = points.get(index-1).x;
+ y0 = points.get(index-1).y;
+ x1 = Double.NaN;
+ y1 = Double.NaN;
+
+ } else {
+
+ x0 = points.get(index-1).x;
+ y0 = points.get(index-1).y;
+ x1 = points.get(index+1).x;
+ y1 = points.get(index+1).y;
+
+ }
+
+
+
+ // Check for intersecting
+ double px0, py0, px1, py1;
+ px0 = 0;
+ py0 = 0;
+ for (int i=1; i < points.size(); i++) {
+ px1 = points.get(i).x;
+ py1 = points.get(i).y;
+
+ if (i != index-1 && i != index && i != index+1) {
+ if (intersects(x0,y0,x,y,px0,py0,px1,py1)) {
+ throw new IllegalArgumentException("segments intersect");
+ }
+ }
+ if (i != index && i != index+1 && i != index+2) {
+ if (intersects(x,y,x1,y1,px0,py0,px1,py1)) {
+ throw new IllegalArgumentException("segments intersect");
+ }
+ }
+
+ px0 = px1;
+ py0 = py1;
+ }
+
+ if (index == 0) {
+
+ System.out.println("Set point zero to x:"+x);
+ for (int i=1; i < points.size(); i++) {
+ Coordinate c = points.get(i);
+ points.set(i, c.setX(c.x - x));
+ }
+
+ } else {
+
+ points.set(index,new Coordinate(x,y));
+
+ }
+ if (index == 0 || index == points.size()-1) {
+ this.length = points.get(points.size()-1).x;
+ }
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+
+ private boolean intersects(double ax0, double ay0, double ax1, double ay1,
+ double bx0, double by0, double bx1, double by1) {
+
+ double d = ((by1-by0)*(ax1-ax0) - (bx1-bx0)*(ay1-ay0));
+
+ double ua = ((bx1-bx0)*(ay0-by0) - (by1-by0)*(ax0-bx0)) / d;
+ double ub = ((ax1-ax0)*(ay0-by0) - (ay1-ay0)*(ax0-bx0)) / d;
+
+ return (ua >= 0) && (ua <= 1) && (ub >= 0) && (ub <= 1);
+ }
+
+
+ @Override
+ public Coordinate[] getFinPoints() {
+ return points.toArray(new Coordinate[0]);
+ }
+
+ @Override
+ public double getSpan() {
+ double max = 0;
+ for (Coordinate c: points) {
+ if (c.y > max)
+ max = c.y;
+ }
+ return max;
+ }
+
+ @Override
+ public String getComponentName() {
+ return "Freeform fin set";
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+/**
+ * This class defines an inner tube that can be used as a motor mount. The component
+ * may also be clustered.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class InnerTube extends ThicknessRingComponent
+implements Clusterable, RadialParent, MotorMount {
+
+ private ClusterConfiguration cluster = ClusterConfiguration.SINGLE;
+ private double clusterScale = 1.0;
+ private double clusterRotation = 0.0;
+
+
+ private boolean motorMount = false;
+ private HashMap<String, Double> ejectionDelays = new HashMap<String, Double>();
+ private HashMap<String, Motor> motors = new HashMap<String, Motor>();
+ private IgnitionEvent ignitionEvent = IgnitionEvent.AUTOMATIC;
+ private double ignitionDelay = 0;
+ private double overhang = 0;
+
+
+ /**
+ * Main constructor.
+ */
+ public InnerTube() {
+ // A-C motor size:
+ this.setOuterRadius(0.019/2);
+ this.setInnerRadius(0.018/2);
+ this.setLength(0.070);
+ }
+
+
+ @Override
+ public double getInnerRadius(double x) {
+ return getInnerRadius();
+ }
+
+
+ @Override
+ public double getOuterRadius(double x) {
+ return getOuterRadius();
+ }
+
+
+ @Override
+ public String getComponentName() {
+ return "Inner Tube";
+ }
+
+ /**
+ * Allow all InternalComponents to be added to this component.
+ */
+ @Override
+ public boolean isCompatible(Class<? extends RocketComponent> type) {
+ return InternalComponent.class.isAssignableFrom(type);
+ }
+
+
+
+ ///////////// Cluster methods //////////////
+
+ /**
+ * Get the current cluster configuration.
+ * @return The current cluster configuration.
+ */
+ public ClusterConfiguration getClusterConfiguration() {
+ return cluster;
+ }
+
+ /**
+ * Set the current cluster configuration.
+ * @param cluster The cluster configuration.
+ */
+ public void setClusterConfiguration(ClusterConfiguration cluster) {
+ this.cluster = cluster;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+ /**
+ * Return the number of tubes in the cluster.
+ * @return Number of tubes in the current cluster.
+ */
+ @Override
+ public int getClusterCount() {
+ return cluster.getClusterCount();
+ }
+
+ /**
+ * Get the cluster scaling. A value of 1.0 indicates that the tubes are packed
+ * touching each other, larger values separate the tubes and smaller values
+ * pack inside each other.
+ */
+ public double getClusterScale() {
+ return clusterScale;
+ }
+
+ /**
+ * Set the cluster scaling.
+ * @see #getClusterScale()
+ */
+ public void setClusterScale(double scale) {
+ scale = Math.max(scale,0);
+ if (MathUtil.equals(clusterScale, scale))
+ return;
+ clusterScale = scale;
+ fireComponentChangeEvent(new ComponentChangeEvent(this,ComponentChangeEvent.MASS_CHANGE));
+ }
+
+
+
+ /**
+ * @return the clusterRotation
+ */
+ public double getClusterRotation() {
+ return clusterRotation;
+ }
+
+
+ /**
+ * @param rotation the clusterRotation to set
+ */
+ public void setClusterRotation(double rotation) {
+ rotation = MathUtil.reduce180(rotation);
+ if (clusterRotation == rotation)
+ return;
+ this.clusterRotation = rotation;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+ @Override
+ public double getClusterSeparation() {
+ return 2*getOuterRadius()*clusterScale;
+ }
+
+
+ public List<Coordinate> getClusterPoints() {
+ List<Coordinate> list = new ArrayList<Coordinate>(getClusterCount());
+ List<Double> points = cluster.getPoints(clusterRotation - getRadialDirection());
+ double separation = getClusterSeparation();
+ for (int i=0; i < points.size()/2; i++) {
+ list.add(new Coordinate(0,points.get(2*i)*separation,points.get(2*i+1)*separation));
+ }
+ return list;
+ }
+
+
+ @Override
+ public Coordinate[] shiftCoordinates(Coordinate[] array) {
+ array = super.shiftCoordinates(array);
+
+ int count = getClusterCount();
+ if (count == 1)
+ return array;
+
+ List<Coordinate> points = getClusterPoints();
+ assert(points.size() == count);
+ Coordinate[] newArray = new Coordinate[array.length * count];
+ for (int i=0; i < array.length; i++) {
+ for (int j=0; j < count; j++) {
+ newArray[i*count + j] = array[i].add(points.get(j));
+ }
+ }
+
+ return newArray;
+ }
+
+
+
+
+ //////////////// Motor mount /////////////////
+
+ @Override
+ public boolean isMotorMount() {
+ return motorMount;
+ }
+
+ @Override
+ public void setMotorMount(boolean mount) {
+ if (motorMount == mount)
+ return;
+ motorMount = mount;
+ fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
+ }
+
+ @Override
+ public Motor getMotor(String id) {
+ return motors.get(id);
+ }
+
+ @Override
+ public void setMotor(String id, Motor motor) {
+ Motor current = motors.get(id);
+ if ((motor == null && current == null) ||
+ (motor != null && motor.equals(current)))
+ return;
+ motors.put(id, motor);
+ fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
+ }
+
+ @Override
+ public double getMotorDelay(String id) {
+ Double delay = ejectionDelays.get(id);
+ if (delay == null)
+ return Motor.PLUGGED;
+ return delay;
+ }
+
+ @Override
+ public void setMotorDelay(String id, double delay) {
+ ejectionDelays.put(id, delay);
+ fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
+ }
+
+ @Override
+ public int getMotorCount() {
+ return getClusterCount();
+ }
+
+ @Override
+ public double getMotorMountDiameter() {
+ return getInnerRadius()*2;
+ }
+
+ @Override
+ public IgnitionEvent getIgnitionEvent() {
+ return ignitionEvent;
+ }
+
+ @Override
+ public void setIgnitionEvent(IgnitionEvent event) {
+ if (ignitionEvent == event)
+ return;
+ ignitionEvent = event;
+ fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
+ }
+
+
+ @Override
+ public double getIgnitionDelay() {
+ return ignitionDelay;
+ }
+
+ @Override
+ public void setIgnitionDelay(double delay) {
+ if (MathUtil.equals(delay, ignitionDelay))
+ return;
+ ignitionDelay = delay;
+ fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
+ }
+
+
+ @Override
+ public double getMotorOverhang() {
+ return overhang;
+ }
+
+ @Override
+ public void setMotorOverhang(double overhang) {
+ if (MathUtil.equals(this.overhang, overhang))
+ return;
+ this.overhang = overhang;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+}
\ No newline at end of file
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+
+/**
+ * A component internal to the rocket. Internal components have no effect on the
+ * the aerodynamics of a rocket, only its mass properties (though the location of the
+ * components is not enforced to be within external components). Internal components
+ * are always attached relative to the parent component, which can be internal or
+ * external, or absolutely.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class InternalComponent extends RocketComponent {
+
+ public InternalComponent() {
+ super(RocketComponent.Position.BOTTOM);
+ }
+
+
+ @Override
+ public final void setRelativePosition(RocketComponent.Position position) {
+ super.setRelativePosition(position);
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+ @Override
+ public final void setPositionValue(double value) {
+ super.setPositionValue(value);
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+ /**
+ * Non-aerodynamic components.
+ * @return <code>false</code>
+ */
+ @Override
+ public final boolean isAerodynamic() {
+ return false;
+ }
+
+ /**
+ * Is massive.
+ * @return <code>true</code>
+ */
+ @Override
+ public final boolean isMassive() {
+ return true;
+ }
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+public class LaunchLug extends ExternalComponent {
+
+ private double radius;
+ private double thickness;
+
+ private double radialDirection = 0;
+
+ /* These are calculated when the component is first attached to any Rocket */
+ private double shiftY, shiftZ;
+
+
+
+ public LaunchLug() {
+ super(Position.MIDDLE);
+ radius = 0.01/2;
+ thickness = 0.001;
+ length = 0.03;
+ }
+
+
+ public double getRadius() {
+ return radius;
+ }
+
+ public void setRadius(double radius) {
+ if (MathUtil.equals(this.radius, radius))
+ return;
+ this.radius = radius;
+ this.thickness = Math.min(this.thickness, this.radius);
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+ public double getInnerRadius() {
+ return radius-thickness;
+ }
+
+ public void setInnerRadius(double innerRadius) {
+ setRadius(innerRadius + thickness);
+ }
+
+ public double getThickness() {
+ return thickness;
+ }
+
+ public void setThickness(double thickness) {
+ if (MathUtil.equals(this.thickness, thickness))
+ return;
+ this.thickness = MathUtil.clamp(thickness, 0, radius);
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+ public double getRadialDirection() {
+ return radialDirection;
+ }
+
+ public void setRadialDirection(double direction) {
+ direction = MathUtil.reduce180(direction);
+ if (MathUtil.equals(this.radialDirection, direction))
+ return;
+ this.radialDirection = direction;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+
+ public void setLength(double length) {
+ if (MathUtil.equals(this.length, length))
+ return;
+ this.length = length;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+
+
+
+
+ @Override
+ public void setRelativePosition(RocketComponent.Position position) {
+ super.setRelativePosition(position);
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+ @Override
+ public void setPositionValue(double value) {
+ super.setPositionValue(value);
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+
+ @Override
+ public Coordinate[] shiftCoordinates(Coordinate[] array) {
+ array = super.shiftCoordinates(array);
+
+ for (int i=0; i < array.length; i++) {
+ array[i] = array[i].add(0, shiftY, shiftZ);
+ }
+
+ return array;
+ }
+
+
+ @Override
+ public void componentChanged(ComponentChangeEvent e) {
+ super.componentChanged(e);
+
+ /*
+ * shiftY and shiftZ must be computed here since calculating them
+ * in shiftCoordinates() would cause an infinite loop due to .toRelative
+ */
+ RocketComponent body;
+ double parentRadius;
+
+ for (body = this.getParent(); body != null; body = body.getParent()) {
+ if (body instanceof SymmetricComponent)
+ break;
+ }
+
+ if (body == null) {
+ parentRadius = 0;
+ } else {
+ SymmetricComponent s = (SymmetricComponent)body;
+ double x1, x2;
+ x1 = this.toRelative(Coordinate.NUL, body)[0].x;
+ x2 = this.toRelative(new Coordinate(length,0,0), body)[0].x;
+ x1 = MathUtil.clamp(x1, 0, body.getLength());
+ x2 = MathUtil.clamp(x2, 0, body.getLength());
+ parentRadius = Math.max(s.getRadius(x1), s.getRadius(x2));
+ }
+
+ shiftY = Math.cos(radialDirection) * (parentRadius + radius);
+ shiftZ = Math.sin(radialDirection) * (parentRadius + radius);
+
+// System.out.println("Computed shift: y="+shiftY+" z="+shiftZ);
+}
+
+
+
+
+ @Override
+ public double getComponentVolume() {
+ return length * Math.PI * (MathUtil.pow2(radius) - MathUtil.pow2(radius-thickness));
+ }
+
+ @Override
+ public Collection<Coordinate> getComponentBounds() {
+ ArrayList<Coordinate> set = new ArrayList<Coordinate>();
+ addBound(set, 0, radius);
+ addBound(set, length, radius);
+ return set;
+ }
+
+ @Override
+ public Coordinate getComponentCG() {
+ return new Coordinate(length/2, 0, 0, getComponentMass());
+ }
+
+ @Override
+ public String getComponentName() {
+ return "Launch lug";
+ }
+
+ @Override
+ public double getLongitudalUnitInertia() {
+ // 1/12 * (3 * (r1^2 + r2^2) + h^2)
+ return (3 * (MathUtil.pow2(getInnerRadius())) + MathUtil.pow2(getRadius()) +
+ MathUtil.pow2(getLength())) / 12;
+ }
+
+ @Override
+ public double getRotationalUnitInertia() {
+ // 1/2 * (r1^2 + r2^2)
+ return (MathUtil.pow2(getInnerRadius()) + MathUtil.pow2(getRadius()))/2;
+ }
+
+
+ @Override
+ public boolean isCompatible(Class<? extends RocketComponent> type) {
+ // Allow nothing to be attached to a LaunchLug
+ return false;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.util.MathUtil;
+
+public class MassComponent extends MassObject {
+ private double mass = 0;
+
+
+ public MassComponent() {
+ super();
+ }
+
+ public MassComponent(double length, double radius, double mass) {
+ super(length, radius);
+ this.mass = mass;
+ }
+
+
+ @Override
+ public double getComponentMass() {
+ return mass;
+ }
+
+ public void setComponentMass(double mass) {
+ mass = Math.max(mass, 0);
+ if (MathUtil.equals(this.mass, mass))
+ return;
+ this.mass = mass;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+ @Override
+ public String getComponentName() {
+ return "Mass component";
+ }
+
+
+ @Override
+ public boolean isCompatible(Class<? extends RocketComponent> type) {
+ // Allow no components to be attached to a MassComponent
+ return false;
+ }
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import static net.sf.openrocket.util.MathUtil.pow2;
+
+import java.util.ArrayList;
+import java.util.Collection;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+
+/**
+ * A MassObject is an internal component that can a specific weight, but not
+ * necessarily a strictly bound shape. It is represented as a homogeneous
+ * cylinder and drawn in the rocket figure with rounded corners.
+ * <p>
+ * Subclasses of this class need only implement the {@link #getComponentMass()},
+ * {@link #getComponentName()} and {@link #isCompatible(RocketComponent)}
+ * methods.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class MassObject extends InternalComponent {
+
+ private double radius;
+
+ private double radialPosition;
+ private double radialDirection;
+
+ private double shiftY = 0;
+ private double shiftZ = 0;
+
+
+ public MassObject() {
+ this(0.03, 0.015);
+ }
+
+ public MassObject(double length, double radius) {
+ super();
+
+ this.length = length;
+ this.radius = radius;
+
+ this.setRelativePosition(Position.TOP);
+ this.setPositionValue(0.0);
+ }
+
+
+
+
+ public void setLength(double length) {
+ length = Math.max(length, 0);
+ if (MathUtil.equals(this.length, length))
+ return;
+ this.length = length;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+
+ public final double getRadius() {
+ return radius;
+ }
+
+
+ public final void setRadius(double radius) {
+ radius = Math.max(radius, 0);
+ if (MathUtil.equals(this.radius, radius))
+ return;
+ this.radius = radius;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+
+ public final double getRadialPosition() {
+ return radialPosition;
+ }
+
+ public final void setRadialPosition(double radialPosition) {
+ radialPosition = Math.max(radialPosition, 0);
+ if (MathUtil.equals(this.radialPosition, radialPosition))
+ return;
+ this.radialPosition = radialPosition;
+ shiftY = radialPosition * Math.cos(radialDirection);
+ shiftZ = radialPosition * Math.sin(radialDirection);
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+ public final double getRadialDirection() {
+ return radialDirection;
+ }
+
+ public final void setRadialDirection(double radialDirection) {
+ radialDirection = MathUtil.reduce180(radialDirection);
+ if (MathUtil.equals(this.radialDirection, radialDirection))
+ return;
+ this.radialDirection = radialDirection;
+ shiftY = radialPosition * Math.cos(radialDirection);
+ shiftZ = radialPosition * Math.sin(radialDirection);
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+
+
+ /**
+ * Shift the coordinates according to the radial position and direction.
+ */
+ @Override
+ public final Coordinate[] shiftCoordinates(Coordinate[] array) {
+ for (int i=0; i < array.length; i++) {
+ array[i] = array[i].add(0, shiftY, shiftZ);
+ }
+ return array;
+ }
+
+ @Override
+ public final Coordinate getComponentCG() {
+ return new Coordinate(length/2, shiftY, shiftZ, getComponentMass());
+ }
+
+ @Override
+ public final double getLongitudalUnitInertia() {
+ return (3*pow2(radius) + pow2(length)) / 12;
+ }
+
+ @Override
+ public final double getRotationalUnitInertia() {
+ return pow2(radius) / 2;
+ }
+
+ @Override
+ public final Collection<Coordinate> getComponentBounds() {
+ Collection<Coordinate> c = new ArrayList<Coordinate>();
+ addBound(c, 0, radius);
+ addBound(c, length, radius);
+ return c;
+ }
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import java.text.Collator;
+import java.util.Comparator;
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+
+/**
+ * Abstract base class for motors. The methods that must be implemented are
+ * {@link #getTotalTime()}, {@link #getThrust(double)} and {@link #getCG(double)}.
+ * Additionally the method {@link #getMaxThrust()} may be overridden for efficiency.
+ * <p>
+ *
+ * NOTE: The current implementation of {@link #getAverageTime()} and
+ * {@link #getAverageThrust()} assume that the class is immutable!
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class Motor implements Comparable<Motor> {
+
+ /**
+ * Enum of rocket motor types.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+ public enum Type {
+ SINGLE("Single-use", "Single-use solid propellant motor"),
+ RELOAD("Reloadable", "Reloadable solid propellant motor"),
+ HYBRID("Hybrid", "Hybrid rocket motor engine"),
+ UNKNOWN("Unknown", "Unknown motor type");
+
+ private final String name;
+ private final String description;
+
+ Type(String name, String description) {
+ this.name = name;
+ this.description = description;
+ }
+
+ /**
+ * Return a short name of this motor type.
+ * @return a short name of the motor type.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Return a long description of this motor type.
+ * @return a description of the motor type.
+ */
+ public String getDescription() {
+ return description;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+
+
+ /**
+ * Ejection charge delay value signifying a "plugged" motor with no ejection charge.
+ * The value is that of <code>Double.POSITIVE_INFINITY</code>.
+ */
+ public static final double PLUGGED = Double.POSITIVE_INFINITY;
+
+
+ /**
+ * Below what portion of maximum thrust is the motor chosen to be off when
+ * calculating average thrust and burn time.double
+ */
+ public static final double AVERAGE_MARGINAL = 0.05;
+
+ /* All data is cached, so divisions can be very tight. */
+ private static final int DIVISIONS = 1000;
+
+
+ // Comparators:
+ private static final Collator COLLATOR = Collator.getInstance(Locale.US);
+ static {
+ COLLATOR.setStrength(Collator.PRIMARY);
+ }
+ private static DesignationComparator DESIGNATION_COMPARATOR = new DesignationComparator();
+
+
+
+
+ private final String manufacturer;
+ private final String designation;
+ private final String description;
+ private final Type motorType;
+
+ private final double[] delays;
+
+ private final double diameter;
+ private final double length;
+
+ /* Cached data */
+ private double maxThrust = -1;
+ private double avgTime = -1;
+ private double avgThrust = -1;
+ private double totalImpulse = -1;
+
+
+
+ /**
+ * Sole constructor. None of the parameters may be <code>null</code>.
+ *
+ * @param manufacturer the manufacturer of the motor.
+ * @param designation the motor designation.
+ * @param description further description, including any comments on the origin
+ * of the thrust curve.
+ * @param delays an array of the standard ejection charge delays. A plugged
+ * motor (no ejection charge) is specified by a delay of
+ * {@link #PLUGGED} (<code>Double.POSITIVE_INFINITY</code>).
+ * @param diameter maximum diameter of the motor
+ * @param length length of the motor
+ */
+ protected Motor(String manufacturer, String designation, String description,
+ Type type, double[] delays, double diameter, double length) {
+
+ if (manufacturer == null || designation == null || description == null ||
+ type == null || delays == null) {
+ throw new IllegalArgumentException("Parameters cannot be null.");
+ }
+
+ this.manufacturer = manufacturer;
+ this.designation = designation;
+ this.description = description.trim();
+ this.motorType = type;
+ this.delays = delays.clone();
+ this.diameter = diameter;
+ this.length = length;
+ }
+
+
+
+ /**
+ * Return the total burn time of the motor. The method {@link #getThrust(double)}
+ * must return zero for time values greater than the return value.
+ *
+ * @return the total burn time of the motor.
+ */
+ public abstract double getTotalTime();
+
+
+ /**
+ * Return the thrust of the motor at the specified time.
+ *
+ * @param time time since the ignition of the motor.
+ * @return the thrust at the specified time.
+ */
+ public abstract double getThrust(double time);
+
+
+ /**
+ * Return the average thrust of the motor between times t1 and t2.
+ *
+ * @param t1 starting time since the ignition of the motor.
+ * @param t2 end time since the ignition of the motor.
+ * @return the average thrust during the time period.
+ */
+ /* TODO: MEDIUM: Implement better method in subclass */
+ public double getThrust(double t1, double t2) {
+ double f = 0;
+ f += getThrust(t1);
+ f += getThrust(0.8*t1 + 0.2*t2);
+ f += getThrust(0.6*t1 + 0.4*t2);
+ f += getThrust(0.4*t1 + 0.6*t2);
+ f += getThrust(0.2*t1 + 0.8*t2);
+ f += getThrust(t2);
+ return f/6;
+ }
+
+
+ /**
+ * Return the mass and CG of the motor at the specified time.
+ *
+ * @param time time since the ignition of the motor.
+ * @return the mass and CG of the motor.
+ */
+ public abstract Coordinate getCG(double time);
+
+
+
+ /**
+ * Return the mass of the motor at the specified time. The original mass
+ * of the motor can be queried by <code>getMass(0)</code> and the burnt mass
+ * by <code>getMass(Double.MAX_VALUE)</code>.
+ *
+ * @param time time since the ignition of the motor.
+ * @return the mass of the motor.
+ */
+ public double getMass(double time) {
+ return getCG(time).weight;
+ }
+
+
+ /**
+ * Return the longitudal moment of inertia of the motor at the specified time.
+ * This default method assumes that the mass of the motor is evenly distributed
+ * in a cylinder with the diameter and length of the motor.
+ *
+ * @param time time since the ignition of the motor.
+ * @return the longitudal moment of inertia of the motor.
+ */
+ public double getLongitudalInertia(double time) {
+ return getMass(time) * (3.0*MathUtil.pow2(diameter/2) + MathUtil.pow2(length))/12;
+ }
+
+
+
+ /**
+ * Return the rotational moment of inertia of the motor at the specified time.
+ * This default method assumes that the mass of the motor is evenly distributed
+ * in a cylinder with the diameter and length of the motor.
+ *
+ * @param time time since the ignition of the motor.
+ * @return the rotational moment of inertia of the motor.
+ */
+ public double getRotationalInertia(double time) {
+ return getMass(time) * MathUtil.pow2(diameter) / 8;
+ }
+
+
+
+
+ /**
+ * Return the maximum thrust. This implementation slices through the thrust curve
+ * searching for the maximum thrust. Subclasses may wish to override this with a
+ * more efficient method.
+ *
+ * @return the maximum thrust of the motor
+ */
+ public double getMaxThrust() {
+ if (maxThrust < 0) {
+ double time = getTotalTime();
+ maxThrust = 0;
+
+ for (int i=0; i < DIVISIONS; i++) {
+ double t = time * i / DIVISIONS;
+ double thrust = getThrust(t);
+
+ if (thrust > maxThrust)
+ maxThrust = thrust;
+ }
+ }
+ return maxThrust;
+ }
+
+
+ /**
+ * Return the time used in calculating the average thrust. The time is the
+ * length of time from motor ignition until the thrust has dropped below
+ * {@link #AVERAGE_MARGINAL} times the maximum thrust.
+ *
+ * @return the nominal burn time.
+ */
+ public double getAverageTime() {
+ // Compute average time lazily
+ if (avgTime < 0) {
+ double max = getMaxThrust();
+ double time = getTotalTime();
+
+ for (int i=DIVISIONS; i >= 0; i--) {
+ avgTime = time * i / DIVISIONS;
+ if (getThrust(avgTime) > max*AVERAGE_MARGINAL)
+ break;
+ }
+ }
+ return avgTime;
+ }
+
+
+ /**
+ * Return the calculated average thrust during time from ignition to
+ * {@link #getAverageTime()}.
+ *
+ * @return the nominal average thrust.
+ */
+ public double getAverageThrust() {
+ // Compute average thrust lazily
+ if (avgThrust < 0) {
+ double time = getAverageTime();
+
+ avgThrust = 0;
+ for (int i=0; i < DIVISIONS; i++) {
+ double t = time * i / DIVISIONS;
+ avgThrust += getThrust(t);
+ }
+ avgThrust /= DIVISIONS;
+ }
+ return avgThrust;
+ }
+
+
+ /**
+ * Return the total impulse of the motor. This is calculated from the entire
+ * burn time, and therefore may differ from the value of {@link #getAverageTime()}
+ * and {@link #getAverageThrust()} multiplied together.
+ *
+ * @return the total impulse of the motor.
+ */
+ public double getTotalImpulse() {
+ // Compute total impulse lazily
+ if (totalImpulse < 0) {
+ double time = getTotalTime();
+ double f0, t0;
+
+ totalImpulse = 0;
+ t0 = 0;
+ f0 = getThrust(0);
+ for (int i=1; i < DIVISIONS; i++) {
+ double t1 = time * i / DIVISIONS;
+ double f1 = getThrust(t1);
+ totalImpulse += 0.5*(f0+f1)*(t1-t0);
+ t0 = t1;
+ f0 = f1;
+ }
+ }
+ return totalImpulse;
+ }
+
+
+ /**
+ * Return the manufacturer of the motor.
+ *
+ * @return the manufacturer
+ */
+ public String getManufacturer() {
+ return manufacturer;
+ }
+
+ /**
+ * Return the designation of the motor.
+ *
+ * @return the designation
+ */
+ public String getDesignation() {
+ return designation;
+ }
+
+ /**
+ * Return the designation of the motor, including a delay.
+ *
+ * @param delay the delay of the motor.
+ * @return designation with delay.
+ */
+ public String getDesignation(double delay) {
+ return getDesignation() + "-" + getDelayString(delay);
+ }
+
+
+ /**
+ * Return extra description for the motor. This may include for example
+ * comments on the source of the thrust curve. The returned <code>String</code>
+ * may include new-lines.
+ *
+ * @return the description
+ */
+ public String getDescription() {
+ return description;
+ }
+
+
+ /**
+ * Return the motor type.
+ *
+ * @return the motorType
+ */
+ public Type getMotorType() {
+ return motorType;
+ }
+
+
+
+ /**
+ * Return the standard ejection charge delays for the motor. "Plugged" motors
+ * with no ejection charge are signified by the value {@link #PLUGGED}
+ * (<code>Double.POSITIVE_INFINITY</code>).
+ *
+ * @return the list of standard ejection charge delays, which may be empty.
+ */
+ public double[] getStandardDelays() {
+ return delays.clone();
+ }
+
+ /**
+ * Return the maximum diameter of the motor.
+ *
+ * @return the diameter
+ */
+ public double getDiameter() {
+ return diameter;
+ }
+
+ /**
+ * Return the length of the motor. This should be a "characteristic" length,
+ * and the exact definition may depend on the motor type. Typically this should
+ * be the length from the bottom of the motor to the end of the maximum diameter
+ * portion, ignoring any smaller ejection charge compartments.
+ *
+ * @return the length
+ */
+ public double getLength() {
+ return length;
+ }
+
+
+ /**
+ * Compares two <code>Motor</code> objects. The motors are considered equal
+ * if they have identical manufacturers, designations and types, near-identical
+ * dimensions, burn times and delays and near-identical thrust curves
+ * (sampled at 10 equidistant points).
+ * <p>
+ * The comment field is ignored when comparing equality.
+ */
+ @Override
+ public boolean equals(Object o) {
+ if (!(o instanceof Motor))
+ return false;
+
+ Motor other = (Motor) o;
+
+ // Tests manufacturer, designation, diameter and length
+ if (this.compareTo(other) != 0)
+ return false;
+
+ if (Math.abs(this.getTotalTime() - other.getTotalTime()) > 0.5 ||
+ this.motorType != other.motorType ||
+ this.delays.length != other.delays.length) {
+
+ return false;
+ }
+
+ for (int i=0; i < delays.length; i++) {
+ // INF - INF == NaN, which produces false when compared
+ if (Math.abs(this.delays[i] - other.delays[i]) > 0.5) {
+ return false;
+ }
+ }
+
+ double time = getTotalTime();
+ for (int i=0; i < 10; i++) {
+ double t = time * i/10;
+ if (Math.abs(this.getThrust(t) - other.getThrust(t)) > 1) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ /**
+ * A <code>hashCode</code> method compatible with the <code>equals</code>
+ * method.
+ */
+ @Override
+ public int hashCode() {
+ return (manufacturer.hashCode() + designation.hashCode() +
+ ((int)(length*1000)) + ((int)(diameter*1000)));
+ }
+
+
+
+ @Override
+ public String toString() {
+ return manufacturer + " " + designation;
+ }
+
+
+ ////////// Static methods
+
+
+ /**
+ * Return a String representation of a delay time. If the delay is {@link #PLUGGED},
+ * returns "P".
+ *
+ * @param delay the delay time.
+ * @return the <code>String</code> representation.
+ */
+ public static String getDelayString(double delay) {
+ return getDelayString(delay,"P");
+ }
+
+ /**
+ * Return a String representation of a delay time. If the delay is {@link #PLUGGED},
+ * <code>plugged</code> is returned.
+ *
+ * @param delay the delay time.
+ * @param plugged the return value if there is no ejection charge.
+ * @return the String representation.
+ */
+ public static String getDelayString(double delay, String plugged) {
+ if (delay == PLUGGED)
+ return plugged;
+ delay = Math.rint(delay*10)/10;
+ if (MathUtil.equals(delay, Math.rint(delay)))
+ return "" + ((int)delay);
+ return "" + delay;
+ }
+
+
+
+
+ //////////// Comparation
+
+
+
+ @Override
+ public int compareTo(Motor other) {
+ int value;
+
+ if (COLLATOR == null) {
+ }
+
+ // 1. Manufacturer
+ value = COLLATOR.compare(this.manufacturer, other.manufacturer);
+ if (value != 0)
+ return value;
+
+ // 2. Designation
+ value = DESIGNATION_COMPARATOR.compare(this.designation, other.designation);
+ if (value != 0)
+ return value;
+
+ // 3. Diameter
+ value = (int)((this.diameter - other.diameter)*1000000);
+ if (value != 0)
+ return value;
+
+ // 4. Length
+ value = (int)((this.length - other.length)*1000000);
+ if (value != 0)
+ return value;
+
+ // 5. Total impulse
+ value = (int)((this.getTotalImpulse() - other.getTotalImpulse())*1000);
+ return value;
+ }
+
+
+
+ public static Comparator<String> getDesignationComparator() {
+ return DESIGNATION_COMPARATOR;
+ }
+
+
+ /**
+ * Compares two motors by their designations. The motors are ordered first
+ * by their motor class, second by their average thrust and lastly by any
+ * extra modifiers at the end of the designation.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+ private static class DesignationComparator implements Comparator<String> {
+ private Pattern pattern =
+ Pattern.compile("^([0-9][0-9]+|1/([1-8]))?([a-zA-Z])([0-9]+)(.*?)$");
+
+ @Override
+ public int compare(String o1, String o2) {
+ int value;
+ Matcher m1, m2;
+
+ m1 = pattern.matcher(o1);
+ m2 = pattern.matcher(o2);
+
+ if (m1.find() && m2.find()) {
+
+ String o1Class = m1.group(3);
+ int o1Thrust = Integer.parseInt(m1.group(4));
+ String o1Extra = m1.group(5);
+
+ String o2Class = m2.group(3);
+ int o2Thrust = Integer.parseInt(m2.group(4));
+ String o2Extra = m2.group(5);
+
+ // 1. Motor class
+ if (o1Class.equalsIgnoreCase("A") && o2Class.equalsIgnoreCase("A")) {
+ // 1/2A and 1/4A comparison
+ String sub1 = m1.group(2);
+ String sub2 = m2.group(2);
+
+ if (sub1 != null || sub2 != null) {
+ if (sub1 == null)
+ sub1 = "1";
+ if (sub2 == null)
+ sub2 = "1";
+ value = -COLLATOR.compare(sub1,sub2);
+ if (value != 0)
+ return value;
+ }
+ }
+ value = COLLATOR.compare(o1Class,o2Class);
+ if (value != 0)
+ return value;
+
+ // 2. Average thrust
+ if (o1Thrust != o2Thrust)
+ return o1Thrust - o2Thrust;
+
+ // 3. Extra modifier
+ return COLLATOR.compare(o1Extra, o2Extra);
+
+ } else {
+
+ System.out.println("Falling back");
+ System.out.println("o1:"+o1 + " o2:"+o2);
+
+ // Not understandable designation, simply compare strings
+ return COLLATOR.compare(o1, o2);
+ }
+ }
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.util.ChangeSource;
+
+public interface MotorMount extends ChangeSource {
+
+ public static enum IgnitionEvent {
+ AUTOMATIC("Automatic (launch or ejection charge)") {
+ @Override
+ public boolean isActivationEvent(FlightEvent e, RocketComponent source) {
+ int count = source.getRocket().getStageCount();
+ int stage = source.getStageNumber();
+
+ if (stage == count-1) {
+ return LAUNCH.isActivationEvent(e, source);
+ } else {
+ return EJECTION_CHARGE.isActivationEvent(e, source);
+ }
+ }
+ },
+ LAUNCH("Launch") {
+ @Override
+ public boolean isActivationEvent(FlightEvent e, RocketComponent source) {
+ return (e.getType() == FlightEvent.Type.LAUNCH);
+ }
+ },
+ EJECTION_CHARGE("First ejection charge of previous stage") {
+ @Override
+ public boolean isActivationEvent(FlightEvent e, RocketComponent source) {
+ if (e.getType() != FlightEvent.Type.EJECTION_CHARGE)
+ return false;
+
+ int charge = e.getSource().getStageNumber();
+ int mount = source.getStageNumber();
+ return (mount+1 == charge);
+ }
+ },
+ BURNOUT("First burnout of previous stage") {
+ @Override
+ public boolean isActivationEvent(FlightEvent e, RocketComponent source) {
+ if (e.getType() != FlightEvent.Type.BURNOUT)
+ return false;
+
+ int charge = e.getSource().getStageNumber();
+ int mount = source.getStageNumber();
+ return (mount+1 == charge);
+ }
+ },
+ NEVER("Never") {
+ @Override
+ public boolean isActivationEvent(FlightEvent e, RocketComponent source) {
+ return false;
+ }
+ },
+ ;
+
+
+ private final String description;
+
+ IgnitionEvent(String description) {
+ this.description = description;
+ }
+
+ public abstract boolean isActivationEvent(FlightEvent e, RocketComponent source);
+
+ @Override
+ public String toString() {
+ return description;
+ }
+ };
+
+
+ /**
+ * Is the component currently a motor mount.
+ *
+ * @return whether the component holds a motor.
+ */
+ public boolean isMotorMount();
+
+ /**
+ * Set whether the component is currently a motor mount.
+ */
+ public void setMotorMount(boolean mount);
+
+
+ /**
+ * Return the motor for the motor configuration. May return <code>null</code>
+ * if no motor has been set. This method must return <code>null</code> if ID
+ * is <code>null</code>.
+ *
+ * @param id the motor configuration ID
+ * @return the motor, or <code>null</code> if not set.
+ */
+ public Motor getMotor(String id);
+
+ /**
+ * Set the motor for the motor configuration. May be set to <code>null</code>
+ * to remove the motor.
+ *
+ * @param id the motor configuration ID
+ * @param motor the motor, or <code>null</code>.
+ */
+ public void setMotor(String id, Motor motor);
+
+ /**
+ * Get the number of similar motors clustered.
+ *
+ * @return the number of motors.
+ */
+ public int getMotorCount();
+
+
+
+ /**
+ * Return the ejection charge delay of given motor configuration.
+ * A "plugged" motor without an ejection charge is given by
+ * {@link Motor#PLUGGED} (<code>Double.POSITIVE_INFINITY</code>).
+ *
+ * @param id the motor configuration ID
+ * @return the ejection charge delay.
+ */
+ public double getMotorDelay(String id);
+
+ /**
+ * Set the ejection change delay of the given motor configuration.
+ * The ejection charge is disable (a "plugged" motor) is set by
+ * {@link Motor#PLUGGED} (<code>Double.POSITIVE_INFINITY</code>).
+ *
+ * @param id the motor configuration ID
+ * @param delay the ejection charge delay.
+ */
+ public void setMotorDelay(String id, double delay);
+
+
+ /**
+ * Return the event that ignites this motor.
+ *
+ * @return the {@link IgnitionEvent} that ignites this motor.
+ */
+ public IgnitionEvent getIgnitionEvent();
+
+ /**
+ * Sets the event that ignites this motor.
+ *
+ * @param event the {@link IgnitionEvent} that ignites this motor.
+ */
+ public void setIgnitionEvent(IgnitionEvent event);
+
+
+ /**
+ * Returns the ignition delay of this motor.
+ *
+ * @return the ignition delay
+ */
+ public double getIgnitionDelay();
+
+ /**
+ * Sets the ignition delay of this motor.
+ *
+ * @param delay the ignition delay.
+ */
+ public void setIgnitionDelay(double delay);
+
+
+ /**
+ * Return the distance that the motors hang outside this motor mount.
+ *
+ * @return the overhang length.
+ */
+ public double getMotorOverhang();
+
+ /**
+ * Sets the distance that the motors hang outside this motor mount.
+ *
+ * @param overhang the overhang length.
+ */
+ public void setMotorOverhang(double overhang);
+
+
+
+ /**
+ * Return the inner diameter of the motor mount.
+ *
+ * @return the inner diameter of the motor mount.
+ */
+ public double getMotorMountDiameter();
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+/**
+ * Rocket nose cones of various types. Implemented as a transition with the
+ * fore radius == 0.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class NoseCone extends Transition {
+
+
+ /********* Constructors **********/
+ public NoseCone() {
+ this(Transition.Shape.OGIVE, 6*DEFAULT_RADIUS, DEFAULT_RADIUS);
+ }
+
+ public NoseCone(Transition.Shape type, double length, double radius) {
+ super();
+ super.setType(type);
+ super.setForeRadiusAutomatic(false);
+ super.setForeRadius(0);
+ super.setForeShoulderLength(0);
+ super.setForeShoulderRadius(0.9*radius);
+ super.setForeShoulderThickness(0);
+ super.setForeShoulderCapped(filled);
+ super.setThickness(0.002);
+ super.setLength(length);
+ super.setClipped(false);
+
+ }
+
+
+ /********** Get/set methods for component parameters **********/
+
+ @Override
+ public double getForeRadius() {
+ return 0;
+ }
+
+ @Override
+ public void setForeRadius(double r) {
+ // No-op
+ }
+
+ @Override
+ public boolean isForeRadiusAutomatic() {
+ return false;
+ }
+
+ @Override
+ public void setForeRadiusAutomatic(boolean b) {
+ // No-op
+ }
+
+ @Override
+ public double getForeShoulderLength() {
+ return 0;
+ }
+
+ @Override
+ public double getForeShoulderRadius() {
+ return 0;
+ }
+
+ @Override
+ public double getForeShoulderThickness() {
+ return 0;
+ }
+
+ @Override
+ public boolean isForeShoulderCapped() {
+ return false;
+ }
+
+ @Override
+ public void setForeShoulderCapped(boolean capped) {
+ // No-op
+ }
+
+ @Override
+ public void setForeShoulderLength(double foreShoulderLength) {
+ // No-op
+ }
+
+ @Override
+ public void setForeShoulderRadius(double foreShoulderRadius) {
+ // No-op
+ }
+
+ @Override
+ public void setForeShoulderThickness(double foreShoulderThickness) {
+ // No-op
+ }
+
+ @Override
+ public boolean isClipped() {
+ return false;
+ }
+
+ @Override
+ public void setClipped(boolean b) {
+ // No-op
+ }
+
+
+
+ /********** RocketComponent methods **********/
+
+ /**
+ * Return component name.
+ */
+ @Override
+ public String getComponentName() {
+ return "Nose cone";
+ }
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Prefs;
+
+public class Parachute extends RecoveryDevice {
+
+ public static final double DEFAULT_CD = 0.8;
+
+ private double diameter;
+
+ private Material lineMaterial;
+ private int lineCount = 6;
+ private double lineLength = 0.3;
+
+
+ public Parachute() {
+ this.diameter = 0.3;
+ this.lineMaterial = Prefs.getDefaultComponentMaterial(Parachute.class, Material.Type.LINE);
+ this.lineLength = 0.3;
+ }
+
+
+ public double getDiameter() {
+ return diameter;
+ }
+
+ public void setDiameter(double d) {
+ if (MathUtil.equals(this.diameter, d))
+ return;
+ this.diameter = d;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+ public final Material getLineMaterial() {
+ return lineMaterial;
+ }
+
+ public final void setLineMaterial(Material mat) {
+ if (mat.getType() != Material.Type.LINE) {
+ throw new IllegalArgumentException("Attempted to set non-line material "+mat);
+ }
+ if (mat.equals(lineMaterial))
+ return;
+ this.lineMaterial = mat;
+ if (getLineCount() != 0)
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ else
+ fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+ }
+
+
+ public final int getLineCount() {
+ return lineCount;
+ }
+
+ public final void setLineCount(int n) {
+ if (this.lineCount == n)
+ return;
+ this.lineCount = n;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+ public final double getLineLength() {
+ return lineLength;
+ }
+
+ public final void setLineLength(double length) {
+ if (MathUtil.equals(this.lineLength, length))
+ return;
+ this.lineLength = length;
+ if (getLineCount() != 0)
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ else
+ fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+ }
+
+
+ @Override
+ public double getComponentCD(double mach) {
+ return DEFAULT_CD; // TODO: HIGH: Better parachute CD estimate?
+ }
+
+ @Override
+ public double getArea() {
+ return Math.PI * MathUtil.pow2(diameter/2);
+ }
+
+ public void setArea(double area) {
+ if (MathUtil.equals(getArea(), area))
+ return;
+ diameter = Math.sqrt(area / Math.PI) * 2;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+ @Override
+ public double getComponentMass() {
+ return super.getComponentMass() +
+ getLineCount() * getLineLength() * getLineMaterial().getDensity();
+ }
+
+ @Override
+ public String getComponentName() {
+ return "Parachute";
+ }
+
+ @Override
+ public boolean isCompatible(Class<? extends RocketComponent> type) {
+ return false;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+public interface RadialParent {
+
+ /**
+ * Return the outer radius of the component at local coordinate <code>x</code>.
+ * Values for <code>x < 0</code> and <code>x > getLength()</code> are undefined.
+ *
+ * @param x the lengthwise position in the coordinates of this component.
+ * @return the outer radius of the component at that position.
+ */
+ public double getOuterRadius(double x);
+
+ /**
+ * Return the inner radius of the component at local coordinate <code>x</code>.
+ * Values for <code>x < 0</code> and <code>x > getLength()</code> are undefined.
+ *
+ * @param x the lengthwise position in the coordinates of this component.
+ * @return the inner radius of the component at that position.
+ */
+ public double getInnerRadius(double x);
+
+
+ /**
+ * Return the length of this component.
+ *
+ * @return the length of this component.
+ */
+ public double getLength();
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+/**
+ * An inner component that consists of a hollow cylindrical component. This can be
+ * an inner tube, tube coupler, centering ring, bulkhead etc.
+ *
+ * The properties include the inner and outer radii, length and radial position.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class RadiusRingComponent extends RingComponent {
+
+ protected double outerRadius = 0;
+ protected double innerRadius = 0;
+
+ @Override
+ public double getOuterRadius() {
+ if (outerRadiusAutomatic && getParent() instanceof RadialParent) {
+ RocketComponent parent = getParent();
+ double pos1 = this.toRelative(Coordinate.NUL, parent)[0].x;
+ double pos2 = this.toRelative(new Coordinate(getLength()), parent)[0].x;
+ pos1 = MathUtil.clamp(pos1, 0, parent.getLength());
+ pos2 = MathUtil.clamp(pos2, 0, parent.getLength());
+ outerRadius = Math.min(((RadialParent)parent).getInnerRadius(pos1),
+ ((RadialParent)parent).getInnerRadius(pos2));
+ }
+
+ return outerRadius;
+ }
+
+ @Override
+ public void setOuterRadius(double r) {
+ r = Math.max(r,0);
+ if (MathUtil.equals(outerRadius, r) && !isOuterRadiusAutomatic())
+ return;
+
+ outerRadius = r;
+ outerRadiusAutomatic = false;
+ if (getInnerRadius() > r) {
+ innerRadius = r;
+ innerRadiusAutomatic = false;
+ }
+
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+ @Override
+ public double getInnerRadius() {
+ return innerRadius;
+ }
+ @Override
+ public void setInnerRadius(double r) {
+ r = Math.max(r,0);
+ if (MathUtil.equals(innerRadius, r))
+ return;
+
+ innerRadius = r;
+ innerRadiusAutomatic = false;
+ if (getOuterRadius() < r) {
+ outerRadius = r;
+ outerRadiusAutomatic = false;
+ }
+
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+ @Override
+ public double getThickness() {
+ return Math.max(getOuterRadius() - getInnerRadius(), 0);
+ }
+ @Override
+ public void setThickness(double thickness) {
+ double outer = getOuterRadius();
+
+ thickness = MathUtil.clamp(thickness, 0, outer);
+ setInnerRadius(outer - thickness);
+ }
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Pair;
+import net.sf.openrocket.util.Prefs;
+
+
+/**
+ * RecoveryDevice is a class representing devices that slow down descent.
+ * Recovery devices report that they have no aerodynamic effect, since they
+ * are within the rocket during ascent.
+ * <p>
+ * A recovery device includes a surface material of which it is made of.
+ * The mass of the component is calculated based on the material and the
+ * area of the device from {@link #getArea()}. {@link #getComponentMass()}
+ * may be overridden if additional mass needs to be included.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class RecoveryDevice extends MassObject {
+
+ public static enum DeployEvent {
+ LAUNCH("Launch (plus NN seconds)") {
+ @Override
+ public boolean isActivationEvent(FlightEvent e, RocketComponent source) {
+ return e.getType() == FlightEvent.Type.LAUNCH;
+ }
+ },
+ EJECTION("First ejection charge of this stage") {
+ @Override
+ public boolean isActivationEvent(FlightEvent e, RocketComponent source) {
+ if (e.getType() != FlightEvent.Type.EJECTION_CHARGE)
+ return false;
+ RocketComponent charge = e.getSource();
+ return charge.getStageNumber() == source.getStageNumber();
+ }
+ },
+ APOGEE("Apogee") {
+ @Override
+ public boolean isActivationEvent(FlightEvent e, RocketComponent source) {
+ return e.getType() == FlightEvent.Type.APOGEE;
+ }
+ },
+ ALTITUDE("Specific altitude during descent") {
+ @SuppressWarnings("unchecked")
+ @Override
+ public boolean isActivationEvent(FlightEvent e, RocketComponent source) {
+ if (e.getType() != FlightEvent.Type.ALTITUDE)
+ return false;
+
+ double alt = ((RecoveryDevice)source).getDeployAltitude();
+ Pair<Double,Double> altitude = (Pair<Double,Double>)e.getData();
+
+ return (altitude.getU() >= alt) && (altitude.getV() <= alt);
+ }
+ },
+ NEVER("Never") {
+ @Override
+ public boolean isActivationEvent(FlightEvent e, RocketComponent source) {
+ return false;
+ }
+ }
+ ;
+
+ private final String description;
+
+ DeployEvent(String description) {
+ this.description = description;
+ }
+
+ public abstract boolean isActivationEvent(FlightEvent e, RocketComponent source);
+
+ @Override
+ public String toString() {
+ return description;
+ }
+
+ }
+
+
+ private DeployEvent deployEvent = DeployEvent.EJECTION;
+ private double deployAltitude = 200;
+ private double deployDelay = 0;
+
+ private double cd = Parachute.DEFAULT_CD;
+ private boolean cdAutomatic = true;
+
+
+ private Material.Surface material;
+
+
+ public RecoveryDevice() {
+ this(Prefs.getDefaultComponentMaterial(RecoveryDevice.class, Material.Type.SURFACE));
+ }
+
+ public RecoveryDevice(Material material) {
+ super();
+ setMaterial(material);
+ }
+
+ public RecoveryDevice(double length, double radius, Material material) {
+ super(length, radius);
+ setMaterial(material);
+ }
+
+
+
+
+ public abstract double getArea();
+
+ public abstract double getComponentCD(double mach);
+
+
+
+ public double getCD() {
+ return getCD(0);
+ }
+
+ public double getCD(double mach) {
+ if (cdAutomatic)
+ cd = getComponentCD(mach);
+ return cd;
+ }
+
+ public void setCD(double cd) {
+ if (MathUtil.equals(this.cd, cd) && !isCDAutomatic())
+ return;
+ this.cd = cd;
+ this.cdAutomatic = false;
+ fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE);
+ }
+
+
+ public boolean isCDAutomatic() {
+ return cdAutomatic;
+ }
+
+ public void setCDAutomatic(boolean auto) {
+ if (cdAutomatic == auto)
+ return;
+ this.cdAutomatic = auto;
+ fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE);
+ }
+
+
+
+ public final Material getMaterial() {
+ return material;
+ }
+
+ public final void setMaterial(Material mat) {
+ if (!(mat instanceof Material.Surface)) {
+ throw new IllegalArgumentException("Attempted to set non-surface material "+mat);
+ }
+ if (mat.equals(material))
+ return;
+ this.material = (Material.Surface)mat;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+
+
+ public DeployEvent getDeployEvent() {
+ return deployEvent;
+ }
+
+ public void setDeployEvent(DeployEvent deployEvent) {
+ if (this.deployEvent == deployEvent)
+ return;
+ this.deployEvent = deployEvent;
+ fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
+ }
+
+
+ public double getDeployAltitude() {
+ return deployAltitude;
+ }
+
+ public void setDeployAltitude(double deployAltitude) {
+ if (MathUtil.equals(this.deployAltitude, deployAltitude))
+ return;
+ this.deployAltitude = deployAltitude;
+ if (getDeployEvent() == DeployEvent.ALTITUDE)
+ fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
+ else
+ fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+ }
+
+
+ public double getDeployDelay() {
+ return deployDelay;
+ }
+
+ public void setDeployDelay(double delay) {
+ delay = MathUtil.max(delay, 0);
+ if (MathUtil.equals(this.deployDelay, delay))
+ return;
+ this.deployDelay = delay;
+ fireComponentChangeEvent(ComponentChangeEvent.EVENT_CHANGE);
+ }
+
+
+
+ @Override
+ public double getComponentMass() {
+ return getArea() * getMaterial().getDensity();
+ }
+
+}
--- /dev/null
+/**
+ *
+ */
+package net.sf.openrocket.rocketcomponent;
+
+public enum ReferenceType {
+
+ NOSECONE {
+ @Override
+ public double getReferenceLength(Configuration config) {
+ for (RocketComponent c: config) {
+ if (c instanceof SymmetricComponent) {
+ SymmetricComponent s = (SymmetricComponent)c;
+ if (s.getForeRadius() >= 0.0005)
+ return s.getForeRadius() * 2;
+ if (s.getAftRadius() >= 0.0005)
+ return s.getAftRadius() * 2;
+ }
+ }
+ return Rocket.DEFAULT_REFERENCE_LENGTH;
+ }
+ },
+
+ MAXIMUM {
+ @Override
+ public double getReferenceLength(Configuration config) {
+ double r = 0;
+ for (RocketComponent c: config) {
+ if (c instanceof SymmetricComponent) {
+ SymmetricComponent s = (SymmetricComponent)c;
+ r = Math.max(r, s.getForeRadius());
+ r = Math.max(r, s.getAftRadius());
+ }
+ }
+ r *= 2;
+ if (r < 0.001)
+ r = Rocket.DEFAULT_REFERENCE_LENGTH;
+ return r;
+ }
+ },
+
+ CUSTOM {
+ @Override
+ public double getReferenceLength(Configuration config) {
+ return config.getRocket().getCustomReferenceLength();
+ }
+ };
+
+ public abstract double getReferenceLength(Configuration rocket);
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+/**
+ * An inner component that consists of a hollow cylindrical component. This can be
+ * an inner tube, tube coupler, centering ring, bulkhead etc.
+ *
+ * The properties include the inner and outer radii, length and radial position.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class RingComponent extends StructuralComponent {
+
+ protected boolean outerRadiusAutomatic = false;
+ protected boolean innerRadiusAutomatic = false;
+
+
+ private double radialDirection = 0;
+ private double radialPosition = 0;
+
+ private double shiftY = 0;
+ private double shiftZ = 0;
+
+
+
+
+ public abstract double getOuterRadius();
+ public abstract void setOuterRadius(double r);
+
+ public abstract double getInnerRadius();
+ public abstract void setInnerRadius(double r);
+
+ public abstract double getThickness();
+ public abstract void setThickness(double thickness);
+
+
+ public final boolean isOuterRadiusAutomatic() {
+ return outerRadiusAutomatic;
+ }
+
+ protected void setOuterRadiusAutomatic(boolean auto) {
+ if (auto == outerRadiusAutomatic)
+ return;
+ outerRadiusAutomatic = auto;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+ public final boolean isInnerRadiusAutomatic() {
+ return innerRadiusAutomatic;
+ }
+
+ protected void setInnerRadiusAutomatic(boolean auto) {
+ if (auto == innerRadiusAutomatic)
+ return;
+ innerRadiusAutomatic = auto;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+
+
+ public final void setLength(double length) {
+ double l = Math.max(length,0);
+ if (this.length == l)
+ return;
+
+ this.length = l;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+ /**
+ * Return the radial direction of displacement of the component. Direction 0
+ * is equivalent to the Y-direction.
+ *
+ * @return the radial direction.
+ */
+ public double getRadialDirection() {
+ return radialDirection;
+ }
+
+ /**
+ * Set the radial direction of displacement of the component. Direction 0
+ * is equivalent to the Y-direction.
+ *
+ * @param dir the radial direction.
+ */
+ public void setRadialDirection(double dir) {
+ dir = MathUtil.reduce180(dir);
+ if (radialDirection == dir)
+ return;
+ radialDirection = dir;
+ shiftY = radialPosition * Math.cos(radialDirection);
+ shiftZ = radialPosition * Math.sin(radialDirection);
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+
+
+ /**
+ * Return the radial position of the component. The position is the distance
+ * of the center of the component from the center of the parent component.
+ *
+ * @return the radial position.
+ */
+ public double getRadialPosition() {
+ return radialPosition;
+ }
+
+ /**
+ * Set the radial position of the component. The position is the distance
+ * of the center of the component from the center of the parent component.
+ *
+ * @param pos the radial position.
+ */
+ public void setRadialPosition(double pos) {
+ pos = Math.max(pos, 0);
+ if (radialPosition == pos)
+ return;
+ radialPosition = pos;
+ shiftY = radialPosition * Math.cos(radialDirection);
+ shiftZ = radialPosition * Math.sin(radialDirection);
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+
+ /**
+ * Return the number of times the component is multiplied.
+ */
+ public int getClusterCount() {
+ if (this instanceof Clusterable)
+ return ((Clusterable)this).getClusterConfiguration().getClusterCount();
+ return 1;
+ }
+
+
+ /**
+ * Shift the coordinates according to the radial position and direction.
+ */
+ @Override
+ public Coordinate[] shiftCoordinates(Coordinate[] array) {
+ for (int i=0; i < array.length; i++) {
+ array[i] = array[i].add(0, shiftY, shiftZ);
+ }
+ return array;
+ }
+
+
+ @Override
+ public Collection<Coordinate> getComponentBounds() {
+ List<Coordinate> bounds = new ArrayList<Coordinate>();
+ addBound(bounds,0,getOuterRadius());
+ addBound(bounds,length,getOuterRadius());
+ return bounds;
+ }
+
+
+
+ @Override
+ public Coordinate getComponentCG() {
+ return new Coordinate(length/2, 0, 0, getComponentMass());
+ }
+
+ @Override
+ public double getComponentMass() {
+ return ringMass(getOuterRadius(), getInnerRadius(), getLength(),
+ getMaterial().getDensity()) * getClusterCount();
+ }
+
+
+ @Override
+ public double getLongitudalUnitInertia() {
+ return ringLongitudalUnitInertia(getOuterRadius(), getInnerRadius(), getLength());
+ }
+
+ @Override
+ public double getRotationalUnitInertia() {
+ return ringRotationalUnitInertia(getOuterRadius(), getInnerRadius());
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import javax.swing.event.ChangeListener;
+import javax.swing.event.EventListenerList;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+/**
+ * Base for all rocket components. This is the "starting point" for all rocket trees.
+ * It provides the actual implementations of several methods defined in RocketComponent
+ * (eg. the rocket listener lists) and the methods defined in RocketComponent call these.
+ * It also defines some other methods that concern the whole rocket, and helper methods
+ * that keep information about the program state.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class Rocket extends RocketComponent {
+ public static final double DEFAULT_REFERENCE_LENGTH = 0.01;
+
+ private static final boolean DEBUG_LISTENERS = false;
+
+
+ /**
+ * The next modification ID to use. This variable may only be accessed via
+ * the synchronized {@link #getNextModID()} method!
+ */
+ private static int nextModID = 1;
+
+
+ /**
+ * List of component change listeners.
+ */
+ private EventListenerList listenerList = new EventListenerList();
+
+ /**
+ * When freezeList != null, events are not dispatched but stored in the list.
+ * When the structure is thawed, a single combined event will be fired.
+ */
+ private List<ComponentChangeEvent> freezeList = null;
+
+
+ private int modID;
+ private int massModID;
+ private int aeroModID;
+ private int treeModID;
+ private int functionalModID;
+
+
+ private ReferenceType refType = ReferenceType.MAXIMUM; // Set in constructor
+ private double customReferenceLength = DEFAULT_REFERENCE_LENGTH;
+
+
+ // The default configuration used in dialogs
+ private final Configuration defaultConfiguration;
+
+
+ private String designer = "";
+ private String revision = "";
+
+
+ // Motor configuration list
+ private List<String> motorConfigurationIDs = new ArrayList<String>();
+ private Map<String, String> motorConfigurationNames = new HashMap<String, String>();
+ {
+ motorConfigurationIDs.add(null);
+ }
+
+
+ // Does the rocket have a perfect finish (a notable amount of laminar flow)
+ private boolean perfectFinish = false;
+
+
+
+ ///////////// Constructor /////////////
+
+ public Rocket() {
+ super(RocketComponent.Position.AFTER);
+ modID = getNextModID();
+ massModID = modID;
+ aeroModID = modID;
+ treeModID = modID;
+ functionalModID = modID;
+ defaultConfiguration = new Configuration(this);
+ }
+
+
+
+ public String getDesigner() {
+ return designer;
+ }
+
+ public void setDesigner(String s) {
+ if (s == null)
+ s = "";
+ designer = s;
+ fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+ }
+
+
+ public String getRevision() {
+ return revision;
+ }
+
+ public void setRevision(String s) {
+ if (s == null)
+ s = "";
+ revision = s;
+ fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+ }
+
+
+
+
+ /**
+ * Return the number of stages in this rocket.
+ *
+ * @return the number of stages in this rocket.
+ */
+ public int getStageCount() {
+ return this.getChildCount();
+ }
+
+
+
+ /**
+ * Return the non-negative modification ID of this rocket. The ID is changed
+ * every time any change occurs in the rocket. This can be used to check
+ * whether it is necessary to void cached data in cases where listeners can not
+ * or should not be used.
+ * <p>
+ * Three other modification IDs are also available, {@link #getMassModID()},
+ * {@link #getAerodynamicModID()} {@link #getTreeModID()}, which change every time
+ * a mass change, aerodynamic change, or tree change occur. Even though the values
+ * of the different modification ID's may be equal, they should be treated totally
+ * separate.
+ * <p>
+ * Note that undo events restore the modification IDs that were in use at the
+ * corresponding undo level. Subsequent modifications, however, produce modIDs
+ * distinct from those already used.
+ *
+ * @return a unique ID number for this modification state.
+ */
+ public int getModID() {
+ return modID;
+ }
+
+ /**
+ * Return the non-negative mass modification ID of this rocket. See
+ * {@link #getModID()} for details.
+ *
+ * @return a unique ID number for this mass-modification state.
+ */
+ public int getMassModID() {
+ return massModID;
+ }
+
+ /**
+ * Return the non-negative aerodynamic modification ID of this rocket. See
+ * {@link #getModID()} for details.
+ *
+ * @return a unique ID number for this aerodynamic-modification state.
+ */
+ public int getAerodynamicModID() {
+ return aeroModID;
+ }
+
+ /**
+ * Return the non-negative tree modification ID of this rocket. See
+ * {@link #getModID()} for details.
+ *
+ * @return a unique ID number for this tree-modification state.
+ */
+ public int getTreeModID() {
+ return treeModID;
+ }
+
+ /**
+ * Return the non-negative functional modificationID of this rocket.
+ * This changes every time a functional change occurs.
+ *
+ * @return a unique ID number for this functional modification state.
+ */
+ public int getFunctionalModID() {
+ return functionalModID;
+ }
+
+
+
+
+ public ReferenceType getReferenceType() {
+ return refType;
+ }
+
+ public void setReferenceType(ReferenceType type) {
+ if (refType == type)
+ return;
+ refType = type;
+ fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+ }
+
+
+ public double getCustomReferenceLength() {
+ return customReferenceLength;
+ }
+
+ public void setCustomReferenceLength(double length) {
+ if (MathUtil.equals(customReferenceLength, length))
+ return;
+
+ this.customReferenceLength = Math.max(length,0.001);
+
+ if (refType == ReferenceType.CUSTOM) {
+ fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+ }
+ }
+
+
+
+
+
+ /**
+ * Set whether the rocket has a perfect finish. This will affect whether the
+ * boundary layer is assumed to be fully turbulent or not.
+ *
+ * @param perfectFinish whether the finish is perfect.
+ */
+ public void setPerfectFinish(boolean perfectFinish) {
+ if (this.perfectFinish == perfectFinish)
+ return;
+ this.perfectFinish = perfectFinish;
+ fireComponentChangeEvent(ComponentChangeEvent.AERODYNAMIC_CHANGE);
+ }
+
+
+
+ /**
+ * Get whether the rocket has a perfect finish.
+ *
+ * @return the perfectFinish
+ */
+ public boolean isPerfectFinish() {
+ return perfectFinish;
+ }
+
+
+
+ /**
+ * Return a new unique modification ID. This method is thread-safe.
+ *
+ * @return a new modification ID unique to this session.
+ */
+ private synchronized int getNextModID() {
+ return nextModID++;
+ }
+
+
+ /**
+ * Make a deep copy of the Rocket structure. This is a helper method which simply
+ * casts the result of the superclass method to a Rocket.
+ */
+ @Override
+ public Rocket copy() {
+ Rocket copy = (Rocket)super.copy();
+ copy.resetListeners();
+ return copy;
+ }
+
+
+
+
+
+
+ /**
+ * Load the rocket structure from the source. The method loads the fields of this
+ * Rocket object and copies the references to siblings from the <code>source</code>.
+ * The object <code>source</code> should not be used after this call, as it is in
+ * an illegal state!
+ * <p>
+ * This method is meant to be used in conjunction with undo/redo functionality,
+ * and therefore fires an UNDO_EVENT, masked with all applicable mass/aerodynamic/tree
+ * changes.
+ */
+ public void loadFrom(Rocket r) {
+ super.copyFrom(r);
+
+ int type = ComponentChangeEvent.UNDO_CHANGE | ComponentChangeEvent.NONFUNCTIONAL_CHANGE;
+ if (this.massModID != r.massModID)
+ type |= ComponentChangeEvent.MASS_CHANGE;
+ if (this.aeroModID != r.aeroModID)
+ type |= ComponentChangeEvent.AERODYNAMIC_CHANGE;
+ if (this.treeModID != r.treeModID)
+ type |= ComponentChangeEvent.TREE_CHANGE;
+
+ this.modID = r.modID;
+ this.massModID = r.massModID;
+ this.aeroModID = r.aeroModID;
+ this.treeModID = r.treeModID;
+ this.functionalModID = r.functionalModID;
+ this.refType = r.refType;
+ this.customReferenceLength = r.customReferenceLength;
+
+ this.motorConfigurationIDs = r.motorConfigurationIDs;
+ this.motorConfigurationNames = r.motorConfigurationNames;
+ this.perfectFinish = r.perfectFinish;
+
+ fireComponentChangeEvent(type);
+ }
+
+
+
+
+ /////// Implement the ComponentChangeListener lists
+
+ /**
+ * Creates a new EventListenerList for this component. This is necessary when cloning
+ * the structure.
+ */
+ public void resetListeners() {
+// System.out.println("RESETTING LISTENER LIST of Rocket "+this);
+ listenerList = new EventListenerList();
+ }
+
+
+ public void printListeners() {
+ System.out.println(""+this+" has "+listenerList.getListenerCount()+" listeners:");
+ Object[] list = listenerList.getListenerList();
+ for (int i=1; i<list.length; i+=2)
+ System.out.println(" "+((i+1)/2)+": "+list[i]);
+ }
+
+ @Override
+ public void addComponentChangeListener(ComponentChangeListener l) {
+ listenerList.add(ComponentChangeListener.class,l);
+ if (DEBUG_LISTENERS)
+ System.out.println(this+": Added listner (now "+listenerList.getListenerCount()+
+ " listeners): "+l);
+ }
+ @Override
+ public void removeComponentChangeListener(ComponentChangeListener l) {
+ listenerList.remove(ComponentChangeListener.class, l);
+ if (DEBUG_LISTENERS)
+ System.out.println(this+": Removed listner (now "+listenerList.getListenerCount()+
+ " listeners): "+l);
+ }
+
+
+ @Override
+ public void addChangeListener(ChangeListener l) {
+ listenerList.add(ChangeListener.class,l);
+ if (DEBUG_LISTENERS)
+ System.out.println(this+": Added listner (now "+listenerList.getListenerCount()+
+ " listeners): "+l);
+ }
+ @Override
+ public void removeChangeListener(ChangeListener l) {
+ listenerList.remove(ChangeListener.class, l);
+ if (DEBUG_LISTENERS)
+ System.out.println(this+": Removed listner (now "+listenerList.getListenerCount()+
+ " listeners): "+l);
+ }
+
+
+ @Override
+ protected void fireComponentChangeEvent(ComponentChangeEvent e) {
+
+ // Update modification ID's only for normal (not undo/redo) events
+ if (!e.isUndoChange()) {
+ modID = getNextModID();
+ if (e.isMassChange())
+ massModID = modID;
+ if (e.isAerodynamicChange())
+ aeroModID = modID;
+ if (e.isTreeChange())
+ treeModID = modID;
+ if (e.getType() != ComponentChangeEvent.NONFUNCTIONAL_CHANGE)
+ functionalModID = modID;
+ }
+
+ if (DEBUG_LISTENERS)
+ System.out.println("FIRING "+e);
+
+ // Check whether frozen
+ if (freezeList != null) {
+ freezeList.add(e);
+ return;
+ }
+
+ // Notify all components first
+ Iterator<RocketComponent> iterator = this.deepIterator(true);
+ while (iterator.hasNext()) {
+ iterator.next().componentChanged(e);
+ }
+
+ // Notify all listeners
+ Object[] listeners = listenerList.getListenerList();
+ for (int i = listeners.length-2; i>=0; i-=2) {
+ if (listeners[i] == ComponentChangeListener.class) {
+ ((ComponentChangeListener) listeners[i+1]).componentChanged(e);
+ } else if (listeners[i] == ChangeListener.class) {
+ ((ChangeListener) listeners[i+1]).stateChanged(e);
+ }
+ }
+ }
+
+
+ /**
+ * Freezes the rocket structure from firing any events. This may be performed to
+ * combine several actions on the structure into a single large action.
+ * <code>thaw()</code> must always be called afterwards.
+ *
+ * NOTE: Always use a try/finally to ensure <code>thaw()</code> is called:
+ * <pre>
+ * Rocket r = c.getRocket();
+ * try {
+ * r.freeze();
+ * // do stuff
+ * } finally {
+ * r.thaw();
+ * }
+ * </pre>
+ *
+ * @see #thaw()
+ */
+ public void freeze() {
+ if (freezeList == null)
+ freezeList = new LinkedList<ComponentChangeEvent>();
+ }
+
+ /**
+ * Thaws a frozen rocket structure and fires a combination of the events fired during
+ * the freeze. The event type is a combination of those fired and the source is the
+ * last component to have been an event source.
+ *
+ * @see #freeze()
+ */
+ public void thaw() {
+ if (freezeList == null)
+ return;
+ if (freezeList.size()==0) {
+ freezeList = null;
+ return;
+ }
+
+ int type = 0;
+ Object c = null;
+ for (ComponentChangeEvent e: freezeList) {
+ type = type | e.getType();
+ c = e.getSource();
+ }
+ freezeList = null;
+
+ fireComponentChangeEvent(new ComponentChangeEvent((RocketComponent)c,type));
+ }
+
+
+
+
+ //////// Motor configurations ////////
+
+
+ /**
+ * Return the default configuration. This should be used in the user interface
+ * to ensure a consistent rocket configuration between dialogs. It should NOT
+ * be used in simulations not relating to the UI.
+ *
+ * @return the default {@link Configuration}.
+ */
+ public Configuration getDefaultConfiguration() {
+ return defaultConfiguration;
+ }
+
+
+ /**
+ * Return an array of the motor configuration IDs. This array is guaranteed
+ * to contain the <code>null</code> ID as the first element.
+ *
+ * @return an array of the motor configuration IDs.
+ */
+ public String[] getMotorConfigurationIDs() {
+ return motorConfigurationIDs.toArray(new String[0]);
+ }
+
+ /**
+ * Add a new motor configuration ID to the motor configurations. The new ID
+ * is returned.
+ *
+ * @return the new motor configuration ID.
+ */
+ public String newMotorConfigurationID() {
+ String id = UUID.randomUUID().toString();
+ motorConfigurationIDs.add(id);
+ fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
+ return id;
+ }
+
+ /**
+ * Add a specified motor configuration ID to the motor configurations.
+ *
+ * @param id the motor configuration ID.
+ * @return true if successful, false if the ID was already used.
+ */
+ public boolean addMotorConfigurationID(String id) {
+ if (id == null || motorConfigurationIDs.contains(id))
+ return false;
+ motorConfigurationIDs.add(id);
+ fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
+ return true;
+ }
+
+ /**
+ * Remove a motor configuration ID from the configuration IDs. The <code>null</code>
+ * ID cannot be removed, and an attempt to remove it will be silently ignored.
+ *
+ * @param id the motor configuration ID to remove
+ */
+ public void removeMotorConfigurationID(String id) {
+ if (id == null)
+ return;
+ motorConfigurationIDs.remove(id);
+ fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
+ }
+
+
+ /**
+ * Return the user-set name of the motor configuration. If no name has been set,
+ * returns an empty string (not null).
+ *
+ * @param id the motor configuration id
+ * @return the configuration name
+ */
+ public String getMotorConfigurationName(String id) {
+ String s = motorConfigurationNames.get(id);
+ if (s == null)
+ return "";
+ return s;
+ }
+
+
+ /**
+ * Set the name of the motor configuration. A name can be unset by passing
+ * <code>null</code> or an empty string.
+ *
+ * @param id the motor configuration id
+ * @param name the name for the motor configuration
+ */
+ public void setMotorConfigurationName(String id, String name) {
+ motorConfigurationNames.put(id,name);
+ fireComponentChangeEvent(ComponentChangeEvent.MOTOR_CHANGE);
+ }
+
+
+ /**
+ * Return a description for the motor configuration. This is either the
+ * name previously set by {@link #setMotorConfigurationName(String, String)} or
+ * a string generated from the motor designations of the components.
+ *
+ * @param id the motor configuration ID.
+ * @return a textual representation of the configuration
+ */
+ @SuppressWarnings("null")
+ public String getMotorConfigurationDescription(String id) {
+ String name;
+ int motorCount = 0;
+
+ if (!motorConfigurationIDs.contains(id)) {
+ throw new IllegalArgumentException("Motor configuration ID does not exist: "+id);
+ }
+
+ name = motorConfigurationNames.get(id);
+ if (name != null && !name.equals(""))
+ return name;
+
+ // Generate the description
+
+ // First iterate over each stage and store the designations of each motor
+ List<List<String>> list = new ArrayList<List<String>>();
+ List<String> currentList = null;
+
+ Iterator<RocketComponent> iterator = this.deepIterator();
+ while (iterator.hasNext()) {
+ RocketComponent c = iterator.next();
+
+ if (c instanceof Stage) {
+
+ currentList = new ArrayList<String>();
+ list.add(currentList);
+
+ } else if (c instanceof MotorMount) {
+
+ MotorMount mount = (MotorMount) c;
+ Motor motor = mount.getMotor(id);
+
+ if (mount.isMotorMount() && motor != null) {
+ String designation = motor.getDesignation(mount.getMotorDelay(id));
+
+ for (int i=0; i < mount.getMotorCount(); i++) {
+ currentList.add(designation);
+ motorCount++;
+ }
+ }
+
+ }
+ }
+
+ if (motorCount == 0) {
+ return "[No motors]";
+ }
+
+ // Change multiple occurrences of a motor to n x motor
+ List<String> stages = new ArrayList<String>();
+
+ for (List<String> stage: list) {
+ String stageName = "";
+ String previous = null;
+ int count = 0;
+
+ Collections.sort(stage);
+ for (String current: stage) {
+ if (current.equals(previous)) {
+
+ count++;
+
+ } else {
+
+ if (previous != null) {
+ String s = "";
+ if (count > 1) {
+ s = "" + count + "\u00d7" + previous;
+ } else {
+ s = previous;
+ }
+
+ if (stageName.equals(""))
+ stageName = s;
+ else
+ stageName = stageName + "," + s;
+ }
+
+ previous = current;
+ count = 1;
+
+ }
+ }
+ if (previous != null) {
+ String s = "";
+ if (count > 1) {
+ s = "" + count + "\u00d7" + previous;
+ } else {
+ s = previous;
+ }
+
+ if (stageName.equals(""))
+ stageName = s;
+ else
+ stageName = stageName + "," + s;
+ }
+
+ stages.add(stageName);
+ }
+
+ name = "[";
+ for (int i=0; i < stages.size(); i++) {
+ String s = stages.get(i);
+ if (s.equals(""))
+ s = "None";
+ if (i==0)
+ name = name + s;
+ else
+ name = name + "; " + s;
+ }
+ name += "]";
+ return name;
+ }
+
+
+
+ //////// Obligatory component information
+
+
+ @Override
+ public String getComponentName() {
+ return "Rocket";
+ }
+
+ @Override
+ public Coordinate getComponentCG() {
+ return new Coordinate(0,0,0,0);
+ }
+
+ @Override
+ public double getComponentMass() {
+ return 0;
+ }
+
+ @Override
+ public double getLongitudalUnitInertia() {
+ return 0;
+ }
+
+ @Override
+ public double getRotationalUnitInertia() {
+ return 0;
+ }
+
+ @Override
+ public Collection<Coordinate> getComponentBounds() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean isAerodynamic() {
+ return false;
+ }
+
+ @Override
+ public boolean isMassive() {
+ return false;
+ }
+
+ /**
+ * Allows only <code>Stage</code> components to be added to the type Rocket.
+ */
+ @Override
+ public boolean isCompatible(Class<? extends RocketComponent> type) {
+ return (Stage.class.isAssignableFrom(type));
+ }
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import java.awt.Color;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.EmptyStackException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Stack;
+import java.util.UUID;
+
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.util.ChangeSource;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.LineStyle;
+import net.sf.openrocket.util.MathUtil;
+
+
+public abstract class RocketComponent implements ChangeSource, Cloneable,
+ Iterable<RocketComponent> {
+
+ /*
+ * Text is suitable to the form
+ * Position relative to: <title>
+ */
+ public enum Position {
+ /** Position relative to the top of the parent component. */
+ TOP("Top of the parent component"),
+ /** Position relative to the middle of the parent component. */
+ MIDDLE("Middle of the parent component"),
+ /** Position relative to the bottom of the parent component. */
+ BOTTOM("Bottom of the parent component"),
+ /** Position after the parent component (for body components). */
+ AFTER("After the parent component"),
+ /** Specify an absolute X-coordinate position. */
+ ABSOLUTE("Tip of the nose cone");
+
+ private String title;
+ Position(String title) {
+ this.title = title;
+ }
+
+ @Override
+ public String toString() {
+ return title;
+ }
+ }
+
+ //////// Parent/child trees
+ /**
+ * Parent component of the current component, or null if none exists.
+ */
+ private RocketComponent parent = null;
+
+ /**
+ * List of child components of this component.
+ */
+ private List<RocketComponent> children = new ArrayList<RocketComponent>();
+
+
+ //////// Parameters common to all components:
+
+ /**
+ * Characteristic length of the component. This is used in calculating the coordinate
+ * transformations and positions of other components in reference to this component.
+ * This may and should be used as the "true" length of the component, where applicable.
+ * By default it is zero, i.e. no translation.
+ */
+ protected double length = 0;
+
+ /**
+ * Positioning of this component relative to the parent component.
+ */
+ protected Position relativePosition;
+
+ /**
+ * Offset of the position of this component relative to the normal position given by
+ * relativePosition. By default zero, i.e. no position change.
+ */
+ protected double position = 0;
+
+
+ // Color of the component, null means to use the default color
+ private Color color = null;
+ private LineStyle lineStyle = null;
+
+
+ // Override mass/CG
+ private double overrideMass = 0;
+ private boolean massOverriden = false;
+ private double overrideCGX = 0;
+ private boolean cgOverriden = false;
+
+ private boolean overrideSubcomponents = false;
+
+
+ // User-given name of the component
+ private String name = null;
+
+ // User-specified comment
+ private String comment = "";
+
+ // Unique ID of the component
+ private String id = null;
+
+ //// NOTE !!! All fields must be copied in the method copyFrom()! ////
+
+
+
+ /**
+ * Default constructor. Sets the name of the component to the component's static name
+ * and the relative position of the component.
+ */
+ public RocketComponent(Position relativePosition) {
+ // These must not fire any events, due to Rocket undo system initialization
+ this.name = getComponentName();
+ this.relativePosition = relativePosition;
+ this.id = UUID.randomUUID().toString();
+ }
+
+
+
+
+
+ //////////// Methods that must be implemented ////////////
+
+
+ /**
+ * Static component name. The name may not vary of the parameters, it must be static.
+ */
+ public abstract String getComponentName(); // Static component type name
+
+ /**
+ * Return the component mass (regardless of mass overriding).
+ */
+ public abstract double getComponentMass(); // Mass of non-overridden component
+
+ /**
+ * Return the component CG and mass (regardless of CG or mass overriding).
+ */
+ public abstract Coordinate getComponentCG(); // CG of non-overridden component
+
+
+ /**
+ * Return the longitudal (around the y- or z-axis) unitary moment of inertia.
+ * The unitary moment of inertia is the moment of inertia with the assumption that
+ * the mass of the component is one kilogram. The inertia is measured in
+ * respect to the non-overridden CG.
+ *
+ * @return the longitudal unitary moment of inertia of this component.
+ */
+ public abstract double getLongitudalUnitInertia();
+
+
+ /**
+ * Return the rotational (around the x-axis) unitary moment of inertia.
+ * The unitary moment of inertia is the moment of inertia with the assumption that
+ * the mass of the component is one kilogram. The inertia is measured in
+ * respect to the non-overridden CG.
+ *
+ * @return the rotational unitary moment of inertia of this component.
+ */
+ public abstract double getRotationalUnitInertia();
+
+
+
+
+ /**
+ * Test whether the given component type can be added to this component. This type safety
+ * is enforced by the <code>addChild()</code> methods. The return value of this method
+ * may change to reflect the current state of this component (e.g. two components of some
+ * type cannot be placed as children).
+ *
+ * @param type The RocketComponent class type to add.
+ * @return Whether such a component can be added.
+ */
+ public abstract boolean isCompatible(Class<? extends RocketComponent> type);
+
+
+ /* Non-abstract helper method */
+ /**
+ * Test whether the given component can be added to this component. This is equivalent
+ * to calling <code>isCompatible(c.getClass())</code>.
+ *
+ * @param c Component to test.
+ * @return Whether the component can be added.
+ * @see #isCompatible(Class)
+ */
+ public final boolean isCompatible(RocketComponent c) {
+ return isCompatible(c.getClass());
+ }
+
+
+
+ /**
+ * Return a collection of bounding coordinates. The coordinates must be such that
+ * the component is fully enclosed in their convex hull.
+ *
+ * @return a collection of coordinates that bound the component.
+ */
+ public abstract Collection<Coordinate> getComponentBounds();
+
+ /**
+ * Return true if the component may have an aerodynamic effect on the rocket.
+ */
+ public abstract boolean isAerodynamic();
+
+ /**
+ * Return true if the component may have an effect on the rocket's mass.
+ */
+ public abstract boolean isMassive();
+
+
+
+
+
+ //////////// Methods that may be overridden ////////////
+
+
+ /**
+ * Shift the coordinates in the array corresponding to radial movement. A component
+ * that has a radial position must shift the coordinates in this array suitably.
+ * If the component is clustered, then a new array must be returned with a
+ * coordinate for each cluster.
+ * <p>
+ * The default implementation simply returns the array, and thus produces no shift.
+ *
+ * @param c an array of coordinates to shift.
+ * @return an array of shifted coordinates. The method may modify the contents
+ * of the passed array and return the array itself.
+ */
+ public Coordinate[] shiftCoordinates(Coordinate[] c) {
+ return c;
+ }
+
+
+ /**
+ * Called when any component in the tree fires a ComponentChangeEvent. This is by
+ * default a no-op, but subclasses may override this method to e.g. invalidate
+ * cached data. The overriding method *must* call
+ * <code>super.componentChanged(e)</code> at some point.
+ *
+ * @param e The event fired
+ */
+ protected void componentChanged(ComponentChangeEvent e) {
+ // No-op
+ }
+
+
+
+
+ /**
+ * Return a descriptive name of the component.
+ *
+ * The description may include extra information about the type of component,
+ * e.g. "Conical nose cone".
+ *
+ * @return A string describing the component.
+ */
+ @Override
+ public final String toString() {
+ if (name.equals(""))
+ return getComponentName();
+ else
+ return name;
+ }
+
+
+ public final void printStructure() {
+ System.out.println("Rocket structure from '"+this.toString()+"':");
+ printStructure(0);
+ }
+
+ private void printStructure(int level) {
+ String s = "";
+
+ for (int i=0; i < level; i++) {
+ s += " ";
+ }
+ s += this.toString() + " (" + this.getComponentName()+")";
+ System.out.println(s);
+
+ for (RocketComponent c: children) {
+ c.printStructure(level+1);
+ }
+ }
+
+
+ /**
+ * Make a deep copy of the rocket component tree structure from this component
+ * downwards. This method does not fire any events.
+ * <p>
+ * This method must be overridden by any component that refers to mutable objects,
+ * or if some fields should not be copied. This should be performed by
+ * <code>RocketComponent c = super.copy();</code> and then cloning/modifying the
+ * appropriate fields.
+ * <p>
+ * This is not performed as serializing/deserializing for performance reasons.
+ *
+ * @return A deep copy of the structure.
+ */
+ public RocketComponent copy() {
+ RocketComponent clone;
+ try {
+ clone = (RocketComponent)this.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException("CloneNotSupportedException encountered, " +
+ "report a bug!",e);
+ }
+
+ // Reset all parent/child information
+ clone.parent = null;
+ clone.children = new ArrayList<RocketComponent>();
+
+ // Add copied children to the structure without firing events.
+ for (RocketComponent c: this.children) {
+ RocketComponent copy = c.copy();
+ clone.children.add(copy);
+ copy.parent = clone;
+ }
+
+ return clone;
+ }
+
+
+ ////////////// Methods that may not be overridden ////////////
+
+
+
+ ////////// Common parameter setting/getting //////////
+
+ /**
+ * Return the color of the object to use in 2D figures, or <code>null</code>
+ * to use the default color.
+ */
+ public final Color getColor() {
+ return color;
+ }
+
+ /**
+ * Set the color of the object to use in 2D figures.
+ */
+ public final void setColor(Color c) {
+ if ((color == null && c == null) ||
+ (color != null && color.equals(c)))
+ return;
+
+ this.color = c;
+ fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+ }
+
+
+ public final LineStyle getLineStyle() {
+ return lineStyle;
+ }
+
+ public final void setLineStyle(LineStyle style) {
+ if (this.lineStyle == style)
+ return;
+ this.lineStyle = style;
+ fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+ }
+
+
+
+
+ /**
+ * Get the current override mass. The mass is not necessarily in use
+ * at the moment.
+ *
+ * @return the override mass
+ */
+ public final double getOverrideMass() {
+ return overrideMass;
+ }
+
+ /**
+ * Set the current override mass. The mass is not set to use by this
+ * method.
+ *
+ * @param m the override mass
+ */
+ public final void setOverrideMass(double m) {
+ overrideMass = Math.max(m,0);
+ if (massOverriden)
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+ /**
+ * Return whether mass override is active for this component. This does NOT
+ * take into account whether a parent component is overriding the mass.
+ *
+ * @return whether the mass is overridden
+ */
+ public final boolean isMassOverridden() {
+ return massOverriden;
+ }
+
+ /**
+ * Set whether the mass is currently overridden.
+ *
+ * @param o whether the mass is overridden
+ */
+ public final void setMassOverridden(boolean o) {
+ if (massOverriden != o) {
+ massOverriden = o;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+ }
+
+
+
+
+
+ /**
+ * Return the current override CG. The CG is not necessarily overridden.
+ *
+ * @return the override CG
+ */
+ public final Coordinate getOverrideCG() {
+ return getComponentCG().setX(overrideCGX);
+ }
+
+ /**
+ * Return the x-coordinate of the current override CG.
+ *
+ * @return the x-coordinate of the override CG.
+ */
+ public final double getOverrideCGX() {
+ return overrideCGX;
+ }
+
+ /**
+ * Set the current override CG to (x,0,0).
+ *
+ * @param x the x-coordinate of the override CG to set.
+ */
+ public final void setOverrideCGX(double x) {
+ if (MathUtil.equals(overrideCGX, x))
+ return;
+ this.overrideCGX = x;
+ if (isCGOverridden())
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ else
+ fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+ }
+
+ /**
+ * Return whether the CG is currently overridden.
+ *
+ * @return whether the CG is overridden
+ */
+ public final boolean isCGOverridden() {
+ return cgOverriden;
+ }
+
+ /**
+ * Set whether the CG is currently overridden.
+ *
+ * @param o whether the CG is overridden
+ */
+ public final void setCGOverridden(boolean o) {
+ if (cgOverriden != o) {
+ cgOverriden = o;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+ }
+
+
+
+ /**
+ * Return whether the mass and/or CG override overrides all subcomponent values
+ * as well. The default implementation is a normal getter/setter implementation,
+ * however, subclasses are allowed to override this behavior if some subclass
+ * always or never overrides subcomponents. In this case the subclass should
+ * also override {@link #isOverrideSubcomponentsEnabled()} to return
+ * <code>false</code>.
+ *
+ * @return whether the current mass and/or CG override overrides subcomponents as well.
+ */
+ public boolean getOverrideSubcomponents() {
+ return overrideSubcomponents;
+ }
+
+
+ /**
+ * Set whether the mass and/or CG override overrides all subcomponent values
+ * as well. See {@link #getOverrideSubcomponents()} for details.
+ *
+ * @param override whether the mass and/or CG override overrides all subcomponent.
+ */
+ public void setOverrideSubcomponents(boolean override) {
+ if (overrideSubcomponents != override) {
+ overrideSubcomponents = override;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+ }
+
+ /**
+ * Return whether the option to override all subcomponents is enabled or not.
+ * The default implementation returns <code>false</code> if neither mass nor
+ * CG is overridden, <code>true</code> otherwise.
+ * <p>
+ * This method may be overridden if the setting of overriding subcomponents
+ * cannot be set.
+ *
+ * @return whether the option to override subcomponents is currently enabled.
+ */
+ public boolean isOverrideSubcomponentsEnabled() {
+ return isCGOverridden() || isMassOverridden();
+ }
+
+
+
+
+ /**
+ * Get the user-defined name of the component.
+ */
+ public final String getName() {
+ return name;
+ }
+
+ /**
+ * Set the user-defined name of the component. If name==null, sets the name to
+ * the default name, currently the component name.
+ */
+ public final void setName(String name) {
+// System.out.println("Set name called:"+name+" orig:"+this.name);
+ if (name==null || name.matches("^\\s*$"))
+ this.name = getComponentName();
+ else
+ this.name = name;
+ fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+ }
+
+
+ /**
+ * Return the comment of the component. The component may contain multiple lines
+ * using \n as a newline separator.
+ *
+ * @return the comment of the component.
+ */
+ public final String getComment() {
+ return comment;
+ }
+
+ /**
+ * Set the comment of the component.
+ *
+ * @param comment the comment of the component.
+ */
+ public final void setComment(String comment) {
+ if (this.comment.equals(comment))
+ return;
+ if (comment == null)
+ this.comment = "";
+ else
+ this.comment = comment;
+ fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+ }
+
+
+
+ /**
+ * Returns the unique ID of the component.
+ *
+ * @return the ID of the component.
+ */
+ public final String getID() {
+ return id;
+ }
+
+
+ /**
+ * Set the unique ID of the component. If <code>id</code> in <code>null</code> then
+ * this method generates a new unique ID for the component.
+ * <p>
+ * This method should be used only in special cases, such as when creating database
+ * entries with empty IDs.
+ *
+ * @param id the ID to set.
+ */
+ public final void setID(String id) {
+ if (id == null) {
+ this.id = UUID.randomUUID().toString();
+ } else {
+ this.id = id;
+ }
+ fireComponentChangeEvent(ComponentChangeEvent.NONFUNCTIONAL_CHANGE);
+ }
+
+
+
+
+ /**
+ * Get the characteristic length of the component, for example the length of a body tube
+ * of the length of the root chord of a fin. This is used in positioning the component
+ * relative to its parent.
+ *
+ * If the length of a component is settable, the class must define the setter method
+ * itself.
+ */
+ public final double getLength() {
+ return length;
+ }
+
+ /**
+ * Get the positioning of the component relative to its parent component.
+ * This is one of the enums of {@link Position}. A setter method is not provided,
+ * but can be provided by a subclass.
+ */
+ public final Position getRelativePosition() {
+ return relativePosition;
+ }
+
+
+ /**
+ * Set the positioning of the component relative to its parent component.
+ * The actual position of the component is maintained to the best ability.
+ * <p>
+ * The default implementation is of protected visibility, since many components
+ * do not support setting the relative position. A component that does support
+ * it should override this with a public method that simply calls this
+ * supermethod AND fire a suitable ComponentChangeEvent.
+ *
+ * @param position the relative positioning.
+ */
+ protected void setRelativePosition(RocketComponent.Position position) {
+ if (this.relativePosition == position)
+ return;
+
+ // Update position so as not to move the component
+ if (this.parent != null) {
+ double thisPos = this.toRelative(Coordinate.NUL,this.parent)[0].x;
+
+ switch (position) {
+ case ABSOLUTE:
+ this.position = this.toAbsolute(Coordinate.NUL)[0].x;
+ break;
+
+ case TOP:
+ this.position = thisPos;
+ break;
+
+ case MIDDLE:
+ this.position = thisPos - (this.parent.length - this.length)/2;
+ break;
+
+ case BOTTOM:
+ this.position = thisPos - (this.parent.length - this.length);
+ break;
+
+ default:
+ assert(false): "Should not occur";
+ }
+ }
+
+ this.relativePosition = position;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+
+
+ /**
+ * Get the position value of the component. The exact meaning of the value is
+ * dependent on the current relative positioning.
+ *
+ * @return the positional value.
+ */
+ public final double getPositionValue() {
+ return position;
+ }
+
+
+ /**
+ * Set the position value of the component. The exact meaning of the value
+ * depends on the current relative positioning.
+ * <p>
+ * The default implementation is of protected visibility, since many components
+ * do not support setting the relative position. A component that does support
+ * it should override this with a public method that simply calls this
+ * supermethod AND fire a suitable ComponentChangeEvent.
+ *
+ * @param value the position value of the component.
+ */
+ public void setPositionValue(double value) {
+ if (MathUtil.equals(this.position, value))
+ return;
+ this.position = value;
+ }
+
+
+
+ /////////// Coordinate changes ///////////
+
+ /**
+ * Returns coordinate c in absolute coordinates. Equivalent to toComponent(c,null).
+ */
+ public Coordinate[] toAbsolute(Coordinate c) {
+ return toRelative(c,null);
+ }
+
+
+ /**
+ * Return coordinate <code>c</code> described in the coordinate system of
+ * <code>dest</code>. If <code>dest</code> is <code>null</code> returns
+ * absolute coordinates.
+ * <p>
+ * This method returns an array of coordinates, each of which represents a
+ * position of the coordinate in clustered cases. The array is guaranteed
+ * to contain at least one element.
+ * <p>
+ * The current implementation does not support rotating components.
+ *
+ * @param c Coordinate in the component's coordinate system.
+ * @param dest Destination component coordinate system.
+ * @return an array of coordinates describing <code>c</code> in coordinates
+ * relative to <code>dest</code>.
+ */
+ public final Coordinate[] toRelative(Coordinate c, RocketComponent dest) {
+ double absoluteX = Double.NaN;
+ RocketComponent search = dest;
+ Coordinate[] array = new Coordinate[1];
+ array[0] = c;
+
+ RocketComponent component = this;
+ while ((component != search) && (component.parent != null)) {
+
+ array = component.shiftCoordinates(array);
+
+ switch (component.relativePosition) {
+ case TOP:
+ for (int i=0; i < array.length; i++) {
+ array[i] = array[i].add(component.position,0,0);
+ }
+ break;
+
+ case MIDDLE:
+ for (int i=0; i < array.length; i++) {
+ array[i] = array[i].add(component.position +
+ (component.parent.length-component.length)/2,0,0);
+ }
+ break;
+
+ case BOTTOM:
+ for (int i=0; i < array.length; i++) {
+ array[i] = array[i].add(component.position +
+ (component.parent.length-component.length),0,0);
+ }
+ break;
+
+ case AFTER:
+ // Add length of all previous brother-components with POSITION_RELATIVE_AFTER
+ int index = component.parent.children.indexOf(component);
+ assert(index >= 0);
+ for (index--; index >= 0; index--) {
+ RocketComponent comp = component.parent.children.get(index);
+ double length = comp.getTotalLength();
+ for (int i=0; i < array.length; i++) {
+ array[i] = array[i].add(length,0,0);
+ }
+ }
+ for (int i=0; i < array.length; i++) {
+ array[i] = array[i].add(component.position + component.parent.length,0,0);
+ }
+ break;
+
+ case ABSOLUTE:
+ search = null; // Requires back-search if dest!=null
+ if (Double.isNaN(absoluteX)) {
+ absoluteX = component.position;
+ }
+ break;
+
+ default:
+ throw new RuntimeException("Unknown relative positioning type of component"+
+ component+": "+component.relativePosition);
+ }
+
+ component = component.parent; // parent != null
+ }
+
+ if (!Double.isNaN(absoluteX)) {
+ for (int i=0; i < array.length; i++) {
+ array[i] = array[i].setX(absoluteX + c.x);
+ }
+ }
+
+ // Check whether destination has been found or whether to backtrack
+ // TODO: LOW: Backtracking into clustered components uses only one component
+ if ((dest != null) && (component != dest)) {
+ Coordinate[] origin = dest.toAbsolute(Coordinate.NUL);
+ for (int i=0; i < array.length; i++) {
+ array[i] = array[i].sub(origin[0]);
+ }
+ }
+
+ return array;
+ }
+
+
+ /**
+ * Recursively sum the lengths of all subcomponents that have position
+ * Position.AFTER.
+ *
+ * @return Sum of the lengths.
+ */
+ private final double getTotalLength() {
+ double l=0;
+ if (relativePosition == Position.AFTER)
+ l = length;
+ for (int i=0; i<children.size(); i++)
+ l += children.get(i).getTotalLength();
+ return l;
+ }
+
+
+
+ /////////// Total mass and CG calculation ////////////
+
+ /**
+ * Return the (possibly overridden) mass of component.
+ *
+ * @return The mass of the component or the given override mass.
+ */
+ public final double getMass() {
+ if (massOverriden)
+ return overrideMass;
+ return getComponentMass();
+ }
+
+ /**
+ * Return the (possibly overridden) center of gravity and mass.
+ *
+ * Returns the CG with the weight of the coordinate set to the weight of the component.
+ * Both CG and mass may be separately overridden.
+ *
+ * @return The CG of the component or the given override CG.
+ */
+ public final Coordinate getCG() {
+ if (cgOverriden)
+ return getOverrideCG().setWeight(getMass());
+
+ if (massOverriden)
+ return getComponentCG().setWeight(getMass());
+
+ return getComponentCG();
+ }
+
+
+ /**
+ * Return the longitudal (around the y- or z-axis) moment of inertia of this component.
+ * The moment of inertia is scaled in reference to the (possibly overridden) mass
+ * and is relative to the non-overridden CG.
+ *
+ * @return the longitudal moment of inertia of this component.
+ */
+ public final double getLongitudalInertia() {
+ return getLongitudalUnitInertia() * getMass();
+ }
+
+ /**
+ * Return the rotational (around the y- or z-axis) moment of inertia of this component.
+ * The moment of inertia is scaled in reference to the (possibly overridden) mass
+ * and is relative to the non-overridden CG.
+ *
+ * @return the rotational moment of inertia of this component.
+ */
+ public final double getRotationalInertia() {
+ return getRotationalUnitInertia() * getMass();
+ }
+
+
+
+ /////////// Children handling ///////////
+
+
+ /**
+ * Adds a child to the rocket component tree. The component is added to the end
+ * of the component's child list. This is a helper method that calls
+ * {@link #addChild(RocketComponent,int)}.
+ *
+ * @param component The component to add.
+ * @throws IllegalArgumentException if the component is already part of some
+ * component tree.
+ * @see #addChild(RocketComponent,int)
+ */
+ public final void addChild(RocketComponent component) {
+ addChild(component,children.size());
+ }
+
+
+ /**
+ * Adds a child to the rocket component tree. The component is added to
+ * the given position of the component's child list.
+ * <p>
+ * This method may be overridden to enforce more strict component addition rules.
+ * The tests should be performed first and then this method called.
+ *
+ * @param component The component to add.
+ * @param position Position to add component to.
+ * @throws IllegalArgumentException If the component is already part of
+ * some component tree.
+ */
+ public void addChild(RocketComponent component, int position) {
+ if (component.parent != null) {
+ throw new IllegalArgumentException("component "+component.getComponentName()+
+ " is already in a tree");
+ }
+ if (!isCompatible(component)) {
+ throw new IllegalStateException("Component "+component.getComponentName()+
+ " not currently compatible with component "+getComponentName());
+ }
+
+ children.add(position,component);
+ component.parent = this;
+
+ fireAddRemoveEvent(component);
+ }
+
+
+ /**
+ * Removes a child from the rocket component tree.
+ *
+ * @param n remove the n'th child.
+ * @throws IndexOutOfBoundsException if n is out of bounds
+ */
+ public final void removeChild(int n) {
+ RocketComponent component = children.remove(n);
+ component.parent = null;
+ fireAddRemoveEvent(component);
+ }
+
+ /**
+ * Removes a child from the rocket component tree. Does nothing if the component
+ * is not present as a child.
+ *
+ * @param component the component to remove
+ */
+ public final void removeChild(RocketComponent component) {
+ if (children.remove(component)) {
+ component.parent = null;
+
+ fireAddRemoveEvent(component);
+ }
+ }
+
+
+
+
+ /**
+ * Move a child to another position.
+ *
+ * @param component the component to move
+ * @param position the component's new position
+ * @throws IllegalArgumentException If an illegal placement was attempted.
+ */
+ public final void moveChild(RocketComponent component, int position) {
+ if (children.remove(component)) {
+ children.add(position, component);
+ fireAddRemoveEvent(component);
+ }
+ }
+
+
+ /**
+ * Fires an AERODYNAMIC_CHANGE, MASS_CHANGE or OTHER_CHANGE event depending on the
+ * type of component removed.
+ */
+ private void fireAddRemoveEvent(RocketComponent component) {
+ Iterator<RocketComponent> iter = component.deepIterator(true);
+ int type = ComponentChangeEvent.TREE_CHANGE;
+ while (iter.hasNext()) {
+ RocketComponent c = iter.next();
+ if (c.isAerodynamic())
+ type |= ComponentChangeEvent.AERODYNAMIC_CHANGE;
+ if (c.isMassive())
+ type |= ComponentChangeEvent.MASS_CHANGE;
+ }
+
+ fireComponentChangeEvent(type);
+ }
+
+
+ public final int getChildCount() {
+ return children.size();
+ }
+
+ public final RocketComponent getChild(int n) {
+ return children.get(n);
+ }
+
+ public final RocketComponent[] getChildren() {
+ return children.toArray(new RocketComponent[0]);
+ }
+
+
+ /**
+ * Returns the position of the child in this components child list, or -1 if the
+ * component is not a child of this component.
+ *
+ * @param child The child to search for.
+ * @return Position in the list or -1 if not found.
+ */
+ public final int getChildPosition(RocketComponent child) {
+ return children.indexOf(child);
+ }
+
+ /**
+ * Get the parent component of this component. Returns <code>null</code> if the component
+ * has no parent.
+ *
+ * @return The parent of this component or <code>null</code>.
+ */
+ public final RocketComponent getParent() {
+ return parent;
+ }
+
+ /**
+ * Get the root component of the component tree.
+ *
+ * @return The root component of the component tree.
+ */
+ public final RocketComponent getRoot() {
+ RocketComponent gp = this;
+ while (gp.parent != null)
+ gp = gp.parent;
+ return gp;
+ }
+
+ /**
+ * Returns the root Rocket component of this component tree. Throws an
+ * IllegalStateException if the root component is not a Rocket.
+ *
+ * @return The root Rocket component of the component tree.
+ * @throws IllegalStateException If the root component is not a Rocket.
+ */
+ public final Rocket getRocket() {
+ RocketComponent r = getRoot();
+ if (r instanceof Rocket)
+ return (Rocket)r;
+ throw new IllegalStateException("getRocket() called with root component "
+ +r.getComponentName());
+ }
+
+
+ /**
+ * Return the Stage component that this component belongs to. Throws an
+ * IllegalStateException if a Stage is not in the parentage of this component.
+ *
+ * @return The Stage component this component belongs to.
+ * @throws IllegalStateException if a Stage component is not in the parentage.
+ */
+ public final Stage getStage() {
+ RocketComponent c = this;
+ while (c != null) {
+ if (c instanceof Stage)
+ return (Stage)c;
+ c = c.getParent();
+ }
+ throw new IllegalStateException("getStage() called without Stage as a parent.");
+ }
+
+ /**
+ * Return the stage number of the stage this component belongs to. The stages
+ * are numbered from zero upwards.
+ *
+ * @return the stage number this component belongs to.
+ */
+ public final int getStageNumber() {
+ if (parent == null) {
+ throw new IllegalArgumentException("getStageNumber() called for root component");
+ }
+
+ RocketComponent stage = this;
+ while (!(stage instanceof Stage)) {
+ stage = stage.parent;
+ }
+ return stage.parent.getChildPosition(stage);
+ }
+
+
+ /**
+ * Find a component with the given ID. The component tree is searched from this component
+ * down (including this component) for the ID and the corresponding component is returned,
+ * or null if not found.
+ *
+ * @param id ID to search for.
+ * @return The component with the ID, or null if not found.
+ */
+ public final RocketComponent findComponent(String id) {
+ Iterator<RocketComponent> iter = this.deepIterator(true);
+ while (iter.hasNext()) {
+ RocketComponent c = iter.next();
+ if (c.id.equals(id))
+ return c;
+ }
+ return null;
+ }
+
+
+ public final RocketComponent getPreviousComponent() {
+ if (parent == null)
+ return null;
+ int pos = parent.getChildPosition(this);
+ assert(pos >= 0);
+ if (pos == 0)
+ return parent;
+ RocketComponent c = parent.getChild(pos-1);
+ while (c.getChildCount() > 0)
+ c = c.getChild(c.getChildCount()-1);
+ return c;
+ }
+
+ public final RocketComponent getNextComponent() {
+ if (getChildCount() > 0)
+ return getChild(0);
+
+ RocketComponent current = this;
+ RocketComponent parent = this.parent;
+
+ while (parent != null) {
+ int pos = parent.getChildPosition(current);
+ if (pos < parent.getChildCount()-1)
+ return parent.getChild(pos+1);
+
+ current = parent;
+ parent = current.parent;
+ }
+ return null;
+ }
+
+
+ /////////// Event handling //////////
+ //
+ // Listener lists are provided by the root Rocket component,
+ // a single listener list for the whole rocket.
+ //
+
+ /**
+ * Adds a ComponentChangeListener to the rocket tree. The listener is added to the root
+ * component, which must be of type Rocket (which overrides this method). Events of all
+ * subcomponents are sent to all listeners.
+ *
+ * @throws IllegalStateException - if the root component is not a Rocket
+ */
+ public void addComponentChangeListener(ComponentChangeListener l) {
+ getRocket().addComponentChangeListener(l);
+ }
+
+ /**
+ * Removes a ComponentChangeListener from the rocket tree. The listener is removed from
+ * the root component, which must be of type Rocket (which overrides this method).
+ *
+ * @param l Listener to remove
+ * @throws IllegalStateException - if the root component is not a Rocket
+ */
+ public void removeComponentChangeListener(ComponentChangeListener l) {
+ getRocket().removeComponentChangeListener(l);
+ }
+
+
+ /**
+ * Adds a <code>ChangeListener</code> to the rocket tree. This is identical to
+ * <code>addComponentChangeListener()</code> except that it uses a
+ * <code>ChangeListener</code>. The same events are dispatched to the
+ * <code>ChangeListener</code>, as <code>ComponentChangeEvent</code> is a subclass
+ * of <code>ChangeEvent</code>.
+ *
+ * @throws IllegalStateException - if the root component is not a <code>Rocket</code>
+ */
+ public void addChangeListener(ChangeListener l) {
+ getRocket().addChangeListener(l);
+ }
+
+ /**
+ * Removes a ChangeListener from the rocket tree. This is identical to
+ * removeComponentChangeListener() except it uses a ChangeListener.
+ *
+ * @param l Listener to remove
+ * @throws IllegalStateException - if the root component is not a Rocket
+ */
+ public void removeChangeListener(ChangeListener l) {
+ getRocket().removeChangeListener(l);
+ }
+
+
+ /**
+ * Fires a ComponentChangeEvent on the rocket structure. The call is passed to the
+ * root component, which must be of type Rocket (which overrides this method).
+ * Events of all subcomponents are sent to all listeners.
+ *
+ * If the component tree root is not a Rocket, the event is ignored. This is the
+ * case when constructing components not in any Rocket tree. In this case it
+ * would be impossible for the component to have listeners in any case.
+ *
+ * @param e Event to send
+ */
+ protected void fireComponentChangeEvent(ComponentChangeEvent e) {
+ if (parent==null) {
+ /* Ignore if root invalid. */
+ return;
+ }
+ getRoot().fireComponentChangeEvent(e);
+ }
+
+
+ /**
+ * Fires a ComponentChangeEvent of the given type. The source of the event is set to
+ * this component.
+ *
+ * @param type Type of event
+ * @see #fireComponentChangeEvent(ComponentChangeEvent)
+ */
+ protected void fireComponentChangeEvent(int type) {
+ fireComponentChangeEvent(new ComponentChangeEvent(this,type));
+ }
+
+
+
+ /////////// Iterator implementation //////////
+
+ /**
+ * Private inner class to implement the Iterator.
+ *
+ * This iterator is fail-fast if the root of the structure is a Rocket.
+ */
+ private class RocketComponentIterator implements Iterator<RocketComponent> {
+ // Stack holds iterators which still have some components left.
+ private final Stack<Iterator<RocketComponent>> iteratorstack =
+ new Stack<Iterator<RocketComponent>>();
+
+ private final Rocket root;
+ private final int treeModID;
+
+ private final RocketComponent original;
+ private boolean returnSelf=false;
+
+ // Construct iterator with component's child's iterator, if it has elements
+ public RocketComponentIterator(RocketComponent c, boolean returnSelf) {
+
+ RocketComponent gp = c.getRoot();
+ if (gp instanceof Rocket) {
+ root = (Rocket)gp;
+ treeModID = root.getTreeModID();
+ } else {
+ root = null;
+ treeModID = -1;
+ }
+
+ Iterator<RocketComponent> i = c.children.iterator();
+ if (i.hasNext())
+ iteratorstack.push(i);
+
+ this.original = c;
+ this.returnSelf = returnSelf;
+ }
+
+ public boolean hasNext() {
+ checkID();
+ if (returnSelf)
+ return true;
+ return !iteratorstack.empty(); // Elements remain if stack is not empty
+ }
+
+ public RocketComponent next() {
+ Iterator<RocketComponent> i;
+
+ checkID();
+
+ // Return original component first
+ if (returnSelf) {
+ returnSelf=false;
+ return original;
+ }
+
+ // Peek first iterator from stack, throw exception if empty
+ try {
+ i = iteratorstack.peek();
+ } catch (EmptyStackException e) {
+ throw new NoSuchElementException("No further elements in " +
+ "RocketComponent iterator");
+ }
+
+ // Retrieve next component of the iterator, remove iterator from stack if empty
+ RocketComponent c = i.next();
+ if (!i.hasNext())
+ iteratorstack.pop();
+
+ // Add iterator of component children to stack if it has children
+ i = c.children.iterator();
+ if (i.hasNext())
+ iteratorstack.push(i);
+
+ return c;
+ }
+
+ private void checkID() {
+ if (root != null) {
+ if (root.getTreeModID() != treeModID) {
+ throw new IllegalStateException("Rocket modified while being iterated");
+ }
+ }
+ }
+
+ public void remove() {
+ throw new UnsupportedOperationException("remove() not supported by " +
+ "RocketComponent iterator");
+ }
+ }
+
+ /**
+ * Returns an iterator that iterates over all children and sub-children.
+ *
+ * The iterator iterates through all children below this object, including itself if
+ * returnSelf is true. The order of the iteration is not specified
+ * (it may be specified in the future).
+ *
+ * If an iterator iterating over only the direct children of the component is required,
+ * use component.getChildren().iterator()
+ *
+ * @param returnSelf boolean value specifying whether the component itself should be
+ * returned
+ * @return An iterator for the children and sub-children.
+ */
+ public final Iterator<RocketComponent> deepIterator(boolean returnSelf) {
+ return new RocketComponentIterator(this,returnSelf);
+ }
+
+ /**
+ * Returns an iterator that iterates over all children and sub-children.
+ *
+ * The iterator does NOT return the component itself. It is thus equivalent to
+ * deepIterator(false).
+ *
+ * @see #iterator()
+ * @return An iterator for the children and sub-children.
+ */
+ public final Iterator<RocketComponent> deepIterator() {
+ return new RocketComponentIterator(this,false);
+ }
+
+
+ /**
+ * Return an iterator that iterates of the children of the component. The iterator
+ * does NOT recurse to sub-children nor return itself.
+ *
+ * @return An iterator for the children.
+ */
+ public final Iterator<RocketComponent> iterator() {
+ return Collections.unmodifiableList(children).iterator();
+ }
+
+ //////////// Helper methods for subclasses
+
+ /**
+ * Helper method to add rotationally symmetric bounds at the specified coordinates.
+ * The X-axis value is <code>x</code> and the radius at the specified position is
+ * <code>r</code>.
+ */
+ protected static final void addBound(Collection<Coordinate> bounds, double x, double r) {
+ bounds.add(new Coordinate(x,-r,-r));
+ bounds.add(new Coordinate(x, r,-r));
+ bounds.add(new Coordinate(x, r, r));
+ bounds.add(new Coordinate(x,-r, r));
+ }
+
+
+ protected static final Coordinate ringCG(double outerRadius, double innerRadius,
+ double x1, double x2, double density) {
+ return new Coordinate((x1+x2)/2, 0, 0,
+ ringMass(outerRadius, innerRadius, x2-x1, density));
+ }
+
+ protected static final double ringMass(double outerRadius, double innerRadius,
+ double length, double density) {
+ return Math.PI*(MathUtil.pow2(outerRadius) - MathUtil.pow2(innerRadius)) *
+ length * density;
+ }
+
+ protected static final double ringLongitudalUnitInertia(double outerRadius,
+ double innerRadius, double length) {
+ // 1/12 * (3 * (r1^2 + r2^2) + h^2)
+ return (3 * (MathUtil.pow2(innerRadius) + MathUtil.pow2(outerRadius)) +
+ MathUtil.pow2(length)) / 12;
+ }
+
+ protected static final double ringRotationalUnitInertia(double outerRadius,
+ double innerRadius) {
+ // 1/2 * (r1^2 + r2^2)
+ return (MathUtil.pow2(innerRadius) + MathUtil.pow2(outerRadius))/2;
+ }
+
+
+
+ //////////// OTHER
+
+
+ /**
+ * Loads the RocketComponent fields from the given component. This method is meant
+ * for use with the undo/redo mechanism.
+ *
+ * The fields are copied by reference, and the supplied component must not be used
+ * after the call, as it is in an undefined state.
+ *
+ * TODO: MEDIUM: Make general to copy all private/protected fields...
+ */
+ protected void copyFrom(RocketComponent src) {
+ // Set parents and children
+ this.parent = null;
+ this.children = src.children;
+ src.children = new ArrayList<RocketComponent>();
+
+ for (RocketComponent c: this.children) {
+ c.parent = this;
+ }
+
+ // Set all parameters
+ this.length = src.length;
+ this.relativePosition = src.relativePosition;
+ this.position = src.position;
+ this.color = src.color;
+ this.lineStyle = src.lineStyle;
+ this.overrideMass = src.overrideMass;
+ this.massOverriden = src.massOverriden;
+ this.overrideCGX = src.overrideCGX;
+ this.cgOverriden = src.cgOverriden;
+ this.overrideSubcomponents = src.overrideSubcomponents;
+ this.name = src.name;
+ this.comment = src.comment;
+ this.id = src.id;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Prefs;
+
+public class ShockCord extends MassObject {
+
+ private Material material;
+ private double cordLength;
+
+ public ShockCord() {
+ material = Prefs.getDefaultComponentMaterial(ShockCord.class, Material.Type.LINE);
+ cordLength = 0.4;
+ }
+
+
+
+ public Material getMaterial() {
+ return material;
+ }
+
+ public void setMaterial(Material m) {
+ if (m.getType() != Material.Type.LINE)
+ throw new RuntimeException("Attempting to set non-linear material.");
+ if (material.equals(m))
+ return;
+ this.material = m;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+ public double getCordLength() {
+ return cordLength;
+ }
+
+ public void setCordLength(double length) {
+ length = MathUtil.max(length, 0);
+ if (MathUtil.equals(length, this.length))
+ return;
+ this.cordLength = length;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+
+ @Override
+ public double getComponentMass() {
+ return material.getDensity() * cordLength;
+ }
+
+ @Override
+ public String getComponentName() {
+ return "Shock cord";
+ }
+
+ @Override
+ public boolean isCompatible(Class<? extends RocketComponent> type) {
+ return false;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+/**
+ * A RingComponent that comes on top of another tube. It's defined by the inner
+ * radius and thickness. The inner radius can be automatic, in which case it
+ * takes the radius of the parent component.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class Sleeve extends RingComponent {
+
+ protected double innerRadius = 0;
+ protected double thickness = 0;
+
+
+ public Sleeve() {
+ super();
+ setInnerRadiusAutomatic(true);
+ setThickness(0.001);
+ setLength(0.05);
+ }
+
+
+ @Override
+ public double getOuterRadius() {
+ return getInnerRadius() + thickness;
+ }
+
+ @Override
+ public void setOuterRadius(double r) {
+ if (MathUtil.equals(getOuterRadius(), r))
+ return;
+
+ innerRadius = Math.max(r - thickness, 0);
+ if (thickness > r)
+ thickness = r;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+ @Override
+ public double getInnerRadius() {
+ // Implement parent inner radius automation
+ if (isInnerRadiusAutomatic() && getParent() instanceof RadialParent) {
+ RocketComponent parent = getParent();
+ double pos1 = this.toRelative(Coordinate.NUL, parent)[0].x;
+ double pos2 = this.toRelative(new Coordinate(getLength()), parent)[0].x;
+ pos1 = MathUtil.clamp(pos1, 0, parent.getLength());
+ pos2 = MathUtil.clamp(pos2, 0, parent.getLength());
+ innerRadius = Math.max(((RadialParent)parent).getOuterRadius(pos1),
+ ((RadialParent)parent).getOuterRadius(pos2));
+ }
+
+ return innerRadius;
+ }
+
+ @Override
+ public void setInnerRadius(double r) {
+ r = Math.max(r,0);
+ if (MathUtil.equals(innerRadius, r))
+ return;
+ innerRadius = r;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+ @Override
+ public double getThickness() {
+ return thickness;
+ }
+
+ @Override
+ public void setThickness(double t) {
+ t = Math.max(t, 0);
+ if (MathUtil.equals(thickness, t))
+ return;
+ thickness = t;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+
+
+ @Override
+ public void setInnerRadiusAutomatic(boolean auto) {
+ super.setOuterRadiusAutomatic(auto);
+ }
+
+ @Override
+ public String getComponentName() {
+ return "Sleeve";
+ }
+
+ @Override
+ public boolean isCompatible(Class<? extends RocketComponent> type) {
+ return false;
+ }
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+public class Stage extends ComponentAssembly {
+
+ @Override
+ public String getComponentName() {
+ return "Stage";
+ }
+
+
+ /**
+ * Check whether the given type can be added to this component. A Stage allows
+ * only BodyComponents to be added.
+ *
+ * @param type The RocketComponent class type to add.
+ * @return Whether such a component can be added.
+ */
+ @Override
+ public boolean isCompatible(Class<? extends RocketComponent> type) {
+ return BodyComponent.class.isAssignableFrom(type);
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.util.MathUtil;
+
+public class Streamer extends RecoveryDevice {
+
+ public static final double DEFAULT_CD = 0.6;
+
+ public static final double MAX_COMPUTED_CD = 0.4;
+
+
+ private double stripLength;
+ private double stripWidth;
+
+
+ public Streamer() {
+ this.stripLength = 0.5;
+ this.stripWidth = 0.05;
+ }
+
+
+ public double getStripLength() {
+ return stripLength;
+ }
+
+ public void setStripLength(double stripLength) {
+ if (MathUtil.equals(this.stripLength, stripLength))
+ return;
+ this.stripLength = stripLength;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+ public double getStripWidth() {
+ return stripWidth;
+ }
+
+ public void setStripWidth(double stripWidth) {
+ if (MathUtil.equals(this.stripWidth, stripWidth))
+ return;
+ this.stripWidth = stripWidth;
+ this.length = stripWidth;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+ @Override
+ public void setLength(double length) {
+ setStripWidth(length);
+ }
+
+
+ public double getAspectRatio() {
+ if (stripWidth > 0.0001)
+ return stripLength/stripWidth;
+ return 1000;
+ }
+
+ public void setAspectRatio(double ratio) {
+ if (MathUtil.equals(getAspectRatio(), ratio))
+ return;
+
+ ratio = Math.max(ratio, 0.01);
+ double area = getArea();
+ stripWidth = Math.sqrt(area / ratio);
+ stripLength = ratio * stripWidth;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+ @Override
+ public double getArea() {
+ return stripWidth * stripLength;
+ }
+
+ public void setArea(double area) {
+ if (MathUtil.equals(getArea(), area))
+ return;
+
+ double ratio = Math.max(getAspectRatio(), 0.01);
+ stripWidth = Math.sqrt(area / ratio);
+ stripLength = ratio * stripWidth;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+
+ @Override
+ public double getComponentCD(double mach) {
+ double density = this.getMaterial().getDensity();
+ double cd;
+
+ cd = 0.034 * ((density + 0.025)/0.105) * (stripLength+1) / stripLength;
+ cd = MathUtil.min(cd, MAX_COMPUTED_CD);
+ return cd;
+ }
+
+ @Override
+ public String getComponentName() {
+ return "Streamer";
+ }
+
+ @Override
+ public boolean isCompatible(Class<? extends RocketComponent> type) {
+ return false;
+ }
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.util.Prefs;
+
+public abstract class StructuralComponent extends InternalComponent {
+
+ private Material material;
+
+ public StructuralComponent() {
+ super();
+ material = Prefs.getDefaultComponentMaterial(this.getClass(), Material.Type.BULK);
+ }
+
+
+ public final Material getMaterial() {
+ return material;
+ }
+
+ public final void setMaterial(Material mat) {
+ if (mat.getType() != Material.Type.BULK) {
+ throw new IllegalArgumentException("Attempted to set non-bulk material "+mat);
+ }
+ if (mat.equals(material))
+ return;
+ this.material = mat;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import static net.sf.openrocket.util.MathUtil.pow2;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+/**
+ * Class for an axially symmetric rocket component generated by rotating
+ * a function y=f(x) >= 0 around the x-axis (eg. tube, cone, etc.)
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public abstract class SymmetricComponent extends BodyComponent implements RadialParent {
+ public static final double DEFAULT_RADIUS = 0.025;
+ public static final double DEFAULT_THICKNESS = 0.002;
+
+ private static final int DIVISIONS = 100; // No. of divisions when integrating
+
+ protected boolean filled = false;
+ protected double thickness = DEFAULT_THICKNESS;
+
+
+ // Cached data, default values signify not calculated
+ private double wetArea = -1;
+ private double planArea = -1;
+ private double planCenter = -1;
+ private double volume = -1;
+ private double fullVolume = -1;
+ private double longitudalInertia = -1;
+ private double rotationalInertia = -1;
+ private Coordinate cg = null;
+
+
+
+ public SymmetricComponent() {
+ super();
+ }
+
+
+ /**
+ * Return the component radius at position x.
+ * @param x Position on x-axis.
+ * @return Radius of the component at the given position, or 0 if outside
+ * the component.
+ */
+ public abstract double getRadius(double x);
+ public abstract double getInnerRadius(double x);
+
+ public abstract double getForeRadius();
+ public abstract boolean isForeRadiusAutomatic();
+ public abstract double getAftRadius();
+ public abstract boolean isAftRadiusAutomatic();
+
+
+ // Implement the Radial interface:
+ public final double getOuterRadius(double x) {
+ return getRadius(x);
+ }
+
+
+ @Override
+ public final double getRadius(double x, double theta) {
+ return getRadius(x);
+ }
+
+ @Override
+ public final double getInnerRadius(double x, double theta) {
+ return getInnerRadius(x);
+ }
+
+
+
+ /**
+ * Return the component wall thickness.
+ */
+ public double getThickness() {
+ if (filled)
+ return Math.max(getForeRadius(),getAftRadius());
+ return Math.min(thickness,Math.max(getForeRadius(),getAftRadius()));
+ }
+
+
+ /**
+ * Set the component wall thickness. Values greater than the maximum radius are not
+ * allowed, and will result in setting the thickness to the maximum radius.
+ */
+ public void setThickness(double thickness) {
+ if ((this.thickness == thickness) && !filled)
+ return;
+ this.thickness = MathUtil.clamp(thickness,0,Math.max(getForeRadius(),getAftRadius()));
+ filled = false;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+ /**
+ * Returns whether the component is set as filled. If it is set filled, then the
+ * wall thickness will have no effect.
+ */
+ public boolean isFilled() {
+ return filled;
+ }
+
+
+ /**
+ * Sets whether the component is set as filled. If the component is filled, then
+ * the wall thickness will have no effect.
+ */
+ public void setFilled(boolean filled) {
+ if (this.filled == filled)
+ return;
+ this.filled = filled;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+ /**
+ * Adds component bounds at a number of points between 0...length.
+ */
+ @Override
+ public Collection<Coordinate> getComponentBounds() {
+ List<Coordinate> list = new ArrayList<Coordinate>(20);
+ for (int n=0; n<=5; n++) {
+ double x = n*length/5;
+ double r = getRadius(x);
+ addBound(list,x,r);
+ }
+ return list;
+ }
+
+
+
+ /**
+ * Calculate volume of the component by integrating over the length of the component.
+ * The method caches the result, so subsequent calls are instant. Subclasses may
+ * override this method for simple shapes and use this method as necessary.
+ *
+ * @return The volume of the component.
+ */
+ @Override
+ public double getComponentVolume() {
+ if (volume < 0)
+ integrate();
+ return volume;
+ }
+
+
+ /**
+ * Calculate full (filled) volume of the component by integrating over the length
+ * of the component. The method caches the result, so subsequent calls are instant.
+ * Subclasses may override this method for simple shapes and use this method as
+ * necessary.
+ *
+ * @return The filled volume of the component.
+ */
+ public double getFullVolume() {
+ if (fullVolume < 0)
+ integrate();
+ return fullVolume;
+ }
+
+
+ /**
+ * Calculate the wetted area of the component by integrating over the length
+ * of the component. The method caches the result, so subsequent calls are instant.
+ * Subclasses may override this method for simple shapes and use this method as
+ * necessary.
+ *
+ * @return The wetted area of the component.
+ */
+ public double getComponentWetArea() {
+ if (wetArea < 0)
+ integrate();
+ return wetArea;
+ }
+
+
+ /**
+ * Calculate the planform area of the component by integrating over the length of
+ * the component. The method caches the result, so subsequent calls are instant.
+ * Subclasses may override this method for simple shapes and use this method as
+ * necessary.
+ *
+ * @return The planform area of the component.
+ */
+ public double getComponentPlanformArea() {
+ if (planArea < 0)
+ integrate();
+ return planArea;
+ }
+
+
+ /**
+ * Calculate the planform center X-coordinate of the component by integrating over
+ * the length of the component. The planform center is defined as
+ * <pre> integrate(x*2*r(x)) / planform area </pre>
+ * The method caches the result, so subsequent calls are instant. Subclasses may
+ * override this method for simple shapes and use this method as necessary.
+ *
+ * @return The planform center of the component.
+ */
+ public double getComponentPlanformCenter() {
+ if (planCenter < 0)
+ integrate();
+ return planCenter;
+ }
+
+
+ /**
+ * Calculate CG of the component by integrating over the length of the component.
+ * The method caches the result, so subsequent calls are instant. Subclasses may
+ * override this method for simple shapes and use this method as necessary.
+ *
+ * @return The CG+mass of the component.
+ */
+ @Override
+ public Coordinate getComponentCG() {
+ if (cg == null)
+ integrate();
+ return cg;
+ }
+
+
+ @Override
+ public double getLongitudalUnitInertia() {
+ if (longitudalInertia < 0) {
+ if (getComponentVolume() > 0.0000001) // == 0.1cm^3
+ integrateInertiaVolume();
+ else
+ integrateInertiaSurface();
+ }
+ return longitudalInertia;
+ }
+
+
+ @Override
+ public double getRotationalUnitInertia() {
+ if (rotationalInertia < 0) {
+ if (getComponentVolume() > 0.0000001)
+ integrateInertiaVolume();
+ else
+ integrateInertiaSurface();
+ }
+ return rotationalInertia;
+ }
+
+
+
+ /**
+ * Performs integration over the length of the component and updates the cached variables.
+ */
+ private void integrate() {
+ double x,r1,r2;
+ double cgx;
+
+ // Check length > 0
+ if (length <= 0) {
+ wetArea = 0;
+ planArea = 0;
+ planCenter = 0;
+ volume = 0;
+ cg = Coordinate.NUL;
+ return;
+ }
+
+
+ // Integrate for volume, CG, wetted area and planform area
+
+ final double l = length/DIVISIONS;
+ final double pil = Math.PI*l; // PI * l
+ final double pil3 = Math.PI*l/3; // PI * l/3
+ r1 = getRadius(0);
+ x = 0;
+ wetArea = 0;
+ planArea = 0;
+ planCenter = 0;
+ fullVolume = 0;
+ volume = 0;
+ cgx = 0;
+
+ for (int n=1; n<=DIVISIONS; n++) {
+ /*
+ * r1 and r2 are the two radii
+ * x is the position of r1
+ * hyp is the length of the hypotenuse from r1 to r2
+ * height if the y-axis height of the component if not filled
+ */
+
+ r2 = getRadius(x+l);
+ final double hyp = MathUtil.hypot(r2-r1, l);
+
+
+ // Volume differential elements
+ final double dV;
+ final double dFullV;
+
+ dFullV = pil3*(r1*r1 + r1*r2 + r2*r2);
+ if (filled || r1<thickness || r2<thickness) {
+ // Filled piece
+ dV = dFullV;
+ } else {
+ // Hollow piece
+ final double height = thickness*hyp/l;
+ dV = pil*height*(r1+r2-height);
+ }
+
+ // Add to the volume-related components
+ volume += dV;
+ fullVolume += dFullV;
+ cgx += (x+l/2)*dV;
+
+ // Wetted area ( * PI at the end)
+ wetArea += hyp*(r1+r2);
+
+ // Planform area & center
+ final double p = l*(r1+r2);
+ planArea += p;
+ planCenter += (x+l/2)*p;
+
+ // Update for next iteration
+ r1 = r2;
+ x += l;
+ }
+
+ wetArea *= Math.PI;
+
+ if (planArea > 0)
+ planCenter /= planArea;
+
+ if (volume == 0) {
+ cg = Coordinate.NUL;
+ } else {
+ // getComponentMass is safe now
+ cg = new Coordinate(cgx/volume,0,0,getComponentMass());
+ }
+ }
+
+
+ /**
+ * Integrate the longitudal and rotational inertia based on component volume.
+ * This method may be used only if the total volume is zero.
+ */
+ private void integrateInertiaVolume() {
+ double x, r1, r2;
+
+ final double l = length/DIVISIONS;
+ final double pil = Math.PI*l; // PI * l
+ final double pil3 = Math.PI*l/3; // PI * l/3
+
+ r1 = getRadius(0);
+ x = 0;
+ longitudalInertia = 0;
+ rotationalInertia = 0;
+
+ double volume = 0;
+
+ for (int n=1; n<=DIVISIONS; n++) {
+ /*
+ * r1 and r2 are the two radii, outer is their average
+ * x is the position of r1
+ * hyp is the length of the hypotenuse from r1 to r2
+ * height if the y-axis height of the component if not filled
+ */
+ r2 = getRadius(x+l);
+ final double outer = (r1 + r2)/2;
+
+
+ // Volume differential elements
+ final double inner;
+ final double dV;
+
+ if (filled || r1<thickness || r2<thickness) {
+ inner = 0;
+ dV = pil3*(r1*r1 + r1*r2 + r2*r2);
+ } else {
+ final double hyp = MathUtil.hypot(r2-r1, l);
+ final double height = thickness*hyp/l;
+ dV = pil*height*(r1+r2-height);
+ inner = Math.max(outer-height, 0);
+ }
+
+ rotationalInertia += dV * (pow2(outer) + pow2(inner))/2;
+ longitudalInertia += dV * ((3 * (pow2(outer) + pow2(inner)) + pow2(l))/12
+ + pow2(x+l/2));
+
+ volume += dV;
+
+ // Update for next iteration
+ r1 = r2;
+ x += l;
+ }
+
+ if (MathUtil.equals(volume,0)) {
+ integrateInertiaSurface();
+ return;
+ }
+
+ rotationalInertia /= volume;
+ longitudalInertia /= volume;
+
+ // Shift longitudal inertia to CG
+ longitudalInertia = Math.max(longitudalInertia - pow2(getComponentCG().x), 0);
+ }
+
+
+
+ /**
+ * Integrate the longitudal and rotational inertia based on component surface area.
+ * This method may be used only if the total volume is zero.
+ */
+ private void integrateInertiaSurface() {
+ double x, r1, r2;
+
+ final double l = length/DIVISIONS;
+
+ r1 = getRadius(0);
+ x = 0;
+ longitudalInertia = 0;
+ rotationalInertia = 0;
+
+ double surface = 0;
+
+ for (int n=1; n<=DIVISIONS; n++) {
+ /*
+ * r1 and r2 are the two radii, outer is their average
+ * x is the position of r1
+ * hyp is the length of the hypotenuse from r1 to r2
+ * height if the y-axis height of the component if not filled
+ */
+ r2 = getRadius(x+l);
+ final double hyp = MathUtil.hypot(r2-r1, l);
+ final double outer = (r1 + r2)/2;
+
+ final double dS = hyp * (r1+r2) * Math.PI;
+
+ rotationalInertia += dS * pow2(outer);
+ longitudalInertia += dS * ((6 * pow2(outer) + pow2(l))/12 + pow2(x+l/2));
+
+ surface += dS;
+
+ // Update for next iteration
+ r1 = r2;
+ x += l;
+ }
+
+ if (MathUtil.equals(surface, 0)) {
+ longitudalInertia = 0;
+ rotationalInertia = 0;
+ return;
+ }
+
+ longitudalInertia /= surface;
+ rotationalInertia /= surface;
+
+ // Shift longitudal inertia to CG
+ longitudalInertia = Math.max(longitudalInertia - pow2(getComponentCG().x), 0);
+ }
+
+
+
+
+ /**
+ * Invalidates the cached volume and CG information.
+ */
+ @Override
+ protected void componentChanged(ComponentChangeEvent e) {
+ super.componentChanged(e);
+ if (!e.isOtherChange()) {
+ wetArea = -1;
+ planArea = -1;
+ planCenter = -1;
+ volume = -1;
+ fullVolume = -1;
+ longitudalInertia = -1;
+ rotationalInertia = -1;
+ cg = null;
+ }
+ }
+
+
+
+ /////////// Auto radius helper methods
+
+
+ /**
+ * Returns the automatic radius for this component towards the
+ * front of the rocket. The automatics will not search towards the
+ * rear of the rocket for a suitable radius. A positive return value
+ * indicates a preferred radius, a negative value indicates that a
+ * match was not found.
+ */
+ protected abstract double getFrontAutoRadius();
+
+ /**
+ * Returns the automatic radius for this component towards the
+ * end of the rocket. The automatics will not search towards the
+ * front of the rocket for a suitable radius. A positive return value
+ * indicates a preferred radius, a negative value indicates that a
+ * match was not found.
+ */
+ protected abstract double getRearAutoRadius();
+
+
+
+ /**
+ * Return the previous symmetric component, or null if none exists.
+ * NOTE: This method currently assumes that there are no external
+ * "pods".
+ *
+ * @return the previous SymmetricComponent, or null.
+ */
+ protected final SymmetricComponent getPreviousSymmetricComponent() {
+ RocketComponent c;
+ for (c = this.getPreviousComponent(); c != null; c = c.getPreviousComponent()) {
+ if (c instanceof SymmetricComponent) {
+ return (SymmetricComponent)c;
+ }
+ if (!(c instanceof Stage) &&
+ (c.relativePosition == RocketComponent.Position.AFTER))
+ return null; // Bad component type as "parent"
+ }
+ return null;
+ }
+
+ /**
+ * Return the next symmetric component, or null if none exists.
+ * NOTE: This method currently assumes that there are no external
+ * "pods".
+ *
+ * @return the next SymmetricComponent, or null.
+ */
+ protected final SymmetricComponent getNextSymmetricComponent() {
+ RocketComponent c;
+ for (c = this.getNextComponent(); c != null; c = c.getNextComponent()) {
+ if (c instanceof SymmetricComponent) {
+ return (SymmetricComponent)c;
+ }
+ if (!(c instanceof Stage) &&
+ (c.relativePosition == RocketComponent.Position.AFTER))
+ return null; // Bad component type as "parent"
+ }
+ return null;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+/**
+ * An inner component that consists of a hollow cylindrical component. This can be
+ * an inner tube, tube coupler, centering ring, bulkhead etc.
+ *
+ * The properties include the inner and outer radii, length and radial position.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class ThicknessRingComponent extends RingComponent {
+
+ protected double outerRadius = 0;
+ protected double thickness = 0;
+
+
+ @Override
+ public double getOuterRadius() {
+ if (isOuterRadiusAutomatic() && getParent() instanceof RadialParent) {
+ RocketComponent parent = getParent();
+ double pos1 = this.toRelative(Coordinate.NUL, parent)[0].x;
+ double pos2 = this.toRelative(new Coordinate(getLength()), parent)[0].x;
+ pos1 = MathUtil.clamp(pos1, 0, parent.getLength());
+ pos2 = MathUtil.clamp(pos2, 0, parent.getLength());
+ outerRadius = Math.min(((RadialParent)parent).getInnerRadius(pos1),
+ ((RadialParent)parent).getInnerRadius(pos2));
+ }
+
+ return outerRadius;
+ }
+
+
+ @Override
+ public void setOuterRadius(double r) {
+ r = Math.max(r,0);
+ if (MathUtil.equals(outerRadius, r) && !isOuterRadiusAutomatic())
+ return;
+
+ outerRadius = r;
+ outerRadiusAutomatic = false;
+
+ if (thickness > outerRadius)
+ thickness = outerRadius;
+
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+
+ @Override
+ public double getThickness() {
+ return Math.min(thickness, getOuterRadius());
+ }
+ @Override
+ public void setThickness(double thickness) {
+ double outer = getOuterRadius();
+
+ thickness = MathUtil.clamp(thickness, 0, outer);
+ if (MathUtil.equals(getThickness(), thickness))
+ return;
+
+ this.thickness = thickness;
+
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+ @Override
+ public double getInnerRadius() {
+ return Math.max(getOuterRadius()-thickness, 0);
+ }
+ @Override
+ public void setInnerRadius(double r) {
+ r = Math.max(r,0);
+ setThickness(getOuterRadius() - r);
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.util.Coordinate;
+
+/**
+ * A class of motors specified by a fixed thrust curve. This is the most
+ * accurate for solid rocket motors.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class ThrustCurveMotor extends Motor {
+
+ private final double[] time;
+ private final double[] thrust;
+ private final Coordinate[] cg;
+
+ private final double totalTime;
+ private final double maxThrust;
+
+
+ /**
+ * Sole constructor. Sets all the properties of the motor.
+ *
+ * @param manufacturer the manufacturer of the motor.
+ * @param designation the designation of the motor.
+ * @param description extra description of the motor.
+ * @param diameter diameter of the motor.
+ * @param length length of the motor.
+ * @param time the time points for the thrust curve.
+ * @param thrust thrust at the time points.
+ * @param cg cg at the time points.
+ */
+ public ThrustCurveMotor(String manufacturer, String designation, String description,
+ Motor.Type type, double[] delays, double diameter, double length,
+ double[] time, double[] thrust, Coordinate[] cg) {
+ super(manufacturer, designation, description, type, delays, diameter, length);
+
+ double max = -1;
+
+ // Check argument validity
+ if ((time.length != thrust.length) || (time.length != cg.length)) {
+ throw new IllegalArgumentException("Array lengths do not match, " +
+ "time:" + time.length + " thrust:" + thrust.length +
+ " cg:" + cg.length);
+ }
+ if (time.length < 2) {
+ throw new IllegalArgumentException("Too short thrust-curve, length=" +
+ time.length);
+ }
+ for (int i=0; i < time.length-1; i++) {
+ if (time[i+1] < time[i]) {
+ throw new IllegalArgumentException("Time goes backwards, " +
+ "time[" + i + "]=" + time[i] + " " +
+ "time[" + (i+1) + "]=" + time[i+1]);
+ }
+ }
+ if (time[0] != 0) {
+ throw new IllegalArgumentException("Curve starts at time=" + time[0]);
+ }
+ for (double t: thrust) {
+ if (t < 0) {
+ throw new IllegalArgumentException("Negative thrust.");
+ }
+ if (t > max)
+ max = t;
+ }
+
+ this.maxThrust = max;
+ this.time = time.clone();
+ this.thrust = thrust.clone();
+ this.cg = cg.clone();
+ this.totalTime = time[time.length-1];
+ }
+
+
+ @Override
+ public double getTotalTime() {
+ return totalTime;
+ }
+
+ @Override
+ public double getMaxThrust() {
+ return maxThrust;
+ }
+
+ @Override
+ public double getThrust(double t) {
+ if ((t < 0) || (t > totalTime))
+ return 0;
+
+ for (int i=0; i < time.length-1; i++) {
+ if ((t >= time[i]) && (t <= time[i+1])) {
+ double delta = time[i+1] - time[i];
+ t = t - time[i];
+ return thrust[i] * (1 - t/delta) + thrust[i+1] * (t/delta);
+ }
+ }
+ assert false : "Should not be reached.";
+ return 0;
+ }
+
+
+ @Override
+ public Coordinate getCG(double t) {
+ if (t <= 0)
+ return cg[0];
+ if (t >= totalTime)
+ return cg[cg.length-1];
+
+ for (int i=0; i < time.length-1; i++) {
+ if ((t >= time[i]) && (t <= time[i+1])) {
+ double delta = time[i+1] - time[i];
+ t = t - time[i];
+ return cg[i].multiply(1 - t/delta).add(cg[i+1].multiply(t/delta));
+ }
+ }
+ assert false : "Should not be reached.";
+ return cg[cg.length-1];
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import static java.lang.Math.sin;
+import static java.lang.Math.sqrt;
+import static net.sf.openrocket.util.MathUtil.pow2;
+import static net.sf.openrocket.util.MathUtil.pow3;
+
+import java.util.Collection;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+
+
+public class Transition extends SymmetricComponent {
+ private static final double CLIP_PRECISION = 0.0001;
+
+
+ private Shape type;
+ private double shapeParameter;
+ private boolean clipped; // Not to be read - use isClipped(), which may be overriden
+
+ private double radius1, radius2;
+ private boolean autoRadius1, autoRadius2; // Whether the start radius is automatic
+
+
+ private double foreShoulderRadius;
+ private double foreShoulderThickness;
+ private double foreShoulderLength;
+ private boolean foreShoulderCapped;
+ private double aftShoulderRadius;
+ private double aftShoulderThickness;
+ private double aftShoulderLength;
+ private boolean aftShoulderCapped;
+
+
+ // Used to cache the clip length
+ private double clipLength=-1;
+
+ public Transition() {
+ super();
+
+ this.radius1 = DEFAULT_RADIUS;
+ this.radius2 = DEFAULT_RADIUS;
+ this.length = DEFAULT_RADIUS * 3;
+ this.autoRadius1 = true;
+ this.autoRadius2 = true;
+
+ this.type = Shape.CONICAL;
+ this.shapeParameter = 0;
+ this.clipped = true;
+ }
+
+
+
+
+ //////// Fore radius ////////
+
+
+ @Override
+ public double getForeRadius() {
+ if (isForeRadiusAutomatic()) {
+ // Get the automatic radius from the front
+ double r = -1;
+ SymmetricComponent c = this.getPreviousSymmetricComponent();
+ if (c != null) {
+ r = c.getFrontAutoRadius();
+ }
+ if (r < 0)
+ r = DEFAULT_RADIUS;
+ return r;
+ }
+ return radius1;
+ }
+
+ public void setForeRadius(double radius) {
+ if ((this.radius1 == radius) && (autoRadius1 == false))
+ return;
+
+ this.autoRadius1 = false;
+ this.radius1 = Math.max(radius,0);
+
+ if (this.thickness > this.radius1 && this.thickness > this.radius2)
+ this.thickness = Math.max(this.radius1, this.radius2);
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+ @Override
+ public boolean isForeRadiusAutomatic() {
+ return autoRadius1;
+ }
+
+ public void setForeRadiusAutomatic(boolean auto) {
+ if (autoRadius1 == auto)
+ return;
+
+ autoRadius1 = auto;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+ //////// Aft radius /////////
+
+ @Override
+ public double getAftRadius() {
+ if (isAftRadiusAutomatic()) {
+ // Return the auto radius from the rear
+ double r = -1;
+ SymmetricComponent c = this.getNextSymmetricComponent();
+ if (c != null) {
+ r = c.getRearAutoRadius();
+ }
+ if (r < 0)
+ r = DEFAULT_RADIUS;
+ return r;
+ }
+ return radius2;
+ }
+
+
+
+ public void setAftRadius(double radius) {
+ if ((this.radius2 == radius) && (autoRadius2 == false))
+ return;
+
+ this.autoRadius2 = false;
+ this.radius2 = Math.max(radius,0);
+
+ if (this.thickness > this.radius1 && this.thickness > this.radius2)
+ this.thickness = Math.max(this.radius1, this.radius2);
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+ @Override
+ public boolean isAftRadiusAutomatic() {
+ return autoRadius2;
+ }
+
+ public void setAftRadiusAutomatic(boolean auto) {
+ if (autoRadius2 == auto)
+ return;
+
+ autoRadius2 = auto;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+
+ //// Radius automatics
+
+ @Override
+ protected double getFrontAutoRadius() {
+ if (isAftRadiusAutomatic())
+ return -1;
+ return getAftRadius();
+ }
+
+
+ @Override
+ protected double getRearAutoRadius() {
+ if (isForeRadiusAutomatic())
+ return -1;
+ return getForeRadius();
+ }
+
+
+
+
+ //////// Type & shape /////////
+
+ public Shape getType() {
+ return type;
+ }
+
+ public void setType(Shape type) {
+ if (this.type == type)
+ return;
+ this.type = type;
+ this.clipped = type.isClippable();
+ this.shapeParameter = type.defaultParameter();
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+ public double getShapeParameter() {
+ return shapeParameter;
+ }
+
+ public void setShapeParameter(double n) {
+ if (shapeParameter == n)
+ return;
+ this.shapeParameter = MathUtil.clamp(n, type.minParameter(), type.maxParameter());
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+ public boolean isClipped() {
+ if (!type.isClippable())
+ return false;
+ return clipped;
+ }
+
+ public void setClipped(boolean c) {
+ if (clipped == c)
+ return;
+ clipped = c;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+ public boolean isClippedEnabled() {
+ return type.isClippable();
+ }
+
+ public double getShapeParameterMin() {
+ return type.minParameter();
+ }
+
+ public double getShapeParameterMax() {
+ return type.maxParameter();
+ }
+
+
+ //////// Shoulders ////////
+
+ public double getForeShoulderRadius() {
+ return foreShoulderRadius;
+ }
+
+ public void setForeShoulderRadius(double foreShoulderRadius) {
+ if (MathUtil.equals(this.foreShoulderRadius, foreShoulderRadius))
+ return;
+ this.foreShoulderRadius = foreShoulderRadius;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+ public double getForeShoulderThickness() {
+ return foreShoulderThickness;
+ }
+
+ public void setForeShoulderThickness(double foreShoulderThickness) {
+ if (MathUtil.equals(this.foreShoulderThickness, foreShoulderThickness))
+ return;
+ this.foreShoulderThickness = foreShoulderThickness;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+ public double getForeShoulderLength() {
+ return foreShoulderLength;
+ }
+
+ public void setForeShoulderLength(double foreShoulderLength) {
+ if (MathUtil.equals(this.foreShoulderLength, foreShoulderLength))
+ return;
+ this.foreShoulderLength = foreShoulderLength;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+ public boolean isForeShoulderCapped() {
+ return foreShoulderCapped;
+ }
+
+ public void setForeShoulderCapped(boolean capped) {
+ if (this.foreShoulderCapped == capped)
+ return;
+ this.foreShoulderCapped = capped;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+
+
+ public double getAftShoulderRadius() {
+ return aftShoulderRadius;
+ }
+
+ public void setAftShoulderRadius(double aftShoulderRadius) {
+ if (MathUtil.equals(this.aftShoulderRadius, aftShoulderRadius))
+ return;
+ this.aftShoulderRadius = aftShoulderRadius;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+ public double getAftShoulderThickness() {
+ return aftShoulderThickness;
+ }
+
+ public void setAftShoulderThickness(double aftShoulderThickness) {
+ if (MathUtil.equals(this.aftShoulderThickness, aftShoulderThickness))
+ return;
+ this.aftShoulderThickness = aftShoulderThickness;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+ public double getAftShoulderLength() {
+ return aftShoulderLength;
+ }
+
+ public void setAftShoulderLength(double aftShoulderLength) {
+ if (MathUtil.equals(this.aftShoulderLength, aftShoulderLength))
+ return;
+ this.aftShoulderLength = aftShoulderLength;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+ public boolean isAftShoulderCapped() {
+ return aftShoulderCapped;
+ }
+
+ public void setAftShoulderCapped(boolean capped) {
+ if (this.aftShoulderCapped == capped)
+ return;
+ this.aftShoulderCapped = capped;
+ fireComponentChangeEvent(ComponentChangeEvent.MASS_CHANGE);
+ }
+
+
+
+
+ /////////// Shape implementations ////////////
+
+
+
+ /**
+ * Return the radius at point x of the transition.
+ */
+ @Override
+ public double getRadius(double x) {
+ if (x<0 || x>length)
+ return 0;
+
+ double r1=getForeRadius();
+ double r2=getAftRadius();
+
+ if (r1 == r2)
+ return r1;
+
+ if (r1 > r2) {
+ x = length-x;
+ double tmp = r1;
+ r1 = r2;
+ r2 = tmp;
+ }
+
+ if (isClipped()) {
+ // Check clip calculation
+ if (clipLength < 0)
+ calculateClip(r1,r2);
+ return type.getRadius(clipLength+x, r2, clipLength+length, shapeParameter);
+ } else {
+ // Not clipped
+ return r1 + type.getRadius(x, r2-r1, length, shapeParameter);
+ }
+ }
+
+ /**
+ * Numerically solve clipLength from the equation
+ * r1 == type.getRadius(clipLength,r2,clipLength+length)
+ * using a binary search. It assumes getRadius() to be monotonically increasing.
+ */
+ private void calculateClip(double r1, double r2) {
+ double min=0, max=length;
+
+ if (r1 >= r2) {
+ double tmp=r1;
+ r1 = r2;
+ r2 = tmp;
+ }
+
+ if (r1==0) {
+ clipLength = 0;
+ return;
+ }
+
+ if (length <= 0) {
+ clipLength = 0;
+ return;
+ }
+
+ // Required:
+ // getR(min,min+length,r2) - r1 < 0
+ // getR(max,max+length,r2) - r1 > 0
+
+ int n=0;
+ while (type.getRadius(max, r2, max+length, shapeParameter) - r1 < 0) {
+ min = max;
+ max *= 2;
+ n++;
+ if (n>10)
+ break;
+ }
+
+ while (true) {
+ clipLength = (min+max)/2;
+ if ((max-min)<CLIP_PRECISION)
+ return;
+ double val = type.getRadius(clipLength, r2, clipLength+length, shapeParameter);
+ if (val-r1 > 0) {
+ max = clipLength;
+ } else {
+ min = clipLength;
+ }
+ }
+ }
+
+
+ @Override
+ public double getInnerRadius(double x) {
+ return Math.max(getRadius(x)-thickness,0);
+ }
+
+
+
+ @Override
+ public Collection<Coordinate> getComponentBounds() {
+ Collection<Coordinate> bounds = super.getComponentBounds();
+ if (foreShoulderLength > 0.001)
+ addBound(bounds, -foreShoulderLength, foreShoulderRadius);
+ if (aftShoulderLength > 0.001)
+ addBound(bounds, getLength() + aftShoulderLength, aftShoulderRadius);
+ return bounds;
+ }
+
+ @Override
+ public double getComponentMass() {
+ double mass = super.getComponentMass();
+ if (getForeShoulderLength() > 0.001) {
+ final double or = getForeShoulderRadius();
+ final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
+ mass += ringMass(or, ir, getForeShoulderLength(), getMaterial().getDensity());
+ }
+ if (isForeShoulderCapped()) {
+ final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
+ mass += ringMass(ir, 0, getForeShoulderThickness(), getMaterial().getDensity());
+ }
+
+ if (getAftShoulderLength() > 0.001) {
+ final double or = getAftShoulderRadius();
+ final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
+ mass += ringMass(or, ir, getAftShoulderLength(), getMaterial().getDensity());
+ }
+ if (isAftShoulderCapped()) {
+ final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
+ mass += ringMass(ir, 0, getAftShoulderThickness(), getMaterial().getDensity());
+ }
+
+ return mass;
+ }
+
+ @Override
+ public Coordinate getComponentCG() {
+ Coordinate cg = super.getComponentCG();
+ if (getForeShoulderLength() > 0.001) {
+ final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
+ cg = cg.average(ringCG(getForeShoulderRadius(), ir, -getForeShoulderLength(), 0,
+ getMaterial().getDensity()));
+ }
+ if (isForeShoulderCapped()) {
+ final double ir = Math.max(getForeShoulderRadius() - getForeShoulderThickness(), 0);
+ cg = cg.average(ringCG(ir, 0, -getForeShoulderLength(),
+ getForeShoulderThickness()-getForeShoulderLength(),
+ getMaterial().getDensity()));
+ }
+
+ if (getAftShoulderLength() > 0.001) {
+ final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
+ cg = cg.average(ringCG(getAftShoulderRadius(), ir, getLength(),
+ getLength()+getAftShoulderLength(), getMaterial().getDensity()));
+ }
+ if (isAftShoulderCapped()) {
+ final double ir = Math.max(getAftShoulderRadius() - getAftShoulderThickness(), 0);
+ cg = cg.average(ringCG(ir, 0,
+ getLength()+getAftShoulderLength()-getAftShoulderThickness(),
+ getLength()+getAftShoulderLength(), getMaterial().getDensity()));
+ }
+ return cg;
+ }
+
+
+ /*
+ * The moments of inertia are not explicitly corrected for the shoulders.
+ * However, since the mass is corrected, the inertia is automatically corrected
+ * to very nearly the correct value.
+ */
+
+
+
+ /**
+ * Returns the name of the component ("Transition").
+ */
+ @Override
+ public String getComponentName() {
+ return "Transition";
+ }
+
+ @Override
+ protected void componentChanged(ComponentChangeEvent e) {
+ super.componentChanged(e);
+ clipLength = -1;
+ }
+
+
+
+ /**
+ * An enumeration listing the possible shapes of transitions.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+ public static enum Shape {
+
+ /**
+ * Conical shape.
+ */
+ CONICAL("Conical",
+ "A conical nose cone has a profile of a triangle.",
+ "A conical transition has straight sides.") {
+ @Override
+ public double getRadius(double x, double radius, double length, double param) {
+ assert x >= 0;
+ assert x <= length;
+ assert radius >= 0;
+ return radius*x/length;
+ }
+ },
+
+ /**
+ * Ogive shape. The shape parameter is the portion of an extended tangent ogive
+ * that will be used. That is, for param==1 a tangent ogive will be produced, and
+ * for smaller values the shape straightens out into a cone at param==0.
+ */
+ OGIVE("Ogive",
+ "An ogive nose cone has a profile that is a segment of a circle. " +
+ "The shape parameter value 1 produces a <b>tangent ogive</b>, which has " +
+ "a smooth transition to the body tube, values less than 1 produce "+
+ "<b>secant ogives</b>.",
+ "An ogive transition has a profile that is a segment of a circle. " +
+ "The shape parameter value 1 produces a <b>tangent ogive</b>, which has " +
+ "a smooth transition to the body tube at the aft end, values less than 1 " +
+ "produce <b>secant ogives</b>.") {
+ @Override
+ public boolean usesParameter() {
+ return true; // Range 0...1 is default
+ }
+ @Override
+ public double defaultParameter() {
+ return 1.0; // Tangent ogive by default
+ }
+ @Override
+ public double getRadius(double x, double radius, double length, double param) {
+ assert x >= 0;
+ assert x <= length;
+ assert radius >= 0;
+ assert param >= 0;
+ assert param <= 1;
+
+ // Impossible to calculate ogive for length < radius, scale instead
+ // TODO: LOW: secant ogive could be calculated lower
+ if (length < radius) {
+ x = x * radius / length;
+ length = radius;
+ }
+
+ if (param < 0.001)
+ return CONICAL.getRadius(x, radius, length, param);
+
+ // Radius of circle is:
+ double R = sqrt((pow2(length)+pow2(radius)) *
+ (pow2((2-param)*length) + pow2(param*radius))/(4*pow2(param*radius)));
+ double L = length/param;
+// double R = (radius + length*length/(radius*param*param))/2;
+ double y0 = sqrt(R*R - L*L);
+ return sqrt(R*R - (L-x)*(L-x)) - y0;
+ }
+ },
+
+ /**
+ * Ellipsoidal shape.
+ */
+ ELLIPSOID("Ellipsoid",
+ "An ellipsoidal nose cone has a profile of a half-ellipse "+
+ "with major axes of lengths 2×<i>Length</i> and <i>Diameter</i>.",
+ "An ellipsoidal transition has a profile of a half-ellipse "+
+ "with major axes of lengths 2×<i>Length</i> and <i>Diameter</i>. If the "+
+ "transition is not clipped, then the profile is extended at the center by the "+
+ "corresponding radius.",true) {
+ @Override
+ public double getRadius(double x, double radius, double length, double param) {
+ assert x >= 0;
+ assert x <= length;
+ assert radius >= 0;
+ x = x*radius/length;
+ return sqrt(2*radius*x-x*x); // radius/length * sphere
+ }
+ },
+
+ POWER("Power series",
+ "A power series nose cone has a profile of "+
+ "<i>Radius</i> × (<i>x</i> / <i>Length</i>)" +
+ "<sup><i>k</i></sup> "+
+ "where <i>k</i> is the shape parameter. For <i>k</i>=0.5 this is a "+
+ "<b>½-power</b> or <b>parabolic</b> nose cone, for <i>k</i>=0.75 a "+
+ "<b>¾-power</b>, and for <i>k</i>=1 a <b>conical</b> nose cone.",
+ "A power series transition has a profile of "+
+ "<i>Radius</i> × (<i>x</i> / <i>Length</i>)" +
+ "<sup><i>k</i></sup> "+
+ "where <i>k</i> is the shape parameter. For <i>k</i>=0.5 the transition is "+
+ "<b>½-power</b> or <b>parabolic</b>, for <i>k</i>=0.75 a <b>¾-power</b>, and for " +
+ "<i>k</i>=1 <b>conical</b>.",true) {
+ @Override
+ public boolean usesParameter() { // Range 0...1
+ return true;
+ }
+ @Override
+ public double defaultParameter() {
+ return 0.5;
+ }
+ @Override
+ public double getRadius(double x, double radius, double length, double param) {
+ assert x >= 0;
+ assert x <= length;
+ assert radius >= 0;
+ assert param >= 0;
+ assert param <= 1;
+ if (param<=0.00001) {
+ if (x<=0.00001)
+ return 0;
+ else
+ return radius;
+ }
+ return radius*Math.pow(x/length, param);
+ }
+
+ },
+
+ PARABOLIC("Parabolic series",
+ "A parabolic series nose cone has a profile of a parabola. The shape "+
+ "parameter defines the segment of the parabola to utilize. The shape " +
+ "parameter 1.0 produces a <b>full parabola</b> which is tangent to the body " +
+ "tube, 0.75 produces a <b>3/4 parabola</b>, 0.5 procudes a " +
+ "<b>1/2 parabola</b> and 0 produces a <b>conical</b> nose cone.",
+ "A parabolic series transition has a profile of a parabola. The shape "+
+ "parameter defines the segment of the parabola to utilize. The shape " +
+ "parameter 1.0 produces a <b>full parabola</b> which is tangent to the body " +
+ "tube at the aft end, 0.75 produces a <b>3/4 parabola</b>, 0.5 procudes a " +
+ "<b>1/2 parabola</b> and 0 produces a <b>conical</b> transition.") {
+
+ // In principle a parabolic transition is clippable, but the difference is
+ // negligible.
+
+ @Override
+ public boolean usesParameter() { // Range 0...1
+ return true;
+ }
+ @Override
+ public double defaultParameter() {
+ return 1.0;
+ }
+ @Override
+ public double getRadius(double x, double radius, double length, double param) {
+ assert x >= 0;
+ assert x <= length;
+ assert radius >= 0;
+ assert param >= 0;
+ assert param <= 1;
+
+ return radius * ((2*x/length - param*pow2(x/length))/(2-param));
+ }
+ },
+
+
+
+ HAACK("Haack series",
+ "The Haack series nose cones are designed to minimize drag. The shape parameter " +
+ "0 produces an <b>LD-Haack</b> or <b>Von Karman</b> nose cone, which minimizes " +
+ "drag for fixed length and diameter, while a value of 0.333 produces an " +
+ "<b>LV-Haack</b> nose cone, which minimizes drag for fixed length and volume.",
+ "The Haack series <i>nose cones</i> are designed to minimize drag. " +
+ "These transition shapes are their equivalents, but do not necessarily produce " +
+ "optimal drag for transitions. " +
+ "The shape parameter 0 produces an <b>LD-Haack</b> or <b>Von Karman</b> shape, " +
+ "while a value of 0.333 produces an <b>LV-Haack</b> shape.",true) {
+ @Override
+ public boolean usesParameter() {
+ return true;
+ }
+ @Override
+ public double maxParameter() {
+ return 1.0/3.0; // Range 0...1/3
+ }
+ @Override
+ public double getRadius(double x, double radius, double length, double param) {
+ assert x >= 0;
+ assert x <= length;
+ assert radius >= 0;
+ assert param >= 0;
+ assert param <= 2;
+
+ double theta = Math.acos(1-2*x/length);
+ if (param==0) {
+ return radius*sqrt((theta-sin(2*theta)/2)/Math.PI);
+ }
+ return radius*sqrt((theta-sin(2*theta)/2+param*pow3(sin(theta)))/Math.PI);
+ }
+ },
+
+// POLYNOMIAL("Smooth polynomial",
+// "A polynomial is fitted such that the nose cone profile is horizontal "+
+// "at the aft end of the transition. The angle at the tip is defined by "+
+// "the shape parameter.",
+// "A polynomial is fitted such that the transition profile is horizontal "+
+// "at the aft end of the transition. The angle at the fore end is defined "+
+// "by the shape parameter.") {
+// @Override
+// public boolean usesParameter() {
+// return true;
+// }
+// @Override
+// public double maxParameter() {
+// return 3.0; // Range 0...3
+// }
+// @Override
+// public double defaultParameter() {
+// return 0.0;
+// }
+// public double getRadius(double x, double radius, double length, double param) {
+// assert x >= 0;
+// assert x <= length;
+// assert radius >= 0;
+// assert param >= 0;
+// assert param <= 3;
+// // p(x) = (k-2)x^3 + (3-2k)x^2 + k*x
+// x = x/length;
+// return radius*((((param-2)*x + (3-2*param))*x + param)*x);
+// }
+// }
+ ;
+
+ // Privete fields of the shapes
+ private final String name;
+ private final String transitionDesc;
+ private final String noseconeDesc;
+ private final boolean canClip;
+
+ // Non-clippable constructor
+ Shape(String name, String noseconeDesc, String transitionDesc) {
+ this(name,noseconeDesc,transitionDesc,false);
+ }
+
+ // Clippable constructor
+ Shape(String name, String noseconeDesc, String transitionDesc, boolean canClip) {
+ this.name = name;
+ this.canClip = canClip;
+ this.noseconeDesc = noseconeDesc;
+ this.transitionDesc = transitionDesc;
+ }
+
+
+ /**
+ * Return the name of the transition shape name.
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * Get a description of the Transition shape.
+ */
+ public String getTransitionDescription() {
+ return transitionDesc;
+ }
+
+ /**
+ * Get a description of the NoseCone shape.
+ */
+ public String getNoseConeDescription() {
+ return noseconeDesc;
+ }
+
+ /**
+ * Check whether the shape differs in clipped mode. The clipping should be
+ * enabled by default if possible.
+ */
+ public boolean isClippable() {
+ return canClip;
+ }
+
+ /**
+ * Return whether the shape uses the shape parameter. (Default false.)
+ */
+ public boolean usesParameter() {
+ return false;
+ }
+
+ /**
+ * Return the minimum value of the shape parameter. (Default 0.)
+ */
+ public double minParameter() {
+ return 0.0;
+ }
+
+ /**
+ * Return the maximum value of the shape parameter. (Default 1.)
+ */
+ public double maxParameter() {
+ return 1.0;
+ }
+
+ /**
+ * Return the default value of the shape parameter. (Default 0.)
+ */
+ public double defaultParameter() {
+ return 0.0;
+ }
+
+ /**
+ * Calculate the basic radius of a transition with the given radius, length and
+ * shape parameter at the point x from the tip of the component. It is assumed
+ * that the fore radius if zero and the aft radius is <code>radius >= 0</code>.
+ * Boattails are achieved by reversing the component.
+ *
+ * @param x Position from the tip of the component.
+ * @param radius Aft end radius >= 0.
+ * @param length Length of the transition >= 0.
+ * @param param Valid shape parameter.
+ * @return The basic radius at the given position.
+ */
+ public abstract double getRadius(double x, double radius, double length, double param);
+
+
+ /**
+ * Returns the name of the shape (same as getName()).
+ */
+ @Override
+ public String toString() {
+ return name;
+ }
+ }
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+import net.sf.openrocket.util.Coordinate;
+
+/**
+ * A set of trapezoidal fins. The root and tip chords are perpendicular to the rocket
+ * base line, while the leading and aft edges may be slanted.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class TrapezoidFinSet extends FinSet {
+ public static final double MAX_SWEEP_ANGLE=(89*Math.PI/180.0);
+
+ /*
+ * sweep tipChord
+ * | |___________
+ * | / |
+ * | / |
+ * | / | height
+ * / |
+ * __________/________________|_____________
+ * length
+ * == rootChord
+ */
+
+ // rootChord == length
+ private double tipChord = 0;
+ private double height = 0;
+ private double sweep = 0;
+
+
+ public TrapezoidFinSet() {
+ this (3, 0.05, 0.05, 0.025, 0.05);
+ }
+
+ // TODO: HIGH: height=0 -> CP = NaN
+ public TrapezoidFinSet(int fins, double rootChord, double tipChord, double sweep,
+ double height) {
+ super();
+
+ this.setFinCount(fins);
+ this.length = rootChord;
+ this.tipChord = tipChord;
+ this.sweep = sweep;
+ this.height = height;
+ }
+
+
+ public void setFinShape(double rootChord, double tipChord, double sweep, double height,
+ double thickness) {
+ if (this.length==rootChord && this.tipChord==tipChord && this.sweep==sweep &&
+ this.height==height && this.thickness==thickness)
+ return;
+ this.length=rootChord;
+ this.tipChord=tipChord;
+ this.sweep=sweep;
+ this.height=height;
+ this.thickness=thickness;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+ public double getRootChord() {
+ return length;
+ }
+ public void setRootChord(double r) {
+ if (length == r)
+ return;
+ length = Math.max(r,0);
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+ public double getTipChord() {
+ return tipChord;
+ }
+ public void setTipChord(double r) {
+ if (tipChord == r)
+ return;
+ tipChord = Math.max(r,0);
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+ /**
+ * Get the sweep length.
+ */
+ public double getSweep() {
+ return sweep;
+ }
+ /**
+ * Set the sweep length.
+ */
+ public void setSweep(double r) {
+ if (sweep == r)
+ return;
+ sweep = r;
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+ /**
+ * Get the sweep angle. This is calculated from the true sweep and height, and is not
+ * stored separetely.
+ */
+ public double getSweepAngle() {
+ if (height == 0) {
+ if (sweep > 0)
+ return Math.PI/2;
+ if (sweep < 0)
+ return -Math.PI/2;
+ return 0;
+ }
+ return Math.atan(sweep/height);
+ }
+ /**
+ * Sets the sweep by the sweep angle. The sweep is calculated and set by this method,
+ * and the angle itself is not stored.
+ */
+ public void setSweepAngle(double r) {
+ if (r > MAX_SWEEP_ANGLE)
+ r = MAX_SWEEP_ANGLE;
+ if (r < -MAX_SWEEP_ANGLE)
+ r = -MAX_SWEEP_ANGLE;
+ double sweep = Math.tan(r) * height;
+ if (Double.isNaN(sweep) || Double.isInfinite(sweep))
+ return;
+ setSweep(sweep);
+ }
+
+ public double getHeight() {
+ return height;
+ }
+ public void setHeight(double r) {
+ if (height == r)
+ return;
+ height = Math.max(r,0);
+ fireComponentChangeEvent(ComponentChangeEvent.BOTH_CHANGE);
+ }
+
+
+
+ /**
+ * Returns the geometry of a trapezoidal fin.
+ */
+ @Override
+ public Coordinate[] getFinPoints() {
+ Coordinate[] c = new Coordinate[4];
+
+ c[0] = Coordinate.NUL;
+ c[1] = new Coordinate(sweep,height);
+ c[2] = new Coordinate(sweep+tipChord,height);
+ c[3] = new Coordinate(length,0);
+
+ return c;
+ }
+
+ /**
+ * Returns the span of a trapezoidal fin.
+ */
+ @Override
+ public double getSpan() {
+ return height;
+ }
+
+
+ @Override
+ public String getComponentName() {
+ return "Trapezoidal fin set";
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.rocketcomponent;
+
+
+public class TubeCoupler extends ThicknessRingComponent {
+
+ public TubeCoupler() {
+ setOuterRadiusAutomatic(true);
+ setThickness(0.002);
+ setLength(0.06);
+ }
+
+
+ // Make setter visible
+ @Override
+ public void setOuterRadiusAutomatic(boolean auto) {
+ super.setOuterRadiusAutomatic(auto);
+ }
+
+
+ @Override
+ public String getComponentName() {
+ return "Tube coupler";
+ }
+
+ @Override
+ public boolean isCompatible(Class<? extends RocketComponent> type) {
+ return false;
+ }
+}
+
--- /dev/null
+package net.sf.openrocket.simulation;
+
+import java.util.Collection;
+
+import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.AtmosphericConditions;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.aerodynamics.GravityModel;
+import net.sf.openrocket.aerodynamics.WindSimulator;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Quaternion;
+
+
+
+
+/**
+ * A flight simulator based on Euler integration. This class is out of date and
+ * deprecated in favor of the Runge-Kutta simulator RK4Simulator.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+@Deprecated
+public class EulerSimulator extends FlightSimulator {
+ /**
+ * Maximum roll step allowed. This is selected as an uneven division of the full
+ * circle so that the simulation will sample the most wind directions
+ */
+ private static final double MAX_ROLL_STEP_ANGLE = 28.32 * Math.PI/180;
+
+ private static final boolean DEBUG = false;
+
+
+ private FlightConditions flightConditions = null;
+ private double currentStep;
+ private double lateralPitchRate = 0; // set by calculateFlightConditions
+
+ public EulerSimulator(AerodynamicCalculator calculator) {
+ super(calculator);
+ }
+
+
+
+ protected FlightDataBranch newFlightData(String name) {
+ return new FlightDataBranch(name, FlightDataBranch.TYPE_TIME); // TODO: ???
+ }
+
+
+
+ @Override
+ protected RK4SimulationStatus initializeSimulation(Configuration configuration,
+ SimulationConditions simulation) {
+
+ RK4SimulationStatus status = new RK4SimulationStatus();
+
+ status.startConditions = simulation;
+
+ status.configuration = configuration;
+ status.flightData = newFlightData("Main");
+ status.launchRod = true;
+ status.time = 0.0;
+
+
+ status.launchRodDirection = new Coordinate(
+ Math.sin(simulation.getLaunchRodAngle()) *
+ Math.cos(simulation.getLaunchRodDirection()),
+ Math.sin(simulation.getLaunchRodAngle()) *
+ Math.sin(simulation.getLaunchRodDirection()),
+ Math.cos(simulation.getLaunchRodAngle())
+ );
+ status.launchRodLength = simulation.getLaunchRodLength();
+ // TODO: take into account launch lug positions
+
+
+ Quaternion o = new Quaternion();
+ o.multiplyLeft(Quaternion.rotation(
+ new Coordinate(0, simulation.getLaunchRodAngle(), 0)));
+ o.multiplyLeft(Quaternion.rotation(
+ new Coordinate(0, 0, simulation.getLaunchRodDirection())));
+ status.orientation = o;
+ status.position = Coordinate.NUL;
+ status.velocity = Coordinate.NUL;
+ status.rotation = Coordinate.NUL;
+
+ status.windSimulator = new WindSimulator();
+ status.windSimulator.setAverage(simulation.getWindSpeedAverage());
+ status.windSimulator.setStandardDeviation(simulation.getWindSpeedDeviation());
+// status.windSimulator.reset();
+
+ currentStep = simulation.getTimeStep();
+
+ status.gravityModel = new GravityModel(simulation.getLaunchLatitude());
+
+ flightConditions = null;
+
+ return status;
+ }
+
+
+
+ @Override
+ protected Collection<FlightEvent> step(SimulationConditions simulation,
+ SimulationStatus simulationStatus) {
+
+ RK4SimulationStatus status = (RK4SimulationStatus)simulationStatus;
+ FlightDataBranch data = status.flightData;
+
+
+ // Add data point and values
+ data.addPoint();
+ data.setValue(FlightDataBranch.TYPE_TIME, status.time);
+
+ if (DEBUG)
+ System.out.println("original direction: "+status.orientation.rotateZ());
+
+ // Calculate current flight conditions
+ if (flightConditions == null) {
+ flightConditions = new FlightConditions(status.configuration);
+ }
+ calculateFlightConditions(flightConditions, status);
+
+ if (DEBUG)
+ System.out.println("flightConditions="+flightConditions);
+
+
+ // Calculate time step
+ double timestep;
+
+// double normalStep = simulation.getTimeStep();
+// double lateralStep = simulation.getMaximumStepAngle() / lateralPitchRate;
+// double rotationStep = Math.abs(MAX_ROLL_STEP_ANGLE / flightConditions.getRollRate());
+// if (lateralStep < normalStep || rotationStep < normalStep) {
+// System.out.printf(" t=%.3f STEP normal %.3f lateral %.3f rotation %.3f\n",
+// status.time, normalStep, lateralStep, rotationStep);
+// }
+
+
+ timestep = MathUtil.min(simulation.getTimeStep(),
+ simulation.getMaximumStepAngle() / lateralPitchRate,
+ Math.abs(MAX_ROLL_STEP_ANGLE / flightConditions.getRollRate()));
+ if (timestep < 0.0001)
+ timestep = 0.0001;
+
+
+
+ // Calculate aerodynamic forces (only axial if still on launch rod)
+ AerodynamicForces forces;
+ calculator.setConfiguration(status.configuration);
+
+ if (status.launchRod) {
+ forces = calculator.getAxialForces(status.time, flightConditions, status.warnings);
+ } else {
+ forces = calculator.getAerodynamicForces(status.time,
+ flightConditions, status.warnings);
+ }
+
+
+ // Check in case of NaN
+ assert(!Double.isNaN(forces.CD));
+ assert(!Double.isNaN(forces.CN));
+ assert(!Double.isNaN(forces.Caxial));
+ assert(!Double.isNaN(forces.Cm));
+ assert(!Double.isNaN(forces.Cyaw));
+ assert(!Double.isNaN(forces.Cside));
+ assert(!Double.isNaN(forces.Croll));
+
+
+ //// Calculate forces and accelerations
+
+ double dynP = (0.5 * flightConditions.getAtmosphericConditions().getDensity() *
+ MathUtil.pow2(flightConditions.getVelocity()));
+ double refArea = flightConditions.getRefArea();
+ double refLength = flightConditions.getRefLength();
+
+
+ // Linear forces
+ double thrust = calculateThrust(status, currentStep);
+ double dragForce = forces.Caxial * dynP * refArea;
+ double fN = forces.CN * dynP * refArea;
+ double fSide = forces.Cside * dynP * refArea;
+
+ double sin = Math.sin(flightConditions.getTheta());
+ double cos = Math.cos(flightConditions.getTheta());
+
+ double forceX = - fN * cos - fSide * sin;
+ double forceY = - fN * sin - fSide * cos;
+ double forceZ = thrust - dragForce;
+
+
+ Coordinate acceleration = new Coordinate(forceX / forces.cg.weight,
+ forceY / forces.cg.weight, forceZ / forces.cg.weight);
+
+ if (DEBUG)
+ System.out.println(" acceleration before rotation: "+acceleration);
+ acceleration = status.orientation.rotate(acceleration);
+ if (DEBUG)
+ System.out.println(" acceleration after rotation: "+acceleration);
+
+ acceleration = acceleration.sub(0, 0, status.gravityModel.getGravity());
+
+
+ // Convert momenta
+ double Cm = forces.Cm - forces.CN * forces.cg.x / refLength;
+ double momX = (-Cm * sin - forces.Cyaw * cos) * dynP * refArea * refLength;
+ double momY = ( Cm * cos - forces.Cyaw * sin) * dynP * refArea * refLength;
+ double momZ = forces.Croll * dynP * refArea * refLength;
+
+ assert(!Double.isNaN(momX));
+ assert(!Double.isNaN(momY));
+ assert(!Double.isNaN(momZ));
+ assert(!Double.isNaN(forces.longitudalInertia));
+ assert(!Double.isNaN(forces.rotationalInertia));
+
+ Coordinate rotAcc = new Coordinate(momX / forces.longitudalInertia,
+ momY / forces.longitudalInertia, momZ / forces.rotationalInertia);
+ rotAcc = status.orientation.rotate(rotAcc);
+
+// System.out.println(" rotAcc="+rotAcc);
+// System.out.println(" momY="+momY+" lI="+forces.longitudalInertia
+// +" Cm="+Cm+" forces.Cm="+forces.Cm+" cg="+forces.cg);
+// System.out.println(" orient before update:"+status.orientation);
+
+
+
+ // Perform position integration
+ Coordinate avgVel = status.velocity.add(acceleration.multiply(currentStep/2));
+ status.velocity = status.velocity.add(acceleration.multiply(currentStep));
+
+ if (status.launchRod) {
+ // Project velocity onto launch rod direction
+ status.velocity = status.launchRodDirection.multiply(
+ status.velocity.dot(status.launchRodDirection));
+ avgVel = status.launchRodDirection.multiply(avgVel.dot(status.launchRodDirection));
+ }
+
+ status.position = status.position.add(avgVel.multiply(currentStep));
+
+ if (status.launchRod) {
+ // Avoid sinking into ground when on the launch rod
+ if (status.position.z < 0) {
+// System.out.println("Corrected sinking from pos:"+status.position+
+// " vel:"+status.velocity);
+ status.position = Coordinate.NUL;
+ status.velocity = Coordinate.NUL;
+ }
+ }
+
+
+ if (!status.launchRod) {
+ // Integrate rotation when off launch rod
+ Coordinate avgRot = status.rotation.add(rotAcc.multiply(currentStep/2));
+ status.rotation = status.rotation.add(rotAcc.multiply(currentStep));
+ Quaternion stepRotation = Quaternion.rotation(avgRot.multiply(currentStep));
+ status.orientation.multiplyLeft(stepRotation);
+
+ status.orientation.normalize();
+
+ if (DEBUG)
+ System.out.println(" step rotation "+
+ (avgRot.length()*currentStep*180/Math.PI) +"\u00b0 " +
+ "step="+currentStep+" after: "+status.orientation.rotateZ());
+ }
+
+ status.time += currentStep;
+
+
+
+ // Check rotation angle step length and correct time step if necessary
+ double rot = status.rotation.length() * currentStep;
+ if (rot > simulation.getMaximumStepAngle()) {
+ currentStep /= 2;
+ if (DEBUG)
+ System.out.println(" *** Step division to: "+currentStep);
+ }
+ if ((rot < simulation.getMaximumStepAngle()/3) &&
+ (currentStep < simulation.getTimeStep() - 0.000001)) {
+ currentStep *= 2;
+ if (DEBUG)
+ System.out.println(" *** Step multiplication to: "+currentStep);
+ }
+
+
+
+ // Store values
+ data.setValue(FlightDataBranch.TYPE_ACCELERATION_Z, acceleration.z);
+ data.setValue(FlightDataBranch.TYPE_ACCELERATION_TOTAL, acceleration.length());
+ data.setValue(FlightDataBranch.TYPE_VELOCITY_TOTAL, status.velocity.length());
+
+ data.setValue(FlightDataBranch.TYPE_AXIAL_DRAG_COEFF, forces.CD);
+ data.setValue(FlightDataBranch.TYPE_FRICTION_DRAG_COEFF, forces.frictionCD);
+ data.setValue(FlightDataBranch.TYPE_PRESSURE_DRAG_COEFF, forces.pressureCD);
+ data.setValue(FlightDataBranch.TYPE_BASE_DRAG_COEFF, forces.baseCD);
+
+ data.setValue(FlightDataBranch.TYPE_CP_LOCATION, forces.cp.x);
+ data.setValue(FlightDataBranch.TYPE_CG_LOCATION, forces.cg.x);
+
+ data.setValue(FlightDataBranch.TYPE_MASS, forces.cg.weight);
+
+ data.setValue(FlightDataBranch.TYPE_THRUST_FORCE, thrust);
+ data.setValue(FlightDataBranch.TYPE_DRAG_FORCE, dragForce);
+
+
+ Coordinate c = status.orientation.rotateZ();
+ double theta = Math.atan2(c.z, MathUtil.hypot(c.x, c.y));
+ double phi = Math.atan2(c.y, c.x);
+ data.setValue(FlightDataBranch.TYPE_ORIENTATION_THETA, theta);
+ data.setValue(FlightDataBranch.TYPE_ORIENTATION_PHI, phi);
+
+ return null;
+ }
+
+
+
+ /*
+ * TODO: MEDIUM: Many parameters are stored one time step late, how to fix?
+ */
+ private void calculateFlightConditions(FlightConditions flightConditions,
+ RK4SimulationStatus status) {
+
+ // Atmospheric conditions
+ AtmosphericConditions cond = status.startConditions.getAtmosphericModel().
+ getConditions(status.position.z + status.startConditions.getLaunchAltitude());
+ flightConditions.setAtmosphericConditions(cond);
+ status.flightData.setValue(FlightDataBranch.TYPE_AIR_TEMPERATURE, cond.temperature);
+ status.flightData.setValue(FlightDataBranch.TYPE_AIR_PRESSURE, cond.pressure);
+ status.flightData.setValue(FlightDataBranch.TYPE_SPEED_OF_SOUND, cond.getMachSpeed());
+
+
+ // Local wind speed and direction
+ double wind = status.windSimulator.getWindSpeed(status.time);
+ status.flightData.setValue(FlightDataBranch.TYPE_WIND_VELOCITY, wind);
+
+ Coordinate windSpeed = status.velocity.sub(wind, 0, 0);
+ windSpeed = status.orientation.invRotate(windSpeed);
+
+ double theta = Math.atan2(windSpeed.y, windSpeed.x);
+ double velocity = windSpeed.length();
+ double aoa = Math.acos(windSpeed.z / velocity);
+
+ if (Double.isNaN(theta) || Double.isInfinite(theta))
+ theta = 0;
+ if (Double.isNaN(velocity) || Double.isInfinite(velocity))
+ velocity = 0;
+ if (Double.isNaN(aoa) || Double.isInfinite(aoa))
+ aoa = 0;
+
+ flightConditions.setTheta(theta);
+ flightConditions.setAOA(aoa);
+ flightConditions.setVelocity(velocity);
+
+ status.flightData.setValue(FlightDataBranch.TYPE_AOA, aoa);
+
+ // Roll, pitch and yaw rate
+ Coordinate rot = status.orientation.invRotate(status.rotation);
+ flightConditions.setRollRate(rot.z);
+ status.flightData.setValue(FlightDataBranch.TYPE_ROLL_RATE, rot.z);
+ double len = MathUtil.hypot(windSpeed.x, windSpeed.y);
+ if (len < 0.001) {
+ flightConditions.setPitchRate(0);
+ flightConditions.setYawRate(0);
+ lateralPitchRate = 0;
+ } else {
+ double sinTheta = windSpeed.x / len;
+ double cosTheta = windSpeed.y / len;
+ flightConditions.setPitchRate(cosTheta*rot.x + sinTheta*rot.y);
+ flightConditions.setYawRate(sinTheta*rot.x + cosTheta*rot.y);
+ lateralPitchRate = MathUtil.hypot(rot.x, rot.y);
+ }
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.simulation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.util.MathUtil;
+
+
+public class FlightData {
+
+ /**
+ * An immutable FlightData object with NaN data.
+ */
+ public static final FlightData NaN_DATA;
+ static {
+ FlightData data = new FlightData();
+ data.immute();
+ NaN_DATA = data;
+ }
+
+ private boolean mutable = true;
+ private final ArrayList<FlightDataBranch> branches = new ArrayList<FlightDataBranch>();
+
+ private final WarningSet warnings = new WarningSet();
+
+ private double maxAltitude = Double.NaN;
+ private double maxVelocity = Double.NaN;
+ private double maxAcceleration = Double.NaN;
+ private double maxMachNumber = Double.NaN;
+ private double timeToApogee = Double.NaN;
+ private double flightTime = Double.NaN;
+ private double groundHitVelocity = Double.NaN;
+
+
+ /**
+ * Create a FlightData object with no content. The resulting object is mutable.
+ */
+ public FlightData() {
+
+ }
+
+
+ /**
+ * Construct an immutable FlightData object with no data branches but the specified
+ * summary information.
+ *
+ * @param maxAltitude maximum altitude.
+ * @param maxVelocity maximum velocity.
+ * @param maxAcceleration maximum acceleration.
+ * @param maxMachNumber maximum Mach number.
+ * @param timeToApogee time to apogee.
+ * @param flightTime total flight time.
+ * @param groundHitVelocity ground hit velocity.
+ */
+ public FlightData(double maxAltitude, double maxVelocity, double maxAcceleration,
+ double maxMachNumber, double timeToApogee, double flightTime,
+ double groundHitVelocity) {
+ this.maxAltitude = maxAltitude;
+ this.maxVelocity = maxVelocity;
+ this.maxAcceleration = maxAcceleration;
+ this.maxMachNumber = maxMachNumber;
+ this.timeToApogee = timeToApogee;
+ this.flightTime = flightTime;
+ this.groundHitVelocity = groundHitVelocity;
+
+ this.immute();
+ }
+
+
+ /**
+ * Create an immutable FlightData object with the specified branches.
+ *
+ * @param branches the branches.
+ */
+ public FlightData(FlightDataBranch ... branches) {
+ this();
+
+ for (FlightDataBranch b: branches)
+ this.addBranch(b);
+
+ calculateIntrestingValues();
+ this.immute();
+ }
+
+
+
+
+ /**
+ * Returns the warning set associated with this object. This WarningSet cannot be
+ * set, so simulations must use this warning set to store their warnings.
+ * The returned WarningSet should not be modified otherwise.
+ *
+ * @return the warnings generated during this simulation.
+ */
+ public WarningSet getWarningSet() {
+ return warnings;
+ }
+
+
+ public void addBranch(FlightDataBranch branch) {
+ if (!mutable)
+ throw new IllegalStateException("FlightData has been made immutable");
+
+ branch.immute();
+ branches.add(branch);
+
+ if (branches.size() == 1) {
+ calculateIntrestingValues();
+ }
+ }
+
+ public int getBranchCount() {
+ return branches.size();
+ }
+
+ public FlightDataBranch getBranch(int n) {
+ return branches.get(n);
+ }
+
+
+
+ public double getMaxAltitude() {
+ return maxAltitude;
+ }
+
+ public double getMaxVelocity() {
+ return maxVelocity;
+ }
+
+ public double getMaxAcceleration() {
+ return maxAcceleration;
+ }
+
+ public double getMaxMachNumber() {
+ return maxMachNumber;
+ }
+
+ public double getTimeToApogee() {
+ return timeToApogee;
+ }
+
+ public double getFlightTime() {
+ return flightTime;
+ }
+
+ public double getGroundHitVelocity() {
+ return groundHitVelocity;
+ }
+
+
+
+ /**
+ * Calculate the max. altitude/velocity/acceleration, time to apogee, flight time
+ * and ground hit velocity.
+ */
+ private void calculateIntrestingValues() {
+ if (branches.isEmpty())
+ return;
+
+ FlightDataBranch branch = branches.get(0);
+ maxAltitude = branch.getMaximum(FlightDataBranch.TYPE_ALTITUDE);
+ maxVelocity = branch.getMaximum(FlightDataBranch.TYPE_VELOCITY_TOTAL);
+ maxAcceleration = branch.getMaximum(FlightDataBranch.TYPE_ACCELERATION_TOTAL);
+ maxMachNumber = branch.getMaximum(FlightDataBranch.TYPE_MACH_NUMBER);
+
+ flightTime = branch.getLast(FlightDataBranch.TYPE_TIME);
+ if (branch.getLast(FlightDataBranch.TYPE_ALTITUDE) < 10) {
+ groundHitVelocity = branch.getLast(FlightDataBranch.TYPE_VELOCITY_TOTAL);
+ } else {
+ groundHitVelocity = Double.NaN;
+ }
+
+ // Time to apogee
+ List<Double> time = branch.get(FlightDataBranch.TYPE_TIME);
+ List<Double> altitude = branch.get(FlightDataBranch.TYPE_ALTITUDE);
+
+ if (time == null || altitude == null) {
+ timeToApogee = Double.NaN;
+ return;
+ }
+ int index = 0;
+ for (Double alt: altitude) {
+ if (alt != null) {
+ if (MathUtil.equals(alt, maxAltitude))
+ break;
+ }
+
+ index++;
+ }
+ if (index < time.size())
+ timeToApogee = time.get(index);
+ else
+ timeToApogee = Double.NaN;
+ }
+
+
+ public void immute() {
+ mutable = false;
+ }
+ public boolean isMutable() {
+ return mutable;
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.simulation;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.Pair;
+
+
+public class FlightDataBranch {
+
+ //// Time
+ public static final Type TYPE_TIME =
+ new Type("Time", UnitGroup.UNITS_FLIGHT_TIME, 1);
+
+
+ //// Vertical position and motion
+ public static final Type TYPE_ALTITUDE =
+ new Type("Altitude", UnitGroup.UNITS_DISTANCE, 10);
+ public static final Type TYPE_VELOCITY_Z =
+ new Type("Vertical velocity", UnitGroup.UNITS_VELOCITY, 11);
+ public static final Type TYPE_ACCELERATION_Z =
+ new Type("Vertical acceleration", UnitGroup.UNITS_ACCELERATION, 12);
+
+
+ //// Total motion
+ public static final Type TYPE_VELOCITY_TOTAL =
+ new Type("Total velocity", UnitGroup.UNITS_VELOCITY, 20);
+ public static final Type TYPE_ACCELERATION_TOTAL =
+ new Type("Total acceleration", UnitGroup.UNITS_ACCELERATION, 21);
+
+
+ //// Lateral position and motion
+
+ public static final Type TYPE_POSITION_X =
+ new Type("Position upwind", UnitGroup.UNITS_DISTANCE, 30);
+ public static final Type TYPE_POSITION_Y =
+ new Type("Position parallel to wind", UnitGroup.UNITS_DISTANCE, 31);
+ public static final Type TYPE_POSITION_XY =
+ new Type("Lateral distance", UnitGroup.UNITS_DISTANCE, 32);
+ public static final Type TYPE_POSITION_DIRECTION =
+ new Type("Lateral direction", UnitGroup.UNITS_ANGLE, 33);
+
+ public static final Type TYPE_VELOCITY_XY =
+ new Type("Lateral velocity", UnitGroup.UNITS_VELOCITY, 34);
+ public static final Type TYPE_ACCELERATION_XY =
+ new Type("Lateral acceleration", UnitGroup.UNITS_ACCELERATION, 35);
+
+
+ //// Angular motion
+ public static final Type TYPE_AOA = new Type("Angle of attack", UnitGroup.UNITS_ANGLE, 40);
+ public static final Type TYPE_ROLL_RATE = new Type("Roll rate", UnitGroup.UNITS_ROLL, 41);
+ public static final Type TYPE_PITCH_RATE = new Type("Pitch rate", UnitGroup.UNITS_ROLL, 42);
+ public static final Type TYPE_YAW_RATE = new Type("Yaw rate", UnitGroup.UNITS_ROLL, 43);
+
+
+ //// Stability information
+ public static final Type TYPE_MASS =
+ new Type("Mass", UnitGroup.UNITS_MASS, 50);
+ public static final Type TYPE_CP_LOCATION =
+ new Type("CP location", UnitGroup.UNITS_LENGTH, 51);
+ public static final Type TYPE_CG_LOCATION =
+ new Type("CG location", UnitGroup.UNITS_LENGTH, 52);
+ public static final Type TYPE_STABILITY =
+ new Type("Stability margin calibers", UnitGroup.UNITS_COEFFICIENT, 53);
+
+
+ //// Characteristic numbers
+ public static final Type TYPE_MACH_NUMBER =
+ new Type("Mach number", UnitGroup.UNITS_COEFFICIENT, 60);
+ public static final Type TYPE_REYNOLDS_NUMBER =
+ new Type("Reynolds number", UnitGroup.UNITS_COEFFICIENT, 61);
+
+
+ //// Thrust and drag
+ public static final Type TYPE_THRUST_FORCE =
+ new Type("Thrust", UnitGroup.UNITS_FORCE, 70);
+ public static final Type TYPE_DRAG_FORCE =
+ new Type("Drag force", UnitGroup.UNITS_FORCE, 71);
+
+ public static final Type TYPE_DRAG_COEFF =
+ new Type("Drag coefficient", UnitGroup.UNITS_COEFFICIENT, 72);
+ public static final Type TYPE_AXIAL_DRAG_COEFF =
+ new Type("Axial drag coefficient", UnitGroup.UNITS_COEFFICIENT, 73);
+
+
+ //// Component drag coefficients
+ public static final Type TYPE_FRICTION_DRAG_COEFF =
+ new Type("Friction drag coefficient", UnitGroup.UNITS_COEFFICIENT, 80);
+ public static final Type TYPE_PRESSURE_DRAG_COEFF =
+ new Type("Pressure drag coefficient", UnitGroup.UNITS_COEFFICIENT, 81);
+ public static final Type TYPE_BASE_DRAG_COEFF =
+ new Type("Base drag coefficient", UnitGroup.UNITS_COEFFICIENT, 82);
+
+
+ //// Other coefficients
+ public static final Type TYPE_NORMAL_FORCE_COEFF =
+ new Type("Normal force coefficient", UnitGroup.UNITS_COEFFICIENT, 90);
+ public static final Type TYPE_PITCH_MOMENT_COEFF =
+ new Type("Pitch moment coefficient", UnitGroup.UNITS_COEFFICIENT, 91);
+ public static final Type TYPE_YAW_MOMENT_COEFF =
+ new Type("Yaw moment coefficient", UnitGroup.UNITS_COEFFICIENT, 92);
+ public static final Type TYPE_SIDE_FORCE_COEFF =
+ new Type("Side force coefficient", UnitGroup.UNITS_COEFFICIENT, 93);
+ public static final Type TYPE_ROLL_MOMENT_COEFF =
+ new Type("Roll moment coefficient", UnitGroup.UNITS_COEFFICIENT, 94);
+ public static final Type TYPE_ROLL_FORCING_COEFF =
+ new Type("Roll forcing coefficient", UnitGroup.UNITS_COEFFICIENT, 95);
+ public static final Type TYPE_ROLL_DAMPING_COEFF =
+ new Type("Roll damping coefficient", UnitGroup.UNITS_COEFFICIENT, 96);
+
+ public static final Type TYPE_PITCH_DAMPING_MOMENT_COEFF =
+ new Type("Pitch damping coefficient", UnitGroup.UNITS_COEFFICIENT, 97);
+ public static final Type TYPE_YAW_DAMPING_MOMENT_COEFF =
+ new Type("Yaw damping coefficient", UnitGroup.UNITS_COEFFICIENT, 98);
+
+
+ //// Reference length + area
+ public static final Type TYPE_REFERENCE_LENGTH =
+ new Type("Reference length", UnitGroup.UNITS_LENGTH, 100);
+ public static final Type TYPE_REFERENCE_AREA =
+ new Type("Reference area", UnitGroup.UNITS_AREA, 101);
+
+
+ //// Orientation
+ public static final Type TYPE_ORIENTATION_THETA =
+ new Type("Vertical orientation (zenith)", UnitGroup.UNITS_ANGLE, 106);
+ public static final Type TYPE_ORIENTATION_PHI =
+ new Type("Lateral orientation (azimuth)", UnitGroup.UNITS_ANGLE, 107);
+
+
+ //// Atmospheric conditions
+ public static final Type TYPE_WIND_VELOCITY = new Type("Wind velocity",
+ UnitGroup.UNITS_VELOCITY, 110);
+ public static final Type TYPE_AIR_TEMPERATURE = new Type("Air temperature",
+ UnitGroup.UNITS_TEMPERATURE, 111);
+ public static final Type TYPE_AIR_PRESSURE = new Type("Air pressure",
+ UnitGroup.UNITS_PRESSURE, 112);
+ public static final Type TYPE_SPEED_OF_SOUND = new Type("Speed of sound",
+ UnitGroup.UNITS_VELOCITY, 113);
+
+
+ //// Simulation information
+ public static final Type TYPE_TIME_STEP = new Type("Simulation time step",
+ UnitGroup.UNITS_TIME_STEP, 200);
+ public static final Type TYPE_COMPUTATION_TIME = new Type("Computation time",
+ UnitGroup.UNITS_SHORT_TIME, 201);
+
+
+ /**
+ * Array of known data types for String -> Type conversion.
+ */
+ private static final Type[] TYPES = {
+ TYPE_TIME,
+ TYPE_ALTITUDE, TYPE_VELOCITY_Z, TYPE_ACCELERATION_Z,
+ TYPE_VELOCITY_TOTAL, TYPE_ACCELERATION_TOTAL,
+ TYPE_POSITION_X, TYPE_POSITION_Y, TYPE_POSITION_XY, TYPE_POSITION_DIRECTION,
+ TYPE_VELOCITY_XY, TYPE_ACCELERATION_XY,
+ TYPE_AOA, TYPE_ROLL_RATE, TYPE_PITCH_RATE, TYPE_YAW_RATE,
+ TYPE_MASS, TYPE_CP_LOCATION, TYPE_CG_LOCATION, TYPE_STABILITY,
+ TYPE_MACH_NUMBER, TYPE_REYNOLDS_NUMBER,
+ TYPE_THRUST_FORCE, TYPE_DRAG_FORCE,
+ TYPE_DRAG_COEFF, TYPE_AXIAL_DRAG_COEFF,
+ TYPE_FRICTION_DRAG_COEFF, TYPE_PRESSURE_DRAG_COEFF, TYPE_BASE_DRAG_COEFF,
+ TYPE_NORMAL_FORCE_COEFF, TYPE_PITCH_MOMENT_COEFF, TYPE_YAW_MOMENT_COEFF, TYPE_SIDE_FORCE_COEFF,
+ TYPE_ROLL_MOMENT_COEFF, TYPE_ROLL_FORCING_COEFF, TYPE_ROLL_DAMPING_COEFF,
+ TYPE_PITCH_DAMPING_MOMENT_COEFF, TYPE_YAW_DAMPING_MOMENT_COEFF,
+ TYPE_REFERENCE_LENGTH, TYPE_REFERENCE_AREA,
+ TYPE_ORIENTATION_THETA, TYPE_ORIENTATION_PHI,
+ TYPE_WIND_VELOCITY, TYPE_AIR_TEMPERATURE, TYPE_AIR_PRESSURE, TYPE_SPEED_OF_SOUND,
+ TYPE_TIME_STEP, TYPE_COMPUTATION_TIME
+ };
+
+ /**
+ * Return a {@link Type} based on a string description. This returns known data types
+ * if possible, or a new type otherwise.
+ *
+ * @param s the string description of the type.
+ * @param u the unit group the new type should belong to if a new group is created.
+ * @return a data type.
+ */
+ public static Type getType(String s, UnitGroup u) {
+ for (Type t: TYPES) {
+ if (t.getName().equalsIgnoreCase(s))
+ return t;
+ }
+ return new Type(s, u);
+ }
+
+
+
+ public static class Type implements Comparable<Type> {
+ private final String name;
+ private final UnitGroup units;
+ private final int priority;
+ private final int hashCode;
+
+ private Type(String typeName, UnitGroup units) {
+ this(typeName, units, 999);
+ }
+
+ public Type(String typeName, UnitGroup units, int priority) {
+ if (typeName == null)
+ throw new IllegalArgumentException("typeName is null");
+ this.name = typeName;
+ this.units = units;
+ this.priority = priority;
+ this.hashCode = this.name.toLowerCase().hashCode();
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public UnitGroup getUnitGroup() {
+ return units;
+ }
+
+ @Override
+ public String toString() {
+ return name;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof Type))
+ return false;
+ return this.name.equalsIgnoreCase(((Type)other).name);
+ }
+ @Override
+ public int hashCode() {
+ return hashCode;
+ }
+
+ @Override
+ public int compareTo(Type o) {
+ return this.priority - o.priority;
+ }
+ }
+
+
+
+
+ /** The name of this flight data branch. */
+ private final String branchName;
+
+ private final Map<Type, ArrayList<Double>> values =
+ new LinkedHashMap<Type, ArrayList<Double>>();
+
+ private final Map<Type, Double> maxValues = new HashMap<Type, Double>();
+ private final Map<Type, Double> minValues = new HashMap<Type, Double>();
+
+
+ private final ArrayList<Pair<Double,FlightEvent>> events =
+ new ArrayList<Pair<Double,FlightEvent>>();
+
+ private boolean mutable = true;
+
+
+ public FlightDataBranch(String name, Type... types) {
+ if (types.length == 0) {
+ throw new IllegalArgumentException("Must specify at least one data type.");
+ }
+
+ this.branchName = name;
+
+ for (Type t: types) {
+ if (values.containsKey(t)) {
+ throw new IllegalArgumentException("Value type "+t+" specified multiple " +
+ "times in constructor.");
+ }
+
+ values.put(t, new ArrayList<Double>());
+ minValues.put(t, Double.NaN);
+ maxValues.put(t, Double.NaN);
+ }
+ }
+
+
+ public String getBranchName() {
+ return branchName;
+ }
+
+ public Type[] getTypes() {
+ Type[] array = values.keySet().toArray(new Type[0]);
+ Arrays.sort(array);
+ return array;
+ }
+
+ public int getLength() {
+ for (Type t: values.keySet()) {
+ return values.get(t).size();
+ }
+ return 0;
+ }
+
+
+
+ public void addPoint() {
+ if (!mutable)
+ throw new IllegalStateException("FlightDataBranch has been made immutable.");
+ for (Type t: values.keySet()) {
+ values.get(t).add(Double.NaN);
+ }
+ }
+
+ public void setValue(Type type, double value) {
+ if (!mutable)
+ throw new IllegalStateException("FlightDataBranch has been made immutable.");
+ ArrayList<Double> list = values.get(type);
+ if (list == null) {
+
+ list = new ArrayList<Double>();
+ int n = getLength();
+ for (int i=0; i < n; i++) {
+ list.add(Double.NaN);
+ }
+ values.put(type, list);
+ minValues.put(type, value);
+ maxValues.put(type, value);
+
+ }
+ list.set(list.size()-1, value);
+ double min = minValues.get(type);
+ double max = maxValues.get(type);
+
+ if (Double.isNaN(min) || (value < min)) {
+ minValues.put(type, value);
+ }
+ if (Double.isNaN(max) || (value > max)) {
+ maxValues.put(type, value);
+ }
+ }
+
+
+ @SuppressWarnings("unchecked")
+ public List<Double> get(Type type) {
+ ArrayList<Double> list = values.get(type);
+ if (list==null)
+ return null;
+ return (List<Double>)list.clone();
+ }
+
+
+ public double get(Type type, int index) {
+ ArrayList<Double> list = values.get(type);
+ if (list==null)
+ return Double.NaN;
+ return list.get(index);
+ }
+
+
+ /**
+ * Return the last value of the specified type in the branch, or NaN if the type is
+ * unavailable.
+ *
+ * @param type the parameter type.
+ * @return the last value in this branch, or NaN.
+ */
+ public double getLast(Type type) {
+ ArrayList<Double> list = values.get(type);
+ if (list==null || list.isEmpty())
+ return Double.NaN;
+ return list.get(list.size()-1);
+ }
+
+ /**
+ * Return the minimum value of the specified type in the branch, or NaN if the type
+ * is unavailable.
+ *
+ * @param type the parameter type.
+ * @return the minimum value in this branch, or NaN.
+ */
+ public double getMinimum(Type type) {
+ Double v = minValues.get(type);
+ if (v==null)
+ return Double.NaN;
+ return v;
+ }
+
+ /**
+ * Return the maximum value of the specified type in the branch, or NaN if the type
+ * is unavailable.
+ *
+ * @param type the parameter type.
+ * @return the maximum value in this branch, or NaN.
+ */
+ public double getMaximum(Type type) {
+ Double v = maxValues.get(type);
+ if (v==null)
+ return Double.NaN;
+ return v;
+ }
+
+
+ public void addEvent(double time, FlightEvent event) {
+ if (!mutable)
+ throw new IllegalStateException("FlightDataBranch has been made immutable.");
+ events.add(new Pair<Double,FlightEvent>(time,event));
+ }
+
+
+ /**
+ * Return the list of events. The list is a list of (time, event) pairs.
+ *
+ * @return the list of events during the flight.
+ */
+ @SuppressWarnings("unchecked")
+ public List<Pair<Double, FlightEvent>> getEvents() {
+ return (List<Pair<Double, FlightEvent>>) events.clone();
+ }
+
+
+ /**
+ * Make this FlightDataBranch immutable. Any calls to the set methods that would
+ * modify this object will after this call throw an <code>IllegalStateException</code>.
+ */
+ public void immute() {
+ mutable = false;
+ }
+
+ public boolean isMutable() {
+ return mutable;
+ }
+}
--- /dev/null
+package net.sf.openrocket.simulation;
+
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+
+/**
+ * A class that defines an event during the flight of a rocket.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class FlightEvent implements Comparable<FlightEvent> {
+
+ /**
+ * The type of the flight event.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+ public enum Type {
+ /**
+ * Rocket launch.
+ */
+ LAUNCH,
+ /**
+ * When the motor has lifted off the ground.
+ */
+ LIFTOFF,
+ /**
+ * Launch rod has been cleared.
+ */
+ LAUNCHROD,
+ /**
+ * Ignition of a motor. Source is the motor mount the motor of which has ignited.
+ */
+ IGNITION,
+ /**
+ * Burnout of a motor. Source is the motor mount the motor of which has burnt out.
+ */
+ BURNOUT,
+ /**
+ * Ejection charge of a motor fired. Source is the motor mount the motor of
+ * which has exploded its ejection charge.
+ */
+ EJECTION_CHARGE,
+ /**
+ * Opening of a recovery device. Source is the RecoveryComponent which has opened.
+ */
+ RECOVERY_DEVICE_DEPLOYMENT,
+ /**
+ * Separation of a stage. Source is the stage which has separated all lower stages.
+ */
+ STAGE_SEPARATION,
+ /**
+ * Apogee has been reached.
+ */
+ APOGEE,
+ /**
+ * Ground has been hit after flight.
+ */
+ GROUND_HIT,
+
+ /**
+ * End of simulation. Placing this to the queue will end the simulation.
+ */
+ SIMULATION_END,
+
+ /**
+ * A change in altitude has occurred. Data is a <code>Pair<Double,Double></code>
+ * which contains the old and new altitudes.
+ */
+ ALTITUDE
+ }
+
+ private final Type type;
+ private final double time;
+ private final RocketComponent source;
+ private final Object data;
+
+
+ public FlightEvent(Type type, double time) {
+ this(type, time, null);
+ }
+
+ public FlightEvent(Type type, double time, RocketComponent source) {
+ this(type,time,source,null);
+ }
+
+ public FlightEvent(Type type, double time, RocketComponent source, Object data) {
+ this.type = type;
+ this.time = time;
+ this.source = source;
+ this.data = data;
+ }
+
+
+
+ public Type getType() {
+ return type;
+ }
+
+ public double getTime() {
+ return time;
+ }
+
+ public RocketComponent getSource() {
+ return source;
+ }
+
+ public Object getData() {
+ return data;
+ }
+
+
+ public FlightEvent resetSource() {
+ return new FlightEvent(type, time, null, data);
+ }
+
+ /**
+ * Compares this event to another event depending on the event time. Secondary
+ * sorting is performed based on the event type ordinal.
+ */
+ @Override
+ public int compareTo(FlightEvent o) {
+ if (this.time < o.time)
+ return -1;
+ if (this.time > o.time)
+ return 1;
+
+ return this.type.ordinal() - o.type.ordinal();
+ }
+
+ @Override
+ public String toString() {
+ return "FlightEvent[type="+type.toString()+",time="+time+",source="+source+"]";
+ }
+}
--- /dev/null
+package net.sf.openrocket.simulation;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.PriorityQueue;
+
+import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
+import net.sf.openrocket.aerodynamics.AtmosphericConditions;
+import net.sf.openrocket.aerodynamics.AtmosphericModel;
+import net.sf.openrocket.aerodynamics.Warning;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.rocketcomponent.Clusterable;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.rocketcomponent.MotorMount;
+import net.sf.openrocket.rocketcomponent.RecoveryDevice;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.simulation.exception.SimulationException;
+import net.sf.openrocket.simulation.exception.SimulationLaunchException;
+import net.sf.openrocket.simulation.exception.SimulationNotSupportedException;
+import net.sf.openrocket.unit.UnitGroup;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Pair;
+
+
+
+
+/**
+ * Abstract class that implements a flight simulation using a specific
+ * {@link AerodynamicCalculator}. The simulation methods are the <code>simulate</code>
+ * methods.
+ * <p>
+ * This class contains the event flight event handling mechanisms common to all
+ * simulations. The simulator calls the {@link #step(SimulationConditions, SimulationStatus)}
+ * method periodically to take time steps. Concrete subclasses of this class specify
+ * how the actual time steps are taken (e.g. Euler or Runge-Kutta integration).
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public abstract class FlightSimulator {
+
+ public static final double RECOVERY_TIME_STEP = 0.5;
+
+ /** The {@link AerodynamicCalculator} to use to calculate the aerodynamic forces. */
+ protected AerodynamicCalculator calculator = null;
+
+ /** The {@link AtmosphericModel} used to model the atmosphere. */
+ protected AtmosphericModel atmosphericModel;
+
+ /** Listener list. */
+ protected final List<SimulationListener> listeners = new ArrayList<SimulationListener>();
+
+
+ private PriorityQueue<FlightEvent> eventQueue;
+ private WarningSet warnings;
+
+
+ public FlightSimulator() {
+
+ }
+
+ public FlightSimulator(AerodynamicCalculator calculator) {
+ this.calculator = calculator;
+ }
+
+
+
+
+ public AerodynamicCalculator getCalculator() {
+ return calculator;
+ }
+
+ public void setCalculator(AerodynamicCalculator calc) {
+ this.calculator = calc;
+ }
+
+
+ /**
+ * Will be removed! Use {@link #simulate(SimulationConditions)} instead.
+ */
+ @Deprecated
+ public FlightData simulate(SimulationConditions simulation,
+ boolean simulateBranches, WarningSet warnings)
+ throws SimulationNotSupportedException {
+ try {
+ return simulate(simulation);
+ } catch (SimulationException e) {
+ throw new SimulationNotSupportedException(e);
+ }
+ }
+
+
+ public FlightData simulate(SimulationConditions simulation)
+ throws SimulationException {
+
+ // Set up flight data
+ FlightData flightData = new FlightData();
+
+ // Set up rocket configuration
+ Configuration configuration = calculator.getConfiguration();
+ configuration.setAllStages();
+ configuration.setMotorConfigurationID(simulation.getMotorConfigurationID());
+
+ if (!configuration.hasMotors()) {
+ throw new SimulationLaunchException("No motors defined.");
+ }
+
+ // Set up the event queue
+ eventQueue = new PriorityQueue<FlightEvent>();
+ eventQueue.add(new FlightEvent(FlightEvent.Type.LAUNCH, 0, simulation.getRocket()));
+
+ // Initialize the simulation
+ SimulationStatus status = initializeSimulation(configuration, simulation);
+ status.warnings = flightData.getWarningSet();
+ warnings = flightData.getWarningSet();
+
+
+ // Start the simulation
+ while (handleEvents(eventQueue, status)) {
+
+ // Take the step
+ double oldAlt = status.position.z;
+
+ if (status.deployedRecoveryDevices.isEmpty()) {
+ step(simulation, status);
+ } else {
+ recoveryStep(simulation, status);
+ }
+
+
+ // Add appropriate events
+
+ if (!status.liftoff) {
+
+ // Avoid sinking into ground before liftoff
+ if (status.position.z < 0) {
+ status.position = Coordinate.NUL;
+ status.velocity = Coordinate.NUL;
+ }
+ // Detect liftoff
+ if (status.position.z > 0.01) {
+ eventQueue.add(new FlightEvent(FlightEvent.Type.LIFTOFF, status.time));
+ status.liftoff = true;
+ }
+
+ } else {
+
+ // Check ground hit after liftoff
+ if (status.position.z < 0) {
+ status.position = status.position.setZ(0);
+ eventQueue.add(new FlightEvent(FlightEvent.Type.GROUND_HIT, status.time));
+ eventQueue.add(new FlightEvent(FlightEvent.Type.SIMULATION_END, status.time));
+ }
+
+ }
+
+
+ // Add altitude event
+ eventQueue.add(new FlightEvent(FlightEvent.Type.ALTITUDE, status.time,
+ status.configuration.getRocket(),
+ new Pair<Double,Double>(oldAlt,status.position.z)));
+
+
+ // Check for launch guide clearance
+ if (status.launchRod && status.position.length() > status.launchRodLength) {
+ eventQueue.add(new FlightEvent(FlightEvent.Type.LAUNCHROD, status.time, null));
+ status.launchRod = false;
+ }
+
+
+ // Check for apogee
+ if (!status.apogeeReached && status.position.z < oldAlt - 0.001) {
+ eventQueue.add(new FlightEvent(FlightEvent.Type.APOGEE, status.time,
+ status.configuration.getRocket()));
+ status.apogeeReached = true;
+ }
+
+
+ // Call listeners
+ SimulationListener[] array = listeners.toArray(new SimulationListener[0]);
+ for (SimulationListener l: array) {
+ addListenerEvents(l.stepTaken(status));
+ }
+ }
+
+ flightData.addBranch(status.flightData);
+
+ System.out.println("Warnings at the end: "+flightData.getWarningSet());
+
+ // TODO: HIGH: Simulate branches
+ return flightData;
+ }
+
+
+
+
+ /**
+ * Handles events occurring during the flight from the <code>eventQueue</code>.
+ * Each event that has occurred before or at the current simulation time is
+ * processed. Suitable events are also added to the flight data.
+ *
+ * @param data the FlightData to add events to.
+ * @param endEvent the event at which to end this simulation.
+ * @param simulateBranches whether to invoke a separate simulation of separated lower
+ * stages
+ * @throws SimulationException
+ */
+ private boolean handleEvents(PriorityQueue<FlightEvent> queue, SimulationStatus status)
+ throws SimulationException {
+ FlightEvent e;
+ boolean ret = true;
+
+ e = queue.peek();
+ // Skip to events if no motor has ignited yet
+ if (!status.motorIgnited) {
+ if (e == null || Double.isNaN(e.getTime()) || e.getTime() > 1000000) {
+ throw new SimulationLaunchException("No motors ignited.");
+ }
+ status.time = e.getTime();
+ }
+
+ while ((e != null) && (e.getTime() <= status.time)) {
+ e = queue.poll();
+
+ // If no motor has ignited and no events are occurring, abort
+ if (!status.motorIgnited) {
+ if (e == null || Double.isNaN(e.getTime()) || e.getTime() > 1000000) {
+ throw new SimulationLaunchException("No motors ignited.");
+ }
+ }
+
+ // If event is motor burnout without liftoff, abort
+ if (e.getType() == FlightEvent.Type.BURNOUT && !status.liftoff) {
+ throw new SimulationLaunchException("Motor burnout without liftoff.");
+ }
+
+ // Add event to flight data
+ if (e.getType() != FlightEvent.Type.ALTITUDE) {
+ status.flightData.addEvent(status.time, e.resetSource());
+ }
+
+ // Check for motor ignition events, add ignition events to queue
+ Iterator<MotorMount> iterator = status.configuration.motorIterator();
+ while (iterator.hasNext()) {
+ MotorMount mount = iterator.next();
+ if (mount.getIgnitionEvent().isActivationEvent(e, (RocketComponent)mount)) {
+ queue.add(new FlightEvent(FlightEvent.Type.IGNITION,
+ status.time + mount.getIgnitionDelay(), (RocketComponent)mount));
+ }
+ }
+
+ // Handle motor ignition events, add burnout events
+ if (e.getType() == FlightEvent.Type.IGNITION) {
+ status.motorIgnited = true;
+
+ String id = status.configuration.getMotorConfigurationID();
+ MotorMount mount = (MotorMount) e.getSource();
+ Motor motor = mount.getMotor(id);
+
+ status.configuration.setIgnitionTime(mount, e.getTime());
+ queue.add(new FlightEvent(FlightEvent.Type.BURNOUT,
+ e.getTime() + motor.getTotalTime(), (RocketComponent)mount));
+ queue.add(new FlightEvent(FlightEvent.Type.EJECTION_CHARGE,
+ e.getTime() + motor.getTotalTime() + mount.getMotorDelay(id),
+ (RocketComponent)mount));
+ }
+
+
+ // Handle stage separation on motor ignition
+ if (e.getType() == FlightEvent.Type.IGNITION) {
+ RocketComponent mount = (RocketComponent) e.getSource();
+ int n = mount.getStageNumber();
+ if (n < mount.getRocket().getStageCount()-1) {
+ if (status.configuration.isStageActive(n+1)) {
+ queue.add(new FlightEvent(FlightEvent.Type.STAGE_SEPARATION, e.getTime(),
+ mount.getStage()));
+ }
+ }
+ }
+ if (e.getType() == FlightEvent.Type.STAGE_SEPARATION) {
+ RocketComponent stage = (RocketComponent) e.getSource();
+ int n = stage.getStageNumber();
+ status.configuration.setToStage(n);
+ }
+
+
+ // Handle recovery device deployment
+ Iterator<RocketComponent> iterator1 = status.configuration.iterator();
+ while (iterator1.hasNext()) {
+ RocketComponent c = iterator1.next();
+ if (!(c instanceof RecoveryDevice))
+ continue;
+ if (((RecoveryDevice)c).getDeployEvent().isActivationEvent(e, c)) {
+ // Delay event by at least 1ms to allow stage separation to occur first
+ queue.add(new FlightEvent(FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT,
+ e.getTime() + Math.max(0.001, ((RecoveryDevice)c).getDeployDelay()), c));
+ }
+ }
+ if (e.getType() == FlightEvent.Type.RECOVERY_DEVICE_DEPLOYMENT) {
+ RocketComponent c = e.getSource();
+ int n = c.getStageNumber();
+ // Ignore event if stage not active
+ if (status.configuration.isStageActive(n)) {
+
+ // Check whether any motor is active anymore
+ iterator = status.configuration.motorIterator();
+ while (iterator.hasNext()) {
+ MotorMount mount = iterator.next();
+ Motor motor = mount.getMotor(status.configuration.getMotorConfigurationID());
+ if (motor == null)
+ continue;
+ if (status.configuration.getIgnitionTime(mount) + motor.getAverageTime()
+ > status.time) {
+ warnings.add(Warning.RECOVERY_DEPLOYMENT_WHILE_BURNING);
+ }
+ }
+
+ // Check for launch rod
+ if (status.launchRod) {
+ warnings.add(Warning.fromString("Recovery device device deployed while on " +
+ "the launch guide."));
+ }
+
+ // Check current velocity
+ if (status.velocity.length() > 20) {
+ // TODO: LOW: Custom warning.
+ warnings.add(Warning.fromString("Recovery device deployment at high " +
+ "speed ("
+ + UnitGroup.UNITS_VELOCITY.toStringUnit(status.velocity.length())
+ + ")."));
+ }
+
+ status.liftoff = true;
+ status.deployedRecoveryDevices.add((RecoveryDevice)c);
+ }
+ }
+
+
+
+ // Simulation end
+ if (e.getType() == FlightEvent.Type.SIMULATION_END) {
+ ret = false;
+ }
+
+
+ // Call listeners
+ SimulationListener[] array = listeners.toArray(new SimulationListener[0]);
+ for (SimulationListener l: array) {
+ addListenerEvents(l.handleEvent(e, status));
+ }
+
+
+ e = queue.peek();
+ // Skip to events if no motor has ignited yet
+ if (!status.motorIgnited) {
+ if (e == null || Double.isNaN(e.getTime()) || e.getTime() > 1000000) {
+ throw new SimulationLaunchException("No motors ignited.");
+ }
+ status.time = e.getTime();
+ }
+ }
+ return ret;
+ }
+
+
+ // TODO: MEDIUM: Create method storeData() which is overridden by simulators
+
+
+ /**
+ * Perform a step during recovery. This is a 3-DOF simulation using simple Euler
+ * integration.
+ *
+ * @param conditions the simulation conditions.
+ * @param status the current simulation status.
+ */
+ protected void recoveryStep(SimulationConditions conditions, SimulationStatus status) {
+ double totalCD = 0;
+ double refArea = status.configuration.getReferenceArea();
+
+ // TODO: MEDIUM: Call listeners during recovery phase
+
+ // Get the atmospheric conditions
+ AtmosphericConditions atmosphere = conditions.getAtmosphericModel().getConditions(
+ conditions.getLaunchAltitude() + status.position.z);
+
+ //// Local wind speed and direction
+ double windSpeed = status.windSimulator.getWindSpeed(status.time);
+ Coordinate airSpeed = status.velocity.add(windSpeed, 0, 0);
+
+ // Get total CD
+ double mach = airSpeed.length() / atmosphere.getMachSpeed();
+ for (RecoveryDevice c: status.deployedRecoveryDevices) {
+ totalCD += c.getCD(mach) * c.getArea() / refArea;
+ }
+
+ // Compute drag force
+ double dynP = (0.5 * atmosphere.getDensity() * airSpeed.length2());
+ double dragForce = totalCD * dynP * refArea;
+ double mass = calculator.getCG(status.time).weight;
+
+
+ // Compute drag acceleration
+ Coordinate linearAcceleration;
+ if (airSpeed.length() > 0.001) {
+ linearAcceleration = airSpeed.normalize().multiply(-dragForce/mass);
+ } else {
+ linearAcceleration = Coordinate.NUL;
+ }
+
+ // Add effect of gravity
+ linearAcceleration = linearAcceleration.sub(0, 0, status.gravityModel.getGravity());
+
+
+ // Select time step
+ double timeStep = MathUtil.min(0.5/linearAcceleration.length(), RECOVERY_TIME_STEP);
+
+ // Perform Euler integration
+ status.position = (status.position.add(status.velocity.multiply(timeStep)).
+ add(linearAcceleration.multiply(MathUtil.pow2(timeStep)/2)));
+ status.velocity = status.velocity.add(linearAcceleration.multiply(timeStep));
+ status.time += timeStep;
+
+
+ // Store data
+ FlightDataBranch data = status.flightData;
+ boolean extra = status.startConditions.getCalculateExtras();
+ data.addPoint();
+
+ data.setValue(FlightDataBranch.TYPE_TIME, status.time);
+ data.setValue(FlightDataBranch.TYPE_ALTITUDE, status.position.z);
+ data.setValue(FlightDataBranch.TYPE_POSITION_X, status.position.x);
+ data.setValue(FlightDataBranch.TYPE_POSITION_Y, status.position.y);
+ if (extra) {
+ data.setValue(FlightDataBranch.TYPE_POSITION_XY,
+ MathUtil.hypot(status.position.x, status.position.y));
+ data.setValue(FlightDataBranch.TYPE_POSITION_DIRECTION,
+ Math.atan2(status.position.y, status.position.x));
+
+ data.setValue(FlightDataBranch.TYPE_VELOCITY_XY,
+ MathUtil.hypot(status.velocity.x, status.velocity.y));
+ data.setValue(FlightDataBranch.TYPE_ACCELERATION_XY,
+ MathUtil.hypot(linearAcceleration.x, linearAcceleration.y));
+
+ data.setValue(FlightDataBranch.TYPE_ACCELERATION_TOTAL,linearAcceleration.length());
+
+ double Re = airSpeed.length() *
+ calculator.getConfiguration().getLength() /
+ atmosphere.getKinematicViscosity();
+ data.setValue(FlightDataBranch.TYPE_REYNOLDS_NUMBER, Re);
+ }
+
+ data.setValue(FlightDataBranch.TYPE_VELOCITY_Z, status.velocity.z);
+ data.setValue(FlightDataBranch.TYPE_ACCELERATION_Z, linearAcceleration.z);
+
+ data.setValue(FlightDataBranch.TYPE_VELOCITY_TOTAL, airSpeed.length());
+ data.setValue(FlightDataBranch.TYPE_MACH_NUMBER, mach);
+
+ data.setValue(FlightDataBranch.TYPE_MASS, mass);
+
+ data.setValue(FlightDataBranch.TYPE_THRUST_FORCE, 0);
+ data.setValue(FlightDataBranch.TYPE_DRAG_FORCE, dragForce);
+
+ data.setValue(FlightDataBranch.TYPE_WIND_VELOCITY, windSpeed);
+ data.setValue(FlightDataBranch.TYPE_AIR_TEMPERATURE, atmosphere.temperature);
+ data.setValue(FlightDataBranch.TYPE_AIR_PRESSURE, atmosphere.pressure);
+ data.setValue(FlightDataBranch.TYPE_SPEED_OF_SOUND, atmosphere.getMachSpeed());
+
+ data.setValue(FlightDataBranch.TYPE_TIME_STEP, timeStep);
+ if (status.simulationStartTime != Long.MIN_VALUE)
+ data.setValue(FlightDataBranch.TYPE_COMPUTATION_TIME,
+ (System.nanoTime() - status.simulationStartTime)/1000000000.0);
+ }
+
+
+
+
+ /**
+ * Add events that listeners have returned, and add a Warning to the
+ * simulation if necessary.
+ *
+ * @param events a collection of the events, or <code>null</code>.
+ */
+ protected final void addListenerEvents(Collection<FlightEvent> events) {
+ if (events == null)
+ return;
+ for (FlightEvent e: events) {
+ if (e != null && e.getTime() < 1000000) {
+ warnings.add(Warning.LISTENERS_AFFECTED);
+ eventQueue.add(e);
+ }
+ }
+ }
+
+
+
+ /**
+ * Calculate the average thrust produced by the motors in the current configuration.
+ * The average is taken between <code>status.time</code> and
+ * <code>status.time + timestep</code>.
+ * <p>
+ * Note: Using this method does not take into account any moments generated by
+ * off-center motors.
+ *
+ * @param status the current simulation status.
+ * @param timestep the time step of the current iteration.
+ * @return the average thrust during the time step.
+ */
+ protected double calculateThrust(SimulationStatus status, double timestep) {
+ double thrust = 0;
+ Iterator<MotorMount> iterator = status.configuration.motorIterator();
+
+ while (iterator.hasNext()) {
+ MotorMount mount = iterator.next();
+
+ // Count the number of motors in a cluster
+ int count = 1;
+ for (RocketComponent c = (RocketComponent)mount; c != null; c = c.getParent()) {
+ if (c instanceof Clusterable)
+ count *= ((Clusterable)c).getClusterConfiguration().getClusterCount();
+ }
+
+ Motor motor = mount.getMotor(status.configuration.getMotorConfigurationID());
+ double ignitionTime = status.configuration.getIgnitionTime(mount);
+ double time = status.time - ignitionTime;
+ thrust += count * motor.getThrust(time, time + timestep);
+ // TODO: MEDIUM: Moment generated by motors
+ }
+
+ return thrust;
+ }
+
+
+
+ /**
+ * Initialize a new {@link SimulationStatus} object for simulation using this simulator.
+ *
+ * @param configuration the starting configuration of the rocket.
+ * @param simulation the simulation conditions.
+ * @return a {@link SimulationStatus} object for the simulation.
+ */
+ protected abstract SimulationStatus initializeSimulation(Configuration configuration,
+ SimulationConditions simulation);
+
+ /**
+ * Make a time step. The current status of the simulation is stored in the
+ * variable <code>status</code> and must be updated by this call.
+ *
+ * @param simulation the simulation conditions.
+ * @param status the current simulation status, received originally from
+ * {@link #initializeSimulation(Configuration, SimulationConditions)}
+ * @return a collection of flight events to handle, or null for none.
+ */
+
+
+ protected abstract Collection<FlightEvent> step(SimulationConditions simulation,
+ SimulationStatus status) throws SimulationException;
+
+
+
+ public void addSimulationListener(SimulationListener l) {
+ listeners.add(l);
+ }
+ public void removeSimulationListener(SimulationListener l) {
+ listeners.remove(l);
+ }
+ public void resetSimulationListeners() {
+ listeners.clear();
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.simulation;
+
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.Quaternion;
+
+public class RK4SimulationStatus extends SimulationStatus {
+ public Quaternion orientation;
+ public Coordinate rotation;
+
+ public Coordinate launchRodDirection;
+
+
+ /**
+ * Provides a copy of the simulation status. The orientation quaternion is
+ * cloned as well, so changing it does not affect other simulation status objects.
+ */
+ @Override
+ public RK4SimulationStatus clone() {
+ RK4SimulationStatus copy = (RK4SimulationStatus) super.clone();
+ copy.orientation = this.orientation.clone();
+ return copy;
+ }
+}
--- /dev/null
+package net.sf.openrocket.simulation;
+
+import java.util.Collection;
+
+import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.AtmosphericConditions;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.aerodynamics.GravityModel;
+import net.sf.openrocket.aerodynamics.Warning;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.aerodynamics.WindSimulator;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.simulation.exception.SimulationException;
+import net.sf.openrocket.util.Coordinate;
+import net.sf.openrocket.util.MathUtil;
+import net.sf.openrocket.util.Quaternion;
+import net.sf.openrocket.util.Rotation2D;
+
+
+public class RK4Simulator extends FlightSimulator {
+
+ /**
+ * A recommended reasonably accurate time step.
+ */
+ public static final double RECOMMENDED_TIME_STEP = 0.05;
+
+ /**
+ * A recommended maximum angle step value.
+ */
+ public static final double RECOMMENDED_ANGLE_STEP = 3*Math.PI/180;
+
+ /**
+ * Maximum roll step allowed. This is selected as an uneven division of the full
+ * circle so that the simulation will sample the most wind directions
+ */
+ private static final double MAX_ROLL_STEP_ANGLE = 28.32 * Math.PI/180;
+// private static final double MAX_ROLL_STEP_ANGLE = 8.32 * Math.PI/180;
+
+ private static final double MAX_ROLL_RATE_CHANGE = 2 * Math.PI/180;
+ private static final double MAX_PITCH_CHANGE = 2 * Math.PI/180;
+
+
+ private static final boolean DEBUG = false;
+
+
+ /* Single instance so it doesn't have to be created each semi-step. */
+ private final FlightConditions flightConditions = new FlightConditions(null);
+
+
+ private Coordinate linearAcceleration;
+ private Coordinate angularAcceleration;
+
+ // set by calculateFlightConditions and calculateAcceleration:
+ private double timestep;
+ private double oldTimestep;
+ private AerodynamicForces forces;
+ private double windSpeed;
+ private double thrustForce, dragForce;
+ private double lateralPitchRate = 0;
+
+ private double rollAcceleration = 0;
+ private double lateralPitchAcceleration = 0;
+
+ private double maxVelocityZ = 0;
+ private double startWarningTime = -1;
+
+ private Rotation2D thetaRotation;
+
+
+ public RK4Simulator() {
+ super();
+ }
+
+
+ public RK4Simulator(AerodynamicCalculator calculator) {
+ super(calculator);
+ }
+
+
+
+
+
+ @Override
+ protected RK4SimulationStatus initializeSimulation(Configuration configuration,
+ SimulationConditions simulation) {
+
+ RK4SimulationStatus status = new RK4SimulationStatus();
+
+ status.startConditions = simulation;
+
+ status.configuration = configuration;
+ // TODO: LOW: Branch names
+ status.flightData = new FlightDataBranch("Main", FlightDataBranch.TYPE_TIME);
+ status.launchRod = true;
+ status.time = 0.0;
+ status.simulationStartTime = System.nanoTime();
+
+ status.launchRodDirection = new Coordinate(
+ Math.sin(simulation.getLaunchRodAngle()) *
+ Math.cos(simulation.getLaunchRodDirection()),
+ Math.sin(simulation.getLaunchRodAngle()) *
+ Math.sin(simulation.getLaunchRodDirection()),
+ Math.cos(simulation.getLaunchRodAngle())
+ );
+ status.launchRodLength = simulation.getLaunchRodLength();
+
+ // Take into account launch lug positions
+ double lugPosition = Double.NaN;
+ for (RocketComponent c: configuration) {
+ if (c instanceof LaunchLug) {
+ double pos = c.toAbsolute(new Coordinate(c.getLength()))[0].x;
+ if (Double.isNaN(lugPosition) || pos > lugPosition) {
+ lugPosition = pos;
+ }
+ }
+ }
+ if (!Double.isNaN(lugPosition)) {
+ double maxX = 0;
+ for (Coordinate c: configuration.getBounds()) {
+ if (c.x > maxX)
+ maxX = c.x;
+ }
+ if (maxX >= lugPosition) {
+ status.launchRodLength = Math.max(0,
+ status.launchRodLength - (maxX - lugPosition));
+ }
+ }
+
+
+ Quaternion o = new Quaternion();
+ o.multiplyLeft(Quaternion.rotation(
+ new Coordinate(0, simulation.getLaunchRodAngle(), 0)));
+ o.multiplyLeft(Quaternion.rotation(
+ new Coordinate(0, 0, simulation.getLaunchRodDirection())));
+ status.orientation = o;
+ status.position = Coordinate.NUL;
+ status.velocity = Coordinate.NUL;
+ status.rotation = Coordinate.NUL;
+
+ /*
+ * Force a very small deviation to the wind speed to avoid insanely
+ * perfect conditions (rocket dropping at exactly 180 deg AOA).
+ */
+ status.windSimulator = new WindSimulator();
+ status.windSimulator.setAverage(simulation.getWindSpeedAverage());
+ status.windSimulator.setStandardDeviation(
+ Math.max(simulation.getWindSpeedDeviation(), 0.005));
+// status.windSimulator.reset();
+
+ status.gravityModel = new GravityModel(simulation.getLaunchLatitude());
+
+ rollAcceleration = 0;
+ lateralPitchAcceleration = 0;
+ oldTimestep = -1;
+ maxVelocityZ = 0;
+ startWarningTime = -1;
+
+ return status;
+ }
+
+
+
+ @Override
+ protected Collection<FlightEvent> step(SimulationConditions simulation,
+ SimulationStatus simulationStatus) throws SimulationException {
+
+ RK4SimulationStatus status = (RK4SimulationStatus)simulationStatus;
+
+ //////// Perform RK4 integration: ////////
+
+ Coordinate k1a, k1v, k1ra, k1rv; // Acceleration, velocity, rot.acc, rot.vel
+ Coordinate k2a, k2v, k2ra, k2rv;
+ Coordinate k3a, k3v, k3ra, k3rv;
+ Coordinate k4a, k4v, k4ra, k4rv;
+ RK4SimulationStatus status2;
+
+
+ // Calculate time step and store data after first call to calculateFlightConditions
+ calculateFlightConditions(status);
+
+
+ /*
+ * Select the time step to use. It is the minimum of the following:
+ * 1. the user-specified time step
+ * 2. the maximum pitch step angle limit
+ * 3. the maximum roll step angle limit
+ * 4. the maximum roll rate change limit (using previous step acceleration)
+ * 5. the maximum pitch change limit (using previous step acceleration)
+ *
+ * The last two are required since near the steady-state roll rate the roll rate
+ * may oscillate significantly even between the sub-steps of the RK4 integration.
+ *
+ * Additionally a low-pass filter is applied to the time step selectively
+ * if the new time step is longer than the previous time step.
+ */
+ double dt1 = simulation.getTimeStep();
+ double dt2 = simulation.getMaximumStepAngle() / lateralPitchRate;
+ double dt3 = Math.abs(MAX_ROLL_STEP_ANGLE / flightConditions.getRollRate());
+ double dt4 = Math.abs(MAX_ROLL_RATE_CHANGE / rollAcceleration);
+ double dt5 = Math.abs(MAX_PITCH_CHANGE / lateralPitchAcceleration);
+ timestep = MathUtil.min(dt1,dt2,dt3);
+ timestep = MathUtil.min(timestep,dt4,dt5);
+
+ if (oldTimestep > 0 && oldTimestep < timestep) {
+ timestep = 0.3*timestep + 0.7*oldTimestep;
+ }
+
+ if (timestep < 0.001)
+ timestep = 0.001;
+
+ oldTimestep = timestep;
+ if (DEBUG)
+ System.out.printf("Time step: %.3f dt1=%.3f dt2=%.3f dt3=%.3f dt4=%.3f dt5=%.3f\n",
+ timestep,dt1,dt2,dt3,dt4,dt5);
+
+
+
+ //// First position, k1 = f(t, y)
+
+ //calculateFlightConditions already called
+ calculateAcceleration(status);
+ k1a = linearAcceleration;
+ k1ra = angularAcceleration;
+ k1v = status.velocity;
+ k1rv = status.rotation;
+
+
+ // Store the flight information
+ storeData(status);
+
+
+
+ //// Second position, k2 = f(t + h/2, y + k1*h/2)
+
+ status2 = status.clone();
+ status2.time = status.time + timestep/2;
+ status2.position = status.position.add(k1v.multiply(timestep/2));
+ status2.velocity = status.velocity.add(k1a.multiply(timestep/2));
+ status2.orientation.multiplyLeft(Quaternion.rotation(k1rv.multiply(timestep/2)));
+ status2.rotation = status.rotation.add(k1ra.multiply(timestep/2));
+
+ calculateFlightConditions(status2);
+ calculateAcceleration(status2);
+ k2a = linearAcceleration;
+ k2ra = angularAcceleration;
+ k2v = status2.velocity;
+ k2rv = status2.rotation;
+
+
+ //// Third position, k3 = f(t + h/2, y + k2*h/2)
+
+ status2.orientation = status.orientation.clone(); // All others are set explicitly
+ status2.position = status.position.add(k2v.multiply(timestep/2));
+ status2.velocity = status.velocity.add(k2a.multiply(timestep/2));
+ status2.orientation.multiplyLeft(Quaternion.rotation(k2rv.multiply(timestep/2)));
+ status2.rotation = status.rotation.add(k2ra.multiply(timestep/2));
+
+ calculateFlightConditions(status2);
+ calculateAcceleration(status2);
+ k3a = linearAcceleration;
+ k3ra = angularAcceleration;
+ k3v = status2.velocity;
+ k3rv = status2.rotation;
+
+
+
+ //// Fourth position, k4 = f(t + h, y + k3*h)
+
+ status2.orientation = status.orientation.clone(); // All others are set explicitly
+ status2.time = status.time + timestep;
+ status2.position = status.position.add(k3v.multiply(timestep));
+ status2.velocity = status.velocity.add(k3a.multiply(timestep));
+ status2.orientation.multiplyLeft(Quaternion.rotation(k3rv.multiply(timestep)));
+ status2.rotation = status.rotation.add(k3ra.multiply(timestep));
+
+ calculateFlightConditions(status2);
+ calculateAcceleration(status2);
+ k4a = linearAcceleration;
+ k4ra = angularAcceleration;
+ k4v = status2.velocity;
+ k4rv = status2.rotation;
+
+
+
+ //// Sum all together, y(n+1) = y(n) + h*(k1 + 2*k2 + 2*k3 + k4)/6
+
+ Coordinate deltaV, deltaP, deltaR, deltaO;
+ deltaV = k2a.add(k3a).multiply(2).add(k1a).add(k4a).multiply(timestep/6);
+ deltaP = k2v.add(k3v).multiply(2).add(k1v).add(k4v).multiply(timestep/6);
+ deltaR = k2ra.add(k3ra).multiply(2).add(k1ra).add(k4ra).multiply(timestep/6);
+ deltaO = k2rv.add(k3rv).multiply(2).add(k1rv).add(k4rv).multiply(timestep/6);
+
+ if (DEBUG)
+ System.out.println("Rot.Acc: "+deltaR+" k1:"+k1ra+" k2:"+k2ra+" k3:"+k3ra+
+ " k4:"+k4ra);
+
+ status.velocity = status.velocity.add(deltaV);
+ status.position = status.position.add(deltaP);
+ status.rotation = status.rotation.add(deltaR);
+ status.orientation.multiplyLeft(Quaternion.rotation(deltaO));
+
+
+ status.orientation.normalizeIfNecessary();
+
+ status.time = status.time + timestep;
+
+
+ return null;
+ }
+
+
+
+ /**
+ * Calculate the linear and angular acceleration at the given status. The results
+ * are stored in the fields {@link #linearAcceleration} and {@link #angularAcceleration}.
+ *
+ * @param status the status of the rocket.
+ * @throws SimulationException
+ */
+ private void calculateAcceleration(RK4SimulationStatus status) throws SimulationException {
+
+ /**
+ * Check whether to store warnings or not. Warnings are ignored when on the
+ * launch rod or 0.25 seconds after departure, and when the velocity has dropped
+ * below 20% of the max. velocity.
+ */
+ WarningSet warnings = status.warnings;
+ maxVelocityZ = MathUtil.max(maxVelocityZ, status.velocity.z);
+ if (status.launchRod) {
+ warnings = null;
+ } else {
+ if (status.velocity.z < 0.2 * maxVelocityZ)
+ warnings = null;
+ if (startWarningTime < 0)
+ startWarningTime = status.time + 0.25;
+ }
+ if (status.time < startWarningTime)
+ warnings = null;
+
+
+ // Calculate aerodynamic forces (only axial if still on launch rod)
+ calculator.setConfiguration(status.configuration);
+
+ if (status.launchRod) {
+ forces = calculator.getAxialForces(status.time, flightConditions, warnings);
+ } else {
+ forces = calculator.getAerodynamicForces(status.time, flightConditions, warnings);
+ }
+
+
+ // Allow listeners to modify the forces
+ int mod = flightConditions.getModCount();
+ SimulationListener[] list = listeners.toArray(new SimulationListener[0]);
+ for (SimulationListener l: list) {
+ l.forceCalculation(status, flightConditions, forces);
+ }
+ if (flightConditions.getModCount() != mod) {
+ status.warnings.add(Warning.LISTENERS_AFFECTED);
+ }
+
+
+ assert(!Double.isNaN(forces.CD));
+ assert(!Double.isNaN(forces.CN));
+ assert(!Double.isNaN(forces.Caxial));
+ assert(!Double.isNaN(forces.Cm));
+ assert(!Double.isNaN(forces.Cyaw));
+ assert(!Double.isNaN(forces.Cside));
+ assert(!Double.isNaN(forces.Croll));
+
+
+ //////// Calculate forces and accelerations ////////
+
+ double dynP = (0.5 * flightConditions.getAtmosphericConditions().getDensity() *
+ MathUtil.pow2(flightConditions.getVelocity()));
+ double refArea = flightConditions.getRefArea();
+ double refLength = flightConditions.getRefLength();
+
+
+ // Linear forces
+ thrustForce = calculateThrust(status, timestep);
+ dragForce = forces.Caxial * dynP * refArea;
+ double fN = forces.CN * dynP * refArea;
+ double fSide = forces.Cside * dynP * refArea;
+
+// double sin = Math.sin(flightConditions.getTheta());
+// double cos = Math.cos(flightConditions.getTheta());
+
+// double forceX = - fN * cos - fSide * sin;
+// double forceY = - fN * sin - fSide * cos;
+ double forceZ = thrustForce - dragForce;
+
+
+// linearAcceleration = new Coordinate(forceX / forces.cg.weight,
+// forceY / forces.cg.weight, forceZ / forces.cg.weight);
+ linearAcceleration = new Coordinate(-fN / forces.cg.weight, -fSide / forces.cg.weight,
+ forceZ / forces.cg.weight);
+
+ linearAcceleration = thetaRotation.rotateZ(linearAcceleration);
+ linearAcceleration = status.orientation.rotate(linearAcceleration);
+
+ linearAcceleration = linearAcceleration.sub(0, 0, status.gravityModel.getGravity());
+
+
+ // If still on launch rod, project acceleration onto launch rod direction and
+ // set angular acceleration to zero.
+ if (status.launchRod) {
+ linearAcceleration = status.launchRodDirection.multiply(
+ linearAcceleration.dot(status.launchRodDirection));
+ angularAcceleration = Coordinate.NUL;
+ rollAcceleration = 0;
+ lateralPitchAcceleration = 0;
+ return;
+ }
+
+
+ // Convert momenta
+ double Cm = forces.Cm - forces.CN * forces.cg.x / refLength;
+ double Cyaw = forces.Cyaw - forces.Cside * forces.cg.x / refLength;
+
+// double momX = (-Cm * sin - Cyaw * cos) * dynP * refArea * refLength;
+// double momY = ( Cm * cos - Cyaw * sin) * dynP * refArea * refLength;
+ double momX = -Cyaw * dynP * refArea * refLength;
+ double momY = Cm * dynP * refArea * refLength;
+
+ double momZ = forces.Croll * dynP * refArea * refLength;
+ if (DEBUG)
+ System.out.printf("Croll: %.3f dynP=%.3f momZ=%.3f\n",forces.Croll,dynP,momZ);
+
+ assert(!Double.isNaN(momX));
+ assert(!Double.isNaN(momY));
+ assert(!Double.isNaN(momZ));
+ assert(!Double.isNaN(forces.longitudalInertia));
+ assert(!Double.isNaN(forces.rotationalInertia));
+
+ angularAcceleration = new Coordinate(momX / forces.longitudalInertia,
+ momY / forces.longitudalInertia, momZ / forces.rotationalInertia);
+
+ rollAcceleration = angularAcceleration.z;
+ // TODO: LOW: This should be hypot, but does it matter?
+ lateralPitchAcceleration = MathUtil.max(Math.abs(angularAcceleration.x),
+ Math.abs(angularAcceleration.y));
+
+ if (DEBUG)
+ System.out.println("rot.inertia = "+forces.rotationalInertia);
+
+ angularAcceleration = thetaRotation.rotateZ(angularAcceleration);
+
+ angularAcceleration = status.orientation.rotate(angularAcceleration);
+ }
+
+
+ /**
+ * Calculate the flight conditions for the current rocket status. The conditions
+ * are stored in the field {@link #flightConditions}. Additional information that
+ * is calculated and will be stored in the flight data is also computed into the
+ * suitable fields.
+ * @throws SimulationException
+ */
+ private void calculateFlightConditions(RK4SimulationStatus status) throws SimulationException {
+
+ flightConditions.setReference(status.configuration);
+
+
+ //// Atmospheric conditions
+ AtmosphericConditions atmosphere = status.startConditions.getAtmosphericModel().
+ getConditions(status.position.z + status.startConditions.getLaunchAltitude());
+ flightConditions.setAtmosphericConditions(atmosphere);
+
+
+ //// Local wind speed and direction
+ windSpeed = status.windSimulator.getWindSpeed(status.time);
+ Coordinate airSpeed = status.velocity.add(windSpeed, 0, 0);
+ airSpeed = status.orientation.invRotate(airSpeed);
+
+
+ // Lateral direction:
+ double len = MathUtil.hypot(airSpeed.x, airSpeed.y);
+ if (len > 0.0001) {
+ thetaRotation = new Rotation2D(airSpeed.y/len, airSpeed.x/len);
+ flightConditions.setTheta(Math.atan2(airSpeed.y, airSpeed.x));
+ } else {
+ thetaRotation = Rotation2D.ID;
+ flightConditions.setTheta(0);
+ }
+
+ double velocity = airSpeed.length();
+ flightConditions.setVelocity(velocity);
+ if (velocity > 0.01) {
+ // aoa must be calculated from the monotonous cosine
+ // sine can be calculated by a simple division
+ flightConditions.setAOA(Math.acos(airSpeed.z / velocity), len / velocity);
+ } else {
+ flightConditions.setAOA(0);
+ }
+
+
+ // Roll, pitch and yaw rate
+ Coordinate rot = status.orientation.invRotate(status.rotation);
+ rot = thetaRotation.invRotateZ(rot);
+
+ flightConditions.setRollRate(rot.z);
+ if (len < 0.001) {
+ flightConditions.setPitchRate(0);
+ flightConditions.setYawRate(0);
+ lateralPitchRate = 0;
+ } else {
+ flightConditions.setPitchRate(rot.y);
+ flightConditions.setYawRate(rot.x);
+ // TODO: LOW: set this as power of two?
+ lateralPitchRate = MathUtil.hypot(rot.x, rot.y);
+ }
+
+
+ // Allow listeners to modify the conditions
+ int mod = flightConditions.getModCount();
+ SimulationListener[] list = listeners.toArray(new SimulationListener[0]);
+ for (SimulationListener l: list) {
+ l.flightConditions(status, flightConditions);
+ }
+ if (mod != flightConditions.getModCount()) {
+ // Re-calculate cached values
+ thetaRotation = new Rotation2D(flightConditions.getTheta());
+ lateralPitchRate = MathUtil.hypot(flightConditions.getPitchRate(),
+ flightConditions.getYawRate());
+ status.warnings.add(Warning.LISTENERS_AFFECTED);
+ }
+
+ }
+
+
+
+ private void storeData(RK4SimulationStatus status) {
+ FlightDataBranch data = status.flightData;
+ boolean extra = status.startConditions.getCalculateExtras();
+
+ data.addPoint();
+ data.setValue(FlightDataBranch.TYPE_TIME, status.time);
+ data.setValue(FlightDataBranch.TYPE_ALTITUDE, status.position.z);
+ data.setValue(FlightDataBranch.TYPE_POSITION_X, status.position.x);
+ data.setValue(FlightDataBranch.TYPE_POSITION_Y, status.position.y);
+
+ if (extra) {
+ data.setValue(FlightDataBranch.TYPE_POSITION_XY,
+ MathUtil.hypot(status.position.x, status.position.y));
+ data.setValue(FlightDataBranch.TYPE_POSITION_DIRECTION,
+ Math.atan2(status.position.y, status.position.x));
+
+ data.setValue(FlightDataBranch.TYPE_VELOCITY_XY,
+ MathUtil.hypot(status.velocity.x, status.velocity.y));
+ data.setValue(FlightDataBranch.TYPE_ACCELERATION_XY,
+ MathUtil.hypot(linearAcceleration.x, linearAcceleration.y));
+
+ data.setValue(FlightDataBranch.TYPE_ACCELERATION_TOTAL,linearAcceleration.length());
+
+ double Re = flightConditions.getVelocity() *
+ calculator.getConfiguration().getLength() /
+ flightConditions.getAtmosphericConditions().getKinematicViscosity();
+ data.setValue(FlightDataBranch.TYPE_REYNOLDS_NUMBER, Re);
+ }
+
+ data.setValue(FlightDataBranch.TYPE_VELOCITY_Z, status.velocity.z);
+ data.setValue(FlightDataBranch.TYPE_ACCELERATION_Z, linearAcceleration.z);
+
+ data.setValue(FlightDataBranch.TYPE_VELOCITY_TOTAL, flightConditions.getVelocity());
+ data.setValue(FlightDataBranch.TYPE_MACH_NUMBER, flightConditions.getMach());
+
+ if (!status.launchRod) {
+ data.setValue(FlightDataBranch.TYPE_CP_LOCATION, forces.cp.x);
+ data.setValue(FlightDataBranch.TYPE_CG_LOCATION, forces.cg.x);
+ data.setValue(FlightDataBranch.TYPE_STABILITY,
+ (forces.cp.x - forces.cg.x) / flightConditions.getRefLength());
+ }
+ data.setValue(FlightDataBranch.TYPE_MASS, forces.cg.weight);
+
+ data.setValue(FlightDataBranch.TYPE_THRUST_FORCE, thrustForce);
+ data.setValue(FlightDataBranch.TYPE_DRAG_FORCE, dragForce);
+
+ if (!status.launchRod) {
+ data.setValue(FlightDataBranch.TYPE_PITCH_MOMENT_COEFF,
+ forces.Cm - forces.CN * forces.cg.x / flightConditions.getRefLength());
+ data.setValue(FlightDataBranch.TYPE_YAW_MOMENT_COEFF,
+ forces.Cyaw - forces.Cside * forces.cg.x / flightConditions.getRefLength());
+ data.setValue(FlightDataBranch.TYPE_NORMAL_FORCE_COEFF, forces.CN);
+ data.setValue(FlightDataBranch.TYPE_SIDE_FORCE_COEFF, forces.Cside);
+ data.setValue(FlightDataBranch.TYPE_ROLL_MOMENT_COEFF, forces.Croll);
+ data.setValue(FlightDataBranch.TYPE_ROLL_FORCING_COEFF, forces.CrollForce);
+ data.setValue(FlightDataBranch.TYPE_ROLL_DAMPING_COEFF, forces.CrollDamp);
+ data.setValue(FlightDataBranch.TYPE_PITCH_DAMPING_MOMENT_COEFF,
+ forces.pitchDampingMoment);
+ }
+
+ data.setValue(FlightDataBranch.TYPE_DRAG_COEFF, forces.CD);
+ data.setValue(FlightDataBranch.TYPE_AXIAL_DRAG_COEFF, forces.Caxial);
+ data.setValue(FlightDataBranch.TYPE_FRICTION_DRAG_COEFF, forces.frictionCD);
+ data.setValue(FlightDataBranch.TYPE_PRESSURE_DRAG_COEFF, forces.pressureCD);
+ data.setValue(FlightDataBranch.TYPE_BASE_DRAG_COEFF, forces.baseCD);
+
+ data.setValue(FlightDataBranch.TYPE_REFERENCE_LENGTH, flightConditions.getRefLength());
+ data.setValue(FlightDataBranch.TYPE_REFERENCE_AREA, flightConditions.getRefArea());
+
+
+ data.setValue(FlightDataBranch.TYPE_PITCH_RATE, flightConditions.getPitchRate());
+ data.setValue(FlightDataBranch.TYPE_YAW_RATE, flightConditions.getYawRate());
+
+
+
+ if (extra) {
+ Coordinate c = status.orientation.rotateZ();
+ double theta = Math.atan2(c.z, MathUtil.hypot(c.x, c.y));
+ double phi = Math.atan2(c.y, c.x);
+ if (phi < -(Math.PI-0.0001))
+ phi = Math.PI;
+ data.setValue(FlightDataBranch.TYPE_ORIENTATION_THETA, theta);
+ data.setValue(FlightDataBranch.TYPE_ORIENTATION_PHI, phi);
+ }
+
+ data.setValue(FlightDataBranch.TYPE_AOA, flightConditions.getAOA());
+ data.setValue(FlightDataBranch.TYPE_ROLL_RATE, flightConditions.getRollRate());
+
+ data.setValue(FlightDataBranch.TYPE_WIND_VELOCITY, windSpeed);
+ data.setValue(FlightDataBranch.TYPE_AIR_TEMPERATURE,
+ flightConditions.getAtmosphericConditions().temperature);
+ data.setValue(FlightDataBranch.TYPE_AIR_PRESSURE,
+ flightConditions.getAtmosphericConditions().pressure);
+ data.setValue(FlightDataBranch.TYPE_SPEED_OF_SOUND,
+ flightConditions.getAtmosphericConditions().getMachSpeed());
+
+
+ data.setValue(FlightDataBranch.TYPE_TIME_STEP, timestep);
+ data.setValue(FlightDataBranch.TYPE_COMPUTATION_TIME,
+ (System.nanoTime() - status.simulationStartTime)/1000000000.0);
+
+
+// data.setValue(FlightDataBranch.TYPE_, 0);
+
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.simulation;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.aerodynamics.AtmosphericModel;
+import net.sf.openrocket.aerodynamics.ExtendedISAModel;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.util.ChangeSource;
+import net.sf.openrocket.util.MathUtil;
+
+
+public class SimulationConditions implements ChangeSource, Cloneable {
+
+ public static final double MAX_LAUNCH_ROD_ANGLE = Math.PI/3;
+
+ /**
+ * The ISA standard atmosphere.
+ */
+ private static final AtmosphericModel ISA_ATMOSPHERIC_MODEL = new ExtendedISAModel();
+
+
+ private final Rocket rocket;
+ private String motorID = null;
+
+
+ /*
+ * NOTE: When adding/modifying parameters, they must also be added to the
+ * equals and copyFrom methods!!
+ */
+
+ // TODO: HIGH: Fetch default values from Prefs!
+
+ private double launchRodLength = 1;
+
+ /** Launch rod angle > 0, radians from vertical */
+ private double launchRodAngle = 0;
+
+ /** Launch rod direction, 0 = upwind, PI = downwind. */
+ private double launchRodDirection = 0;
+
+
+ private double windAverage = 2.0;
+ private double windTurbulence = 0.1;
+
+ private double launchAltitude = 0;
+ private double launchLatitude = 45;
+
+ private boolean useISA = true;
+ private double launchTemperature = ExtendedISAModel.STANDARD_TEMPERATURE;
+ private double launchPressure = ExtendedISAModel.STANDARD_PRESSURE;
+ private AtmosphericModel atmosphericModel = null;
+
+
+ private double timeStep = RK4Simulator.RECOMMENDED_TIME_STEP;
+ private double maximumAngle = RK4Simulator.RECOMMENDED_ANGLE_STEP;
+
+ private boolean calculateExtras = true;
+
+
+ private List<ChangeListener> listeners = new ArrayList<ChangeListener>();
+
+
+
+ public SimulationConditions(Rocket rocket) {
+ this.rocket = rocket;
+ }
+
+
+
+ public Rocket getRocket() {
+ return rocket;
+ }
+
+
+ public String getMotorConfigurationID() {
+ return motorID;
+ }
+
+ public void setMotorConfigurationID(String id) {
+ if (id != null)
+ id = id.intern();
+ if (id == motorID)
+ return;
+ motorID = id;
+ fireChangeEvent();
+ }
+
+
+ public double getLaunchRodLength() {
+ return launchRodLength;
+ }
+
+ public void setLaunchRodLength(double launchRodLength) {
+ if (MathUtil.equals(this.launchRodLength, launchRodLength))
+ return;
+ this.launchRodLength = launchRodLength;
+ fireChangeEvent();
+ }
+
+
+ public double getLaunchRodAngle() {
+ return launchRodAngle;
+ }
+
+ public void setLaunchRodAngle(double launchRodAngle) {
+ launchRodAngle = MathUtil.clamp(launchRodAngle, 0, MAX_LAUNCH_ROD_ANGLE);
+ if (MathUtil.equals(this.launchRodAngle, launchRodAngle))
+ return;
+ this.launchRodAngle = launchRodAngle;
+ fireChangeEvent();
+ }
+
+
+ public double getLaunchRodDirection() {
+ return launchRodDirection;
+ }
+
+ public void setLaunchRodDirection(double launchRodDirection) {
+ launchRodDirection = MathUtil.reduce180(launchRodDirection);
+ if (MathUtil.equals(this.launchRodDirection, launchRodDirection))
+ return;
+ this.launchRodDirection = launchRodDirection;
+ fireChangeEvent();
+ }
+
+
+
+ public double getWindSpeedAverage() {
+ return windAverage;
+ }
+
+ public void setWindSpeedAverage(double windAverage) {
+ if (MathUtil.equals(this.windAverage, windAverage))
+ return;
+ this.windAverage = MathUtil.max(windAverage, 0);
+ fireChangeEvent();
+ }
+
+
+ public double getWindSpeedDeviation() {
+ return windAverage * windTurbulence;
+ }
+
+ public void setWindSpeedDeviation(double windDeviation) {
+ if (windAverage < 0.1) {
+ windAverage = 0.1;
+ }
+ setWindTurbulenceIntensity(windDeviation / windAverage);
+ }
+
+
+ /**
+ * Return the wind turbulence intensity (standard deviation / average).
+ *
+ * @return the turbulence intensity
+ */
+ public double getWindTurbulenceIntensity() {
+ return windTurbulence;
+ }
+
+ /**
+ * Set the wind standard deviation to match the given turbulence intensity.
+ *
+ * @param intensity the turbulence intensity
+ */
+ public void setWindTurbulenceIntensity(double intensity) {
+ // Does not check equality so that setWindSpeedDeviation can be sure of event firing
+ this.windTurbulence = intensity;
+ fireChangeEvent();
+ }
+
+
+
+
+
+ public double getLaunchAltitude() {
+ return launchAltitude;
+ }
+
+ public void setLaunchAltitude(double altitude) {
+ if (MathUtil.equals(this.launchAltitude, altitude))
+ return;
+ this.launchAltitude = altitude;
+ fireChangeEvent();
+ }
+
+
+ public double getLaunchLatitude() {
+ return launchLatitude;
+ }
+
+ public void setLaunchLatitude(double launchLatitude) {
+ launchLatitude = MathUtil.clamp(launchLatitude, -90, 90);
+ if (MathUtil.equals(this.launchLatitude, launchLatitude))
+ return;
+ this.launchLatitude = launchLatitude;
+ fireChangeEvent();
+ }
+
+
+
+
+
+ public boolean isISAAtmosphere() {
+ return useISA;
+ }
+
+ public void setISAAtmosphere(boolean isa) {
+ if (isa == useISA)
+ return;
+ useISA = isa;
+ fireChangeEvent();
+ }
+
+
+ public double getLaunchTemperature() {
+ return launchTemperature;
+ }
+
+
+
+ public void setLaunchTemperature(double launchTemperature) {
+ if (MathUtil.equals(this.launchTemperature, launchTemperature))
+ return;
+ this.launchTemperature = launchTemperature;
+ this.atmosphericModel = null;
+ fireChangeEvent();
+ }
+
+
+
+ public double getLaunchPressure() {
+ return launchPressure;
+ }
+
+
+
+ public void setLaunchPressure(double launchPressure) {
+ if (MathUtil.equals(this.launchPressure, launchPressure))
+ return;
+ this.launchPressure = launchPressure;
+ this.atmosphericModel = null;
+ fireChangeEvent();
+ }
+
+
+ /**
+ * Returns an atmospheric model corresponding to the launch conditions. The
+ * atmospheric models may be shared between different calls.
+ *
+ * @return an AtmosphericModel object.
+ */
+ public AtmosphericModel getAtmosphericModel() {
+ if (useISA) {
+ return ISA_ATMOSPHERIC_MODEL;
+ }
+ if (atmosphericModel == null) {
+ atmosphericModel = new ExtendedISAModel(launchAltitude,
+ launchTemperature, launchPressure);
+ }
+ return atmosphericModel;
+ }
+
+
+ public double getTimeStep() {
+ return timeStep;
+ }
+
+ public void setTimeStep(double timeStep) {
+ if (MathUtil.equals(this.timeStep, timeStep))
+ return;
+ this.timeStep = timeStep;
+ fireChangeEvent();
+ }
+
+ public double getMaximumStepAngle() {
+ return maximumAngle;
+ }
+
+ public void setMaximumStepAngle(double maximumAngle) {
+ maximumAngle = MathUtil.clamp(maximumAngle, 1*Math.PI/180, 20*Math.PI/180);
+ if (MathUtil.equals(this.maximumAngle, maximumAngle))
+ return;
+ this.maximumAngle = maximumAngle;
+ fireChangeEvent();
+ }
+
+
+
+ public boolean getCalculateExtras() {
+ return calculateExtras;
+ }
+
+
+
+ public void setCalculateExtras(boolean calculateExtras) {
+ if (this.calculateExtras == calculateExtras)
+ return;
+ this.calculateExtras = calculateExtras;
+ fireChangeEvent();
+ }
+
+
+
+ @Override
+ public SimulationConditions clone() {
+ try {
+ SimulationConditions copy = (SimulationConditions)super.clone();
+ copy.listeners = new ArrayList<ChangeListener>();
+ return copy;
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+
+ public void copyFrom(SimulationConditions src) {
+ if (this.rocket != src.rocket) {
+ throw new IllegalArgumentException("Unable to copy simulation conditions of "+
+ "a difference rocket");
+ }
+ if (this.equals(src))
+ return;
+
+ this.motorID = src.motorID;
+ this.launchAltitude = src.launchAltitude;
+ this.launchLatitude = src.launchLatitude;
+ this.launchPressure = src.launchPressure;
+ this.launchRodAngle = src.launchRodAngle;
+ this.launchRodDirection = src.launchRodDirection;
+ this.launchRodLength = src.launchRodLength;
+ this.launchTemperature = src.launchTemperature;
+ this.maximumAngle = src.maximumAngle;
+ this.timeStep = src.timeStep;
+ this.windAverage = src.windAverage;
+ this.windTurbulence = src.windTurbulence;
+ this.calculateExtras = src.calculateExtras;
+
+ fireChangeEvent();
+ }
+
+
+
+ /**
+ * Compares whether the two simulation conditions are equal. The two are considered
+ * equal if the rocket, motor id and all variables are equal.
+ */
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof SimulationConditions))
+ return false;
+ SimulationConditions o = (SimulationConditions)other;
+ return ((this.rocket == o.rocket) &&
+ this.motorID == o.motorID &&
+ MathUtil.equals(this.launchAltitude, o.launchAltitude) &&
+ MathUtil.equals(this.launchLatitude, o.launchLatitude) &&
+ MathUtil.equals(this.launchPressure, o.launchPressure) &&
+ MathUtil.equals(this.launchRodAngle, o.launchRodAngle) &&
+ MathUtil.equals(this.launchRodDirection, o.launchRodDirection) &&
+ MathUtil.equals(this.launchRodLength, o.launchRodLength) &&
+ MathUtil.equals(this.launchTemperature, o.launchTemperature) &&
+ MathUtil.equals(this.maximumAngle, o.maximumAngle) &&
+ MathUtil.equals(this.timeStep, o.timeStep) &&
+ MathUtil.equals(this.windAverage, o.windAverage) &&
+ MathUtil.equals(this.windTurbulence, o.windTurbulence) &&
+ this.calculateExtras == o.calculateExtras);
+ }
+
+ /**
+ * Hashcode method compatible with {@link #equals(Object)}.
+ */
+ @Override
+ public int hashCode() {
+ if (motorID == null)
+ return rocket.hashCode();
+ return rocket.hashCode() + motorID.hashCode();
+ }
+
+ @Override
+ public void addChangeListener(ChangeListener listener) {
+ listeners.add(listener);
+ }
+
+ @Override
+ public void removeChangeListener(ChangeListener listener) {
+ listeners.remove(listener);
+ }
+
+ private final ChangeEvent event = new ChangeEvent(this);
+ private void fireChangeEvent() {
+ ChangeListener[] array = listeners.toArray(new ChangeListener[0]);
+
+ for (int i=array.length-1; i >=0; i--) {
+ array[i].stateChanged(event);
+ }
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.simulation;
+
+import java.util.Collection;
+
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.simulation.exception.SimulationException;
+
+
+
+public interface SimulationListener {
+
+
+ public void flightConditions(SimulationStatus status, FlightConditions conditions)
+ throws SimulationException;
+
+
+ public void forceCalculation(SimulationStatus status, FlightConditions conditions,
+ AerodynamicForces forces) throws SimulationException;
+
+
+ /**
+ * Called every time a simulation step has been taken. The parameter contains the
+ * simulation status. This method may abort the simulation by returning a
+ * <code>SIMULATION_END</code> event. Note that this event and all others within
+ * the current time are still handled, so be careful not to create an infinite loop
+ * of events.
+ *
+ * @param status the current flight status.
+ * @return new flight events to handle, or <code>null</code> for none.
+ */
+ public Collection<FlightEvent> stepTaken(SimulationStatus status)
+ throws SimulationException;
+
+
+ /**
+ * Called every time an event is handled by the simulation system. The parameters
+ * contain the event and current simulation status. This method may abort the
+ * simulation by returning a <code>SIMULATION_END</code> event. Note that this
+ * event and all others within the current time are still handled, so be careful
+ * not to create an infinite loop of events.
+ *
+ * @param event the event that triggered this call.
+ * @param status the current flight status.
+ * @return new flight events to handle, or <code>null</code> for none.
+ */
+ public Collection<FlightEvent> handleEvent(FlightEvent event, SimulationStatus status)
+ throws SimulationException;
+
+}
--- /dev/null
+package net.sf.openrocket.simulation;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import net.sf.openrocket.aerodynamics.GravityModel;
+import net.sf.openrocket.aerodynamics.WarningSet;
+import net.sf.openrocket.aerodynamics.WindSimulator;
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.RecoveryDevice;
+import net.sf.openrocket.util.Coordinate;
+
+
+public class SimulationStatus implements Cloneable {
+
+ public SimulationConditions startConditions;
+
+ public double time;
+ public Configuration configuration;
+ public FlightDataBranch flightData;
+
+ public Coordinate position;
+ public Coordinate velocity;
+
+ public WindSimulator windSimulator;
+ public GravityModel gravityModel;
+
+ public double launchRodLength;
+
+
+ /** Nanosecond time when the simulation was started. */
+ public long simulationStartTime = Long.MIN_VALUE;
+
+
+ /** Set to true when a motor has ignited. */
+ public boolean motorIgnited = false;
+
+ /** Set to true when the rocket has risen from the ground. */
+ public boolean liftoff = false;
+
+ /** <code>true</code> while the rocket is on the launch rod. */
+ public boolean launchRod = true;
+
+ /** Set to true when apogee has been detected. */
+ public boolean apogeeReached = false;
+
+ /** Contains a list of deployed recovery devices. */
+ public final Set<RecoveryDevice> deployedRecoveryDevices = new HashSet<RecoveryDevice>();
+
+
+ public WarningSet warnings;
+
+
+ /** Available for special purposes by the listeners. */
+ public Object extra = null;
+
+
+ /**
+ * Returns a (shallow) copy of this object. The general purpose is that the
+ * conditions, flight data etc. point to the same objects. However, subclasses
+ * are allowed to deep-clone specific objects, such as those pertaining to the
+ * current orientation of the rocket.
+ */
+ @Override
+ public SimulationStatus clone() {
+ try {
+ return (SimulationStatus) super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException("BUG: CloneNotSupportedException?!?",e);
+ }
+ }
+}
--- /dev/null
+package net.sf.openrocket.simulation.exception;
+
+
+/**
+ * An exception signifying that a simulation was cancelled.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class SimulationCancelledException extends SimulationException {
+
+ public SimulationCancelledException() {
+
+ }
+
+ public SimulationCancelledException(String message) {
+ super(message);
+ }
+
+ public SimulationCancelledException(Throwable cause) {
+ super(cause);
+ }
+
+ public SimulationCancelledException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.simulation.exception;
+
+public class SimulationException extends Exception {
+
+ public SimulationException() {
+
+ }
+
+ public SimulationException(String message) {
+ super(message);
+ }
+
+ public SimulationException(Throwable cause) {
+ super(cause);
+ }
+
+ public SimulationException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.simulation.exception;
+
+/**
+ * An exception signifying that a problem occurred at launch, for example
+ * that no motors were defined or no motors ignited.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class SimulationLaunchException extends SimulationException {
+
+ public SimulationLaunchException() {
+
+ }
+
+ public SimulationLaunchException(String message) {
+ super(message);
+ }
+
+ public SimulationLaunchException(Throwable cause) {
+ super(cause);
+ }
+
+ public SimulationLaunchException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.simulation.exception;
+
+
+public class SimulationListenerException extends SimulationException {
+
+ public SimulationListenerException() {
+ }
+
+ public SimulationListenerException(String message) {
+ super(message);
+ }
+
+ public SimulationListenerException(Throwable cause) {
+ super(cause);
+ }
+
+ public SimulationListenerException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.simulation.exception;
+
+
+/**
+ * A exception that signifies that the attempted simulation is not supported.
+ * The reason for not being supported may be due to unsupported combination of
+ * simulator/calculator, unsupported rocket structure or other reasons.
+ * <p>
+ * This exception signifies a fatal problem in the simulation; for non-fatal conditions
+ * add a warning to the simulation results.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class SimulationNotSupportedException extends SimulationException {
+
+ public SimulationNotSupportedException() {
+ }
+
+ public SimulationNotSupportedException(String message) {
+ super(message);
+ }
+
+ public SimulationNotSupportedException(Throwable cause) {
+ super(cause);
+ }
+
+ public SimulationNotSupportedException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
--- /dev/null
+package net.sf.openrocket.simulation.listeners;
+
+import java.util.Collection;
+
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationListener;
+import net.sf.openrocket.simulation.SimulationStatus;
+import net.sf.openrocket.simulation.exception.SimulationException;
+
+
+public abstract class AbstractSimulationListener implements SimulationListener {
+
+ @Override
+ public void flightConditions(SimulationStatus status, FlightConditions conditions)
+ throws SimulationException {
+ // No-op
+ }
+
+ @Override
+ public void forceCalculation(SimulationStatus status, FlightConditions conditions,
+ AerodynamicForces forces) throws SimulationException {
+ // No-op
+ }
+
+ @Override
+ public Collection<FlightEvent> handleEvent(FlightEvent event,
+ SimulationStatus status) throws SimulationException {
+ // No-op
+ return null;
+ }
+
+ @Override
+ public Collection<FlightEvent> stepTaken(SimulationStatus status)
+ throws SimulationException {
+ // No-op
+ return null;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.simulation.listeners;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationStatus;
+import net.sf.openrocket.simulation.exception.SimulationException;
+
+
+/**
+ * A simulation listeners that ends the simulation when apogee is reached.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class ApogeeEndListener extends AbstractSimulationListener {
+
+ public static final ApogeeEndListener INSTANCE = new ApogeeEndListener();
+
+ @Override
+ public Collection<FlightEvent> handleEvent(FlightEvent event,
+ SimulationStatus status) throws SimulationException {
+
+ if (event.getType() == FlightEvent.Type.APOGEE) {
+ return Collections.singleton(new FlightEvent(FlightEvent.Type.SIMULATION_END,
+ status.time));
+ }
+ return null;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.simulation.listeners;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintStream;
+import java.util.Collection;
+import java.util.Iterator;
+
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.simulation.FlightDataBranch;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationStatus;
+
+
+public class CSVSaveListener extends AbstractSimulationListener {
+
+ private static enum Types {
+ TIME {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.time;
+ }
+ },
+ POSITION_X {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.position.x;
+ }
+ },
+ POSITION_Y {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.position.y;
+ }
+ },
+ ALTITUDE {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.position.z;
+ }
+ },
+ VELOCITY_X {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.velocity.x;
+ }
+ },
+ VELOCITY_Y {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.velocity.y;
+ }
+ },
+ VELOCITY_Z {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.velocity.z;
+ }
+ },
+ THETA {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_ORIENTATION_THETA);
+ }
+ },
+ PHI {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_ORIENTATION_PHI);
+ }
+ },
+ AOA {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_AOA);
+ }
+ },
+ ROLLRATE {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_ROLL_RATE);
+ }
+ },
+ PITCHRATE {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_PITCH_RATE);
+ }
+ },
+
+ PITCHMOMENT {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_PITCH_MOMENT_COEFF);
+ }
+ },
+ YAWMOMENT {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_YAW_MOMENT_COEFF);
+ }
+ },
+ ROLLMOMENT {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_ROLL_MOMENT_COEFF);
+ }
+ },
+ NORMALFORCE {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_NORMAL_FORCE_COEFF);
+ }
+ },
+ SIDEFORCE {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_SIDE_FORCE_COEFF);
+ }
+ },
+ AXIALFORCE {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_DRAG_FORCE);
+ }
+ },
+ WINDSPEED {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_WIND_VELOCITY);
+ }
+ },
+ PITCHDAMPING {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.
+ TYPE_PITCH_DAMPING_MOMENT_COEFF);
+ }
+ },
+ CA {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_AXIAL_DRAG_COEFF);
+ }
+ },
+ CD {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_DRAG_COEFF);
+ }
+ },
+ CDpressure {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_PRESSURE_DRAG_COEFF);
+ }
+ },
+ CDfriction {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_FRICTION_DRAG_COEFF);
+ }
+ },
+ CDbase {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_BASE_DRAG_COEFF);
+ }
+ },
+ MACH {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_MACH_NUMBER);
+ }
+ },
+ RE {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_REYNOLDS_NUMBER);
+ }
+ },
+
+ CONTROL_ANGLE {
+ @Override
+ public double getValue(SimulationStatus status) {
+ Iterator<RocketComponent> iterator =
+ status.configuration.getRocket().deepIterator();
+ FinSet fin = null;
+
+ while (iterator.hasNext()) {
+ RocketComponent c = iterator.next();
+ if (c instanceof FinSet && c.getName().equals("CONTROL")) {
+ fin = (FinSet)c;
+ break;
+ }
+ }
+ if (fin==null)
+ return 0;
+ return fin.getCantAngle();
+ }
+ },
+
+ EXTRA {
+ @Override
+ public double getValue(SimulationStatus status) {
+ if (status.extra instanceof Double)
+ return (Double)status.extra;
+ else
+ return Double.NaN;
+ }
+ },
+
+ MASS {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_MASS);
+ }
+ }
+
+ ;
+
+ public abstract double getValue(SimulationStatus status);
+ }
+
+
+ public static final String FILENAME_FORMAT = "simulation-%03d.csv";
+
+ private File file;
+ private PrintStream output = null;
+
+
+
+ @Override
+ public Collection<FlightEvent> handleEvent(FlightEvent event,
+ SimulationStatus status) {
+
+ if (event.getType() == FlightEvent.Type.LAUNCH) {
+ int n = 1;
+
+ if (output != null) {
+ System.err.println("WARNING: Ending simulation logging to CSV file " +
+ "(SIMULATION_END not encountered).");
+ output.close();
+ output = null;
+ }
+
+ do {
+ file = new File(String.format(FILENAME_FORMAT, n));
+ n++;
+ } while (file.exists());
+
+ System.err.println("Opening file "+file+" for CSV output.");
+ try {
+ output = new PrintStream(file);
+ } catch (FileNotFoundException e) {
+ System.err.println("ERROR OPENING FILE: "+e);
+ }
+
+ final Types[] types = Types.values();
+ StringBuilder s = new StringBuilder("# " + types[0].toString());
+ for (int i=1; i<types.length; i++) {
+ s.append("," + types[i].toString());
+ }
+ output.println(s);
+
+ } else if (event.getType() == FlightEvent.Type.SIMULATION_END && output != null) {
+
+ System.err.println("Ending simulation logging to CSV file: "+file);
+ output.close();
+ output = null;
+
+ } else if (event.getType() != FlightEvent.Type.ALTITUDE){
+
+ if (output != null) {
+ output.println("# Event "+event);
+ } else {
+ System.err.println("WARNING: Event "+event+" encountered without open file");
+ }
+
+ }
+
+ return null;
+ }
+
+
+ @Override
+ public Collection<FlightEvent> stepTaken(SimulationStatus status) {
+
+ final Types[] types = Types.values();
+ StringBuilder s;
+
+ if (output != null) {
+
+ s = new StringBuilder("" + types[0].getValue(status));
+ for (int i=1; i<types.length; i++) {
+ s.append("," + types[i].getValue(status));
+ }
+ output.println(s);
+
+ } else {
+
+ System.err.println("WARNING: stepTaken called with no open file " +
+ "(t="+status.time+")");
+ }
+
+ return null;
+ }
+}
--- /dev/null
+package net.sf.openrocket.simulation.listeners;
+
+import java.util.Collection;
+
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationStatus;
+import net.sf.openrocket.simulation.exception.SimulationCancelledException;
+
+
+/**
+ * A simulation listener that throws a {@link SimulationCancelledException} if
+ * this thread has been interrupted. The conditions is checked every time a step
+ * is taken.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class InterruptListener extends AbstractSimulationListener {
+
+ public static final InterruptListener INSTANCE = new InterruptListener();
+
+ @Override
+ public Collection<FlightEvent> stepTaken(SimulationStatus status)
+ throws SimulationCancelledException {
+
+ if (Thread.interrupted()) {
+ throw new SimulationCancelledException("The simulation was interrupted.");
+ }
+
+ return null;
+ }
+}
--- /dev/null
+package net.sf.openrocket.simulation.listeners;
+
+import java.util.Collection;
+
+import net.sf.openrocket.simulation.FlightDataBranch;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationStatus;
+
+
+public class PrintSimulationListener extends AbstractSimulationListener {
+
+ @Override
+ public Collection<FlightEvent> handleEvent(FlightEvent event,
+ SimulationStatus status) {
+
+ System.out.println("*** handleEvent *** "+event.toString() +
+ " position="+status.position + " velocity="+status.velocity);
+ return null;
+ }
+
+ @Override
+ public Collection<FlightEvent> stepTaken(SimulationStatus status) {
+
+ FlightDataBranch data = status.flightData;
+ System.out.printf("*** stepTaken *** time=%.3f position="+status.position+
+ " velocity="+status.velocity+"=%.3f\n", status.time, status.velocity.length());
+ System.out.printf(" thrust=%.3fN drag==%.3fN mass=%.3fkg " +
+ "accZ=%.3fm/s2 acc=%.3fm/s2\n",
+ data.getLast(FlightDataBranch.TYPE_THRUST_FORCE),
+ data.getLast(FlightDataBranch.TYPE_DRAG_FORCE),
+ data.getLast(FlightDataBranch.TYPE_MASS),
+ data.getLast(FlightDataBranch.TYPE_ACCELERATION_Z),
+ data.getLast(FlightDataBranch.TYPE_ACCELERATION_TOTAL));
+
+ return null;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.simulation.listeners;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.PrintStream;
+import java.util.Collection;
+
+import net.sf.openrocket.simulation.FlightDataBranch;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationStatus;
+
+
+public class RollSaveListener extends AbstractSimulationListener {
+
+ private static enum Types {
+ TIME {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.time;
+ }
+ },
+ ALTITUDE {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.position.z;
+ }
+ },
+ VELOCITY_Z {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.velocity.z;
+ }
+ },
+ THETA {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_ORIENTATION_THETA);
+ }
+ },
+ AOA {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_AOA);
+ }
+ },
+ ROLLRATE {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_ROLL_RATE);
+ }
+ },
+ PITCHRATE {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_PITCH_RATE);
+ }
+ },
+ ROLLMOMENT {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_ROLL_MOMENT_COEFF);
+ }
+ },
+ MACH {
+ @Override
+ public double getValue(SimulationStatus status) {
+ return status.flightData.getLast(FlightDataBranch.TYPE_MACH_NUMBER);
+ }
+ },
+
+ ;
+
+ public abstract double getValue(SimulationStatus status);
+ }
+
+
+ public static final String FILENAME_FORMAT = "simulation-%03d.csv";
+
+ private File file;
+ private PrintStream output = null;
+
+
+
+ @Override
+ public Collection<FlightEvent> handleEvent(FlightEvent event,
+ SimulationStatus status) {
+
+ if (event.getType() == FlightEvent.Type.LAUNCH) {
+ int n = 1;
+
+ if (output != null) {
+ System.err.println("WARNING: Ending simulation logging to CSV file " +
+ "(SIMULATION_END not encountered).");
+ output.close();
+ output = null;
+ }
+
+ do {
+ file = new File(String.format(FILENAME_FORMAT, n));
+ n++;
+ } while (file.exists());
+
+ System.err.println("Opening file "+file+" for CSV output.");
+ try {
+ output = new PrintStream(file);
+ } catch (FileNotFoundException e) {
+ System.err.println("ERROR OPENING FILE: "+e);
+ }
+
+ final Types[] types = Types.values();
+ StringBuilder s = new StringBuilder("# " + types[0].toString());
+ for (int i=1; i<types.length; i++) {
+ s.append("," + types[i].toString());
+ }
+ output.println(s);
+
+ } else if (event.getType() == FlightEvent.Type.SIMULATION_END && output != null) {
+
+ System.err.println("Ending simulation logging to CSV file: "+file);
+ output.close();
+ output = null;
+
+ } else if (event.getType() != FlightEvent.Type.ALTITUDE){
+
+ if (output != null) {
+ output.println("# Event "+event);
+ } else {
+ System.err.println("WARNING: Event "+event+" encountered without open file");
+ }
+
+ }
+
+ return null;
+ }
+
+
+ @Override
+ public Collection<FlightEvent> stepTaken(SimulationStatus status) {
+
+ final Types[] types = Types.values();
+ StringBuilder s;
+
+ if (output != null) {
+
+ s = new StringBuilder("" + types[0].getValue(status));
+ for (int i=1; i<types.length; i++) {
+ s.append("," + types[i].getValue(status));
+ }
+ output.println(s);
+
+ } else {
+
+ System.err.println("WARNING: stepTaken called with no open file " +
+ "(t="+status.time+")");
+ }
+
+ return null;
+ }
+}
--- /dev/null
+package net.sf.openrocket.simulation.listeners;
+
+import java.util.Collection;
+import java.util.Collections;
+
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationStatus;
+
+
+public class StopSimulationListener extends AbstractSimulationListener {
+
+ private final int REPORT = 500;
+
+ private final double stopTime;
+ private final int stopStep;
+
+ private int step = 0;
+
+ private long startTime = -1;
+ private long time = -1;
+
+ public StopSimulationListener(double t, int n) {
+ stopTime = t;
+ stopStep = n;
+ }
+
+
+ @Override
+ public Collection<FlightEvent> handleEvent(FlightEvent event,
+ SimulationStatus status) {
+
+ if (event.getType() == FlightEvent.Type.LAUNCH) {
+ System.out.println("Simulation starting.");
+ time = System.nanoTime();
+ startTime = System.nanoTime();
+ }
+
+ return null;
+ }
+
+ @Override
+ public Collection<FlightEvent> stepTaken(SimulationStatus status) {
+ step ++;
+ if ((step%REPORT) == 0) {
+ long t = System.nanoTime();
+
+ System.out.printf("Step %4d, time=%.3f, took %d us/step (avg. %d us/step)\n",
+ step,status.time,(t-time)/1000/REPORT,(t-startTime)/1000/step);
+ time = t;
+ }
+ if (status.time >= stopTime || step >= stopStep) {
+ System.out.printf("Stopping simulation, step=%d time=%.3f\n",step,status.time);
+ return Collections.singleton(new FlightEvent(FlightEvent.Type.SIMULATION_END,
+ status.time, null));
+ }
+ return null;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.simulation.listeners.haisu;
+
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.simulation.SimulationStatus;
+import net.sf.openrocket.simulation.listeners.AbstractSimulationListener;
+
+public class HaisuCatoListener extends AbstractSimulationListener {
+
+ private static final double POSITION = 0.8;
+ private static final double CNA = 5.16;
+
+ private final double alpha;
+
+ public HaisuCatoListener(double alpha) {
+ this.alpha = alpha;
+ }
+
+ @Override
+ public void forceCalculation(SimulationStatus status, FlightConditions conditions,
+ AerodynamicForces forces) {
+
+ double cn = CNA * alpha;
+ double cm = cn * POSITION / conditions.getRefLength();
+
+ double theta = conditions.getTheta();
+ double costheta = Math.cos(theta);
+
+ forces.CN += cn * costheta;
+ forces.Cm += cm * costheta;
+
+ if (Math.abs(costheta) < 0.99) {
+ System.err.println("THETA = "+(theta*180/Math.PI)+ " aborting...");
+ System.exit(1);
+ }
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.simulation.listeners.haisu;
+
+import java.util.Collection;
+
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.simulation.FlightEvent;
+import net.sf.openrocket.simulation.SimulationStatus;
+import net.sf.openrocket.simulation.listeners.AbstractSimulationListener;
+import net.sf.openrocket.util.MathUtil;
+
+
+public class RollControlListener extends AbstractSimulationListener {
+
+ private static final double DELTA_T = 0.01;
+ private static final double START_TIME = 0.5;
+
+ private static final double MACH = 0.9;
+
+ private static final double SETPOINT = 0.0;
+
+ private static final double TURNRATE = 10 * Math.PI/180; // per second
+
+
+ /*
+ * At M=0.3 KP oscillation threshold between 0.35 and 0.4. Good KI=3
+ * At M=0.6 KP oscillation threshold between 0.07 and 0.08 Good KI=2
+ * At M=0.9 KP oscillation threshold between 0.013 and 0.014 Good KI=0.5
+ */
+ private static final double KP = 0.007;
+ private static final double KI = 0.2;
+
+
+ private static final double MAX_ANGLE = 15 * Math.PI/180;
+
+
+
+
+ private double rollrate;
+
+ private double intState = 0;
+
+ private double finPosition = 0;
+
+
+ public RollControlListener() {
+ }
+
+ @Override
+ public void flightConditions(SimulationStatus status, FlightConditions conditions) {
+ // Limit movement:
+
+// conditions.setAOA(0);
+// conditions.setTheta(0);
+// conditions.setMach(MACH);
+// conditions.setPitchRate(0);
+// conditions.setYawRate(0);
+// status.position = new Coordinate(0,0,100);
+// status.velocity = Coordinate.NUL;
+
+
+ rollrate = conditions.getRollRate();
+ }
+
+ @Override
+ public Collection<FlightEvent> stepTaken(SimulationStatus status) {
+
+ if (status.time < START_TIME)
+ return null;
+
+ // PID controller
+ FinSet finset = null;
+ for (RocketComponent c: status.configuration) {
+ if ((c instanceof FinSet) && (c.getName().equals("CONTROL"))) {
+ finset = (FinSet)c;
+ break;
+ }
+ }
+ if (finset==null) {
+ throw new RuntimeException("CONTROL fin not found");
+ }
+
+
+ double error = SETPOINT - rollrate;
+
+
+ error = Math.signum(error) * error * error; //// pow2(error)
+
+ double p = KP * error;
+ intState += error * DELTA_T;
+ double i = KI * intState;
+
+ double value = p+i;
+
+
+ if (Math.abs(value) > MAX_ANGLE) {
+ System.err.printf("Attempting to set angle %.1f at t=%.3f, clamping.\n",
+ value*180/Math.PI, status.time);
+ value = MathUtil.clamp(value, -MAX_ANGLE, MAX_ANGLE);
+ }
+
+
+ if (finPosition < value) {
+ finPosition = Math.min(finPosition + TURNRATE*DELTA_T, value);
+ } else {
+ finPosition = Math.max(finPosition - TURNRATE*DELTA_T, value);
+ }
+
+ if (MathUtil.equals(status.time*10, Math.rint(status.time*10))) {
+ System.err.printf("t=%.3f angle=%.1f current=%.1f\n",status.time,
+ value*180/Math.PI, finPosition*180/Math.PI);
+ }
+
+ finset.setCantAngle(finPosition);
+
+ return null;
+ }
+
+
+
+
+
+}
--- /dev/null
+package net.sf.openrocket.unit;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Iterator;
+
+import javax.swing.event.ChangeEvent;
+import javax.swing.event.ChangeListener;
+
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.rocketcomponent.SymmetricComponent;
+import net.sf.openrocket.util.MathUtil;
+
+
+public class CaliberUnit extends GeneralUnit {
+
+ public static final double DEFAULT_CALIBER = 0.01;
+
+ private final Configuration configuration;
+ private final Rocket rocket;
+
+ private double caliber = -1;
+
+
+ /* Listener for rocket and configuration, resets the caliber to -1. */
+ private final ChangeListener listener = new ChangeListener() {
+ @Override
+ public void stateChanged(ChangeEvent e) {
+ caliber = -1;
+ }
+ };
+
+
+
+ public CaliberUnit(Configuration configuration) {
+ super(1.0, "cal");
+ this.configuration = configuration;
+
+ if (configuration == null) {
+ this.rocket = null;
+ } else {
+ this.rocket = configuration.getRocket();
+ configuration.addChangeListener(listener);
+ }
+ }
+
+ public CaliberUnit(Rocket rocket) {
+ super(1.0, "cal");
+ this.configuration = null;
+ this.rocket = rocket;
+ if (rocket != null) {
+ rocket.addChangeListener(listener);
+ }
+ }
+
+
+ @Override
+ public double fromUnit(double value) {
+ if (caliber < 0)
+ calculateCaliber();
+
+ return value * caliber;
+ }
+
+ @Override
+ public double toUnit(double value) {
+ if (caliber < 0)
+ calculateCaliber();
+
+ return value / caliber;
+ }
+
+
+ // TODO: HIGH: Check caliber calculation method...
+ private void calculateCaliber() {
+ caliber = 0;
+
+ Iterator<RocketComponent> iterator;
+ if (configuration != null) {
+ iterator = configuration.iterator();
+ } else if (rocket != null) {
+ iterator = rocket.deepIterator();
+ } else {
+ Collection<RocketComponent> set = Collections.emptyList();
+ iterator = set.iterator();
+ }
+
+ while (iterator.hasNext()) {
+ RocketComponent c = iterator.next();
+ if (c instanceof SymmetricComponent) {
+ double r1 = ((SymmetricComponent)c).getForeRadius() * 2;
+ double r2 = ((SymmetricComponent)c).getAftRadius() * 2;
+ caliber = MathUtil.max(caliber, r1, r2);
+ }
+ }
+
+ if (caliber <= 0)
+ caliber = DEFAULT_CALIBER;
+ }
+}
--- /dev/null
+package net.sf.openrocket.unit;
+
+import java.text.DecimalFormat;
+
+public class DegreeUnit extends GeneralUnit {
+
+ public DegreeUnit() {
+ super(Math.PI/180.0,"\u00b0");
+ }
+
+ @Override
+ public boolean hasSpace() {
+ return false;
+ }
+
+ @Override
+ public double round(double v) {
+ return Math.rint(v);
+ }
+
+ private final DecimalFormat decFormat = new DecimalFormat("0.#");
+ @Override
+ public String toString(double value) {
+ double val = toUnit(value);
+ return decFormat.format(val);
+ }
+}
--- /dev/null
+package net.sf.openrocket.unit;
+
+import java.util.ArrayList;
+
+public class FixedPrecisionUnit extends Unit {
+
+ private final double precision;
+ private final String formatString;
+
+ public FixedPrecisionUnit(String unit, double precision) {
+ this(unit, precision, 1.0);
+ }
+
+ public FixedPrecisionUnit(String unit, double precision, double multiplier) {
+ super(multiplier, unit);
+
+ this.precision = precision;
+
+ int decimals = 0;
+ double p = precision;
+ while ((p - Math.floor(p)) > 0.0000001) {
+ p *= 10;
+ decimals++;
+ }
+ formatString = "%." + decimals + "f";
+ }
+
+
+ @Override
+ public double getNextValue(double value) {
+ return round(value + precision);
+ }
+
+ @Override
+ public double getPreviousValue(double value) {
+ return round(value - precision);
+ }
+
+
+ @Override
+ public double round(double value) {
+ return Math.rint(value/precision)*precision;
+ }
+
+
+
+
+ @Override
+ public String toString(double value) {
+ return String.format(formatString, value);
+ }
+
+
+
+ // TODO: LOW: This is copied from GeneralUnit, perhaps combine
+ @Override
+ public Tick[] getTicks(double start, double end, double minor, double major) {
+ // Convert values
+ start = toUnit(start);
+ end = toUnit(end);
+ minor = toUnit(minor);
+ major = toUnit(major);
+
+ if (minor <= 0 || major <= 0 || major < minor) {
+ throw new IllegalArgumentException("getTicks called with minor="+minor+" major="+major);
+ }
+
+ ArrayList<Tick> ticks = new ArrayList<Tick>();
+
+ int mod2,mod3,mod4; // Moduli for minor-notable, major-nonnotable, major-notable
+ double minstep;
+
+ // Find the smallest possible step size
+ double one=1;
+ while (one > minor)
+ one /= 10;
+ while (one < minor)
+ one *= 10;
+ // one is the smallest round-ten that is larger than minor
+ if (one/2 >= minor) {
+ // smallest step is round-five
+ minstep = one/2;
+ mod2 = 2; // Changed later if clashes with major ticks
+ } else {
+ minstep = one;
+ mod2 = 10; // Changed later if clashes with major ticks
+ }
+
+ // Find step size for major ticks
+ one = 1;
+ while (one > major)
+ one /= 10;
+ while (one < major)
+ one *= 10;
+ if (one/2 >= major) {
+ // major step is round-five, major-notable is next round-ten
+ double majorstep = one/2;
+ mod3 = (int)Math.round(majorstep/minstep);
+ mod4 = mod3*2;
+ } else {
+ // major step is round-ten, major-notable is next round-ten
+ mod3 = (int)Math.round(one/minstep);
+ mod4 = mod3*10;
+ }
+ // Check for clashes between minor-notable and major-nonnotable
+ if (mod3 == mod2) {
+ if (mod2==2)
+ mod2 = 1; // Every minor tick is notable
+ else
+ mod2 = 5; // Every fifth minor tick is notable
+ }
+
+
+ // Calculate starting position
+ int pos = (int)Math.ceil(start/minstep);
+// System.out.println("mod2="+mod2+" mod3="+mod3+" mod4="+mod4);
+ while (pos*minstep <= end) {
+ double unitValue = pos*minstep;
+ double value = fromUnit(unitValue);
+
+ if (pos%mod4 == 0)
+ ticks.add(new Tick(value,unitValue,true,true));
+ else if (pos%mod3 == 0)
+ ticks.add(new Tick(value,unitValue,true,false));
+ else if (pos%mod2 == 0)
+ ticks.add(new Tick(value,unitValue,false,true));
+ else
+ ticks.add(new Tick(value,unitValue,false,false));
+
+ pos++;
+ }
+
+ return ticks.toArray(new Tick[0]);
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.unit;
+
+import java.util.ArrayList;
+
+public class GeneralUnit extends Unit {
+
+ private final int significantNumbers;
+ private final int decimalRounding;
+
+ // Values smaller that this are rounded using decimal rounding
+ // [pre-calculated as 10^(significantNumbers-1)]
+ private final double decimalLimit;
+
+ // Pre-calculated as 10^significantNumbers
+ private final double significantNumbersLimit;
+
+
+ public GeneralUnit(double multiplier, String unit) {
+ this(multiplier, unit, 2, 10);
+ }
+
+ public GeneralUnit(double multiplier, String unit, int significantNumbers) {
+ this(multiplier, unit, significantNumbers, 10);
+ }
+
+ public GeneralUnit(double multiplier, String unit, int significantNumbers, int decimalRounding) {
+ super(multiplier, unit);
+ assert(significantNumbers>0);
+ assert(decimalRounding>0);
+
+ this.significantNumbers = significantNumbers;
+ this.decimalRounding = decimalRounding;
+
+ double d=1;
+ double e=10;
+ for (int i=1; i<significantNumbers; i++) {
+ d *= 10.0;
+ e *= 10.0;
+ }
+ decimalLimit = d;
+ significantNumbersLimit = e;
+ }
+
+ @Override
+ public double round(double value) {
+ if (value < decimalLimit) {
+ // Round to closest 1/decimalRounding
+ return Math.rint(value*decimalRounding)/decimalRounding;
+ } else {
+ // Round to given amount of significant numbers
+ double m = 1;
+ while (value >= significantNumbersLimit) {
+ m *= 10.0;
+ value /= 10.0;
+ }
+ return Math.rint(value)*m;
+ }
+ }
+
+
+
+
+ // TODO: LOW: untested
+ // start, end and scale in this units
+// @Override
+ public ArrayList<Tick> getTicks(double start, double end, double scale) {
+ ArrayList<Tick> ticks = new ArrayList<Tick>();
+ double delta;
+ int normal, major;
+
+ // TODO: LOW: more fine-grained (e.g. 0||||5||||10||||15||||20)
+ if (scale <= 1.0/decimalRounding) {
+ delta = 1.0/decimalRounding;
+ normal = 1;
+ major = decimalRounding;
+ } else if (scale <= 1.0) {
+ delta = 1.0/decimalRounding;
+ normal = decimalRounding;
+ major = decimalRounding*10;
+ } else {
+ double r = scale;
+ delta = 1;
+ while (r > 10) {
+ r /= 10;
+ delta *= 10;
+ }
+ normal = 10;
+ major = 100; // TODO: LOW: More fine-grained with 5
+ }
+
+ double v = Math.ceil(start/delta)*delta;
+ int n = (int)Math.round(v/delta);
+
+// while (v <= end) {
+// if (n%major == 0)
+// ticks.add(new Tick(v,Tick.MAJOR));
+// else if (n%normal == 0)
+// ticks.add(new Tick(v,Tick.NORMAL));
+// else
+// ticks.add(new Tick(v,Tick.MINOR));
+// v += delta;
+// n++;
+// }
+
+ return ticks;
+ }
+
+ @Override
+ public Tick[] getTicks(double start, double end, double minor, double major) {
+ // Convert values
+ start = toUnit(start);
+ end = toUnit(end);
+ minor = toUnit(minor);
+ major = toUnit(major);
+
+ if (minor <= 0 || major <= 0 || major < minor) {
+ throw new IllegalArgumentException("getTicks called with minor="+minor+" major="+major);
+ }
+
+ ArrayList<Tick> ticks = new ArrayList<Tick>();
+
+ int mod2,mod3,mod4; // Moduli for minor-notable, major-nonnotable, major-notable
+ double minstep;
+
+ // Find the smallest possible step size
+ double one=1;
+ while (one > minor)
+ one /= 10;
+ while (one < minor)
+ one *= 10;
+ // one is the smallest round-ten that is larger than minor
+ if (one/2 >= minor) {
+ // smallest step is round-five
+ minstep = one/2;
+ mod2 = 2; // Changed later if clashes with major ticks
+ } else {
+ minstep = one;
+ mod2 = 10; // Changed later if clashes with major ticks
+ }
+
+ // Find step size for major ticks
+ one = 1;
+ while (one > major)
+ one /= 10;
+ while (one < major)
+ one *= 10;
+ if (one/2 >= major) {
+ // major step is round-five, major-notable is next round-ten
+ double majorstep = one/2;
+ mod3 = (int)Math.round(majorstep/minstep);
+ mod4 = mod3*2;
+ } else {
+ // major step is round-ten, major-notable is next round-ten
+ mod3 = (int)Math.round(one/minstep);
+ mod4 = mod3*10;
+ }
+ // Check for clashes between minor-notable and major-nonnotable
+ if (mod3 == mod2) {
+ if (mod2==2)
+ mod2 = 1; // Every minor tick is notable
+ else
+ mod2 = 5; // Every fifth minor tick is notable
+ }
+
+
+ // Calculate starting position
+ int pos = (int)Math.ceil(start/minstep);
+// System.out.println("mod2="+mod2+" mod3="+mod3+" mod4="+mod4);
+ while (pos*minstep <= end) {
+ double unitValue = pos*minstep;
+ double value = fromUnit(unitValue);
+
+ if (pos%mod4 == 0)
+ ticks.add(new Tick(value,unitValue,true,true));
+ else if (pos%mod3 == 0)
+ ticks.add(new Tick(value,unitValue,true,false));
+ else if (pos%mod2 == 0)
+ ticks.add(new Tick(value,unitValue,false,true));
+ else
+ ticks.add(new Tick(value,unitValue,false,false));
+
+ pos++;
+ }
+
+ return ticks.toArray(new Tick[0]);
+ }
+
+
+ @Override
+ public double getNextValue(double value) {
+ // TODO: HIGH: Auto-generated method stub
+ return value+1;
+ }
+
+ @Override
+ public double getPreviousValue(double value) {
+ // TODO: HIGH: Auto-generated method stub
+ return value-1;
+ }
+
+
+ ///// TESTING:
+
+ private static void printTicks(double start, double end, double minor, double major) {
+ Tick[] ticks = Unit.NOUNIT2.getTicks(start, end, minor, major);
+ String str = "Ticks for ("+start+","+end+","+minor+","+major+"):";
+ for (int i=0; i<ticks.length; i++) {
+ str += " "+ticks[i].value;
+ if (ticks[i].major) {
+ if (ticks[i].notable)
+ str += "*";
+ else
+ str += "o";
+ } else {
+ if (ticks[i].notable)
+ str += "_";
+ else
+ str += " ";
+ }
+ }
+ System.out.println(str);
+ }
+ public static void main(String[] arg) {
+ printTicks(0,100,1,10);
+ printTicks(4.7,11.0,0.15,0.7);
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.unit;
+
+import java.text.DecimalFormat;
+
+public class RadianUnit extends GeneralUnit {
+
+ public RadianUnit() {
+ super(1,"rad");
+ }
+
+ @Override
+ public double round(double v) {
+ return Math.rint(v*10.0)/10.0;
+ }
+
+ private final DecimalFormat decFormat = new DecimalFormat("0.0");
+ @Override
+ public String toString(double value) {
+ double val = toUnit(value);
+ return decFormat.format(val);
+ }
+}
--- /dev/null
+package net.sf.openrocket.unit;
+
+public class TemperatureUnit extends FixedPrecisionUnit {
+
+ protected final double addition;
+
+ public TemperatureUnit(double multiplier, double addition, String unit) {
+ super(unit, 1, multiplier);
+
+ this.addition = addition;
+ }
+
+ @Override
+ public boolean hasSpace() {
+ return false;
+ }
+
+ @Override
+ public double toUnit(double value) {
+ return value/multiplier - addition;
+ }
+
+ @Override
+ public double fromUnit(double value) {
+ return (value + addition)*multiplier;
+ }
+}
--- /dev/null
+package net.sf.openrocket.unit;
+
+public final class Tick {
+ public final double value;
+ public final double unitValue;
+ public final boolean major;
+ public final boolean notable;
+
+ public Tick(double value, double unitValue, boolean major, boolean notable) {
+ this.value = value;
+ this.unitValue = unitValue;
+ this.major = major;
+ this.notable = notable;
+ }
+
+ @Override
+ public String toString() {
+ String s = "Tick[value="+value;
+ if (major)
+ s += ",major";
+ else
+ s += ",minor";
+ if (notable)
+ s += ",notable";
+ s+= "]";
+ return s;
+ }
+}
--- /dev/null
+package net.sf.openrocket.unit;
+
+import java.text.DecimalFormat;
+
+public abstract class Unit {
+
+ /** No unit with 2 digit precision */
+ public static final Unit NOUNIT2 = new GeneralUnit(1,"\u200b", 2); // zero-width space
+
+ protected final double multiplier; // meters = units * multiplier
+ protected final String unit;
+
+ /**
+ * Creates a new Unit with a given multiplier and unit name.
+ *
+ * Multiplier e.g. 1 in = 0.0254 meter
+ *
+ * @param multiplier The multiplier to use on the value, 1 this unit == multiplier SI units
+ * @param unit The unit's short form.
+ */
+ public Unit(double multiplier, String unit) {
+ if (multiplier == 0)
+ throw new IllegalArgumentException("Unit has multiplier=0");
+ this.multiplier = multiplier;
+ this.unit = unit;
+ }
+
+ /**
+ * Converts from SI units to this unit. The default implementation simply divides by the
+ * multiplier.
+ *
+ * @param value Value in SI unit
+ * @return Value in these units
+ */
+ public double toUnit(double value) {
+ return value/multiplier;
+ }
+
+ /**
+ * Convert from this type of units to SI units. The default implementation simply
+ * multiplies by the multiplier.
+ *
+ * @param value Value in these units
+ * @return Value in SI units
+ */
+ public double fromUnit(double value) {
+ return value*multiplier;
+ }
+
+
+ /**
+ * Return the unit name.
+ *
+ * @return the unit.
+ */
+ public String getUnit() {
+ return unit;
+ }
+
+ /**
+ * Whether the value and unit should be separated by a whitespace. This method
+ * returns true as most units have a space between the value and unit, but may be
+ * overridden.
+ *
+ * @return true if the value and unit should be separated
+ */
+ public boolean hasSpace() {
+ return true;
+ }
+
+
+ // Testcases for toString(double)
+ public static void main(String arg[]) {
+ System.out.println(NOUNIT2.toString(0.0049));
+ System.out.println(NOUNIT2.toString(0.0050));
+ System.out.println(NOUNIT2.toString(0.0051));
+ System.out.println(NOUNIT2.toString(0.00123));
+ System.out.println(NOUNIT2.toString(0.0123));
+ System.out.println(NOUNIT2.toString(0.1234));
+ System.out.println(NOUNIT2.toString(1.2345));
+ System.out.println(NOUNIT2.toString(12.345));
+ System.out.println(NOUNIT2.toString(123.456));
+ System.out.println(NOUNIT2.toString(1234.5678));
+ System.out.println(NOUNIT2.toString(12345.6789));
+ System.out.println(NOUNIT2.toString(123456.789));
+ System.out.println(NOUNIT2.toString(1234567.89));
+ System.out.println(NOUNIT2.toString(12345678.9));
+
+ System.out.println(NOUNIT2.toString(-0.0049));
+ System.out.println(NOUNIT2.toString(-0.0050));
+ System.out.println(NOUNIT2.toString(-0.0051));
+ System.out.println(NOUNIT2.toString(-0.00123));
+ System.out.println(NOUNIT2.toString(-0.0123));
+ System.out.println(NOUNIT2.toString(-0.1234));
+ System.out.println(NOUNIT2.toString(-1.2345));
+ System.out.println(NOUNIT2.toString(-12.345));
+ System.out.println(NOUNIT2.toString(-123.456));
+ System.out.println(NOUNIT2.toString(-1234.5678));
+ System.out.println(NOUNIT2.toString(-12345.6789));
+ System.out.println(NOUNIT2.toString(-123456.789));
+ System.out.println(NOUNIT2.toString(-1234567.89));
+ System.out.println(NOUNIT2.toString(-12345678.9));
+
+ }
+
+
+ @Override
+ public String toString() {
+ return unit;
+ }
+
+ private static final DecimalFormat intFormat = new DecimalFormat("#");
+ private static final DecimalFormat decFormat = new DecimalFormat("0.##");
+ private static final DecimalFormat expFormat = new DecimalFormat("0.00E0");
+
+ /**
+ * Format the given value (in SI units) to a string representation of the value in this
+ * units. An suitable amount of decimals for the unit are used in the representation.
+ * The unit is not appended to the numerical value.
+ *
+ * @param value Value in SI units.
+ * @return A string representation of the number in these units.
+ */
+ public String toString(double value) {
+ double val = toUnit(value);
+
+ if (Math.abs(val) > 1E6) {
+ return expFormat.format(val);
+ }
+ if (Math.abs(val) >= 100) {
+ return intFormat.format(val);
+ }
+ if (Math.abs(val) <= 0.005) {
+ return "0";
+ }
+
+ double sign = Math.signum(val);
+ val = Math.abs(val);
+ double mul = 1.0;
+ while (val < 100) {
+ mul *= 10;
+ val *= 10;
+ }
+ val = Math.rint(val)/mul * sign;
+
+ return decFormat.format(val);
+ }
+
+
+ /**
+ * Return a string with the specified value and unit. The value is converted into
+ * this unit. If <code>value</code> is NaN, returns "N/A" (not applicable).
+ *
+ * @param value the value to print in SI units.
+ * @return the value and unit, or "N/A".
+ */
+ public String toStringUnit(double value) {
+ if (Double.isNaN(value))
+ return "N/A";
+
+ String s = toString(value);
+ if (hasSpace())
+ s += " ";
+ s += unit;
+ return s;
+ }
+
+
+
+
+ /**
+ * Round the value (in the current units) to a precision suitable for rough valuing
+ * (approximately 2 significant numbers).
+ *
+ * @param value Value in current units
+ * @return Rounded value.
+ */
+ public abstract double round(double value);
+
+ /**
+ * Return the next rounded value after the given value.
+ * @param value Value in these units.
+ * @return The next suitable rounded value.
+ */
+ public abstract double getNextValue(double value);
+
+ /**
+ * Return the previous rounded value before the given value.
+ * @param value Value in these units.
+ * @return The previous suitable rounded value.
+ */
+ public abstract double getPreviousValue(double value);
+
+ //public abstract ArrayList<Tick> getTicks(double start, double end, double scale);
+
+ /**
+ * Return ticks in the range start - end (in current units). minor is the minimum
+ * distance between minor, non-notable ticks and major the minimum distance between
+ * major non-notable ticks. The values are in current units, i.e. no conversion is
+ * performed.
+ */
+ public abstract Tick[] getTicks(double start, double end, double minor, double major);
+
+ /**
+ * Compares whether the two units are equal. Equality requires the unit classes,
+ * multiplier values and units to be equal.
+ */
+ @Override
+ public boolean equals(Object other) {
+ if (other == null)
+ return false;
+ if (this.getClass() != other.getClass())
+ return false;
+ return ((this.multiplier == ((Unit)other).multiplier) &&
+ this.unit.equals(((Unit)other).unit));
+ }
+
+ @Override
+ public int hashCode() {
+ return this.getClass().hashCode() + this.unit.hashCode();
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.unit;
+
+import static net.sf.openrocket.util.MathUtil.pow2;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import net.sf.openrocket.rocketcomponent.Configuration;
+import net.sf.openrocket.rocketcomponent.Rocket;
+
+
+/**
+ * A group of units (eg. length, mass etc.). Contains a list of different units of a same
+ * quantity.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class UnitGroup {
+
+ public static final UnitGroup UNITS_NONE;
+
+ public static final UnitGroup UNITS_MOTOR_DIMENSIONS;
+ public static final UnitGroup UNITS_LENGTH;
+ public static final UnitGroup UNITS_DISTANCE;
+
+ public static final UnitGroup UNITS_AREA;
+ public static final UnitGroup UNITS_STABILITY;
+ public static final UnitGroup UNITS_VELOCITY;
+ public static final UnitGroup UNITS_ACCELERATION;
+ public static final UnitGroup UNITS_MASS;
+ public static final UnitGroup UNITS_ANGLE;
+ public static final UnitGroup UNITS_DENSITY_BULK;
+ public static final UnitGroup UNITS_DENSITY_SURFACE;
+ public static final UnitGroup UNITS_DENSITY_LINE;
+ public static final UnitGroup UNITS_FORCE;
+ public static final UnitGroup UNITS_IMPULSE;
+
+ /** Time in the order of less than a second (time step etc). */
+ public static final UnitGroup UNITS_TIME_STEP;
+
+ /** Time in the order of seconds (motor delay etc). */
+ public static final UnitGroup UNITS_SHORT_TIME;
+
+ /** Time in the order of the flight time of a rocket. */
+ public static final UnitGroup UNITS_FLIGHT_TIME;
+ public static final UnitGroup UNITS_ROLL;
+ public static final UnitGroup UNITS_TEMPERATURE;
+ public static final UnitGroup UNITS_PRESSURE;
+ public static final UnitGroup UNITS_RELATIVE;
+ public static final UnitGroup UNITS_ROUGHNESS;
+
+ public static final UnitGroup UNITS_COEFFICIENT;
+
+
+ public static final Map<String, UnitGroup> UNITS;
+
+
+ /*
+ * Note: Units may not use HTML tags.
+ */
+ static {
+ UNITS_NONE = new UnitGroup();
+ UNITS_NONE.addUnit(Unit.NOUNIT2);
+
+ UNITS_LENGTH = new UnitGroup();
+ UNITS_LENGTH.addUnit(new GeneralUnit(0.001,"mm"));
+ UNITS_LENGTH.addUnit(new GeneralUnit(0.01,"cm"));
+ UNITS_LENGTH.addUnit(new GeneralUnit(1,"m"));
+ UNITS_LENGTH.addUnit(new GeneralUnit(0.0254,"in"));
+ UNITS_LENGTH.addUnit(new GeneralUnit(0.3048,"ft"));
+ UNITS_LENGTH.setDefaultUnit(1);
+
+ UNITS_MOTOR_DIMENSIONS = new UnitGroup();
+ UNITS_MOTOR_DIMENSIONS.addUnit(new GeneralUnit(0.001,"mm"));
+ UNITS_MOTOR_DIMENSIONS.addUnit(new GeneralUnit(0.01,"cm"));
+ UNITS_MOTOR_DIMENSIONS.addUnit(new GeneralUnit(0.0254,"in"));
+ UNITS_MOTOR_DIMENSIONS.setDefaultUnit(0);
+
+ UNITS_DISTANCE = new UnitGroup();
+ UNITS_DISTANCE.addUnit(new GeneralUnit(1,"m"));
+ UNITS_DISTANCE.addUnit(new GeneralUnit(1000,"km"));
+ UNITS_DISTANCE.addUnit(new GeneralUnit(0.3048,"ft"));
+ UNITS_DISTANCE.addUnit(new GeneralUnit(0.9144,"yd"));
+ UNITS_DISTANCE.addUnit(new GeneralUnit(1609.344,"mi"));
+
+ UNITS_AREA = new UnitGroup();
+ UNITS_AREA.addUnit(new GeneralUnit(pow2(0.001),"mm\u00b2"));
+ UNITS_AREA.addUnit(new GeneralUnit(pow2(0.01),"cm\u00b2"));
+ UNITS_AREA.addUnit(new GeneralUnit(1,"m\u00b2"));
+ UNITS_AREA.addUnit(new GeneralUnit(pow2(0.0254),"in\u00b2"));
+ UNITS_AREA.addUnit(new GeneralUnit(pow2(0.3048),"ft\u00b2"));
+ UNITS_AREA.setDefaultUnit(1);
+
+
+ UNITS_STABILITY = new UnitGroup();
+ UNITS_STABILITY.addUnit(new GeneralUnit(0.001,"mm"));
+ UNITS_STABILITY.addUnit(new GeneralUnit(0.01,"cm"));
+ UNITS_STABILITY.addUnit(new GeneralUnit(0.0254,"in"));
+ UNITS_STABILITY.addUnit(new CaliberUnit((Rocket)null));
+ UNITS_STABILITY.setDefaultUnit(3);
+
+ UNITS_VELOCITY = new UnitGroup();
+ UNITS_VELOCITY.addUnit(new GeneralUnit(1, "m/s"));
+ UNITS_VELOCITY.addUnit(new GeneralUnit(1/3.6, "km/h"));
+ UNITS_VELOCITY.addUnit(new GeneralUnit(1/0.3048, "ft/s"));
+ UNITS_VELOCITY.addUnit(new GeneralUnit(0.44704, "mph"));
+
+ UNITS_ACCELERATION = new UnitGroup();
+ UNITS_ACCELERATION.addUnit(new GeneralUnit(1, "m/s\u00b2"));
+ UNITS_ACCELERATION.addUnit(new GeneralUnit(1/0.3048, "ft/s\00b2"));
+
+
+ UNITS_MASS = new UnitGroup();
+ UNITS_MASS.addUnit(new GeneralUnit(0.001,"g"));
+ UNITS_MASS.addUnit(new GeneralUnit(1,"kg"));
+ UNITS_MASS.addUnit(new GeneralUnit(0.0283495,"oz"));
+ UNITS_MASS.addUnit(new GeneralUnit(0.0453592,"lb"));
+
+ UNITS_ANGLE = new UnitGroup();
+ UNITS_ANGLE.addUnit(new DegreeUnit());
+ UNITS_ANGLE.addUnit(new FixedPrecisionUnit("rad",0.01));
+
+ UNITS_DENSITY_BULK = new UnitGroup();
+ UNITS_DENSITY_BULK.addUnit(new GeneralUnit(1000,"g/cm\u00b3"));
+ UNITS_DENSITY_BULK.addUnit(new GeneralUnit(1,"kg/m\u00b3"));
+ UNITS_DENSITY_BULK.addUnit(new GeneralUnit(1729.004,"oz/in\u00b3"));
+ UNITS_DENSITY_BULK.addUnit(new GeneralUnit(16.01846,"lb/ft\u00b3"));
+
+ UNITS_DENSITY_SURFACE = new UnitGroup();
+ UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(10,"g/cm\u00b2"));
+ UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(0.001,"g/m\u00b2"));
+ UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(1,"kg/m\u00b2"));
+ UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(43.9418,"oz/in\u00b2"));
+ UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(0.30515173,"oz/ft\u00b2"));
+ UNITS_DENSITY_SURFACE.addUnit(new GeneralUnit(4.88243,"lb/ft\u00b2"));
+ UNITS_DENSITY_SURFACE.setDefaultUnit(1);
+
+ UNITS_DENSITY_LINE = new UnitGroup();
+ UNITS_DENSITY_LINE.addUnit(new GeneralUnit(0.001,"g/m"));
+ UNITS_DENSITY_LINE.addUnit(new GeneralUnit(1,"kg/m"));
+ UNITS_DENSITY_LINE.addUnit(new GeneralUnit(0.0930102,"oz/ft"));
+
+ UNITS_FORCE = new UnitGroup();
+ UNITS_FORCE.addUnit(new GeneralUnit(1,"N"));
+ UNITS_FORCE.addUnit(new GeneralUnit(4.448222,"lbf"));
+ UNITS_FORCE.addUnit(new GeneralUnit(9.80665,"kgf"));
+
+ UNITS_IMPULSE = new UnitGroup();
+ UNITS_IMPULSE.addUnit(new GeneralUnit(1,"Ns"));
+ UNITS_IMPULSE.addUnit(new GeneralUnit(4.448222, "lbf\u00b7s"));
+
+ UNITS_TIME_STEP = new UnitGroup();
+ UNITS_TIME_STEP.addUnit(new FixedPrecisionUnit("ms", 1, 0.001));
+ UNITS_TIME_STEP.addUnit(new FixedPrecisionUnit("s", 0.01));
+ UNITS_TIME_STEP.setDefaultUnit(1);
+
+ UNITS_SHORT_TIME = new UnitGroup();
+ UNITS_SHORT_TIME.addUnit(new GeneralUnit(1,"s"));
+
+ UNITS_FLIGHT_TIME = new UnitGroup();
+ UNITS_FLIGHT_TIME.addUnit(new GeneralUnit(1,"s"));
+ UNITS_FLIGHT_TIME.addUnit(new GeneralUnit(60,"min"));
+
+ UNITS_ROLL = new UnitGroup();
+ UNITS_ROLL.addUnit(new GeneralUnit(1, "rad/s"));
+ UNITS_ROLL.addUnit(new GeneralUnit(2*Math.PI, "r/s"));
+ UNITS_ROLL.addUnit(new GeneralUnit(2*Math.PI/60, "rpm"));
+ UNITS_ROLL.setDefaultUnit(1);
+
+ UNITS_TEMPERATURE = new UnitGroup();
+ UNITS_TEMPERATURE.addUnit(new FixedPrecisionUnit("K", 1));
+ UNITS_TEMPERATURE.addUnit(new TemperatureUnit(1, 273.15, "\u00b0C"));
+ UNITS_TEMPERATURE.addUnit(new TemperatureUnit(5.0/9.0, 459.67, "\u00b0F"));
+ UNITS_TEMPERATURE.setDefaultUnit(1);
+
+ UNITS_PRESSURE = new UnitGroup();
+ UNITS_PRESSURE.addUnit(new FixedPrecisionUnit("mbar", 1, 1.0e2));
+ UNITS_PRESSURE.addUnit(new FixedPrecisionUnit("bar", 0.001, 1.0e5));
+ UNITS_PRESSURE.addUnit(new FixedPrecisionUnit("atm", 0.001, 1.01325e5));
+ UNITS_PRESSURE.addUnit(new GeneralUnit(133.322, "mmHg"));
+ UNITS_PRESSURE.addUnit(new GeneralUnit(3386.389, "inHg"));
+ UNITS_PRESSURE.addUnit(new GeneralUnit(6894.757, "psi"));
+ UNITS_PRESSURE.addUnit(new GeneralUnit(1, "Pa"));
+
+ UNITS_RELATIVE = new UnitGroup();
+ UNITS_RELATIVE.addUnit(new FixedPrecisionUnit("\u200b", 0.01));
+ UNITS_RELATIVE.addUnit(new FixedPrecisionUnit("%", 1, 0.01));
+ UNITS_RELATIVE.setDefaultUnit(1);
+
+
+ UNITS_ROUGHNESS = new UnitGroup();
+ UNITS_ROUGHNESS.addUnit(new GeneralUnit(0.000001, "\u03bcm"));
+ UNITS_ROUGHNESS.addUnit(new GeneralUnit(0.0000254, "mil"));
+
+
+ UNITS_COEFFICIENT = new UnitGroup();
+ UNITS_COEFFICIENT.addUnit(new FixedPrecisionUnit("\u200b", 0.01)); // zero-width space
+
+
+ HashMap<String,UnitGroup> map = new HashMap<String,UnitGroup>();
+ map.put("NONE", UNITS_NONE);
+ map.put("LENGTH", UNITS_LENGTH);
+ map.put("MOTOR_DIMENSIONS", UNITS_MOTOR_DIMENSIONS);
+ map.put("DISTANCE", UNITS_DISTANCE);
+ map.put("VELOCITY", UNITS_VELOCITY);
+ map.put("ACCELERATION", UNITS_ACCELERATION);
+ map.put("AREA", UNITS_AREA);
+ map.put("STABILITY", UNITS_STABILITY);
+ map.put("MASS", UNITS_MASS);
+ map.put("ANGLE", UNITS_ANGLE);
+ map.put("DENSITY_BULK", UNITS_DENSITY_BULK);
+ map.put("DENSITY_SURFACE", UNITS_DENSITY_SURFACE);
+ map.put("DENSITY_LINE", UNITS_DENSITY_LINE);
+ map.put("FORCE", UNITS_FORCE);
+ map.put("IMPULSE", UNITS_IMPULSE);
+ map.put("TIME_STEP", UNITS_TIME_STEP);
+ map.put("SHORT_TIME", UNITS_SHORT_TIME);
+ map.put("FLIGHT_TIME", UNITS_FLIGHT_TIME);
+ map.put("ROLL", UNITS_ROLL);
+ map.put("TEMPERATURE", UNITS_TEMPERATURE);
+ map.put("PRESSURE", UNITS_PRESSURE);
+ map.put("RELATIVE", UNITS_RELATIVE);
+ map.put("ROUGHNESS", UNITS_ROUGHNESS);
+ map.put("COEFFICIENT", UNITS_COEFFICIENT);
+
+ UNITS = Collections.unmodifiableMap(map);
+ }
+
+ public static void setDefaultMetricUnits() {
+ UNITS_LENGTH.setDefaultUnit("cm");
+ UNITS_MOTOR_DIMENSIONS.setDefaultUnit("mm");
+ UNITS_DISTANCE.setDefaultUnit("m");
+ UNITS_AREA.setDefaultUnit("cm\u00b2");
+ UNITS_STABILITY.setDefaultUnit("cal");
+ UNITS_VELOCITY.setDefaultUnit("m/s");
+ UNITS_ACCELERATION.setDefaultUnit("m/s\u00b2");
+ UNITS_MASS.setDefaultUnit("g");
+ UNITS_ANGLE.setDefaultUnit(0);
+ UNITS_DENSITY_BULK.setDefaultUnit("g/cm\u00b3");
+ UNITS_DENSITY_SURFACE.setDefaultUnit("g/m\u00b2");
+ UNITS_DENSITY_LINE.setDefaultUnit("g/m");
+ UNITS_FORCE.setDefaultUnit("N");
+ UNITS_IMPULSE.setDefaultUnit("Ns");
+ UNITS_TIME_STEP.setDefaultUnit("s");
+ UNITS_FLIGHT_TIME.setDefaultUnit("s");
+ UNITS_ROLL.setDefaultUnit("r/s");
+ UNITS_TEMPERATURE.setDefaultUnit(1);
+ UNITS_PRESSURE.setDefaultUnit("mbar");
+ UNITS_RELATIVE.setDefaultUnit("%");
+ UNITS_ROUGHNESS.setDefaultUnit("\u03bcm");
+ }
+
+ public static void setDefaultImperialUnits() {
+ UNITS_LENGTH.setDefaultUnit("in");
+ UNITS_MOTOR_DIMENSIONS.setDefaultUnit("in");
+ UNITS_DISTANCE.setDefaultUnit("ft");
+ UNITS_AREA.setDefaultUnit("in\u00b2");
+ UNITS_STABILITY.setDefaultUnit("cal");
+ UNITS_VELOCITY.setDefaultUnit("ft/s");
+ UNITS_ACCELERATION.setDefaultUnit("ft/s\u00b2");
+ UNITS_MASS.setDefaultUnit("oz");
+ UNITS_ANGLE.setDefaultUnit(0);
+ UNITS_DENSITY_BULK.setDefaultUnit("oz/in\u00b3");
+ UNITS_DENSITY_SURFACE.setDefaultUnit("oz/ft\u00b2");
+ UNITS_DENSITY_LINE.setDefaultUnit("oz/ft");
+ UNITS_FORCE.setDefaultUnit("N");
+ UNITS_IMPULSE.setDefaultUnit("Ns");
+ UNITS_TIME_STEP.setDefaultUnit("s");
+ UNITS_FLIGHT_TIME.setDefaultUnit("s");
+ UNITS_ROLL.setDefaultUnit("r/s");
+ UNITS_TEMPERATURE.setDefaultUnit(2);
+ UNITS_PRESSURE.setDefaultUnit("mbar");
+ UNITS_RELATIVE.setDefaultUnit("%");
+ UNITS_ROUGHNESS.setDefaultUnit("mil");
+ }
+
+
+
+ public static UnitGroup stabilityUnits(Rocket rocket) {
+ return new StabilityUnitGroup(rocket);
+ }
+
+
+ public static UnitGroup stabilityUnits(Configuration config) {
+ return new StabilityUnitGroup(config);
+ }
+
+
+ //////////////////////////////////////////////////////
+
+
+ private ArrayList<Unit> units = new ArrayList<Unit>();
+ private int defaultUnit = 0;
+
+ public int getUnitCount() {
+ return units.size();
+ }
+
+ public Unit getDefaultUnit() {
+ return units.get(defaultUnit);
+ }
+
+ public int getDefaultUnitIndex() {
+ return defaultUnit;
+ }
+
+ public void setDefaultUnit(int n) {
+ if (n<0 || n>=units.size()) {
+ throw new IllegalArgumentException("index out of range: "+n);
+ }
+ defaultUnit = n;
+ }
+
+ /**
+ * Set the default unit based on the unit name. Does nothing if the name
+ * does not match any of the units.
+ *
+ * @param name the unit name (<code>null</code> ok).
+ * @return <code>true</code> if the the default was set,
+ * <code>false</code> if a matching unit was not found.
+ */
+ public boolean setDefaultUnit(String name) {
+ if (name == null)
+ return false;
+
+ for (int i=0; i < units.size(); i++) {
+ if (name.equals(units.get(i).getUnit())) {
+ setDefaultUnit(i);
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ public Unit getUnit(int n) {
+ return units.get(n);
+ }
+
+ public int getUnitIndex(Unit u) {
+ return units.indexOf(u);
+ }
+
+ public void addUnit(Unit u) {
+ units.add(u);
+ }
+
+ public void addUnit(int n, Unit u) {
+ units.add(n,u);
+ }
+
+ public void removeUnit(int n) {
+ units.remove(n);
+ }
+
+ public boolean contains(Unit u) {
+ return units.contains(u);
+ }
+
+ public Unit[] getUnits() {
+ return units.toArray(new Unit[0]);
+ }
+
+
+ /**
+ * Return the value formatted by the default unit of this group.
+ * It is the same as calling <code>getDefaultUnit().toString(value)</code>.
+ *
+ * @param value the SI value to format.
+ * @return the formatted string.
+ * @see Unit#toString(double)
+ */
+ public String toString(double value) {
+ return this.getDefaultUnit().toString(value);
+ }
+
+
+ /**
+ * Return the value formatted by the default unit of this group including the unit.
+ * It is the same as calling <code>getDefaultUnit().toStringUnit(value)</code>.
+ *
+ * @param value the SI value to format.
+ * @return the formatted string.
+ * @see Unit#toStringUnit(double)
+ */
+ public String toStringUnit(double value) {
+ return this.getDefaultUnit().toStringUnit(value);
+ }
+
+
+ private static final Pattern STRING_PATTERN = Pattern.compile("^\\s*([0-9.,-]+)(.*?)$");
+ /**
+ * Converts a string into an SI value. If the string has one of the units in this
+ * group appended to it, that unit will be used in conversion. Otherwise the default
+ * unit will be used. If an unknown unit is specified or the value does not parse
+ * with <code>Double.parseDouble</code> then a <code>NumberFormatException</code>
+ * is thrown.
+ * <p>
+ * This method is applicable only for simple units without e.g. powers.
+ *
+ * @param str the string to parse.
+ * @return the SI value.
+ * @throws NumberFormatException if the string cannot be parsed.
+ */
+ public double fromString(String str) {
+ Matcher matcher = STRING_PATTERN.matcher(str);
+
+ if (!matcher.matches()) {
+ throw new NumberFormatException("string did not match required pattern");
+ }
+
+ double value = Double.parseDouble(matcher.group(1));
+ String unit = matcher.group(2).trim();
+
+ if (unit.equals("")) {
+ value = this.getDefaultUnit().fromUnit(value);
+ } else {
+ int i;
+ for (i=0; i < units.size(); i++) {
+ Unit u = units.get(i);
+ if (unit.equalsIgnoreCase(u.getUnit())) {
+ value = u.fromUnit(value);
+ break;
+ }
+ }
+ if (i >= units.size()) {
+ throw new NumberFormatException("unknown unit "+unit);
+ }
+ }
+
+ return value;
+ }
+
+
+ ///////////////////////////
+
+
+ /**
+ * A private class that switches the CaliberUnit to a rocket-specific CaliberUnit.
+ * All other methods are passed through to UNITS_STABILITY.
+ */
+ private static class StabilityUnitGroup extends UnitGroup {
+
+ private final CaliberUnit caliberUnit;
+
+
+ public StabilityUnitGroup(Rocket rocket) {
+ caliberUnit = new CaliberUnit(rocket);
+ }
+
+ public StabilityUnitGroup(Configuration config) {
+ caliberUnit = new CaliberUnit(config);
+ }
+
+
+ //// Modify CaliberUnit to use local variable
+
+ @Override
+ public Unit getDefaultUnit() {
+ return getUnit(UNITS_STABILITY.getDefaultUnitIndex());
+ }
+
+ @Override
+ public Unit getUnit(int n) {
+ Unit u = UNITS_STABILITY.getUnit(n);
+ if (u instanceof CaliberUnit) {
+ return caliberUnit;
+ }
+ return u;
+ }
+
+ @Override
+ public int getUnitIndex(Unit u) {
+ if (u instanceof CaliberUnit) {
+ for (int i=0; i < UNITS_STABILITY.getUnitCount(); i++) {
+ if (UNITS_STABILITY.getUnit(i) instanceof CaliberUnit)
+ return i;
+ }
+ }
+ return UNITS_STABILITY.getUnitIndex(u);
+ }
+
+
+
+ //// Pass on to UNITS_STABILITY
+
+ @Override
+ public int getDefaultUnitIndex() {
+ return UNITS_STABILITY.getDefaultUnitIndex();
+ }
+
+ @Override
+ public void setDefaultUnit(int n) {
+ UNITS_STABILITY.setDefaultUnit(n);
+ }
+
+ @Override
+ public int getUnitCount() {
+ return UNITS_STABILITY.getUnitCount();
+ }
+
+
+ //// Unsupported methods
+
+ @Override
+ public void addUnit(int n, Unit u) {
+ throw new UnsupportedOperationException("StabilityUnitGroup must not be modified");
+ }
+
+ @Override
+ public void addUnit(Unit u) {
+ throw new UnsupportedOperationException("StabilityUnitGroup must not be modified");
+ }
+
+ @Override
+ public void removeUnit(int n) {
+ throw new UnsupportedOperationException("StabilityUnitGroup must not be modified");
+ }
+ }
+}
--- /dev/null
+package net.sf.openrocket.util;
+
+import static net.sf.openrocket.aerodynamics.AtmosphericConditions.GAMMA;
+import static net.sf.openrocket.aerodynamics.AtmosphericConditions.R;
+
+import java.io.File;
+import java.io.PrintStream;
+import java.util.Arrays;
+
+import net.sf.openrocket.aerodynamics.AerodynamicCalculator;
+import net.sf.openrocket.aerodynamics.AerodynamicForces;
+import net.sf.openrocket.aerodynamics.AtmosphericConditions;
+import net.sf.openrocket.aerodynamics.BarrowmanCalculator;
+import net.sf.openrocket.aerodynamics.ExactAtmosphericConditions;
+import net.sf.openrocket.aerodynamics.FlightConditions;
+import net.sf.openrocket.document.OpenRocketDocument;
+import net.sf.openrocket.file.GeneralRocketLoader;
+import net.sf.openrocket.file.RocketLoadException;
+import net.sf.openrocket.file.RocketLoader;
+import net.sf.openrocket.rocketcomponent.Configuration;
+
+public class Analysis {
+
+ private static final double MACH_MIN = 0.01;
+ private static final double MACH_MAX = 5.00001;
+ private static final double MACH_STEP = 0.02;
+
+ private static final double AOA_MACH = 0.6;
+ private static final double AOA_MIN = 0;
+ private static final double AOA_MAX = 15.00001*Math.PI/180;
+ private static final double AOA_STEP = 0.5*Math.PI/180;
+
+ private static final double REYNOLDS = 9.8e6;
+ private static final double STAG_TEMP = 330;
+
+
+ private final RocketLoader loader = new GeneralRocketLoader();
+ private final AerodynamicCalculator calculator = new BarrowmanCalculator();
+
+ private final FlightConditions conditions;
+ private final double length;
+
+ private final Configuration config;
+
+ private final AtmosphericConditions atmosphere;
+
+
+
+ private Analysis(String filename) throws RocketLoadException {
+
+ OpenRocketDocument doc = loader.load(new File(filename));
+ config = doc.getRocket().getDefaultConfiguration();
+
+ calculator.setConfiguration(config);
+
+ conditions = new FlightConditions(config);
+ System.out.println("Children: " + Arrays.toString(config.getRocket().getChildren()));
+ System.out.println("Children: " + Arrays.toString(config.getRocket().getChild(0).getChildren()));
+ length = config.getLength();
+ System.out.println("Rocket length: " + (length*1000)+"mm");
+
+ atmosphere = new ExactAtmosphericConditions();
+
+ }
+
+
+ private double computeVelocityAndAtmosphere(double mach, double reynolds, double stagTemp) {
+ final double temperature;
+ final double pressure;
+
+
+ temperature = stagTemp / (1 + (GAMMA-1)/2 * MathUtil.pow2(mach));
+
+ // Speed of sound
+ double c = 331.3 * Math.sqrt(1 + (temperature - 273.15)/273.15);
+
+ // Free-stream velocity
+ double v0 = c * mach;
+
+// kin.visc. = (3.7291e-06 + 4.9944e-08 * temperature) / density
+ pressure = reynolds * (3.7291e-06 + 4.9944e-08 * temperature) * R * temperature /
+ (v0 * length);
+
+ atmosphere.pressure = pressure;
+ atmosphere.temperature = temperature;
+ conditions.setAtmosphericConditions(atmosphere);
+ conditions.setVelocity(v0);
+
+ if (Math.abs(conditions.getMach() - mach) > 0.001) {
+ System.err.println("Computed mach: "+conditions.getMach() + " requested "+mach);
+// System.exit(1);
+ }
+
+ return v0;
+ }
+
+
+
+ private void computeVsMach(PrintStream stream) {
+
+ conditions.setAOA(0);
+ conditions.setTheta(45*Math.PI/180);
+ stream.println("% Mach, Caxial, CP, , CNa, Croll");
+
+ for (double mach = MACH_MIN; mach <= MACH_MAX; mach += MACH_STEP) {
+
+ computeVelocityAndAtmosphere(mach, REYNOLDS, STAG_TEMP);
+// conditions.setMach(mach);
+
+
+ AerodynamicForces forces = calculator.getAerodynamicForces(0, conditions, null);
+
+
+ double Re = conditions.getVelocity() *
+ calculator.getConfiguration().getLength() /
+ conditions.getAtmosphericConditions().getKinematicViscosity();
+ if (Math.abs(Re - REYNOLDS) > 1) {
+ throw new RuntimeException("Re="+Re);
+ }
+ stream.printf("%f, %f, %f, %f, %f\n", mach, forces.Caxial, forces.cp.x, forces.CNa,
+ forces.Croll);
+ }
+
+ }
+
+
+
+ private void computeVsAOA(PrintStream stream, double thetaDeg) {
+
+ computeVelocityAndAtmosphere(AOA_MACH, REYNOLDS, STAG_TEMP);
+ conditions.setTheta(thetaDeg * Math.PI/180);
+ stream.println("% AOA, CP, CN, Cm at theta = "+thetaDeg);
+
+ for (double aoa = AOA_MIN; aoa <= AOA_MAX; aoa += AOA_STEP) {
+
+ conditions.setAOA(aoa);
+ AerodynamicForces forces = calculator.getAerodynamicForces(0, conditions, null);
+
+
+ double Re = conditions.getVelocity() *
+ calculator.getConfiguration().getLength() /
+ conditions.getAtmosphericConditions().getKinematicViscosity();
+ if (Math.abs(Re - REYNOLDS) > 1) {
+ throw new RuntimeException("Re="+Re);
+ }
+ stream.printf("%f, %f, %f, %f\n", aoa*180/Math.PI, forces.cp.x, forces.CN, forces.Cm);
+ }
+
+ }
+
+
+
+
+ public static void main(String arg[]) throws Exception {
+
+ if (arg.length != 2) {
+ System.err.println("Arguments: <rocket file> <output prefix>");
+ System.exit(1);
+ }
+
+ Analysis a = new Analysis(arg[0]);
+ final String prefix = arg[1];
+
+
+ String name;
+ double v0 = a.computeVelocityAndAtmosphere(0.6, 9.8e6, 322);
+ System.out.printf("Sanity test: mach = %.1f v=%.1f temp=%.1f pres=%.0f c=%.1f " +
+ "ref.length=%.1fmm\n",
+ a.conditions.getMach(), v0, a.atmosphere.temperature, a.atmosphere.pressure,
+ a.atmosphere.getMachSpeed(), a.conditions.getRefLength()*1000);
+ System.out.println();
+
+
+ // CA, CP, Croll vs. Mach at AOA=0
+ name = prefix + "-CA-CP-CNa-Croll-vs-Mach.csv";
+ System.out.println("Computing CA, CP, CNa, Croll vs. Mach to file "+name);
+ a.computeVsMach(new PrintStream(name));
+
+
+ // CN & Cm vs. AOA at M=0.6
+ name = prefix + "-CP-CN-Cm-vs-AOA-0.csv";
+ System.out.println("Computing CP, CN, Cm vs. AOA at theta=0 to file "+name);
+ a.computeVsAOA(new PrintStream(name), 0);
+
+ // CN & Cm vs. AOA at M=0.6
+ name = prefix + "-CP-CN-Cm-vs-AOA-22.5.csv";
+ System.out.println("Computing CP, CN, Cm vs. AOA at theta=22.5 to file "+name);
+ a.computeVsAOA(new PrintStream(name), 0);
+
+ // CN & Cm vs. AOA at M=0.6
+ name = prefix + "-CP-CN-Cm-vs-AOA-45.csv";
+ System.out.println("Computing CP, CN, Cm vs. AOA at theta=45 to file "+name);
+ a.computeVsAOA(new PrintStream(name), 0);
+
+
+ System.out.println("Done.");
+ }
+
+
+
+
+
+}
--- /dev/null
+package net.sf.openrocket.util;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Random;
+
+public class Base64 {
+
+ public static final int DEFAULT_CHARS_PER_LINE = 72;
+
+ private static final char[] ALPHABET = new char[] {
+ 'A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P',
+ 'Q','R','S','T','U','V','W','X','Y','Z','a','b','c','d','e','f',
+ 'g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v',
+ 'w','x','y','z','0','1','2','3','4','5','6','7','8','9','+','/'
+ };
+ private static final char PAD = '=';
+
+ private static final Map<Character,Integer> REVERSE = new HashMap<Character,Integer>();
+ static {
+ for (int i=0; i<64; i++) {
+ REVERSE.put(ALPHABET[i], i);
+ }
+ REVERSE.put('-', 62);
+ REVERSE.put('_', 63);
+ REVERSE.put(PAD, 0);
+ }
+
+
+ public static String encode(byte[] data) {
+ return encode(data, DEFAULT_CHARS_PER_LINE);
+ }
+
+ public static String encode(byte[] data, int maxColumn) {
+ StringBuilder builder = new StringBuilder();
+ int column = 0;
+
+ for (int position=0; position < data.length; position+=3) {
+ if (column+4 > maxColumn) {
+ builder.append('\n');
+ column = 0;
+ }
+ builder.append(encodeGroup(data, position));
+ column += 4;
+ }
+ builder.append('\n');
+ return builder.toString();
+ }
+
+
+
+
+ public static byte[] decode(String data) {
+ byte[] array = new byte[data.length()*3/4];
+ char[] block = new char[4];
+ int length = 0;
+
+ for (int position=0; position < data.length(); ) {
+ int p;
+ for (p=0; p<4 && position < data.length(); position++) {
+ char c = data.charAt(position);
+ if (!Character.isWhitespace(c)) {
+ block[p] = c;
+ p++;
+ }
+ }
+
+ if (p==0)
+ break;
+ if (p!=4) {
+ throw new IllegalArgumentException("Data ended when decoding Base64, p="+p);
+ }
+
+ int l = decodeGroup(block, array, length);
+ length += l;
+ if (l < 3)
+ break;
+ }
+ return Arrays.copyOf(array, length);
+ }
+
+
+ //// Helper methods
+
+
+ /**
+ * Encode three bytes of data into four characters.
+ */
+ private static char[] encodeGroup(byte[] data, int position) {
+ char[] c = new char[] { '=','=','=','=' };
+ int b1=0, b2=0, b3=0;
+ int length = data.length - position;
+
+ if (length == 0)
+ return c;
+
+ if (length >= 1) {
+ b1 = ((int)data[position])&0xFF;
+ }
+ if (length >= 2) {
+ b2 = ((int)data[position+1])&0xFF;
+ }
+ if (length >= 3) {
+ b3 = ((int)data[position+2])&0xFF;
+ }
+
+ c[0] = ALPHABET[b1>>2];
+ c[1] = ALPHABET[(b1 & 3)<<4 | (b2>>4)];
+ if (length == 1)
+ return c;
+ c[2] = ALPHABET[(b2 & 15)<<2 | (b3>>6)];
+ if (length == 2)
+ return c;
+ c[3] = ALPHABET[b3 & 0x3f];
+ return c;
+ }
+
+
+ /**
+ * Decode four chars from data into 0-3 bytes of data starting at position in array.
+ * @return the number of bytes decoded.
+ */
+ private static int decodeGroup(char[] data, byte[] array, int position) {
+ int b1, b2, b3, b4;
+
+ try {
+ b1 = REVERSE.get(data[0]);
+ b2 = REVERSE.get(data[1]);
+ b3 = REVERSE.get(data[2]);
+ b4 = REVERSE.get(data[3]);
+ } catch (NullPointerException e) {
+ // If auto-boxing fails
+ throw new IllegalArgumentException("Illegal characters in the sequence to be "+
+ "decoded: "+Arrays.toString(data));
+ }
+
+ array[position] = (byte)((b1 << 2) | (b2 >> 4));
+ array[position+1] = (byte)((b2 << 4) | (b3 >> 2));
+ array[position+2] = (byte)((b3 << 6) | (b4));
+
+ // Check the amount of data decoded
+ if (data[0] == PAD)
+ return 0;
+ if (data[1] == PAD) {
+ throw new IllegalArgumentException("Illegal character padding in sequence to be "+
+ "decoded: "+Arrays.toString(data));
+ }
+ if (data[2] == PAD)
+ return 1;
+ if (data[3] == PAD)
+ return 2;
+
+ return 3;
+ }
+
+
+
+ public static void main(String[] arg) {
+ Random rnd = new Random();
+
+ for (int round=0; round < 1000; round++) {
+ int n = rnd.nextInt(1000);
+ n = 100000;
+
+ byte[] array = new byte[n];
+ rnd.nextBytes(array);
+
+ String encoded = encode(array);
+
+ System.out.println(encoded);
+ System.exit(0);
+// for (int i=0; i<1000; i++) {
+// int pos = rnd.nextInt(encoded.length());
+// String s1 = encoded.substring(0, pos);
+// String s2 = encoded.substring(pos);
+// switch (rnd.nextInt(15)) {
+// case 0:
+// encoded = s1 + " " + s2;
+// break;
+// case 1:
+// encoded = s1 + "\u0009" + s2;
+// break;
+// case 2:
+// encoded = s1 + "\n" + s2;
+// break;
+// case 3:
+// encoded = s1 + "\u000B" + s2;
+// break;
+// case 4:
+// encoded = s1 + "\r" + s2;
+// break;
+// case 5:
+// encoded = s1 + "\u000C" + s2;
+// break;
+// case 6:
+// encoded = s1 + "\u001C" + s2;
+// break;
+// }
+// }
+
+ byte[] decoded = null;
+ try {
+ decoded = decode(encoded);
+ } catch (IllegalArgumentException e) {
+ e.printStackTrace();
+ System.err.println("Bad data:\n"+encoded);
+ System.exit(1);
+ }
+
+ if (!Arrays.equals(array, decoded)) {
+ System.err.println("Data differs! n="+n);
+ System.exit(1);
+ }
+ System.out.println("n="+n+" ok!");
+ }
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.util;
+
+import javax.swing.event.ChangeListener;
+
+/**
+ * An interface defining an object firing ChangeEvents. Why isn't this included in the Java API??
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public interface ChangeSource {
+
+ public void addChangeListener(ChangeListener listener);
+ public void removeChangeListener(ChangeListener listener);
+
+}
--- /dev/null
+package net.sf.openrocket.util;
+
+import java.io.Serializable;
+
+/**
+ * An immutable class of weighted coordinates. The weights are non-negative.
+ *
+ * Can also be used as non-weighted coordinates with weight=0.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public final class Coordinate implements Serializable {
+ public static final Coordinate NUL = new Coordinate(0,0,0,0);
+ public static final Coordinate NaN = new Coordinate(Double.NaN,Double.NaN,
+ Double.NaN,Double.NaN);
+ public static final double COMPARISON_DELTA = 0.000001;
+ public final double x,y,z;
+ public final double weight;
+
+
+ private double length = -1; /* Cached when calculated */
+
+
+ /* Count and report the number of times a Coordinate is constructed: */
+// private static int count=0;
+// {
+// count++;
+// if ((count % 1000) == 0) {
+// System.err.println("Coordinate instantiated "+count+" times");
+// }
+// }
+
+
+
+ public Coordinate() {
+ this(0,0,0,0);
+ }
+
+ public Coordinate(double x) {
+ this(x,0,0,0);
+ }
+
+ public Coordinate(double x, double y) {
+ this(x,y,0,0);
+ }
+
+ public Coordinate(double x, double y, double z) {
+ this(x,y,z,0);
+ }
+ public Coordinate(double x, double y, double z, double w) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.weight=w;
+ }
+
+
+ public boolean isWeighted() {
+ return (weight != 0);
+ }
+
+ public boolean isNaN() {
+ return Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(z) || Double.isNaN(weight);
+ }
+
+ public Coordinate setX(double x) {
+ return new Coordinate(x,this.y,this.z,this.weight);
+ }
+
+ public Coordinate setY(double y) {
+ return new Coordinate(this.x,y,this.z,this.weight);
+ }
+
+ public Coordinate setZ(double z) {
+ return new Coordinate(this.x,this.y,z,this.weight);
+ }
+
+ public Coordinate setWeight(double weight) {
+ return new Coordinate(this.x, this.y, this.z, weight);
+ }
+
+ public Coordinate setXYZ(Coordinate c) {
+ return new Coordinate(c.x, c.y, c.z, this.weight);
+ }
+
+
+ /**
+ * Add the coordinate and weight of two coordinates.
+ *
+ * @param other the other <code>Coordinate</code>
+ * @return the sum of the coordinates
+ */
+ public Coordinate add(Coordinate other) {
+ return new Coordinate(this.x+other.x, this.y+other.y, this.z+other.z,
+ this.weight+other.weight);
+ }
+
+ public Coordinate add(double x, double y, double z) {
+ return new Coordinate(this.x+x, this.y+y, this.z+z, this.weight);
+ }
+
+ public Coordinate add(double x, double y, double z, double weight) {
+ return new Coordinate(this.x+x, this.y+y, this.z+z, this.weight+weight);
+ }
+
+ /**
+ * Subtract a Coordinate from this Coordinate. The weight of the resulting Coordinate
+ * is the same as of this Coordinate, the weight of the argument is ignored.
+ *
+ * @param other Coordinate to subtract from this.
+ * @return The result
+ */
+ public Coordinate sub(Coordinate other) {
+ return new Coordinate(this.x-other.x, this.y-other.y, this.z-other.z, this.weight);
+ }
+
+ /**
+ * Subtract the specified values from this Coordinate. The weight of the result
+ * is the same as the weight of this Coordinate.
+ *
+ * @param x x value to subtract
+ * @param y y value to subtract
+ * @param z z value to subtract
+ * @return the result.
+ */
+ public Coordinate sub(double x, double y, double z) {
+ return new Coordinate(this.x - x, this.y - y, this.z - z, this.weight);
+ }
+
+
+ /**
+ * Multiply the <code>Coordinate</code> with a scalar. All coordinates and the
+ * weight are multiplied by the given scalar.
+
+ * @param m Factor to multiply by.
+ * @return The product.
+ */
+ public Coordinate multiply(double m) {
+ return new Coordinate(this.x*m, this.y*m, this.z*m, this.weight*m);
+ }
+
+ /**
+ * Dot product of two Coordinates, taken as vectors. Equal to
+ * x1*x2+y1*y2+z1*z2
+ * @param other Coordinate to take product with.
+ * @return The dot product.
+ */
+ public double dot(Coordinate other) {
+ return this.x*other.x + this.y*other.y + this.z*other.z;
+ }
+ /**
+ * Dot product of two Coordinates.
+ */
+ public static double dot(Coordinate v1, Coordinate v2) {
+ return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z;
+ }
+
+ /**
+ * Distance from the origin to the Coordinate.
+ */
+ public double length() {
+ if (length < 0) {
+ length = Math.sqrt(x*x+y*y+z*z);
+ }
+ return length;
+ }
+
+ /**
+ * Square of the distance from the origin to the Coordinate.
+ */
+ public double length2() {
+ return x*x+y*y+z*z;
+ }
+
+ /**
+ * Returns a new coordinate which has the same direction from the origin as this
+ * coordinate but is at a distance of one. If this coordinate is the origin,
+ * this method throws an <code>IllegalStateException</code>. The weight of the
+ * coordinate is unchanged.
+ *
+ * @return the coordinate normalized to distance one of the origin.
+ * @throws IllegalStateException if this coordinate is the origin.
+ */
+ public Coordinate normalize() {
+ double l = length();
+ if (l < 0.0000001) {
+ throw new IllegalStateException("Cannot normalize zero coordinate");
+ }
+ return new Coordinate(x/l, y/l, z/l, weight);
+ }
+
+
+
+
+ /**
+ * Weighted average of two coordinates. If either of the weights are positive,
+ * the result is the weighted average of the coordinates and the weight is the sum
+ * of the original weights. If the sum of the weights is zero (and especially if
+ * both of the weights are zero), the result is the unweighted average of the
+ * coordinates with weight zero.
+ * <p>
+ * If <code>other</code> is <code>null</code> then this <code>Coordinate</code> is
+ * returned.
+ */
+ public Coordinate average(Coordinate other) {
+ double x,y,z,w;
+
+ if (other == null)
+ return this;
+
+ w = this.weight + other.weight;
+ if (Math.abs(w) < MathUtil.pow2(MathUtil.EPSILON)) {
+ x = (this.x+other.x)/2;
+ y = (this.y+other.y)/2;
+ z = (this.z+other.z)/2;
+ w = 0;
+ } else {
+ x = (this.x*this.weight + other.x*other.weight)/w;
+ y = (this.y*this.weight + other.y*other.weight)/w;
+ z = (this.z*this.weight + other.z*other.weight)/w;
+ }
+ return new Coordinate(x,y,z,w);
+ }
+
+
+ /**
+ * Tests whether the coordinates (not weight!) are the same.
+ *
+ * Compares only the (x,y,z) coordinates, NOT the weight. Coordinate comparison is
+ * done to the precision of COMPARISON_DELTA.
+ *
+ * @param other Coordinate to compare to.
+ * @return true if the coordinates are equal
+ */
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof Coordinate))
+ return false;
+
+ final Coordinate c = (Coordinate)other;
+ return (MathUtil.equals(this.x, c.x) &&
+ MathUtil.equals(this.y, c.y) &&
+ MathUtil.equals(this.z, c.z));
+ }
+
+ /**
+ * Hash code method compatible with {@link #equals(Object)}.
+ */
+ @Override
+ public int hashCode() {
+ return (int)((x+y+z)*100000);
+ }
+
+
+ @Override
+ public String toString() {
+ if (isWeighted())
+ return String.format("(%.3f,%.3f,%.3f,w=%.3f)", x,y,z,weight);
+ else
+ return String.format("(%.3f,%.3f,%.3f)", x,y,z);
+ }
+
+
+
+ public static void main(String[] arg) {
+ double a=1.2;
+ double x;
+ Coordinate c;
+ long t1, t2;
+
+ x = 0;
+ t1 = System.nanoTime();
+ for (int i=0; i < 100000000; i++) {
+ x = x + a;
+ }
+ t2 = System.nanoTime();
+ System.out.println("Value: "+x);
+ System.out.println("Plain addition: "+ ((t2-t1+500000)/1000000) + " ms");
+
+ c = Coordinate.NUL;
+ t1 = System.nanoTime();
+ for (int i=0; i < 100000000; i++) {
+ c = c.add(a,0,0);
+ }
+ t2 = System.nanoTime();
+ System.out.println("Value: "+c.x);
+ System.out.println("Coordinate addition: "+ ((t2-t1+500000)/1000000) + " ms");
+
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.util;
+
+import java.awt.Component;
+import java.awt.KeyboardFocusManager;
+import java.awt.Window;
+import java.awt.event.ActionEvent;
+import java.awt.event.KeyEvent;
+import java.awt.event.WindowEvent;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.swing.AbstractAction;
+import javax.swing.Action;
+import javax.swing.JButton;
+import javax.swing.JComponent;
+import javax.swing.JDialog;
+import javax.swing.JRootPane;
+import javax.swing.KeyStroke;
+import javax.swing.RootPaneContainer;
+import javax.swing.SwingUtilities;
+
+public class GUIUtil {
+
+ private static final KeyStroke ESCAPE = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);
+ private static final String CLOSE_ACTION_KEY = "escape:WINDOW_CLOSING";
+
+
+ /**
+ * Add the correct action to close a JDialog when the ESC key is pressed.
+ * The dialog is closed by sending is a WINDOW_CLOSING event.
+ *
+ * @param dialog the dialog for which to install the action.
+ */
+ public static void installEscapeCloseOperation(final JDialog dialog) {
+ Action dispatchClosing = new AbstractAction() {
+ public void actionPerformed(ActionEvent event) {
+ dialog.dispatchEvent(new WindowEvent(dialog, WindowEvent.WINDOW_CLOSING));
+ }
+ };
+ JRootPane root = dialog.getRootPane();
+ root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(ESCAPE, CLOSE_ACTION_KEY);
+ root.getActionMap().put(CLOSE_ACTION_KEY, dispatchClosing);
+ }
+
+
+ /**
+ * Set the given button as the default button of the frame/dialog it is in. The button
+ * must be first attached to the window component hierarchy.
+ *
+ * @param button the button to set as the default button.
+ */
+ public static void setDefaultButton(JButton button) {
+ Window w = SwingUtilities.windowForComponent(button);
+ if (w == null) {
+ throw new IllegalArgumentException("Attach button to a window first.");
+ }
+ if (!(w instanceof RootPaneContainer)) {
+ throw new IllegalArgumentException("Button not attached to RootPaneContainer, w="+w);
+ }
+ ((RootPaneContainer)w).getRootPane().setDefaultButton(button);
+ }
+
+
+
+ /**
+ * Change the behavior of a component so that TAB and Shift-TAB cycles the focus of
+ * the components. This is necessary for e.g. <code>JTextArea</code>.
+ *
+ * @param c the component to modify
+ */
+ public static void setTabToFocusing(Component c) {
+ Set<KeyStroke> strokes = new HashSet<KeyStroke>(Arrays.asList(KeyStroke.getKeyStroke("pressed TAB")));
+ c.setFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS, strokes);
+ strokes = new HashSet<KeyStroke>(Arrays.asList(KeyStroke.getKeyStroke("shift pressed TAB")));
+ c.setFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS, strokes);
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.util;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.swing.Icon;
+import javax.swing.ImageIcon;
+
+import net.sf.openrocket.document.Simulation;
+
+
+public class Icons {
+
+ /**
+ * Icons used for showing the status of a simulation (up to date, out of date, etc).
+ */
+ public static final Map<Simulation.Status, Icon> SIMULATION_STATUS_ICON_MAP;
+ static {
+ HashMap<Simulation.Status, Icon> map = new HashMap<Simulation.Status, Icon>();
+ map.put(Simulation.Status.NOT_SIMULATED, new ImageIcon("pix/spheres/gray-16x16.png", "Not simulated"));
+ map.put(Simulation.Status.UPTODATE, new ImageIcon("pix/spheres/green-16x16.png", "Up to date"));
+ map.put(Simulation.Status.LOADED, new ImageIcon("pix/spheres/yellow-16x16.png", "Loaded from file"));
+ map.put(Simulation.Status.OUTDATED, new ImageIcon("pix/spheres/red-16x16.png", "Out-of-date"));
+ map.put(Simulation.Status.EXTERNAL, new ImageIcon("pix/spheres/blue-16x16.png", "Imported data"));
+ SIMULATION_STATUS_ICON_MAP = Collections.unmodifiableMap(map);
+ }
+
+ public static final Icon SIMULATION_LISTENER_OK;
+ public static final Icon SIMULATION_LISTENER_ERROR;
+ static {
+ SIMULATION_LISTENER_OK = SIMULATION_STATUS_ICON_MAP.get(Simulation.Status.UPTODATE);
+ SIMULATION_LISTENER_ERROR = SIMULATION_STATUS_ICON_MAP.get(Simulation.Status.OUTDATED);
+ }
+
+
+ public static final Icon FILE_NEW = new ImageIcon(ClassLoader.getSystemResource("pix/icons/document-new.png"), "New document");
+ public static final Icon FILE_OPEN = new ImageIcon(ClassLoader.getSystemResource("pix/icons/document-open.png"), "Open document");
+ public static final Icon FILE_SAVE = new ImageIcon(ClassLoader.getSystemResource("pix/icons/document-save.png"), "Save document");
+ public static final Icon FILE_SAVE_AS = new ImageIcon(ClassLoader.getSystemResource("pix/icons/document-save-as.png"), "Save document as");
+ public static final Icon FILE_CLOSE = new ImageIcon(ClassLoader.getSystemResource("pix/icons/document-close.png"), "Close document");
+ public static final Icon FILE_QUIT = new ImageIcon(ClassLoader.getSystemResource("pix/icons/application-exit.png"), "Quit OpenRocket");
+
+ public static final Icon EDIT_UNDO = new ImageIcon(ClassLoader.getSystemResource("pix/icons/edit-undo.png"), "Undo");
+ public static final Icon EDIT_REDO = new ImageIcon(ClassLoader.getSystemResource("pix/icons/edit-redo.png"), "Redo");
+ public static final Icon EDIT_CUT = new ImageIcon(ClassLoader.getSystemResource("pix/icons/edit-cut.png"), "Cut");
+ public static final Icon EDIT_COPY = new ImageIcon(ClassLoader.getSystemResource("pix/icons/edit-copy.png"), "Copy");
+ public static final Icon EDIT_PASTE = new ImageIcon(ClassLoader.getSystemResource("pix/icons/edit-paste.png"), "Paste");
+ public static final Icon EDIT_DELETE = new ImageIcon(ClassLoader.getSystemResource("pix/icons/edit-delete.png"), "Delete");
+
+ public static final Icon ZOOM_IN = new ImageIcon(ClassLoader.getSystemResource("pix/icons/zoom-in.png"), "Zoom in");
+ public static final Icon ZOOM_OUT = new ImageIcon(ClassLoader.getSystemResource("pix/icons/zoom-out.png"), "Zoom out");
+
+ public static final Icon PREFERENCES = new ImageIcon(ClassLoader.getSystemResource("pix/icons/preferences.png"), "Preferences");
+
+}
--- /dev/null
+package net.sf.openrocket.util;
+
+import java.util.Arrays;
+
+/**
+ * An enumeration of line styles. The line styles are defined by an array of
+ * floats suitable for <code>BasicStroke</code>.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public enum LineStyle {
+ SOLID("Solid",new float[] { 10f, 0f }),
+ DASHED("Dashed",new float[] { 6f, 4f }),
+ DOTTED("Dotted",new float[] { 2f, 3f }),
+ DASHDOT("Dash-dotted",new float[] { 8f, 3f, 2f, 3f})
+ ;
+
+ private final String name;
+ private final float[] dashes;
+ LineStyle(String name, float[] dashes) {
+ this.name = name;
+ this.dashes = dashes;
+ }
+ public float[] getDashes() {
+ return Arrays.copyOf(dashes, dashes.length);
+ }
+ @Override
+ public String toString() {
+ return name;
+ }
+}
\ No newline at end of file
--- /dev/null
+package net.sf.openrocket.util;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.TreeMap;
+
+public class LinearInterpolator implements Cloneable {
+
+ private TreeMap<Double, Double> sortMap = new TreeMap<Double,Double>();
+
+ /**
+ * Construct a <code>LinearInterpolator</code> with no points. Some points must be
+ * added using {@link #addPoints(double[], double[])} before using the interpolator.
+ */
+ public LinearInterpolator() {
+ }
+
+ /**
+ * Construct a <code>LinearInterpolator</code> with the given points.
+ *
+ * @param x the x-coordinates of the points.
+ * @param y the y-coordinates of the points.
+ * @throws IllegalArgumentException if the lengths of <code>x</code> and <code>y</code>
+ * are not equal.
+ * @see #addPoints(double[], double[])
+ */
+ public LinearInterpolator(double[] x, double[] y) {
+ addPoints(x,y);
+ }
+
+
+ /**
+ * Add the point to the linear interpolation.
+ *
+ * @param x the x-coordinate of the point.
+ * @param y the y-coordinate of the point.
+ */
+ public void addPoint(double x, double y) {
+ sortMap.put(x, y);
+ }
+
+ /**
+ * Add the points to the linear interpolation.
+ *
+ * @param x the x-coordinates of the points.
+ * @param y the y-coordinates of the points.
+ * @throws IllegalArgumentException if the lengths of <code>x</code> and <code>y</code>
+ * are not equal.
+ */
+ public void addPoints(double[] x, double[] y) {
+ if (x.length != y.length) {
+ throw new IllegalArgumentException("Array lengths do not match, x="+x.length +
+ " y="+y.length);
+ }
+ for (int i=0; i < x.length; i++) {
+ sortMap.put(x[i],y[i]);
+ }
+ }
+
+
+
+ public double getValue(double x) {
+ Map.Entry<Double,Double> e1, e2;
+ double x1, x2;
+ double y1, y2;
+
+ e1 = sortMap.floorEntry(x);
+
+ if (e1 == null) {
+ // x smaller than any value in the set
+ e1 = sortMap.firstEntry();
+ if (e1 == null) {
+ throw new IllegalStateException("No points added yet to the interpolator.");
+ }
+ return e1.getValue();
+ }
+
+ x1 = e1.getKey();
+ e2 = sortMap.higherEntry(x1);
+
+ if (e2 == null) {
+ // x larger than any value in the set
+ return e1.getValue();
+ }
+
+ x2 = e2.getKey();
+ y1 = e1.getValue();
+ y2 = e2.getValue();
+
+ return (x - x1)/(x2-x1) * (y2-y1) + y1;
+ }
+
+
+ public double[] getXPoints() {
+ double[] x = new double[sortMap.size()];
+ Iterator<Double> iter = sortMap.keySet().iterator();
+ for (int i=0; iter.hasNext(); i++) {
+ x[i] = iter.next();
+ }
+ return x;
+ }
+
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public LinearInterpolator clone() {
+ try {
+ LinearInterpolator other = (LinearInterpolator)super.clone();
+ other.sortMap = (TreeMap<Double,Double>)this.sortMap.clone();
+ return other;
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException("CloneNotSupportedException?!",e);
+ }
+ }
+
+
+ public static void main(String[] args) {
+ LinearInterpolator interpolator = new LinearInterpolator(
+ new double[] {1, 1.5, 2, 4, 5},
+ new double[] {0, 1, 0, 2, 2}
+ );
+
+ for (double x=0; x < 6; x+=0.1) {
+ System.out.printf("%.1f: %.2f\n", x, interpolator.getValue(x));
+ }
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.util;
+
+public class MathUtil {
+ public static final double EPSILON = 0.00000001; // 10mm^3 in m^3
+
+ /**
+ * The square of x (x^2). On Sun's JRE using this method is as fast as typing x*x.
+ * @param x x
+ * @return x^2
+ */
+ public static double pow2(double x) {
+ return x*x;
+ }
+
+ /**
+ * The cube of x (x^3).
+ * @param x x
+ * @return x^3
+ */
+ public static double pow3(double x) {
+ return x*x*x;
+ }
+
+ public static double pow4(double x) {
+ return (x*x)*(x*x);
+ }
+
+ /**
+ * Clamps the value x to the range min - max.
+ * @param x Original value.
+ * @param min Minimum value to return.
+ * @param max Maximum value to return.
+ * @return The clamped value.
+ */
+ public static double clamp(double x, double min, double max) {
+ if (x < min)
+ return min;
+ if (x > max)
+ return max;
+ return x;
+ }
+
+ public static float clamp(float x, float min, float max) {
+ if (x < min)
+ return min;
+ if (x > max)
+ return max;
+ return x;
+ }
+
+ public static int clamp(int x, int min, int max) {
+ if (x < min)
+ return min;
+ if (x > max)
+ return max;
+ return x;
+ }
+
+
+ /**
+ * Maps a value from one value range to another.
+ *
+ * @param value the value to map.
+ * @param fromMin the minimum of the starting range.
+ * @param fromMax the maximum of the starting range.
+ * @param toMin the minimum of the destination range.
+ * @param toMax the maximum of the destination range.
+ * @return the mapped value.
+ * @throws IllegalArgumentException if fromMin == fromMax, but toMin != toMax.
+ */
+ public static double map(double value, double fromMin, double fromMax,
+ double toMin, double toMax) {
+ if (equals(toMin, toMax))
+ return toMin;
+ if (equals(fromMin, fromMax)) {
+ throw new IllegalArgumentException("from range is singular an to range is not.");
+ }
+ return (value - fromMin)/(fromMax-fromMin) * (toMax - toMin) + toMin;
+ }
+
+ /**
+ * Compute the minimum of two values. This is performed by direct comparison.
+ * However, if one of the values is NaN and the other is not, the non-NaN value is
+ * returned.
+ */
+ public static double min(double x, double y) {
+ if (Double.isNaN(y))
+ return x;
+ return (x < y) ? x : y;
+ }
+
+ /**
+ * Compute the maximum of two values. This is performed by direct comparison.
+ * However, if one of the values is NaN and the other is not, the non-NaN value is
+ * returned.
+ */
+ public static double max(double x, double y) {
+ if (Double.isNaN(x))
+ return y;
+ return (x < y) ? y : x;
+ }
+
+ /**
+ * Compute the minimum of three values. This is performed by direct comparison.
+ * However, if one of the values is NaN and the other is not, the non-NaN value is
+ * returned.
+ */
+ public static double min(double x, double y, double z) {
+ if (x < y || Double.isNaN(y)) {
+ return min(x,z);
+ } else {
+ return min(y,z);
+ }
+ }
+
+ /**
+ * Compute the maximum of three values. This is performed by direct comparison.
+ * However, if one of the values is NaN and the other is not, the non-NaN value is
+ * returned.
+ */
+ public static double max(double x, double y, double z) {
+ if (x > y || Double.isNaN(y)) {
+ return max(x,z);
+ } else {
+ return max(y,z);
+ }
+ }
+
+ /**
+ * Calculates the hypotenuse <code>sqrt(x^2+y^2)</code>. This method is SIGNIFICANTLY
+ * faster than <code>Math.hypot(x,y)</code>.
+ */
+ public static double hypot(double x, double y) {
+ return Math.sqrt(x*x + y*y);
+ }
+
+ /**
+ * Reduce the angle x to the range 0 - 2*PI.
+ * @param x Original angle.
+ * @return The equivalent angle in the range 0 ... 2*PI.
+ */
+ public static double reduce360(double x) {
+ double d = Math.floor(x / (2*Math.PI));
+ return x - d*2*Math.PI;
+ }
+
+ /**
+ * Reduce the angle x to the range -PI - PI.
+ *
+ * Either -PI and PI might be returned, depending on the rounding function.
+ *
+ * @param x Original angle.
+ * @return The equivalent angle in the range -PI ... PI.
+ */
+ public static double reduce180(double x) {
+ double d = Math.rint(x / (2*Math.PI));
+ return x - d*2*Math.PI;
+ }
+
+
+ public static boolean equals(double a, double b) {
+ double absb = Math.abs(b);
+
+ if (absb < EPSILON/2) {
+ // Near zero
+ return Math.abs(a) < EPSILON/2;
+ }
+ return Math.abs(a-b) < EPSILON*absb;
+ }
+
+ public static double sign(double x) {
+ return (x<0) ? -1.0 : 1.0;
+ }
+
+ /* Math.abs() is about 3x as fast as this:
+
+ public static double abs(double x) {
+ return (x<0) ? -x : x;
+ }
+ */
+
+
+ public static void main(String[] arg) {
+ double nan = Double.NaN;
+ System.out.println("min(5,6) = " + min(5, 6));
+ System.out.println("min(5,nan) = " + min(5, nan));
+ System.out.println("min(nan,6) = " + min(nan, 6));
+ System.out.println("min(nan,nan) = " + min(nan, nan));
+ System.out.println();
+ System.out.println("max(5,6) = " + max(5, 6));
+ System.out.println("max(5,nan) = " + max(5, nan));
+ System.out.println("max(nan,6) = " + max(nan, 6));
+ System.out.println("max(nan,nan) = " + max(nan, nan));
+ System.out.println();
+ System.out.println("min(5,6,7) = " + min(5, 6, 7));
+ System.out.println("min(5,6,nan) = " + min(5, 6, nan));
+ System.out.println("min(5,nan,7) = " + min(5, nan, 7));
+ System.out.println("min(5,nan,nan) = " + min(5, nan, nan));
+ System.out.println("min(nan,6,7) = " + min(nan, 6, 7));
+ System.out.println("min(nan,6,nan) = " + min(nan, 6, nan));
+ System.out.println("min(nan,nan,7) = " + min(nan, nan, 7));
+ System.out.println("min(nan,nan,nan) = " + min(nan, nan, nan));
+ System.out.println();
+ System.out.println("max(5,6,7) = " + max(5, 6, 7));
+ System.out.println("max(5,6,nan) = " + max(5, 6, nan));
+ System.out.println("max(5,nan,7) = " + max(5, nan, 7));
+ System.out.println("max(5,nan,nan) = " + max(5, nan, nan));
+ System.out.println("max(nan,6,7) = " + max(nan, 6, 7));
+ System.out.println("max(nan,6,nan) = " + max(nan, 6, nan));
+ System.out.println("max(nan,nan,7) = " + max(nan, nan, 7));
+ System.out.println("max(nan,nan,nan) = " + max(nan, nan, nan));
+ System.out.println();
+
+
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.util;
+
+import java.io.Serializable;
+
+/**
+ * An immutable class of weighted coordinates. The weights are non-negative.
+ *
+ * Can also be used as non-weighted coordinates with weight=0.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public final class MutableCoordinate implements Serializable {
+ public static final MutableCoordinate NUL = new MutableCoordinate(0,0,0,0);
+ public static final MutableCoordinate NaN = new MutableCoordinate(Double.NaN,Double.NaN,
+ Double.NaN,Double.NaN);
+ public static final double COMPARISON_DELTA = 0.000001;
+ private double x,y,z;
+ private double weight;
+
+
+ /* Count and report the number of times a Coordinate is constructed: */
+// private static int count=0;
+// {
+// count++;
+// if ((count % 1000) == 0) {
+// System.out.println("Coordinate instantiated "+count+" times");
+// }
+// }
+
+
+ public MutableCoordinate() {
+ x=0;
+ y=0;
+ z=0;
+ weight=0;
+ }
+
+ public MutableCoordinate(double x) {
+ this.x = x;
+ this.y = 0;
+ this.z = 0;
+ weight = 0;
+ }
+
+ public MutableCoordinate(double x, double y) {
+ this.x = x;
+ this.y = y;
+ this.z = 0;
+ weight = 0;
+ }
+
+ public MutableCoordinate(double x, double y, double z) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ weight = 0;
+ }
+ public MutableCoordinate(double x, double y, double z, double w) {
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.weight=w;
+ }
+
+
+ public boolean isWeighted() {
+ return (weight != 0);
+ }
+
+ public boolean isNaN() {
+ return Double.isNaN(x) || Double.isNaN(y) || Double.isNaN(z) || Double.isNaN(weight);
+ }
+
+ public MutableCoordinate setX(double x) {
+ return new MutableCoordinate(x,this.y,this.z,this.weight);
+ }
+
+ public MutableCoordinate setY(double y) {
+ return new MutableCoordinate(this.x,y,this.z,this.weight);
+ }
+
+ public MutableCoordinate setZ(double z) {
+ return new MutableCoordinate(this.x,this.y,z,this.weight);
+ }
+
+ public MutableCoordinate setWeight(double weight) {
+ return new MutableCoordinate(this.x, this.y, this.z, weight);
+ }
+
+ public MutableCoordinate setXYZ(MutableCoordinate c) {
+ return new MutableCoordinate(c.x, c.y, c.z, this.weight);
+ }
+
+ public double getX() {
+ return x;
+ }
+ public double getY() {
+ return y;
+ }
+ public double getZ() {
+ return z;
+ }
+
+
+ /**
+ * Add the coordinate and weight of two coordinates.
+ *
+ * @param other the other <code>Coordinate</code>
+ * @return the sum of the coordinates
+ */
+ public MutableCoordinate add(MutableCoordinate other) {
+ this.x += other.x;
+ this.y += other.y;
+ this.z += other.z;
+ this.weight += other.weight;
+ return this;
+ }
+
+ public MutableCoordinate add(double x, double y, double z) {
+ this.x += x;
+ this.y += y;
+ this.z += z;
+ return this;
+ }
+
+ public MutableCoordinate add(double x, double y, double z, double weight) {
+ return new MutableCoordinate(this.x+x, this.y+y, this.z+z, this.weight+weight);
+ }
+
+ /**
+ * Subtract a Coordinate from this Coordinate. The weight of the resulting Coordinate
+ * is the same as of this Coordinate, the weight of the argument is ignored.
+ *
+ * @param other Coordinate to subtract from this.
+ * @return The result
+ */
+ public MutableCoordinate sub(MutableCoordinate other) {
+ return new MutableCoordinate(this.x-other.x, this.y-other.y, this.z-other.z, this.weight);
+ }
+
+ /**
+ * Subtract the specified values from this Coordinate. The weight of the result
+ * is the same as the weight of this Coordinate.
+ *
+ * @param x x value to subtract
+ * @param y y value to subtract
+ * @param z z value to subtract
+ * @return the result.
+ */
+ public MutableCoordinate sub(double x, double y, double z) {
+ return new MutableCoordinate(this.x - x, this.y - y, this.z - z, this.weight);
+ }
+
+
+ /**
+ * Multiply the <code>Coordinate</code> with a scalar. All coordinates and the
+ * weight are multiplied by the given scalar.
+
+ * @param m Factor to multiply by.
+ * @return The product.
+ */
+ public MutableCoordinate multiply(double m) {
+ return new MutableCoordinate(this.x*m, this.y*m, this.z*m, this.weight*m);
+ }
+
+ /**
+ * Dot product of two Coordinates, taken as vectors. Equal to
+ * x1*x2+y1*y2+z1*z2
+ * @param other Coordinate to take product with.
+ * @return The dot product.
+ */
+ public double dot(MutableCoordinate other) {
+ return this.x*other.x + this.y*other.y + this.z*other.z;
+ }
+ /**
+ * Dot product of two Coordinates.
+ */
+ public static double dot(MutableCoordinate v1, MutableCoordinate v2) {
+ return v1.x*v2.x + v1.y*v2.y + v1.z*v2.z;
+ }
+
+ /**
+ * Distance from the origin to the Coordinate.
+ */
+ public double length() {
+ return Math.sqrt(x*x+y*y+z*z);
+ }
+
+ /**
+ * Square of the distance from the origin to the Coordinate.
+ */
+ public double length2() {
+ return x*x+y*y+z*z;
+ }
+
+ /**
+ * Returns a new coordinate which has the same direction from the origin as this
+ * coordinate but is at a distance of one. If this coordinate is the origin,
+ * this method throws an <code>IllegalStateException</code>. The weight of the
+ * coordinate is unchanged.
+ *
+ * @return the coordinate normalized to distance one of the origin.
+ * @throws IllegalStateException if this coordinate is the origin.
+ */
+ public MutableCoordinate normalize() {
+ double l = length();
+ if (l < 0.0000001) {
+ throw new IllegalStateException("Cannot normalize zero coordinate");
+ }
+ return new MutableCoordinate(x/l, y/l, z/l, weight);
+ }
+
+
+
+
+ /**
+ * Weighted average of two coordinates. If either of the weights are positive,
+ * the result is the weighted average of the coordinates and the weight is the sum
+ * of the original weights. If the sum of the weights is zero (and especially if
+ * both of the weights are zero), the result is the unweighted average of the
+ * coordinates with weight zero.
+ * <p>
+ * If <code>other</code> is <code>null</code> then this <code>Coordinate</code> is
+ * returned.
+ */
+ public MutableCoordinate average(MutableCoordinate other) {
+ double x,y,z,w;
+
+ if (other == null)
+ return this;
+
+ w = this.weight + other.weight;
+ if (MathUtil.equals(w, 0)) {
+ x = (this.x+other.x)/2;
+ y = (this.y+other.y)/2;
+ z = (this.z+other.z)/2;
+ w = 0;
+ } else {
+ x = (this.x*this.weight + other.x*other.weight)/w;
+ y = (this.y*this.weight + other.y*other.weight)/w;
+ z = (this.z*this.weight + other.z*other.weight)/w;
+ }
+ return new MutableCoordinate(x,y,z,w);
+ }
+
+
+ /**
+ * Tests whether the coordinates (not weight!) are the same.
+ *
+ * Compares only the (x,y,z) coordinates, NOT the weight. Coordinate comparison is
+ * done to the precision of COMPARISON_DELTA.
+ *
+ * @param other Coordinate to compare to.
+ * @return true if the coordinates are equal
+ */
+ @Override
+ public boolean equals(Object other) {
+ if (!(other instanceof MutableCoordinate))
+ return false;
+
+ final MutableCoordinate c = (MutableCoordinate)other;
+ return (MathUtil.equals(this.x, c.x) &&
+ MathUtil.equals(this.y, c.y) &&
+ MathUtil.equals(this.z, c.z));
+ }
+
+ /**
+ * Hash code method compatible with {@link #equals(Object)}.
+ */
+ @Override
+ public int hashCode() {
+ return (int)((x+y+z)*100000);
+ }
+
+
+ @Override
+ public String toString() {
+ if (isWeighted())
+ return String.format("(%.3f,%.3f,%.3f,w=%.3f)", x,y,z,weight);
+ else
+ return String.format("(%.3f,%.3f,%.3f)", x,y,z);
+ }
+
+
+
+ public static void main(String[] arg) {
+ double a=1.2;
+ double x;
+ MutableCoordinate c;
+ long t1, t2;
+
+ x = 0;
+ t1 = System.nanoTime();
+ for (int i=0; i < 100000000; i++) {
+ x = x + a;
+ }
+ t2 = System.nanoTime();
+ System.out.println("Value: "+x);
+ System.out.println("Plain addition: "+ ((t2-t1+500000)/1000000) + " ms");
+
+ c = MutableCoordinate.NUL;
+ t1 = System.nanoTime();
+ for (int i=0; i < 100000000; i++) {
+ c = c.add(a,0,0);
+ }
+ t2 = System.nanoTime();
+ System.out.println("Value: "+c.x);
+ System.out.println("Coordinate addition: "+ ((t2-t1+500000)/1000000) + " ms");
+
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.util;
+
+public class Pair<U,V> {
+
+ private final U u;
+ private final V v;
+
+
+ public Pair(U u, V v) {
+ this.u = u;
+ this.v = v;
+ }
+
+ public U getU() {
+ return u;
+ }
+
+ public V getV() {
+ return v;
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.util;
+import java.util.Random;
+
+
+/**
+ * A class that provides a source of pink noise with a power spectrum density
+ * proportional to 1/f^alpha. The values are computed by applying an IIR filter to
+ * generated Gaussian random numbers. The number of poles used in the filter may be
+ * specified. Values as low as 3 produce good results, but using a larger number of
+ * poles allows lower frequencies to be amplified. Below the cutoff frequency the
+ * power spectrum density if constant.
+ * <p>
+ * The IIR filter use by this class is presented by N. Jeremy Kasdin, Proceedings of
+ * the IEEE, Vol. 83, No. 5, May 1995, p. 822.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class PinkNoise {
+ private final int poles;
+ private final double[] multipliers;
+
+ private final double[] values;
+ private final Random rnd;
+
+
+ /**
+ * Generate pink noise with alpha=1.0 using a five-pole IIR.
+ */
+ public PinkNoise() {
+ this(1.0, 5, new Random());
+ }
+
+
+ /**
+ * Generate a specific pink noise using a five-pole IIR.
+ *
+ * @param alpha the exponent of the pink noise, 1/f^alpha.
+ */
+ public PinkNoise(double alpha) {
+ this(alpha, 5, new Random());
+ }
+
+
+ /**
+ * Generate pink noise specifying alpha and the number of poles. The larger the
+ * number of poles, the lower are the lowest frequency components that are amplified.
+ *
+ * @param alpha the exponent of the pink noise, 1/f^alpha.
+ * @param poles the number of poles to use.
+ */
+ public PinkNoise(double alpha, int poles) {
+ this(alpha, poles, new Random());
+ }
+
+
+ /**
+ * Generate pink noise specifying alpha, the number of poles and the randomness source.
+ *
+ * @param alpha the exponent of the pink noise, 1/f^alpha.
+ * @param poles the number of poles to use.
+ * @param random the randomness source.
+ */
+ public PinkNoise(double alpha, int poles, Random random) {
+ this.rnd = random;
+ this.poles = poles;
+ this.multipliers = new double[poles];
+ this.values = new double[poles];
+
+ double a = 1;
+ for (int i=0; i < poles; i++) {
+ a = (i - alpha/2) * a / (i+1);
+ multipliers[i] = a;
+ }
+
+ // Fill the history with random values
+ for (int i=0; i < 5*poles; i++)
+ this.nextValue();
+ }
+
+
+
+ public double nextValue() {
+ double x = rnd.nextGaussian();
+// double x = rnd.nextDouble()-0.5;
+
+ for (int i=0; i < poles; i++) {
+ x -= multipliers[i] * values[i];
+ }
+ System.arraycopy(values, 0, values, 1, values.length-1);
+ values[0] = x;
+
+ return x;
+ }
+
+
+ public static void main(String[] arg) {
+
+ PinkNoise source;
+
+ source = new PinkNoise(1.0, 100);
+ double std = 0;
+ for (int i=0; i < 1000000; i++) {
+
+ }
+
+
+// int n = 5000000;
+// double avgavg=0;
+// double avgstd = 0;
+// double[] val = new double[n];
+//
+// for (int j=0; j < 10; j++) {
+// double avg=0, std=0;
+// source = new PinkNoise(5.0/3.0, 2);
+//
+// for (int i=0; i < n; i++) {
+// val[i] = source.nextValue();
+// avg += val[i];
+// }
+// avg /= n;
+// for (int i=0; i < n; i++) {
+// std += (val[i]-avg)*(val[i]-avg);
+// }
+// std /= n;
+// std = Math.sqrt(std);
+//
+// System.out.println("avg:"+avg+" stddev:"+std);
+// avgavg += avg;
+// avgstd += std;
+// }
+// avgavg /= 10;
+// avgstd /= 10;
+// System.out.println("Average avg:"+avgavg+" std:"+avgstd);
+//
+ // Two poles:
+
+ }
+
+
+}
--- /dev/null
+package net.sf.openrocket.util;
+
+import java.util.Arrays;
+
+/**
+ * A class for polynomial interpolation. The interpolation constraints can be specified
+ * either as function values or values of the n'th derivative of the function.
+ * Using an interpolation consists of three steps:
+ * <p>
+ * 1. constructing a <code>PolyInterpolator</code> using the interpolation x coordinates <br>
+ * 2. generating the interpolation polynomial using the function and derivative values <br>
+ * 3. evaluating the polynomial at the desired point
+ * <p>
+ * The constructor takes an array of double arrays. The first array defines x coordinate
+ * values for the function values, the second array x coordinate values for the function's
+ * derivatives, the third array for second derivatives and so on. Constructing the
+ * <code>PolyInterpolator</code> is relatively slow, O(n^3) where n is the order of the
+ * polynomial. (It contains calculation of the inverse of an n x n matrix.)
+ * <p>
+ * Generating the interpolation polynomial is performed by the method
+ * {@link #interpolator(double...)}, which takes as an argument the function and
+ * derivative values. This operation takes O(n^2) time.
+ * <p>
+ * Finally, evaluating the polynomial at different positions takes O(n) time.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+public class PolyInterpolator {
+
+ // Order of the polynomial
+ private final int count;
+
+ private final double[][] interpolationMatrix;
+
+
+ /**
+ * Construct a polynomial interpolation generator. All arguments to the constructor
+ * are the x coordinates of the interpolated function. The first array correspond to
+ * the function values, the second to function derivatives, the third to second
+ * derivatives and so forth. The order of the polynomial is automatically calculated
+ * from the total number of constraints.
+ * <p>
+ * The construction takes O(n^3) time.
+ *
+ * @param points an array of constraints, the first corresponding to function value
+ * constraints, the second to derivative constraints etc.
+ */
+ public PolyInterpolator(double[] ... points) {
+ int count = 0;
+ for (int i=0; i < points.length; i++) {
+ count += points[i].length;
+ }
+ if (count == 0) {
+ throw new IllegalArgumentException("No interpolation points defined.");
+ }
+
+ this.count = count;
+
+ int[] mul = new int[count];
+ Arrays.fill(mul, 1);
+
+ double[][] matrix = new double[count][count];
+ int row = 0;
+ for (int j=0; j < points.length; j++) {
+
+ for (int i=0; i < points[j].length; i++) {
+ double x = 1;
+ for (int col = count-1-j; col>= 0; col--) {
+ matrix[row][col] = x*mul[col];
+ x *= points[j][i];
+ }
+ row++;
+ }
+
+ for (int i=0; i < count; i++) {
+ mul[i] *= (count-i-j-1);
+ }
+ }
+ assert(row == count);
+
+ interpolationMatrix = inverse(matrix);
+ }
+
+
+ /**
+ * Generates an interpolation polynomial. The arguments supplied to this method
+ * are the function values, derivatives, second derivatives etc. in the order
+ * specified in the constructor (i.e. values first, then derivatives etc).
+ * <p>
+ * This method takes O(n^2) time.
+ *
+ * @param values the function values, derivatives etc. at positions defined in the
+ * constructor.
+ * @return the coefficients of the interpolation polynomial, the highest order
+ * term first and the constant last.
+ */
+ public double[] interpolator(double... values) {
+ if (values.length != count) {
+ throw new IllegalArgumentException("Wrong number of arguments "+values.length+
+ " expected "+count);
+ }
+
+ double[] ret = new double[count];
+
+ for (int j=0; j < count; j++) {
+ for (int i=0; i < count; i++) {
+ ret[j] += interpolationMatrix[j][i] * values[i];
+ }
+ }
+
+ return ret;
+ }
+
+
+ /**
+ * Interpolate the given values at the point <code>x</code>. This is equivalent
+ * to generating an interpolation polynomial and evaluating the polynomial at the
+ * specified point.
+ *
+ * @param x point at which to evaluate the interpolation polynomial.
+ * @param values the function, derivatives etc. at position defined in the
+ * constructor.
+ * @return the value of the interpolation.
+ */
+ public double interpolate(double x, double... values) {
+ return eval(x, interpolator(values));
+ }
+
+
+ /**
+ * Evaluate a polynomial at the specified point <code>x</code>. The coefficients are
+ * assumed to have the highest order coefficient first and the constant term last.
+ *
+ * @param x position at which to evaluate the polynomial.
+ * @param coefficients polynomial coefficients, highest term first and constant last.
+ * @return the value of the polynomial.
+ */
+ public static double eval(double x, double[] coefficients) {
+ double v = 1;
+ double result = 0;
+ for (int i = coefficients.length-1; i >= 0; i--) {
+ result += coefficients[i] * v;
+ v *= x;
+ }
+ return result;
+ }
+
+
+
+
+ private static double[][] inverse(double[][] matrix) {
+ int n = matrix.length;
+
+ double x[][] = new double[n][n];
+ double b[][] = new double[n][n];
+ int index[] = new int[n];
+ for (int i=0; i<n; ++i)
+ b[i][i] = 1;
+
+ // Transform the matrix into an upper triangle
+ gaussian(matrix, index);
+
+ // Update the matrix b[i][j] with the ratios stored
+ for (int i=0; i<n-1; ++i)
+ for (int j=i+1; j<n; ++j)
+ for (int k=0; k<n; ++k)
+ b[index[j]][k] -= matrix[index[j]][i]*b[index[i]][k];
+
+ // Perform backward substitutions
+ for (int i=0; i<n; ++i) {
+ x[n-1][i] = b[index[n-1]][i]/matrix[index[n-1]][n-1];
+ for (int j=n-2; j>=0; --j) {
+ x[j][i] = b[index[j]][i];
+ for (int k=j+1; k<n; ++k) {
+ x[j][i] -= matrix[index[j]][k]*x[k][i];
+ }
+ x[j][i] /= matrix[index[j]][j];
+ }
+ }
+ return x;
+ }
+
+ private static void gaussian(double a[][],
+ int index[]) {
+ int n = index.length;
+ double c[] = new double[n];
+
+ // Initialize the index
+ for (int i=0; i<n; ++i) index[i] = i;
+
+ // Find the rescaling factors, one from each row
+ for (int i=0; i<n; ++i) {
+ double c1 = 0;
+ for (int j=0; j<n; ++j) {
+ double c0 = Math.abs(a[i][j]);
+ if (c0 > c1) c1 = c0;
+ }
+ c[i] = c1;
+ }
+
+ // Search the pivoting element from each column
+ int k = 0;
+ for (int j=0; j<n-1; ++j) {
+ double pi1 = 0;
+ for (int i=j; i<n; ++i) {
+ double pi0 = Math.abs(a[index[i]][j]);
+ pi0 /= c[index[i]];
+ if (pi0 > pi1) {
+ pi1 = pi0;
+ k = i;
+ }
+ }
+
+ // Interchange rows according to the pivoting order
+ int itmp = index[j];
+ index[j] = index[k];
+ index[k] = itmp;
+ for (int i=j+1; i<n; ++i) {
+ double pj = a[index[i]][j]/a[index[j]][j];
+
+ // Record pivoting ratios below the diagonal
+ a[index[i]][j] = pj;
+
+ // Modify other elements accordingly
+ for (int l=j+1; l<n; ++l)
+ a[index[i]][l] -= pj*a[index[j]][l];
+ }
+ }
+ }
+
+
+
+
+ public static void main(String[] arg) {
+
+ PolyInterpolator p0 = new PolyInterpolator(
+ new double[] {0.6, 1.1},
+ new double[] {0.6, 1.1}
+ );
+ double[] r0 = p0.interpolator(1.5, 1.6, 2, -3);
+
+ PolyInterpolator p1 = new PolyInterpolator(
+ new double[] {0.6, 1.1},
+ new double[] {0.6, 1.1},
+ new double[] {0.6}
+ );
+ double[] r1 = p1.interpolator(1.5, 1.6, 2, -3, 0);
+
+ PolyInterpolator p2 = new PolyInterpolator(
+ new double[] {0.6, 1.1},
+ new double[] {0.6, 1.1},
+ new double[] {0.6, 1.1}
+ );
+ double[] r2 = p2.interpolator(1.5, 1.6, 2, -3, 0, 0);
+
+
+ for (double x=0.6; x <= 1.11; x += 0.01) {
+ System.out.println(x + " " + eval(x,r0) + " " + eval(x,r1) + " " + eval(x,r2));
+ }
+
+ }
+}
--- /dev/null
+package net.sf.openrocket.util;
+
+import java.awt.Color;
+import java.awt.Dimension;
+import java.awt.Point;
+import java.awt.Toolkit;
+import java.io.File;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.prefs.BackingStoreException;
+import java.util.prefs.Preferences;
+
+import net.sf.openrocket.database.Databases;
+import net.sf.openrocket.document.Simulation;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.BodyComponent;
+import net.sf.openrocket.rocketcomponent.FinSet;
+import net.sf.openrocket.rocketcomponent.InternalComponent;
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+import net.sf.openrocket.rocketcomponent.MassObject;
+import net.sf.openrocket.rocketcomponent.RecoveryDevice;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+import net.sf.openrocket.simulation.RK4Simulator;
+import net.sf.openrocket.simulation.SimulationConditions;
+import net.sf.openrocket.unit.UnitGroup;
+
+
+public class Prefs {
+
+ /**
+ * Whether to use the debug-node instead of the normal node.
+ */
+ public static final boolean DEBUG = false;
+
+ /**
+ * Whether to clear all preferences at application startup. This has an effect only
+ * if DEBUG is true.
+ */
+ public static final boolean CLEARPREFS = true;
+
+ /**
+ * The node name to use in the Java preferences storage.
+ */
+ public static final String NODENAME = (DEBUG?"OpenRocket-debug":"OpenRocket");
+
+
+
+ private static final String VERSION = "0.9.0";
+
+
+ public static final String BODY_COMPONENT_INSERT_POSITION_KEY = "BodyComponentInsertPosition";
+
+
+ public static final String CONFIRM_DELETE_SIMULATION = "ConfirmDeleteSimulation";
+
+
+ /**
+ * Node to this application's preferences.
+ */
+ public static final Preferences NODE;
+
+
+ static {
+ Preferences root = Preferences.userRoot();
+ if (DEBUG && CLEARPREFS) {
+ try {
+ if (root.nodeExists(NODENAME)) {
+ root.node(NODENAME).removeNode();
+ }
+ } catch (BackingStoreException e) {
+ throw new RuntimeException("Unable to clear preference node",e);
+ }
+ }
+ NODE = root.node(NODENAME);
+ }
+
+
+
+
+ ///////// Default component attributes
+
+ private static final HashMap<Class<?>,String> DEFAULT_COLORS =
+ new HashMap<Class<?>,String>();
+ static {
+ DEFAULT_COLORS.put(BodyComponent.class, "0,0,240");
+ DEFAULT_COLORS.put(FinSet.class, "0,0,200");
+ DEFAULT_COLORS.put(LaunchLug.class, "0,0,180");
+ DEFAULT_COLORS.put(InternalComponent.class, "170,0,100");
+ DEFAULT_COLORS.put(MassObject.class, "0,0,0");
+ DEFAULT_COLORS.put(RecoveryDevice.class, "255,0,0");
+ }
+
+
+ private static final HashMap<Class<?>,String> DEFAULT_LINE_STYLES =
+ new HashMap<Class<?>,String>();
+ static {
+ DEFAULT_LINE_STYLES.put(RocketComponent.class, LineStyle.SOLID.name());
+ DEFAULT_LINE_STYLES.put(MassObject.class, LineStyle.DASHED.name());
+ }
+
+
+ private static final Material DEFAULT_LINE_MATERIAL =
+ Databases.findMaterial(Material.Type.LINE, "Elastic cord (round 2mm, 1/16 in)", 0.0018);
+ private static final Material DEFAULT_SURFACE_MATERIAL =
+ Databases.findMaterial(Material.Type.SURFACE, "Ripstop nylon", 0.067);
+ private static final Material DEFAULT_BULK_MATERIAL =
+ Databases.findMaterial(Material.Type.BULK, "Cardboard", 680);
+
+
+ //////////////////////
+
+
+ public static String getVersion() {
+ return VERSION;
+ }
+
+
+
+ public static void storeVersion() {
+ NODE.put("OpenRocketVersion", getVersion());
+ }
+
+
+ /**
+ * Returns a limited-range integer value from the preferences. If the value
+ * in the preferences is negative or greater than max, then the default value
+ * is returned.
+ *
+ * @param key The preference to retrieve.
+ * @param max Maximum allowed value for the choice.
+ * @param def Default value.
+ * @return The preference value.
+ */
+ public static int getChoise(String key, int max, int def) {
+ int v = NODE.getInt(key, def);
+ if ((v<0) || (v>max))
+ return def;
+ return v;
+ }
+
+
+ /**
+ * Helper method that puts an integer choice value into the preferences.
+ *
+ * @param key the preference key.
+ * @param value the value to store.
+ */
+ public static void putChoise(String key, int value) {
+ NODE.putInt(key, value);
+ storeVersion();
+ }
+
+
+
+ public static String getString(String key, String def) {
+ return NODE.get(key, def);
+ }
+
+ public static void putString(String key, String value) {
+ NODE.put(key, value);
+ storeVersion();
+ }
+
+
+
+
+ //////////////////
+
+ public static File getDefaultDirectory() {
+ String file = NODE.get("defaultDirectory", null);
+ if (file == null)
+ return null;
+ return new File(file);
+ }
+
+ public static void setDefaultDirectory(File dir) {
+ String d;
+ if (dir == null) {
+ d = null;
+ } else {
+ d = dir.getAbsolutePath();
+ }
+ NODE.put("defaultDirectory", d);
+ storeVersion();
+ }
+
+
+
+ public static Color getDefaultColor(Class<? extends RocketComponent> c) {
+ String color = get("componentColors", c, DEFAULT_COLORS);
+ if (color == null)
+ return Color.BLACK;
+
+ String[] rgb = color.split(",");
+ if (rgb.length==3) {
+ try {
+ int red = MathUtil.clamp(Integer.parseInt(rgb[0]),0,255);
+ int green = MathUtil.clamp(Integer.parseInt(rgb[1]),0,255);
+ int blue = MathUtil.clamp(Integer.parseInt(rgb[2]),0,255);
+ return new Color(red,green,blue);
+ } catch (NumberFormatException ignore) { }
+ }
+
+ return Color.BLACK;
+ }
+
+ public static void setDefaultColor(Class<? extends RocketComponent> c, Color color) {
+ if (color==null)
+ return;
+ String string = color.getRed() + "," + color.getGreen() + "," + color.getBlue();
+ set("componentColors", c, string);
+ }
+
+ public static Color getMotorBorderColor() {
+ // TODO: MEDIUM: Motor color (settable?)
+ return new Color(0,0,0,200);
+ }
+
+
+ public static Color getMotorFillColor() {
+ // TODO: MEDIUM: Motor fill color (settable?)
+ return new Color(0,0,0,100);
+ }
+
+
+ public static LineStyle getDefaultLineStyle(Class<? extends RocketComponent> c) {
+ String value = get("componentStyle", c, DEFAULT_LINE_STYLES);
+ try {
+ return LineStyle.valueOf(value);
+ } catch (Exception e) {
+ return LineStyle.SOLID;
+ }
+ }
+
+ public static void setDefaultLineStyle(Class<? extends RocketComponent> c,
+ LineStyle style) {
+ if (style == null)
+ return;
+ set("componentStyle", c, style.name());
+ }
+
+
+ /**
+ * Return the DPI setting of the monitor. This is either the setting provided
+ * by the system or a user-specified DPI setting.
+ *
+ * @return the DPI setting to use.
+ */
+ public static double getDPI() {
+ int dpi = NODE.getInt("DPI", 0); // Tenths of a dpi
+
+ if (dpi < 10) {
+ dpi = Toolkit.getDefaultToolkit().getScreenResolution()*10;
+ }
+ if (dpi < 10)
+ dpi = 960;
+
+ return ((double)dpi)/10.0;
+ }
+
+
+ public static double getDefaultMach() {
+ // TODO: HIGH: implement custom default mach number
+ return 0.3;
+ }
+
+
+
+
+ public static Material getDefaultComponentMaterial(
+ Class<? extends RocketComponent> componentClass,
+ Material.Type type) {
+
+ String material = get("componentMaterials", componentClass, null);
+ if (material != null) {
+ try {
+ Material m = Material.fromStorableString(material);
+ if (m.getType() == type)
+ return m;
+ } catch (IllegalArgumentException ignore) { }
+ }
+
+ switch (type) {
+ case LINE:
+ return DEFAULT_LINE_MATERIAL;
+ case SURFACE:
+ return DEFAULT_SURFACE_MATERIAL;
+ case BULK:
+ return DEFAULT_BULK_MATERIAL;
+ }
+ throw new IllegalArgumentException("Unknown material type: "+type);
+ }
+
+ public static void setDefaultComponentMaterial(
+ Class<? extends RocketComponent> componentClass, Material material) {
+
+ set("componentMaterials", componentClass,
+ material==null ? null : material.toStorableString());
+ }
+
+
+ public static int getMaxThreadCount() {
+ return Runtime.getRuntime().availableProcessors();
+ }
+
+
+
+ public static Point getWindowPosition(Class<?> c) {
+ int x, y;
+ String pref = NODE.node("windows").get("position." + c.getCanonicalName(), null);
+
+ if (pref == null)
+ return null;
+
+ if (pref.indexOf(',')<0)
+ return null;
+
+ try {
+ x = Integer.parseInt(pref.substring(0,pref.indexOf(',')));
+ y = Integer.parseInt(pref.substring(pref.indexOf(',')+1));
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ return new Point(x,y);
+ }
+
+ public static void setWindowPosition(Class<?> c, Point p) {
+ NODE.node("windows").put("position." + c.getCanonicalName(), "" + p.x + "," + p.y);
+ storeVersion();
+ }
+
+
+
+
+ public static Dimension getWindowSize(Class<?> c) {
+ int x, y;
+ String pref = NODE.node("windows").get("size." + c.getCanonicalName(), null);
+
+ if (pref == null)
+ return null;
+
+ if (pref.indexOf(',')<0)
+ return null;
+
+ try {
+ x = Integer.parseInt(pref.substring(0,pref.indexOf(',')));
+ y = Integer.parseInt(pref.substring(pref.indexOf(',')+1));
+ } catch (NumberFormatException e) {
+ return null;
+ }
+ return new Dimension(x,y);
+ }
+
+ public static void setWindowSize(Class<?> c, Dimension d) {
+ NODE.node("windows").put("size." + c.getCanonicalName(), "" + d.width + "," + d.height);
+ storeVersion();
+ }
+
+
+ //// Background flight data computation
+
+ public static boolean computeFlightInBackground() {
+ return NODE.getBoolean("backgroundFlight", true);
+ }
+
+ public static Simulation getBackgroundSimulation(Rocket rocket) {
+ Simulation s = new Simulation(rocket);
+ SimulationConditions cond = s.getConditions();
+
+ cond.setTimeStep(RK4Simulator.RECOMMENDED_TIME_STEP*2);
+ cond.setWindSpeedAverage(1.0);
+ cond.setWindSpeedDeviation(0.1);
+ cond.setLaunchRodLength(5);
+ return s;
+ }
+
+
+
+
+ ///////// Default unit storage
+
+ public static void loadDefaultUnits() {
+ Preferences prefs = NODE.node("units");
+ try {
+
+ for (String key: prefs.keys()) {
+ UnitGroup group = UnitGroup.UNITS.get(key);
+ if (group == null)
+ continue;
+
+ group.setDefaultUnit(prefs.get(key, null));
+ }
+
+ } catch (BackingStoreException e) {
+ System.err.println("BackingStoreException:");
+ e.printStackTrace();
+ }
+ }
+
+ public static void storeDefaultUnits() {
+ Preferences prefs = NODE.node("units");
+
+ for (String key: UnitGroup.UNITS.keySet()) {
+ UnitGroup group = UnitGroup.UNITS.get(key);
+ if (group == null || group.getUnitCount() < 2)
+ continue;
+
+ prefs.put(key, group.getDefaultUnit().getUnit());
+ }
+ }
+
+
+
+ //// Helper methods
+
+ private static String get(String directory,
+ Class<? extends RocketComponent> componentClass,
+ Map<Class<?>, String> defaultMap) {
+
+ // Search preferences
+ Class<?> c = componentClass;
+ Preferences prefs = NODE.node(directory);
+ while (c!=null && RocketComponent.class.isAssignableFrom(c)) {
+ String value = prefs.get(c.getSimpleName(), null);
+ if (value != null)
+ return value;
+ c = c.getSuperclass();
+ }
+
+ if (defaultMap == null)
+ return null;
+
+ // Search defaults
+ c = componentClass;
+ while (RocketComponent.class.isAssignableFrom(c)) {
+ String value = defaultMap.get(c);
+ if (value != null)
+ return value;
+ c = c.getSuperclass();
+ }
+
+ return null;
+ }
+
+
+ private static void set(String directory, Class<? extends RocketComponent> componentClass,
+ String value) {
+ Preferences prefs = NODE.node(directory);
+ if (value == null)
+ prefs.remove(componentClass.getSimpleName());
+ else
+ prefs.put(componentClass.getSimpleName(), value);
+ storeVersion();
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.util;
+
+
+public class Quaternion implements Cloneable {
+
+ protected double w, x, y, z;
+ protected int modCount = 0;
+
+ public Quaternion() {
+ this(1,0,0,0);
+ }
+
+ public Quaternion(double w, double x, double y, double z) {
+ this.w = w;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ }
+
+
+ public static Quaternion rotation(Coordinate rotation) {
+ double length = rotation.length();
+ if (length < 0.000001) {
+ return new Quaternion(1,0,0,0);
+ }
+ double sin = Math.sin(length/2);
+ double cos = Math.cos(length/2);
+ return new Quaternion(cos,
+ sin*rotation.x/length, sin*rotation.y/length, sin*rotation.z/length);
+ }
+
+ public static Quaternion rotation(Coordinate axis, double angle) {
+ Coordinate a = axis.normalize();
+ double sin = Math.sin(angle);
+ double cos = Math.cos(angle);
+ return new Quaternion(cos, sin*a.x, sin*a.y, sin*a.z);
+ }
+
+
+ public double getW() {
+ return w;
+ }
+
+ public void setW(double w) {
+ this.w = w;
+ modCount++;
+ }
+
+ public double getX() {
+ return x;
+ }
+
+ public void setX(double x) {
+ this.x = x;
+ modCount++;
+ }
+
+ public double getY() {
+ return y;
+ }
+
+ public void setY(double y) {
+ this.y = y;
+ modCount++;
+ }
+
+ public double getZ() {
+ return z;
+ }
+
+ public void setZ(double z) {
+ this.z = z;
+ modCount++;
+ }
+
+
+ public void setAll(double w, double x, double y, double z) {
+ this.w = w;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ modCount++;
+ }
+
+
+ /**
+ * Multiply this quaternion by the other quaternion from the right side. This
+ * calculates the product <code>this = this * other</code>.
+ *
+ * @param other the quaternion to multiply this quaternion by.
+ * @return this quaternion.
+ */
+ public Quaternion multiplyRight(Quaternion other) {
+ double w = (this.w*other.w - this.x*other.x - this.y*other.y - this.z*other.z);
+ double x = (this.w*other.x + this.x*other.w + this.y*other.z - this.z*other.y);
+ double y = (this.w*other.y + this.y*other.w + this.z*other.x - this.x*other.z);
+ double z = (this.w*other.z + this.z*other.w + this.x*other.y - this.y*other.x);
+
+ this.w = w;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ return this;
+ }
+
+ /**
+ * Multiply this quaternion by the other quaternion from the left side. This
+ * calculates the product <code>this = other * this</code>.
+ *
+ * @param other the quaternion to multiply this quaternion by.
+ * @return this quaternion.
+ */
+ public Quaternion multiplyLeft(Quaternion other) {
+ /* other(abcd) * this(wxyz) */
+
+ double w = (other.w*this.w - other.x*this.x - other.y*this.y - other.z*this.z);
+ double x = (other.w*this.x + other.x*this.w + other.y*this.z - other.z*this.y);
+ double y = (other.w*this.y + other.y*this.w + other.z*this.x - other.x*this.z);
+ double z = (other.w*this.z + other.z*this.w + other.x*this.y - other.y*this.x);
+
+ this.w = w;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ return this;
+ }
+
+
+
+
+
+
+ @Override
+ public Quaternion clone() {
+ try {
+ return (Quaternion) super.clone();
+ } catch (CloneNotSupportedException e) {
+ throw new RuntimeException("CloneNotSupportedException encountered");
+ }
+ }
+
+
+
+ /**
+ * Normalize this quaternion. After the call the norm of the quaternion is exactly
+ * one. If this quaternion is the zero quaternion, throws
+ * <code>IllegalStateException</code>. Returns this quaternion.
+ *
+ * @return this quaternion.
+ * @throws IllegalStateException if the norm of this quaternion is zero.
+ */
+ public Quaternion normalize() {
+ double norm = norm();
+ if (norm < 0.0000001) {
+ throw new IllegalStateException("attempting to normalize zero-quaternion");
+ }
+ x /= norm;
+ y /= norm;
+ z /= norm;
+ w /= norm;
+ return this;
+ }
+
+
+ /**
+ * Normalize this quaternion if the norm is more than 1ppm from one.
+ *
+ * @return this quaternion.
+ * @throws IllegalStateException if the norm of this quaternion is zero.
+ */
+ public Quaternion normalizeIfNecessary() {
+ double n2 = norm2();
+ if (n2 < 0.999999 || n2 > 1.000001)
+ normalize();
+ return this;
+ }
+
+
+
+ /**
+ * Return the norm of this quaternion.
+ *
+ * @return the norm of this quaternion sqrt(w^2 + x^2 + y^2 + z^2).
+ */
+ public double norm() {
+ return Math.sqrt(x*x + y*y + z*z + w*w);
+ }
+
+ /**
+ * Return the square of the norm of this quaternion.
+ *
+ * @return the square of the norm of this quaternion (w^2 + x^2 + y^2 + z^2).
+ */
+ public double norm2() {
+ return x*x + y*y + z*z + w*w;
+ }
+
+
+ public Coordinate rotate(Coordinate coord) {
+ double a,b,c,d;
+
+ assert(Math.abs(norm2()-1) < 0.00001) : "Quaternion not unit length: "+this;
+
+ a = - x * coord.x - y * coord.y - z * coord.z; // w
+ b = w * coord.x + y * coord.z - z * coord.y; // x i
+ c = w * coord.y - x * coord.z + z * coord.x; // y j
+ d = w * coord.z + x * coord.y - y * coord.x; // z k
+
+ assert(MathUtil.equals(a*w + b*x + c*y + d*z, 0)) :
+ ("Should be zero: " + (a*w - b*x - c*y - d*z) + " in " + this + " c=" + coord);
+
+ return new Coordinate(
+ - a*x + b*w - c*z + d*y,
+ - a*y + b*z + c*w - d*x,
+ - a*z - b*y + c*x + d*w,
+ coord.weight
+ );
+ }
+
+ public Coordinate invRotate(Coordinate coord) {
+ double a,b,c,d;
+
+ assert(Math.abs(norm2()-1) < 0.00001) : "Quaternion not unit length: "+this;
+
+ a = + x * coord.x + y * coord.y + z * coord.z;
+ b = w * coord.x - y * coord.z + z * coord.y;
+ c = w * coord.y + x * coord.z - z * coord.x;
+ d = w * coord.z - x * coord.y + y * coord.x;
+
+ assert(MathUtil.equals(a*w - b*x - c*y - d*z, 0)):
+ ("Should be zero: " + (a*w - b*x - c*y - d*z) + " in " + this + " c=" + coord);
+
+ return new Coordinate(
+ a*x + b*w + c*z - d*y,
+ a*y - b*z + c*w + d*x,
+ a*z + b*y - c*x + d*w,
+ coord.weight
+ );
+ }
+
+
+ /**
+ * Rotate the coordinate (0,0,1) using this quaternion. The result is returned
+ * as a Coordinate. This method is equivalent to calling
+ * <code>q.rotate(new Coordinate(0,0,1))</code> but requires only about half of the
+ * multiplications.
+ *
+ * @return The coordinate (0,0,1) rotated using this quaternion.
+ */
+ public Coordinate rotateZ() {
+ return new Coordinate(
+ 2*(w*y + x*z),
+ 2*(y*z - w*x),
+ w*w - x*x - y*y + z*z
+ );
+ }
+
+
+ @Override
+ public String toString() {
+ return String.format("Quaternion[%f,%f,%f,%f,norm=%f]",w,x,y,z,this.norm());
+ }
+
+ public static void main(String[] arg) {
+
+ Quaternion q = new Quaternion(Math.random()-0.5,Math.random()-0.5,
+ Math.random()-0.5,Math.random()-0.5);
+ q.normalize();
+
+ q = new Quaternion(-0.998717,0.000000,0.050649,-0.000000);
+
+ Coordinate coord = new Coordinate(10*(Math.random()-0.5),
+ 10*(Math.random()-0.5), 10*(Math.random()-0.5));
+
+ System.out.println("Quaternion: "+q);
+ System.out.println("Coordinate: "+coord);
+ coord = q.invRotate(coord);
+ System.out.println("Rotated: "+ coord);
+ coord = q.rotate(coord);
+ System.out.println("Back: "+coord);
+
+// Coordinate c = new Coordinate(0,1,0);
+// Coordinate rot = new Coordinate(Math.PI/4,0,0);
+//
+// System.out.println("Before: "+c);
+// c = rotation(rot).invRotate(c);
+// System.out.println("After: "+c);
+
+
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.util;
+
+public class QuaternionMultiply {
+
+ private static class Value {
+ public int sign = 1;
+ public String value;
+
+ public Value multiply(Value other) {
+ Value result = new Value();
+ result.sign = this.sign * other.sign;
+ if (this.value.compareTo(other.value) < 0)
+ result.value = this.value + "*" + other.value;
+ else
+ result.value = other.value + "*" + this.value;
+ return result;
+ }
+ @Override
+ public String toString() {
+ String s;
+
+ if (sign < 0)
+ s = "-";
+ else
+ s = "+";
+
+ if (sign == 0)
+ s += " 0";
+ else
+ s += " " + value;
+
+ return s;
+ }
+ }
+
+
+
+ private static Value[] multiply(Value[] first, Value[] second) {
+ return null;
+ }
+
+
+ public static void main(String[] arg) {
+ if (arg.length % 4 != 0 || arg.length < 4) {
+ System.out.println("Must have modulo 4 args, at least 4");
+ return;
+ }
+
+ Value[][] values = new Value[arg.length/4][4];
+
+ for (int i=0; i<arg.length; i++) {
+ Value value = new Value();
+
+ if (arg[i].equals("")) {
+ value.sign = 0;
+ } else {
+ if (arg[i].startsWith("-")) {
+ value.sign = -1;
+ value.value = arg[i].substring(1);
+ } else if (arg[i].startsWith("+")) {
+ value.sign = 1;
+ value.value = arg[i].substring(1);
+ } else {
+ value.sign = 1;
+ value.value = arg[i];
+ }
+ }
+
+ values[i/4][i%4] = value;
+ }
+
+ System.out.println("Multiplying:");
+ for (int i=0; i < values.length; i++) {
+ print(values[i]);
+ }
+ System.out.println("Result:");
+
+ Value[] result = values[0];
+ for (int i=1; i < values.length; i++) {
+ result = multiply(result, values[i]);
+ }
+ print(result);
+ }
+
+ private static void print(Value[] q) {
+ System.out.println(" " + q[0] + " " + q[1] + " i " + q[2] + " j " + q[3] + " k");
+ }
+
+}
+
--- /dev/null
+package net.sf.openrocket.util;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+
+import net.sf.openrocket.rocketcomponent.RocketComponent;
+
+
+public class Reflection {
+
+ private static final String ROCKETCOMPONENT_PACKAGE = "net.sf.openrocket.rocketcomponent";
+
+ /**
+ * Simple wrapper class that converts the Method.invoke() exceptions into suitable
+ * RuntimeExceptions.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+ public static class Method {
+ private final java.lang.reflect.Method method;
+ public Method(java.lang.reflect.Method m) {
+ method = m;
+ }
+ /**
+ * Same as Method.invoke(), but the possible exceptions are wrapped into
+ * RuntimeExceptions.
+ */
+ public Object invoke(Object obj, Object... args) {
+ try {
+ return method.invoke(obj, args);
+ } catch (IllegalArgumentException e) {
+ throw new RuntimeException("Error while invoking method '"+method+"'. "+
+ "Please report this as a bug.",e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Error while invoking method '"+method+"'. "+
+ "Please report this as a bug.",e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException("Error while invoking method '"+method+"'. "+
+ "Please report this as a bug.",e);
+ }
+ }
+ /**
+ * Invoke static method. Equivalent to invoke(null, args...).
+ */
+ public Object invokeStatic(Object... args) {
+ return invoke(null,args);
+ }
+ /**
+ * Same as Method.toString().
+ */
+ @Override
+ public String toString() {
+ return method.toString();
+ }
+ }
+
+
+ /**
+ * Throws an exception if method not found.
+ */
+ public static Reflection.Method findMethodStatic(
+ Class<? extends RocketComponent> componentClass,
+ String method, Class<?>... params) {
+ Reflection.Method m = findMethod(ROCKETCOMPONENT_PACKAGE, componentClass,
+ "", method, params);
+ if (m == null) {
+ throw new RuntimeException("Could not find method for componentClass="
+ +componentClass+" method="+method);
+ }
+ return m;
+ }
+
+
+
+ public static Reflection.Method findMethod(String pack, RocketComponent component,
+ String method, Class<?>...params) {
+ return findMethod(pack,component.getClass(),"",method,params);
+ }
+
+
+ public static Reflection.Method findMethod(String pack, RocketComponent component,
+ String suffix, String method, Class<?>... params) {
+ return findMethod(pack, component.getClass(), suffix, method, params);
+ }
+
+
+ public static Reflection.Method findMethod(String pack,
+ Class<? extends RocketComponent> componentClass,
+ String suffix, String method, Class<?>... params) {
+ Class<?> currentclass;
+ String name;
+
+ currentclass = componentClass;
+ while ((currentclass != null) && (currentclass != Object.class)) {
+ name = currentclass.getCanonicalName();
+ if (name.lastIndexOf('.')>=0)
+ name = name.substring(name.lastIndexOf(".")+1);
+ name = pack + "." + name + suffix;
+
+ try {
+ Class<?> c = Class.forName(name);
+ java.lang.reflect.Method m = c.getMethod(method,params);
+ return new Reflection.Method(m);
+ } catch (ClassNotFoundException ignore) {
+ } catch (NoSuchMethodException ignore) {
+ }
+
+ currentclass = currentclass.getSuperclass();
+ }
+ return null;
+ }
+
+
+ public static Object construct(String pack, RocketComponent component, String suffix,
+ Object... params) {
+
+ Class<?> currentclass;
+ String name;
+
+ currentclass = component.getClass();
+ while ((currentclass != null) && (currentclass != Object.class)) {
+ name = currentclass.getCanonicalName();
+ if (name.lastIndexOf('.')>=0)
+ name = name.substring(name.lastIndexOf(".")+1);
+ name = pack + "." + name + suffix;
+
+ try {
+ Class<?> c = Class.forName(name);
+ Class<?>[] paramClasses = new Class<?>[params.length];
+ for (int i=0; i < params.length; i++) {
+ paramClasses[i] = params[i].getClass();
+ }
+
+ // Constructors must be searched manually. Why?!
+ main: for (Constructor<?> constructor: c.getConstructors()) {
+ Class<?>[] parameterTypes = constructor.getParameterTypes();
+ if (params.length != parameterTypes.length)
+ continue;
+ for (int i=0; i < params.length; i++) {
+ if (!parameterTypes[i].isInstance(params[i]))
+ continue main;
+ }
+ // Matching constructor found
+ return constructor.newInstance(params);
+ }
+ } catch (ClassNotFoundException ignore) {
+ } catch (IllegalArgumentException e) {
+ throw new RuntimeException("Construction of "+name+" failed",e);
+ } catch (InstantiationException e) {
+ throw new RuntimeException("Construction of "+name+" failed",e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Construction of "+name+" failed",e);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException("Construction of "+name+" failed",e);
+ }
+
+ currentclass = currentclass.getSuperclass();
+ }
+ throw new RuntimeException("Suitable constructor for component "+component+
+ " not found");
+ }
+}
--- /dev/null
+package net.sf.openrocket.util;
+
+public class Rotation2D {
+
+ public static final Rotation2D ID = new Rotation2D(0.0, 1.0);
+
+ public final double sin, cos;
+
+
+ public Rotation2D(double angle) {
+ this(Math.sin(angle), Math.cos(angle));
+ }
+
+ public Rotation2D(double sin, double cos) {
+ this.sin = sin;
+ this.cos = cos;
+ }
+
+ public Coordinate rotateX(Coordinate c) {
+ return new Coordinate(c.x, cos*c.y - sin*c.z, cos*c.z + sin*c.y, c.weight);
+ }
+
+ public Coordinate rotateY(Coordinate c) {
+ return new Coordinate(cos*c.x + sin*c.z, c.y, cos*c.z - sin*c.x, c.weight);
+ }
+
+ public Coordinate rotateZ(Coordinate c) {
+ return new Coordinate(cos*c.x - sin*c.y, cos*c.y + sin*c.x, c.z, c.weight);
+ }
+
+
+ public Coordinate invRotateX(Coordinate c) {
+ return new Coordinate(c.x, cos*c.y + sin*c.z, cos*c.z - sin*c.y, c.weight);
+ }
+
+ public Coordinate invRotateY(Coordinate c) {
+ return new Coordinate(cos*c.x - sin*c.z, c.y, cos*c.z + sin*c.x, c.weight);
+ }
+
+ public Coordinate invRotateZ(Coordinate c) {
+ return new Coordinate(cos*c.x + sin*c.y, cos*c.y - sin*c.x, c.z, c.weight);
+ }
+
+
+
+ public static void main(String arg[]) {
+ Coordinate c = new Coordinate(1,1,1,2.5);
+ Rotation2D rot = new Rotation2D(Math.PI/4);
+
+ System.out.println("X: "+rot.rotateX(c));
+ System.out.println("Y: "+rot.rotateY(c));
+ System.out.println("Z: "+rot.rotateZ(c));
+ System.out.println("invX: "+rot.invRotateX(c));
+ System.out.println("invY: "+rot.invRotateY(c));
+ System.out.println("invZ: "+rot.invRotateZ(c));
+ }
+
+}
--- /dev/null
+package net.sf.openrocket.util;
+
+import net.sf.openrocket.database.Databases;
+import net.sf.openrocket.material.Material;
+import net.sf.openrocket.rocketcomponent.BodyTube;
+import net.sf.openrocket.rocketcomponent.Bulkhead;
+import net.sf.openrocket.rocketcomponent.CenteringRing;
+import net.sf.openrocket.rocketcomponent.FreeformFinSet;
+import net.sf.openrocket.rocketcomponent.InnerTube;
+import net.sf.openrocket.rocketcomponent.LaunchLug;
+import net.sf.openrocket.rocketcomponent.MassComponent;
+import net.sf.openrocket.rocketcomponent.Motor;
+import net.sf.openrocket.rocketcomponent.NoseCone;
+import net.sf.openrocket.rocketcomponent.Rocket;
+import net.sf.openrocket.rocketcomponent.Stage;
+import net.sf.openrocket.rocketcomponent.Transition;
+import net.sf.openrocket.rocketcomponent.TrapezoidFinSet;
+import net.sf.openrocket.rocketcomponent.TubeCoupler;
+import net.sf.openrocket.rocketcomponent.FinSet.CrossSection;
+import net.sf.openrocket.rocketcomponent.RocketComponent.Position;
+
+public class Test {
+
+ public static double noseconeLength=0.10,noseconeRadius=0.01;
+ public static double bodytubeLength=0.20,bodytubeRadius=0.01,bodytubeThickness=0.001;
+
+ public static int finCount=3;
+ public static double finRootChord=0.04,finTipChord=0.05,finSweep=0.01,finThickness=0.003, finHeight=0.03;
+
+ public static double materialDensity=1000; // kg/m3
+
+
+ public static Rocket makeRocket() {
+ Rocket rocket;
+ Stage stage,stage2;
+ NoseCone nosecone;
+ BodyTube bodytube, bt2;
+ Transition transition;
+ TrapezoidFinSet finset;
+
+ rocket = new Rocket();
+ stage = new Stage();
+ stage.setName("Stage1");
+ stage2 = new Stage();
+ stage2.setName("Stage2");
+ nosecone = new NoseCone(Transition.Shape.ELLIPSOID,noseconeLength,noseconeRadius);
+ bodytube = new BodyTube(bodytubeLength,bodytubeRadius,bodytubeThickness);
+ transition = new Transition();
+ bt2 = new BodyTube(bodytubeLength,bodytubeRadius*2,bodytubeThickness);
+ bt2.setMotorMount(true);
+
+ finset = new TrapezoidFinSet(finCount,finRootChord,finTipChord,finSweep,finHeight);
+
+
+ // Stage construction
+ rocket.addChild(stage);
+ rocket.addChild(stage2);
+
+
+ // Component construction
+ stage.addChild(nosecone);
+
+ stage.addChild(bodytube);
+
+
+ stage2.addChild(transition);
+
+ stage2.addChild(bt2);
+
+ bodytube.addChild(finset);
+
+
+ rocket.getDefaultConfiguration().setAllStages();
+
+ return rocket;
+ }
+
+
+ public static Rocket makeSmallFlyable() {
+ Rocket rocket;
+ Stage stage;
+ NoseCone nosecone;
+ BodyTube bodytube;
+ TrapezoidFinSet finset;
+
+ rocket = new Rocket();
+ stage = new Stage();
+ stage.setName("Stage1");
+
+ nosecone = new NoseCone(Transition.Shape.ELLIPSOID,noseconeLength,noseconeRadius);
+ bodytube = new BodyTube(bodytubeLength,bodytubeRadius,bodytubeThickness);
+
+ finset = new TrapezoidFinSet(finCount,finRootChord,finTipChord,finSweep,finHeight);
+
+
+ // Stage construction
+ rocket.addChild(stage);
+
+
+ // Component construction
+ stage.addChild(nosecone);
+ stage.addChild(bodytube);
+
+ bodytube.addChild(finset);
+
+ Material material = Prefs.getDefaultComponentMaterial(null, Material.Type.BULK);
+ nosecone.setMaterial(material);
+ bodytube.setMaterial(material);
+ finset.setMaterial(material);
+
+ String id = rocket.newMotorConfigurationID();
+ bodytube.setMotorMount(true);
+
+ for (Motor m: Databases.MOTOR) {
+ if (m.getDesignation().equals("B4")) {
+ bodytube.setMotor(id, m);
+ break;
+ }
+ }
+ bodytube.setMotorOverhang(0.005);
+ rocket.getDefaultConfiguration().setMotorConfigurationID(id);
+
+ rocket.getDefaultConfiguration().setAllStages();
+
+
+ return rocket;
+ }
+
+
+ public static Rocket makeBigBlue() {
+ Rocket rocket;
+ Stage stage;
+ NoseCone nosecone;
+ BodyTube bodytube;
+ FreeformFinSet finset;
+ MassComponent mcomp;
+
+ rocket = new Rocket();
+ stage = new Stage();
+ stage.setName("Stage1");
+
+ nosecone = new NoseCone(Transition.Shape.ELLIPSOID,0.105,0.033);
+ nosecone.setThickness(0.001);
+ bodytube = new BodyTube(0.69,0.033,0.001);
+
+ finset = new FreeformFinSet();
+ finset.setPoints(new Coordinate[] {
+ new Coordinate(0, 0),
+ new Coordinate(0.115, 0.072),
+ new Coordinate(0.255, 0.072),
+ new Coordinate(0.255, 0.037),
+ new Coordinate(0.150, 0)
+ });
+ finset.setThickness(0.003);
+ finset.setFinCount(4);
+
+ finset.setCantAngle(0*Math.PI/180);
+ System.err.println("Fin cant angle: "+(finset.getCantAngle() * 180/Math.PI));
+
+ mcomp = new MassComponent(0.2,0.03,0.045 + 0.060);
+ mcomp.setRelativePosition(Position.TOP);
+ mcomp.setPositionValue(0);
+
+ // Stage construction
+ rocket.addChild(stage);
+ rocket.setPerfectFinish(false);
+
+
+ // Component construction
+ stage.addChild(nosecone);
+ stage.addChild(bodytube);
+
+ bodytube.addChild(finset);
+
+ bodytube.addChild(mcomp);
+
+// Material material = new Material("Test material", 500);
+// nosecone.setMaterial(material);
+// bodytube.setMaterial(material);
+// finset.setMaterial(material);
+
+ String id = rocket.newMotorConfigurationID();
+ bodytube.setMotorMount(true);
+
+ for (Motor m: Databases.MOTOR) {
+ if (m.getDesignation().equals("F12J")) {
+ bodytube.setMotor(id, m);
+ break;
+ }
+ }
+ bodytube.setMotorOverhang(0.005);
+ rocket.getDefaultConfiguration().setMotorConfigurationID(id);
+
+ rocket.getDefaultConfiguration().setAllStages();
+
+
+ return rocket;
+ }
+
+
+
+ public static Rocket makeIsoHaisu() {
+ Rocket rocket;
+ Stage stage;
+ NoseCone nosecone;
+ BodyTube tube1, tube2, tube3;
+ TrapezoidFinSet finset;
+ TrapezoidFinSet auxfinset;
+ MassComponent mcomp;
+
+ final double R = 0.07;
+
+ rocket = new Rocket();
+ stage = new Stage();
+ stage.setName("Stage1");
+
+ nosecone = new NoseCone(Transition.Shape.OGIVE,0.53,R);
+ nosecone.setThickness(0.005);
+ nosecone.setMassOverridden(true);
+ nosecone.setOverrideMass(0.588);
+ stage.addChild(nosecone);
+
+ tube1 = new BodyTube(0.505,R,0.005);
+ tube1.setMassOverridden(true);
+ tube1.setOverrideMass(0.366);
+ stage.addChild(tube1);
+
+ tube2 = new BodyTube(0.605,R,0.005);
+ tube2.setMassOverridden(true);
+ tube2.setOverrideMass(0.427);
+ stage.addChild(tube2);
+
+ tube3 = new BodyTube(1.065,R,0.005);
+ tube3.setMassOverridden(true);
+ tube3.setOverrideMass(0.730);
+ stage.addChild(tube3);
+
+
+ LaunchLug lug = new LaunchLug();
+ tube1.addChild(lug);
+
+ TubeCoupler coupler = new TubeCoupler();
+ coupler.setOuterRadiusAutomatic(true);
+ coupler.setThickness(0.005);
+ coupler.setLength(0.28);
+ coupler.setMassOverridden(true);
+ coupler.setOverrideMass(0.360);
+ coupler.setRelativePosition(Position.BOTTOM);
+ coupler.setPositionValue(-0.14);
+ tube1.addChild(coupler);
+
+
+ // Parachute
+ MassComponent mass = new MassComponent(0.05, 0.05, 0.280);
+ mass.setRelativePosition(Position.TOP);
+ mass.setPositionValue(0.2);
+ tube1.addChild(mass);
+
+ // Cord
+ mass = new MassComponent(0.05, 0.05, 0.125);
+ mass.setRelativePosition(Position.TOP);
+ mass.setPositionValue(0.2);
+ tube1.addChild(mass);
+
+ // Payload
+ mass = new MassComponent(0.40, R, 1.500);
+ mass.setRelativePosition(Position.TOP);
+ mass.setPositionValue(0.25);
+ tube1.addChild(mass);
+
+
+ auxfinset = new TrapezoidFinSet();
+ auxfinset.setName("CONTROL");
+ auxfinset.setFinCount(2);
+ auxfinset.setRootChord(0.05);
+ auxfinset.setTipChord(0.05);
+ auxfinset.setHeight(0.10);
+ auxfinset.setSweep(0);
+ auxfinset.setThickness(0.008);
+ auxfinset.setCrossSection(CrossSection.AIRFOIL);
+ auxfinset.setRelativePosition(Position.TOP);
+ auxfinset.setPositionValue(0.28);
+ auxfinset.setBaseRotation(Math.PI/2);
+ tube1.addChild(auxfinset);
+
+
+
+
+ coupler = new TubeCoupler();
+ coupler.setOuterRadiusAutomatic(true);
+ coupler.setLength(0.28);
+ coupler.setRelativePosition(Position.TOP);
+ coupler.setPositionValue(0.47);
+ coupler.setMassOverridden(true);
+ coupler.setOverrideMass(0.360);
+ tube2.addChild(coupler);
+
+
+
+ // Parachute
+ mass = new MassComponent(0.1, 0.05, 0.028);
+ mass.setRelativePosition(Position.TOP);
+ mass.setPositionValue(0.14);
+ tube2.addChild(mass);
+
+ Bulkhead bulk = new Bulkhead();
+ bulk.setOuterRadiusAutomatic(true);
+ bulk.setMassOverridden(true);
+ bulk.setOverrideMass(0.050);
+ bulk.setRelativePosition(Position.TOP);
+ bulk.setPositionValue(0.27);
+ tube2.addChild(bulk);
+
+ // Chord
+ mass = new MassComponent(0.1, 0.05, 0.125);
+ mass.setRelativePosition(Position.TOP);
+ mass.setPositionValue(0.19);
+ tube2.addChild(mass);
+
+
+
+ InnerTube inner = new InnerTube();
+ inner.setOuterRadius(0.08/2);
+ inner.setInnerRadius(0.0762/2);
+ inner.setLength(0.86);
+ inner.setMassOverridden(true);
+ inner.setOverrideMass(0.388);
+ tube3.addChild(inner);
+
+
+ CenteringRing center = new CenteringRing();
+ center.setInnerRadiusAutomatic(true);
+ center.setOuterRadiusAutomatic(true);
+ center.setLength(0.005);
+ center.setMassOverridden(true);
+ center.setOverrideMass(0.038);
+ center.setRelativePosition(Position.BOTTOM);
+ center.setPositionValue(0);
+ tube3.addChild(center);
+
+
+ center = new CenteringRing();
+ center.setInnerRadiusAutomatic(true);
+ center.setOuterRadiusAutomatic(true);
+ center.setLength(0.005);
+ center.setMassOverridden(true);
+ center.setOverrideMass(0.038);
+ center.setRelativePosition(Position.TOP);
+ center.setPositionValue(0.28);
+ tube3.addChild(center);
+
+
+ center = new CenteringRing();
+ center.setInnerRadiusAutomatic(true);
+ center.setOuterRadiusAutomatic(true);
+ center.setLength(0.005);
+ center.setMassOverridden(true);
+ center.setOverrideMass(0.038);
+ center.setRelativePosition(Position.TOP);
+ center.setPositionValue(0.83);
+ tube3.addChild(center);
+
+
+
+
+
+ finset = new TrapezoidFinSet();
+ finset.setRootChord(0.495);
+ finset.setTipChord(0.1);
+ finset.setHeight(0.185);
+ finset.setThickness(0.005);
+ finset.setSweep(0.3);
+ finset.setRelativePosition(Position.BOTTOM);
+ finset.setPositionValue(-0.03);
+ finset.setBaseRotation(Math.PI/2);
+ tube3.addChild(finset);
+
+
+ finset.setCantAngle(0*Math.PI/180);
+ System.err.println("Fin cant angle: "+(finset.getCantAngle() * 180/Math.PI));
+
+
+ // Stage construction
+ rocket.addChild(stage);
+ rocket.setPerfectFinish(false);
+
+
+
+ String id = rocket.newMotorConfigurationID();
+ tube3.setMotorMount(true);
+
+ for (Motor m: Databases.MOTOR) {
+ if (m.getDesignation().equals("L540")) {
+ tube3.setMotor(id, m);
+ break;
+ }
+ }
+ tube3.setMotorOverhang(0.02);
+ rocket.getDefaultConfiguration().setMotorConfigurationID(id);
+
+// tube3.setIgnitionEvent(MotorMount.IgnitionEvent.NEVER);
+
+ rocket.getDefaultConfiguration().setAllStages();
+
+
+ return rocket;
+ }
+
+
+
+}
--- /dev/null
+package net.sf.openrocket.util;
+
+import java.util.*;
+
+/**
+ * Defines an affine transformation of the form A*x+c, where x and c are Coordinates and
+ * A is a 3x3 matrix.
+ *
+ * The Transformations are immutable. All modification methods return a new transformation.
+ *
+ * @author Sampo Niskanen <sampo.niskanen@iki.fi>
+ */
+
+public class Transformation implements java.io.Serializable {
+
+
+ public static final Transformation IDENTITY =
+ new Transformation();
+
+ public static final Transformation PROJECT_XY =
+ new Transformation(new double[][]{{1,0,0},{0,1,0},{0,0,0}});
+ public static final Transformation PROJECT_YZ =
+ new Transformation(new double[][]{{0,0,0},{0,1,0},{0,0,1}});
+ public static final Transformation PROJECT_XZ =
+ new Transformation(new double[][]{{1,0,0},{0,0,0},{0,0,1}});
+
+ private static final int X = 0;
+ private static final int Y = 1;
+ private static final int Z = 2;
+
+ private final Coordinate translate;
+ private final double[][] rotation = new double[3][3];
+
+ /**
+ * Create identity transformation.
+ */
+ public Transformation() {
+ translate = new Coordinate(0,0,0);
+ rotation[X][X]=1;
+ rotation[Y][Y]=1;
+ rotation[Z][Z]=1;
+ }
+
+ /**
+ * Create transformation with only translation.
+ * @param x Translation in x-axis.
+ * @param y Translation in y-axis.
+ * @param z Translation in z-axis.
+ */
+ public Transformation(double x,double y,double z) {
+ translate = new Coordinate(x,y,z);
+ rotation[X][X]=1;
+ rotation[Y][Y]=1;
+ rotation[Z][Z]=1;
+ }
+
+ /**
+ * Create transformation with only translation.
+ * @param translation The translation term.
+ */
+ public Transformation(Coordinate translation) {
+ this.translate = translation;
+ rotation[X][X]=1;
+ rotation[Y][Y]=1;
+ rotation[Z][Z]=1;
+ }
+
+ /**
+ * Create transformation with given rotation matrix and translation.
+ * @param rotation
+ * @param translation
+ */
+ public Transformation(double[][] rotation, Coordinate translation) {
+ for (int i=0; i<3; i++)
+ for (int j=0; j<3; j++)
+ this.rotation[i][j] = rotation[i][j];
+ this.translate = translation;
+ }
+
+
+ /**
+ * Create transformation with given rotation matrix and translation.
+ * @param rotation
+ * @param translation
+ */
+ public Transformation(double[][] rotation) {
+ for (int i=0; i<3; i++)
+ for (int j=0; j<3; j++)
+ this.rotation[i][j] = rotation[i][j];
+ this.translate = Coordinate.NUL;
+ }
+
+
+
+
+
+ /**
+ * Transform a coordinate according to this transformation.
+ *
+ * @param orig the coordinate to transform.
+ * @return the result.
+ */
+ public Coordinate transform(Coordinate orig) {
+ final double x,y,z;
+
+ x = rotation[X][X]*orig.x + rotation[X][Y]*orig.y + rotation[X][Z]*orig.z + translate.x;
+ y = rotation[Y][X]*orig.x + rotation[Y][Y]*orig.y + rotation[Y][Z]*orig.z + translate.y;
+ z = rotation[Z][X]*orig.x + rotation[Z][Y]*orig.y + rotation[Z][Z]*orig.z + translate.z;
+
+ return new Coordinate(x,y,z,orig.weight);
+ }
+
+
+ /**
+ * Transform an array of coordinates. The transformed coordinates are stored
+ * in the same array, and the array is returned.
+ *
+ * @param orig the coordinates to transform.
+ * @return <code>orig</code>, with the coordinates transformed.
+ */
+ public Coordinate[] transform(Coordinate[] orig) {
+ for (int i=0; i < orig.length; i++) {
+ orig[i] = transform(orig[i]);
+ }
+ return orig;
+ }
+
+ /**
+ * Transforms all coordinates in a Collection. The original coordinate elements are
+ * removed from the set and replaced with the transformed ones. The Collection given
+ * must implement the .clear() and .addAll() methods.
+ *
+ * @param set Collection of coordinates to transform.
+ */
+ public void transform(Collection<Coordinate> set) {
+ ArrayList<Coordinate> temp = new ArrayList<Coordinate>(set.size());
+ Iterator<Coordinate> iter = set.iterator();
+ while (iter.hasNext())
+ temp.add(this.transform(iter.next()));
+ set.clear();
+ set.addAll(temp);
+ }
+
+ /**
+ * Applies only the linear transformation A*x
+ * @param orig Coordinate to transform.
+ */
+ public Coordinate linearTransform(Coordinate orig) {
+ final double x,y,z;
+
+ x = rotation[X][X]*orig.x + rotation[X][Y]*orig.y + rotation[X][Z]*orig.z;
+ y = rotation[Y][X]*orig.x + rotation[Y][Y]*orig.y + rotation[Y][Z]*orig.z;
+ z = rotation[Z][X]*orig.x + rotation[Z][Y]*orig.y + rotation[Z][Z]*orig.z;
+
+ return new Coordinate(x,y,z,orig.weight);
+ }
+
+ /**
+ * Applies the given transformation before this tranformation. The resulting
+ * transformation result.transform(c) will equal this.transform(other.transform(c)).
+ *
+ * @param other Transformation to apply
+ * @return The new transformation
+ */
+ public Transformation applyTransformation(Transformation other) {
+ // other = Ax+b
+ // this = Cx+d
+ // C(Ax+b)+d = CAx + Cb+d
+
+ // Translational portion
+ Transformation combined = new Transformation(
+ this.linearTransform(other.translate).add(this.translate)
+ );
+
+ // Linear portion
+ for (int i=0; i<3; i++) {
+ final double x,y,z;
+ x = rotation[i][X];
+ y = rotation[i][Y];
+ z = rotation[i][Z];
+ combined.rotation[i][X] =
+ x*other.rotation[X][X] + y*other.rotation[Y][X] + z*other.rotation[Z][X];
+ combined.rotation[i][Y] =
+ x*other.rotation[X][Y] + y*other.rotation[Y][Y] + z*other.rotation[Z][Y];
+ combined.rotation[i][Z] =
+ x*other.rotation[X][Z] + y*other.rotation[Y][Z] + z*other.rotation[Z][Z];
+ }
+ return combined;
+ }
+
+
+ /**
+ * Rotate around x-axis a given angle.
+ * @param theta The angle to rotate in radians.
+ * @return The transformation.
+ */
+ public static Transformation rotate_x(double theta) {
+ return new Transformation(new double[][]{
+ {1,0,0},
+ {0,Math.cos(theta),-Math.sin(theta)},
+ {0,Math.sin(theta),Math.cos(theta)}});
+ }
+
+ /**
+ * Rotate around y-axis a given angle.
+ * @param theta The angle to rotate in radians.
+ * @return The transformation.
+ */
+ public static Transformation rotate_y(double theta) {
+ return new Transformation(new double[][]{
+ {Math.cos(theta),0,Math.sin(theta)},
+ {0,1,0},
+ {-Math.sin(theta),0,Math.cos(theta)}});
+ }
+
+ /**
+ * Rotate around z-axis a given angle.
+ * @param theta The angle to rotate in radians.
+ * @return The transformation.
+ */
+ public static Transformation rotate_z(double theta) {
+ return new Transformation(new double[][]{
+ {Math.cos(theta),-Math.sin(theta),0},
+ {Math.sin(theta),Math.cos(theta),0},
+ {0,0,1}});
+ }
+
+
+
+ public void print(String... str) {
+ for (String s: str) {
+ System.out.println(s);
+ }
+ System.out.printf("[%3.2f %3.2f %3.2f] [%3.2f]\n",
+ rotation[X][X],rotation[X][Y],rotation[X][Z],translate.x);
+ System.out.printf("[%3.2f %3.2f %3.2f] + [%3.2f]\n",
+ rotation[Y][X],rotation[Y][Y],rotation[Y][Z],translate.y);
+ System.out.printf("[%3.2f %3.2f %3.2f] [%3.2f]\n",
+ rotation[Z][X],rotation[Z][Y],rotation[Z][Z],translate.z);
+ System.out.println();
+ }
+
+
+ public static void main(String[] arg) {
+ Transformation t;
+
+ t = new Transformation();
+ t.print("Empty");
+ t = new Transformation(1,2,3);
+ t.print("1,2,3");
+ t = new Transformation(new Coordinate(2,3,4));
+ t.print("coord 2,3 4");
+
+ t = Transformation.rotate_y(0.01);
+ t.print("rotate_y 0.01");
+
+ t = new Transformation(-1,0,0);
+ t = t.applyTransformation(Transformation.rotate_y(0.01));
+ t = t.applyTransformation(new Transformation(1,0,0));
+ t.print("shift-rotate-shift");
+ }
+
+}