commit 7bce78ae08683d013e15fdda7363e57470d82281 Author: Jeff Laughlin Date: Mon Jan 6 22:39:16 2014 -0500 initial commit diff --git a/AGPL3.txt b/AGPL3.txt new file mode 100644 index 0000000..dba13ed --- /dev/null +++ b/AGPL3.txt @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are 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. + + 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. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + 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 Affero 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. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + 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 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 work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero 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 Affero 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 Affero 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 Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + 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 AGPL, see +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0d86728 --- /dev/null +++ b/README.md @@ -0,0 +1,10 @@ + +Includes: +* QRZ.com lookup +* ADIF file parsing +* Cabrillo file parsing +* cty.dat parsing and querying +* Logged callsign georeferencing +* Geojson output +* KML output + diff --git a/geolog b/geolog new file mode 100755 index 0000000..0c47fa2 --- /dev/null +++ b/geolog @@ -0,0 +1,23 @@ +#!/usr/bin/env python +# +# Copyright 2009, 2012, 2014 by Jeffrey M. Laughlin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +import sys + +from hamtools.geolog import main + +sys.exit(main()) + diff --git a/geolog.ini b/geolog.ini new file mode 100644 index 0000000..eff542c --- /dev/null +++ b/geolog.ini @@ -0,0 +1,5 @@ +[qrz] +username=myusername +password=mypassword +#cachepath=/home/jeff/.qrz_cache + diff --git a/hamtools/__init__.py b/hamtools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hamtools/adif.py b/hamtools/adif.py new file mode 100644 index 0000000..12fa644 --- /dev/null +++ b/hamtools/adif.py @@ -0,0 +1,181 @@ +#!/usr/bin/env python +# +# Copyright 2009, 2012, 2013 by Jeffrey M. Laughlin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +""" +Python API for reading the Amateur Data Interchange Format, aka ADIF. + +ADIF version 1.0 is supported. +""" + +from collections import namedtuple, OrderedDict +from decimal import Decimal +from datetime import datetime + + +BLOCKSIZE = 1024 + + +class ParseError(Exception): + pass + + +class Field(object): + __slots__ = ['name', 'type', 'body'] + def __init__(self, name='', type='', body=''): + self.name = name + self.type = type + self.body = body + + def __repr__(self): + return ' '.join(('')) + + def __eq__(self, s2): + if self.name == s2.name and self.type == s2.type and s2.body == self.body: + return True + return False + + def __ne__(self, s2): + if self.name == s2.name or self.type == s2.type or s2.body == self.body: + return False + return True + + +class Reader(object): + def __init__(self, flo): + self.adif_ver = None + self.flo = flo + # read header + flo.seek(0) + c = flo.read(1) + if c == '<': + self.header_present = False + flo.seek(0) + else: + self.header_present = True + for field in self._lex(blocksize=1): + if field.name == 'adif_ver': + self.adif_ver = Decimal(field.body) + elif field.name == 'eoh': + break + self.bookmark = flo.tell() + + def _lex(self, blocksize=BLOCKSIZE): + """Given a file like object, yield named tuple for each record.""" + flo = self.flo + state = 'comment' + pos = 0 + field = Field() + while True: + buf = flo.read(blocksize) + #print buf + for c in buf: + #print "c = '%s'" % c + if state == 'comment': + if c == '<': + state = 'name' + field = Field() + elif state == 'name': + if c == ':': + state = 'len' + field_len = '' + elif c == '>': + state = 'comment' + field.name = field.name.lower() + field.type = field.type.lower() + #print field + yield field + else: + field.name = field.name + c + elif state == 'len': + if c == ':': + state = 'type' + field_len = int(field_len) + elif c == '>': + state = 'body' + field_len = int(field_len) + else: + field_len = field_len + c + elif state == 'type': + if c == ':': + raise ParseError() + elif c == '>': + state = 'body' + else: + field.type = field.type + c + elif state == 'body': + if field_len > 0: + field.body = field.body + c + field_len -= 1 + if field_len == 0: + state = 'comment' + field.name = field.name.lower() + field.type = field.type.lower() + #print field + yield field + else: + raise Exception("Invalid state at %d" % pos) + pos += 1 + #print field + #print pos + if len(buf) < blocksize: + #print 'EOF' + break + + def __iter__(self): + """Iterate over records in file""" + self.flo.seek(self.bookmark) + rec = OrderedDict() + for field in self._lex(): + #print field + if field.name == 'eor': + #print 'yield rec' + if 'qso_date' in rec and 'time_on' in rec: + rec['app_datetime_on'] = datetime(int(rec['qso_date'][:4]), + int(rec['qso_date'][4:6]), + int(rec['qso_date'][6:8]), + int(rec['time_on'][:2]), + int(rec['time_on'][2:])) + if 'qso_date' in rec and 'time_off' in rec: + rec['app_datetime_off'] = datetime(int(rec['qso_date'][:4]), + int(rec['qso_date'][4:6]), + int(rec['qso_date'][6:8]), + int(rec['time_off'][:2]), + int(rec['time_off'][2:])) + yield rec + rec = OrderedDict() + else: + # TODO: type conversion and validation + rec[field.name] = field.body + #print rec + +def format_header(header_text=' ', adif_ver=None): + assert header_text != '' + if adif_ver is None: + return "%s" % header_text + return "%s%s" % ( + header_text, len(adif_ver), adif_ver) + +def format_record(record): + fields = [] + for k,v in record.iteritems(): + if k == 'app_datetime_on': + continue + fields.append("<%s:%d>%s" % (k, len(v), v)) + fields.append('') + return ''.join(fields) + diff --git a/hamtools/ctydat.py b/hamtools/ctydat.py new file mode 100644 index 0000000..8e3bbfe --- /dev/null +++ b/hamtools/ctydat.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python +""" +cty.dat file reader and lookup + +Copyright 2014 by Jeffrey M. Laughlin +Copyright (C) 2005-2009 Fabian Kurz, DJ1YFK + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . + +Ported from yfklog + +Alpha quality, largely untested; please help! +""" + +from collections import defaultdict +import re + +import pdb + + +class InvalidDxcc(Exception): + pass + + +class CtyDat(object): + fields = ['name', 'cq', 'itu', 'cont', 'lat', 'lon', 'utcoff', 'prefix'] + + def __init__(self, infile): + self.prefixes = defaultdict(list) + self.dxcc = {} + for line in infile: + if line[0] != ' ': + # DXCC line + line=line.strip() + fields = [f.strip() for f in line.split(':')] + dxcc = dict(zip(self.fields, fields)) + mainprefix = dxcc['prefix'] + self.dxcc[mainprefix] = dxcc + else: + line=line.strip() + line = line.rstrip(';') + line = line.rstrip(',') + prefixes = line.split(',') + self.prefixes[mainprefix].extend(prefixes) + + def getwpx(self, call): + prefix = None + a,b,c = None, None, None + fields = call.split('/') + try: a,b,c = fields + except Exception: + try: a,b = fields + except Exception: + a = fields + + if c is None and None not in (a,b): + if b in ('QRP', 'LGT'): + b = a + a = None + + if b.isdigit(): + raise Exception("invalid callsign %s" % call) + + if a is None and c is None: + if re.search('\d', b) is not None: + prefix = re.search('(.+\d)[A-Z]*', b).group(0) + else: + prefix = b[0:2] + '0' + elif a is None and c is not None: + if len(c) == 1 and c.isdigit(): + _1 = re.search('(.+\d)[A-Z]*', b).group(0) + mo = re.search('^([A-Z]\d)\d$', _1) + if mo is not None: + prefix = _1 + c + else: + mo = re.search('(.*[A-Z])\d+', _1) + prefix = mo.group(0) + c + elif re.search('(^P$)|(^M{1,2}$)|(^AM$)|(^A$)', c) is not None: + mo = re.search('(.+\d)[A-Z]*', b) + prefix = mo.group(0) + elif re.search('^\d\d+$/', c) is not None: + mo = re.search('(.+\d)[A-Z]*', b) + prefix = mo.group(0) + else: + if c[-1].isdigit(): + prefix = c + else: + prefix = c + '0' + elif a is not None: + if a[-1].isdigit(): + prefix = a + else: + prefix = a + '0' + return prefix + + def getdxcc(self, call): + matchchars = 0 + goodzone = None + matchprefix = None + if re.search('(^OH/)|(/OH[1-9]?$)', call) is not None: + call = 'OH' + elif re.search('(^3D2R)|(^3D2.+\/R)', call) is not None: + call = '3D2RR' + elif re.search('^3D2C', call) is not None: + call = '3D2CR' + elif '/' in call: + prefix = self.getwpx(call) + if prefix is None: + prefix = 'QQ' + call = prefix + 'AA' + + letter = call[0] + for mainprefix, tests in self.prefixes.iteritems(): + for test in tests: + testlen = len(test) + if letter != test[0]: + continue + zones = '' + if testlen > 5 and '(' in test or '[' in test: + mo = re.search('^([A-Z0-9\/]+)([\[\(].+)', test) + if mo.group(1) is not None: + zones += mo.group(1) + testlen = len(mo.group(0)) + + if call[:testlen] == test[:testlen] and matchchars <= testlen: + matchchars = testlen + matchprefix = mainprefix + goodzone = zones + + try: + mydxcc = self.dxcc[matchprefix] + except KeyError: + raise InvalidDxcc(matchprefix) + + if goodzone is not None: + mo = re.search('\((\d+)\)', goodzone) + if mo is not None: + mydxcc['cq'] = mo.group(0) + mo = re.search('\[(\d+)\]', goodzone) + if mo is not None: + mydxcc['itu'] = mo.group(0) + + if mydxcc['prefix'].startswith('*'): + if (mydxcc['prefix'] == '*TA1'): mydxcc['prefix'] = "TA" # Turkey + if (mydxcc['prefix'] == '*4U1V'): mydxcc['prefix'] = "OE" # 4U1VIC is in OE.. + if (mydxcc['prefix'] == '*GM/s'): mydxcc['prefix'] = "GM" # Shetlands + if (mydxcc['prefix'] == '*IG9'): mydxcc['prefix'] = "I" # African Italy + if (mydxcc['prefix'] == '*IT9'): mydxcc['prefix'] = "I" # Sicily + if (mydxcc['prefix'] == '*JW/b'): mydxcc['prefix'] = "JW" # Bear Island + + return mydxcc + diff --git a/hamtools/geolog.py b/hamtools/geolog.py new file mode 100755 index 0000000..f11df53 --- /dev/null +++ b/hamtools/geolog.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python +# +# Copyright 2009, 2012, 2014 by Jeffrey M. Laughlin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import argparse +import ConfigParser +import sys +import traceback +import os + +import qrz +import logging + +import geojson as gj + +from ctydat import CtyDat, InvalidDxcc + +log = logging.getLogger('geolog') +#log.setLevel(logging.INFO) + +# 1. Load log +# 2. Georeference log +# 3. Cache georeferenced calls +# 4. Output GeoJSON + +CABRILLO_FIELDS = ['header', 'freq', 'mode', 'date', 'time', + 'from_call', 'sent_qst', 'sent_ex', 'to_call', 'receved_qst', + 'received_ex'] + +CACHEPATH = os.path.join(os.environ['HOME'], '.qrz_cache') + +class Log(object): + def __init__(self): + self.qsos = [] + self.callsign = None + + @staticmethod + def from_cabrillo(logfile): + self = Log() + for line in logfile: + if line.startswith("QSO"): + qso = dict(zip(CABRILLO_FIELDS, line.split())) + qso['time'] = float(qso['time'] + '.000000001') + qso['freq'] = float(qso['freq'] + '.000000001') + self.qsos.append(qso) + log.debug(qso) + elif line.startswith("CALLSIGN:"): + self.callsign = line.split()[1] + log.info("Callsign: %s" % self.callsign) + log.info("Read %d records" % len(self.qsos)) + return self + + def georeference(self, sess, ctydat): + try: + rec = sess.qrz(self.callsign) + assert None not in (rec['lat'], rec['lon']) + self.lat, self.lon = rec['lat'], rec['lon'] + log.debug("qrz rec %s" % rec) + except Exception, e: + log.warning("QRZ lookup failed for %s" % self.callsign, exc_info=True) + raise + + for qso in self.qsos: + qso['lat'], qso['lon'] = None, None + try: + rec = sess.qrz(qso['to_call']) + log.debug("qrz rec %s" % rec) + if rec['call'] != qso['to_call']: + log.warning("qrz %s != %s" % (rec['call'], + qso['to_call'])) + assert None not in (rec['lat'], rec['lon']) + qso['lat'], qso['lon'] = rec['lat'], rec['lon'] + except Exception, e: + log.warning("QRZ lookup failed for %s" % qso['to_call'], exc_info=True) + try: + dxcc = ctydat.getdxcc(qso['to_call']) + qso['lat'] = float(dxcc['lat']) + qso['lon'] = float(dxcc['lon']) * -1 + except Exception: + log.warning("cty.dat lookup failed for %s" % qso['to_call'], exc_info=True) + raise + + def geojson_dumps(self, *args, **kwargs): + points = [] + lines = [] + for qso in self.qsos: + if None in (qso['lat'], qso['lon']): + log.warning("No coords %s" % qso) + continue + point = gj.Point((qso['lon'], qso['lat'])) + points.append(gj.Feature(geometry=point, + properties=qso)) + line = gj.LineString([ + (self.lon, self.lat), + (qso['lon'], qso['lat']) + ]) + lines.append(gj.Feature(geometry=line, + properties=qso)) + return ( + gj.dumps(gj.FeatureCollection(points), *args, **kwargs), + gj.dumps(gj.FeatureCollection(lines), *args, **kwargs), + ) + + +def geolog(logfile, outfile, username, password): + with open(logfile) as logfile: + log.info("Opened %r" % logfile) + qsolog = Log.from_cabrillo(logfile) + + with open('/home/jeff/Downloads/ctydat/cty.dat') as ctydat: + ctydat = CtyDat(ctydat) + with qrz.Session(username, password) as sess: + qsolog.georeference(sess, ctydat) + + points, lines = qsolog.geojson_dumps(sort_keys=True) + + pointfile = '_'.join((outfile, 'points.geojson')) + with open(pointfile, "w") as pointfile: + pointfile.write(points) + + linefile = '_'.join((outfile, 'lines.geojson')) + with open(linefile, "w") as linefile: + linefile.write(lines) + + +def main(argv=None): + if argv is None: + argv = sys.argv + + logging.basicConfig(level=logging.INFO) + + parser = argparse.ArgumentParser( + description= +"""Read ham log and output GIS data for callsigns worked. + +Output files will be prefixed with output path. +""") + parser.add_argument('infile', type=str, + help='Input log file (ADIF or Cabrillo)') + parser.add_argument('outpath', type=str, + help='Output path prefix') + parser.add_argument('-c', '--cfg', type=str, + help='Config file path', default=os.path.join(os.environ['HOME'], '.geologrc')) + parser.add_argument('-v', '--verbose', type=bool, + help='Turn on additional output', default=False) + args = parser.parse_args(argv[1:]) + + cfg = ConfigParser.SafeConfigParser() + + cfg.read(args.cfg) + + try: + un = cfg.get('qrz', 'username') + except ConfigParser.Error: + un = raw_input("QRZ.com user name:") + + try: + un = cfg.get('qrz', 'password') + except ConfigParser.Error: + pw = raw_input("QRZ.com password (not stored):") + + try: + cachepath = cfg.get('qrz', 'cachepath') + except ConfigParser.Error: + cachepath = CACHEPATH + + geolog(args.infile, args.outfile, un, pw, cachepath) + + return 0 + +if __name__ == "__main__": + sys.exit(main()) + diff --git a/hamtools/kml.py b/hamtools/kml.py new file mode 100755 index 0000000..dadfadd --- /dev/null +++ b/hamtools/kml.py @@ -0,0 +1,358 @@ +#!/usr/bin/env python +# +# Copyright 2009, 2012 by Jeffrey M. Laughlin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +from xml.dom.minidom import getDOMImplementation + +ALT_CLAMPED = "clampedToGround" +ALT_RELATIVE = "relativeToGround" +ALT_ABSOLUTE = "absolute" + +class KML(object): + + def __init__(self, comment=None): + impl = getDOMImplementation() + self.xml = impl.createDocument(None, "kml", None) + self.root = self.xml.documentElement + self.root.setAttribute("xmlns", "http://earth.google.com/kml/2.1") + if comment: + self.root.appendChild(self.xml.createComment(comment)) + + # dirty subclassing ;-) + def __getattr__(self, name): + if not hasattr(self.root, name): + raise AttributeError, "%s instance has no attribute '%s'" %\ + (self.__class__.__name__, name) + else: + return getattr(self.root, name) + + def createDocument(self, name, desc=None, visible=True): + doc = self.xml.createElement("Document") + + nm = self.xml.createElement('name') + nm.appendChild(self.xml.createTextNode(name)) + doc.appendChild(nm) + if desc: + elt = self.xml.createElement('description') + elt.appendChild(self.xml.createCDATASection(desc)) + doc.appendChild(elt) + if not visible: + elt = self.xml.createElement('visibility') + elt.appendChild(self.xml.createTextNode("0")) + doc.appendChild(elt) + + return doc + + def createIcon(self, iconUrl=None, x=None, y=None, w=None, h=None, dim=None): + icon = self.xml.createElement("Icon") + if iconUrl != None: + url = self.xml.createElement("href") + url.appendChild(self.xml.createTextNode(iconUrl)) + icon.appendChild(url) + if dim: + dimensions=dim + else: + dimensions = [ ('x', x), ('y', y), ('w', w), ('h', h) ] + for d in dimensions: + if d[1] != None: + elt = self.xml.createElement(d[0]) + elt.appendChild(self.xml.createTextNode(`d[1]`)) + icon.appendChild(elt) + return icon + + def createIconStyle(self, scale=None, icon=None, color=None): + istyle = self.xml.createElement("IconStyle") + if scale != None: + elt = self.xml.createElement("scale") + elt.appendChild(self.xml.createTextNode(`scale`)) + istyle.appendChild(elt) + if icon != None: + istyle.appendChild(icon) + if color: + elt = self.xml.createElement("color") + elt.appendChild(self.xml.createTextNode(color)) + istyle.appendChild(elt) + return istyle + + def createLabelStyle(self, scale=None, color=None, colorMode=None): + lstyle = self.xml.createElement("LabelStyle") + if scale != None: + elt = self.xml.createElement("scale") + elt.appendChild(self.xml.createTextNode(`scale`)) + lstyle.appendChild(elt) + if color: + elt = self.xml.createElement("color") + elt.appendChild(self.xml.createTextNode(color)) + lstyle.appendChild(elt) + if colorMode: + elt = self.xml.createElement("colorMode") + elt.appendChild(self.xml.createTextNode(colorMode)) + lstyle.appendChild(elt) + return lstyle + + def createPolyStyle(self, color=None, outline=True): + pstyle = self.xml.createElement("PolyStyle") + if color: + elt = self.xml.createElement("color") + elt.appendChild(self.xml.createTextNode(color)) + pstyle.appendChild(elt) + if not outline: + elt = self.xml.createElement("outline") + elt.appendChild(self.xml.createTextNode("0")) + pstyle.appendChild(elt) + + return pstyle + + def createLineStyle(self, color=None, width=None): + lstyle = self.xml.createElement("LineStyle") + if color: + elt = self.xml.createElement("color") + elt.appendChild(self.xml.createTextNode(color)) + lstyle.appendChild(elt) + if width != None: + elt = self.xml.createElement("width") + elt.appendChild(self.xml.createTextNode("%f" % width)) + lstyle.appendChild(elt) + + return lstyle + + def createBalloonStyle(self, text, color=None): + bstyle = self.xml.createElement("BalloonStyle") + elt = self.xml.createElement("text") + elt.appendChild(self.xml.createCDATASection(text)) + bstyle.appendChild(elt) + if color: + elt = self.xml.createElement("color") + elt.appendChild(self.xml.createTextNode(color)) + bstyle.appendChild(elt) + return bstyle + + def createStyle(self, id=None, children=None): + style = self.xml.createElement("Style") + if id: + style.setAttribute("id", id) + if children: + if type(children) == list: + for c in children: + style.appendChild(c) + else: + style.appendChild(children) + return style + + def createLookAt(self, lat, lon, range=None, tilt=None, heading=None): + pt = self.xml.createElement("LookAt") + elt = self.xml.createElement("longitude") + elt.appendChild(self.xml.createTextNode("%f" % lon)) + pt.appendChild(elt) + elt = self.xml.createElement("latitude") + elt.appendChild(self.xml.createTextNode("%f" % lat)) + pt.appendChild(elt) + if range != None: + elt = self.xml.createElement("range") + elt.appendChild(self.xml.createTextNode(`range`)) + pt.appendChild(elt) + if tilt != None: + elt = self.xml.createElement("tilt") + elt.appendChild(self.xml.createTextNode(`tilt`)) + pt.appendChild(elt) + if heading != None: + elt = self.xml.createElement("heading") + elt.appendChild(self.xml.createTextNode(`heading`)) + pt.appendChild(elt) + return pt + + def createFolder(self, name, isOpen=False): + fld = self.xml.createElement("Folder") + elt = self.xml.createElement("name") + elt.appendChild(self.xml.createTextNode(name)) + fld.appendChild(elt) + + if isOpen: + elt = self.xml.createElement("open") + elt.appendChild(self.xml.createTextNode("1")) + fld.appendChild(elt) + + return fld + + def createPlacemark(self, name, lat=None, lon=None, desc=None, style=None, range=None, tilt=None, alt=None, + heading=None, visible=True, altMode=None, timeStamp=None): + pm = self.xml.createElement("Placemark") + elt = self.xml.createElement("name") + elt.appendChild(self.xml.createTextNode(name)) + pm.appendChild(elt) + + if lat != None and lon != None: + pt = self.xml.createElement("Point") + elt = self.xml.createElement("coordinates") + coor = "%f,%f" % (lon, lat) + if alt != None: coor += ",%f" % alt + elt.appendChild(self.xml.createTextNode(coor)) + pt.appendChild(elt) + if altMode: + elt = self.xml.createElement("altitudeMode") + elt.appendChild(self.xml.createTextNode(altMode)) + pt.appendChild(elt) + pm.appendChild(pt) + + if range or tilt: + pm.appendChild(self.createLookAt(lat, lon, range, tilt, heading)) + + if style: + if type(style) == str or type(style) == unicode: + elt = self.xml.createElement("styleUrl") + elt.appendChild(self.xml.createTextNode(style)) + pm.appendChild(elt) + else: + pm.appendChild(style) + + if desc: + elt = self.xml.createElement("description") + elt.appendChild(self.xml.createCDATASection(desc)) + pm.appendChild(elt) + + if not visible: + elt = self.xml.createElement("visibility") + elt.appendChild(self.xml.createTextNode("0")) + pm.appendChild(elt) + + if timeStamp: + pm.appendChild(self.createTimeStamp(timeStamp)) + + return pm + + def createTimeStamp(self, dt, tz="Z"): + ts = self.xml.createElement("TimeStamp") + wh = self.xml.createElement("when") + wh.appendChild(self.xml.createTextNode(dt.strftime("%Y-%m-%dT%H:%M:%S") + tz)) + ts.appendChild(wh) + return ts + + def createMultiGeometry(self): + return self.xml.createElement("MultiGeometry") + + def createLineString(self, coords, altMode=None, tessel=False, extrude=False): + ls = self.xml.createElement("LineString") + + if tessel: + elt = self.xml.createElement("tessellate") + elt.appendChild(self.xml.createTextNode("1")) + ls.appendChild(elt) + + if extrude: + elt = self.xml.createElement("extrude") + elt.appendChild(self.xml.createTextNode("1")) + ls.appendChild(elt) + + if altMode: + elt = self.xml.createElement("altitudeMode") + elt.appendChild(self.xml.createTextNode(altMode)) + ls.appendChild(elt) + + elt = self.xml.createElement("coordinates") + coo = "" + for c in coords: + coo += "%f,%f,%f " % (c[1], c[0], c[2]) + elt.appendChild(self.xml.createTextNode(coo)) + + ls.appendChild(elt) + + return ls + + def createLink(self, url, # This is KML 2.1 / GoogleEarth > 4.x + refreshMode=None, refreshInterval=None): # TODO: all view... elts + ln = self.xml.createElement("Link") + elt = self.xml.createElement("href") + elt.appendChild(self.xml.createTextNode(url)) + ln.appendChild(elt) + if refreshMode != None: + if not refreshMode in [ "onChange", "onInterval", "onExpire" ]: + raise ValueError("Invalid refresh mode '%s'" % refreshMode) + elt = self.xml.createElement("refreshMode") + elt.appendChild(self.xml.createTextNode(refreshMode)) + ln.appendChild(elt) + if refreshInterval != None: + elt = self.xml.createElement("refreshInterval") + elt.appendChild(self.xml.createTextNode(str(refreshInterval))) + ln.appendChild(elt) + return ln + + def createNetworkLink(self, url, name=None, desc=None, + visible=True, isOpen=True, + refreshMode=None, refreshInterval=None): # TODO: all view... elts + nl = self.xml.createElement("NetworkLink") + ln = self.createLink(url, refreshMode, refreshInterval) + nl.appendChild(ln) + if name != None: + elt = self.xml.createElement("name") + elt.appendChild(self.xml.createTextNode(name)) + nl.appendChild(elt) + if desc != None: + elt = self.xml.createElement("description") + elt.appendChild(self.xml.createCDATASection(desc)) + nl.appendChild(elt) + if not visible: + elt = self.xml.createElement("visibility") + elt.appendChild(self.xml.createTextNode("0")) + nl.appendChild(elt) + if isOpen: + elt = self.xml.createElement("open") + elt.appendChild(self.xml.createTextNode("1")) + nl.appendChild(elt) + return nl + + + def createNetworkLinkControl(self, name=None, desc=None, + refresh=None, msg=None, cookie=None): + nlc = self.xml.createElement("NetworkLinkControl") + if name != None: + elt = self.xml.createElement("linkName") + elt.appendChild(self.xml.createTextNode(name)) + nlc.appendChild(elt) + if desc != None: + elt = self.xml.createElement("linkDescription") + elt.appendChild(self.xml.createCDATASection(desc)) + nlc.appendChild(elt) + if refresh != None: + elt = self.xml.createElement("minRefreshPeriod") + elt.appendChild(self.xml.createTextNode("%d" % refresh)) + nlc.appendChild(elt) + if msg != None: + elt = self.xml.createElement("message") + elt.appendChild(self.xml.createTextNode(msg)) + nlc.appendChild(elt) + if cookie != None: + elt = self.xml.createElement("cookie") + elt.appendChild(self.xml.createTextNode(cookie)) + nlc.appendChild(elt) + return nlc + + def writepretty(self, f, encoding="utf-8"): + self.xml.writexml(f, addindent=" ", newl="\n") + + def writeplain(self, f): + self.xml.writexml(f) + + write = writepretty + + # def appendChild(self, node): + # self.root.appendChild(node) + + + # root.appendChild(root) + +if __name__ == "__main__": + print "Syntax OK!" diff --git a/hamtools/log2kml.py b/hamtools/log2kml.py new file mode 100755 index 0000000..1bf40d1 --- /dev/null +++ b/hamtools/log2kml.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python +# +# Copyright 2009, 2012 by Jeffrey M. Laughlin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import sys +import qrz +import kml +import traceback + +class Log(object): + def __init__(self, call, lat, lon): + dom = self.dom = kml.KML() + doc = dom.createDocument(call + " log") + folder = self.folder = dom.createFolder(call + " log") + doc.appendChild(folder) + dom.root.appendChild(doc) + callnode = self.dom.createPlacemark(call, lat, lon) + self.folder.appendChild(callnode) + self.lat = lat + self.lon = lon + + def add_qso(self, qrzsession, call): + callrec = qrzsession.qrz(call) + lat, lon = callrec['lat'], callrec['lon'] + assert lat is not None + assert lon is not None + callnode = self.dom.createPlacemark(call, callrec["lat"], callrec["lon"]) + callnode2 = self.dom.createPlacemark(call, callrec["lat"], callrec["lon"]) + theline = self.dom.createLineString(((callrec["lat"], callrec["lon"],0),(self.lat,self.lon,0)), tessel=True) + self.folder.appendChild(callnode2) + callnode.appendChild(theline) + callnode.removeChild(callnode.childNodes[1]) + self.folder.appendChild(callnode) + + def save(self, file): + self.dom.writepretty(file) + +def log2kml(logfile, outfile, qrzsess, call, lat, lon): + mylog = Log(call, lat, lon) + for line in logfile: + if line.startswith("QSO"): + callsign = line[55:67].strip() + print '"' + callsign + '"' + try: + mylog.add_qso(qrzsess, callsign) + except Exception: + traceback.print_exc() + mylog.save(outfile) + + +if __name__ == "__main__": + logfile = open(sys.argv[1]) + outfile = open(sys.argv[2], "w") + + qrzs = qrz.Session("n1ywb", raw_input("password:")) + + log2kml(logfile, outfile, qrzs, "n1ywb", 44.197227, -72.486237) + diff --git a/hamtools/qrz.py b/hamtools/qrz.py new file mode 100755 index 0000000..fc0b451 --- /dev/null +++ b/hamtools/qrz.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python +# +# Copyright 2009, 2012, 2014 by Jeffrey M. Laughlin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import httplib, urllib +from xml.dom import minidom +import sys +import os +import sqlite3 +import logging + +log = logging.getLogger('qrz') + +CACHEPATH = os.path.join(os.environ['HOME'], '.qrz_cache') + +testSessionXML = """\ + + + + 2331uf894c4bd29f3923f3bacf02c532d7bd9 + Sun Aug 16 03:51:47 2006 + + +""" + +class NotFound(Exception): + pass + +class Callsign(object): + conversions = dict( + lat=float, + lon=float, + ) + + def __init__(self, node): + self.node = node + + def __getitem__(self, key): + try: + value = self.node.getElementsByTagName(key)[0].firstChild.data + except IndexError, e: + return None + if key in self.conversions.keys(): + value = self.conversions[key](value) + return value + +class Session(object): + def __init__(self, user, passwd, cachepath=CACHEPATH): + # post http://xml.qrz.com/xml?username=user;password=passwdo + # self.key = minidom parse key + # check for alert or error + xml = self.request(dict(username=user, password=passwd)) + log.debug(xml) + dom = minidom.parseString(xml) + session = dom.getElementsByTagName("Session")[0] + self.checkErr(session) + self.key = session.getElementsByTagName("Key")[0].firstChild.data + self.db = sqlite3.connect(cachepath) + try: + self.db.execute(""" + create table dict ( + key, + val + ) + """) + self.db.commit(); + except sqlite3.OperationalError: + log.warning("maybe cache table exists?") + + def request(self, params): + hc = httplib.HTTPConnection("xml.qrz.com") + headers = {"Content-type": "application/x-www-form-urlencoded", + "Accept": "text/plain"} + + hc.request("POST", "/xml", urllib.urlencode(params), headers) + resp = hc.getresponse() + if resp.status != httplib.OK: + raise Exception("Status %d" % resp.status) + return resp.read() + + def checkErr(self, session): + errs = [] + try: + errs = session.getElementsByTagName("Error") + except Exception: + pass + if len(errs) > 0: + err = errs[0].firstChild.data + if err.startswith("Not found"): + raise NotFound(err) + else: + raise Exception(err) + + alert = None + try: + alert = session.getElementsByTagName("Alert")[0].firstChild.data + except Exception: + pass + if alert is not None: + raise Exception(alert) + + + def qrz(self, callsign): + # http://xml.qrz.com/xml?s=2331uf894c4bd29f3923f3bacf02c532d7bd9;callsign=aa7bq + log.debug("qrz %s" % callsign) + miss = True + c = self.db.cursor() + xml = c.execute('select val from dict where key == ?', + (callsign,)).fetchone() + if not xml: + xml = self.request(dict(s=self.key,callsign=callsign)) + log.debug("miss %s" % callsign) + else: + miss = False + xml = xml[0] + log.debug("hit %s" % callsign) + try: + dom = minidom.parseString(xml) + session = dom.getElementsByTagName("Session")[0] + self.checkErr(session) + if miss: + self.db.execute("insert into dict values (?, ?)", (callsign, xml)) + self.db.commit(); + callnode = dom.getElementsByTagName("Callsign")[0] + data = Callsign(callnode) + if data['call'] != callsign: + raise Exception("qrz callsign mismatch") + return data + except Exception: + log.debug(xml) + raise + + def __enter__(self, *args, **kwargs): + return self + + def __exit__(self, *args, **kwargs): + self.db.close() + + diff --git a/hamtools/test_adif.py b/hamtools/test_adif.py new file mode 100644 index 0000000..c612a7a --- /dev/null +++ b/hamtools/test_adif.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python +# +# Copyright 2009, 2012 by Jeffrey M. Laughlin +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# + +import cStringIO as StringIO +from nose.tools import * +import adif +from decimal import Decimal +from datetime import datetime + +TEST_ADIF = """ +Exported using NA Version 10.57, conforming to ADIF specification 1.0 +1.00 + +AB9RN14.150SSB201207141200 +59 59 08 +K4NNQ14.150SSB201207141206 +59 59 08 +KC0YSH14.150SSB201207141206 +59 59 07 +""" + +def test_parse(): + flo = StringIO.StringIO(TEST_ADIF) + reader = adif.Reader(flo) + i = reader._lex() + eq_(i.next(), adif.Field(name='call', type='', body='AB9RN')) + eq_(i.next(), adif.Field(name='freq', type='', body='14.150')) + eq_(i.next(), adif.Field(name='mode', type='', body='SSB')) + eq_(i.next(), adif.Field(name='qso_date', type='', body='20120714')) + eq_(i.next(), adif.Field(name='time_on', type='', body='1200')) + eq_(i.next(), adif.Field(name='rst_sent', type='', body='59 ')) + eq_(i.next(), adif.Field(name='rst_rcvd', type='', body='59 ')) + eq_(i.next(), adif.Field(name='comment', type='', body='08')) + eq_(i.next(), adif.Field(name='eor', type='', body='')) + eq_(i.next(), adif.Field(name='call', type='', body='K4NNQ')) + +def test_iter_records(): + flo = StringIO.StringIO(TEST_ADIF) + reader = adif.Reader(flo) + i = iter(reader) + eq_(i.next(), {'call': 'AB9RN', 'freq': '14.150', 'mode': 'SSB', 'qso_date': '20120714', + 'time_on': '1200', 'rst_sent': '59 ', 'rst_rcvd': '59 ', + 'comment': '08', + 'app_datetime_on': datetime(2012, 07, 14, 12, 0)}) + +def test_version(): + flo = StringIO.StringIO(TEST_ADIF) + reader = adif.Reader(flo) + eq_(reader.adif_ver, Decimal('1.00')) + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..3201385 --- /dev/null +++ b/setup.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python + +from distutils.core import setup + +setup( + name='hamtools', + version='0.1', + description='N1YWB Python Ham Tools', + author='Jeff Laughlin', + author_email='n1ywb@arrl.net', + url='https://github.com/n1ywb/python-hamtools', + py_modules = ['hamtools'], + scripts = ['geolog', 'vk'], + install_requires = ['geojson'], + long_description = ( +"""Collection of amateur radio tools. Includes ability to read ADIF and +Cabrillo log files, cty.dat files, georeference callsigns via QRZ.com and +cty.dat, and output GIS data in GeoJSON and KML formats.""") +) + diff --git a/vk b/vk new file mode 100755 index 0000000..9c83481 --- /dev/null +++ b/vk @@ -0,0 +1,36 @@ +#!/usr/bin/env python +""" +Trivial voice keyer utility. + +Copyright 2014 by Jeffrey M. Laughlin + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as +published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +""" + + +import serial +import os +import sys + +sp = serial.Serial("/dev/ttyUSB0", rtscts=1) + +sp.setRTS(1) + +# play sound +os.system("aplay " + sys.argv[1]) + +sp.setRTS(0) + +sp.close +