[
  {
    "path": ".gitignore",
    "content": "__pycache__/\n*.py[cod]\n*$py.class\n\n"
  },
  {
    "path": "LICENSE",
    "content": "                    GNU GENERAL PUBLIC LICENSE\n                       Version 3, 29 June 2007\n\n Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>\n Everyone is permitted to copy and distribute verbatim copies\n of this license document, but changing it is not allowed.\n\n                            Preamble\n\n  The GNU General Public License is a free, copyleft license for\nsoftware and other kinds of works.\n\n  The licenses for most software and other practical works are designed\nto take away your freedom to share and change the works.  By contrast,\nthe GNU General Public License is intended to guarantee your freedom to\nshare and change all versions of a program--to make sure it remains free\nsoftware for all its users.  We, the Free Software Foundation, use the\nGNU General Public License for most of our software; it applies also to\nany other work released this way by its authors.  You can apply it to\nyour programs, too.\n\n  When we speak of free software, we are referring to freedom, not\nprice.  Our General Public Licenses are designed to make sure that you\nhave the freedom to distribute copies of free software (and charge for\nthem if you wish), that you receive source code or can get it if you\nwant it, that you can change the software or use pieces of it in new\nfree programs, and that you know you can do these things.\n\n  To protect your rights, we need to prevent others from denying you\nthese rights or asking you to surrender the rights.  Therefore, you have\ncertain responsibilities if you distribute copies of the software, or if\nyou modify it: responsibilities to respect the freedom of others.\n\n  For example, if you distribute copies of such a program, whether\ngratis or for a fee, you must pass on to the recipients the same\nfreedoms that you received.  You must make sure that they, too, receive\nor can get the source code.  And you must show them these terms so they\nknow their rights.\n\n  Developers that use the GNU GPL protect your rights with two steps:\n(1) assert copyright on the software, and (2) offer you this License\ngiving you legal permission to copy, distribute and/or modify it.\n\n  For the developers' and authors' protection, the GPL clearly explains\nthat there is no warranty for this free software.  For both users' and\nauthors' sake, the GPL requires that modified versions be marked as\nchanged, so that their problems will not be attributed erroneously to\nauthors of previous versions.\n\n  Some devices are designed to deny users access to install or run\nmodified versions of the software inside them, although the manufacturer\ncan do so.  This is fundamentally incompatible with the aim of\nprotecting users' freedom to change the software.  The systematic\npattern of such abuse occurs in the area of products for individuals to\nuse, which is precisely where it is most unacceptable.  Therefore, we\nhave designed this version of the GPL to prohibit the practice for those\nproducts.  If such problems arise substantially in other domains, we\nstand ready to extend this provision to those domains in future versions\nof the GPL, as needed to protect the freedom of users.\n\n  Finally, every program is threatened constantly by software patents.\nStates should not allow patents to restrict development and use of\nsoftware on general-purpose computers, but in those that do, we wish to\navoid the special danger that patents applied to a free program could\nmake it effectively proprietary.  To prevent this, the GPL assures that\npatents cannot be used to render the program non-free.\n\n  The precise terms and conditions for copying, distribution and\nmodification follow.\n\n                       TERMS AND CONDITIONS\n\n  0. Definitions.\n\n  \"This License\" refers to version 3 of the GNU General Public License.\n\n  \"Copyright\" also means copyright-like laws that apply to other kinds of\nworks, such as semiconductor masks.\n\n  \"The Program\" refers to any copyrightable work licensed under this\nLicense.  Each licensee is addressed as \"you\".  \"Licensees\" and\n\"recipients\" may be individuals or organizations.\n\n  To \"modify\" a work means to copy from or adapt all or part of the work\nin a fashion requiring copyright permission, other than the making of an\nexact copy.  The resulting work is called a \"modified version\" of the\nearlier work or a work \"based on\" the earlier work.\n\n  A \"covered work\" means either the unmodified Program or a work based\non the Program.\n\n  To \"propagate\" a work means to do anything with it that, without\npermission, would make you directly or secondarily liable for\ninfringement under applicable copyright law, except executing it on a\ncomputer or modifying a private copy.  Propagation includes copying,\ndistribution (with or without modification), making available to the\npublic, and in some countries other activities as well.\n\n  To \"convey\" a work means any kind of propagation that enables other\nparties to make or receive copies.  Mere interaction with a user through\na computer network, with no transfer of a copy, is not conveying.\n\n  An interactive user interface displays \"Appropriate Legal Notices\"\nto the extent that it includes a convenient and prominently visible\nfeature that (1) displays an appropriate copyright notice, and (2)\ntells the user that there is no warranty for the work (except to the\nextent that warranties are provided), that licensees may convey the\nwork under this License, and how to view a copy of this License.  If\nthe interface presents a list of user commands or options, such as a\nmenu, a prominent item in the list meets this criterion.\n\n  1. Source Code.\n\n  The \"source code\" for a work means the preferred form of the work\nfor making modifications to it.  \"Object code\" means any non-source\nform of a work.\n\n  A \"Standard Interface\" means an interface that either is an official\nstandard defined by a recognized standards body, or, in the case of\ninterfaces specified for a particular programming language, one that\nis widely used among developers working in that language.\n\n  The \"System Libraries\" of an executable work include anything, other\nthan the work as a whole, that (a) is included in the normal form of\npackaging a Major Component, but which is not part of that Major\nComponent, and (b) serves only to enable use of the work with that\nMajor Component, or to implement a Standard Interface for which an\nimplementation is available to the public in source code form.  A\n\"Major Component\", in this context, means a major essential component\n(kernel, window system, and so on) of the specific operating system\n(if any) on which the executable work runs, or a compiler used to\nproduce the work, or an object code interpreter used to run it.\n\n  The \"Corresponding Source\" for a work in object code form means all\nthe source code needed to generate, install, and (for an executable\nwork) run the object code and to modify the work, including scripts to\ncontrol those activities.  However, it does not include the work's\nSystem Libraries, or general-purpose tools or generally available free\nprograms which are used unmodified in performing those activities but\nwhich are not part of the work.  For example, Corresponding Source\nincludes interface definition files associated with source files for\nthe work, and the source code for shared libraries and dynamically\nlinked subprograms that the work is specifically designed to require,\nsuch as by intimate data communication or control flow between those\nsubprograms and other parts of the work.\n\n  The Corresponding Source need not include anything that users\ncan regenerate automatically from other parts of the Corresponding\nSource.\n\n  The Corresponding Source for a work in source code form is that\nsame work.\n\n  2. Basic Permissions.\n\n  All rights granted under this License are granted for the term of\ncopyright on the Program, and are irrevocable provided the stated\nconditions are met.  This License explicitly affirms your unlimited\npermission to run the unmodified Program.  The output from running a\ncovered work is covered by this License only if the output, given its\ncontent, constitutes a covered work.  This License acknowledges your\nrights of fair use or other equivalent, as provided by copyright law.\n\n  You may make, run and propagate covered works that you do not\nconvey, without conditions so long as your license otherwise remains\nin force.  You may convey covered works to others for the sole purpose\nof having them make modifications exclusively for you, or provide you\nwith facilities for running those works, provided that you comply with\nthe terms of this License in conveying all material for which you do\nnot control copyright.  Those thus making or running the covered works\nfor you must do so exclusively on your behalf, under your direction\nand control, on terms that prohibit them from making any copies of\nyour copyrighted material outside their relationship with you.\n\n  Conveying under any other circumstances is permitted solely under\nthe conditions stated below.  Sublicensing is not allowed; section 10\nmakes it unnecessary.\n\n  3. Protecting Users' Legal Rights From Anti-Circumvention Law.\n\n  No covered work shall be deemed part of an effective technological\nmeasure under any applicable law fulfilling obligations under article\n11 of the WIPO copyright treaty adopted on 20 December 1996, or\nsimilar laws prohibiting or restricting circumvention of such\nmeasures.\n\n  When you convey a covered work, you waive any legal power to forbid\ncircumvention of technological measures to the extent such circumvention\nis effected by exercising rights under this License with respect to\nthe covered work, and you disclaim any intention to limit operation or\nmodification of the work as a means of enforcing, against the work's\nusers, your or third parties' legal rights to forbid circumvention of\ntechnological measures.\n\n  4. Conveying Verbatim Copies.\n\n  You may convey verbatim copies of the Program's source code as you\nreceive it, in any medium, provided that you conspicuously and\nappropriately publish on each copy an appropriate copyright notice;\nkeep intact all notices stating that this License and any\nnon-permissive terms added in accord with section 7 apply to the code;\nkeep intact all notices of the absence of any warranty; and give all\nrecipients a copy of this License along with the Program.\n\n  You may charge any price or no price for each copy that you convey,\nand you may offer support or warranty protection for a fee.\n\n  5. Conveying Modified Source Versions.\n\n  You may convey a work based on the Program, or the modifications to\nproduce it from the Program, in the form of source code under the\nterms of section 4, provided that you also meet all of these conditions:\n\n    a) The work must carry prominent notices stating that you modified\n    it, and giving a relevant date.\n\n    b) The work must carry prominent notices stating that it is\n    released under this License and any conditions added under section\n    7.  This requirement modifies the requirement in section 4 to\n    \"keep intact all notices\".\n\n    c) You must license the entire work, as a whole, under this\n    License to anyone who comes into possession of a copy.  This\n    License will therefore apply, along with any applicable section 7\n    additional terms, to the whole of the work, and all its parts,\n    regardless of how they are packaged.  This License gives no\n    permission to license the work in any other way, but it does not\n    invalidate such permission if you have separately received it.\n\n    d) If the work has interactive user interfaces, each must display\n    Appropriate Legal Notices; however, if the Program has interactive\n    interfaces that do not display Appropriate Legal Notices, your\n    work need not make them do so.\n\n  A compilation of a covered work with other separate and independent\nworks, which are not by their nature extensions of the covered work,\nand which are not combined with it such as to form a larger program,\nin or on a volume of a storage or distribution medium, is called an\n\"aggregate\" if the compilation and its resulting copyright are not\nused to limit the access or legal rights of the compilation's users\nbeyond what the individual works permit.  Inclusion of a covered work\nin an aggregate does not cause this License to apply to the other\nparts of the aggregate.\n\n  6. Conveying Non-Source Forms.\n\n  You may convey a covered work in object code form under the terms\nof sections 4 and 5, provided that you also convey the\nmachine-readable Corresponding Source under the terms of this License,\nin one of these ways:\n\n    a) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by the\n    Corresponding Source fixed on a durable physical medium\n    customarily used for software interchange.\n\n    b) Convey the object code in, or embodied in, a physical product\n    (including a physical distribution medium), accompanied by a\n    written offer, valid for at least three years and valid for as\n    long as you offer spare parts or customer support for that product\n    model, to give anyone who possesses the object code either (1) a\n    copy of the Corresponding Source for all the software in the\n    product that is covered by this License, on a durable physical\n    medium customarily used for software interchange, for a price no\n    more than your reasonable cost of physically performing this\n    conveying of source, or (2) access to copy the\n    Corresponding Source from a network server at no charge.\n\n    c) Convey individual copies of the object code with a copy of the\n    written offer to provide the Corresponding Source.  This\n    alternative is allowed only occasionally and noncommercially, and\n    only if you received the object code with such an offer, in accord\n    with subsection 6b.\n\n    d) Convey the object code by offering access from a designated\n    place (gratis or for a charge), and offer equivalent access to the\n    Corresponding Source in the same way through the same place at no\n    further charge.  You need not require recipients to copy the\n    Corresponding Source along with the object code.  If the place to\n    copy the object code is a network server, the Corresponding Source\n    may be on a different server (operated by you or a third party)\n    that supports equivalent copying facilities, provided you maintain\n    clear directions next to the object code saying where to find the\n    Corresponding Source.  Regardless of what server hosts the\n    Corresponding Source, you remain obligated to ensure that it is\n    available for as long as needed to satisfy these requirements.\n\n    e) Convey the object code using peer-to-peer transmission, provided\n    you inform other peers where the object code and Corresponding\n    Source of the work are being offered to the general public at no\n    charge under subsection 6d.\n\n  A separable portion of the object code, whose source code is excluded\nfrom the Corresponding Source as a System Library, need not be\nincluded in conveying the object code work.\n\n  A \"User Product\" is either (1) a \"consumer product\", which means any\ntangible personal property which is normally used for personal, family,\nor household purposes, or (2) anything designed or sold for incorporation\ninto a dwelling.  In determining whether a product is a consumer product,\ndoubtful cases shall be resolved in favor of coverage.  For a particular\nproduct received by a particular user, \"normally used\" refers to a\ntypical or common use of that class of product, regardless of the status\nof the particular user or of the way in which the particular user\nactually uses, or expects or is expected to use, the product.  A product\nis a consumer product regardless of whether the product has substantial\ncommercial, industrial or non-consumer uses, unless such uses represent\nthe only significant mode of use of the product.\n\n  \"Installation Information\" for a User Product means any methods,\nprocedures, authorization keys, or other information required to install\nand execute modified versions of a covered work in that User Product from\na modified version of its Corresponding Source.  The information must\nsuffice to ensure that the continued functioning of the modified object\ncode is in no case prevented or interfered with solely because\nmodification has been made.\n\n  If you convey an object code work under this section in, or with, or\nspecifically for use in, a User Product, and the conveying occurs as\npart of a transaction in which the right of possession and use of the\nUser Product is transferred to the recipient in perpetuity or for a\nfixed term (regardless of how the transaction is characterized), the\nCorresponding Source conveyed under this section must be accompanied\nby the Installation Information.  But this requirement does not apply\nif neither you nor any third party retains the ability to install\nmodified object code on the User Product (for example, the work has\nbeen installed in ROM).\n\n  The requirement to provide Installation Information does not include a\nrequirement to continue to provide support service, warranty, or updates\nfor a work that has been modified or installed by the recipient, or for\nthe User Product in which it has been modified or installed.  Access to a\nnetwork may be denied when the modification itself materially and\nadversely affects the operation of the network or violates the rules and\nprotocols for communication across the network.\n\n  Corresponding Source conveyed, and Installation Information provided,\nin accord with this section must be in a format that is publicly\ndocumented (and with an implementation available to the public in\nsource code form), and must require no special password or key for\nunpacking, reading or copying.\n\n  7. Additional Terms.\n\n  \"Additional permissions\" are terms that supplement the terms of this\nLicense by making exceptions from one or more of its conditions.\nAdditional permissions that are applicable to the entire Program shall\nbe treated as though they were included in this License, to the extent\nthat they are valid under applicable law.  If additional permissions\napply only to part of the Program, that part may be used separately\nunder those permissions, but the entire Program remains governed by\nthis License without regard to the additional permissions.\n\n  When you convey a copy of a covered work, you may at your option\nremove any additional permissions from that copy, or from any part of\nit.  (Additional permissions may be written to require their own\nremoval in certain cases when you modify the work.)  You may place\nadditional permissions on material, added by you to a covered work,\nfor which you have or can give appropriate copyright permission.\n\n  Notwithstanding any other provision of this License, for material you\nadd to a covered work, you may (if authorized by the copyright holders of\nthat material) supplement the terms of this License with terms:\n\n    a) Disclaiming warranty or limiting liability differently from the\n    terms of sections 15 and 16 of this License; or\n\n    b) Requiring preservation of specified reasonable legal notices or\n    author attributions in that material or in the Appropriate Legal\n    Notices displayed by works containing it; or\n\n    c) Prohibiting misrepresentation of the origin of that material, or\n    requiring that modified versions of such material be marked in\n    reasonable ways as different from the original version; or\n\n    d) Limiting the use for publicity purposes of names of licensors or\n    authors of the material; or\n\n    e) Declining to grant rights under trademark law for use of some\n    trade names, trademarks, or service marks; or\n\n    f) Requiring indemnification of licensors and authors of that\n    material by anyone who conveys the material (or modified versions of\n    it) with contractual assumptions of liability to the recipient, for\n    any liability that these contractual assumptions directly impose on\n    those licensors and authors.\n\n  All other non-permissive additional terms are considered \"further\nrestrictions\" within the meaning of section 10.  If the Program as you\nreceived it, or any part of it, contains a notice stating that it is\ngoverned by this License along with a term that is a further\nrestriction, you may remove that term.  If a license document contains\na further restriction but permits relicensing or conveying under this\nLicense, you may add to a covered work material governed by the terms\nof that license document, provided that the further restriction does\nnot survive such relicensing or conveying.\n\n  If you add terms to a covered work in accord with this section, you\nmust place, in the relevant source files, a statement of the\nadditional terms that apply to those files, or a notice indicating\nwhere to find the applicable terms.\n\n  Additional terms, permissive or non-permissive, may be stated in the\nform of a separately written license, or stated as exceptions;\nthe above requirements apply either way.\n\n  8. Termination.\n\n  You may not propagate or modify a covered work except as expressly\nprovided under this License.  Any attempt otherwise to propagate or\nmodify it is void, and will automatically terminate your rights under\nthis License (including any patent licenses granted under the third\nparagraph of section 11).\n\n  However, if you cease all violation of this License, then your\nlicense from a particular copyright holder is reinstated (a)\nprovisionally, unless and until the copyright holder explicitly and\nfinally terminates your license, and (b) permanently, if the copyright\nholder fails to notify you of the violation by some reasonable means\nprior to 60 days after the cessation.\n\n  Moreover, your license from a particular copyright holder is\nreinstated permanently if the copyright holder notifies you of the\nviolation by some reasonable means, this is the first time you have\nreceived notice of violation of this License (for any work) from that\ncopyright holder, and you cure the violation prior to 30 days after\nyour receipt of the notice.\n\n  Termination of your rights under this section does not terminate the\nlicenses of parties who have received copies or rights from you under\nthis License.  If your rights have been terminated and not permanently\nreinstated, you do not qualify to receive new licenses for the same\nmaterial under section 10.\n\n  9. Acceptance Not Required for Having Copies.\n\n  You are not required to accept this License in order to receive or\nrun a copy of the Program.  Ancillary propagation of a covered work\noccurring solely as a consequence of using peer-to-peer transmission\nto receive a copy likewise does not require acceptance.  However,\nnothing other than this License grants you permission to propagate or\nmodify any covered work.  These actions infringe copyright if you do\nnot accept this License.  Therefore, by modifying or propagating a\ncovered work, you indicate your acceptance of this License to do so.\n\n  10. Automatic Licensing of Downstream Recipients.\n\n  Each time you convey a covered work, the recipient automatically\nreceives a license from the original licensors, to run, modify and\npropagate that work, subject to this License.  You are not responsible\nfor enforcing compliance by third parties with this License.\n\n  An \"entity transaction\" is a transaction transferring control of an\norganization, or substantially all assets of one, or subdividing an\norganization, or merging organizations.  If propagation of a covered\nwork results from an entity transaction, each party to that\ntransaction who receives a copy of the work also receives whatever\nlicenses to the work the party's predecessor in interest had or could\ngive under the previous paragraph, plus a right to possession of the\nCorresponding Source of the work from the predecessor in interest, if\nthe predecessor has it or can get it with reasonable efforts.\n\n  You may not impose any further restrictions on the exercise of the\nrights granted or affirmed under this License.  For example, you may\nnot impose a license fee, royalty, or other charge for exercise of\nrights granted under this License, and you may not initiate litigation\n(including a cross-claim or counterclaim in a lawsuit) alleging that\nany patent claim is infringed by making, using, selling, offering for\nsale, or importing the Program or any portion of it.\n\n  11. Patents.\n\n  A \"contributor\" is a copyright holder who authorizes use under this\nLicense of the Program or a work on which the Program is based.  The\nwork thus licensed is called the contributor's \"contributor version\".\n\n  A contributor's \"essential patent claims\" are all patent claims\nowned or controlled by the contributor, whether already acquired or\nhereafter acquired, that would be infringed by some manner, permitted\nby this License, of making, using, or selling its contributor version,\nbut do not include claims that would be infringed only as a\nconsequence of further modification of the contributor version.  For\npurposes of this definition, \"control\" includes the right to grant\npatent sublicenses in a manner consistent with the requirements of\nthis License.\n\n  Each contributor grants you a non-exclusive, worldwide, royalty-free\npatent license under the contributor's essential patent claims, to\nmake, use, sell, offer for sale, import and otherwise run, modify and\npropagate the contents of its contributor version.\n\n  In the following three paragraphs, a \"patent license\" is any express\nagreement or commitment, however denominated, not to enforce a patent\n(such as an express permission to practice a patent or covenant not to\nsue for patent infringement).  To \"grant\" such a patent license to a\nparty means to make such an agreement or commitment not to enforce a\npatent against the party.\n\n  If you convey a covered work, knowingly relying on a patent license,\nand the Corresponding Source of the work is not available for anyone\nto copy, free of charge and under the terms of this License, through a\npublicly available network server or other readily accessible means,\nthen you must either (1) cause the Corresponding Source to be so\navailable, or (2) arrange to deprive yourself of the benefit of the\npatent license for this particular work, or (3) arrange, in a manner\nconsistent with the requirements of this License, to extend the patent\nlicense to downstream recipients.  \"Knowingly relying\" means you have\nactual knowledge that, but for the patent license, your conveying the\ncovered work in a country, or your recipient's use of the covered work\nin a country, would infringe one or more identifiable patents in that\ncountry that you have reason to believe are valid.\n\n  If, pursuant to or in connection with a single transaction or\narrangement, you convey, or propagate by procuring conveyance of, a\ncovered work, and grant a patent license to some of the parties\nreceiving the covered work authorizing them to use, propagate, modify\nor convey a specific copy of the covered work, then the patent license\nyou grant is automatically extended to all recipients of the covered\nwork and works based on it.\n\n  A patent license is \"discriminatory\" if it does not include within\nthe scope of its coverage, prohibits the exercise of, or is\nconditioned on the non-exercise of one or more of the rights that are\nspecifically granted under this License.  You may not convey a covered\nwork if you are a party to an arrangement with a third party that is\nin the business of distributing software, under which you make payment\nto the third party based on the extent of your activity of conveying\nthe work, and under which the third party grants, to any of the\nparties who would receive the covered work from you, a discriminatory\npatent license (a) in connection with copies of the covered work\nconveyed by you (or copies made from those copies), or (b) primarily\nfor and in connection with specific products or compilations that\ncontain the covered work, unless you entered into that arrangement,\nor that patent license was granted, prior to 28 March 2007.\n\n  Nothing in this License shall be construed as excluding or limiting\nany implied license or other defenses to infringement that may\notherwise be available to you under applicable patent law.\n\n  12. No Surrender of Others' Freedom.\n\n  If conditions are imposed on you (whether by court order, agreement or\notherwise) that contradict the conditions of this License, they do not\nexcuse you from the conditions of this License.  If you cannot convey a\ncovered work so as to satisfy simultaneously your obligations under this\nLicense and any other pertinent obligations, then as a consequence you may\nnot convey it at all.  For example, if you agree to terms that obligate you\nto collect a royalty for further conveying from those to whom you convey\nthe Program, the only way you could satisfy both those terms and this\nLicense would be to refrain entirely from conveying the Program.\n\n  13. Use with the GNU Affero General Public License.\n\n  Notwithstanding any other provision of this License, you have\npermission to link or combine any covered work with a work licensed\nunder version 3 of the GNU Affero General Public License into a single\ncombined work, and to convey the resulting work.  The terms of this\nLicense will continue to apply to the part which is the covered work,\nbut the special requirements of the GNU Affero General Public License,\nsection 13, concerning interaction through a network will apply to the\ncombination as such.\n\n  14. Revised Versions of this License.\n\n  The Free Software Foundation may publish revised and/or new versions of\nthe GNU General Public License from time to time.  Such new versions will\nbe similar in spirit to the present version, but may differ in detail to\naddress new problems or concerns.\n\n  Each version is given a distinguishing version number.  If the\nProgram specifies that a certain numbered version of the GNU General\nPublic License \"or any later version\" applies to it, you have the\noption of following the terms and conditions either of that numbered\nversion or of any later version published by the Free Software\nFoundation.  If the Program does not specify a version number of the\nGNU General Public License, you may choose any version ever published\nby the Free Software Foundation.\n\n  If the Program specifies that a proxy can decide which future\nversions of the GNU General Public License can be used, that proxy's\npublic statement of acceptance of a version permanently authorizes you\nto choose that version for the Program.\n\n  Later license versions may give you additional or different\npermissions.  However, no additional obligations are imposed on any\nauthor or copyright holder as a result of your choosing to follow a\nlater version.\n\n  15. Disclaimer of Warranty.\n\n  THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY\nAPPLICABLE LAW.  EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT\nHOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM \"AS IS\" WITHOUT WARRANTY\nOF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,\nTHE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR\nPURPOSE.  THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM\nIS WITH YOU.  SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF\nALL NECESSARY SERVICING, REPAIR OR CORRECTION.\n\n  16. Limitation of Liability.\n\n  IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING\nWILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS\nTHE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY\nGENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE\nUSE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF\nDATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD\nPARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),\nEVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF\nSUCH DAMAGES.\n\n  17. Interpretation of Sections 15 and 16.\n\n  If the disclaimer of warranty and limitation of liability provided\nabove cannot be given local legal effect according to their terms,\nreviewing courts shall apply local law that most closely approximates\nan absolute waiver of all civil liability in connection with the\nProgram, unless a warranty or assumption of liability accompanies a\ncopy of the Program in return for a fee.\n\n                     END OF TERMS AND CONDITIONS\n\n            How to Apply These Terms to Your New Programs\n\n  If you develop a new program, and you want it to be of the greatest\npossible use to the public, the best way to achieve this is to make it\nfree software which everyone can redistribute and change under these terms.\n\n  To do so, attach the following notices to the program.  It is safest\nto attach them to the start of each source file to most effectively\nstate the exclusion of warranty; and each file should have at least\nthe \"copyright\" line and a pointer to where the full notice is found.\n\n    <one line to give the program's name and a brief idea of what it does.>\n    Copyright (C) <year>  <name of author>\n\n    This program is free software: you can redistribute it and/or modify\n    it under the terms of the GNU General Public License as published by\n    the Free Software Foundation, either version 3 of the License, or\n    (at your option) any later version.\n\n    This program is distributed in the hope that it will be useful,\n    but WITHOUT ANY WARRANTY; without even the implied warranty of\n    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n    GNU General Public License for more details.\n\n    You should have received a copy of the GNU General Public License\n    along with this program.  If not, see <https://www.gnu.org/licenses/>.\n\nAlso add information on how to contact you by electronic and paper mail.\n\n  If the program does terminal interaction, make it output a short\nnotice like this when it starts in an interactive mode:\n\n    <program>  Copyright (C) <year>  <name of author>\n    This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.\n    This is free software, and you are welcome to redistribute it\n    under certain conditions; type `show c' for details.\n\nThe hypothetical commands `show w' and `show c' should show the appropriate\nparts of the General Public License.  Of course, your program's commands\nmight be different; for a GUI interface, you would use an \"about box\".\n\n  You should also get your employer (if you work as a programmer) or school,\nif any, to sign a \"copyright disclaimer\" for the program, if necessary.\nFor more information on this, and how to apply and follow the GNU GPL, see\n<https://www.gnu.org/licenses/>.\n\n  The GNU General Public License does not permit incorporating your program\ninto proprietary programs.  If your program is a subroutine library, you\nmay consider it more useful to permit linking proprietary applications with\nthe library.  If this is what you want to do, use the GNU Lesser General\nPublic License instead of this License.  But first, please read\n<https://www.gnu.org/licenses/why-not-lgpl.html>.\n"
  },
  {
    "path": "README.md",
    "content": "Blender GIS\n==========\nBlender minimum version required : v2.83\n\nNote : Since 2022, the OpenTopography web service requires an API key. Please register to opentopography.org and request a key. This service is still free.\n\n\n[Wiki](https://github.com/domlysz/BlenderGIS/wiki/Home) - [FAQ](https://github.com/domlysz/BlenderGIS/wiki/FAQ) - [Quick start guide](https://github.com/domlysz/BlenderGIS/wiki/Quick-start) - [Flowchart](https://raw.githubusercontent.com/wiki/domlysz/blenderGIS/flowchart.jpg)\n--------------------\n\n## Functionalities overview\n\n**GIS datafile import :** Import in Blender most commons GIS data format : Shapefile vector, raster image, geotiff DEM, OpenStreetMap xml.\n\nThere are a lot of possibilities to create a 3D terrain from geographic data with BlenderGIS, check the [Flowchart](https://raw.githubusercontent.com/wiki/domlysz/blenderGIS/flowchart.jpg) to have an overview.\n\nExemple : import vector contour lines, create faces by triangulation and put a topographic raster texture.\n\n![](https://raw.githubusercontent.com/wiki/domlysz/blenderGIS/Blender28x/gif/bgis_demo_delaunay.gif)\n\n**Grab geodata directly from the web :** display dynamics web maps inside Blender 3d view, requests for OpenStreetMap data (buildings, roads ...), get true elevation data from the NASA SRTM mission.\n\n![](https://raw.githubusercontent.com/wiki/domlysz/blenderGIS/Blender28x/gif/bgis_demo_webdata.gif)\n\n**And more :** Manage georeferencing informations of a scene, compute a terrain mesh by Delaunay triangulation, drop objects on a terrain mesh, make terrain analysis using shader nodes, setup new cameras from geotagged photos, setup a camera to render with Blender a new georeferenced raster.\n"
  },
  {
    "path": "__init__.py",
    "content": "# -*- coding:utf-8 -*-\r\n\r\n#  ***** GPL LICENSE BLOCK *****\r\n#\r\n#  This program is free software: you can redistribute it and/or modify\r\n#  it under the terms of the GNU General Public License as published by\r\n#  the Free Software Foundation, either version 3 of the License, or\r\n#  (at your option) any later version.\r\n#\r\n#  This program is distributed in the hope that it will be useful,\r\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r\n#  GNU General Public License for more details.\r\n#\r\n#  You should have received a copy of the GNU General Public License\r\n#  along with this program.  If not, see <http://www.gnu.org/licenses/>.\r\n#  All rights reserved.\r\n#  ***** GPL LICENSE BLOCK *****\r\n\r\nimport bpy\r\n\r\nbl_info = {\r\n\t'name': 'BlenderGIS',\r\n\t'description': 'Various tools for handle geodata',\r\n\t'author': 'domlysz',\r\n\t'license': 'GPL',\r\n\t'deps': '',\r\n\t'version': (2, 2, 14),\r\n\t'blender': (2, 83, 0),\r\n\t'location': 'View3D > Tools > GIS',\r\n\t'warning': '',\r\n\t'wiki_url': 'https://github.com/domlysz/BlenderGIS/wiki',\r\n\t'tracker_url': 'https://github.com/domlysz/BlenderGIS/issues',\r\n\t'link': '',\r\n\t'support': 'COMMUNITY',\r\n\t'category': '3D View'\r\n\t}\r\n\r\nclass BlenderVersionError(Exception):\r\n\tpass\r\n\r\nif bl_info['blender'] > bpy.app.version:\r\n\traise BlenderVersionError(f\"This addon requires Blender >= {bl_info['blender']}\")\r\nif bpy.app.version[0] > 5: #prevent breaking changes on major release\r\n\traise BlenderVersionError(f\"This addon is not tested against Blender {bpy.app.version[0]}.x breaking changes\")\r\n\r\n\r\n#Modules\r\nCAM_GEOPHOTO = True\r\nCAM_GEOREF = True\r\nEXPORT_SHP = True\r\nGET_DEM = True\r\nIMPORT_GEORASTER = True\r\nIMPORT_OSM = True\r\nIMPORT_SHP = True\r\nIMPORT_ASC = True\r\nDELAUNAY = True\r\nTERRAIN_NODES = True\r\nTERRAIN_RECLASS = True\r\nBASEMAPS = True\r\nDROP = True\r\nEARTH_SPHERE = True\r\n\r\nimport os, sys, tempfile\r\nfrom datetime import datetime\r\n\r\ndef getAppData():\r\n\thome = os.path.expanduser('~')\r\n\tloc = os.path.join(home, '.bgis')\r\n\tif not os.path.exists(loc):\r\n\t\tos.mkdir(loc)\r\n\treturn loc\r\n\r\nAPP_DATA = getAppData()\r\n\r\nimport logging\r\nfrom logging.handlers import RotatingFileHandler\r\n#temporary set log level, will be overriden reading addon prefs\r\n#logsFormat = \"%(levelname)s:%(name)s:%(lineno)d:%(message)s\"\r\nlogsFormat = '{levelname}:{name}:{lineno}:{message}'\r\nlogsFileName = 'bgis.log'\r\ntry:\r\n\t#logsFilePath = os.path.join(os.path.dirname(__file__), logsFileName)\r\n\tlogsFilePath = os.path.join(APP_DATA, logsFileName)\r\n\t#logging.basicConfig(level=logging.getLevelName('DEBUG'), format=logsFormat, style='{', filename=logsFilePath, filemode='w')\r\n\tlogHandler = RotatingFileHandler(logsFilePath, mode='a', maxBytes=512000, backupCount=1)\r\nexcept PermissionError:\r\n\t#logsFilePath = os.path.join(bpy.app.tempdir, logsFileName)\r\n\tlogsFilePath = os.path.join(tempfile.gettempdir(), logsFileName)\r\n\tlogHandler = RotatingFileHandler(logsFilePath, mode='a', maxBytes=512000, backupCount=1)\r\nlogHandler.setFormatter(logging.Formatter(logsFormat, style='{'))\r\nlogger = logging.getLogger(__name__)\r\nlogger.addHandler(logHandler)\r\nlogger.setLevel(logging.DEBUG)\r\nlogger.info('###### Starting new Blender session : {}'.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S')))\r\n\r\ndef _excepthook(exc_type, exc_value, exc_traceback):\r\n\tif 'BlenderGIS' in exc_traceback.tb_frame.f_code.co_filename:\r\n\t\tlogger.error(\"Uncaught exception\", exc_info=(exc_type, exc_value, exc_traceback))\r\n\tsys.__excepthook__(exc_type, exc_value, exc_traceback)\r\n\r\nsys.excepthook = _excepthook #warn, this is a global variable, can be overrided by another addon\r\n\r\n####\r\n'''\r\nWorkaround for `sys.excepthook` thread\r\nhttps://stackoverflow.com/questions/1643327/sys-excepthook-and-threading\r\n'''\r\nimport threading\r\n\r\ninit_original = threading.Thread.__init__\r\n\r\ndef init(self, *args, **kwargs):\r\n\r\n\tinit_original(self, *args, **kwargs)\r\n\trun_original = self.run\r\n\r\n\tdef run_with_except_hook(*args2, **kwargs2):\r\n\t\ttry:\r\n\t\t\trun_original(*args2, **kwargs2)\r\n\t\texcept Exception:\r\n\t\t\tsys.excepthook(*sys.exc_info())\r\n\r\n\tself.run = run_with_except_hook\r\n\r\nthreading.Thread.__init__ = init\r\n\r\n####\r\n\r\n\r\nimport ssl\r\nif (not os.environ.get('PYTHONHTTPSVERIFY', '') and\r\n\tgetattr(ssl, '_create_unverified_context', None)):\r\n\tssl._create_default_https_context = ssl._create_unverified_context\r\n\r\n#from .core.checkdeps import HAS_GDAL, HAS_PYPROJ, HAS_PIL, HAS_IMGIO\r\nfrom .core.settings import settings\r\n\r\n#Import all modules which contains classes that must be registed (classes derived from bpy.types.*)\r\nfrom . import prefs\r\nfrom . import geoscene\r\n\r\nif CAM_GEOPHOTO:\r\n\tfrom .operators import add_camera_exif\r\nif CAM_GEOREF:\r\n\tfrom .operators import add_camera_georef\r\nif EXPORT_SHP:\r\n\tfrom .operators import io_export_shp\r\nif GET_DEM:\r\n\tfrom .operators import io_get_dem\r\nif IMPORT_GEORASTER:\r\n\tfrom .operators import io_import_georaster\r\nif IMPORT_OSM:\r\n\tfrom .operators import io_import_osm\r\nif IMPORT_SHP:\r\n\tfrom .operators import io_import_shp\r\nif IMPORT_ASC:\r\n\tfrom .operators import io_import_asc\r\nif DELAUNAY:\r\n\tfrom .operators import mesh_delaunay_voronoi\r\nif TERRAIN_NODES:\r\n\tfrom .operators import nodes_terrain_analysis_builder\r\nif TERRAIN_RECLASS:\r\n\tfrom .operators import nodes_terrain_analysis_reclassify\r\nif BASEMAPS:\r\n\tfrom .operators import view3d_mapviewer\r\nif DROP:\r\n\tfrom .operators import object_drop\r\nif EARTH_SPHERE:\r\n\tfrom .operators import mesh_earth_sphere\r\n\r\n\r\nimport bpy.utils.previews as iconsLib\r\nicons_dict = {}\r\n\r\n\r\nclass BGIS_OT_logs(bpy.types.Operator):\r\n\tbl_idname = \"bgis.logs\"\r\n\tbl_description = 'Display BlenderGIS logs'\r\n\tbl_label = \"Logs\"\r\n\r\n\tdef execute(self, context):\r\n\t\tif logsFileName in bpy.data.texts:\r\n\t\t\tlogs = bpy.data.texts[logsFileName]\r\n\t\telse:\r\n\t\t\tlogs = bpy.data.texts.load(logsFilePath)\r\n\t\tbpy.ops.screen.area_split(direction='VERTICAL', factor=0.5)\r\n\t\tarea = bpy.context.area\r\n\t\tarea.type = 'TEXT_EDITOR'\r\n\t\tarea.spaces[0].text = logs\r\n\t\tbpy.ops.text.reload()\r\n\t\treturn {'FINISHED'}\r\n\r\n\r\nclass VIEW3D_MT_menu_gis_import(bpy.types.Menu):\r\n\tbl_label = \"Import\"\r\n\tdef draw(self, context):\r\n\t\tif IMPORT_SHP:\r\n\t\t\tself.layout.operator(\"importgis.shapefile_file_dialog\", icon_value=icons_dict[\"shp\"].icon_id, text='Shapefile (.shp)')\r\n\t\tif IMPORT_GEORASTER:\r\n\t\t\tself.layout.operator(\"importgis.georaster\", icon_value=icons_dict[\"raster\"].icon_id, text=\"Georeferenced raster (.tif .jpg .jp2 .png)\")\r\n\t\tif IMPORT_OSM:\r\n\t\t\tself.layout.operator(\"importgis.osm_file\", icon_value=icons_dict[\"osm\"].icon_id, text=\"Open Street Map xml (.osm)\")\r\n\t\tif IMPORT_ASC:\r\n\t\t\tself.layout.operator('importgis.asc_file', icon_value=icons_dict[\"asc\"].icon_id, text=\"ESRI ASCII Grid (.asc)\")\r\n\r\nclass VIEW3D_MT_menu_gis_export(bpy.types.Menu):\r\n\tbl_label = \"Export\"\r\n\tdef draw(self, context):\r\n\t\tif EXPORT_SHP:\r\n\t\t\tself.layout.operator('exportgis.shapefile', text=\"Shapefile (.shp)\", icon_value=icons_dict[\"shp\"].icon_id)\r\n\r\nclass VIEW3D_MT_menu_gis_webgeodata(bpy.types.Menu):\r\n\tbl_label = \"Web geodata\"\r\n\tdef draw(self, context):\r\n\t\tif BASEMAPS:\r\n\t\t\tself.layout.operator(\"view3d.map_start\", icon_value=icons_dict[\"layers\"].icon_id)\r\n\t\tif IMPORT_OSM:\r\n\t\t\tself.layout.operator(\"importgis.osm_query\", icon_value=icons_dict[\"osm\"].icon_id)\r\n\t\tif GET_DEM:\r\n\t\t\tself.layout.operator(\"importgis.dem_query\", icon_value=icons_dict[\"raster\"].icon_id)\r\n\r\nclass VIEW3D_MT_menu_gis_camera(bpy.types.Menu):\r\n\tbl_label = \"Camera\"\r\n\tdef draw(self, context):\r\n\t\tif CAM_GEOREF:\r\n\t\t\tself.layout.operator(\"camera.georender\", icon_value=icons_dict[\"georefCam\"].icon_id, text='Georender')\r\n\t\tif CAM_GEOPHOTO:\r\n\t\t\tself.layout.operator(\"camera.geophotos\", icon_value=icons_dict[\"exifCam\"].icon_id, text='Geophotos')\r\n\t\t\tself.layout.operator(\"camera.geophotos_setactive\", icon='FILE_REFRESH')\r\n\r\nclass VIEW3D_MT_menu_gis_mesh(bpy.types.Menu):\r\n\tbl_label = \"Mesh\"\r\n\tdef draw(self, context):\r\n\t\tif DELAUNAY:\r\n\t\t\tself.layout.operator(\"tesselation.delaunay\", icon_value=icons_dict[\"delaunay\"].icon_id, text='Delaunay')\r\n\t\t\tself.layout.operator(\"tesselation.voronoi\", icon_value=icons_dict[\"voronoi\"].icon_id, text='Voronoi')\r\n\t\tif EARTH_SPHERE:\r\n\t\t\tself.layout.operator(\"earth.sphere\", icon=\"WORLD\", text='lonlat to sphere')\r\n\t\t\t#self.layout.operator(\"earth.curvature\", icon=\"SPHERECURVE\", text='Earth curvature correction')\r\n\t\t\tself.layout.operator(\"earth.curvature\", icon_value=icons_dict[\"curve\"].icon_id, text='Earth curvature correction')\r\n\r\nclass VIEW3D_MT_menu_gis_object(bpy.types.Menu):\r\n\tbl_label = \"Object\"\r\n\tdef draw(self, context):\r\n\t\tif DROP:\r\n\t\t\tself.layout.operator(\"object.drop\", icon_value=icons_dict[\"drop\"].icon_id, text='Drop')\r\n\r\nclass VIEW3D_MT_menu_gis_nodes(bpy.types.Menu):\r\n\tbl_label = \"Nodes\"\r\n\tdef draw(self, context):\r\n\t\tif TERRAIN_NODES:\r\n\t\t\tself.layout.operator(\"analysis.nodes\", icon_value=icons_dict[\"terrain\"].icon_id, text='Terrain analysis')\r\n\r\nclass VIEW3D_MT_menu_gis(bpy.types.Menu):\r\n\tbl_label = \"GIS\"\r\n\t# Set the menu operators and draw functions\r\n\tdef draw(self, context):\r\n\t\tlayout = self.layout\r\n\t\tlayout.operator(\"bgis.pref_show\", icon='PREFERENCES')\r\n\t\tlayout.separator()\r\n\t\tlayout.menu('VIEW3D_MT_menu_gis_webgeodata', icon=\"URL\")\r\n\t\tlayout.menu('VIEW3D_MT_menu_gis_import', icon='IMPORT')\r\n\t\tlayout.menu('VIEW3D_MT_menu_gis_export', icon='EXPORT')\r\n\t\tlayout.menu('VIEW3D_MT_menu_gis_camera', icon='CAMERA_DATA')\r\n\t\tlayout.menu('VIEW3D_MT_menu_gis_mesh', icon='MESH_DATA')\r\n\t\tlayout.menu('VIEW3D_MT_menu_gis_object', icon='CUBE')\r\n\t\tlayout.menu('VIEW3D_MT_menu_gis_nodes', icon='NODETREE')\r\n\t\tlayout.separator()\r\n\t\tlayout.operator(\"bgis.logs\", icon='TEXT')\r\n\r\nmenus = [\r\nVIEW3D_MT_menu_gis,\r\nVIEW3D_MT_menu_gis_webgeodata,\r\nVIEW3D_MT_menu_gis_import,\r\nVIEW3D_MT_menu_gis_export,\r\nVIEW3D_MT_menu_gis_camera,\r\nVIEW3D_MT_menu_gis_mesh,\r\nVIEW3D_MT_menu_gis_object,\r\nVIEW3D_MT_menu_gis_nodes\r\n]\r\n\r\n\r\ndef add_gis_menu(self, context):\r\n\tif context.mode == 'OBJECT':\r\n\t\tself.layout.menu('VIEW3D_MT_menu_gis')\r\n\r\n\r\ndef register():\r\n\t#icons\r\n\tglobal icons_dict\r\n\ticons_dict = iconsLib.new()\r\n\ticons_dir = os.path.join(os.path.dirname(__file__), \"icons\")\r\n\tfor icon in os.listdir(icons_dir):\r\n\t\tname, ext = os.path.splitext(icon)\r\n\t\ticons_dict.load(name, os.path.join(icons_dir, icon), 'IMAGE')\r\n\r\n\t#operators\r\n\tprefs.register()\r\n\tgeoscene.register()\r\n\r\n\tfor menu in menus:\r\n\t\ttry:\r\n\t\t\tbpy.utils.register_class(menu)\r\n\t\texcept ValueError as e:\r\n\t\t\tlogger.warning('{} is already registered, now unregister and retry... '.format(menu))\r\n\t\t\tbpy.utils.unregister_class(menu)\r\n\t\t\tbpy.utils.register_class(menu)\r\n\r\n\tbpy.utils.register_class(BGIS_OT_logs)\r\n\r\n\tif BASEMAPS:\r\n\t\tview3d_mapviewer.register()\r\n\tif IMPORT_GEORASTER:\r\n\t\tio_import_georaster.register()\r\n\tif IMPORT_SHP:\r\n\t\tio_import_shp.register()\r\n\tif EXPORT_SHP:\r\n\t\tio_export_shp.register()\r\n\tif IMPORT_OSM:\r\n\t\tio_import_osm.register()\r\n\tif IMPORT_ASC:\r\n\t\tio_import_asc.register()\r\n\tif DELAUNAY:\r\n\t\tmesh_delaunay_voronoi.register()\r\n\tif DROP:\r\n\t\tobject_drop.register()\r\n\tif GET_DEM:\r\n\t\tio_get_dem.register()\r\n\tif CAM_GEOPHOTO:\r\n\t\tadd_camera_exif.register()\r\n\tif CAM_GEOREF:\r\n\t\tadd_camera_georef.register()\r\n\tif TERRAIN_NODES:\r\n\t\tnodes_terrain_analysis_builder.register()\r\n\tif TERRAIN_RECLASS:\r\n\t\tnodes_terrain_analysis_reclassify.register()\r\n\tif EARTH_SPHERE:\r\n\t\tmesh_earth_sphere.register()\r\n\r\n\t#menus\r\n\tbpy.types.VIEW3D_MT_editor_menus.append(add_gis_menu)\r\n\r\n\t#shortcuts\r\n\tif not bpy.app.background: #no ui when running as background\r\n\t\twm = bpy.context.window_manager\r\n\t\tkc =  wm.keyconfigs.active\r\n\t\tif '3D View' in kc.keymaps:\r\n\t\t\tkm = kc.keymaps['3D View']\r\n\t\t\tif BASEMAPS:\r\n\t\t\t\tkmi = km.keymap_items.new(idname='view3d.map_start', type='NUMPAD_ASTERIX', value='PRESS')\r\n\r\n\t#Setup prefs\r\n\tpreferences = bpy.context.preferences.addons[__package__].preferences\r\n\tlogger.setLevel(logging.getLevelName(preferences.logLevel)) #will affect all child logger\r\n\r\n\t#update core settings according to addon prefs\r\n\tsettings.proj_engine = preferences.projEngine\r\n\tsettings.img_engine = preferences.imgEngine\r\n\tsettings.maptiler_api_key = preferences.maptiler_api_key\r\n\r\ndef unregister():\r\n\r\n\tglobal icons_dict\r\n\ticonsLib.remove(icons_dict)\r\n\r\n\tif not bpy.app.background: #no ui when running as background\r\n\t\twm = bpy.context.window_manager\r\n\t\tif '3D View' in  wm.keyconfigs.active.keymaps:\r\n\t\t\tkm = wm.keyconfigs.active.keymaps['3D View']\r\n\t\t\tif BASEMAPS:\r\n\t\t\t\tif 'view3d.map_start' in km.keymap_items:\r\n\t\t\t\t\tkmi = km.keymap_items.remove(km.keymap_items['view3d.map_start'])\r\n\r\n\tbpy.types.VIEW3D_MT_editor_menus.remove(add_gis_menu)\r\n\r\n\tfor menu in menus:\r\n\t\tbpy.utils.unregister_class(menu)\r\n\r\n\tbpy.utils.unregister_class(BGIS_OT_logs)\r\n\r\n\tprefs.unregister()\r\n\tgeoscene.unregister()\r\n\tif BASEMAPS:\r\n\t\tview3d_mapviewer.unregister()\r\n\tif IMPORT_GEORASTER:\r\n\t\tio_import_georaster.unregister()\r\n\tif IMPORT_SHP:\r\n\t\tio_import_shp.unregister()\r\n\tif EXPORT_SHP:\r\n\t\tio_export_shp.unregister()\r\n\tif IMPORT_OSM:\r\n\t\tio_import_osm.unregister()\r\n\tif IMPORT_ASC:\r\n\t\tio_import_asc.unregister()\r\n\tif DELAUNAY:\r\n\t\tmesh_delaunay_voronoi.unregister()\r\n\tif DROP:\r\n\t\tobject_drop.unregister()\r\n\tif GET_DEM:\r\n\t\tio_get_dem.unregister()\r\n\tif CAM_GEOPHOTO:\r\n\t\tadd_camera_exif.unregister()\r\n\tif CAM_GEOREF:\r\n\t\tadd_camera_georef.unregister()\r\n\tif TERRAIN_NODES:\r\n\t\tnodes_terrain_analysis_builder.unregister()\r\n\tif TERRAIN_RECLASS:\r\n\t\tnodes_terrain_analysis_reclassify.unregister()\r\n\tif EARTH_SPHERE:\r\n\t\tmesh_earth_sphere.unregister()\r\n\r\nif __name__ == \"__main__\":\r\n\tregister()\r\n"
  },
  {
    "path": "clients/QtMapServiceClient.py",
    "content": "# -*- coding:utf-8 -*-\nimport sys, os, time\nimport sys, os\nsys.path.append(os.path.abspath('..'))\n\nfrom PyQt4 import QtGui, QtCore, uic\nimport threading\nimport tempfile\n\nfrom core.basemaps import GRIDS, SOURCES, MapService, BBoxRequest, BBoxRequestMZ\nfrom core.lib import shapefile\nfrom core.proj import reprojPts\n\nfrom xml.etree import ElementTree as etree\nimport re\n\n#on the fly ui dialogs compilation\nmainForm, mainBase = uic.loadUiType('QtMapServiceClient.ui')\n\nprojSysLst={\n2154 : \"Lambert 93\",\n3942 : \"Lambert CC42\",\n3943 : \"Lambert CC43\",\n3944 : \"Lambert CC44\",\n3945 : \"Lambert CC45\",\n3946 :\"Lambert CC46\",\n3947 : \"Lambert CC47\",\n3948 : \"Lambert CC48\",\n3949 : \"Lambert CC49\",\n3950 : \"Lambert CC50\"\n}\n\ndef getShpExtent(pathShp):\n\tshp = shapefile.Reader(pathShp)\n\tshapes = shp.shapes() #we expect only one feature !\n\tif len(shapes) != 1:\n\t\treturn\n\telse:\n\t\textent = shapes[0].bbox #xmin, ymin, xmax, ymax\n\t\treturn extent\n\ndef getKmlExtent(kmlFile, crs2):\n\n\tdef formatCoor(coorText):\n\t\tcoorText = coorText.strip()\n\t\tcoordinates = []\n\t\tfor elem in str(coorText).split(\" \"):\n\t\t\tcoordinates.append(tuple(map(float, elem.split(\",\"))))\n\t\treturn coordinates\n\n\tdef namespace(element):\n\t\tm = re.match('\\{.*\\}', element.tag)\n\t\treturn m.group(0) if m else ''\n\n\troot = etree.parse(kmlFile).getroot()\n\tns = namespace(root)\n\tpolygons = []\n\tfor poly in root.iter(ns+\"Polygon\"):\n\t\tfor attributes in poly.iter(ns+\"coordinates\"):\n\t\t\tpolygons.append(formatCoor(attributes.text))\n\tif len(polygons) != 1:\n\t\treturn\n\telse:\n\t\tpts = polygons[0] #first feature\n\t\tpts = reprojPts(4326, crs2, pts)\n\t\txmin = min([pt[0] for pt in pts])\n\t\tymin = min([pt[1] for pt in pts])\n\t\txmax = max([pt[0] for pt in pts])\n\t\tymax = max([pt[1] for pt in pts])\n\t\textent = [xmin, ymin, xmax, ymax]\n\t\treturn list(map(round,extent))\n\n\n\nclass QtMapServiceClient(QtGui.QMainWindow, mainForm):\n\n\tdef __init__(self):\n\t\t#UI init\n\t\tQtGui.QMainWindow.__init__(self)\n\t\tself.setupUi(self)\n\t\t#\n\t\tfor k, v in SOURCES.items():\n\t\t\tself.cbProvider.addItem(v['name'], k) #text, data\n\n\t\tself.extent = None\n\t\tself.inCacheFolder.setText(tempfile.gettempdir())\n\n\t\tself.btCacheFolder.clicked.connect(self.setCacheFolder)\n\t\tself.btBrowseOutFolder.clicked.connect(self.setInOutFolder)\n\t\tself.btOkMosaic.clicked.connect(self.uiDoProcess)\n\t\tself.btCancel.clicked.connect(self.uiDoCancelThread)\n\t\tself.btExtentShp.clicked.connect(self.uiDoReadShpExtent)\n\n\t\tself.cbProvider.currentIndexChanged.connect(self.uiDoUpdateProvider)\n\t\tself.cbLayer.currentIndexChanged.connect(self.uiDoUpdateScales)\n\t\tself.cbZoom.currentIndexChanged.connect(self.uiDoUpdateRes)\n\n\t\tself.chkJPG.stateChanged.connect(self.uiUpdateMaskOption)\n\t\tself.chkSeedCache.stateChanged.connect(self.uiUpdateSeedOption)\n\t\t#\n\t\tself.uiDoUpdateProvider()\n\t\tself.inVectorFile.setText(\"*.kml *.shp...\")\n\n\n\t@property\n\tdef provider(self):\n\t\tk = self.cbProvider.itemData(self.cbProvider.currentIndex())\n\t\tcacheFolder = str(self.inCacheFolder.text())\n\t\treturn MapService(k, cacheFolder)\n\n\t@property\n\tdef layer(self):\n\t\treturn self.cbLayer.itemData(self.cbLayer.currentIndex())\n\n\t@property\n\tdef outProj(self):\n\t\treturn self.cbOutProj.itemData(self.cbOutProj.currentIndex())\n\n\t@property\n\tdef zoom(self):\n\t\tz = self.cbZoom.itemData(self.cbZoom.currentIndex())\n\t\tif z is not None:\n\t\t\treturn int(z)\n\n\t@property\n\tdef rq(self):\n\t\tif self.extent is not None and self.zoom is not None:\n\t\t\trq = self.provider.srcTms.bboxRequest(self.extent, self.zoom)\n\t\t\treturn rq\n\n\n\tdef uiUpdateMaskOption(self):\n\t\tif self.chkJPG.isChecked():\n\t\t\tself.chkMask.setEnabled(True)\n\t\telse:\n\t\t\tself.chkMask.setEnabled(False)\n\n\tdef uiUpdateSeedOption(self):\n\t\tif self.chkSeedCache.isChecked():\n\t\t\tself.chkRecurseUpZoomLevels.setEnabled(True)\n\t\t\tself.chkReproj.setEnabled(False)\n\t\t\tself.cbOutProj.setEnabled(False)\n\t\t\tself.chkBuildOverview.setEnabled(False)\n\t\t\tself.chkJPG.setEnabled(False)\n\t\t\tself.chkMask.setEnabled(False)\n\t\t\tself.chkBigtiff.setEnabled(False)\n\t\t\tself.inName.setEnabled(False)\n\t\t\tself.inOutFolder.setEnabled(False)\n\t\t\tself.btBrowseOutFolder.setEnabled(False)\n\t\telse:\n\t\t\tself.chkRecurseUpZoomLevels.setEnabled(False)\n\t\t\tself.chkReproj.setEnabled(True)\n\t\t\tself.cbOutProj.setEnabled(True)\n\t\t\tself.chkBuildOverview.setEnabled(True)\n\t\t\tself.chkJPG.setEnabled(True)\n\t\t\tself.chkMask.setEnabled(True)\n\t\t\tself.chkBigtiff.setEnabled(True)\n\t\t\tself.inName.setEnabled(True)\n\t\t\tself.inOutFolder.setEnabled(True)\n\t\t\tself.btBrowseOutFolder.setEnabled(True)\n\n\tdef uiDoUpdateProvider(self):\n\t\t'''Triggered when cbProvider idx change'''\n\t\t#clear comboboxes\n\t\tself.cbLayer.clear()\n\t\tself.cbOutProj.clear()\n\t\t#seed layers combobox\n\t\tfor layerKey, layer in self.provider.layers.items():\n\t\t\tself.cbLayer.addItem(layer.name, layerKey)\n\t\t#reproj sys\n\t\tfor k, v in projSysLst.items():\n\t\t\tself.cbOutProj.addItem(v, k)\n\t\tself.cbOutProj.setCurrentIndex(self.cbOutProj.findData(2154))\n\t\t#\n\t\tself.updateExtent()\n\n\tdef uiDoUpdateScales(self):\n\t\t'''Triggered when cbLayer idx change'''\n\t\tif self.layer is not None:\n\t\t\tlay = self.provider.layers[self.layer]\n\t\t\tself.cbZoom.clear()\n\t\t\tfor z in range(lay.zmin, lay.zmax):\n\t\t\t\tself.cbZoom.addItem(str(z), str(z))\n\n\tdef uiDoUpdateRes(self, zoomLevel):\n\t\t'''Triggered when cbZoom idx change'''\n\t\tif self.rq is not None:\n\t\t\tself.lbRes.setText(str(round(self.rq.res, 2))+\" m/px\")\n\t\t\tself.uiDoRequestInfos()\n\n\tdef uiDoReadShpExtent(self):\n\t\tpath = str(self.setOpenFileName('Shapefile (*.shp *.kml)'))\n\t\tself.inVectorFile.setText(path)\n\t\tself.updateExtent()\n\n\tdef updateExtent(self):\n\t\tpath = self.inVectorFile.text()\n\t\tif not os.path.exists(path):\n\t\t\tpass\n\t\telse:\n\t\t\text = path[-3:]\n\t\t\tif ext == 'shp':\n\t\t\t\tself.extent = getShpExtent(path) #xmin, ymin, xmax, ymax\n\t\t\telif ext == 'kml':\n\t\t\t\tself.extent = getKmlExtent(path, self.provider.srcTms.CRS)\n\t\t\tif not self.extent:\n\t\t\t\tQtGui.QMessageBox.information(self, \"Cannot read vector extent file\", \"This file must contains only one polygon\")\n\t\t\t\treturn\n\t\t\t#\n\t\t\tself.uiDoRequestInfos()\n\t\t\tself.inVectorFile.setText(path)\n\n\n\tdef uiDoRequestInfos(self):\n\t\tif self.rq is not None:\n\t\t\ttileSize = self.rq.tileSize\n\t\t\tres = self.rq.res\n\t\t\tcols, rows = self.rq.nbTilesX, self.rq.nbTilesY\n\t\t\tn = self.rq.nbTiles\n\t\t\t#rqTiles = rq.tiles #[(x,y,z)]\n\t\t\t#\n\t\t\txmin, ymin, xmax, ymax = self.extent\n\t\t\tdstX = xmax-xmin\n\t\t\tdstY = ymax-ymin\n\t\t\ttxtEmprise = str(round(dstX)) + \" x \" + str(round(dstY)) + \" m\"\n\t\t\t#\n\t\t\tnbPx = int(cols * tileSize * rows * tileSize)\n\t\t\tif nbPx > 1000000:\n\t\t\t\ttxtNbPx = str(int(nbPx/1000000)) + \" Mpix\"\n\t\t\telse:\n\t\t\t\ttxtNbPx = str(nbPx) + \" pix\"\n\t\t\t#\n\t\t\ttxtNbTiles = str(n) + \" tile(s)\"\n\t\t\t#\n\t\t\tresultStr = txtNbTiles + \" (\" + str(cols) + 'x' + str(rows) + \") - \" + txtNbPx + \" - \" + txtEmprise\n\t\t\tself.requestInfos.setText(resultStr)\n\n\n\n\tdef uiDoProcess(self):\n\n\t\toutFolder = str(self.inOutFolder.text())\n\t\tnameTemplate = str(self.inName.text())\n\t\tcacheFolder = str(self.inCacheFolder.text())\n\n\t\tif not self.chkSeedCache:\n\t\t\tif not os.path.exists(outFolder):\n\t\t\t\t\tQtGui.QMessageBox.information(self, \"Error\", \"Output folder does not exists\")\n\t\t\t\t\treturn\n\t\t\tif not nameTemplate:\n\t\t\t\t\tQtGui.QMessageBox.information(self, \"Error\", \"Basename is not defined\")\n\t\t\t\t\treturn\n\t\tif not os.path.exists(cacheFolder):\n\t\t\t\tQtGui.QMessageBox.information(self, \"Error\", \"Cache folder does not exists\")\n\t\t\t\treturn\n\n\n\t\t#Options\n\t\treproj = self.chkReproj.isChecked()\n\t\toutProj = self.cbOutProj.itemData(self.cbOutProj.currentIndex())\n\t\treprojOptions = (reproj, outProj)\n\t\tbuildOvv = self.chkBuildOverview.isChecked()\n\t\tjpgInTiff = self.chkJPG.isChecked()\n\t\tmask = self.chkMask.isChecked()\n\t\tbigTiff = self.chkBigtiff.isChecked()\n\t\t#Start map service\n\t\tself.btOkMosaic.setEnabled(False)\n\n\t\tif self.chkReproj:\n\t\t\toutCRS = self.outProj\n\t\telse:\n\t\t\toutCRS = None\n\t\toutFile = outFolder + os.sep + nameTemplate + '.tif'\n\n\t\tseedOnly = self.chkSeedCache.isChecked()\n\t\trecurseUpZoomLevels = self.chkRecurseUpZoomLevels.isChecked()\n\t\tself.thread = DownloadTiles(self.provider, self.layer, self.extent, self.zoom, outFile, outCRS, seedOnly, recurseUpZoomLevels)\n\t\tself.thread.finished.connect(self.uiProcessFinished)\n\t\tself.thread.terminated.connect(self.uiProcessFinished)\n\t\tself.thread.updateBar1.connect(self.uiDoUpdateBar1)\n\t\tself.thread.configBar1.connect(self.uiDoConfigBar1)\n\t\tself.thread.processInfo.connect(self.updateProcessInfo)\n\t\tself.thread.start()\n\n\n\tdef uiProcessFinished(self):\n\t\tself.updateUi()\n\t\tQtGui.QMessageBox.information(self, \"Info\", \"Finished\")\n\n\n\tdef uiDoCancelThread(self):\n\t\ttry:\n\t\t\tself.thread.cancel()\n\t\texcept:\n\t\t\tpass\n\n\tdef uiSendQuestion(self, titre, msg):\n\t\tchoice = QtGui.QMessageBox.question(self, titre, msg, QtGui.QMessageBox.Yes, QtGui.QMessageBox.No)\n\t\tif choice == QtGui.QMessageBox.Yes:\n\t\t\treturn True\n\t\telse:\n\t\t\treturn False\n\n\tdef updateUi(self):\n\t\tself.btOkMosaic.setEnabled(True)\n\n\tdef uiDoUpdateBar1(self, num):\n\t\tself.pBar1.setValue(num)\n\n\tdef uiDoConfigBar1(self, nb):\n\t\tself.pBar1.setMinimum(0)\n\t\tself.pBar1.setMaximum(nb)\n\n\tdef updateProcessInfo(self, txt):\n\t\tself.processInfo.setText(txt)\n\n#Set des inputbox\n\tdef setInOutFolder(self):\n\t\tpath = self.setExistingDirectory()\n\t\tif path:\n\t\t\tself.inOutFolder.setText(path)\n\n\tdef setCacheFolder(self):\n\t\tpath = self.setExistingDirectory()\n\t\tif path:\n\t\t\tself.inCacheFolder.setText(path)\n\n\tdef setInFolder(self):\n\t\tpath = self.setExistingDirectory()\n\t\tif path:\n\t\t\tself.inVectorFile.setText(path)\n\n#Standard dialogs\n\tdef setOpenFileName(self, filtre):\n\t\tfileName = QtGui.QFileDialog.getOpenFileName(self, \"Select file\", QtCore.QDir.rootPath(),filtre)\n\t\treturn QtCore.QDir.toNativeSeparators(fileName)\n\n\tdef setExistingDirectory(self):\n\t\tdirectory = QtGui.QFileDialog.getExistingDirectory(self, \"Select directory\", QtCore.QDir.rootPath(), QtGui.QFileDialog.ShowDirsOnly)\n\t\treturn QtCore.QDir.toNativeSeparators(directory)\n\n\tdef setSaveFileName(self):\n\t\tsaveFileName = QtGui.QFileDialog.getSaveFileName(self, \"Save file\", QtCore.QDir.rootPath())\n\t\treturn QtCore.QDir.toNativeSeparators(saveFileName)\n\n\n\nclass DownloadTiles(QtCore.QThread):\n\n\t#custum signals\n\tconfigBar1 = QtCore.pyqtSignal(int)\n\tupdateBar1 = QtCore.pyqtSignal(int)\n\tprocessInfo = QtCore.pyqtSignal(str)\n\n\tdef __init__(self, srv, layer, extent, zoom, outFile, outCRS, seedOnly, recurseUpZoomLevels):\n\t\tQtCore.QThread.__init__(self, None)\n\t\tself.srv = srv\n\t\tself.layer = layer\n\t\tself.extent = extent\n\n\t\tself.outFile = outFile\n\t\tself.outCRS = outCRS\n\t\tself.seedOnly = seedOnly\n\t\tif recurseUpZoomLevels and seedOnly:\n\t\t\tself.zoom = list(range(self.srv.layers[self.layer].zmin, zoom+1))\n\t\t\tself.rq = BBoxRequestMZ(self.srv.srcTms, self.extent, self.zoom)\n\t\t\tprint(self.rq.nbTiles, self.srv.srcTms.bboxRequest(self.extent, zoom).nbTiles)\n\t\telse:\n\t\t\tself.zoom = zoom\n\t\t\tself.rq = self.srv.srcTms.bboxRequest(self.extent, self.zoom)\n\n\tdef run(self):\n\t\tself.srv.start()\n\t\tself.configBar1.emit(self.rq.nbTiles)\n\t\t#self.configBar1.emit(0) #alternative moves\n\n\t\tif self.seedOnly:\n\t\t\tthread = threading.Thread(target=self.seedCache)\n\t\telse:\n\t\t\tthread = threading.Thread(target=self.getImage)\n\t\t#thread.setDaemon(True) #daemon threads will die when the main non-daemon thread have exited.\n\t\tthread.start()\n\n\t\twhile thread.isAlive():\n\t\t\ttime.sleep(0.05)\n\t\t\tself.processInfo.emit(self.srv.report)\n\t\t\tself.updateBar1.emit(self.srv.cptTiles)\n\n\t\tself.srv.stop()\n\n\tdef seedCache(self):\n\t\tself.srv.seedCache(self.layer, self.extent, self.zoom, toDstGrid=False)\n\n\tdef getImage(self):\n\t\tself.srv.getImage(self.layer, self.extent, self.zoom, path=self.outFile, bigTiff=True, outCRS=self.outCRS, toDstGrid=False)\n\n\tdef cancel(self):\n\t\tself.srv.stop()\n\n\t'''\n\t#no need for pausing because downloading tiles are saved in cache,\n\t#so restarting an aborted process will reuse existing tiles\n\tdef pause(self):\n\t\tself.srv.pause()\n\n\tdef resume(self):\n\t\tself.srv.resume()\n\t'''\n\n\nif __name__ == \"__main__\":\n\tapp = QtGui.QApplication(sys.argv)\n\twindow = QtMapServiceClient()\n\twindow.show()\n\tsys.exit(app.exec_())\n"
  },
  {
    "path": "clients/QtMapServiceClient.ui",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<ui version=\"4.0\">\n <class>QtMapService</class>\n <widget class=\"QDialog\" name=\"QtMapService\">\n  <property name=\"geometry\">\n   <rect>\n    <x>0</x>\n    <y>0</y>\n    <width>350</width>\n    <height>380</height>\n   </rect>\n  </property>\n  <property name=\"minimumSize\">\n   <size>\n    <width>350</width>\n    <height>380</height>\n   </size>\n  </property>\n  <property name=\"maximumSize\">\n   <size>\n    <width>350</width>\n    <height>380</height>\n   </size>\n  </property>\n  <property name=\"windowTitle\">\n   <string>MapService Qt Client</string>\n  </property>\n  <widget class=\"QProgressBar\" name=\"pBar1\">\n   <property name=\"geometry\">\n    <rect>\n     <x>8</x>\n     <y>316</y>\n     <width>291</width>\n     <height>23</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n    </font>\n   </property>\n   <property name=\"statusTip\">\n    <string/>\n   </property>\n   <property name=\"value\">\n    <number>0</number>\n   </property>\n  </widget>\n  <widget class=\"QLineEdit\" name=\"inName\">\n   <property name=\"geometry\">\n    <rect>\n     <x>106</x>\n     <y>252</y>\n     <width>135</width>\n     <height>20</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n    </font>\n   </property>\n  </widget>\n  <widget class=\"QPushButton\" name=\"btOkMosaic\">\n   <property name=\"geometry\">\n    <rect>\n     <x>300</x>\n     <y>310</y>\n     <width>41</width>\n     <height>35</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n    </font>\n   </property>\n   <property name=\"text\">\n    <string>Go</string>\n   </property>\n   <property name=\"autoDefault\">\n    <bool>true</bool>\n   </property>\n   <property name=\"default\">\n    <bool>false</bool>\n   </property>\n  </widget>\n  <widget class=\"QLabel\" name=\"label_60\">\n   <property name=\"geometry\">\n    <rect>\n     <x>128</x>\n     <y>95</y>\n     <width>81</width>\n     <height>19</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n     <underline>false</underline>\n    </font>\n   </property>\n   <property name=\"text\">\n    <string>Zoom</string>\n   </property>\n  </widget>\n  <widget class=\"QComboBox\" name=\"cbZoom\">\n   <property name=\"geometry\">\n    <rect>\n     <x>164</x>\n     <y>91</y>\n     <width>77</width>\n     <height>25</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n    </font>\n   </property>\n  </widget>\n  <widget class=\"QPushButton\" name=\"btCancel\">\n   <property name=\"geometry\">\n    <rect>\n     <x>300</x>\n     <y>354</y>\n     <width>41</width>\n     <height>21</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n    </font>\n   </property>\n   <property name=\"text\">\n    <string>Stop</string>\n   </property>\n   <property name=\"autoDefault\">\n    <bool>true</bool>\n   </property>\n   <property name=\"default\">\n    <bool>false</bool>\n   </property>\n  </widget>\n  <widget class=\"QPushButton\" name=\"btBrowseOutFolder\">\n   <property name=\"geometry\">\n    <rect>\n     <x>302</x>\n     <y>280</y>\n     <width>31</width>\n     <height>21</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n    </font>\n   </property>\n   <property name=\"text\">\n    <string>...</string>\n   </property>\n  </widget>\n  <widget class=\"QPushButton\" name=\"btExtentShp\">\n   <property name=\"geometry\">\n    <rect>\n     <x>305</x>\n     <y>63</y>\n     <width>29</width>\n     <height>21</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n    </font>\n   </property>\n   <property name=\"text\">\n    <string>...</string>\n   </property>\n  </widget>\n  <widget class=\"QLabel\" name=\"label_55\">\n   <property name=\"geometry\">\n    <rect>\n     <x>6</x>\n     <y>280</y>\n     <width>101</width>\n     <height>20</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n     <underline>true</underline>\n    </font>\n   </property>\n   <property name=\"text\">\n    <string>Output folder</string>\n   </property>\n  </widget>\n  <widget class=\"QLineEdit\" name=\"inOutFolder\">\n   <property name=\"geometry\">\n    <rect>\n     <x>105</x>\n     <y>280</y>\n     <width>195</width>\n     <height>20</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n    </font>\n   </property>\n  </widget>\n  <widget class=\"QComboBox\" name=\"cbLayer\">\n   <property name=\"geometry\">\n    <rect>\n     <x>5</x>\n     <y>92</y>\n     <width>117</width>\n     <height>25</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n    </font>\n   </property>\n  </widget>\n  <widget class=\"QLineEdit\" name=\"processInfo\">\n   <property name=\"enabled\">\n    <bool>false</bool>\n   </property>\n   <property name=\"geometry\">\n    <rect>\n     <x>8</x>\n     <y>354</y>\n     <width>286</width>\n     <height>19</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n    </font>\n   </property>\n  </widget>\n  <widget class=\"QLabel\" name=\"label_69\">\n   <property name=\"geometry\">\n    <rect>\n     <x>6</x>\n     <y>254</y>\n     <width>83</width>\n     <height>16</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n     <underline>true</underline>\n    </font>\n   </property>\n   <property name=\"text\">\n    <string>Basename</string>\n   </property>\n  </widget>\n  <widget class=\"QCheckBox\" name=\"chkReproj\">\n   <property name=\"geometry\">\n    <rect>\n     <x>11</x>\n     <y>171</y>\n     <width>101</width>\n     <height>21</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n    </font>\n   </property>\n   <property name=\"text\">\n    <string>Reprojection</string>\n   </property>\n   <property name=\"checked\">\n    <bool>false</bool>\n   </property>\n  </widget>\n  <widget class=\"QComboBox\" name=\"cbOutProj\">\n   <property name=\"geometry\">\n    <rect>\n     <x>112</x>\n     <y>168</y>\n     <width>153</width>\n     <height>23</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n    </font>\n   </property>\n  </widget>\n  <widget class=\"QLineEdit\" name=\"inVectorFile\">\n   <property name=\"geometry\">\n    <rect>\n     <x>82</x>\n     <y>63</y>\n     <width>219</width>\n     <height>20</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n    </font>\n   </property>\n  </widget>\n  <widget class=\"QLineEdit\" name=\"requestInfos\">\n   <property name=\"enabled\">\n    <bool>false</bool>\n   </property>\n   <property name=\"geometry\">\n    <rect>\n     <x>5</x>\n     <y>124</y>\n     <width>331</width>\n     <height>19</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n    </font>\n   </property>\n  </widget>\n  <widget class=\"QLineEdit\" name=\"lbRes\">\n   <property name=\"enabled\">\n    <bool>false</bool>\n   </property>\n   <property name=\"geometry\">\n    <rect>\n     <x>248</x>\n     <y>92</y>\n     <width>85</width>\n     <height>23</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n    </font>\n   </property>\n  </widget>\n  <widget class=\"QLabel\" name=\"label_61\">\n   <property name=\"geometry\">\n    <rect>\n     <x>8</x>\n     <y>10</y>\n     <width>55</width>\n     <height>20</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n     <underline>true</underline>\n    </font>\n   </property>\n   <property name=\"text\">\n    <string>Provider</string>\n   </property>\n  </widget>\n  <widget class=\"QComboBox\" name=\"cbProvider\">\n   <property name=\"geometry\">\n    <rect>\n     <x>82</x>\n     <y>8</y>\n     <width>147</width>\n     <height>25</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n    </font>\n   </property>\n  </widget>\n  <widget class=\"QLabel\" name=\"label_59\">\n   <property name=\"geometry\">\n    <rect>\n     <x>8</x>\n     <y>62</y>\n     <width>71</width>\n     <height>20</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n     <underline>true</underline>\n    </font>\n   </property>\n   <property name=\"text\">\n    <string>Extent file</string>\n   </property>\n  </widget>\n  <widget class=\"QLabel\" name=\"label_70\">\n   <property name=\"geometry\">\n    <rect>\n     <x>8</x>\n     <y>198</y>\n     <width>153</width>\n     <height>16</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n     <underline>true</underline>\n    </font>\n   </property>\n   <property name=\"text\">\n    <string>Tiff format options</string>\n   </property>\n  </widget>\n  <widget class=\"QWidget\" name=\"layoutWidget\">\n   <property name=\"geometry\">\n    <rect>\n     <x>11</x>\n     <y>216</y>\n     <width>337</width>\n     <height>33</height>\n    </rect>\n   </property>\n   <layout class=\"QHBoxLayout\" name=\"horizontalLayout\">\n    <item>\n     <widget class=\"QCheckBox\" name=\"chkBuildOverview\">\n      <property name=\"font\">\n       <font>\n        <family>Liberation Sans</family>\n       </font>\n      </property>\n      <property name=\"toolTip\">\n       <string>Build pyramids for speed up display performance</string>\n      </property>\n      <property name=\"text\">\n       <string>Pyramids</string>\n      </property>\n      <property name=\"checked\">\n       <bool>true</bool>\n      </property>\n     </widget>\n    </item>\n    <item>\n     <widget class=\"QCheckBox\" name=\"chkJPG\">\n      <property name=\"font\">\n       <font>\n        <family>Liberation Sans</family>\n       </font>\n      </property>\n      <property name=\"toolTip\">\n       <string>Use JPEG compression (destructive, no alpha channel)</string>\n      </property>\n      <property name=\"text\">\n       <string>Comp. jpeg</string>\n      </property>\n      <property name=\"checked\">\n       <bool>true</bool>\n      </property>\n     </widget>\n    </item>\n    <item>\n     <widget class=\"QCheckBox\" name=\"chkMask\">\n      <property name=\"font\">\n       <font>\n        <family>Liberation Sans</family>\n       </font>\n      </property>\n      <property name=\"toolTip\">\n       <string>Use an internal mask to store alpha and nodata values (useful with jpeg compression)</string>\n      </property>\n      <property name=\"text\">\n       <string>Mask</string>\n      </property>\n      <property name=\"checked\">\n       <bool>true</bool>\n      </property>\n     </widget>\n    </item>\n    <item>\n     <widget class=\"QCheckBox\" name=\"chkBigtiff\">\n      <property name=\"font\">\n       <font>\n        <family>Liberation Sans</family>\n       </font>\n      </property>\n      <property name=\"toolTip\">\n       <string>Allows creating raster greater than 4GB</string>\n      </property>\n      <property name=\"text\">\n       <string>Bigtiff</string>\n      </property>\n      <property name=\"checked\">\n       <bool>true</bool>\n      </property>\n     </widget>\n    </item>\n   </layout>\n  </widget>\n  <widget class=\"QCheckBox\" name=\"chkSeedCache\">\n   <property name=\"geometry\">\n    <rect>\n     <x>11</x>\n     <y>146</y>\n     <width>135</width>\n     <height>21</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n    </font>\n   </property>\n   <property name=\"text\">\n    <string>Only seed cache</string>\n   </property>\n   <property name=\"checked\">\n    <bool>false</bool>\n   </property>\n  </widget>\n  <widget class=\"QLineEdit\" name=\"inCacheFolder\">\n   <property name=\"geometry\">\n    <rect>\n     <x>82</x>\n     <y>39</y>\n     <width>217</width>\n     <height>20</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n    </font>\n   </property>\n  </widget>\n  <widget class=\"QPushButton\" name=\"btCacheFolder\">\n   <property name=\"geometry\">\n    <rect>\n     <x>304</x>\n     <y>39</y>\n     <width>29</width>\n     <height>21</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n    </font>\n   </property>\n   <property name=\"text\">\n    <string>...</string>\n   </property>\n  </widget>\n  <widget class=\"QLabel\" name=\"label_62\">\n   <property name=\"geometry\">\n    <rect>\n     <x>8</x>\n     <y>38</y>\n     <width>79</width>\n     <height>20</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n     <underline>true</underline>\n    </font>\n   </property>\n   <property name=\"text\">\n    <string>Cache folder</string>\n   </property>\n  </widget>\n  <widget class=\"QCheckBox\" name=\"chkRecurseUpZoomLevels\">\n   <property name=\"enabled\">\n    <bool>false</bool>\n   </property>\n   <property name=\"geometry\">\n    <rect>\n     <x>163</x>\n     <y>146</y>\n     <width>215</width>\n     <height>21</height>\n    </rect>\n   </property>\n   <property name=\"font\">\n    <font>\n     <family>Liberation Sans</family>\n    </font>\n   </property>\n   <property name=\"text\">\n    <string>Get recurse up zoom levels</string>\n   </property>\n   <property name=\"checked\">\n    <bool>false</bool>\n   </property>\n  </widget>\n </widget>\n <tabstops>\n  <tabstop>inVectorFile</tabstop>\n  <tabstop>btExtentShp</tabstop>\n  <tabstop>cbLayer</tabstop>\n  <tabstop>cbZoom</tabstop>\n  <tabstop>lbRes</tabstop>\n  <tabstop>requestInfos</tabstop>\n  <tabstop>chkReproj</tabstop>\n  <tabstop>cbOutProj</tabstop>\n  <tabstop>chkBuildOverview</tabstop>\n  <tabstop>inName</tabstop>\n  <tabstop>inOutFolder</tabstop>\n  <tabstop>btBrowseOutFolder</tabstop>\n  <tabstop>btOkMosaic</tabstop>\n  <tabstop>processInfo</tabstop>\n  <tabstop>btCancel</tabstop>\n </tabstops>\n <resources/>\n <connections/>\n <designerdata>\n  <property name=\"gridDeltaX\">\n   <number>2</number>\n  </property>\n  <property name=\"gridDeltaY\">\n   <number>2</number>\n  </property>\n  <property name=\"gridSnapX\">\n   <bool>true</bool>\n  </property>\n  <property name=\"gridSnapY\">\n   <bool>true</bool>\n  </property>\n  <property name=\"gridVisible\">\n   <bool>true</bool>\n  </property>\n </designerdata>\n</ui>\n"
  },
  {
    "path": "core/__init__.py",
    "content": "import logging\nlogging.basicConfig(level=logging.getLevelName('INFO'))\n\nfrom .checkdeps import HAS_GDAL, HAS_PYPROJ, HAS_IMGIO, HAS_PIL\nfrom .settings import settings\nfrom .errors import OverlapError\n\nfrom .utils import XY, BBOX\n\nfrom .proj import SRS, Reproj, reprojPt, reprojPts, reprojBbox, reprojImg\n\nfrom .georaster import GeoRef, GeoRaster, NpImage\n\nfrom .basemaps import GRIDS, SOURCES, MapService, GeoPackage, TileMatrix\n\nfrom .lib import shapefile\n"
  },
  {
    "path": "core/basemaps/__init__.py",
    "content": "from .servicesDefs import GRIDS, SOURCES\nfrom .mapservice import MapService, TileMatrix, BBoxRequest, BBoxRequestMZ\nfrom .gpkg import GeoPackage\n"
  },
  {
    "path": "core/basemaps/gpkg.py",
    "content": "# -*- coding:utf-8 -*-\n\n#  ***** GPL LICENSE BLOCK *****\n#\n#  This program is free software: you can redistribute it and/or modify\n#  it under the terms of the GNU General Public License as published by\n#  the Free Software Foundation, either version 3 of the License, or\n#  (at your option) any later version.\n#\n#  This program is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#  GNU General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public License\n#  along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#  All rights reserved.\n#  ***** GPL LICENSE BLOCK *****\nimport logging\nlog = logging.getLogger(__name__)\n\nimport os\nimport io\nimport math\nimport datetime\nimport sqlite3\n\n\n#http://www.geopackage.org/spec/#tiles\n#https://github.com/GitHubRGI/geopackage-python/blob/master/Packaging/tiles2gpkg_parallel.py\n#https://github.com/Esri/raster2gpkg/blob/master/raster2gpkg.py\n\n\n#table_name refer to the name of the table witch contains tiles data\n#here for simplification, table_name will always be named \"gpkg_tiles\"\n\nclass GeoPackage():\n\n\tMAX_DAYS = 90\n\n\tdef __init__(self, path, tm):\n\t\tself.dbPath = path\n\t\tself.name = os.path.splitext(os.path.basename(path))[0]\n\n\t\t#Get props from TileMatrix object\n\t\tself.auth, self.code = tm.CRS.split(':')\n\t\tself.code = int(self.code)\n\t\tself.tileSize = tm.tileSize\n\t\tself.xmin, self.ymin, self.xmax, self.ymax = tm.globalbbox\n\t\tself.resolutions = tm.getResList()\n\n\t\tif not self.isGPKG():\n\t\t\tself.create()\n\t\t\tself.insertMetadata()\n\n\t\t\tself.insertCRS(self.code, str(self.code), self.auth)\n\t\t\t#self.insertCRS(3857, \"Web Mercator\")\n\t\t\t#self.insertCRS(4326, \"WGS84\")\n\n\t\t\tself.insertTileMatrixSet()\n\n\n\tdef isGPKG(self):\n\t\tif not os.path.exists(self.dbPath):\n\t\t\treturn False\n\t\tdb = sqlite3.connect(self.dbPath)\n\n\t\t#check application id\n\t\tapp_id = db.execute(\"PRAGMA application_id\").fetchone()\n\t\tif not app_id[0] == 1196437808:\n\t\t\tdb.close()\n\t\t\treturn False\n\t\t#quick check of table schema\n\t\ttry:\n\t\t\tdb.execute('SELECT table_name FROM gpkg_contents LIMIT 1')\n\t\t\tdb.execute('SELECT srs_name FROM gpkg_spatial_ref_sys LIMIT 1')\n\t\t\tdb.execute('SELECT table_name FROM gpkg_tile_matrix_set LIMIT 1')\n\t\t\tdb.execute('SELECT table_name FROM gpkg_tile_matrix LIMIT 1')\n\t\t\tdb.execute('SELECT zoom_level, tile_column, tile_row, tile_data FROM gpkg_tiles LIMIT 1')\n\t\texcept Exception as e:\n\t\t\tlog.error('Incorrect GPKG schema', exc_info=True)\n\t\t\tdb.close()\n\t\t\treturn False\n\t\telse:\n\t\t\tdb.close()\n\t\t\treturn True\n\n\n\tdef create(self):\n\t\t\"\"\"Create default geopackage schema on the database.\"\"\"\n\t\tdb = sqlite3.connect(self.dbPath) #this attempt will create a new file if not exist\n\t\tcursor = db.cursor()\n\n\t\t# Add GeoPackage version 1.0 (\"GP10\" in ASCII) to the Sqlite header\n\t\tcursor.execute(\"PRAGMA application_id = 1196437808;\")\n\n\t\tcursor.execute(\"\"\"\n\t\t\tCREATE TABLE gpkg_contents (\n\t\t\t\ttable_name TEXT NOT NULL PRIMARY KEY,\n\t\t\t\tdata_type TEXT NOT NULL,\n\t\t\t\tidentifier TEXT UNIQUE,\n\t\t\t\tdescription TEXT DEFAULT '',\n\t\t\t\tlast_change DATETIME NOT NULL DEFAULT\n\t\t\t\t(strftime('%Y-%m-%dT%H:%M:%fZ','now')),\n\t\t\t\tmin_x DOUBLE,\n\t\t\t\tmin_y DOUBLE,\n\t\t\t\tmax_x DOUBLE,\n\t\t\t\tmax_y DOUBLE,\n\t\t\t\tsrs_id INTEGER,\n\t\t\t\tCONSTRAINT fk_gc_r_srs_id FOREIGN KEY (srs_id)\n\t\t\t\t\tREFERENCES gpkg_spatial_ref_sys(srs_id));\n\t\t\"\"\")\n\n\t\tcursor.execute(\"\"\"\n\t\t\tCREATE TABLE gpkg_spatial_ref_sys (\n\t\t\t\tsrs_name TEXT NOT NULL,\n\t\t\t\tsrs_id INTEGER NOT NULL PRIMARY KEY,\n\t\t\t\torganization TEXT NOT NULL,\n\t\t\t\torganization_coordsys_id INTEGER NOT NULL,\n\t\t\t\tdefinition TEXT NOT NULL,\n\t\t\t\tdescription TEXT);\n\t\t\"\"\")\n\n\t\tcursor.execute(\"\"\"\n\t\t\tCREATE TABLE gpkg_tile_matrix_set (\n\t\t\t\ttable_name TEXT NOT NULL PRIMARY KEY,\n\t\t\t\tsrs_id INTEGER NOT NULL,\n\t\t\t\tmin_x DOUBLE NOT NULL,\n\t\t\t\tmin_y DOUBLE NOT NULL,\n\t\t\t\tmax_x DOUBLE NOT NULL,\n\t\t\t\tmax_y DOUBLE NOT NULL,\n\t\t\t\tCONSTRAINT fk_gtms_table_name FOREIGN KEY (table_name)\n\t\t\t\t\tREFERENCES gpkg_contents(table_name),\n\t\t\t\tCONSTRAINT fk_gtms_srs FOREIGN KEY (srs_id)\n\t\t\t\t\tREFERENCES gpkg_spatial_ref_sys(srs_id));\n\t\t\"\"\")\n\n\t\tcursor.execute(\"\"\"\n\t\t\tCREATE TABLE gpkg_tile_matrix (\n\t\t\t\ttable_name TEXT NOT NULL,\n\t\t\t\tzoom_level INTEGER NOT NULL,\n\t\t\t\tmatrix_width INTEGER NOT NULL,\n\t\t\t\tmatrix_height INTEGER NOT NULL,\n\t\t\t\ttile_width INTEGER NOT NULL,\n\t\t\t\ttile_height INTEGER NOT NULL,\n\t\t\t\tpixel_x_size DOUBLE NOT NULL,\n\t\t\t\tpixel_y_size DOUBLE NOT NULL,\n\t\t\t\tCONSTRAINT pk_ttm PRIMARY KEY (table_name, zoom_level),\n\t\t\t\tCONSTRAINT fk_ttm_table_name FOREIGN KEY (table_name)\n\t\t\t\t\tREFERENCES gpkg_contents(table_name));\n\t\t\"\"\")\n\n\t\tcursor.execute(\"\"\"\n\t\t\tCREATE TABLE gpkg_tiles (\n\t\t\t\tid INTEGER PRIMARY KEY AUTOINCREMENT,\n\t\t\t\tzoom_level INTEGER NOT NULL,\n\t\t\t\ttile_column INTEGER NOT NULL,\n\t\t\t\ttile_row INTEGER NOT NULL,\n\t\t\t\ttile_data BLOB NOT NULL,\n\t\t\t\tlast_modified TIMESTAMP DEFAULT (datetime('now','localtime')),\n\t\t\t\tUNIQUE (zoom_level, tile_column, tile_row));\n\t\t\"\"\")\n\n\t\tdb.close()\n\n\n\tdef insertMetadata(self):\n\t\tdb = sqlite3.connect(self.dbPath)\n\t\tquery = \"\"\"INSERT INTO gpkg_contents (\n\t\t\t\t\ttable_name, data_type,\n\t\t\t\t\tidentifier, description,\n\t\t\t\t\tmin_x, min_y, max_x, max_y,\n\t\t\t\t\tsrs_id)\n\t\t\t\tVALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);\"\"\"\n\t\tdb.execute(query, (\"gpkg_tiles\", \"tiles\", self.name, \"Created with BlenderGIS\", self.xmin, self.ymin, self.xmax, self.ymax, self.code))\n\t\tdb.commit()\n\t\tdb.close()\n\n\n\tdef insertCRS(self, code, name, auth='EPSG', wkt=''):\n\t\tdb = sqlite3.connect(self.dbPath)\n\t\tdb.execute(\"\"\" INSERT INTO gpkg_spatial_ref_sys (\n\t\t\t\t\tsrs_id,\n\t\t\t\t\torganization,\n\t\t\t\t\torganization_coordsys_id,\n\t\t\t\t\tsrs_name,\n\t\t\t\t\tdefinition)\n\t\t\t\tVALUES (?, ?, ?, ?, ?)\n\t\t\t\"\"\", (code, auth, code, name, wkt))\n\t\tdb.commit()\n\t\tdb.close()\n\n\n\tdef insertTileMatrixSet(self):\n\t\tdb = sqlite3.connect(self.dbPath)\n\n\t\t#Tile matrix set\n\t\tquery = \"\"\"INSERT OR REPLACE INTO gpkg_tile_matrix_set (\n\t\t\t\t\ttable_name, srs_id,\n\t\t\t\t\tmin_x, min_y, max_x, max_y)\n\t\t\t\tVALUES (?, ?, ?, ?, ?, ?);\"\"\"\n\t\tdb.execute(query, ('gpkg_tiles', self.code, self.xmin, self.ymin, self.xmax, self.ymax))\n\n\n\t\t#Tile matrix of each levels\n\t\tfor level, res in enumerate(self.resolutions):\n\n\t\t\tw = math.ceil( (self.xmax - self.xmin) / (self.tileSize * res) )\n\t\t\th = math.ceil( (self.ymax - self.ymin) / (self.tileSize * res) )\n\n\t\t\tquery = \"\"\"INSERT OR REPLACE INTO gpkg_tile_matrix (\n\t\t\t\t\t\ttable_name, zoom_level,\n\t\t\t\t\t\tmatrix_width, matrix_height,\n\t\t\t\t\t\ttile_width, tile_height,\n\t\t\t\t\t\tpixel_x_size, pixel_y_size)\n\t\t\t\t\tVALUES (?, ?, ?, ?, ?, ?, ?, ?);\"\"\"\n\t\t\tdb.execute(query, ('gpkg_tiles', level, w, h, self.tileSize, self.tileSize, res, res))\n\n\n\t\tdb.commit()\n\t\tdb.close()\n\n\n\tdef hasTile(self, x, y, z):\n\t\tif self.getTile(x ,y, z) is not None:\n\t\t\treturn True\n\t\telse:\n\t\t\treturn False\n\n\tdef getTile(self, x, y, z):\n\t\t'''return tilde_data if tile exists otherwie return None'''\n\t\t#connect with detect_types parameter for automatically convert date to Python object\n\t\tdb = sqlite3.connect(self.dbPath, detect_types=sqlite3.PARSE_DECLTYPES)\n\t\tquery = 'SELECT tile_data, last_modified FROM gpkg_tiles WHERE zoom_level=? AND tile_column=? AND tile_row=?'\n\t\tresult = db.execute(query, (z, x, y)).fetchone()\n\t\tdb.close()\n\t\tif result is None:\n\t\t\treturn None\n\t\ttimeDelta = datetime.datetime.now() - result[1]\n\t\tif timeDelta.days > self.MAX_DAYS:\n\t\t\treturn None\n\t\treturn result[0]\n\n\tdef putTile(self, x, y, z, data):\n\t\tdb = sqlite3.connect(self.dbPath)\n\t\tquery = \"\"\"INSERT OR REPLACE INTO gpkg_tiles\n\t\t(tile_column, tile_row, zoom_level, tile_data) VALUES (?,?,?,?)\"\"\"\n\t\tdb.execute(query, (x, y, z, data))\n\t\tdb.commit()\n\t\tdb.close()\n\n\n\tdef listExistingTiles(self, tiles):\n\t\t\"\"\"\n\t\tinput : tiles list [(x,y,z)]\n\t\toutput : tiles list set [(x,y,z)] of existing records in cache db\"\"\"\n\n\t\tdb = sqlite3.connect(self.dbPath, detect_types=sqlite3.PARSE_DECLTYPES)\n\n\t\t# split out the axises\n\t\tx, y, z = zip(*tiles)\n\n\t\tquery = \"SELECT tile_column, tile_row, zoom_level FROM gpkg_tiles \" \\\n\t\t\t\t\"WHERE julianday() - julianday(last_modified) < ?\" \\\n\t\t\t\t\"AND zoom_level BETWEEN ? AND ? AND tile_column BETWEEN ? AND ? AND tile_row BETWEEN ? AND ?\"\n\n\t\tresult = db.execute(\n\t\t\tquery,\n\t\t\t(\n\t\t\t\tGeoPackage.MAX_DAYS,\n\t\t\t\tmin(z), max(z),\n\t\t\t\tmin(x), max(x),\n\t\t\t\tmin(y), max(y)\n\t\t\t)\n\t\t).fetchall()\n\n\t\tdb.close()\n\n\t\treturn set(result)\n\n\tdef listMissingTiles(self, tiles):\n\t\texisting = self.listExistingTiles(tiles)\n\t\treturn set(tiles) - existing # difference\n\n\n\tdef getTiles(self, tiles):\n\t\t\"\"\"tiles = list of (x,y,z) tuple\n\t\treturn list of (x,y,z,data) tuple\"\"\"\n\n\t\tdb = sqlite3.connect(self.dbPath, detect_types=sqlite3.PARSE_DECLTYPES)\n\n\t\t# split out the axises\n\t\tx, y, z = zip(*tiles)\n\n\t\tquery = \"SELECT tile_column, tile_row, zoom_level, tile_data FROM gpkg_tiles \" \\\n\t\t\t\t\"WHERE julianday() - julianday(last_modified) < ?\" \\\n\t\t\t\t\"AND zoom_level BETWEEN ? AND ? AND tile_column BETWEEN ? AND ? AND tile_row BETWEEN ? AND ?\"\n\n\t\tresult = db.execute(\n\t\t\tquery,\n\t\t\t(\n\t\t\t\tGeoPackage.MAX_DAYS,\n\t\t\t\tmin(z), max(z),\n\t\t\t\tmin(x), max(x),\n\t\t\t\tmin(y), max(y)\n\t\t\t)\n\t\t).fetchall()\n\n\t\tdb.close()\n\n\t\treturn result\n\n\n\tdef putTiles(self, tiles):\n\t\t\"\"\"tiles = list of (x,y,z,data) tuple\"\"\"\n\t\tdb = sqlite3.connect(self.dbPath)\n\t\tquery = \"\"\"INSERT OR REPLACE INTO gpkg_tiles\n\t\t(tile_column, tile_row, zoom_level, tile_data) VALUES (?,?,?,?)\"\"\"\n\t\tdb.executemany(query, tiles)\n\t\tdb.commit()\n\t\tdb.close()\n"
  },
  {
    "path": "core/basemaps/mapservice.py",
    "content": "# -*- coding:utf-8 -*-\n\n#  ***** GPL LICENSE BLOCK *****\n#\n#  This program is free software: you can redistribute it and/or modify\n#  it under the terms of the GNU General Public License as published by\n#  the Free Software Foundation, either version 3 of the License, or\n#  (at your option) any later version.\n#\n#  This program is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#  GNU General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public License\n#  along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#  All rights reserved.\n#  ***** GPL LICENSE BLOCK *****\n\n#built-in imports\nimport logging\nlog = logging.getLogger(__name__)\n\nimport math\nimport threading\nimport queue\nimport time\nimport urllib.request\nfrom ..lib import imghdr\nimport sys, time, os\n\n#core imports\nfrom .servicesDefs import GRIDS, SOURCES\nfrom .gpkg import GeoPackage\nfrom ..georaster import NpImage, GeoRef, BigTiffWriter\nfrom ..utils import BBOX\nfrom ..proj.reproj import reprojPt, reprojBbox, reprojImg\nfrom ..proj.ellps import dd2meters, meters2dd\nfrom ..proj.srs import SRS\n\nfrom .. import settings\nUSER_AGENT = settings.user_agent\n\nTIMEOUT = 4\n\n# Set mosaic backgroung image color, it will be the base color for area not covered\n# by the map service (ie when requests return non valid data)\nMOSAIC_BKG_COLOR = (128,128,128,255)\n\nEMPTY_TILE_COLOR = (255,192,203,255) #color for cached tile with empty data\nCORRUPTED_TILE_COLOR = (255,0,0,255) #color for cached tile which is non valid image data\n\nclass TileMatrix():\n\t\"\"\"\n\tWill inherit attributes from grid source definition\n\t\t\"CRS\" >> epsg code\n\t\t\"bbox\" >> (xmin, ymin, xmax, ymax)\n\t\t\"bboxCRS\" >> epsg code\n\t\t\"tileSize\"\n\t\t\"originLoc\" >> \"NW\" or SW\n\n\t\t\"resFactor\"\n\t\t\"initRes\" >> optional\n\t\t\"nbLevels\" >> optional\n\n\t\tor\n\n\t\t\"resolutions\"\n\n\t# Three ways to define a grid:\n\t# - submit a list of \"resolutions\" (This parameters override the others)\n\t# - submit \"resFactor\" and \"initRes\"\n\t# - submit just \"resFactor\" (initRes will be computed)\n\t\"\"\"\n\n\tdefaultNbLevels = 24\n\n\tdef __init__(self, gridDef):\n\n\t\t#create class attributes from grid dictionnary\n\t\tfor k, v in gridDef.items():\n\t\t\tsetattr(self, k, v)\n\n\t\t#Convert bbox to grid crs is needed\n\t\tif self.bboxCRS != self.CRS: #WARN here we assume crs is 4326, TODO\n\t\t\tlonMin, latMin, lonMax, latMax = self.bbox\n\t\t\tself.xmin, self.ymax = self.geoToProj(lonMin, latMax)\n\t\t\tself.xmax, self.ymin = self.geoToProj(lonMax, latMin)\n\t\telse:\n\t\t\tself.xmin, self.xmax = self.bbox[0], self.bbox[2]\n\t\t\tself.ymin, self.ymax = self.bbox[1], self.bbox[3]\n\n\n\t\tif not hasattr(self, 'resolutions'):\n\n\t\t\t#Set resFactor if not submited\n\t\t\tif not hasattr(self, 'resFactor'):\n\t\t\t\tself.resFactor = 2\n\n\t\t\t#Set initial resolution if not submited\n\t\t\tif not hasattr(self, 'initRes'):\n\t\t\t\t# at zoom level zero, 1 tile covers whole bounding box\n\t\t\t\tdx = abs(self.xmax - self.xmin)\n\t\t\t\tdy = abs(self.ymax - self.ymin)\n\t\t\t\tdst = max(dx, dy)\n\t\t\t\tself.initRes = dst / self.tileSize\n\n\t\t\t#Set number of levels if not submited\n\t\t\tif not hasattr(self, 'nbLevels'):\n\t\t\t\tself.nbLevels = self.defaultNbLevels\n\n\t\telse:\n\t\t\tself.resolutions.sort(reverse=True)\n\t\t\tself.nbLevels = len(self.resolutions)\n\n\n\t\t# Define tile matrix origin\n\t\tif self.originLoc == \"NW\":\n\t\t\tself.originx, self.originy = self.xmin, self.ymax\n\t\telif self.originLoc == \"SW\":\n\t\t\tself.originx, self.originy = self.xmin, self.ymin\n\t\telse:\n\t\t\traise NotImplementedError\n\n\t\t#Determine unit of CRS (decimal degrees or meters)\n\t\tself.crs = SRS(self.CRS)\n\t\tif self.crs.isGeo:\n\t\t\tself.units = 'degrees'\n\t\telse: #(if units cannot be determined we assume its meters)\n\t\t\tself.units = 'meters'\n\n\n\t@property\n\tdef globalbbox(self):\n\t\treturn self.xmin, self.ymin, self.xmax, self.ymax\n\n\n\tdef geoToProj(self, long, lat):\n\t\t\"\"\"convert longitude latitude in decimal degrees to grid crs\"\"\"\n\t\tif self.CRS == 'EPSG:4326':\n\t\t\treturn long, lat\n\t\telse:\n\t\t\treturn reprojPt(4326, self.CRS, long, lat)\n\n\tdef projToGeo(self, x, y):\n\t\t\"\"\"convert grid crs coords to longitude latitude in decimal degrees\"\"\"\n\t\tif self.CRS == 'EPSG:4326':\n\t\t\treturn x, y\n\t\telse:\n\t\t\treturn reprojPt(self.CRS, 4326, x, y)\n\n\n\tdef getResList(self):\n\t\tif hasattr(self, 'resolutions'):\n\t\t\treturn self.resolutions\n\t\telse:\n\t\t\treturn [self.initRes / self.resFactor**zoom for zoom in range(self.nbLevels)]\n\n\tdef getRes(self, zoom):\n\t\t\"\"\"Resolution (meters/pixel) for given zoom level (measured at Equator)\"\"\"\n\t\tif hasattr(self, 'resolutions'):\n\t\t\tif zoom > len(self.resolutions):\n\t\t\t\tzoom = len(self.resolutions)\n\t\t\treturn self.resolutions[zoom]\n\t\telse:\n\t\t\treturn self.initRes / self.resFactor**zoom\n\n\n\tdef getNearestZoom(self, res, rule='closer'):\n\t\t\"\"\"\n\t\tReturn the zoom level closest to the submited resolution\n\t\trule in ['closer', 'lower', 'higher']\n\t\tlower return the previous zoom level, higher return the next\n\t\t\"\"\"\n\t\tresLst = self.getResList() #ordered\n\n\t\tfor z1, v1 in enumerate(resLst):\n\t\t\tif v1 == res:\n\t\t\t\treturn z1\n\t\t\tif z1 == len(resLst) - 1:\n\t\t\t\treturn z1\n\t\t\tz2 = z1+1\n\t\t\tv2 = resLst[z2]\n\t\t\tif v2 == res:\n\t\t\t\treturn z2\n\n\t\t\tif v1 > res > v2:\n\t\t\t\tif rule == 'lower':\n\t\t\t\t\treturn z1\n\t\t\t\telif rule == 'higher':\n\t\t\t\t\treturn z2\n\t\t\t\telse: #closer\n\t\t\t\t\td1 = v1 - res\n\t\t\t\t\td2 = res - v2\n\t\t\t\t\tif d1 < d2:\n\t\t\t\t\t\treturn z1\n\t\t\t\t\telse:\n\t\t\t\t\t\treturn z2\n\n\tdef getPrevResFac(self, z):\n\t\t\"\"\"return res factor to previous zoom level\"\"\"\n\t\treturn self.getFromToResFac(z, z-1)\n\n\tdef getNextResFac(self, z):\n\t\t\"\"\"return res factor to next zoom level\"\"\"\n\t\treturn self.getFromToResFac(z, z+1)\n\n\tdef getFromToResFac(self, z1, z2):\n\t\t\"\"\"return res factor from z1 to z2\"\"\"\n\t\tif z1 == z2:\n\t\t\treturn 1\n\t\tif z1 < z2:\n\t\t\tif z2 >= self.nbLevels - 1:\n\t\t\t\treturn 1\n\t\t\telse:\n\t\t\t\treturn self.getRes(z2) / self.getRes(z1)\n\t\telif z1 > z2:\n\t\t\tif z2 <= 0:\n\t\t\t\treturn 1\n\t\t\telse:\n\t\t\t\treturn self.getRes(z2) / self.getRes(z1)\n\n\tdef getTileNumber(self, x, y, zoom):\n\t\t\"\"\"Convert projeted coords to tiles number\"\"\"\n\t\tres = self.getRes(zoom)\n\t\tgeoTileSize = self.tileSize * res\n\t\tdx = x - self.originx\n\t\tif self.originLoc == \"NW\":\n\t\t\tdy = self.originy - y\n\t\telse:\n\t\t\tdy = y - self.originy\n\t\tcol = dx / geoTileSize\n\t\trow = dy / geoTileSize\n\t\tcol = int(math.floor(col))\n\t\trow = int(math.floor(row))\n\t\treturn col, row\n\n\tdef getTileCoords(self, col, row, zoom):\n\t\t\"\"\"\n\t\tConvert tiles number to projeted coords\n\t\t(top left pixel if matrix origin is NW)\n\t\t\"\"\"\n\t\tres = self.getRes(zoom)\n\t\tgeoTileSize = self.tileSize * res\n\t\tx = self.originx + (col * geoTileSize)\n\t\tif self.originLoc == \"NW\":\n\t\t\ty = self.originy - (row * geoTileSize)\n\t\telse:\n\t\t\ty = self.originy + (row * geoTileSize) #bottom left\n\t\t\ty += geoTileSize #top left\n\t\treturn x, y\n\n\n\tdef getTileBbox(self, col, row, zoom):\n\t\txmin, ymax = self.getTileCoords(col, row, zoom)\n\t\txmax = xmin + (self.tileSize * self.getRes(zoom))\n\t\tymin = ymax - (self.tileSize * self.getRes(zoom))\n\t\treturn xmin, ymin, xmax, ymax\n\n\n\tdef bboxRequest(self, bbox, zoom):\n\t\treturn BBoxRequest(self, bbox, zoom)\n\nclass BBoxRequestMZ():\n\t'''Multiple Zoom BBox request'''\n\tdef __init__(self, tm, bbox, zooms):\n\n\t\tself.tm = tm\n\t\tself.bboxrequests = {}\n\t\tfor z in zooms:\n\t\t\tself.bboxrequests[z] = BBoxRequest(tm, bbox, z)\n\n\t@property\n\tdef tiles(self):\n\t\ttiles = []\n\t\tfor bboxrequest in self.bboxrequests.values():\n\t\t\ttiles.extend(bboxrequest.tiles)\n\t\treturn tiles\n\n\t@property\n\tdef nbTiles(self):\n\t\treturn len(self.tiles)\n\n\tdef __getitem__(self, z):\n\t\treturn self.bboxrequests[z]\n\n\nclass BBoxRequest():\n\n\tdef __init__(self, tm, bbox, zoom):\n\n\t\tself.tm = tm\n\t\tself.zoom = zoom\n\t\tself.tileSize = tm.tileSize\n\t\tself.res = tm.getRes(zoom)\n\n\t\txmin, ymin, xmax, ymax = bbox\n\n\t\t#Get first tile indices (top left of requested bbox)\n\t\tself.firstCol, self.firstRow = tm.getTileNumber(xmin, ymax, zoom)\n\n\t\t#correction of top left coord\n\t\txmin, ymax = tm.getTileCoords(self.firstCol, self.firstRow, zoom)\n\t\tself.bbox = BBOX(xmin, ymin, xmax, ymax)\n\n\t\t#Total number of tiles required\n\t\tself.nbTilesX = math.ceil( (xmax - xmin) / (self.tileSize * self.res) )\n\t\tself.nbTilesY = math.ceil( (ymax - ymin) / (self.tileSize * self.res) )\n\n\t@property\n\tdef cols(self):\n\t\treturn [self.firstCol+i for i in range(self.nbTilesX)]\n\n\t@property\n\tdef rows(self):\n\t\tif self.tm.originLoc == \"NW\":\n\t\t\treturn [self.firstRow+i for i in range(self.nbTilesY)]\n\t\telse:\n\t\t\treturn [self.firstRow-i for i in range(self.nbTilesY)]\n\n\t@property\n\tdef tiles(self):\n\t\treturn [(c, r, self.zoom) for c in self.cols for r in self.rows]\n\n\t@property\n\tdef nbTiles(self):\n\t\treturn self.nbTilesX * self.nbTilesY\n\n\t#megapixel, geosize\n\n\n\nclass MapService():\n\t\"\"\"\n\tRepresent a tile service from source\n\n\tWill inherit attributes from source definition\n\t\tname\n\t\tdescription\n\t\tservice >> 'WMS', 'TMS' or 'WMTS'\n\t\tgrid >> key identifier of the tile matrix used by this source\n\t\tmatrix >> for WMTS only, name of the matrix as refered in url\n\t\tquadTree >> boolean, for TMS only. Flag if tile coords are stord through a quadkey\n\t\tlayers >> a list layers with the following attributes\n\t\t\turlkey\n\t\t\tname\n\t\t\tdescription\n\t\t\tformat >> 'jpeg' or 'png'\n\t\t\tstyle\n\t\t\tzmin & zmax\n\t\turlTemplate\n\t\treferer\n\n\tService status code\n\t\t0 = no running tasks\n\t\t1 = getting cache (create a new db if needed)\n\t\t2 = downloading\n\t\t3 = building mosaic\n\t\t4 = reprojecting\n\t\"\"\"\n\n\t# resampling algo for reprojection\n\tRESAMP_ALG = 'BL' #NN:Nearest Neighboor, BL:Bilinear, CB:Cubic, CBS:Cubic Spline, LCZ:Lanczos\n\n\tdef __init__(self, srckey, cacheFolder, dstGridKey=None):\n\n\n\t\t#create class attributes from source dictionnary\n\t\tself.srckey = srckey\n\t\tsource = SOURCES[self.srckey]\n\t\tfor k, v in source.items():\n\t\t\tsetattr(self, k, v)\n\n\t\t#Build objects from layers definitions\n\t\tclass Layer(): pass\n\t\tlayersObj = {}\n\t\tfor layKey, layDict in self.layers.items():\n\t\t\tlay = Layer()\n\t\t\tfor k, v in layDict.items():\n\t\t\t\tsetattr(lay, k, v)\n\t\t\tlayersObj[layKey] = lay\n\t\tself.layers = layersObj\n\n\t\t#Build source tile matrix set\n\t\tself.srcGridKey = self.grid\n\t\tself.srcTms = TileMatrix(GRIDS[self.srcGridKey])\n\n\t\t#Build destination tile matrix set\n\t\tself.setDstGrid(dstGridKey)\n\n\t\t#Init cache dict\n\t\tself.cacheFolder = cacheFolder\n\t\tself.caches = {}\n\n\t\t#Fake browser header\n\t\tself.headers = {\n\t\t\t'Accept' : 'image/png,image/*;q=0.8,*/*;q=0.5' ,\n\t\t\t'Accept-Charset' : 'ISO-8859-1,utf-8;q=0.7,*;q=0.7' ,\n\t\t\t#'Accept-Encoding' : 'gzip,deflate', #urllib2 doesn't automatically uncompress the data\n\t\t\t'Accept-Language' : 'fr,en-us,en;q=0.5' ,\n\t\t\t#'Keep-Alive': 115 ,\n\t\t\t'Proxy-Connection' : 'keep-alive',\n\t\t\t'User-Agent' : USER_AGENT,\n\t\t\t'Referer' : self.referer}\n\n\t\t#Downloading progress\n\t\tself.running = False #flag using to stop getTiles() / getImage() process\n\t\tself.nbTiles = 0\n\t\tself.cptTiles = 0\n\n\t\t#codes that indicate the current status of the service\n\t\tself.status = 0\n\n\t\tself.lock = threading.RLock()\n\n\tdef reportLoop(self):\n\t\tmsg = self.report\n\t\twhile self.running:\n\t\t\ttime.sleep(0.05)\n\t\t\tif self.report != msg:\n\t\t\t\t#sys.stdout.write(\"\\033[F\") #back to previous line\n\t\t\t\tsys.stdout.write(\"\\033[K\") #clear line\n\t\t\t\tsys.stdout.flush()\n\t\t\t\tprint(self.report, end='\\r') #'\\r' will move the cursor back to the beginning of the line\n\t\t\t\tmsg = self.report\n\n\tdef start(self):\n\t\tself.running = True\n\t\treporter = threading.Thread(target=self.reportLoop)\n\t\treporter.setDaemon(True) #daemon threads will die when the main non-daemon thread have exited.\n\t\treporter.start()\n\n\tdef stop(self):\n\t\tself.running = False\n\n\t@property\n\tdef report(self):\n\t\tif self.status == 0:\n\t\t\treturn ''\n\t\tif self.status == 1:\n\t\t\treturn 'Get cache database...'\n\t\tif self.status == 2:\n\t\t\treturn 'Downloading... ' + str(self.cptTiles)+'/'+str(self.nbTiles)\n\t\tif self.status == 3:\n\t\t\treturn 'Building mosaic...'\n\t\tif self.status == 4:\n\t\t\treturn 'Reprojecting...'\n\n\n\tdef setDstGrid(self, grdkey):\n\t\t'''Set destination tile matrix'''\n\t\tif grdkey is not None and grdkey != self.srcGridKey:\n\t\t\tself.dstGridKey = grdkey\n\t\t\tself.dstTms = TileMatrix(GRIDS[grdkey])\n\t\telse:\n\t\t\tself.dstGridKey = None\n\t\t\tself.dstTms = None\n\n\n\tdef getCache(self, laykey, useDstGrid):\n\t\t'''Return existing cache for requested layer or built it if not exists'''\n\t\tif useDstGrid:\n\t\t\tif self.dstGridKey is not None:\n\t\t\t\tgrdkey = self.dstGridKey\n\t\t\t\ttm = self.dstTms\n\t\t\telse:\n\t\t\t\traise ValueError('No destination grid defined')\n\t\telse:\n\t\t\tgrdkey = self.srcGridKey\n\t\t\ttm = self.srcTms\n\n\t\tmapKey = self.srckey + '_' + laykey + '_' + grdkey\n\t\tcache = self.caches.get(mapKey)\n\t\tif cache is None:\n\t\t\tdbPath = os.path.join(self.cacheFolder, mapKey + \".gpkg\")\n\t\t\tself.caches[mapKey] = GeoPackage(dbPath, tm)\n\t\t\treturn self.caches[mapKey]\n\t\telse:\n\t\t\treturn cache\n\n\tdef getTM(self, dstGrid=False):\n\t\tif dstGrid:\n\t\t\tif self.dstTms is not None:\n\t\t\t\treturn self.dstTms\n\t\t\telse:\n\t\t\t\traise ValueError('No destination grid defined')\n\t\telse:\n\t\t\treturn self.srcTms\n\n\n\tdef buildUrl(self, laykey, col, row, zoom):\n\t\t\"\"\"\n\t\tReceive tiles coords in source tile matrix space and build request url\n\t\t\"\"\"\n\t\turl = self.urlTemplate\n\t\tlay = self.layers[laykey]\n\t\ttm = self.srcTms\n\n\t\tif self.service == 'TMS':\n\t\t\turl = url.replace(\"{LAY}\", lay.urlKey)\n\t\t\tif not self.quadTree:\n\t\t\t\turl = url.replace(\"{X}\", str(col))\n\t\t\t\turl = url.replace(\"{Y}\", str(row))\n\t\t\t\turl = url.replace(\"{Z}\", str(zoom))\n\t\t\telse:\n\t\t\t\tquadkey = self.getQuadKey(col, row, zoom)\n\t\t\t\turl = url.replace(\"{QUADKEY}\", quadkey)\n\n\t\tif self.service == 'WMTS':\n\t\t\turl = self.urlTemplate['BASE_URL']\n\t\t\tif url[-1] != '?' :\n\t\t\t\turl += '?'\n\t\t\tparams = ['='.join([k,v]) for k, v in self.urlTemplate.items() if k != 'BASE_URL']\n\t\t\turl += '&'.join(params)\n\t\t\turl = url.replace(\"{LAY}\", lay.urlKey)\n\t\t\turl = url.replace(\"{FORMAT}\", lay.format)\n\t\t\turl = url.replace(\"{STYLE}\", lay.style)\n\t\t\turl = url.replace(\"{MATRIX}\", self.matrix)\n\t\t\turl = url.replace(\"{X}\", str(col))\n\t\t\turl = url.replace(\"{Y}\", str(row))\n\t\t\turl = url.replace(\"{Z}\", str(zoom))\n\n\t\tif self.service == 'WMS':\n\t\t\turl = self.urlTemplate['BASE_URL']\n\t\t\tif url[-1] != '?' :\n\t\t\t\turl += '?'\n\t\t\tparams = ['='.join([k,v]) for k, v in self.urlTemplate.items() if k != 'BASE_URL']\n\t\t\turl += '&'.join(params)\n\t\t\turl = url.replace(\"{LAY}\", lay.urlKey)\n\t\t\turl = url.replace(\"{FORMAT}\", lay.format)\n\t\t\turl = url.replace(\"{STYLE}\", lay.style)\n\t\t\turl = url.replace(\"{CRS}\", str(tm.CRS))\n\t\t\turl = url.replace(\"{WIDTH}\", str(tm.tileSize))\n\t\t\turl = url.replace(\"{HEIGHT}\", str(tm.tileSize))\n\n\t\t\txmin, ymax = tm.getTileCoords(col, row, zoom)\n\t\t\txmax = xmin + tm.tileSize * tm.getRes(zoom)\n\t\t\tymin = ymax - tm.tileSize * tm.getRes(zoom)\n\t\t\tif self.urlTemplate['VERSION'] == '1.3.0' and tm.CRS == 'EPSG:4326':\n\t\t\t\tbbox = ','.join(map(str,[ymin,xmin,ymax,xmax]))\n\t\t\telse:\n\t\t\t\tbbox = ','.join(map(str,[xmin,ymin,xmax,ymax]))\n\t\t\turl = url.replace(\"{BBOX}\", bbox)\n\n\t\treturn url\n\n\n\tdef getQuadKey(self, x, y, z):\n\t\t\"Converts TMS tile coordinates to Microsoft QuadTree\"\n\t\tquadKey = \"\"\n\t\tfor i in range(z, 0, -1):\n\t\t\tdigit = 0\n\t\t\tmask = 1 << (i-1)\n\t\t\tif (x & mask) != 0:\n\t\t\t\tdigit += 1\n\t\t\tif (y & mask) != 0:\n\t\t\t\tdigit += 2\n\t\t\tquadKey += str(digit)\n\t\treturn quadKey\n\n\n\tdef isTileInMapsBounds(self, col, row, zoom, tm):\n\t\t'''Test if the tile is not out of tile matrix bounds'''\n\t\tx,y = tm.getTileCoords(col, row, zoom) #top left\n\t\tif row < 0 or col < 0:\n\t\t\treturn False\n\t\telif not tm.xmin <= x < tm.xmax or not tm.ymin < y <= tm.ymax:\n\t\t\treturn False\n\t\telse:\n\t\t\treturn True\n\n\n\tdef downloadTile(self, laykey, col, row, zoom):\n\t\t\"\"\"\n\t\tDownload bytes data of requested tile in source tile matrix space\n\t\tReturn None if unable to download a valid stream\n\t\t\"\"\"\n\n\t\turl = self.buildUrl(laykey, col, row, zoom)\n\t\tlog.debug(url)\n\n\t\ttry:\n\t\t\t#make request\n\t\t\treq = urllib.request.Request(url, None, self.headers)\n\t\t\thandle = urllib.request.urlopen(req, timeout=TIMEOUT)\n\t\t\t#open image stream\n\t\t\tdata = handle.read()\n\t\t\thandle.close()\n\t\texcept Exception as e:\n\t\t\tlog.error(\"Can't download tile x{} y{}. Error {}\".format(col, row, e))\n\t\t\tdata = None\n\n\t\t#Make sure the stream is correct\n\t\tif data is not None:\n\t\t\tformat = imghdr.what(None, data)\n\t\t\tif format is None:\n\t\t\t\tdata = None\n\n\t\tif data is None:\n\t\t\tlog.debug(\"Invalid tile data for request {}\".format(url))\n\n\t\treturn data\n\n\n\tdef tileRequest(self, laykey, col, row, zoom, toDstGrid=True):\n\t\t\"\"\"\n\t\tReturn bytes data of the requested tile or None if unable to get valid data\n\t\tTile is downloaded from map service and, if needed, reprojected to fit the destination grid\n\t\t\"\"\"\n\n\t\t#Select tile matrix set\n\t\ttm = self.getTM(toDstGrid)\n\n\t\t#don't try to get tiles out of map bounds\n\t\tif not self.isTileInMapsBounds(col, row, zoom, tm):\n\t\t\treturn None\n\n\t\tif not toDstGrid:\n\t\t\tdata = self.downloadTile(laykey, col, row, zoom)\n\t\telse:\n\t\t\tdata = self.buildDstTile(laykey, col, row, zoom)\n\n\t\treturn data\n\n\n\tdef buildDstTile(self, laykey, col, row, zoom):\n\t\t'''build a tile that fit the destination tile matrix'''\n\n\t\t#get tile bbox\n\t\tbbox = self.dstTms.getTileBbox(col, row, zoom)\n\t\txmin, ymin, xmax, ymax = bbox\n\n\t\t#get closest zoom level\n\t\tres = self.dstTms.getRes(zoom)\n\t\tif self.dstTms.units == 'degrees' and self.srcTms.units == 'meters':\n\t\t\tres2 = dd2meters(res)\n\t\telif self.srcTms.units == 'degrees' and self.dstTms.units == 'meters':\n\t\t\tres2 = meters2dd(res)\n\t\telse:\n\t\t\tres2 = res\n\t\t_zoom = self.srcTms.getNearestZoom(res2)\n\t\t_res = self.srcTms.getRes(_zoom)\n\n\t\t#reproj bbox\n\t\tcrs1, crs2 = self.srcTms.CRS, self.dstTms.CRS\n\t\ttry:\n\t\t\t_bbox = reprojBbox(crs2, crs1, bbox)\n\t\texcept Exception as e:\n\t\t\tlog.warning('Cannot reproj tile bbox - ' + str(e))\n\t\t\treturn None\n\n\t\t#list, download and merge the tiles required to build this one (recursive call)\n\t\tmosaic = self.getImage(laykey, _bbox, _zoom, toDstGrid=False, nbThread=4, cpt=False)\n\n\t\tif mosaic is None:\n\t\t\treturn None\n\n\t\t#Reprojection\n\t\ttileSize = self.dstTms.tileSize\n\t\timg = NpImage(reprojImg(crs1, crs2, mosaic.toGDAL(), out_ul=(xmin,ymax), out_size=(tileSize,tileSize), out_res=res, sqPx=True, resamplAlg=self.RESAMP_ALG))\n\n\t\treturn img.toBLOB()\n\n\n\n\n\tdef seedTiles(self, laykey, tiles, toDstGrid=True, nbThread=10, buffSize=5000, cpt=True):\n\t\t\"\"\"\n\t\tSeed the cache by downloading the requested tiles from map service\n\t\tDownloads are performed through thread to speed up\n\n\t\tbuffSize : maximum number of tiles keeped in memory before put them in cache database\n\t\t\"\"\"\n\n\t\tdef downloading(laykey, tilesQueue, tilesData, toDstGrid):\n\t\t\t'''Worker that process the queue and seed tilesData array [(x,y,z,data)]'''\n\t\t\t#infinite loop that processes items into the queue\n\t\t\twhile not tilesQueue.empty(): #empty is True if all item was get but it not tell if all task was done\n\t\t\t\t#cancel thread if requested\n\t\t\t\tif not self.running:\n\t\t\t\t\tbreak\n\t\t\t\t#Get a job into the queue\n\t\t\t\tcol, row, zoom = tilesQueue.get() #get() pop the item from queue\n\t\t\t\t#do the job\n\t\t\t\tdata = self.tileRequest(laykey, col, row, zoom, toDstGrid)\n\t\t\t\tif data is not None:\n\t\t\t\t\ttilesData.put( (col, row, zoom, data) ) #will block if the queue is full\n\t\t\t\tif cpt:\n\t\t\t\t\tself.cptTiles += 1\n\t\t\t\t#self.nTaskDone += 1\n\t\t\t\t#flag it's done\n\t\t\t\ttilesQueue.task_done() #it's just a count of finished tasks used by join() to know if the work is finished\n\n\t\tdef finished():\n\t\t\t#return self.nTaskDone == nMissing\n\t\t\t#self.nTaskDone is not reliable because the recursive call to getImage will\n\t\t\t#start multiple threads to seedTiles() and all these process will increments nTaskDone\n\t\t\treturn not any([t.is_alive() for t in threads])\n\n\t\tdef putInCache(tilesData, jobs, cache):\n\t\t\twhile True:\n\t\t\t\tif tilesData.full() or \\\n\t\t\t\t( (finished() or not self.running) and not tilesData.empty()):\n\t\t\t\t\tdata = [tilesData.get() for i in range(tilesData.qsize())]\n\t\t\t\t\twith self.lock:\n\t\t\t\t\t\tcache.putTiles(data)\n\t\t\t\tif finished() and tilesData.empty():\n\t\t\t\t\tbreak\n\t\t\t\tif not self.running:\n\t\t\t\t\tbreak\n\n\t\tif cpt:\n\t\t\t#init cpt progress\n\t\t\tself.nbTiles = len(tiles)\n\t\t\tself.cptTiles = 0\n\n\t\t#self.nTaskDone = 0\n\n\t\t#Get cache db\n\t\tif cpt:\n\t\t\tself.status = 1\n\t\tcache = self.getCache(laykey, toDstGrid)\n\t\tmissing = cache.listMissingTiles(tiles)\n\t\tnMissing = len(missing)\n\t\tnExists = self.nbTiles - len(missing)\n\t\tlog.debug(\"{} tiles requested, {} already in cache, {} remains to download\".format(self.nbTiles, nExists, nMissing))\n\t\tif cpt:\n\t\t\tself.cptTiles += nExists\n\n\t\t#Downloading tiles\n\t\tif cpt:\n\t\t\tself.status = 2\n\t\tif len(missing) > 0:\n\n\t\t\t#Result queue\n\t\t\ttilesData = queue.Queue(maxsize=buffSize)\n\n\t\t\t#Seed the queue\n\t\t\tjobs = queue.Queue()\n\t\t\tfor tile in missing:\n\t\t\t\tjobs.put(tile)\n\n\t\t\t#Launch threads\n\t\t\tthreads = []\n\t\t\tfor i in range(nbThread):\n\t\t\t\tt = threading.Thread(target=downloading, args=(laykey, jobs, tilesData, toDstGrid))\n\t\t\t\tt.setDaemon(True)\n\t\t\t\tthreads.append(t)\n\t\t\t\tt.start()\n\n\t\t\tseeder = threading.Thread(target=putInCache, args=(tilesData, jobs, cache))\n\t\t\tseeder.setDaemon(True)\n\t\t\tseeder.start()\n\t\t\tseeder.join()\n\n\t\t\t#Make sure all threads has finished\n\t\t\tfor t in threads:\n\t\t\t\tt.join()\n\n\t\t#Reinit status and cpt progress\n\t\tif cpt:\n\t\t\tself.status = 0\n\t\t\tself.nbTiles, self.cptTiles = 0, 0\n\n\n\tdef getTiles(self, laykey, tiles, toDstGrid=True, nbThread=10, cpt=True):\n\t\t\"\"\"\n\t\tReturn bytes data of requested tiles\n\t\tinput: [(x,y,z)] >> output: [(x,y,z,data)]\n\t\tTiles are downloaded from map service or directly pick up from cache database.\n\t\t\"\"\"\n\t\t#seed the cache\n\t\tself.seedTiles(laykey, tiles, toDstGrid=toDstGrid, nbThread=10, cpt=cpt)\n\t\t#request the cache and return\n\t\tcache = self.getCache(laykey, toDstGrid)\n\t\treturn cache.getTiles(tiles) #[(x,y,z,data)]\n\n\n\tdef getTile(self, laykey, col, row, zoom, toDstGrid=True):\n\t\treturn self.getTiles(laykey, [col, row, zoom], toDstGrid)[0]\n\n\n\tdef bboxRequest(self, bbox, zoom, dstGrid=True):\n\t\t#Select tile matrix set\n\t\ttm = self.getTM(dstGrid)\n\t\treturn BBoxRequest(tm, bbox, zoom)\n\n\n\tdef seedCache(self, laykey, bbox, zoom, toDstGrid=True, nbThread=10, buffSize=5000):\n\t\t\"\"\"\n\t\tSeed the cache with the tiles covering the requested bbox\n\t\t\"\"\"\n\t\t#Select tile matrix set\n\t\ttm = self.getTM(toDstGrid)\n\t\tif isinstance(zoom, list):\n\t\t\trq = BBoxRequestMZ(tm, bbox, zoom)\n\t\telse:\n\t\t\trq = BBoxRequest(tm, bbox, zoom)\n\t\tself.seedTiles(laykey, rq.tiles, toDstGrid=toDstGrid, nbThread=10, buffSize=5000)\n\n\n\tdef getImage(self, laykey, bbox, zoom, path=None, bigTiff=False, outCRS=None, toDstGrid=True, nbThread=10, cpt=True):\n\t\t\"\"\"\n\t\tBuild a mosaic of tiles covering the requested bounding box\n\t\t#laykey (str)\n\t\t#bbox\n\t\t#zoom (int)\n\t\t#path (str): if None the function will return a georeferenced NpImage object. If not None, then the resulting output will be\n\t\twriten as geotif file on disk and the function will return None\n\t\t#bigTiff (bool): if true then the raster will be writen by small part with the help of GDAL API. If false the raster will be\n\t\twriten at one, in this case all the tiles must fit in memory otherwise it will raise a memory overflow error\n\t\t#outCRS : destination CRS if a reprojection if expected (require GDAL support)\n\t\t#toDstGrid (bool) : decide if the function will seed the destination tile matrix sets for this MapService instance\n\t\t(different from the source tile matrix set)\n\t\t#nbThread (int) : nimber of threads that will be used for downloading tiles\n\t\t#cpt (bool) : define if the service must report or not tiles downloading count for this request\n\t\t\"\"\"\n\n\t\t#Select tile matrix set\n\t\ttm = self.getTM(toDstGrid)\n\n\t\t#Get request\n\t\trq = BBoxRequest(tm, bbox, zoom)\n\t\ttileSize = rq.tileSize\n\t\tres = rq.res\n\t\tcols, rows = rq.cols, rq.rows\n\t\trqTiles = rq.tiles #[(x,y,z)]\n\n\t\t##method 1) Seed the cache with all required tiles\n\t\tself.seedCache(laykey, bbox, zoom, toDstGrid=toDstGrid, nbThread=nbThread, buffSize=5000)\n\t\tcache = self.getCache(laykey, toDstGrid)\n\n\t\tif not self.running:\n\t\t\tif cpt:\n\t\t\t\tself.status = 0\n\t\t\treturn\n\n\t\t#Get georef parameters\n\t\timg_w, img_h = len(cols) * tileSize, len(rows) * tileSize\n\t\txmin, ymin, xmax, ymax = rq.bbox\n\t\tgeoref = GeoRef((img_w, img_h), (res, -res), (xmin, ymax), pxCenter=False, crs=tm.crs)\n\n\t\tif bigTiff and path is None:\n\t\t\traise ValueError('No output path defined for creating bigTiff')\n\n\t\tif not bigTiff:\n\t\t\t#Create numpy image in memory\n\t\t\tmosaic = NpImage.new(img_w, img_h, bkgColor=MOSAIC_BKG_COLOR, georef=georef)\n\t\t\tchunkSize = rq.nbTiles\n\t\telse:\n\t\t\t#Create bigtiff file on disk\n\t\t\tmosaic = BigTiffWriter(path, img_w, img_h, georef)\n\t\t\tds = mosaic.ds\n\t\t\tchunkSize = 5 #number of tiles to extract in one cache request\n\n\t\t#Build mosaic\n\t\tfor i in range(0, rq.nbTiles, chunkSize):\n\t\t\tchunkTiles = rqTiles[i:i+chunkSize]\n\n\t\t\t##method 1) Get cached tiles\n\t\t\ttiles = cache.getTiles(chunkTiles) #[(x,y,z,data)]\n\n\t\t\t##method 2) Get tiles from www or cache (all tiles must fit in memory)\n\t\t\t#tiles = self.getTiles(laykey, chunkTiles, toDstGrid, nbThread, cpt)\n\n\t\t\tif cpt:\n\t\t\t\tself.status = 3\n\t\t\tfor tile in tiles:\n\n\t\t\t\tif not self.running:\n\t\t\t\t\tif cpt:\n\t\t\t\t\t\tself.status = 0\n\t\t\t\t\treturn None\n\n\t\t\t\tcol, row, z, data = tile\n\n\t\t\t\t#TODO corrupted or empty tiles must be deleted from cache are fetched again\n\t\t\t\tif data is None:\n\t\t\t\t\t#create an empty tile\n\t\t\t\t\timg = NpImage.new(tileSize, tileSize, bkgColor=EMPTY_TILE_COLOR)\n\t\t\t\telse:\n\t\t\t\t\ttry:\n\t\t\t\t\t\timg = NpImage(data)\n\t\t\t\t\texcept Exception as e:\n\t\t\t\t\t\tlog.error('Corrupted tile on cache', exc_info=True)\n\t\t\t\t\t\t#create an empty tile if we are unable to get a valid stream\n\t\t\t\t\t\timg = NpImage.new(tileSize, tileSize, bkgColor=CORRUPTED_TILE_COLOR)\n\n\n\t\t\t\tposx = (col - rq.firstCol) * tileSize\n\t\t\t\tposy = abs((row - rq.firstRow)) * tileSize\n\t\t\t\tmosaic.paste(img, posx, posy)\n\n\t\tif not self.running:\n\t\t\tif cpt:\n\t\t\t\tself.status = 0\n\t\t\treturn None\n\n\t\t#Reproject if needed\n\t\tif outCRS is not None and outCRS != tm.CRS:\n\t\t\tif cpt:\n\t\t\t\tself.status = 4\n\t\t\ttime.sleep(0.1) #make sure client have enough time to get the new status...\n\n\t\t\tif not bigTiff:\n\t\t\t\tmosaic = NpImage(reprojImg(tm.CRS, outCRS, mosaic.toGDAL(), sqPx=True, resamplAlg=self.RESAMP_ALG))\n\t\t\telse:\n\t\t\t\toutPath = path[:-4] + '_' + str(outCRS) + '.tif'\n\t\t\t\tds = reprojImg(tm.CRS, outCRS, mosaic.ds, sqPx=True, resamplAlg=self.RESAMP_ALG, path=outPath)\n\n\t\t#build overviews for file output\n\t\tif bigTiff:\n\t\t\tds.BuildOverviews(overviewlist=[2,4,8,16,32])\n\t\t\tds = None\n\n\t\tif not bigTiff and path is not None:\n\t\t\tmosaic.save(path)\n\n\t\t#Finish\n\t\tif cpt:\n\t\t\tself.status = 0\n\t\tif path is None:\n\t\t\treturn mosaic\n\t\telse:\n\t\t\treturn None\n"
  },
  {
    "path": "core/basemaps/servicesDefs.py",
    "content": "# -*- coding:utf-8 -*-\n\nimport math\n\n####################################\n\n#        Tiles maxtrix definitions\n\n####################################\n\n# Three ways to define a grid (inpired by http://mapproxy.org/docs/1.8.0/configuration.html#id6):\n# - submit a list of resolutions > \"resolutions\": [32,16,8,4] (This parameters override the others)\n# - submit just \"resFactor\", initial res is computed such as at zoom level zero, 1 tile covers whole bounding box\n# - submit \"resFactor\" and \"initRes\"\n\n\n# About Web Mercator\n# Technically, the Mercator projection is defined for any latitude up to (but not including)\n# 90 degrees, but it makes sense to cut it off sooner because it grows exponentially with\n# increasing latitude. The logic behind this particular cutoff value, which is the one used\n# by Google Maps, is that it makes the projection square. That is, the rectangle is equal in\n# the X and Y directions. In this case the maximum latitude attained must correspond to y = w/2.\n# y = 2*pi*R / 2 = pi*R --> y/R = pi\n# lat = atan(sinh(y/R)) = atan(sinh(pi))\n# wm_origin = (-20037508, 20037508) with 20037508 = GRS80.perimeter / 2\n\ncutoff_lat = math.atan(math.sinh(math.pi)) * 180/math.pi #= 85.05112°\n\n\nGRIDS = {\n\n\n\t\"WM\" : {\n\t\t\"name\" : 'Web Mercator',\n\t\t\"description\" : 'Global grid in web mercator projection',\n\t\t\"CRS\": 'EPSG:3857',\n\t\t\"bbox\": [-180, -cutoff_lat, 180, cutoff_lat], #w,s,e,n\n\t\t\"bboxCRS\": 'EPSG:4326',\n\t\t#\"bbox\": [-20037508, -20037508, 20037508, 20037508],\n\t\t#\"bboxCRS\": 3857,\n\t\t\"tileSize\": 256,\n\t\t\"originLoc\": \"NW\", #North West or South West\n\t\t\"resFactor\" : 2\n\t},\n\n\n\t\"WGS84\" : {\n\t\t\"name\" : 'WGS84',\n\t\t\"description\" : 'Global grid in wgs84 projection',\n\t\t\"CRS\": 'EPSG:4326',\n\t\t\"bbox\": [-180, -90, 180, 90], #w,s,e,n\n\t\t\"bboxCRS\": 'EPSG:4326',\n\t\t\"tileSize\": 256,\n\t\t\"originLoc\": \"NW\", #North West or South West\n\t\t\"resFactor\" : 2\n\t},\n\n\t#this one produce valid MBtiles files, because origin is bottom left\n\t\"WM_SW\" : {\n\t\t\"name\" : 'Web Mercator TMS',\n\t\t\"description\" : 'Global grid in web mercator projection, origin South West',\n\t\t\"CRS\": 'EPSG:3857',\n\t\t\"bbox\": [-180, -cutoff_lat, 180, cutoff_lat], #w,s,e,n\n\t\t\"bboxCRS\": 'EPSG:4326',\n\t\t#\"bbox\": [-20037508, -20037508, 20037508, 20037508],\n\t\t#\"bboxCRS\": 'EPSG:3857',\n\t\t\"tileSize\": 256,\n\t\t\"originLoc\": \"SW\", #North West or South West\n\t\t\"resFactor\" : 2\n\t},\n\n\n\t#####################\n\t#Custom grid example\n\t######################\n\n\t# >> France Lambert 93\n\t\"LB93\" : {\n\t\t\"name\" : 'Fr Lambert 93',\n\t\t\"description\" : 'Local grid in French Lambert 93 projection',\n\t\t\"CRS\": 'EPSG:2154',\n\t\t\"bbox\": [99200, 6049600, 1242500, 7110500], #w,s,e,n\n\t\t\"bboxCRS\": 'EPSG:2154',\n\t\t\"tileSize\": 256,\n\t\t\"originLoc\": \"NW\", #North West or South West\n\t\t\"resFactor\" : 2\n\t},\n\n\t# >> Another France Lambert 93 (submited list of resolution)\n\t\"LB93_2\" : {\n\t\t\"name\" : 'Fr Lambert 93 v2',\n\t\t\"description\" : 'Local grid in French Lambert 93 projection',\n\t\t\"CRS\": 'EPSG:2154',\n\t\t\"bbox\": [99200, 6049600, 1242500, 7110500], #w,s,e,n\n\t\t\"bboxCRS\": 'EPSG:2154',\n\t\t\"tileSize\": 256,\n\t\t\"originLoc\": \"SW\", #North West or South West\n\t\t\"resolutions\" : [4000, 2000, 1000, 500, 250, 100, 50, 25, 10, 5, 2, 1, 0.5, 0.25, 0.1] #15 levels\n\t},\n\n\n\t# >> France Lambert 93 used by CRAIG WMTS\n\t# WMTS resolution = ScaleDenominator * 0.00028\n\t# (0.28 mm = physical distance of a pixel (WMTS assumes a DPI 90.7)\n\t\"LB93_CRAIG\" : {\n\t\t\"name\" : 'Fr Lambert 93 CRAIG',\n\t\t\"description\" : 'Local grid in French Lambert 93 projection',\n\t\t\"CRS\": 'EPSG:2154',\n\t\t\"bbox\": [-357823.23, 6037001.46, 1313634.34, 7230727.37], #w,s,e,n\n\t\t\"bboxCRS\": 'EPSG:2154',\n\t\t\"tileSize\": 256,\n\t\t\"originLoc\": \"NW\",\n\t\t\"initRes\": 1354.666,\n\t\t\"resFactor\" : 2\n\t},\n\n}\n\n\n####################################\n\n#        Sources definitions\n\n####################################\n\n#With TMS or WMTS, grid must match the one used by the service\n#With WMS you can use any grid you want but the grid CRS must\n#match one of those provided by the WMS service\n\n#The grid associated to the source define the CRS\n#A source can have multiple layers but have only one grid\n#so to support multiple grid it's necessary to duplicate source definition\n\nSOURCES = {\n\n\n\t###############\n\t# TMS examples\n\t###############\n\n\n\t\"GOOGLE\" : {\n\t\t\"name\" : 'Google',\n\t\t\"description\" : 'Google map',\n\t\t\"service\": 'TMS',\n\t\t\"grid\": 'WM',\n\t\t\"quadTree\": False,\n\t\t\"layers\" : {\n\t\t\t\"SAT\" : {\"urlKey\" : 's', \"name\" : 'Satellite', \"description\" : '', \"format\" : 'jpeg', \"zmin\" : 0, \"zmax\" : 22},\n\t\t\t\"MAP\" : {\"urlKey\" : 'm', \"name\" : 'Map', \"description\" : '', \"format\" : 'png', \"zmin\" : 0, \"zmax\" : 22}\n\t\t},\n\t\t\"urlTemplate\": \"http://mt0.google.com/vt/lyrs={LAY}&x={X}&y={Y}&z={Z}\",\n\t\t\"referer\": \"https://www.google.com/maps\"\n\t},\n\n\n\t\"OSM\" : {\n\t\t\"name\" : 'OSM',\n\t\t\"description\" : 'Open Street Map',\n\t\t\"service\": 'TMS',\n\t\t\"grid\": 'WM',\n\t\t\"quadTree\": False,\n\t\t\"layers\" : {\n\t\t\t\"MAPNIK\" : {\"urlKey\" : '', \"name\" : 'Mapnik', \"description\" : '', \"format\" : 'png', \"zmin\" : 0, \"zmax\" : 19}\n\t\t},\n\t\t\"urlTemplate\": \"https://tile.openstreetmap.org/{Z}/{X}/{Y}.png\",\n\t\t\"referer\": \"\" #https://www.openstreetmap.org will return 418 error\n\t},\n\n\n\t\"BING\" : {\n\t\t\"name\" : 'Bing',\n\t\t\"description\" : 'Microsoft Bing Map',\n\t\t\"service\": 'TMS',\n\t\t\"grid\": 'WM',\n\t\t\"quadTree\": True,\n\t\t\"layers\" : {\n\t\t\t\"SAT\" : {\"urlKey\" : 'A', \"name\" : 'Satellite', \"description\" : '', \"format\" : 'jpeg', \"zmin\" : 0, \"zmax\" : 22},\n\t\t\t\"MAP\" : {\"urlKey\" : 'G', \"name\" : 'Map', \"description\" : '', \"format\" : 'png', \"zmin\" : 0, \"zmax\" : 22}\n\t\t},\n\t\t\"urlTemplate\": \"http://ak.dynamic.t0.tiles.virtualearth.net/comp/ch/{QUADKEY}?it={LAY}\",\n\t\t\"referer\": \"http://www.bing.com/maps\"\n\t},\n\n\n\t\"ESRI\" : {\n\t\t\"name\" : 'Esri',\n\t\t\"description\" : 'Esri ArcGIS',\n\t\t\"service\": 'TMS',\n\t\t\"grid\": 'WM',\n\t\t\"quadTree\": False,\n\t\t\"layers\" : {\n\t\t\t\"AERIAL\" : {\"urlKey\" : 'World_Imagery', \"name\" : 'Aerial', \"description\" : '', \"format\" : 'jpeg', \"zmin\" : 0, \"zmax\" : 23},\n\t\t\t\"NATGEO\" : {\"urlKey\" : 'NatGeo_World_Map', \"name\" : 'National Geographic', \"description\" : '', \"format\" : 'jpeg', \"zmin\" : 0, \"zmax\" : 16},\n\t\t\t\"USATOPO\" : {\"urlKey\" : 'USA_Topo_Maps', \"name\" : 'USA Topo', \"description\" : '', \"format\" : 'jpeg', \"zmin\" : 0, \"zmax\" : 15},\n\t\t\t\"PHYSICAL\" : {\"urlKey\" : 'World_Physical_Map', \"name\" : 'Physical', \"description\" : '', \"format\" : 'jpeg', \"zmin\" : 0, \"zmax\" : 8},\n\t\t\t\"RELIEF\" : {\"urlKey\" : 'World_Shaded_Relief', \"name\" : 'Shaded Relief', \"description\" : '', \"format\" : 'jpeg', \"zmin\" : 0, \"zmax\" : 13},\n\t\t\t\"STREET\" : {\"urlKey\" : 'World_Street_Map', \"name\" : 'Street Map', \"description\" : '', \"format\" : 'jpeg', \"zmin\" : 0, \"zmax\" : 23},\n\t\t\t\"TOPO\" : {\"urlKey\" : 'World_Topo_Map', \"name\" : 'Topo with relief', \"description\" : '', \"format\" : 'jpeg', \"zmin\" : 0, \"zmax\" : 23},\n\t\t\t\"TERRAINB\" : {\"urlKey\" : 'World_Terrain_Base', \"name\" : 'Terrain Base', \"description\" : '', \"format\" : 'jpeg', \"zmin\" : 0, \"zmax\" : 13},\n\t\t\t\"CANVASLIGHTB\" : {\"urlKey\" : 'Canvas/World_Light_Gray_Base', \"name\" : 'Canvas Light Gray Base', \"description\" : '', \"format\" : 'jpeg', \"zmin\" : 0, \"zmax\" : 23},\n\t\t\t\"CANVASDARKB\" : {\"urlKey\" : 'Canvas/World_Dark_Gray_Base', \"name\" : 'Canvas Dark Gray Base', \"description\" : '', \"format\" : 'jpeg', \"zmin\" : 0, \"zmax\" : 23},\n\t\t\t\"OCEANB\" : {\"urlKey\" : 'Ocean/World_Ocean_Base', \"name\" : 'Ocean Base', \"description\" : '', \"format\" : 'jpeg', \"zmin\" : 0, \"zmax\" : 23}\n\t\t},\n\t\t\"urlTemplate\": \"https://server.arcgisonline.com/ArcGIS/rest/services/{LAY}/MapServer/tile/{Z}/{Y}/{X}\",\n\t\t\"referer\": \"https://server.arcgisonline.com/arcgis/rest/services\"\n\t},\n\n\n\t###############\n\t# WMS examples\n\t###############\n\n\t#with WMS you can set source grid as you want, the only condition is that the grid\n\t#crs must match one on crs provided by WMS\n\n\n\t\"OSM_WMS\" : {\n\t\t\"name\" : 'OSM WMS',\n\t\t\"description\" : 'Open Street Map WMS',\n\t\t\"service\": 'WMS',\n\t\t\"grid\": 'WM',\n\t\t\"layers\" : {\n\t\t\t\"WRLD\" : {\"urlKey\" : 'osm_auto:all', \"name\" : 'WMS', \"description\" : '', \"format\" : 'png', \"style\" : '', \"zmin\" : 0, \"zmax\" : 20}\n\t\t},\n\t\t\"urlTemplate\": {\n\t\t\t\"BASE_URL\" : ' http://maps.heigit.org/osm-wms/service?',\n\t\t\t\"SERVICE\" : 'WMS',\n\t\t\t\"VERSION\" : '1.1.1',\n\t\t\t\"REQUEST\" : 'GetMap',\n\t\t\t\"SRS\" : '{CRS}', #EPSG:xxxx\n\t\t\t\"LAYERS\" : '{LAY}',\n\t\t\t\"FORMAT\" : 'image/{FORMAT}',\n\t\t\t\"STYLES\" : '{STYLE}',\n\t\t\t\"BBOX\" : '{BBOX}', #xmin,ymin,xmax,ymax, in \"SRS\" projection\n\t\t\t\"WIDTH\" : '{WIDTH}',\n\t\t\t\"HEIGHT\" : '{HEIGHT}',\n\t\t\t\"TRANSPARENT\" : \"False\"\n\t\t\t},\n\t\t\"referer\": \"http://www.osm-wms.de/\"\n\t},\n\n\n\t\"GEOPORTAIL\" : {\n\t\t\"name\" : 'Geoportail',\n\t\t\"description\" : 'Geoportail.fr',\n\t\t\"service\": 'WMTS',\n\t\t\"grid\": 'WM',\n\t\t\"matrix\" : 'PM',\n\t\t\"layers\" : {\n\t\t\t\"ORTHO\" : {\"urlKey\" : 'ORTHOIMAGERY.ORTHOPHOTOS', \"name\" : 'Orthophotos', \"description\" : '',\n\t\t\t\t\"format\" : 'jpeg', \"style\" : 'normal', \"zmin\" : 0, \"zmax\" : 22},\n\t\t\t\"CAD\" : {\"urlKey\" : 'CADASTRALPARCELS.PARCELS', \"name\" : 'Cadastre', \"description\" : '',\n\t\t\t\t\"format\" : 'png', \"style\" : 'bdparcellaire', \"zmin\" : 0, \"zmax\" : 22}\n\t\t},\n\t\t\"urlTemplate\": {\n\t\t\t\"BASE_URL\" : 'https://data.geopf.fr/wmts?',\n\t\t\t\"SERVICE\" : 'WMTS',\n\t\t\t\"VERSION\" : '1.0.0',\n\t\t\t\"REQUEST\" : 'GetTile',\n\t\t\t\"LAYER\" : '{LAY}',\n\t\t\t\"STYLE\" : '{STYLE}',\n\t\t\t\"FORMAT\" : 'image/{FORMAT}',\n\t\t\t\"TILEMATRIXSET\" : '{MATRIX}',\n\t\t\t\"TILEMATRIX\" : '{Z}',\n\t\t\t\"TILEROW\" : '{Y}',\n\t\t\t\"TILECOL\" : '{X}'\n\t\t\t},\n\t\t\"referer\": \"http://www.geoportail.gouv.fr/accueil\"\n\t},\n\n\t\"GEOPORTAIL2\" : {\n\t\t\"name\" : 'Geoportail ©scan',\n\t\t\"description\" : 'Geoportail.fr',\n\t\t\"service\": 'WMTS',\n\t\t\"grid\": 'WM',\n\t\t\"matrix\" : 'PM',\n\t\t\"layers\" : {\n\t\t\t\"SCAN25\" : {\"urlKey\" : 'GEOGRAPHICALGRIDSYSTEMS.MAPS.SCAN25TOUR', \"name\" : 'Scan25', \"description\" : '',\n\t\t\t\t\"format\" : 'jpeg', \"style\" : 'normal', \"zmin\" : 0, \"zmax\" : 22},\n\t\t\t\"SCAN\" : {\"urlKey\" : 'GEOGRAPHICALGRIDSYSTEMS.MAPS', \"name\" : 'Scan', \"description\" : '',\n\t\t\t\t\"format\" : 'jpeg', \"style\" : 'normal', \"zmin\" : 0, \"zmax\" : 22}\n\t\t},\n\t\t\"urlTemplate\": {\n\t\t\t\"BASE_URL\" : 'https://data.geopf.fr/private/wmts?',\n\t\t\t\"SERVICE\" : 'WMTS',\n\t\t\t\"VERSION\" : '1.0.0',\n\t\t\t\"REQUEST\" : 'GetTile',\n\t\t\t\"LAYER\" : '{LAY}',\n\t\t\t\"STYLE\" : '{STYLE}',\n\t\t\t\"FORMAT\" : 'image/{FORMAT}',\n\t\t\t\"TILEMATRIXSET\" : '{MATRIX}',\n\t\t\t\"TILEMATRIX\" : '{Z}',\n\t\t\t\"TILEROW\" : '{Y}',\n\t\t\t\"TILECOL\" : '{X}',\n\t\t\t\"apikey\" : \"ign_scan_ws\"\n\t\t\t},\n\t\t\"referer\": \"http://www.geoportail.gouv.fr/accueil\"\n\t}\n\n}\n\"\"\"\n\t#http://wms.craig.fr/ortho?SERVICE=WMS&REQUEST=GetCapabilities\n\t# example of valid location in Auvergne : lat 45.77 long 3.082\n\t\"CRAIG_WMS\" : {\n\t\t\"name\" : 'CRAIG WMS',\n\t\t\"description\" : \"Centre Régional Auvergnat de l'Information Géographique\",\n\t\t\"service\": 'WMS',\n\t\t\"grid\": 'LB93',\n\t\t\"layers\" : {\n\t\t\t\"ORTHO\" : {\"urlKey\" : 'auvergne', \"name\" : 'Auv25cm_2013', \"description\" : '', \"format\" : 'png', \"style\" : 'default', \"zmin\" : 0, \"zmax\" : 22}\n\t\t},\n\t\t\"urlTemplate\": {\n\t\t\t\"BASE_URL\" : 'http://wms.craig.fr/ortho?',\n\t\t\t\"SERVICE\" : 'WMS',\n\t\t\t\"VERSION\" : '1.3.0',\n\t\t\t\"REQUEST\" : 'GetMap',\n\t\t\t\"CRS\" : '{CRS}',\n\t\t\t\"LAYERS\" : '{LAY}',\n\t\t\t\"FORMAT\" : 'image/{FORMAT}',\n\t\t\t\"STYLES\" : '{STYLE}',\n\t\t\t\"BBOX\" : '{BBOX}', #xmin,ymin,xmax,ymax, in \"SRS\" projection\n\t\t\t\"WIDTH\" : '{WIDTH}',\n\t\t\t\"HEIGHT\" : '{HEIGHT}',\n\t\t\t\"TRANSPARENT\" : \"False\"\n\t\t\t},\n\t\t\"referer\": \"http://www.craig.fr/\"\n\t},\n\n\n\t###############\n\t# WMTS examples\n\t###############\n\n\n\t# http://tiles.craig.fr/ortho/service?service=WMTS&REQUEST=GetCapabilities\n\t# example of valid location in Auvergne : lat 45.77 long 3.082\n\t\"CRAIG_WMTS93\" : {\n\t\t\"name\" : 'CRAIG WMTS93',\n\t\t\"description\" : \"Centre Régional Auvergnat de l'Information Géographique\",\n\t\t\"service\": 'WMTS',\n\t\t\"grid\": 'LB93_CRAIG',\n\t\t\"matrix\" : 'lambert93',\n\t\t\"layers\" : {\n\t\t\t\"ORTHO\" : {\"urlKey\" : 'ortho_2013', \"name\" : 'Auv25cm_2013', \"description\" : '',\n\t\t\t\t\"format\" : 'jpeg', \"style\" : 'default', \"zmin\" : 0, \"zmax\" : 15}\n\t\t},\n\t\t\"urlTemplate\": {\n\t\t\t\"BASE_URL\" : 'http://tiles.craig.fr/ortho/service?',\n\t\t\t\"SERVICE\" : 'WMTS',\n\t\t\t\"VERSION\" : '1.0.0',\n\t\t\t\"REQUEST\" : 'GetTile',\n\t\t\t\"LAYER\" : '{LAY}',\n\t\t\t\"STYLE\" : '{STYLE}',\n\t\t\t\"FORMAT\" : 'image/{FORMAT}',\n\t\t\t\"TILEMATRIXSET\" : '{MATRIX}',\n\t\t\t\"TILEMATRIX\" : '{Z}',\n\t\t\t\"TILEROW\" : '{Y}',\n\t\t\t\"TILECOL\" : '{X}'\n\t\t\t},\n\t\t\"referer\": \"http://www.craig.fr/\"\n\t},\n\n\n\n}\n\"\"\"\n"
  },
  {
    "path": "core/checkdeps.py",
    "content": "import logging\nlog = logging.getLogger(__name__)\n\n#GDAL\ntry:\n\tfrom osgeo import gdal\nexcept:\n\tHAS_GDAL = False\n\tlog.debug('GDAL Python binding unavailable')\nelse:\n\tHAS_GDAL = True\n\tlog.debug('GDAL Python binding available')\n\n\n#PyProj\ntry:\n\timport pyproj\nexcept:\n\tHAS_PYPROJ = False\n\tlog.debug('PyProj unavailable')\nelse:\n\tHAS_PYPROJ = True\n\tlog.debug('PyProj available')\n\n\n#PIL/Pillow\ntry:\n\tfrom PIL import Image\nexcept:\n\tHAS_PIL = False\n\tlog.debug('Pillow unavailable')\nelse:\n\tHAS_PIL = True\n\tlog.debug('Pillow available')\n\n\n#Imageio freeimage plugin\ntry:\n\tfrom .lib import imageio\n\timageio.plugins._freeimage.get_freeimage_lib() #try to download freeimage lib\nexcept Exception as e:\n\tlog.error(\"Cannot install ImageIO's Freeimage plugin\", exc_info=True)\n\tHAS_IMGIO = False\nelse:\n\tHAS_IMGIO = True\n\tlog.debug('ImageIO Freeimage plugin available')\n"
  },
  {
    "path": "core/errors.py",
    "content": "\n\n\nclass OverlapError(Exception):\n\tdef __init__(self):\n\t\tpass\n\tdef __str__(self):\n\t\treturn \"Non overlap data\"\n\nclass ReprojError(Exception):\n\tdef __init__(self, value):\n\t\tself.value = value\n\tdef __str__(self):\n\t\treturn repr(self.value)\n\nclass ApiKeyError(Exception):\n\tdef __init__(self):\n\t\tpass\n\tdef __str__(self):\n\t\treturn \"Missing or wrong API key\"\n"
  },
  {
    "path": "core/georaster/__init__.py",
    "content": "from .georef import GeoRef\nfrom .georaster import GeoRaster\nfrom .npimg import NpImage\nfrom .bigtiffwriter import BigTiffWriter\nfrom .img_utils import getImgFormat, getImgDim, isValidStream\n"
  },
  {
    "path": "core/georaster/bigtiffwriter.py",
    "content": "# -*- coding:utf-8 -*-\n\n# This file is part of BlenderGIS\n\n#  ***** GPL LICENSE BLOCK *****\n#\n#  This program is free software: you can redistribute it and/or modify\n#  it under the terms of the GNU General Public License as published by\n#  the Free Software Foundation, either version 3 of the License, or\n#  (at your option) any later version.\n#\n#  This program is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#  GNU General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public License\n#  along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#  All rights reserved.\n#  ***** GPL LICENSE BLOCK *****\n\n\nimport os\nimport numpy as np\nfrom .npimg import NpImage\n\n\nfrom ..checkdeps import HAS_GDAL, HAS_PIL, HAS_IMGIO\n\nif HAS_GDAL:\n\tfrom osgeo import gdal\n\n\nclass BigTiffWriter():\n\t'''\n\tThis class is designed to write a bigtif with jpeg compression\n\twriting a large tiff file without trigger a memory overflow is possible with the help of GDAL library\n\tjpeg compression allows to maintain a reasonable file size\n\ttransparency or nodata are stored in an internal tiff mask because it's not possible to have an alpha channel when using jpg compression\n\t'''\n\n\n\tdef __del__(self):\n\t\t# properly close gdal dataset\n\t\tself.ds = None\n\n\n\tdef __init__(self, path, w, h, georef, geoTiffOptions={'TFW':'YES', 'TILED':'YES', 'BIGTIFF':'YES', 'COMPRESS':'JPEG', 'JPEG_QUALITY':80, 'PHOTOMETRIC':'YCBCR'}):\n\t\t'''\n\t\tpath = fule system path for the ouput tiff\n\t\tw, h = width and height in pixels\n\t\tgeoref : a Georef object used to set georeferencing informations, optional\n\t\tgeoTiffOptions : GDAL create option for tiff format\n\t\t'''\n\n\t\tif not HAS_GDAL:\n\t\t\traise ImportError(\"GDAL interface unavailable\")\n\n\n\t\t#control path validity\n\n\t\tself.w = w\n\t\tself.h = h\n\t\tself.size = (w, h)\n\n\t\tself.path = path\n\t\tself.georef = georef\n\n\t\tif geoTiffOptions.get('COMPRESS', None) == 'JPEG':\n\t\t\t#JPEG in tiff cannot have an alpha band, workaround is to use internal tiff mask\n\t\t\tself.useMask = True\n\t\t\tgdal.SetConfigOption('GDAL_TIFF_INTERNAL_MASK', 'YES')\n\t\t\tn = 3 #RGB\n\t\telse:\n\t\t\tself.useMask = False\n\t\t\tn = 4 #RGBA\n\t\tself.nbBands = n\n\n\t\toptions = [str(k) + '=' + str(v) for k, v in geoTiffOptions.items()]\n\n\t\tdriver = gdal.GetDriverByName(\"GTiff\")\n\t\tgdtype = gdal.GDT_Byte #GDT_UInt16, GDT_Int16, GDT_UInt32, GDT_Int32\n\t\tself.dtype = 'uint8'\n\n\t\tself.ds = driver.Create(path, w, h, n, gdtype, options)\n\t\tif self.useMask:\n\t\t\tself.ds.CreateMaskBand(gdal.GMF_PER_DATASET)#The mask band is shared between all bands on the dataset\n\t\t\tself.mask = self.ds.GetRasterBand(1).GetMaskBand()\n\t\t\tself.mask.Fill(255)\n\t\telif n == 4:\n\t\t\tself.ds.GetRasterBand(4).Fill(255)\n\n\t\t#Write georef infos\n\t\tself.ds.SetGeoTransform(self.georef.toGDAL())\n\t\tif self.georef.crs is not None:\n\t\t\tself.ds.SetProjection(self.georef.crs.getOgrSpatialRef().ExportToWkt())\n\t\t#self.georef.toWorldFile(os.path.splitext(path)[0] + '.tfw')\n\n\n\tdef paste(self, data, x, y):\n\t\t'''data = numpy array or NpImg'''\n\t\timg = NpImage(data)\n\t\tdata = img.data\n\t\t#Write RGB\n\t\tfor bandIdx in range(3): #writearray is available only at band level\n\t\t\tbandArray = data[:,:,bandIdx]\n\t\t\tself.ds.GetRasterBand(bandIdx+1).WriteArray(bandArray, x, y)\n\t\t#Process alpha\n\t\thasAlpha = data.shape[2] == 4\n\t\tif hasAlpha:\n\t\t\talpha = data[:,:,3]\n\t\t\tif self.useMask:\n\t\t\t\tself.mask.WriteArray(alpha, x, y)\n\t\t\telse:\n\t\t\t\tself.ds.GetRasterBand(4).WriteArray(alpha, x, y)\n\t\telse:\n\t\t\tpass # replaced by fill method\n\t\t\t'''\n\t\t\t#make alpha band or internal mask fully opaque\n\t\t\th, w = data.shape[0], data.shape[1]\n\t\t\talpha = np.full((h, w), 255, np.uint8)\n\t\t\tif self.useMask:\n\t\t\t\tself.mask.WriteArray(alpha, x, y)\n\t\t\telse:\n\t\t\t\tself.ds.GetRasterBand(4).WriteArray(alpha, x, y)\n\t\t\t'''\n\n\n\n\tdef __repr__(self):\n\t\treturn '\\n'.join([\n\t\t\"* Data infos :\",\n\t\t\" size {}\".format(self.size),\n\t\t\" type {}\".format(self.dtype),\n\t\t\" number of bands {}\".format(self.nbBands),\n\t\t\"* Georef & Geometry : \\n{}\".format(self.georef)\n\t\t])\n"
  },
  {
    "path": "core/georaster/georaster.py",
    "content": "# -*- coding:utf-8 -*-\r\n\r\n# This file is part of BlenderGIS\r\n\r\n#  ***** GPL LICENSE BLOCK *****\r\n#\r\n#  This program is free software: you can redistribute it and/or modify\r\n#  it under the terms of the GNU General Public License as published by\r\n#  the Free Software Foundation, either version 3 of the License, or\r\n#  (at your option) any later version.\r\n#\r\n#  This program is distributed in the hope that it will be useful,\r\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r\n#  GNU General Public License for more details.\r\n#\r\n#  You should have received a copy of the GNU General Public License\r\n#  along with this program.  If not, see <http://www.gnu.org/licenses/>.\r\n#  All rights reserved.\r\n#  ***** GPL LICENSE BLOCK *****\r\n\r\nimport os\r\n\r\nimport logging\r\nlog = logging.getLogger(__name__)\r\n\r\nfrom ..lib import Tyf #geotags reader\r\n\r\nfrom .georef import GeoRef\r\nfrom .npimg import NpImage\r\nfrom .img_utils import getImgFormat, getImgDim\r\n\r\nfrom ..utils import XY as xy\r\nfrom ..errors import OverlapError\r\nfrom ..checkdeps import HAS_GDAL\r\n\r\nif HAS_GDAL:\r\n\tfrom osgeo import gdal\r\n\r\n\r\nclass GeoRaster():\r\n\t'''A class to represent a georaster file'''\r\n\r\n\r\n\tdef __init__(self, path, subBoxGeo=None, useGDAL=False):\r\n\t\t'''\r\n\t\tsubBoxGeo : a BBOX object in CRS coordinate space\r\n\t\tuseGDAL : use GDAL (if available) for extract raster informations\r\n\t\t'''\r\n\t\tself.path = path\r\n\t\tself.wfPath = self._getWfPath()\r\n\r\n\t\tself.format = None #image file format (jpeg, tiff, png ...)\r\n\t\tself.size = None #raster dimension (width, height) in pixel\r\n\t\tself.depth = None #8, 16, 32\r\n\t\tself.dtype = None #int, uint, float\r\n\t\tself.nbBands = None #number of bands\r\n\t\tself.noData = None\r\n\r\n\t\tself.georef = None\r\n\r\n\t\tif not useGDAL or not HAS_GDAL:\r\n\r\n\t\t\tself.format = getImgFormat(path)\r\n\t\t\tif self.format not in ['TIFF', 'BMP', 'PNG', 'JPEG', 'JPEG2000']:\r\n\t\t\t\traise IOError(\"Unsupported format {}\".format(self.format))\r\n\r\n\t\t\tif self.isTiff:\r\n\t\t\t\tself._fromTIFF()\r\n\t\t\t\tif not self.isGeoref and self.hasWorldFile:\r\n\t\t\t\t\tself.georef = GeoRef.fromWorldFile(self.wfPath, self.size)\r\n\t\t\t\telse:\r\n\t\t\t\t\tpass\r\n\t\t\telse:\r\n\t\t\t\t# Try to read file header\r\n\t\t\t\tw, h = getImgDim(self.path)\r\n\t\t\t\tif w is None or h is None:\r\n\t\t\t\t\traise IOError(\"Unable to read raster size\")\r\n\t\t\t\telse:\r\n\t\t\t\t\tself.size = xy(w, h)\r\n\t\t\t\t#georef\r\n\t\t\t\tif self.hasWorldFile:\r\n\t\t\t\t\tself.georef = GeoRef.fromWorldFile(self.wfPath, self.size)\r\n\t\t\t\t#TODO add function to extract dtype, nBands & depth from jpg, png, bmp or jpeg2000\r\n\r\n\t\telse:\r\n\t\t\tself._fromGDAL()\r\n\r\n\t\tif not self.isGeoref:\r\n\t\t\traise IOError(\"Unable to read georef infos from worldfile or geotiff tags\")\r\n\r\n\t\tif subBoxGeo is not None:\r\n\t\t\tself.georef.setSubBoxGeo(subBoxGeo)\r\n\r\n\r\n\t#GeoGef delegation by composition instead of inheritance\r\n\t#this special method is called whenever the requested attribute or method is not found in the object\r\n\tdef __getattr__(self, attr):\r\n\t\treturn getattr(self.georef, attr)\r\n\r\n\r\n\t############################################\r\n\t# Initialization Helpers\r\n\t############################################\r\n\r\n\tdef _getWfPath(self):\r\n\t\t'''Try to find a worlfile path for this raster'''\r\n\t\text = self.path[-3:].lower()\r\n\t\textTest = []\r\n\t\textTest.append(ext[0] + ext[2] +'w')# tfx, jgw, pgw ...\r\n\t\textTest.append(extTest[0]+'x')# tfwx\r\n\t\textTest.append(ext+'w')# tifw\r\n\t\textTest.append('wld')#*.wld\r\n\t\textTest.extend( [ext.upper() for ext in extTest] )\r\n\t\tfor wfExt in extTest:\r\n\t\t\tpathTest = self.path[0:len(self.path)-3] + wfExt\r\n\t\t\tif os.path.isfile(pathTest):\r\n\t\t\t\treturn pathTest\r\n\t\treturn None\r\n\r\n\tdef _fromTIFF(self):\r\n\t\t'''Use Tyf to extract raster infos from geotiff tags'''\r\n\t\tif not self.isTiff or not self.fileExists:\r\n\t\t\treturn\r\n\t\ttif = Tyf.open(self.path)[0]\r\n\t\t#Warning : Tyf object does not support k in dict test syntax nor get() method, use try block instead\r\n\t\tself.size = xy(tif['ImageWidth'], tif['ImageLength'])\r\n\t\tself.nbBands = tif['SamplesPerPixel']\r\n\t\tself.depth = tif['BitsPerSample']\r\n\t\tif self.nbBands > 1:\r\n\t\t\tself.depth = self.depth[0]\r\n\t\tsampleFormatMap = {1:'uint', 2:'int', 3:'float', None:'uint', 6:'complex'}\r\n\t\ttry:\r\n\t\t\tself.dtype = sampleFormatMap[tif['SampleFormat']]\r\n\t\texcept KeyError:\r\n\t\t\tself.dtype = 'uint'\r\n\t\ttry:\r\n\t\t\tself.noData = float(tif['GDAL_NODATA'])\r\n\t\texcept KeyError:\r\n\t\t\tself.noData = None\r\n\t\t#Get Georef\r\n\t\ttry:\r\n\t\t\tself.georef = GeoRef.fromTyf(tif)\r\n\t\texcept Exception as e:\r\n\t\t\tlog.warning('Cannot extract georefencing informations from tif tags')#, exc_info=True)\r\n\t\t\tpass\r\n\r\n\r\n\tdef _fromGDAL(self):\r\n\t\t'''Use GDAL to extract raster infos and init'''\r\n\t\tif self.path is None or not self.fileExists:\r\n\t\t\traise IOError(\"Cannot find file on disk\")\r\n\t\tds = gdal.Open(self.path, gdal.GA_ReadOnly)\r\n\t\tself.size = xy(ds.RasterXSize, ds.RasterYSize)\r\n\t\tself.format = ds.GetDriver().ShortName\r\n\t\tif self.format in ['JP2OpenJPEG', 'JP2ECW', 'JP2KAK', 'JP2MrSID'] :\r\n\t\t\tself.format = 'JPEG2000'\r\n\t\tself.nbBands = ds.RasterCount\r\n\t\tb1 = ds.GetRasterBand(1) #first band (band index does not count from 0)\r\n\t\tself.noData = b1.GetNoDataValue()\r\n\t\tddtype = gdal.GetDataTypeName(b1.DataType)#Byte, UInt16, Int16, UInt32, Int32, Float32, Float64\r\n\t\tif ddtype == \"Byte\":\r\n\t\t\tself.dtype = 'uint'\r\n\t\t\tself.depth = 8\r\n\t\telse:\r\n\t\t\tself.dtype = ddtype[0:len(ddtype)-2].lower()\r\n\t\t\tself.depth = int(ddtype[-2:])\r\n\t\t#Get Georef\r\n\t\tself.georef = GeoRef.fromGDAL(ds)\r\n\t\t#Close (gdal has no garbage collector)\r\n\t\tds, b1 = None, None\r\n\r\n\t#######################################\r\n\t# Dynamic properties\r\n\t#######################################\r\n\t@property\r\n\tdef fileExists(self):\r\n\t\t'''Test if the file exists on disk'''\r\n\t\treturn os.path.isfile(self.path)\r\n\t@property\r\n\tdef baseName(self):\r\n\t\tif self.path is not None:\r\n\t\t\tfolder, fileName = os.path.split(self.path)\r\n\t\t\tbaseName, ext = os.path.splitext(fileName)\r\n\t\t\treturn baseName\r\n\t@property\r\n\tdef isTiff(self):\r\n\t\t'''Flag if the image format is TIFF'''\r\n\t\tif self.format in ['TIFF', 'GTiff']:\r\n\t\t\treturn True\r\n\t\telse:\r\n\t\t\treturn False\r\n\t@property\r\n\tdef hasWorldFile(self):\r\n\t\treturn self.wfPath is not None\r\n\t@property\r\n\tdef isGeoref(self):\r\n\t\t'''Flag if georef parameters have been extracted'''\r\n\t\tif self.georef is not None:\r\n\t\t\tif self.origin is not None and self.pxSize is not None and self.rotation is not None:\r\n\t\t\t\treturn True\r\n\t\t\telse:\r\n\t\t\t\treturn False\r\n\t\telse:\r\n\t\t\treturn False\r\n\t@property\r\n\tdef isOneBand(self):\r\n\t\treturn self.nbBands == 1\r\n\t@property\r\n\tdef isFloat(self):\r\n\t\treturn self.dtype in ['Float', 'float']\r\n\t@property\r\n\tdef ddtype(self):\r\n\t\t'''\r\n\t\tGet data type and depth in a concatenate string like\r\n\t\t'int8', 'int16', 'uint16', 'int32', 'uint32', 'float32' ...\r\n\t\tCan be used to define numpy or gdal data type\r\n\t\t'''\r\n\t\tif self.dtype is None or self.depth is None:\r\n\t\t\treturn None\r\n\t\telse:\r\n\t\t\treturn self.dtype + str(self.depth)\r\n\r\n\r\n\tdef __repr__(self):\r\n\t\treturn '\\n'.join([\r\n\t\t'* Paths infos :',\r\n\t\t' path {}'.format(self.path),\r\n\t\t' worldfile {}'.format(self.wfPath),\r\n\t\t' format {}'.format(self.format),\r\n\t\t\"* Data infos :\",\r\n\t\t\" size {}\".format(self.size),\r\n\t\t\" bit depth {}\".format(self.depth),\r\n\t\t\" data type {}\".format(self.dtype),\r\n\t\t\" number of bands {}\".format(self.nbBands),\r\n\t\t\" nodata value {}\".format(self.noData),\r\n\t\t\"* Georef & Geometry : \\n{}\".format(self.georef)\r\n\t\t])\r\n\r\n\t#######################################\r\n\t# Methods\r\n\t#######################################\r\n\r\n\tdef toGDAL(self):\r\n\t\t'''Get GDAL dataset'''\r\n\t\treturn gdal.Open(self.path, gdal.GA_ReadOnly)\r\n\r\n\tdef readAsNpArray(self, subset=True):\r\n\t\t'''Read raster pixels values as Numpy Array'''\r\n\r\n\t\tif subset and self.subBoxGeo is not None:\r\n\t\t\t#georef = GeoRef(self.size, self.pxSize, self.subBoxGeoOrigin, rot=self.rotation, pxCenter=True)\r\n\t\t\timg = NpImage(self.path, subBoxPx=self.subBoxPx, noData=self.noData, georef=self.georef, adjustGeoref=True)\r\n\t\telse:\r\n\t\t\timg = NpImage(self.path, noData=self.noData, georef=self.georef)\r\n\t\treturn img\r\n"
  },
  {
    "path": "core/georaster/georef.py",
    "content": "# -*- coding:utf-8 -*-\r\n\r\n# This file is part of BlenderGIS\r\n\r\n#  ***** GPL LICENSE BLOCK *****\r\n#\r\n#  This program is free software: you can redistribute it and/or modify\r\n#  it under the terms of the GNU General Public License as published by\r\n#  the Free Software Foundation, either version 3 of the License, or\r\n#  (at your option) any later version.\r\n#\r\n#  This program is distributed in the hope that it will be useful,\r\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r\n#  GNU General Public License for more details.\r\n#\r\n#  You should have received a copy of the GNU General Public License\r\n#  along with this program.  If not, see <http://www.gnu.org/licenses/>.\r\n#  All rights reserved.\r\n#  ***** GPL LICENSE BLOCK *****\r\n\r\nimport math\r\n\r\nfrom ..proj import SRS\r\nfrom ..utils import XY as xy, BBOX\r\nfrom ..errors import OverlapError\r\n\r\n\r\nclass GeoRef():\r\n\t'''\r\n\tRepresents georefencing informations of a raster image\r\n\tNote : image origin is upper-left whereas map origin is lower-left\r\n\t'''\r\n\r\n\tdef __init__(self, rSize, pxSize, origin, rot=xy(0,0), pxCenter=True, subBoxGeo=None, crs=None):\r\n\t\t'''\r\n\t\trSize : dimensions of the raster in pixels (width, height) tuple\r\n\t\tpxSize : dimension of a pixel in map units (x scale, y scale) tuple. y is always negative\r\n\t\torigin : upper left geo coords of pixel center, (x, y) tuple\r\n\t\tpxCenter : set it to True is the origin anchor point is located at pixel center\r\n\t\t\tor False if it's lolcated at pixel corner\r\n\t\trotation : rotation terms (xrot, yrot) <--> (yskew, xskew)\r\n\t\tsubBoxGeo : a BBOX object that define the working extent (subdataset) in geographic coordinate space\r\n\t\t'''\r\n\t\tself.rSize = xy(*rSize)\r\n\t\tself.origin = xy(*origin)\r\n\t\tself.pxSize = xy(*pxSize)\r\n\t\tif not pxCenter:\r\n\t\t\t#adjust topleft coord to pixel center\r\n\t\t\tself.origin[0] += abs(self.pxSize.x/2)\r\n\t\t\tself.origin[1] -= abs(self.pxSize.y/2)\r\n\t\tself.rotation = xy(*rot)\r\n\t\tif subBoxGeo is not None:\r\n\t\t\t# Define a subbox at init is optionnal, we can also do it later\r\n\t\t\t# Setting the subBox will check if the box overlap the raster extent\r\n\t\t\tself.setSubBoxGeo(subBoxGeo)\r\n\t\telse:\r\n\t\t\tself.subBoxGeo = None\r\n\t\tif crs is not None:\r\n\t\t\tif isinstance(crs, SRS):\r\n\t\t\t\tself.crs = crs\r\n\t\t\telse:\r\n\t\t\t\traise IOError(\"CRS must be SRS() class object not \" + str(type(crs)))\r\n\t\telse:\r\n\t\t\tself.crs = crs\r\n\r\n\t############################################\r\n\t# Alternative constructors\r\n\t############################################\r\n\r\n\t@classmethod\r\n\tdef fromGDAL(cls, ds):\r\n\t\t'''init from gdal dataset instance'''\r\n\t\tgeoTrans = ds.GetGeoTransform()\r\n\t\tif geoTrans is not None:\r\n\t\t\txmin, resx, rotx, ymax, roty, resy = geoTrans\r\n\t\t\tw, h = ds.RasterXSize, ds.RasterYSize\r\n\t\t\ttry:\r\n\t\t\t\tcrs = SRS.fromGDAL(ds)\r\n\t\t\texcept Exception as e:\r\n\t\t\t\tcrs = None\r\n\t\t\treturn cls((w, h), (resx, resy), (xmin, ymax), rot=(rotx, roty), pxCenter=False, crs=crs)\r\n\t\telse:\r\n\t\t\treturn None\r\n\r\n\t@classmethod\r\n\tdef fromWorldFile(cls, wfPath, rasterSize):\r\n\t\t'''init from a worldfile'''\r\n\t\ttry:\r\n\t\t\twith open(wfPath,'r') as f:\r\n\t\t\t\twf = f.readlines()\r\n\t\t\tpxSize = xy(float(wf[0].replace(',','.')), float(wf[3].replace(',','.')))\r\n\t\t\trotation = xy(float(wf[1].replace(',','.')), float(wf[2].replace(',','.')))\r\n\t\t\torigin = xy(float(wf[4].replace(',','.')), float(wf[5].replace(',','.')))\r\n\t\t\treturn cls(rasterSize, pxSize, origin, rot=rotation, pxCenter=True, crs=None)\r\n\t\texcept Exception as e:\r\n\t\t\traise IOError(\"Unable to read worldfile. {}\".format(e))\r\n\r\n\t@classmethod\r\n\tdef fromTyf(cls, tif):\r\n\t\t'''read geotags from Tyf instance'''\r\n\t\t#Warning : Tyf object does not support k in dict test syntax nor get() method, use try block instead\r\n\t\tw, h = tif['ImageWidth'], tif['ImageLength']\r\n\r\n\t\t#Search for a transformation matrix\r\n\t\ttry:\r\n\t\t\t#34264: (\"ModelTransformation\", \"a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p\")\r\n\t\t\t# 4x4 transform matrix in 3D space\r\n\t\t\ttransfoMatrix = tif['ModelTransformationTag']\r\n\t\texcept KeyError:\r\n\t\t\ttransfoMatrix = None\r\n\r\n\t\t#Search for upper left coord and pixel scales\r\n\t\ttry:\r\n\t\t\t#33922: (\"ModelTiepoint\", \"I,J,K,X,Y,Z\")\r\n\t\t\tmodelTiePoint = tif['ModelTiepointTag']\r\n\t\t\t#33550 (\"ModelPixelScale\", \"ScaleX, ScaleY, ScaleZ\")\r\n\t\t\tmodelPixelScale = tif['ModelPixelScaleTag']\r\n\t\texcept KeyError:\r\n\t\t\tmodelTiePoint = None\r\n\t\t\tmodelPixelScale = None\r\n\r\n\t\tif transfoMatrix is not None:\r\n\t\t\ta,b,c,d, \\\r\n\t\t\te,f,g,h, \\\r\n\t\t\ti,j,k,l, \\\r\n\t\t\tm,n,o,p = transfoMatrix\r\n\t\t\t#get only 2d affine parameters\r\n\t\t\torigin = xy(d, h)\r\n\t\t\tpxSize = xy(a, f)\r\n\t\t\trotation = xy(e, b)\r\n\t\telif modelTiePoint is not None and modelPixelScale is not None:\r\n\t\t\torigin = xy(*modelTiePoint[3:5])\r\n\t\t\tpxSize = xy(*modelPixelScale[0:2])\r\n\t\t\tpxSize[1] = -pxSize.y #make negative value\r\n\t\t\trotation = xy(0, 0)\r\n\t\telse:\r\n\t\t\traise IOError(\"Unable to read geotags\")\r\n\r\n\t\t#Define anchor point for top left coord\r\n\t\t#\thttp://www.remotesensing.org/geotiff/spec/geotiff2.5.html#2.5.2\r\n\t\t#\thttp://www.remotesensing.org/geotiff/spec/geotiff6.html#6.3.1.2\r\n\t\t# >> 1 = area (cell anchor = top left corner)\r\n\t\t# >> 2 = point (cell anchor = center)\r\n\t\t#geotags = Tyf.gkd.Gkd(tif)\r\n\t\t#cellAnchor = geotags['GTRasterTypeGeoKey']\r\n\t\ttry:\r\n\t\t\tgeotags = tif['GeoKeyDirectoryTag']\r\n\t\texcept KeyError:\r\n\t\t\tcellAnchor = 1 #if this key is missing then RasterPixelIsArea is the default\r\n\t\telse:\r\n\t\t\t#get GTRasterTypeGeoKey value\r\n\t\t\tcellAnchor = geotags[geotags.index(1025)+3] #http://www.remotesensing.org/geotiff/spec/geotiff2.4.html\r\n\t\tif cellAnchor == 1:\r\n\t\t\t#adjust topleft coord to pixel center\r\n\t\t\torigin[0] += abs(pxSize.x/2)\r\n\t\t\torigin[1] -= abs(pxSize.y/2)\r\n\r\n\t\t#TODO extract crs (transcript geokeys to proj4 string)\r\n\t\t\r\n\t\treturn cls((w, h), pxSize, origin, rot=rotation, pxCenter=True, crs=None)\r\n\r\n\t############################################\r\n\t# Export\r\n\t############################################\r\n\r\n\tdef toGDAL(self):\r\n\t\t'''return a tuple of georef parameters ordered to define geotransformation of a gdal datasource'''\r\n\t\txmin, ymax = self.corners[0]\r\n\t\txres, yres = self.pxSize\r\n\t\txrot, yrot = self.rotation\r\n\t\treturn (xmin, xres, xrot, ymax, yrot, yres)\r\n\r\n\tdef toWorldFile(self, path):\r\n\t\t'''export geo transformation to a worldfile'''\r\n\t\txmin, ymax = self.origin\r\n\t\txres, yres = self.pxSize\r\n\t\txrot, yrot = self.rotation\r\n\t\twf = (xres, xrot, yrot, yres, xmin, ymax)\r\n\t\tf = open(path,'w')\r\n\t\tf.write( '\\n'.join(list(map(str, wf))) )\r\n\t\tf.close()\r\n\r\n\t############################################\r\n\t# Dynamic properties\r\n\t############################################\r\n\r\n\t@property\r\n\tdef hasCRS(self):\r\n\t\treturn self.crs is not None\r\n\r\n\t@property\r\n\tdef hasRotation(self):\r\n\t\treturn self.rotation.x != 0 or self.rotation.y != 0\r\n\r\n\t#TODO\r\n\t#def getCorners(self, center=True):\r\n\t#def getUL(self, center=True)\r\n\r\n\t\"\"\"\r\n\t@property\r\n\tdef ul(self):\r\n\t\t'''upper left corner'''\r\n\t\treturn self.geoFromPx(0, yPxRange, True)\r\n\t@property\r\n\tdef ur(self):\r\n\t\t'''upper right corner'''\r\n\t\treturn self.geoFromPx(xPxRange, yPxRange, True)\r\n\t@property\r\n\tdef bl(self):\r\n\t\t'''bottom left corner'''\r\n\t\treturn self.geoFromPx(0, 0, True)\r\n\t@property\r\n\tdef br(self):\r\n\t\t'''bottom right corner'''\r\n\t\treturn self.geoFromPx(xPxRange, 0, True)\r\n\t\"\"\"\r\n\r\n\t@property\r\n\tdef cornersCenter(self):\r\n\t\t'''\r\n\t\t(x,y) geo coordinates of image corners (upper left, upper right, bottom right, bottom left)\r\n\t\t(pt1, pt2, pt3, pt4) <--> (upper left, upper right, bottom right, bottom left)\r\n\t\tThe coords are located at the pixel center\r\n\t\t'''\r\n\t\txPxRange = self.rSize.x - 1\r\n\t\tyPxRange = self.rSize.y - 1\r\n\t\t#pixel center\r\n\t\tpt1 = self.geoFromPx(0, 0, pxCenter=True)#upperLeft\r\n\t\tpt2 = self.geoFromPx(xPxRange, 0, pxCenter=True)#upperRight\r\n\t\tpt3 = self.geoFromPx(xPxRange, yPxRange, pxCenter=True)#bottomRight\r\n\t\tpt4 = self.geoFromPx(0, yPxRange, pxCenter=True)#bottomLeft\r\n\t\treturn (pt1, pt2, pt3, pt4)\r\n\r\n\t@property\r\n\tdef corners(self):\r\n\t\t'''\r\n\t\t(x,y) geo coordinates of image corners (upper left, upper right, bottom right, bottom left)\r\n\t\t(pt1, pt2, pt3, pt4) <--> (upper left, upper right, bottom right, bottom left)\r\n\t\tRepresent the true corner location (upper left for pt1, upper right for pt2 ...)\r\n\t\t'''\r\n\t\t#get corners at center\r\n\t\tpt1, pt2, pt3, pt4 = self.cornersCenter\r\n\t\t#pixel center offset\r\n\t\txOffset = abs(self.pxSize.x/2)\r\n\t\tyOffset = abs(self.pxSize.y/2)\r\n\t\tpt1 = xy(pt1.x - xOffset, pt1.y + yOffset)\r\n\t\tpt2 = xy(pt2.x + xOffset, pt2.y + yOffset)\r\n\t\tpt3 = xy(pt3.x + xOffset, pt3.y - yOffset)\r\n\t\tpt4 = xy(pt4.x - xOffset, pt4.y - yOffset)\r\n\t\treturn (pt1, pt2, pt3, pt4)\r\n\r\n\t@property\r\n\tdef bbox(self):\r\n\t\t'''Return a bbox class object'''\r\n\t\tpts = self.corners\r\n\t\txmin = min([pt.x for pt in pts])\r\n\t\txmax = max([pt.x for pt in pts])\r\n\t\tymin = min([pt.y for pt in pts])\r\n\t\tymax = max([pt.y for pt in pts])\r\n\t\treturn BBOX(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax)\r\n\r\n\t@property\r\n\tdef bboxPx(self):\r\n\t\treturn BBOX(xmin=0, ymin=0, xmax=self.rSize.x, ymax=self.rSize.y)\r\n\r\n\t@property\r\n\tdef center(self):\r\n\t\t'''(x,y) geo coordinates of image center'''\r\n\t\treturn xy(self.corners[0].x + self.geoSize.x/2, self.corners[0].y - self.geoSize.y/2)\r\n\r\n\t@property\r\n\tdef geoSize(self):\r\n\t\t'''raster dimensions (width, height) in map units'''\r\n\t\treturn xy(self.rSize.x * abs(self.pxSize.x), self.rSize.y * abs(self.pxSize.y))\r\n\r\n\t@property\r\n\tdef orthoGeoSize(self):\r\n\t\t'''ortho geo size when affine transfo applied a rotation'''\r\n\t\tpxWidth = math.sqrt(self.pxSize.x**2 + self.rotation.x**2)\r\n\t\tpxHeight = math.sqrt(self.pxSize.y**2 + self.rotation.y**2)\r\n\t\treturn xy(self.rSize.x*pxWidth, self.rSize.y*pxHeight)\r\n\r\n\t@property\r\n\tdef orthoPxSize(self):\r\n\t\t'''ortho pixels size when affine transfo applied a rotation'''\r\n\t\tpxWidth = math.sqrt(self.pxSize.x**2 + self.rotation.x**2)\r\n\t\tpxHeight = math.sqrt(self.pxSize.y**2 + self.rotation.y**2)\r\n\t\treturn xy(pxWidth, pxHeight)\r\n\r\n\r\n\tdef geoFromPx(self, xPx, yPx, reverseY=False, pxCenter=True):\r\n\t\t\"\"\"\r\n\t\tAffine transformation (cf. ESRI WorldFile spec.)\r\n\t\tReturn geo coords of the center of an given pixel\r\n\t\txPx = the column number of the pixel in the image counting from left\r\n\t\tyPx = the row number of the pixel in the image counting from top\r\n\t\tuse reverseY option is yPx is counting from bottom instead of top\r\n\t\tNumber of pixels is range from 0 (not 1)\r\n\t\t\"\"\"\r\n\r\n\t\tif pxCenter:\r\n\t\t\t#force pixel center, in this case we need to cast the inputs to floor integer\r\n\t\t\txPx, yPx = math.floor(xPx), math.floor(yPx)\r\n\t\t\tox, oy = self.origin.x, self.origin.y\r\n\t\telse:\r\n\t\t\t#normal behaviour, coord at pixel's top left corner\r\n\t\t\tox = self.origin.x - abs(self.pxSize.x/2)\r\n\t\t\toy = self.origin.y + abs(self.pxSize.y/2)\r\n\r\n\t\tif reverseY:#the users given y pixel in the image counting from bottom\r\n\t\t\tyPxRange = self.rSize.y - 1\r\n\t\t\tyPx = yPxRange - yPx\r\n\r\n\t\tx = self.pxSize.x * xPx + self.rotation.y * yPx + ox\r\n\t\ty = self.pxSize.y * yPx + self.rotation.x * xPx + oy\r\n\r\n\t\treturn xy(x, y)\r\n\r\n\r\n\tdef pxFromGeo(self, x, y, reverseY=False, round2Floor=False):\r\n\t\t\"\"\"\r\n\t\tAffine transformation (cf. ESRI WorldFile spec.)\r\n\t\tReturn pixel position of given geographic coords\r\n\t\tuse reverseY option to get y pixels counting from bottom\r\n\t\tPixels position is range from 0 (not 1)\r\n\t\t\"\"\"\r\n\t\t# aliases for more readability\r\n\t\tpxSizex, pxSizey = self.pxSize\r\n\t\trotx, roty = self.rotation\r\n\t\toffx = self.origin.x - abs(self.pxSize.x/2)\r\n\t\toffy = self.origin.y + abs(self.pxSize.y/2)\r\n\t\t# transfo\r\n\t\txPx  = (pxSizey*x - rotx*y + rotx*offy - pxSizey*offx) / (pxSizex*pxSizey - rotx*roty)\r\n\t\tyPx = (-roty*x + pxSizex*y + roty*offx - pxSizex*offy) / (pxSizex*pxSizey - rotx*roty)\r\n\t\tif reverseY:#the users want y pixel position counting from bottom\r\n\t\t\tyPxRange = self.rSize.y - 1\r\n\t\t\tyPx = yPxRange - yPx\r\n\t\t\tyPx += 1 #adjust because the coord start at pixel's top left coord\r\n\t\t#round to floor\r\n\t\tif round2Floor:\r\n\t\t\txPx, yPx = math.floor(xPx), math.floor(yPx)\r\n\t\treturn xy(xPx, yPx)\r\n\r\n\t#Alias\r\n\tdef pxToGeo(self, xPx, yPx, reverseY=False):\r\n\t\treturn self.geoFromPx(xPx, yPx, reverseY)\r\n\tdef geoToPx(self, x, y, reverseY=False, round2Floor=False):\r\n\t\treturn self.pxFromGeo(x, y, reverseY, round2Floor)\r\n\r\n\t############################################\r\n\t# Subbox handlers\r\n\t############################################\r\n\r\n\tdef setSubBoxGeo(self, subBoxGeo):\r\n\t\t'''set a subbox in geographic coordinate space\r\n\t\tif needed, coords will be adjusted to avoid being outside raster size'''\r\n\t\tif self.hasRotation:\r\n\t\t\traise IOError(\"A subbox cannot be define if the raster has rotation parameter\")\r\n\t\t#Before set the property, ensure that the desired subbox overlap the raster extent\r\n\t\tif not self.bbox.overlap(subBoxGeo):\r\n\t\t\traise OverlapError()\r\n\t\telif self.bbox.isWithin(subBoxGeo):\r\n\t\t\t#Ignore because subbox is greater than raster extent\r\n\t\t\treturn\r\n\t\telse:\r\n\t\t\t#convert the subbox in pixel coordinate space\r\n\t\t\txminPx, ymaxPx = self.pxFromGeo(subBoxGeo.xmin, subBoxGeo.ymin, round2Floor=True)#y pixels counting from top\r\n\t\t\txmaxPx, yminPx = self.pxFromGeo(subBoxGeo.xmax, subBoxGeo.ymax, round2Floor=True)#idem\r\n\t\t\tsubBoxPx = BBOX(xmin=xminPx, ymin=yminPx, xmax=xmaxPx, ymax=ymaxPx)#xmax and ymax include\r\n\t\t\t#set the subbox\r\n\t\t\tself.setSubBoxPx(subBoxPx)\r\n\r\n\r\n\tdef setSubBoxPx(self, subBoxPx):\r\n\t\tif not self.bboxPx.overlap(subBoxPx):\r\n\t\t\traise OverlapError()\r\n\t\txminPx, xmaxPx = subBoxPx.xmin, subBoxPx.xmax\r\n\t\tyminPx, ymaxPx = subBoxPx.ymin, subBoxPx.ymax\r\n\t\t#adjust against raster size if needed\r\n\t\t#we count pixel number from 0 but size represents total number of pixel (counting from 1), so we must use size-1\r\n\t\tsizex, sizey = self.rSize\r\n\t\tif xminPx < 0: xminPx = 0\r\n\t\tif xmaxPx >= sizex: xmaxPx = sizex - 1\r\n\t\tif yminPx < 0: yminPx = 0\r\n\t\tif ymaxPx >= sizey: ymaxPx = sizey - 1\r\n\t\t#get the adjusted geo coords at pixels center\r\n\t\txmin, ymin = self.geoFromPx(xminPx, ymaxPx)\r\n\t\txmax, ymax = self.geoFromPx(xmaxPx, yminPx)\r\n\t\t#set the subbox\r\n\t\tself.subBoxGeo = BBOX(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax)\r\n\r\n\r\n\tdef applySubBox(self):\r\n\t\tif self.subBoxGeo is not None:\r\n\t\t\tself.rSize = self.subBoxPxSize\r\n\t\t\tself.origin = self.subBoxGeoOrigin\r\n\t\t\tself.subBoxGeo = None\r\n\r\n\tdef getSubBoxGeoRef(self):\r\n\t\treturn GeoRef(self.subBoxPxSize, self.pxSize, self.subBoxGeoOrigin, pxCenter=True, crs=self.crs)\r\n\r\n\t@property\r\n\tdef subBoxPx(self):\r\n\t\t'''return the subbox as bbox object in pixels coordinates space'''\r\n\t\tif self.subBoxGeo is None:\r\n\t\t\treturn None\r\n\t\txmin, ymax = self.pxFromGeo(self.subBoxGeo.xmin, self.subBoxGeo.ymin, round2Floor=True)#y pixels counting from top\r\n\t\txmax, ymin = self.pxFromGeo(self.subBoxGeo.xmax, self.subBoxGeo.ymax, round2Floor=True)\r\n\t\treturn BBOX(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax)#xmax and ymax include\r\n\r\n\t@property\r\n\tdef subBoxPxSize(self):\r\n\t\t'''dimension of the subbox in pixels'''\r\n\t\tif self.subBoxGeo is None:\r\n\t\t\treturn None\r\n\t\tbbpx = self.subBoxPx\r\n\t\tw, h = bbpx.xmax - bbpx.xmin, bbpx.ymax - bbpx.ymin\r\n\t\treturn xy(w+1, h+1)\r\n\r\n\t@property\r\n\tdef subBoxGeoSize(self):\r\n\t\t'''dimension of the subbox in map units'''\r\n\t\tif self.subBoxGeo is None:\r\n\t\t\treturn None\r\n\t\tsizex, sizey = self.subBoxPxSize\r\n\t\treturn xy(sizex * abs(self.pxSize.x), sizey * abs(self.pxSize.y))\r\n\r\n\t@property\r\n\tdef subBoxPxOrigin(self):\r\n\t\t'''pixel coordinate of subbox origin'''\r\n\t\tif self.subBoxGeo is None:\r\n\t\t\treturn None\r\n\t\treturn xy(self.subBoxPx.xmin, self.subBoxPx.ymin)\r\n\r\n\t@property\r\n\tdef subBoxGeoOrigin(self):\r\n\t\t'''geo coordinate of subbox origin, adjusted at pixel center'''\r\n\t\tif self.subBoxGeo is None:\r\n\t\t\treturn None\r\n\t\treturn xy(self.subBoxGeo.xmin, self.subBoxGeo.ymax)\r\n\r\n\t####\r\n\r\n\tdef __repr__(self):\r\n\t\ts = [\r\n\t\t' spatial ref system {}'.format(self.crs),\r\n\t\t' origin geo {}'.format(self.origin),\r\n\t\t' pixel size {}'.format(self.pxSize),\r\n\t\t' rotation {}'.format(self.rotation),\r\n\t\t' bounding box {}'.format(self.bbox),\r\n\t\t' geoSize {}'.format(self.geoSize)\r\n\t\t]\r\n\r\n\t\tif self.subBoxGeo is not None:\r\n\t\t\ts.extend([\r\n\t\t\t' subbox origin (geo space) {}'.format(self.subBoxGeoOrigin),\r\n\t\t\t' subbox origin (px space) {}'.format(self.subBoxPxOrigin),\r\n\t\t\t' subbox (geo space) {}'.format(self.subBoxGeo),\r\n\t\t\t' subbox (px space) {}'.format(self.subBoxPx),\r\n\t\t\t' sub geoSize {}'.format(self.subBoxGeoSize),\r\n\t\t\t' sub pxSize {}'.format(self.subBoxPxSize),\r\n\t\t\t])\r\n\r\n\t\treturn '\\n'.join(s)\r\n"
  },
  {
    "path": "core/georaster/img_utils.py",
    "content": "# -*- coding:utf-8 -*-\r\n\r\n# This file is part of BlenderGIS\r\n\r\n#  ***** GPL LICENSE BLOCK *****\r\n#\r\n#  This program is free software: you can redistribute it and/or modify\r\n#  it under the terms of the GNU General Public License as published by\r\n#  the Free Software Foundation, either version 3 of the License, or\r\n#  (at your option) any later version.\r\n#\r\n#  This program is distributed in the hope that it will be useful,\r\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r\n#  GNU General Public License for more details.\r\n#\r\n#  You should have received a copy of the GNU General Public License\r\n#  along with this program.  If not, see <http://www.gnu.org/licenses/>.\r\n#  All rights reserved.\r\n#  ***** GPL LICENSE BLOCK *****\r\n\r\n\r\nimport struct\r\nfrom ..lib import imghdr\r\n\r\n\r\ndef isValidStream(data):\r\n\tif data is None:\r\n\t\treturn False\r\n\tformat = imghdr.what(None, data)\r\n\tif format is None:\r\n\t\treturn False\r\n\treturn True\r\n\r\n\r\ndef getImgFormat(filepath):\r\n\t\"\"\"\r\n\tRead header of an image file and try to determine it's format\r\n\tno requirements, support JPEG, JPEG2000, PNG, GIF, BMP, TIFF, EXR\r\n\t\"\"\"\r\n\tformat = None\r\n\twith open(filepath, 'rb') as fhandle:\r\n\t\thead = fhandle.read(32)\r\n\t\t# handle GIFs\r\n\t\tif head[:6] in (b'GIF87a', b'GIF89a'):\r\n\t\t\tformat = 'GIF'\r\n\t\t# handle PNG\r\n\t\telif head.startswith(b'\\211PNG\\r\\n\\032\\n'):\r\n\t\t\tformat = 'PNG'\r\n\t\t# handle JPEGs\r\n\t\t#elif head[6:10] in (b'JFIF', b'Exif')\r\n\t\telif (b'JFIF' in head or b'Exif' in head or b'8BIM' in head) or head.startswith(b'\\xff\\xd8'):\r\n\t\t\tformat = 'JPEG'\r\n\t\t# handle JPEG2000s\r\n\t\telif head.startswith(b'\\x00\\x00\\x00\\x0cjP  \\r\\n\\x87\\n'):\r\n\t\t\tformat = 'JPEG2000'\r\n\t\t# handle BMP\r\n\t\telif head.startswith(b'BM'):\r\n\t\t\tformat = 'BMP'\r\n\t\t# handle TIFF\r\n\t\telif head[:2] in (b'MM', b'II'):\r\n\t\t\tformat = 'TIFF'\r\n\t\t# handle EXR\r\n\t\telif head.startswith(b'\\x76\\x2f\\x31\\x01'):\r\n\t\t\tformat = 'EXR'\r\n\treturn format\r\n\r\n\r\n\r\ndef getImgDim(filepath):\r\n\t\"\"\"\r\n\tReturn (width, height) for a given img file content\r\n\tno requirements, support JPEG, JPEG2000, PNG, GIF, BMP\r\n\t\"\"\"\r\n\twidth, height = None, None\r\n\r\n\twith open(filepath, 'rb') as fhandle:\r\n\t\thead = fhandle.read(32)\r\n\t\t# handle GIFs\r\n\t\tif head[:6] in (b'GIF87a', b'GIF89a'):\r\n\t\t\ttry:\r\n\t\t\t\twidth, height = struct.unpack(\"<hh\", head[6:10])\r\n\t\t\texcept struct.error:\r\n\t\t\t\traise ValueError(\"Invalid GIF file\")\r\n\t\t# handle PNG\r\n\t\telif head.startswith(b'\\211PNG\\r\\n\\032\\n'):\r\n\t\t\ttry:\r\n\t\t\t\twidth, height = struct.unpack(\">LL\", head[16:24])\r\n\t\t\texcept struct.error:\r\n\t\t\t\t# Maybe this is for an older PNG version.\r\n\t\t\t\ttry:\r\n\t\t\t\t\twidth, height = struct.unpack(\">LL\", head[8:16])\r\n\t\t\t\texcept struct.error:\r\n\t\t\t\t\traise ValueError(\"Invalid PNG file\")\r\n\t\t# handle JPEGs\r\n\t\telif (b'JFIF' in head or b'Exif' in head or b'8BIM' in head) or head.startswith(b'\\xff\\xd8'):\r\n\t\t\ttry:\r\n\t\t\t\tfhandle.seek(0) # Read 0xff next\r\n\t\t\t\tsize = 2\r\n\t\t\t\tftype = 0\r\n\t\t\t\twhile not 0xc0 <= ftype <= 0xcf:\r\n\t\t\t\t\tfhandle.seek(size, 1)\r\n\t\t\t\t\tbyte = fhandle.read(1)\r\n\t\t\t\t\twhile ord(byte) == 0xff:\r\n\t\t\t\t\t\tbyte = fhandle.read(1)\r\n\t\t\t\t\tftype = ord(byte)\r\n\t\t\t\t\tsize = struct.unpack('>H', fhandle.read(2))[0] - 2\r\n\t\t\t\t# We are at a SOFn block\r\n\t\t\t\tfhandle.seek(1, 1)  # Skip `precision' byte.\r\n\t\t\t\theight, width = struct.unpack('>HH', fhandle.read(4))\r\n\t\t\texcept struct.error:\r\n\t\t\t\traise ValueError(\"Invalid JPEG file\")\r\n\t\t# handle JPEG2000s\r\n\t\telif head.startswith(b'\\x00\\x00\\x00\\x0cjP  \\r\\n\\x87\\n'):\r\n\t\t\tfhandle.seek(48)\r\n\t\t\ttry:\r\n\t\t\t\theight, width = struct.unpack('>LL', fhandle.read(8))\r\n\t\t\texcept struct.error:\r\n\t\t\t\traise ValueError(\"Invalid JPEG2000 file\")\r\n\t\t# handle BMP\r\n\t\telif head.startswith(b'BM'):\r\n\t\t\timgtype = 'BMP'\r\n\t\t\ttry:\r\n\t\t\t\twidth, height = struct.unpack(\"<LL\", head[18:26])\r\n\t\t\texcept struct.error:\r\n\t\t\t\traise ValueError(\"Invalid BMP file\")\r\n\r\n\treturn width, height\r\n"
  },
  {
    "path": "core/georaster/npimg.py",
    "content": "# -*- coding:utf-8 -*-\r\n\r\n# This file is part of BlenderGIS\r\n\r\n#  ***** GPL LICENSE BLOCK *****\r\n#\r\n#  This program is free software: you can redistribute it and/or modify\r\n#  it under the terms of the GNU General Public License as published by\r\n#  the Free Software Foundation, either version 3 of the License, or\r\n#  (at your option) any later version.\r\n#\r\n#  This program is distributed in the hope that it will be useful,\r\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r\n#  GNU General Public License for more details.\r\n#\r\n#  You should have received a copy of the GNU General Public License\r\n#  along with this program.  If not, see <http://www.gnu.org/licenses/>.\r\n#  All rights reserved.\r\n#  ***** GPL LICENSE BLOCK *****\r\n\r\nimport os\r\nimport io\r\nimport random\r\n\r\nimport numpy as np\r\n\r\nfrom .georef import GeoRef\r\nfrom ..proj.reproj import reprojImg\r\nfrom ..maths.fillnodata import replace_nans #inpainting function (ie fill nodata)\r\nfrom ..utils import XY as xy\r\nfrom ..checkdeps import HAS_GDAL, HAS_PIL, HAS_IMGIO\r\nfrom .. import settings\r\n\r\nif HAS_PIL:\r\n\tfrom PIL import Image\r\n\r\nif HAS_GDAL:\r\n\tfrom osgeo import gdal\r\n\r\nif HAS_IMGIO:\r\n\tfrom ..lib import imageio\r\n\r\n\r\nclass NpImage():\r\n\t'''Represent an image as Numpy array'''\r\n\r\n\tdef _getIFACE(self):\r\n\r\n\t\tengine = settings.img_engine\r\n\r\n\t\tif engine == 'AUTO':\r\n\t\t\tif HAS_GDAL:\r\n\t\t\t\treturn 'GDAL'\r\n\t\t\telif HAS_IMGIO:\r\n\t\t\t\treturn 'IMGIO'\r\n\t\t\telif HAS_PIL:\r\n\t\t\t\treturn 'PIL'\r\n\t\t\telse:\r\n\t\t\t\traise ImportError(\"No image engine available\")\r\n\t\telif engine == 'GDAL'and HAS_GDAL:\r\n\t\t\treturn 'GDAL'\r\n\t\telif engine == 'IMGIO' and HAS_IMGIO:\r\n\t\t\treturn 'IMGIO'\r\n\t\telif engine == 'PIL'and HAS_PIL:\r\n\t\t\treturn 'PIL'\r\n\t\telse:\r\n\t\t\traise ImportError(str(engine) + \" interface unavailable\")\r\n\r\n\t#GeoGef delegation by composition instead of inheritance\r\n\t#this special method is called whenever the requested attribute or method is not found in the object\r\n\tdef __getattr__(self, attr):\r\n\t\tif self.isGeoref:\r\n\t\t\treturn getattr(self.georef, attr)\r\n\t\telse:#TODO raise specific msg if request for a georef attribute and not self.isgeoref\r\n\t\t\traise AttributeError(str(type(self)) + 'object has no attribute' + str(attr))\r\n\r\n\r\n\tdef __init__(self, data, subBoxPx=None, noData=None, georef=None, adjustGeoref=False):\r\n\t\t'''\r\n\t\tinit from file path, bytes data, Numpy array, NpImage, PIL Image or GDAL dataset\r\n\t\tsubBoxPx : a BBOX object in pixel coordinates space used as data filter (will by applyed) (y counting from top)\r\n\t\tnoData : the value used to represent nodata, will be used to define a numpy mask\r\n\t\tgeoref : a Georef object used to set georeferencing informations, optional\r\n\t\tadjustGeoref: determine if the submited georef must be adjusted against the subbox or if its already correct\r\n\r\n\t\tNotes :\r\n\t\t* With GDAL the subbox filter can be applyed at reading level whereas with others imaging\r\n\t\tlibrary, all the data must be extracted before we can extract the subset (using numpy slice).\r\n\t\tIn this case, the dataset must fit entirely in memory otherwise it will raise an overflow error\r\n\t\t* If no georef was submited and when the class is init using gdal support or from another npImage instance,\r\n\t\texisting georef of input data will be automatically extracted and adjusted against the subbox\r\n\t\t'''\r\n\t\tself.IFACE = self._getIFACE()\r\n\r\n\t\tself.data = None\r\n\t\tself.subBoxPx = subBoxPx\r\n\t\tself.noData = noData\r\n\r\n\t\tself.georef = georef\r\n\t\tif self.subBoxPx is not None and self.georef is not None:\r\n\t\t\tif adjustGeoref:\r\n\t\t\t\tself.georef.setSubBoxPx(subBoxPx)\r\n\t\t\t\tself.georef.applySubBox()\r\n\r\n\t\t#init from another NpImage instance\r\n\t\tif isinstance(data, NpImage):\r\n\t\t\tself.data = self._applySubBox(data.data)\r\n\t\t\tif data.isGeoref and not self.isGeoref:\r\n\t\t\t\tself.georef = data.georef\r\n\t\t\t\t#adjust georef against subbox\r\n\t\t\t\tif self.subBoxPx is not None:\r\n\t\t\t\t\tself.georef.setSubBoxPx(subBoxPx)\r\n\t\t\t\t\tself.georef.applySubBox()\r\n\r\n\t\t#init from numpy array\r\n\t\tif isinstance(data, np.ndarray):\r\n\t\t\tself.data = self._applySubBox(data)\r\n\r\n\t\t#init from bytes data (BLOB)\r\n\t\tif isinstance(data, bytes):\r\n\t\t\tself.data = self._npFromBLOB(data)\r\n\r\n\t\t#init from file path\r\n\t\tif isinstance(data, str):\r\n\t\t\tif os.path.exists(data):\r\n\t\t\t\tself.data = self._npFromPath(data)\r\n\t\t\telse:\r\n\t\t\t\traise ValueError('Unable to load image data')\r\n\r\n\t\t#init from GDAL dataset instance\r\n\t\tif HAS_GDAL:\r\n\t\t\tif isinstance(data, gdal.Dataset):\r\n\t\t\t\tself.data = self._npFromGDAL(data)\r\n\r\n\t\t#init from PIL Image instance\r\n\t\tif HAS_PIL:\r\n\t\t\tif isinstance(data, Image.Image):\r\n\t\t\t\tself.data = self._npFromPIL(data)\r\n\r\n\t\tif self.data is None:\r\n\t\t\traise ValueError('Unable to load image data')\r\n\r\n\t\t#Mask nodata value to avoid bias when computing min or max statistics\r\n\t\tif self.noData is not None:\r\n\t\t\tself.data = np.ma.masked_array(self.data, self.data == self.noData)\r\n\r\n\t@property\r\n\tdef size(self):\r\n\t\treturn xy(self.data.shape[1], self.data.shape[0])\r\n\r\n\t@property\r\n\tdef isGeoref(self):\r\n\t\t'''Flag if georef parameters have been extracted'''\r\n\t\tif self.georef is not None:\r\n\t\t\treturn True\r\n\t\telse:\r\n\t\t\treturn False\r\n\r\n\t@property\r\n\tdef nbBands(self):\r\n\t\tif len(self.data.shape) == 2:\r\n\t\t\treturn 1\r\n\t\telif len(self.data.shape) == 3:\r\n\t\t\treturn self.data.shape[2]\r\n\r\n\t@property\r\n\tdef hasAlpha(self):\r\n\t\treturn self.nbBands == 4\r\n\r\n\t@property\r\n\tdef isOneBand(self):\r\n\t\treturn self.nbBands == 1\r\n\r\n\t@property\r\n\tdef dtype(self):\r\n\t\t'''return string ['int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32', 'float32', 'float64']'''\r\n\t\treturn self.data.dtype\r\n\r\n\t@property\r\n\tdef isFloat(self):\r\n\t\tif self.dtype in ['float16', 'float32', 'float64']:\r\n\t\t\treturn True\r\n\t\telse:\r\n\t\t\treturn False\r\n\r\n\tdef getMin(self, bandIdx=0):\r\n\t\tif self.nbBands == 1:\r\n\t\t\treturn self.data.min()\r\n\t\telse:\r\n\t\t\treturn self.data[:,:,bandIdx].min()\r\n\r\n\tdef getMax(self, bandIdx=0):\r\n\t\tif self.nbBands == 1:\r\n\t\t\treturn self.data.max()\r\n\t\telse:\r\n\t\t\treturn self.data[:,:,bandIdx].max()\r\n\r\n\t@classmethod\r\n\tdef new(cls, w, h, bkgColor=(255,255,255,255), noData=None, georef=None):\r\n\t\tr, g, b, a = bkgColor\r\n\t\tdata = np.empty((h, w, 4), np.uint8)\r\n\t\tdata[:,:,0] = r\r\n\t\tdata[:,:,1] = g\r\n\t\tdata[:,:,2] = b\r\n\t\tdata[:,:,3] = a\r\n\t\treturn cls(data, noData=noData, georef=georef)\r\n\r\n\tdef _applySubBox(self, data):\r\n\t\t'''Use numpy slice to extract subset of data'''\r\n\t\tif self.subBoxPx is not None:\r\n\t\t\tx1, x2 = self.subBoxPx.xmin, self.subBoxPx.xmax+1\r\n\t\t\ty1, y2 = self.subBoxPx.ymin, self.subBoxPx.ymax+1\r\n\t\t\tif len(data.shape) == 2: #one band\r\n\t\t\t\tdata = data[y1:y2, x1:x2]\r\n\t\t\telse:\r\n\t\t\t\tdata = data[y1:y2, x1:x2, :]\r\n\t\t\tself.subBoxPx = None\r\n\t\treturn data\r\n\r\n\tdef _npFromPath(self, path):\r\n\t\t'''Get Numpy array from a file path'''\r\n\t\tif self.IFACE == 'PIL':\r\n\t\t\timg = Image.open(path)\r\n\t\t\treturn self._npFromPIL(img)\r\n\t\telif self.IFACE == 'IMGIO':\r\n\t\t\treturn self._npFromImgIO(path)\r\n\t\telif self.IFACE == 'GDAL':\r\n\t\t\tds = gdal.Open(path)\r\n\t\t\treturn self._npFromGDAL(ds)\r\n\r\n\tdef _npFromBLOB(self, data):\r\n\t\t'''Get Numpy array from Bytes data'''\r\n\r\n\t\tif self.IFACE == 'PIL':\r\n\t\t\t#convert bytes object to bytesio (stream buffer) and open it with PIL\r\n\t\t\timg = Image.open(io.BytesIO(data))\r\n\t\t\tdata = self._npFromPIL(img)\r\n\r\n\t\telif self.IFACE == 'IMGIO':\r\n\t\t\timg = io.BytesIO(data)\r\n\t\t\tdata = self._npFromImgIO(img)\r\n\r\n\t\telif self.IFACE == 'GDAL':\r\n\t\t\t#Use a virtual memory file to create gdal dataset from buffer\r\n\t\t\t#build a random name to make the function thread safe\r\n\t\t\tvsipath = '/vsimem/' + ''.join(random.choice('abcdefghijklmnopqrstuvwxyz') for i in range(5))\r\n\t\t\tgdal.FileFromMemBuffer(vsipath, data)\r\n\t\t\tds = gdal.Open(vsipath)\r\n\t\t\tdata = self._npFromGDAL(ds)\r\n\t\t\tds = None\r\n\t\t\tgdal.Unlink(vsipath)\r\n\r\n\t\treturn data\r\n\r\n\tdef _npFromImgIO(self, img):\r\n\t\t'''Use ImageIO to extract numpy array from image path or bytesIO'''\r\n\t\tdata = imageio.imread(img)\r\n\t\treturn self._applySubBox(data)\r\n\r\n\tdef _npFromPIL(self, img):\r\n\t\t'''Get Numpy array from PIL Image instance'''\r\n\t\tif img.mode == 'P': #palette (indexed color)\r\n\t\t\timg = img.convert('RGBA')\r\n\t\tdata = np.asarray(img)\r\n\t\t#data.setflags(write=True) #PIL return a non writable array\r\n\t\treturn self._applySubBox(data)\r\n\r\n\tdef _npFromGDAL(self, ds):\r\n\t\t'''Get Numpy array from GDAL dataset instance'''\r\n\t\tif self.subBoxPx is not None:\r\n\t\t\tstartx, starty = self.subBoxPx.xmin, self.subBoxPx.ymin\r\n\t\t\twidth = (self.subBoxPx.xmax - self.subBoxPx.xmin) + 1\r\n\t\t\theight = (self.subBoxPx.ymax - self.subBoxPx.ymin) + 1\r\n\t\t\tdata = ds.ReadAsArray(startx, starty, width, height)\r\n\t\telse:\r\n\t\t\tdata = ds.ReadAsArray()\r\n\t\tif len(data.shape) == 3: #multiband\r\n\t\t\tdata = np.rollaxis(data, 0, 3) # because first axis is band index\r\n\t\telse: #one band raster or indexed color (= palette = pseudo color table (pct))\r\n\t\t\tctable = ds.GetRasterBand(1).GetColorTable()\r\n\t\t\tif ctable is not None:\r\n\t\t\t\t#Swap index values to their corresponding color (rgba)\r\n\t\t\t\tnbColors = ctable.GetCount()\r\n\t\t\t\tkeys = np.array( [i for i in range(nbColors)] )\r\n\t\t\t\tvalues = np.array( [ctable.GetColorEntry(i) for i in range(nbColors)] )\r\n\t\t\t\tsortIdx = np.argsort(keys)\r\n\t\t\t\tidx = np.searchsorted(keys, data, sorter=sortIdx)\r\n\t\t\t\tdata = values[sortIdx][idx]\r\n\r\n\t\t#Try to extract georef\r\n\t\tif not self.isGeoref:\r\n\t\t\tself.georef = GeoRef.fromGDAL(ds)\r\n\t\t\t#adjust georef against subbox\r\n\t\t\tif self.subBoxPx is not None and self.georef is not None:\r\n\t\t\t\tself.georef.applySubBox()\r\n\r\n\t\treturn data\r\n\r\n\r\n\r\n\tdef toBLOB(self, ext='PNG'):\r\n\t\t'''Get bytes raw data'''\r\n\t\tif ext == 'JPG':\r\n\t\t\text = 'JPEG'\r\n\r\n\t\tif self.IFACE == 'PIL':\r\n\t\t\tb = io.BytesIO()\r\n\t\t\timg = Image.fromarray(self.data)\r\n\t\t\timg.save(b, format=ext)\r\n\t\t\tdata = b.getvalue() #convert bytesio to bytes\r\n\r\n\t\telif self.IFACE == 'IMGIO':\r\n\t\t\tif ext == 'JPEG' and self.hasAlpha:\r\n\t\t\t\tself.removeAlpha()\r\n\t\t\tdata = imageio.imwrite(imageio.RETURN_BYTES, self.data, format=ext)\r\n\r\n\t\telif self.IFACE == 'GDAL':\r\n\t\t\tmem = self.toGDAL()\r\n\t\t\t#build a random name to make the function thread safe\r\n\t\t\tname = ''.join(random.choice('abcdefghijklmnopqrstuvwxyz') for i in range(5))\r\n\t\t\tvsiname = '/vsimem/' + name + '.png'\r\n\t\t\tout = gdal.GetDriverByName(ext).CreateCopy(vsiname, mem)\r\n\t\t\t# Read /vsimem/output.png\r\n\t\t\tf = gdal.VSIFOpenL(vsiname, 'rb')\r\n\t\t\tgdal.VSIFSeekL(f, 0, 2) # seek to end\r\n\t\t\tsize = gdal.VSIFTellL(f)\r\n\t\t\tgdal.VSIFSeekL(f, 0, 0) # seek to beginning\r\n\t\t\tdata = gdal.VSIFReadL(1, size, f)\r\n\t\t\tgdal.VSIFCloseL(f)\r\n\t\t\t# Cleanup\r\n\t\t\tgdal.Unlink(vsiname)\r\n\t\t\tmem = None\r\n\r\n\t\treturn data\r\n\r\n\r\n\r\n\tdef toPIL(self):\r\n\t\t'''Get PIL Image instance'''\r\n\t\treturn Image.fromarray(self.data)\r\n\r\n\r\n\tdef toGDAL(self):\r\n\t\t'''Get GDAL memory driver dataset'''\r\n\t\tw, h = self.size\r\n\t\tn = self.nbBands\r\n\t\tdtype = str(self.dtype)\r\n\t\tif dtype == 'uint8': dtype = 'byte'\r\n\t\tdtype = gdal.GetDataTypeByName(dtype)\r\n\t\tmem = gdal.GetDriverByName('MEM').Create('', w, h, n, dtype)\r\n\t\t#writearray is available only at band level\r\n\t\tif self.isOneBand:\r\n\t\t\tmem.GetRasterBand(1).WriteArray(self.data)\r\n\t\telse:\r\n\t\t\tfor bandIdx in range(n):\r\n\t\t\t\tbandArray = self.data[:,:,bandIdx]\r\n\t\t\t\tmem.GetRasterBand(bandIdx+1).WriteArray(bandArray)\r\n\t\t#write georef\r\n\t\tif self.isGeoref:\r\n\t\t\tmem.SetGeoTransform(self.georef.toGDAL())\r\n\t\t\tif self.georef.crs is not None:\r\n\t\t\t\tmem.SetProjection(self.georef.crs.getOgrSpatialRef().ExportToWkt())\r\n\t\treturn mem\r\n\r\n\r\n\tdef removeAlpha(self):\r\n\t\tif self.hasAlpha:\r\n\t\t\tself.data = self.data[:, :, 0:3]\r\n\r\n\tdef addAlpha(self, opacity=255):\r\n\t\tif self.nbBands == 3:\r\n\t\t\tw, h = self.size\r\n\t\t\talpha = np.empty((h,w), dtype=self.dtype)\r\n\t\t\talpha.fill(opacity)\r\n\t\t\talpha = np.expand_dims(alpha, axis=2)\r\n\t\t\tself.data = np.append(self.data, alpha, axis=2)\r\n\r\n\r\n\tdef save(self, path):\r\n\t\t'''\r\n\t\tsave the numpy array to a new image file\r\n\t\toutput format is defined by path extension\r\n\t\t'''\r\n\r\n\t\timgFormat = path[-3:]\r\n\r\n\t\tif self.IFACE == 'PIL':\r\n\t\t\tself.toPIL().save(path)\r\n\t\telif self.IFACE == 'IMGIO':\r\n\t\t\tif imgFormat == 'jpg' and self.hasAlpha:\r\n\t\t\t\tself.removeAlpha()\r\n\t\t\timageio.imwrite(path, self.data)#float32 support ok\r\n\t\telif self.IFACE == 'GDAL':\r\n\t\t\tif imgFormat == 'png':\r\n\t\t\t\tdriver = 'PNG'\r\n\t\t\telif imgFormat == 'jpg':\r\n\t\t\t\tdriver = 'JPEG'\r\n\t\t\telif imgFormat == 'tif':\r\n\t\t\t\tdriver = 'Gtiff'\r\n\t\t\telse:\r\n\t\t\t\traise ValueError('Cannot write to '+ imgFormat + ' image format')\r\n\t\t\t#Some format like jpg or png has no create method implemented\r\n\t\t\t#because we can't write data at random with these formats\r\n\t\t\t#so we must use an intermediate memory driver, write data to it\r\n\t\t\t#and then write the output file with the createcopy method\r\n\t\t\tmem = self.toGDAL()\r\n\t\t\tout = gdal.GetDriverByName(driver).CreateCopy(path, mem)\r\n\t\t\tmem = out = None\r\n\r\n\t\tif self.isGeoref:\r\n\t\t\tself.georef.toWorldFile(os.path.splitext(path)[0] + '.wld')\r\n\r\n\r\n\tdef paste(self, data, x, y):\r\n\r\n\t\timg = NpImage(data)\r\n\t\tdata = img.data\r\n\t\tw, h = img.size\r\n\r\n\t\tif img.isOneBand and self.isOneBand:\r\n\t\t\tself.data[y:y+h, x:x+w] = data\r\n\t\telif (not img.isOneBand and self.isOneBand) or (img.isOneBand and not self.isOneBand):\r\n\t\t\traise ValueError('Paste error, cannot mix one band with multiband')\r\n\r\n\t\tif self.hasAlpha:\r\n\t\t\tn = img.nbBands\r\n\t\t\tself.data[y:y+h, x:x+w, 0:n] = data\r\n\t\telse:\r\n\t\t\tn = self.nbBands\r\n\t\t\tself.data[y:y+h, x:x+w, :] = data[:, :, 0:n]\r\n\r\n\tdef cast2float(self):\r\n\t\tif not self.isFloat:\r\n\t\t\tself.data = self.data.astype('float32')\r\n\r\n\tdef fillNodata(self):\r\n\t\t#if not self.noData in self.data:\r\n\t\tif not np.ma.is_masked(self.data):\r\n\t\t\t#do not process it if its not necessary\r\n\t\t\treturn\r\n\t\tif self.IFACE == 'GDAL':\r\n\t\t\t# gdal.FillNodata need a band object to apply on\r\n\t\t\t# so we create a memory datasource (1 band, float)\r\n\t\t\theight, width = self.data.shape\r\n\t\t\tds = gdal.GetDriverByName('MEM').Create('', width, height, 1, gdal.GetDataTypeByName('float32'))\r\n\t\t\tb = ds.GetRasterBand(1)\r\n\t\t\tb.SetNoDataValue(self.noData)\r\n\t\t\tself.data =  np.ma.filled(self.data, self.noData)# Fill mask with nodata value\r\n\t\t\tb.WriteArray(self.data)\r\n\t\t\tgdal.FillNodata(targetBand=b, maskBand=None, maxSearchDist=max(self.size.xy), smoothingIterations=0)\r\n\t\t\tself.data = b.ReadAsArray()\r\n\t\t\tds, b = None, None\r\n\t\telse: #Call the inpainting function\r\n\t\t\t# Cast to float\r\n\t\t\tself.cast2float()\r\n\t\t\t# Fill mask with NaN (warning NaN is a special value for float arrays only)\r\n\t\t\tself.data =  np.ma.filled(self.data, np.NaN)\r\n\t\t\t# Inpainting\r\n\t\t\tself.data = replace_nans(self.data, max_iter=5, tolerance=0.5, kernel_size=2, method='localmean')\r\n\r\n\tdef reproj(self, crs1, crs2, out_ul=None, out_size=None, out_res=None, sqPx=False, resamplAlg='BL'):\r\n\t\tds1 = self.toGDAL()\r\n\t\tif not self.isGeoref:\r\n\t\t\traise IOError('Unable to reproject non georeferenced image')\r\n\t\tds2 = reprojImg(crs1, crs2, ds1, out_ul=out_ul, out_size=out_size, out_res=out_res, sqPx=sqPx, resamplAlg=resamplAlg)\r\n\t\treturn NpImage(ds2)\r\n\r\n\tdef __repr__(self):\r\n\t\treturn '\\n'.join([\r\n\t\t\"* Data infos :\",\r\n\t\t\" size {}\".format(self.size),\r\n\t\t\" type {}\".format(self.dtype),\r\n\t\t\" number of bands {}\".format(self.nbBands),\r\n\t\t\" nodata value {}\".format(self.noData),\r\n\t\t\"* Statistics : min {} max {}\".format(self.getMin(), self.getMax()),\r\n\t\t\"* Georef & Geometry : \\n{}\".format(self.georef)\r\n\t\t])\r\n"
  },
  {
    "path": "core/lib/Tyf/VERSION",
    "content": "1.2.5"
  },
  {
    "path": "core/lib/Tyf/__init__.py",
    "content": "# -*- encoding:utf-8 -*-\n__copyright__ = \"Copyright © 2012-2015, THOORENS Bruno - http://bruno.thoorens.free.fr/licences/tyf.html\"\n__author__    = \"THOORENS Bruno\"\n__tiff__      = (6, 0)\n__geotiff__   = (1, 8, 1)\n\nimport io, os, sys, struct, operator, collections\n\n__PY3__ = True if sys.version_info[0] >= 3 else False\n\nunpack = lambda fmt, fileobj: struct.unpack(fmt, fileobj.read(struct.calcsize(fmt)))\npack = lambda fmt, fileobj, value: fileobj.write(struct.pack(fmt, *value))\n\nTYPES = {\n\t1:  (\"B\",  \"UCHAR or USHORT\"),\n\t2:  (\"c\",  \"ASCII\"),\n\t3:  (\"H\",  \"UBYTE\"),\n\t4:  (\"L\",  \"ULONG\"),\n\t5:  (\"LL\", \"URATIONAL\"),\n\t6:  (\"b\",  \"CHAR or SHORT\"),\n\t7:  (\"c\",  \"UNDEFINED\"),\n\t8:  (\"h\",  \"BYTE\"),\n\t9:  (\"l\",  \"LONG\"),\n\t10: (\"ll\", \"RATIONAL\"),\n\t11: (\"f\",  \"FLOAT\"),\n\t12: (\"d\",  \"DOUBLE\"),\n}\n\n# assure compatibility python 2 & 3\nif __PY3__:\n\tfrom io import BytesIO as StringIO\n\tTYPES[2] = (\"s\", \"ASCII\")\n\tTYPES[7] = (\"s\", \"UDEFINED\")\n\timport functools\n\treduce = functools.reduce\n\tlong = int\n\timport urllib.request as urllib\nelse:\n\tfrom StringIO import StringIO\n\timport urllib\n\treduce = __builtins__[\"reduce\"]\n\nfrom . import ifd, gkd, tags\n\n\ndef _read_IFD(obj, fileobj, offset, byteorder=\"<\"):\n\t# fileobj seek must be on the start offset\n\tfileobj.seek(offset)\n\t# get number of entry\n\tnb_entry, = unpack(byteorder+\"H\", fileobj)\n\n\t# for each entry\n\tfor i in range(nb_entry):\n\t\t# read tag, type and count values\n\t\ttag, typ, count = unpack(byteorder+\"HHL\", fileobj)\n\t\t# extract data\n\t\tdata = fileobj.read(struct.calcsize(\"=L\"))\n\t\tif not isinstance(data, bytes):\n\t\t\tdata = data.encode()\n\t\t_typ = TYPES[typ][0]\n\n\t\t# create a tifftag\n\t\ttt = ifd.TiffTag(tag, typ, name=obj.tagname)\n\t\t# initialize what we already know\n\t\t# tt.type = typ\n\t\ttt.count = count\n\t\t# to know if ifd entry value is an offset\n\t\ttt._determine_if_offset()\n\n\t\t# if value is offset\n\t\tif tt.value_is_offset:\n\t\t\t# read offset value\n\t\t\tvalue, = struct.unpack(byteorder+\"L\", data)\n\t\t\tfmt = byteorder + _typ*count\n\t\t\tbckp = fileobj.tell()\n\t\t\t# go to offset in the file\n\t\t\tfileobj.seek(value)\n\t\t\t# if ascii type, convert to bytes\n\t\t\tif typ == 2: tt.value = b\"\".join(e for e in unpack(fmt, fileobj))\n\t\t\t# else if undefined type, read data\n\t\t\telif typ == 7: tt.value = fileobj.read(count)\n\t\t\t# else unpack data\n\t\t\telse: tt.value = unpack(fmt, fileobj)\n\t\t\t# go back to ifd entry\n\t\t\tfileobj.seek(bckp)\n\n\t\t# if value is in the ifd entry\n\t\telse:\n\t\t\tif typ in [2, 7]:\n\t\t\t\ttt.value = data[:count]\n\t\t\telse:\n\t\t\t\tfmt = byteorder + _typ*count\n\t\t\t\ttt.value = struct.unpack(fmt, data[:count*struct.calcsize(\"=\"+_typ)])\n\n\t\tobj.addtag(tt)\n\ndef from_buffer(obj, fileobj, offset, byteorder=\"<\", custom_sub_ifd={}):\n\t# read data from offset\n\t_read_IFD(obj, fileobj, offset, byteorder)\n\t# get next ifd offset\n\tnext_ifd, = unpack(byteorder+\"L\", fileobj)\n\n\t# finding by default those SubIFD\n\tsub_ifd = {34665:\"Exif tag\", 34853:\"GPS tag\", 40965:\"Interoperability tag\"}\n\t# adding other SubIFD if asked\n\tsub_ifd.update(custom_sub_ifd)\n\t## read registered SubIFD\n\tfor key,value in sub_ifd.items():\n\t\tif key in obj:\n\t\t\tobj.sub_ifd[key] = ifd.Ifd(tagname=value)\n\t\t\t_read_IFD(obj.sub_ifd[key], fileobj, obj[key], byteorder)\n\n\treturn next_ifd\n\n# for speed reason : load raster only if asked or if needed\ndef _load_raster(obj, fileobj):\n\t# striped raster data\n\tif 273 in obj:\n\t\tfor offset,bytecount in zip(obj.get(273).value, obj.get(279).value):\n\t\t\tfileobj.seek(offset)\n\t\t\tobj.stripes += (fileobj.read(bytecount), )\n\t# free raster data\n\telif 288 in obj:\n\t\tfor offset,bytecount in zip(obj.get(288).value, obj.get(289).value):\n\t\t\tfileobj.seek(offset)\n\t\t\tobj.free += (fileobj.read(bytecount), )\n\t# tiled raster data\n\telif 324 in obj:\n\t\tfor offset,bytecount in zip(obj.get(324).value, obj.get(325).value):\n\t\t\tfileobj.seek(offset)\n\t\t\tobj.tiles += (fileobj.read(bytecount), )\n\t# get interExchange (thumbnail data for JPEG/EXIF data)\n\tif 513 in obj:\n\t\tfileobj.seek(obj[513])\n\t\tobj.jpegIF = fileobj.read(obj[514])\n\ndef _write_IFD(obj, fileobj, offset, byteorder=\"<\"):\n\t# go where obj have to be written\n\tfileobj.seek(offset)\n\t# sort data to be writen\n\ttags = sorted(list(dict.values(obj)), key=lambda e:e.tag)\n\t# write number of entries\n\tpack(byteorder+\"H\", fileobj, (len(tags),))\n\n\tfirst_entry_offset = fileobj.tell()\n\t# write all ifd entries\n\tfor t in tags:\n\t\t# write tag, type & count\n\t\tpack(byteorder+\"HHL\", fileobj, (t.tag, t.type, t.count))\n\n\t\t# if value is not an offset\n\t\tif not t.value_is_offset:\n\t\t\tvalue = t._fill()\n\t\t\tn = len(value)\n\t\t\tif __PY3__ and t.type in [2, 7]:\n\t\t\t\tfmt = str(n)+TYPES[t.type][0]\n\t\t\t\tvalue = (value,)\n\t\t\telse:\n\t\t\t\tfmt = n*TYPES[t.type][0]\n\t\t\tpack(byteorder+fmt, fileobj, value)\n\t\telse:\n\t\t\tpack(byteorder+\"L\", fileobj, (0,))\n\n\tnext_ifd_offset = fileobj.tell()\n\tpack(byteorder+\"L\", fileobj, (0,))\n\n\t# prepare jumps\n\tdata_offset = fileobj.tell()\n\tstep1 = struct.calcsize(\"=HHLL\")\n\tstep2 = struct.calcsize(\"=HHL\")\n\n\t# comme back to first ifd entry\n\tfileobj.seek(first_entry_offset)\n\tfor t in tags:\n\t\t# for each tag witch value needs offset\n\t\tif t.value_is_offset:\n\t\t\t# go to offset value location (jump over tag, type, count)\n\t\t\tfileobj.seek(step2, 1)\n\t\t\t# write offset where value is about to be stored\n\t\t\tpack(byteorder+\"L\", fileobj, (data_offset,))\n\t\t\t# remember where i am in ifd entries\n\t\t\tbckp = fileobj.tell()\n\t\t\t# go to offset where value is about to be stored\n\t\t\tfileobj.seek(data_offset)\n\t\t\t# prepare value according to python version\n\t\t\tif __PY3__ and t.type in [2, 7]:\n\t\t\t\tfmt = str(t.count)+TYPES[t.type][0]\n\t\t\t\tvalue = (t.value,)\n\t\t\telse:\n\t\t\t\tfmt = t.count*TYPES[t.type][0]\n\t\t\t\tvalue = t.value\n\t\t\t# write value\n\t\t\t# print(\">>>\", fmt, value)\n\t\t\tpack(byteorder+fmt, fileobj, value)\n\t\t\t# remmember where to put next value\n\t\t\tdata_offset = fileobj.tell()\n\t\t\t# go to where I was in ifd entries\n\t\t\tfileobj.seek(bckp)\n\t\telse:\n\t\t\tfileobj.seek(step1, 1)\n\n\treturn next_ifd_offset\n\ndef to_buffer(obj, fileobj, offset, byteorder=\"<\"):\n\tobj._check()\n\n\tsize = obj.size\n\traw_offset = offset + size[\"ifd\"] + size[\"data\"]\n\t# add SubIFD sizes...\n\tfor tag, p_ifd in sorted(obj.sub_ifd.items(), key=lambda e:e[0]):\n\t\tobj.set(tag, 4, raw_offset)\n\t\tsize = p_ifd.size\n\t\traw_offset = raw_offset + size[\"ifd\"] + size[\"data\"]\n\n\t# knowing where raw image have to be writen, update [Strip/Free/Tile]Offsets\n\tif 273 in obj:\n\t\t_279 = obj.get(279).value\n\t\tstripoffsets = (raw_offset,)\n\t\tfor bytecount in _279[:-1]:\n\t\t\tstripoffsets += (stripoffsets[-1]+bytecount, )\n\t\tobj.set(273, 4, stripoffsets)\n\t\tnext_ifd = stripoffsets[-1] + _279[-1]\n\telif 288 in obj:\n\t\t_289 = obj.get(289).value\n\t\tfreeoffsets = (raw_offset,)\n\t\tfor bytecount in _289[:-1]:\n\t\t\tfreeoffsets += (freeoffsets[-1]+bytecount, )\n\t\tobj.set(288, 4, freeoffsets)\n\t\tnext_ifd = freeoffsets[-1] + _289[-1]\n\telif 324 in obj:\n\t\t_325 = obj.get(325).value\n\t\ttileoffsets = (raw_offset,)\n\t\tfor bytecount in _325[:-1]:\n\t\t\ttileoffsets += (tileoffsets[-1]+bytecount, )\n\t\tobj.set(324, 4, tileoffsets)\n\t\tnext_ifd = tileoffsets[-1] + _325[-1]\n\telif 513 in obj:\n\t\tinterexchangeoffset = raw_offset\n\t\tobj.set(513, 4, raw_offset)\n\t\tnext_ifd = interexchangeoffset + obj[514]\n\telse:\n\t\tnext_ifd = raw_offset\n\n\t# write IFD\n\tnext_ifd_offset = _write_IFD(obj, fileobj, offset, byteorder)\n\t# write SubIFD\n\tfor tag, p_ifd in sorted(obj.sub_ifd.items(), key=lambda e:e[0]):\n\t\t_write_IFD(p_ifd, fileobj, obj[tag], byteorder)\n\n\t# write raster data\n\tif len(obj.stripes):\n\t\tfor offset,data in zip(stripoffsets, obj.stripes):\n\t\t\tfileobj.seek(offset)\n\t\t\tfileobj.write(data)\n\telif len(obj.free):\n\t\tfor offset,data in zip(freeoffsets, obj.stripes):\n\t\t\tfileobj.seek(offset)\n\t\t\tfileobj.write(data)\n\telif len(obj.tiles):\n\t\tfor offset,data in zip(tileoffsets, obj.tiles):\n\t\t\tfileobj.seek(offset)\n\t\t\tfileobj.write(data)\n\telif obj.jpegIF != b\"\":\n\t\tfileobj.seek(interexchangeoffset)\n\t\tfileobj.write(obj.jpegIF)\n\n\tfileobj.seek(next_ifd_offset)\n\treturn next_ifd\n\n\ndef _fileobj(f, mode):\n\tif hasattr(f, \"close\"):\n\t\tfileobj = f\n\t\t_close = False\n\telse:\n\t\tfileobj = io.open(f, mode)\n\t\t_close = True\n\n\treturn fileobj, _close\n\n\nclass TiffFile(list):\n\n\tgkd = property(lambda obj: [gkd.Gkd(ifd) for ifd in obj], None, None, \"list of geotiff directory\")\n\thas_raster = property(lambda obj: reduce(operator.__or__, [ifd.has_raster for ifd in obj]), None, None, \"\")\n\traster_loaded = property(lambda obj: reduce(operator.__and__, [ifd.raster_loaded for ifd in obj]), None, None, \"\")\n\n\tdef __init__(self, fileobj):\n\t\t# Initialize a TiffFile object from buffer fileobj, fileobj have to be in 'wb' mode\n\n\t\t# determine byteorder\n\t\tfirst, = unpack(\">H\", fileobj)\n\t\tbyteorder = \"<\" if first == 0x4949 else \">\"\n\n\t\tmagic_number, = unpack(byteorder+\"H\", fileobj)\n\t\tif magic_number not in [0x732E,0x2A]: #29486, 42\n\t\t\tfileobj.close()\n\t\t\tif magic_number == 0x2B: # 43\n\t\t\t\traise IOError(\"BigTIFF file not supported\")\n\t\t\telse:\n\t\t\t\traise IOError(\"Bad magic number. Not a valid TIFF file\")\n\t\tnext_ifd, = unpack(byteorder+\"L\", fileobj)\n\n\t\tifds = []\n\t\twhile next_ifd != 0:\n\t\t\ti = ifd.Ifd(sub_ifd={\n\t\t\t\t34665:[tags.exfT,\"Exif tag\"],\n\t\t\t\t34853:[tags.gpsT,\"GPS tag\"]\n\t\t\t})\n\t\t\tnext_ifd = from_buffer(i, fileobj, next_ifd, byteorder)\n\t\t\tifds.append(i)\n\n\t\tif hasattr(fileobj, \"name\"):\n\t\t\tself._filename = fileobj.name\n\t\telse:\n\t\t\tfor i in ifds:\n\t\t\t\t_load_raster(i, fileobj)\n\n\t\tlist.__init__(self, ifds)\n\n\tdef __getitem__(self, item):\n\t\tif isinstance(item, tuple): return list.__getitem__(self, item[0])[item[-1]]\n\t\telse: return list.__getitem__(self, item)\n\n\tdef __add__(self, value):\n\t\tself.load_raster()\n\t\tif isinstance(value, TiffFile):\n\t\t\tvalue.load_raster()\n\t\t\tfor i in value: self.append(i)\n\t\telif isinstance(value, ifd.Ifd):\n\t\t\tself.append(value)\n\t\treturn self\n\t__iadd__ = __add__\n\n\tdef load_raster(self, idx=None):\n\t\tif hasattr(self, \"_filename\"):\n\t\t\tin_ = io.open(self._filename, \"rb\")\n\t\t\tfor ifd in iter(self) if idx == None else [self[idx]]:\n\t\t\t\tif not ifd.raster_loaded: _load_raster(ifd, in_)\n\t\t\tin_.close()\n\n\tdef save(self, f, byteorder=\"<\", idx=None):\n\t\tself.load_raster()\n\t\tfileobj, _close = _fileobj(f, \"wb\")\n\n\t\tpack(byteorder+\"HH\", fileobj, (0x4949 if byteorder == \"<\" else 0x4d4d, 0x2A,))\n\t\tnext_ifd = 8\n\n\t\tfor i in iter(self) if idx == None else [self[idx]]:\n\t\t\tpack(byteorder+\"L\", fileobj, (next_ifd,))\n\t\t\tnext_ifd = to_buffer(i, fileobj, next_ifd, byteorder)\n\n\t\tif _close: fileobj.close()\n\n\nclass JpegFile(collections.OrderedDict):\n\n\tjfif = property(lambda obj: collections.OrderedDict.__getitem__(obj, 0xffe0), None, None, \"JFIF data\")\n\texif = property(lambda obj: collections.OrderedDict.__getitem__(obj, 0xffe1)[0], None, None, \"Image IFD\")\n\tifd1 = property(lambda obj: collections.OrderedDict.__getitem__(obj, 0xffe1)[1], None, None, \"Thumbnail IFD\")\n\n\tdef __init__(self, fileobj):\n\t\tmarkers = collections.OrderedDict()\n\t\tmarker, = unpack(\">H\", fileobj)\n\t\tif marker != 0xffd8: raise Exception(\"not a valid jpeg file\")\n\t\twhile marker != 0xffd9: # EOI (End Of Image) Marker\n\t\t\tmarker, count = unpack(\">HH\", fileobj)\n\t\t\t# here is raster data marker, copy all after marker id\n\t\t\tif marker == 0xffda:\n\t\t\t\tfileobj.seek(-2, 1)\n\t\t\t\tmarkers[0xffda] = fileobj.read()[:-2]\n\t\t\t\t# say it is the end of the file\n\t\t\t\tmarker = 0xffd9\n\t\t\telif marker == 0xffe1:\n\t\t\t\tstring = StringIO(fileobj.read(count-2)[6:])\n\t\t\t\ttry: markers[marker] = TiffFile(string)\n\t\t\t\texcept: setattr(markers, \"_0xffe1\", string.getvalue())\n\t\t\t\tstring.close()\n\t\t\telse:\n\t\t\t\tmarkers[marker] = fileobj.read(count-2)\n\n\t\tcollections.OrderedDict.__init__(self, markers)\n\n\tdef __getitem__(self, item):\n\t\ttry: return collections.OrderedDict.__getitem__(self, 0xffe1)[0,item]\n\t\texcept KeyError: return collections.OrderedDict.__getitem__(self, item)\n\n\tdef _pack(self, marker, fileobj):\n\t\tdata = self[marker]\n\t\tif marker == 0xffda:\n\t\t\tpack(\">H\", fileobj, (marker,))\n\t\telif marker == 0xffe1:\n\t\t\tstring = StringIO()\n\t\t\tself[marker].save(string)\n\t\t\tdata = b\"Exif\\x00\\x00\" + string.getvalue()\n\t\t\tpack(\">HH\", fileobj, (marker, len(data) + 2))\n\t\t\tstring.close()\n\t\telse:\n\t\t\tpack(\">HH\", fileobj, (marker, len(data) + 2))\n\t\tfileobj.write(data)\n\n\tdef save(self, f):\n\t\tfileobj, _close = _fileobj(f, \"wb\")\n\n\t\tpack(\">H\", fileobj, (0xffd8,))\n\t\tfor key in self: self._pack(key, fileobj)\n\t\tpack(\">H\", fileobj, (0xffd9,))\n\n\t\tif _close: fileobj.close()\n\n\tdef save_thumbnail(self, f):\n\t\ttry:\n\t\t\tifd = self.ifd1\n\t\texcept IndexError:\n\t\t\tpass\n\t\telse:\n\t\t\tcompression = ifd[259]\n\t\t\tif hasattr(f, \"close\"):\n\t\t\t\tfileobj = f\n\t\t\t\t_close = False\n\t\t\telse:\n\t\t\t\tfileobj = io.open(os.path.splitext(f)[0] + (\".jpg\" if compression == 6 else \".tif\"), \"wb\")\n\t\t\t\t_close = True\n\n\t\t\tif compression == 6:\n\t\t\t\tfileobj.write(ifd.jpegIF)\n\t\t\telif compression == 1:\n\t\t\t\tself[0xffe1].save(fileobj, idx=1)\n\n\t\t\tif _close: fileobj.close()\n\n\tdef dump_exif(self, f):\n\t\tfileobj, _close = _fileobj(f, \"wb\")\n\t\tself[0xffe1].save(fileobj)\n\t\tif _close: fileobj.close()\n\n\tdef load_exif(self, f):\n\t\tfileobj, _close = _fileobj(f, \"rb\")\n\t\tself[0xffe1] = TiffFile(fileobj)\n\t\tself[0xffe1].load_raster()\n\t\tif _close: fileobj.close()\n\n\tdef strip_exif(self):\n\t\tfor key in [k for k in self.exif.sub_ifd if k in self.exif]:\n\t\t\tself.exif.pop(key)\n\t\tself.exif.sub_ifd = {}\n\t\tfor key in list(k for k in self.exif if k not in tags.bTT):\n\t\t\tself.exif.pop(key)\n\t\twhile len(self[0xffe1]) > 1:\n\t\t\tself[0xffe1].pop(-1)\n\n\ndef jpeg_extract(f):\n\tfileobj, _close = _fileobj(f, \"rb\")\n\n\tifd = False\n\tmarker, = unpack(\">H\", fileobj)\n\tif marker != 0xffd8: raise Exception(\"not a valid jpeg file\")\n\twhile marker != 0xffd9:\n\t\tmarker, count = unpack(\">HH\", fileobj)\n\t\tif marker == 0xffe1:\n\t\t\tstring = StringIO(fileobj.read(count-2)[6:])\n\t\t\tifd = TiffFile(string)\n\t\t\tstring.close()\n\t\t\tmarker = 0xffd9\n\t\telse:\n\t\t\tfileobj.read(count-2)\n\n\tif _close: fileobj.close()\n\treturn ifd\n\ndef open(f):\n\tfileobj, _close = _fileobj(f, \"rb\")\n\t\t\n\tfirst, = unpack(\">H\", fileobj)\n\tfileobj.seek(0)\n\n\tif first == 0xffd8: obj = JpegFile(fileobj)\n\telif first in [0x4d4d, 0x4949]: obj = TiffFile(fileobj)\n\n\tif _close: fileobj.close()\n\ttry: return obj\n\texcept: raise Exception(\"file is not a valid JPEG nor TIFF image\")\n\n\n'''\n# if PIL exists do some overridings\ntry: from PIL import Image as _Image\nexcept ImportError: pass\nelse:\n\tdef _getexif(im):\n\t\ttry:\n\t\t\tdata = im.info[\"exif\"]\n\t\texcept KeyError:\n\t\t\treturn None\n\t\tfileobj = io.BytesIO(data[6:])\n\t\texif = TiffFile(fileobj)\n\t\tfileobj.close()\n\t\treturn exif\n\n\tclass Image(_Image.Image):\n\n\t\t_image_ = _Image.Image\n\n\t\t@staticmethod\n\t\tdef open(*args, **kwargs):\n\t\t\treturn _Image.open(*args, **kwargs)\n\n\t\tdef save(self, fp, format=\"JPEG\", **params):\n\n\t\t\tifd = params.pop(\"ifd\", False)\n\t\t\tif ifd != False:\n\t\t\t\tfileobj = StringIO()\n\t\t\t\tif isinstance(ifd, TiffFile):\n\t\t\t\t\tifd.load_raster()\n\t\t\t\t\tifd.save(fileobj)\n\t\t\t\telif isinstance(ifd, JpegFile):\n\t\t\t\t\tifd[0xffe1].save(fileobj)\n\t\t\t\tdata = fileobj.getvalue()\n\t\t\t\tfileobj.close()\n\t\t\t\tif len(data) > 0:\n\t\t\t\t\tparams[\"exif\"] = b\"Exif\\x00\\x00\" + (data.encode() if isinstance(data, str) else data)\n\n\t\t\tImage._image_.save(self, fp, format=\"JPEG\", **params)\n\t_Image.Image = Image\n\n\tfrom PIL import JpegImagePlugin\n\tJpegImagePlugin._getexif = _getexif\n\tdel _getexif\n'''\n"
  },
  {
    "path": "core/lib/Tyf/decoders.py",
    "content": "# -*- encoding:utf-8 -*-\n# Copyright 2012-2015, THOORENS Bruno - http://bruno.thoorens.free.fr/licences/tyf.html\nimport datetime\n\n###############\n# type decoders\n\n_1 = _3 = _4 = _6 = _8 = _9 = _11 = _12 = lambda value: value[0] if len(value) == 1 else value\n\n_2 = lambda value: value[:-1]\n\ndef _5(value):\n\tresult = tuple((float(n)/(1 if d==0 else d)) for n,d in zip(value[0::2], value[1::2]))\n\treturn result[0] if len(result) == 1 else result\n\n_7 = lambda value: value\n\n_10 = _5\n\n#######################\n# Tag-specific decoders\n\n# XPTitle XPComment XBAuthor\n_0x9c9b = _0x9c9c = _0x9c9d = lambda value : \"\".join(chr(e) for e in value[0::2]).encode()[:-1]\n# UserComment GPSProcessingMethod\n_0x9286 = _0x1b = lambda value: value[8:]\n#GPSLatitudeRef\n_0x1 = lambda value: 1 if value in [b\"N\\x00\", b\"N\"] else -1\n#GPSLatitude\ndef _0x2(value):\n\tdegrees, minutes, seconds = _5(value)\n\treturn (seconds/60 + minutes)/60 + degrees\n#GPSLatitudeRef\n_0x3 = lambda value: 1 if value in [b\"E\\x00\", b\"E\"] else -1\n#GPSLongitude\n_0x4 = _0x2\n#GPSAltitudeRef\n_0x5 = lambda value: 1 if value == 0 else -1\n# GPSTimeStamp\n_0x7 = lambda value: datetime.time(*[int(e) for e in _5(value)])\n# GPSDateStamp\n_0x1d = lambda value: datetime.datetime.strptime(_2(value).decode(), \"%Y:%m:%d\")\n# DateTime DateTimeOriginal DateTimeDigitized\n_0x132 = _0x9003 = _0x9004 = lambda value: datetime.datetime.strptime(_2(value).decode(), \"%Y:%m:%d %H:%M:%S\")\n"
  },
  {
    "path": "core/lib/Tyf/encoders.py",
    "content": "# -*- encoding:utf-8 -*-\n# Copyright 2012-2015, THOORENS Bruno - http://bruno.thoorens.free.fr/licences/tyf.html\nfrom . import reduce\nimport math, fractions, datetime\n###############\n# type encoders\n\n_m_short = 0\n_M_short = 2**8\ndef _1(value):\n\tvalue = int(value)\n\treturn (_m_short, ) if value < _m_short else \\\n\t       (_M_short, ) if value > _M_short else \\\n\t       (value, )\n\ndef _2(value):\n\tif not isinstance(value, bytes):\n\t\tvalue = value.encode()\n\tvalue += b\"\\x00\" if value[-1] != b\"\\x00\" else \"\"\n\treturn value\n\n_m_byte = 0\n_M_byte = 2**16\ndef _3(value):\n\tvalue = int(value)\n\treturn (_m_byte, ) if value < _m_byte else \\\n\t       (_M_byte, ) if value > _M_byte else \\\n\t       (value, )\n\n_m_long = 0\n_M_long = 2**32\ndef _4(value):\n\tvalue = int(value)\n\treturn (_m_long, ) if value < _m_long else \\\n\t       (_M_long, ) if value > _M_long else \\\n\t       (value, )\n\ndef _5(value):\n\tif not isinstance(value, tuple): value = (value, )\n\treturn reduce(tuple.__add__, [(f.numerator, f.denominator) for f in [fractions.Fraction(str(v)).limit_denominator(10000000) for v in value]])\n\n_m_s_short = -_M_short/2\n_M_s_short = _M_short/2-1\ndef _6(value):\n\tvalue = int(value)\n\treturn (_m_s_short, ) if value < _m_s_short else \\\n\t       (_M_s_short, ) if value > _M_s_short else \\\n\t       (value, )\n\ndef _7(value):\n\tif not isinstance(value, bytes):\n\t\tvalue = value.encode()\n\treturn value\n\n_m_s_byte = -_M_byte/2\n_M_s_byte = _M_byte/2-1\ndef _8(value):\n\tvalue = int(value)\n\treturn (_m_s_byte, ) if value < _m_s_byte else \\\n\t       (_M_s_byte, ) if value > _M_s_byte else \\\n\t       (value, )\n\n_m_s_long = -_M_long/2\n_M_s_long = _M_long/2-1\ndef _9(value):\n\tvalue = int(value)\n\treturn (_m_s_long, ) if value < _m_s_long else \\\n\t       (_M_s_long, ) if value > _M_s_long else \\\n\t       (value, )\n\n_10 = _5\n\ndef _11(value):\n\treturn (float(value), )\n\n_12 = _11\n\n\n#######################\n# Tag-specific encoders\n\n# XPTitle XPComment XBAuthor\n_0x9c9b = _0x9c9c = _0x9c9d = lambda value : reduce(tuple.__add__, [(ord(e), 0) for e in value])\n# UserComment GPSProcessingMethod\n_0x9286 = _0x1b = lambda value: b\"ASCII\\x00\\x00\\x00\" + (value.encode() if not isinstance(value, bytes) else value)\n# GPSLatitudeRef\n_0x1 = lambda value: b\"N\\x00\" if bool(value >= 0) == True else b\"S\\x00\"\n# GPSLatitude\ndef _0x2(value):\n\tvalue = abs(value)\n\n\tdegrees = math.floor(value)\n\tminutes = (value - degrees) * 60\n\tseconds = (minutes - math.floor(minutes)) * 60\n\tminutes = math.floor(minutes)\n\n\tif seconds >= (60.-0.0001):\n\t\tseconds = 0.\n\t\tminutes += 1\n\n\tif minutes >= (60.-0.0001):\n\t\tminutes = 0.\n\t\tdegrees += 1\n\n\treturn _5((degrees, minutes, seconds))\n#GPSLongitudeRef\n_0x3 = lambda value: b\"E\\x00\" if bool(value >= 0) == True else b\"W\\x00\"\n#GPSLongitude\n_0x4 = _0x2\n#GPSAltitudeRef\n_0x5 = lambda value: _3(1 if value < 0 else 0)\n#GPSAltitude\n_0x6 = lambda value: _5(abs(value))\n# GPSTimeStamp\n_0x7 = lambda value: _5(tuple(float(e) for e in [value.hour, value.minute, value.second]))\n# GPSDateStamp\n_0x1d = lambda value: _2(value.strftime(\"%Y:%m:%d\"))\n# DateTime DateTimeOriginal DateTimeDigitized\n_0x132 = _0x9003 = _0x9004 = lambda value: _2(value.strftime(\"%Y:%m:%d %H:%M:%S\"))\n"
  },
  {
    "path": "core/lib/Tyf/gkd.py",
    "content": "# -*- encoding: utf-8 -*-\n# Copyright 2012-2015, THOORENS Bruno - http://bruno.thoorens.free.fr/licences/tyf.html\n# ~ http://www.remotesensing.org/geotiff/spec/geotiffhome.html\n\nfrom . import ifd, tags, values, __geotiff__, __PY3__\nimport collections\n\nGeoKeyModel = {\n\t33550: collections.namedtuple(\"ModelPixelScale\", \"ScaleX, ScaleY, ScaleZ\"),\n\t33922: collections.namedtuple(\"ModelTiepoint\", \"I,J,K,X,Y,Z\"),\n\t34264: collections.namedtuple(\"ModelTransformation\", \"a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p\")\n}\n\ndef Transform(obj, x=0., y=0., z1=0.,z2=1.):\n\treturn (\n\t\tobj[0] *  x + obj[1] *  y + obj[2] *  z1 + obj[3] *  z2,\n\t\tobj[4] *  x + obj[5] *  y + obj[6] *  z1 + obj[7] *  z2,\n\t\tobj[8] *  x + obj[9] *  y + obj[10] * z1 + obj[11] * z2,\n\t\tobj[12] * x + obj[13] * y + obj[14] * z1 + obj[15] * z2\n\t)\n\n_TAGS = {\n\t# GeoTIFF Configuration GeoKeys\n\t1024: (\"GTModelTypeGeoKey\", [3], 0, None),\n\t1025: (\"GTRasterTypeGeoKey\", [3], 1, None),\n\t1026: (\"GTCitationGeoKey\", [2], None, None),             # ASCII text\n\n\t# Geographic CS Parameter GeoKeys\n\t2048: (\"GeographicTypeGeoKey\", [3], 4326, None),         # epsg datum code [4001 - 4999]\n\t2049: (\"GeogCitationGeoKey\", [2], None, None),           # ASCII text\n\t2050: (\"GeogGeodeticDatumGeoKey\", [3], None, None),      # use 2048 !\n\t2051: (\"GeogPrimeMeridianGeoKey\", [3], 8901, None),      # epsg prime meridian code [8001 - 8999]\n\t2052: (\"GeogLinearUnitsGeoKey\", [3], 9001, None),        # epsg linear unit code [9000 - 9099]\n\t2053: (\"GeogLinearUnitSizeGeoKey\", [12], None, None),    # custom unit in meters\n\t2054: (\"GeogAngularUnitsGeoKey\", [3], 9101, None),\n\t2055: (\"GeogAngularUnitsSizeGeoKey\", [12], None, None),  # custom unit in radians\n\t2056: (\"GeogEllipsoidGeoKey\", [3], None, None),          # epsg ellipsoid code [7000 - 7999]\n\t2057: (\"GeogSemiMajorAxisGeoKey\", [12], None, None),\n\t2058: (\"GeogSemiMinorAxisGeoKey\", [12], None, None),\n\t2059: (\"GeogInvFlatteningGeoKey\", [12], None, None),\n\t2060: (\"GeogAzimuthUnitsGeoKey\",[3], None, None),\n\t2061: (\"GeogPrimeMeridianLongGeoKey\", [12], None, None), # custom prime meridian value in GeogAngularUnits\n\t\n\t# Projected CS Parameter GeoKeys\n\t3072: (\"ProjectedCSTypeGeoKey\", [3], None, None),        # epsg grid code [20000 - 32760]\n\t3073: (\"PCSCitationGeoKey\", [2], None, None),            # ASCII text\n\t3074: (\"ProjectionGeoKey\", [3], None, None),             # [10000 - 19999]\n\t3075: (\"ProjCoordTransGeoKey\", [3], None, None),\n\t3076: (\"ProjLinearUnitsGeoKey\", [3], None, None),\n\t3077: (\"ProjLinearUnitSizeGeoKey\", [12], None, None),    # custom unit in meters\n\t3078: (\"ProjStdParallel1GeoKey\", [12], None, None),\n\t3079: (\"ProjStdParallel2GeoKey\", [12], None, None),\n\t3080: (\"ProjNatOriginLongGeoKey\", [12], None, None),\n\t3081: (\"ProjNatOriginLatGeoKey\", [12], None, None),\n\t3082: (\"ProjFalseEastingGeoKey\", [12], None, None),\n\t3083: (\"ProjFalseNorthingGeoKey\", [12], None, None),\n\t3084: (\"ProjFalseOriginLongGeoKey\", [12], None, None),\n\t3085: (\"ProjFalseOriginLatGeoKey\", [12], None, None),\n\t3086: (\"ProjFalseOriginEastingGeoKey\", [12], None, None),\n\t3087: (\"ProjFalseOriginNorthingGeoKey\", [12], None, None),\n\t3088: (\"ProjCenterLongGeoKey\", [12], None, None),\n\t3089: (\"ProjCenterLatGeoKey\", [12], None, None),\n\t3090: (\"ProjCenterEastingGeoKey\", [12], None, None),\n\t3091: (\"ProjFalseOriginNorthingGeoKey\", [12], None, None),\n\t3092: (\"ProjScaleAtNatOriginGeoKey\", [12], None, None),\n\t3093: (\"ProjScaleAtCenterGeoKey\", [12], None, None),\n\t3094: (\"ProjAzimuthAngleGeoKey\", [12], None, None),\n\t3095: (\"ProjStraightVertPoleLongGeoKey\", [12], None, None),\n\t\n\t# Vertical CS Parameter Keys\n\t4096: (\"VerticalCSTypeGeoKey\", [3], None, None),\n\t4097: (\"VerticalCitationGeoKey\", [2], None, None),\n\t4098: (\"VerticalDatumGeoKey\", [3], None, None),\n\t4099: (\"VerticalUnitsGeoKey\", [3], None, None),\n}\n\n_2TAG = dict((v[0], t) for t,v in _TAGS.items())\n_2KEY = dict((v, k) for k,v in _2TAG.items())\n\nif __PY3__:\n\timport functools\n\treduce = functools.reduce\n\tlong = int\n\nclass GkdTag(ifd.TiffTag):\n\tstrict = True\n\n\tdef __init__(self, tag=0x0, value=None, name=\"GeoTiff Tag\"):\n\t\tself.name = name\n\t\tif tag == 0: return\n\t\tself.key, types, default, self.comment = _TAGS.get(tag, (\"Unknown\", [0,], None, \"Undefined tag\"))\n\t\tvalue = default if value == None else value\n\n\t\tself.tag = tag\n\t\trestricted = getattr(values, self.key, {})\n\n\t\tif restricted:\n\t\t\treverse = dict((v,k) for k,v in restricted.items())\n\t\t\tif value in restricted:\n\t\t\t\tself.meaning = restricted.get(value)\n\t\t\telif value in reverse:\n\t\t\t\tvalue = reverse[value]\n\t\t\t\tself.meaning = value\n\t\t\telif GkdTag.strict:\n\t\t\t\traise ValueError('\"%s\" value must be one of %s, get %s instead' % (self.key, list(restricted.keys()), value))\n\n\t\tself.type, self.count, self.value = self._encode(value, types)\n\n\tdef __setattr__(self, attr, value):\n\t\tobject.__setattr__(self, attr, value)\n\n\tdef _encode(self, value, types):\n\t\tif isinstance(value, str): value = value.encode()\n\t\telif not hasattr(value, \"__len__\"): value = (value, )\n\t\ttyp = 0\n\t\tif 2 in types: typ = 34737\n\t\telif 12 in types: typ = 34736\n\t\treturn typ, len(value), value\n\n\tdef _decode(self):\n\t\tif self.count == 1: return self.value[0]\n\t\telse: return self.value\n\n\nclass Gkd(dict):\n\ttagname = \"Geotiff Tag\"\n\tversion = __geotiff__[0]\n\trevision = __geotiff__[1:]\n\n\tdef __init__(self, value={}, **pairs):\n\t\tdict.__init__(self)\n\t\tself.from_ifd(value, **pairs)\n\n\tdef __getitem__(self, tag):\n\t\tif isinstance(tag, str): tag = _2TAG[tag]\n\t\treturn dict.__getitem__(self, tag)._decode()\n\n\tdef __setitem__(self, tag, value):\n\t\tif isinstance(tag, str): tag = _2TAG[tag]\n\t\tdict.__setitem__(self, tag, GkdTag(tag, value, name=self.tagname))\n\n\tdef get(self, tag, error=None):\n\t\tif hasattr(self, \"_%s\" % tag): return getattr(self, \"_%s\" % tag)\n\t\telse: return dict.get(self, tag, error)\n\n\tdef to_ifd(self):\n\t\t_34735, _34736, _34737, nbkey, _ifd = (), (), b\"\", 0, {}\n\t\tfor key,tag in sorted(self.items(), key = lambda a: a[0]):\n\t\t\tif tag.type == 0:\n\t\t\t\t_34735 += (key, 0, 1) + tag.value\n\t\t\t\tnbkey += 1\n\t\t\telif tag.type == 34736: # GeoDoubleParamsTag\n\t\t\t\t_34735 += (key, 34736, 1, len(_34736))\n\t\t\t\t_34736 += tag.value\n\t\t\t\tnbkey += 1\n\t\t\telif tag.type == 34737: # GeoAsciiParamsTag\n\t\t\t\t_34735 += (key, 34737, tag.count+1, len(_34737))\n\t\t\t\t_34737 += tag.value + b\"|\"\n\t\t\t\tnbkey += 1\n\n\t\tresult = ifd.Ifd()\n\t\tresult.set(33922, 12, reduce(tuple.__add__, [tuple(e) for e in self.get(33922, ([0.,0.,0.,0.,0.,0.],))]))\n\t\tresult.set(33550, 12, tuple(self.get(33550, (1.,1.,1.))))\n\t\tresult.set(34264, 12, tuple(self.get(34264, (1.,0.,0.,0.,0.,-1.,0.,0.,0.,0.,1.,0.,0.,0.,0.,1.))))\n\t\tresult.set(34735, 3, (self.version,) + self.revision + (nbkey,) + _34735)\n\t\tresult.set(34736, 12, _34736)\n\t\tresult.set(34737, 2, _34737)\n\t\treturn result\n\n\tdef from_ifd(self, ifd = {}, **kw):\n\t\tpairs = dict(ifd, **kw)\n\t\tfor tag in [t for t in [33922, 33550, 34264] if t in pairs]: # ModelTiepointTag, ModelPixelScaleTag, ModelTransformationTag\n\t\t\tnt = GeoKeyModel[tag]\n\t\t\tif tag == 33922: # can be more than one TiePoint\n\t\t\t\tn = len(nt._fields)\n\t\t\t\tseq = ifd[tag]\n\t\t\t\tsetattr(self, \"_%s\" % tag, tuple(nt(*seq[i:i+n]) for i in range(0, len(seq), n)))\n\t\t\telse:\n\t\t\t\tsetattr(self, \"_%s\" % tag, nt(*ifd[tag]))\n\t\tif 34736 in pairs: # GeoDoubleParamsTag\n\t\t\t_34736 = ifd[34736]\n\t\tif 34737 in pairs: # GeoAsciiParamsTag\n\t\t\t_34737 = ifd[34737]\n\t\tif 34735 in pairs: # GeoKeyDirectoryTag\n\t\t\t_34735 = ifd[34735]\n\t\t\tself.version = _34735[0]\n\t\t\tself.revision = _34735[1:3]\n\t\t\tfor (tag, typ, count, value) in zip(_34735[4::4],_34735[5::4],_34735[6::4],_34735[7::4]):\n\t\t\t\tif typ == 0: self[tag] = value\n\t\t\t\telif typ == 34736: self[tag] = _34736[value]\n\t\t\t\telif typ == 34737: self[tag] = _34737[value:value+count-1]\n\n\tdef getModelTransformation(self, tie_index=0):\n\t\tif hasattr(self, \"_34264\"):\n\t\t\tmatrix = GeoKeyModel[34264](*getattr(self, \"_34264\"))\n\t\telif hasattr(self, \"_33922\") and hasattr(self, \"_33550\"):\n\t\t\tSx, Sy, Sz = getattr(self, \"_33550\")\n\t\t\tI, J, K, X, Y, Z = getattr(self, \"_33922\")[tie_index]\n\t\t\tmatrix = GeoKeyModel[34264](\n\t\t\t\tSx,  0., 0., X - I*Sx,\n\t\t\t\t0., -Sy, 0., Y + J*Sy,\n\t\t\t\t0., 0. , Sz, Z - K*Sz,\n\t\t\t\t0., 0. , 0., 1.\n\t\t\t)\n\t\telse:\n\t\t\tmatrix = GeoKeyModel[34264](\n\t\t\t\t1., 0. , 0., 0.,\n\t\t\t\t0., -1., 0., 0.,\n\t\t\t\t0., 0. , 1., 0.,\n\t\t\t\t0., 0. , 0., 1.\n\t\t\t)\n\t\treturn lambda x,y,z1=0.,z2=1.,m=matrix: Transform(m, x,y,z1,z2)\n\n\tdef tags(self):\n\t\tfor v in sorted(dict.values(self), key=lambda e:e.tag):\n\t\t\tyield v\n"
  },
  {
    "path": "core/lib/Tyf/ifd.py",
    "content": "# -*- encoding:utf-8 -*-\n# Copyright 2012-2015, THOORENS Bruno - http://bruno.thoorens.free.fr/licences/tyf.html\n\nfrom . import io, os, tags, encoders, decoders, reduce, values, TYPES, urllib, StringIO\nimport struct, fractions\n\nclass TiffTag(object):\n\n\t# IFD entries values\n\ttag = 0x0\n\ttype = 0\n\tcount = 0\n\tvalue = None\n\n\t# end user side values\n\tkey = \"Undefined\"\n\tname = \"Undefined tag\"\n\tcomment = \"Nothing about this tag\"\n\tmeaning = None\n\n\tdef __init__(self, tag, type=None, value=None, name=\"Tiff tag\"):\n\t\tself.key, _typ, default, self.comment = tags.get(tag)\n\t\tself.tag = tag\n\t\tself.name = name\n\t\tself.type = _typ[-1] if type == None else type\n\n\t\tif value != None: self._encode(value)\n\t\telif default != None: self.value = (default,) if not hasattr(default, \"len\") else default\n\n\tdef __setattr__(self, attr, value):\n\t\tif attr == \"type\":\n\t\t\ttry: object.__setattr__(self, \"_encoder\", getattr(encoders, \"_%s\"%hex(self.tag)))\n\t\t\texcept AttributeError: object.__setattr__(self, \"_encoder\", getattr(encoders, \"_%s\"%value))\n\t\t\ttry: object.__setattr__(self, \"_decoder\", getattr(decoders, \"_%s\"%hex(self.tag)))\n\t\t\texcept AttributeError: object.__setattr__(self, \"_decoder\", getattr(decoders, \"_%s\"%value))\n\t\telif attr == \"value\":\n\t\t\trestricted = getattr(values, self.key, None)\n\t\t\tif restricted != None:\n\t\t\t\tv = value[0] if isinstance(value, tuple) else value\n\t\t\t\tself.meaning = restricted.get(v, \"no description found [%r]\" % (v,))\n\t\t\tself.count = len(value) // (1 if self.type not in [5,10] else 2)\n\t\t\tself._determine_if_offset()\n\t\tobject.__setattr__(self, attr, value)\n\n\tdef __repr__(self):\n\t\treturn \"<%s 0x%x: %s = %r>\" % (self.name, self.tag, self.key, self.value) + (\"\" if not self.meaning else ' := %r'%self.meaning)\n\n\tdef _encode(self, value):\n\t\tself.value = self._encoder(value)\n\n\tdef _decode(self):\n\t\treturn self._decoder(self.value)\n\n\tdef _determine_if_offset(self):\n\t\tif self.count == 1 and self.type in [1, 2, 3, 4, 6, 7, 8, 9]: setattr(self, \"value_is_offset\", False)\n\t\telif self.count <= 2 and self.type in [3, 8]: setattr(self, \"value_is_offset\", False)\n\t\telif self.count <= 4 and self.type in [1, 2, 6, 7]: setattr(self, \"value_is_offset\", False)\n\t\telse: setattr(self, \"value_is_offset\", True)\n\n\tdef _fill(self):\n\t\ts = struct.calcsize(\"=\"+TYPES[self.type][0])\n\t\tvoidspace = (struct.calcsize(\"=L\") - self.count*s)//s\n\t\tif self.type in [2, 7]: return self.value + b\"\\x00\"*voidspace\n\t\telif self.type in [1, 3, 6, 8]: return self.value + ((0,)*voidspace)\n\t\treturn self.value\n\n\tdef calcsize(self):\n\t\treturn struct.calcsize(\"=\" + TYPES[self.type][0] * (self.count*(2 if self.type in [5,10] else 1))) if self.value_is_offset else 0\n\n\nclass Ifd(dict):\n\ttagname = \"Tiff Tag\"\n\n\texif_ifd = property(lambda obj: obj.sub_ifd.get(34665, {}), None, None, \"shortcut to EXIF sub ifd\")\n\tgps_ifd = property(lambda obj: obj.sub_ifd.get(34853, {}), None, None, \"shortcut to GPS sub ifd\")\n\thas_raster = property(lambda obj: 273 in obj or 288 in obj or 324 in obj or 513 in obj, None, None, \"return true if it contains raster data\")\n\traster_loaded = property(lambda obj: not(obj.has_raster) or bool(len(obj.stripes+obj.tiles+obj.free)+len(obj.jpegIF)), None, None, \"\")\n\tsize = property(\n\t\tlambda obj: {\n\t\t\t\"ifd\": struct.calcsize(\"=H\" + (len(obj)*\"HHLL\") + \"L\"),\n\t\t\t\"data\": reduce(int.__add__, [t.calcsize() for t in dict.values(obj)])\n\t\t}, None, None, \"return ifd-packed size and data-packed size\")\n\t\t\n\tdef __init__(self, sub_ifd={}, **kwargs):\n\t\tself._sub_ifd = sub_ifd\n\t\tsetattr(self, \"tagname\", kwargs.pop(\"tagname\", \"Tiff tag\"))\n\t\tdict.__init__(self)\n\n\t\tself.sub_ifd = {}\n\t\tself.stripes = ()\n\t\tself.tiles = ()\n\t\tself.free = ()\n\t\tself.jpegIF = b\"\"\n\n\tdef __setitem__(self, tag, value):\n\t\tfor t,(ts,tname) in self._sub_ifd.items():\n\t\t\ttag = tags._2tag(tag, family=ts)\n\t\t\tif tag in ts:\n\t\t\t\tif not t in self.sub_ifd:\n\t\t\t\t\tself.sub_ifd[t] = Ifd(sub_ifd={}, tagname=tname)\n\t\t\t\tself.sub_ifd[t].addtag(TiffTag(tag, value=value))\n\t\t\t\treturn\n\t\telse:\n\t\t\ttag = tags._2tag(tag)\n\t\t\tdict.__setitem__(self, tag, TiffTag(tag, value=value, name=self.tagname))\n\n\tdef __getitem__(self, tag):\n\t\tfor i in self.sub_ifd.values():\n\t\t\ttry: return i[tag]\n\t\t\texcept KeyError: pass\n\t\treturn dict.__getitem__(self, tags._2tag(tag))._decode()\n\n\tdef _check(self):\n\t\tfor key in self.sub_ifd:\n\t\t\tif key not in self:\n\t\t\t\tself.addtag(TiffTag(key, 4, 0, name=self.tagname))\n\n\tdef set(self, tag, typ, value):\n\t\tfor t,(ts,tname) in self._sub_ifd.items():\n\t\t\tif tag in ts:\n\t\t\t\tif not t in self.sub_ifd:\n\t\t\t\t\tself.sub_ifd[t] = Ifd(sub_ifd={}, tagname=tname)\n\t\t\t\tself.sub_ifd[t].set(tag, typ, value)\n\t\t\t\treturn\n\t\ttifftag = TiffTag(tag=tag, type=typ, name=self.tagname)\n\t\ttifftag.value = (value,) if not hasattr(value, \"__len__\") else value\n\t\ttifftag.name = self.tagname\n\t\tdict.__setitem__(self, tag, tifftag)\n\n\tdef get(self, tag):\n\t\tfor i in self.sub_ifd.values():\n\t\t\tif tag in i: return i.get(tag)\n\t\treturn dict.get(self, tags._2tag(tag))\n\n\tdef addtag(self, tifftag):\n\t\tif isinstance(tifftag, TiffTag):\n\t\t\ttifftag.name = self.tagname\n\t\t\tdict.__setitem__(self, tifftag.tag, tifftag)\n\n\tdef tags(self):\n\t\tfor v in sorted(dict.values(self), key=lambda e:e.tag):\n\t\t\tyield v\n\t\tfor i in self.sub_ifd.values():\n\t\t\tfor v in sorted(dict.values(i), key=lambda e:e.tag):\n\t\t\t\tyield v\n\n\tdef set_location(self, longitude, latitude, altitude=0.):\n\t\tif 34853 not in self._sub_ifd:\n\t\t\tself._sub_ifd[34853] = [tags.gpsT, \"GPS tag\"]\n\t\tself[1] = self[2] = latitude\n\t\tself[3] = self[4] = longitude\n\t\tself[5] = self[6] = altitude\n\n\tdef get_location(self):\n\t\tif set([1,2,3,4,5,6]) <= set(self.gps_ifd.keys()):\n\t\t\treturn (\n\t\t\t\tself[3] * self[4],\n\t\t\t\tself[1] * self[2],\n\t\t\t\tself[5] * self[6]\n\t\t\t)\n\n\tdef load_location(self, zoom=15, size=\"256x256\", mcolor=\"0xff00ff\", format=\"png\", scale=1):\n\t\tif set([1,2,3,4]) <= set(self.gps_ifd.keys()):\n\t\t\tgps_ifd = self.gps_ifd\n\t\t\tlatitude = gps_ifd[1] * gps_ifd[2]\n\t\t\tlongitude = gps_ifd[3] * gps_ifd[4]\n\t\t\ttry:\n\t\t\t\topener = urllib.urlopen(\"https://maps.googleapis.com/maps/api/staticmap?center=%s,%s&zoom=%s&size=%s&markers=color:%s%%7C%s,%s&format=%s&scale=%s\" % (\n\t\t\t\t\tlatitude, longitude,\n\t\t\t\t\tzoom, size, mcolor,\n\t\t\t\t\tlatitude, longitude,\n\t\t\t\t\tformat, scale\n\t\t\t\t))\n\t\t\texcept:\n\t\t\t\treturn StringIO()\n\t\t\telse:\n\t\t\t\treturn StringIO(opener.read())\n\t\t\t\tprint(\"googleapis connexion error\")\n\t\telse:\n\t\t\treturn StringIO()\n\n\tdef dump_location(self, tilename, zoom=15, size=\"256x256\", mcolor=\"0xff00ff\", format=\"png\", scale=1):\n\t\tif set([1,2,3,4]) <= set(self.gps_ifd.keys()):\n\t\t\tgps_ifd = self.gps_ifd\n\t\t\tlatitude = gps_ifd[1] * gps_ifd[2]\n\t\t\tlongitude = gps_ifd[3] * gps_ifd[4]\n\t\t\ttry:\n\t\t\t\turllib.urlretrieve(\"https://maps.googleapis.com/maps/api/staticmap?center=%s,%s&zoom=%s&size=%s&markers=color:%s%%7C%s,%s&format=%s&scale=%s\" % (\n\t\t\t\t\t\tlatitude, longitude,\n\t\t\t\t\t\tzoom, size, mcolor,\n\t\t\t\t\t\tlatitude, longitude,\n\t\t\t\t\t\tformat, scale\n\t\t\t\t\t),\n\t\t\t\t\tos.path.splitext(tilename)[0] + \".\"+format\n\t\t\t\t)\n\t\t\texcept:\n\t\t\t\tprint(\"googleapis connexion error\")\n"
  },
  {
    "path": "core/lib/Tyf/tags.py",
    "content": "# -*- encoding:utf-8 -*-\n# Copyright 2012-2015, THOORENS Bruno - http://bruno.thoorens.free.fr/licences/tyf.html\n# ~ http://wwwawaresystemsbe/imaging/tiff/tifftagshtml\nfrom . import values\n\n# Baseline TIFF tags\nbTT = {\n\t254:   (\"NewSubfileType\", [1], 0, \"A general indication of the kind of data contained in this subfile\"),\n\t255:   (\"SubfileType\", [1], None, \"Deprecated, use NewSubfiletype instead\"),\n\t256:   (\"ImageWidth\", [1,4], None, \"Number of columns in the image, ie, the number of pixels per row\"),\n\t257:   (\"ImageLength\", [1,4], None, \"Number of rows of pixels in the image\"),\n\t258:   (\"BitsPerSample\", [1], 1, \"Array size = SamplesPerPixel, number of bits per component\"),\n\t259:   (\"Compression\", [1], 1, \"Compression scheme used on the image data\"),\n\t262:   (\"PhotometricInterpretation\", [1], None, \"The color space of the image data\"),\n\t263:   (\"Thresholding\", [1], 1, \"For black and white TIFF files that represent shades of gray, the technique used to convert from gray to black and white pixels\"),\n\t264:   (\"CellWidth\", [1], None, \"The width of the dithering or halftoning matrix used to create a dithered or halftoned bilevel file\"),\n\t265:   (\"CellLength\", [1], None, \"The length of the dithering or halftoning matrix used to create a dithered or halftoned bilevel file\"),\n\t266:   (\"FillOrder\", [1], 1, \"The logical order of bits within a byt\"),\n\t270:   (\"ImageDescription\", [2], None, \"A string that describes the subject of the image\"),\n\t271:   (\"Make\", [2], None, \"The scanner manufacturer\"),\n\t272:   (\"Model\", [2], None, \"The scanner model name or number\"),\n\t273:   (\"StripOffsets\", [1,4], None, \"For each strip, the byte offset of that strip\"),\n\t274:   (\"Orientation\", [1], 1, \"The orientation of the image with respect to the rows and columns\"),\n\t277:   (\"SamplesPerPixel\", [1], 1, \"The number of components per pixel\"),\n\t278:   (\"RowsPerStrip\", [1,4], 2**32-1, \"The number of rows per strip\"),\n\t279:   (\"StripByteCounts\", [1,4], None, \"For each strip, the number of bytes in the strip after compression\"),\n\t280:   (\"MinSampleValue\", [1], 0, \"The minimum component value used\"),\n\t281:   (\"MaxSampleValue\", [1], 1, \"The maximum component value used\"),\n\t282:   (\"XResolution\", [5], None, \"The number of pixels per ResolutionUnit in the ImageWidth direction\"),\n\t283:   (\"YResolution\", [5], None, \"The number of pixels per ResolutionUnit in the ImageLength direction\"),\n\t284:   (\"PlanarConfiguration\", [1], 1, \"How the components of each pixel are stored\"),\n\t288:   (\"FreeOffsets\", [4], None, \"For each string of contiguous unused bytes in a TIFF file, the byte offset of the string\"),\n\t289:   (\"FreeByteCounts\", [4], None, \"For each string of contiguous unused bytes in a TIFF file, the number of bytes in the string\"),\n\t290:   (\"GrayResponseUnit\", [1], 2, \"The precision of the information contained in the GrayResponseCurve\"),\n\t291:   (\"GrayResponseCurve\", [1], 2, \"Array size = 2**SamplesPerPixel\"),\n\t296:   (\"ResolutionUnit\", [4], 2, \"The unit of measurement for XResolution and YResolution\"),\n\t305:   (\"Software\", [2], None, \"Name and version number of the software package(s) used to create the image\"),\n\t306:   (\"DateTime\", [2], None, \"Date and time of image creation, aray size = 20, 'YYYY:MM:DD HH:MM:SS\\0'\"),\n\t315:   (\"Artist\", [2], None, \"Person who created the image\"),\n\t316:   (\"HostComputer\", [2], None, \"The computer and/or operating system in use at the time of image creation\"),\n\t320:   (\"ColorMap\", [1], None, \"A color map for palette color images\"),\n\t338:   (\"ExtraSamples\", [1], 1, \"Description of extra components\"),\n\t33432: (\"Copyright\", [2], None, \"Copyright notice\"),\n}\n\n# Extension TIFF tags\nxTT = {\n\t269:   (\"DocumentName\", [2], None, \"The name of the document from which this image was scanned\"),\n\t285:   (\"PageName\", [2], None, \"The name of the page from which this image was scanned\"),\n\t286:   (\"XPosition\", [5], None, \"X position of the image\"),\n\t287:   (\"YPosition\", [5], None, \"Y position of the image\"),\n\t292:   (\"T4Options\", [4], 0, \"Options for Group 3 Fax compression\"),\n\t293:   (\"T6Options\", [4], 0, \"Options for Group 6 Fax compression\"),\n\t297:   (\"PageNumber\", [1], None, \"The page number of the page from which this image was scanned\"),\n\t301:   (\"TransferFunction\", [1], 1*(1<<1), \"Describes a transfer function for the image in tabular style\"),\n\t317:   (\"Predictor\", [3], 1, \"A mathematical operator that is applied to the image data before an encoding scheme is applied\"),\n\t318:   (\"WhitePoint\", [5], None, \"The chromaticity of the white point of the image\"),\n\t319:   (\"PrimaryChromaticies\", [5], None, \"The chromaticities of the primaries of the image\"),\n\t321:   (\"HalftoneHints\", [1], None, \"Conveys to the halftone function the range of gray levels within a colorimetrically-specified image that should retain tonal detail\"),\n\t322:   (\"TileWidth\", [1,4], None, \"The tile width in pixels This is the number of columns in each tile\"),\n\t323:   (\"TileLength\", [1,4], None, \"The tile length (height) in pixels This is the number of rows in each tile\"),\n\t324:   (\"TileOffsets\", [4], None, \"For each tile, the byte offset of that tile, as compressed and stored on disk\"),\n\t325:   (\"TileByteCounts\", [1,4], None, \"For each tile, the number of (compressed) bytes in that tile\"),\n\t326:   (\"BadFaxLinea\", [1,4], None, \"Used in the TIFF-F standard, denotes the number of 'bad' scan lines encountered by the facsimile device\"),\n\t327:   (\"CleanFaxData\", [1], None, \"Used in the TIFF-F standard, indicates if 'bad' lines encountered during reception are stored in the data, or if 'bad' lines have been replaced by the receiver\"),\n\t328:   (\"ConsecutiveBadFaxLines\", [1,4], None, \"Used in the TIFF-F standard, denotes the maximum number of consecutive 'bad' scanlines received\"),\n\t328:   (\"SubIFDs\", [2,4], None, \"Offset to child IFDs\"), # ???\n\t332:   (\"InkSet\", [1], None, \"The set of inks used in a separated (PhotometricInterpretation=5) image\"),\n\t333:   (\"InkNames\", [2], None, \"The name of each ink used in a separated image\"),\n\t334:   (\"NumberOfInks\", [1], 4, \"The number of inks\"),\n\t336:   (\"DotRange\", [1,3], (0,1), \"The component values that correspond to a 0%% dot and 100%% dot\"),\n\t337:   (\"TargetPrinter\", [2], None, \"A description of the printing environment for which this separation is intended\"),\n\t339:   (\"SampleFormat\", [1], 1, \"Specifies how to interpret each data sample in a pixel\"),\n\t340:   (\"SMinSampleValue\", [3,7,8,12], None, \"Specifies the minimum sample value\"),\n\t341:   (\"SMaxSampleValue\", [3,7,8,12], None, \"Specifies the maximum sample value\"),\n\t342:   (\"TransferRange\", [1], None, \"Expands the range of the TransferFunction\"),\n\t343:   (\"ClipPath\", [3], None, \"Mirrors the essentials of PostScript's path creation functionality\"),\n\t344:   (\"XClipPathUnits\", [4], None, \"The number of units that span the width of the image, in terms of integer ClipPath coordinates\"),\n\t345:   (\"YClipPathUnits\", [4], None, \"The number of units that span the height of the image, in terms of integer ClipPath coordinates\"),\n\t346:   (\"Indexed\", [1], 0, \"Aims to broaden the support for indexed images to include support for any color space\"),\n\t347:   (\"JPEGTables\", [7], None, \"JPEG quantization and/or Huffman tables\"),\n\t351:   (\"OPIProxy\", [1], 0, \"OPI-related\"),\n\t400:   (\"GlobalParametersIFD\", [2,4], None, \"Used in the TIFF-FX standard to point to an IFD containing tags that are globally applicable to the complete TIFF file\"),\n\t401:   (\"ProfileType\", [4], None, \"Used in the TIFF-FX standard, denotes the type of data stored in this file or IFD\"),\n\t402:   (\"FaxProfile\", [3], None, \"Used in the TIFF-FX standard, denotes the 'profile' that applies to this file\"),\n\t403:   (\"CodingMethods\", [4], None, \"Used in the TIFF-FX standard, indicates which coding methods are used in the file\"),\n\t404:   (\"VersionYear\", [3], None, \"Used in the TIFF-FX standard, denotes the year of the standard specified by the FaxProfile field\"),\n\t405:   (\"ModeNumber\", [3], None, \"Used in the TIFF-FX standard, denotes the mode of the standard specified by the FaxProfile field\"),\n\t433:   (\"Decode\", [10],None, \"Used in the TIFF-F and TIFF-FX standards, holds information about the ITULAB (PhotometricInterpretation = 10) encoding\"),\n\t434:   (\"DefaultImageColor\", [1], None, \"Defined in the Mixed Raster Content part of RFC 2301, is the default color needed in areas where no image is available\"),\n\t512:   (\"JPEGProc\", [1], None, \"Old-style JPEG compression field TechNote2 invalidates this part of the specification\"),\n\t513:   (\"JPEGInterchangeFormat\", [4], None, \"Old-style JPEG compression field TechNote2 invalidates this part of the specification\"),\n\t514:   (\"JPEGInterchangeFormatLength\", [4], None, \"Old-style JPEG compression field TechNote2 invalidates this part of the specification\"),\n\t515:   (\"JPEGRestartInterval\", [1], None, \"Old-style JPEG compression field TechNote2 invalidates this part of the specification\"),\n\t517:   (\"JPEGLosslessPredictors\", [1], None, \"Old-style JPEG compression field TechNote2 invalidates this part of the specification\"),\n\t518:   (\"JPEGPointTransforms\", [1], None, \"Old-style JPEG compression field TechNote2 invalidates this part of the specification\"),\n\t519:   (\"JPEGQTables\", [4], None, \"Old-style JPEG compression field TechNote2 invalidates this part of the specification\"),\n\t520:   (\"JPEGDCTables\", [4], None, \"Old-style JPEG compression field TechNote2 invalidates this part of the specificationl\"),\n\t521:   (\"JPEGACTables\", [4], None, \"Old-style JPEG compression field TechNote2 invalidates this part of the specification\"),\n\t529:   (\"YCbCrCoefficients\", [5], (299,1000,587,1000,114,1000), \"The transformation from RGB to YCbCr image data\"),\n\t530:   (\"YCbCrSubSampling\", [1], (2,2), \"Specifies the subsampling factors used for the chrominance components of a YCbCr image\"),\n\t531:   (\"YCbCrPositioning\", [1], 1, \"Specifies the positioning of subsampled chrominance components relative to luminance samples\"),\n\t532:   (\"ReferenceBlackWhite\", [5], (0,1,1,1,0,1,1,1,0,1,1,1), \"Specifies a pair of headroom and footroom image data values (codes) for each pixel component\"),\n\t559:   (\"StripRowCounts\", [4], None, \"Defined in the Mixed Raster Content part of RFC 2301, used to replace RowsPerStrip for IFDs with variable-sized strips\"),\n\t700:   (\"XMP\", [3], None, \"XML packet containing XMP metadata\"),\n\t32781: (\"ImageID\", [2], None, \"OPI-related\"),\n\t34732: (\"ImageLayer\", [1,4], None, \"Defined in the Mixed Raster Content part of RFC 2301, used to denote the particular function of this Image in the mixed raster scheme\"),\n}\n\n# Private TIFF tags\npTT = {\n\t32932:  (\"Wang Annotation\", [3], None, \"Annotation data, as used in 'Imaging for Windows'\"),\n\t33445:  (\"MD FileTag\", [4], None, \"Specifies the pixel data format encoding in the Molecular Dynamics GEL file format\"),\n\t33446:  (\"MD ScalePixel\", [5], None, \"Specifies a scale factor in the Molecular Dynamics GEL file format\"),\n\t33447:  (\"MD ColorTable\", [1], None, \"Used to specify the conversion from 16bit to 8bit in the Molecular Dynamics GEL file format\"),\n\t33448:  (\"MD LabName\", [2], None, \"Name of the lab that scanned this file, as used in the Molecular Dynamics GEL file format\"),\n\t33449:  (\"MD SampleInfo\", [2], None, \"Information about the sample, as used in the Molecular Dynamics GEL file format\"),\n\t33450:  (\"MD PrepDate\", [2], None, \"Date the sample was prepared, as used in the Molecular Dynamics GEL file format\"),\n\t33451:  (\"MD PrepTime\", [2], None, \"Time the sample was prepared, as used in the Molecular Dynamics GEL file format\"),\n\t33452:  (\"MD FileUnits\", [2], None, \"Units for data in this file, as used in the Molecular Dynamics GEL file format\"),\n\t33550:  (\"ModelPixelScaleTag\", [12], None, \"Used in interchangeable GeoTIFF files\"),\n\t33723:  (\"IPTC\", [3,7], None, \"IPTC (International Press Telecommunications Council) metadata\"),\n\t33918:  (\"INGR Packet Data Tag\", [3], None, \"Intergraph Application specific storage\"),\n\t33919:  (\"INGR Flag Registers\", [4], None, \"Intergraph Application specific flags\"),\n\t33920:  (\"IrasB Transformation Matrix\", [12], None, \"Originally part of Intergraph's GeoTIFF tags, but likely understood by IrasB only\"),\n\t33922:  (\"ModelTiepointTag\", [12], None, \"Originally part of Intergraph's GeoTIFF tags, but now used in interchangeable GeoTIFF files\"),\n\t34264:  (\"ModelTransformationTag\", [12], None, \"Used in interchangeable GeoTIFF files\"),\n\t34377:  (\"Photoshop\", [1], None, \"Collection of Photoshop 'Image Resource Blocks'\"),\n\t34665:  (\"Exif IFD\", [4], None, \"A pointer to the Exif IFD\"),\n\t34675:  (\"ICC Profile\", [7], None, \"ICC profile data\"),\n\t34735:  (\"GeoKeyDirectoryTag\", [3], None, \"Used in interchangeable GeoTIFF files\"),\n\t34736:  (\"GeoDoubleParamsTag\", [12], None, \"Used in interchangeable GeoTIFF files\"),\n\t34737:  (\"GeoAsciiParamsTag\", [2], None, \"Used in interchangeable GeoTIFF files\"),\n\t34853:  (\"GPS IFD\", [4], None, \"A pointer to the Exif-related GPS Info IFD\"),\n\t34908:  (\"HylaFAX FaxRecvParams\", [4], None, \"Used by HylaFAX\"),\n\t34909:  (\"HylaFAX FaxSubAddress\", [2], None, \"Used by HylaFAX\"),\n\t34910:  (\"HylaFAX FaxRecvTime\", [4], None, \"Used by HylaFAX\"),\n\t37724:  (\"ImageSourceData\", [7], None, \"Used by Adobe Photoshop\"),\n\t40965:  (\"Interoperability IFD\", [4], None, \"A pointer to the Exif-related Interoperability IFD\"),\n\t42112:  (\"GDAL_METADATA\", [2], None, \"Used by the GDAL library, holds an XML list of name=value 'metadata' values about the image as a whole, and about specific samples\"),\n\t42113:  (\"GDAL_NODATA\", [2], None, \"Used by the GDAL library, contains an ASCII encoded nodata or background pixel value\"),\n\t50215:  (\"Oce Scanjob Description\", [2], None, \"Used in the Oce scanning process\"),\n\t50216:  (\"Oce Application Selector\", [2], None, \"Used in the Oce scanning process\"),\n\t50217:  (\"Oce Identification Number\", [2], None, \"Used in the Oce scanning process\"),\n\t50218:  (\"Oce ImageLogic Characteristics\", [2], None, \"Used in the Oce scanning process\"),\n\t50706:  (\"DNGVersion\", [3], None, \"Used in IFD 0 of DNG files\"),\n\t50707:  (\"DNGBackwardVersion\", [3], None, \"Used in IFD 0 of DNG files\"),\n\t50708:  (\"UniqueCameraModel\", [2], None, \"Used in IFD 0 of DNG files\"),\n\t50709:  (\"LocalizedCameraModel\", [2,3], None, \"Used in IFD 0 of DNG files\"),\n\t50710:  (\"CFAPlaneColor\", [3], None, \"Used in Raw IFD of DNG files\"),\n\t50711:  (\"CFALayout\", [1], None, \"Used in Raw IFD of DNG files\"),\n\t50712:  (\"LinearizationTable\", [1], None, \"Used in Raw IFD of DNG files\"),\n\t50713:  (\"BlackLevelRepeatDim\", [1], None, \"Used in Raw IFD of DNG files\"),\n\t50714:  (\"BlackLevel\", [1,4,5], None, \"Used in Raw IFD of DNG files\"),\n\t50715:  (\"BlackLevelDeltaH\", [10], None, \"Used in Raw IFD of DNG files\"),\n\t50716:  (\"BlackLevelDeltaV\", [10], None, \"Used in Raw IFD of DNG files\"),\n\t50717:  (\"WhiteLevel\", [1,4], None, \"Used in Raw IFD of DNG files\"),\n\t50718:  (\"DefaultScale\", [5], None, \"Used in Raw IFD of DNG files\"),\n\t50719:  (\"DefaultCropOrigin\", [1,4,5], None, \"Used in Raw IFD of DNG files\"),\n\t50720:  (\"DefaultCropSize\", [1,4,5], None, \"Used in Raw IFD of DNG files\"),\n\t50721:  (\"ColorMatrix1\", [10], None, \"Used in IFD 0 of DNG files\"),\n\t50722:  (\"ColorMatrix2\", [10], None, \"Used in IFD 0 of DNG files\"),\n\t50723:  (\"CameraCalibration1\", [10], None, \"Used in IFD 0 of DNG files\"),\n\t50724:  (\"CameraCalibration2\", [10], None, \"Used in IFD 0 of DNG files\"),\n\t50725:  (\"ReductionMatrix1\", [10], None, \"Used in IFD 0 of DNG files\"),\n\t50726:  (\"ReductionMatrix2\", [10], None, \"Used in IFD 0 of DNG files\"),\n\t50727:  (\"AnalogBalance\", [5], None, \"Used in IFD 0 of DNG files\"),\n\t50728:  (\"AsShotNeutral\", [1,5], None, \"Used in IFD 0 of DNG files\"),\n\t50729:  (\"AsShotWhiteXY\", [5], None, \"Used in IFD 0 of DNG files\"),\n\t50730:  (\"BaselineExposure\", [10], None, \"Used in IFD 0 of DNG files\"),\n\t50731:  (\"BaselineNoise\", [10], None, \"Used in IFD 0 of DNG files\"),\n\t50732:  (\"BaselineSharpness\", [10], None, \"Used in IFD 0 of DNG files\"),\n\t50733:  (\"BayerGreenSplit\", [4], None, \"Used in Raw IFD of DNG files\"),\n\t50734:  (\"LinearResponseLimit\", [5], None, \"Used in IFD 0 of DNG files\"),\n\t50735:  (\"CameraSerialNumber\", [2], None, \"Used in IFD 0 of DNG files\"),\n\t50736:  (\"LensInfo\", [5], None, \"Used in IFD 0 of DNG files\"),\n\t50737:  (\"ChromaBlurRadius\", [5], None, \"Used in Raw IFD of DNG files\"),\n\t50738:  (\"AntiAliasStrength\", [5], None, \"Used in Raw IFD of DNG files\"),\n\t50740:  (\"DNGPrivateData\", [3], None, \"Used in IFD 0 of DNG files\"),\n\t50741:  (\"MakerNoteSafety\", [1], None, \"Used in IFD 0 of DNG files\"),\n\t50778:  (\"CalibrationIlluminant1\", [1], None, \"Used in IFD 0 of DNG files\"),\n\t50779:  (\"CalibrationIlluminant2\", [1], None, \"Used in IFD 0 of DNG files\"),\n\t50780:  (\"BestQualityScale\", [5], None, \"Used in Raw IFD of DNG files\"),\n\t50784:  (\"Alias Layer Metadata\", [2], None, \"Alias Sketchbook Pro layer usage description\"),\n\t# XP tags\n\t0x9c9b: (\"XPTitle\", [4], None, \"\"),\n\t0x9c9c: (\"XPComment\", [4], None, \"\"),\n\t0x9c9d: (\"XPAuthor\", [4], None, \"\"),\n\t0x9c9e: (\"XPKeywords\", [4], None, \"\"),\n\t0x9c9f: (\"XPSubject\", [4], None, \"\"),\n\t0xea1c: (\"Padding\", [7], None, \"\"),\n\t0xea1d: (\"OffsetSchema\", [9], None, \"\"),\n}\n\nexfT = {\n\t33434: (\"ExposureTime\", [5], None, \"Exposure time, given in seconds\"),\n\t33437: (\"FNumber\", [5], None, \"The F number\"),\n\t34850: (\"ExposureProgram\", [1], 0, \"The class of the program used by the camera to set exposure when the picture is taken\"),\n\t34852: (\"SpectralSensitivity\", [2], None, \"Indicates the spectral sensitivity of each channel of the camera used\"),\n\t34855: (\"ISOSpeedRatings\", [1], None, \"Indicates the ISO Speed and ISO Latitude of the camera or input device as specified in ISO 12232\"),\n\t34856: (\"OECF\", [7], None, \"Indicates the Opto-Electric Conversion Function (OECF) specified in ISO 14524\"),\n\t36864: (\"ExifVersion\", [7], b\"0220\", \"The version of the supported Exif standard\"),\n\t36867: (\"DateTimeOriginal\", [2], None, \"The date and time when the original image data was generated\"),\n\t36868: (\"DateTimeDigitized\", [2], None, \"The date and time when the image was stored as digital data\"),\n\t37121: (\"ComponentsConfiguration\", [7], None, \"Specific to compressed data; specifies the channels and complements PhotometricInterpretation\"),\n\t37122: (\"CompressedBitsPerPixel\", [5], None, \"Specific to compressed data; states the compressed bits per pixel\"),\n\t37377: (\"ShutterSpeedValue\", [11], None, \"Shutter speed\"),\n\t37378: (\"ApertureValue\", [5], None, \"The lens aperture\"),\n\t37379: (\"BrightnessValue\", [5], None, \"The value of brightness\"),\n\t37380: (\"ExposureBiasValue\", [11], None, \"The exposure bias\"),\n\t37381: (\"MaxApertureValue\", [5], None, \"The smallest F number of the lens\"),\n\t37382: (\"SubjectDistance\", [5], None, \"The distance to the subject, given in meters\"),\n\t37383: (\"MeteringMode\", [1], 0, \"The metering mode\"),\n\t37384: (\"LightSource\", [1], 0, \"The kind of light source\"),\n\t37385: (\"Flash\", [1], None, \"Indicates the status of flash when the image was shot\"),\n\t37386: (\"FocalLength\", [5], None, \"The actual focal length of the lens, in mm\"),\n\t37396: (\"SubjectArea\", [1], None, \"Indicates the location and area of the main subject in the overall scene\"),\n\t37500: (\"MakerNote\", [7], None, \"Manufacturer specific information\"),\n\t37510: (\"UserComment\", [7], None, \"Keywords or comments on the image; complements ImageDescription\"),\n\t37520: (\"SubsecTime\", [2], None, \"A tag used to record fractions of seconds for the DateTime tag\"),\n\t37521: (\"SubsecTimeOriginal\", [2], None, \"A tag used to record fractions of seconds for the DateTimeOriginal tag\"),\n\t37522: (\"SubsecTimeDigitized\", [2], None, \"A tag used to record fractions of seconds for the DateTimeDigitized tag\"),\n\t40960: (\"FlashpixVersion\", [7], b\"0100\", \"The Flashpix format version supported by a FPXR file\"),\n\t40961: (\"ColorSpace\", [1], None, \"The color space information tag is always recorded as the color space specifier\"),\n\t40962: (\"PixelXDimension\", [1,4], None, \"Specific to compressed data; the valid width of the meaningful image\"),\n\t40963: (\"PixelYDimension\", [1,4], None, \"Specific to compressed data; the valid height of the meaningful image\"),\n\t40964: (\"RelatedSoundFile\", [2], None, \"Used to record the name of an audio file related to the image data\"),\n\t41483: (\"FlashEnergy\", [5], None, \"Indicates the strobe energy at the time the image is captured, as measured in Beam Candle Power Seconds\"),\n\t41484: (\"SpatialFrequencyResponse\", [7], None, \"Records the camera or input device spatial frequency table and SFR values in the direction of image width, image height, and diagonal direction, as specified in ISO 12233\"),\n\t41486: (\"FocalPlaneXResolution\", [5], None, \"Indicates the number of pixels in the image width (X) direction per FocalPlaneResolutionUnit on the camera focal plane\"),\n\t41487: (\"FocalPlaneYResolution\", [5], None, \"Indicates the number of pixels in the image height (Y) direction per FocalPlaneResolutionUnit on the camera focal plane\"),\n\t41488: (\"FocalPlaneResolutionUnit\", [1], 2, \"Indicates the unit for measuring FocalPlaneXResolution and FocalPlaneYResolution\"),\n\t41492: (\"SubjectLocation\", [2], None, \"Indicates the location of the main subject in the scene\"),\n\t41493: (\"ExposureIndex\", [5], None, \"Indicates the exposure index selected on the camera or input device at the time the image is captured\"),\n\t41495: (\"SensingMethod\", [1], None, \"Indicates the image sensor type on the camera or input device\"),\n\t41728: (\"FileSource\", [7], b\"3\", \"Indicates the image source\"),\n\t41729: (\"SceneType\", [7], b\"1\", \"Indicates the type of scene\"),\n\t41730: (\"CFAPattern\", [7], None, \"Indicates the color filter array (CFA) geometric pattern of the image sensor when a one-chip color area sensor is used\"),\n\t41985: (\"CustomRendered\", [1], 0, \"Indicates the use of special processing on image data, such as rendering geared to output\"),\n\t41986: (\"ExposureMode\", [1], None, \"Indicates the exposure mode set when the image was shot\"),\n\t41987: (\"WhiteBalance\", [1], None, \"Indicates the white balance mode set when the image was shot\"),\n\t41988: (\"DigitalZoomRatio\", [5], None, \"Indicates the digital zoom ratio when the image was shot\"),\n\t41989: (\"FocalLengthIn35mmFilm\", [1], None, \"Indicates the equivalent focal length assuming a 35mm film camera, in mm\"),\n\t41990: (\"SceneCaptureType\", [1], 0, \"Indicates the type of scene that was shot\"),\n\t41991: (\"GainControl\", [1], None, \"Indicates the degree of overall image gain adjustment\"),\n\t41992: (\"Contrast\", [1], 0, \"Indicates the direction of contrast processing applied by the camera when the image was shot\"),\n\t41993: (\"Saturation\", [1], 0, \"Indicates the direction of saturation processing applied by the camera when the image was shot\"),\n\t41994: (\"Sharpness\", [1], 0, \"Indicates the direction of sharpness processing applied by the camera when the image was shot\"),\n\t41995: (\"DeviceSettingDescription\", [7], None, \"This tag indicates information on the picture-taking conditions of a particular camera model\"),\n\t41996: (\"SubjectDistanceRange\", [1], None, \"Indicates the distance to the subject\"),\n\t42016: (\"ImageUniqueID\", [2], None, \"Indicates an identifier assigned uniquely to each image\"),\n}\n\ngpsT = {\n\t0:  (\"GPSVersionID\", [3], (2,2,0,0), \"Indicates the version of GPSInfoIFD\"),\n\t1:  (\"GPSLatitudeRef\", [2], None, \"Indicates whether the latitude is north or south latitude\"),\n\t2:  (\"GPSLatitude\", [5], None, \"Indicates the latitude\"),\n\t3:  (\"GPSLongitudeRef\", [2], None, \"Indicates whether the longitude is east or west longitude\"),\n\t4:  (\"GPSLongitude\", [5], None, \"Indicates the longitude\"),\n\t5:  (\"GPSAltitudeRef\", [3], None, \"Indicates the altitude used as the reference altitude\"),\n\t6:  (\"GPSAltitude\", [5], None, \"Indicates the altitude based on the reference in GPSAltitudeRef\"),\n\t7:  (\"GPSTimeStamp\", [5], None, \"Indicates the time as UTC (Coordinated Universal Time)\"),\n\t8:  (\"GPSSatellites\", [2], None, \"Indicates the GPS satellites used for measurements\"),\n\t9:  (\"GPSStatus\", [2], None, \"Indicates the status of the GPS receiver when the image is recorded\"),\n\t10: (\"GPSMeasureMode\", [2], None, \"Indicates the GPS measurement mode\"),\n\t11: (\"GPSDOP\", [5], None, \"Indicates the GPS DOP (data degree of precision)\"),\n\t12: (\"GPSSpeedRef\", [2], b'K\\x00', \"Indicates the unit used to express the GPS receiver speed of movement\"),\n\t13: (\"GPSSpeed\", [5], None, \"Indicates the speed of GPS receiver movem5nt\"),\n\t14: (\"GPSTrackRef\", [2], b'T\\x00', \"Indicates the reference for giving the direction of GPS receiver movement\"),\n\t15: (\"GPSTrack\", [5], None, \"Indicates the direction of GPS receiver movement\"),\n\t16: (\"GPSImgDirectionRef\", [2], b'T\\x00', \"Indicates the reference for giving the direction of the image when it is captured\"),\n\t17: (\"GPSImgDirection\", [5], None, \"Indicates the direction of the image when it was captured\"),\n\t18: (\"GPSMapDatum\", [2], None, \"Indicates the geodetic survey data used by the GPS receiver\"),\n\t19: (\"GPSDestLatitudeRef\", [2], None, \"Indicates whether the latitude of the destination point is north or south latitude\"),\n\t20: (\"GPSDestLatitude\", [5], None, \"Indicates the latitude of the destination point\"),\n\t21: (\"GPSDestLongitudeRef\", [2], None, \"Indicates whether the longitude of the destination point is east or west longitude\"),\n\t22: (\"GPSDestLongitude\", [5], None, \"Indicates the longitude of the destination point\"),\n\t23: (\"GPSDestBearingRef\", [2], None, \"Indicates the reference used for giving the bearing to the destination point\"),\n\t24: (\"GPSDestBearing\", [5], None, \"Indicates the bearing to the destination point\"),\n\t25: (\"GPSDestDistanceRef\", [2], None, \"Indicates the unit used to express the distance to the destination point\"),\n\t26: (\"GPSDestDistance\", [5], None, \"Indicates the distance to the destination point\"),\n\t27: (\"GPSProcessingMethod\", [7], None, \"A character string recording the name of the method used for location finding\"),\n\t28: (\"GPSAreaInformation\", [7], None, \"A character string recording the name of the GPS area\"),\n\t29: (\"GPSDateStamp\", [2], None, \"A character string recording date and time information relative to UTC (Coordinated Universal Time)\"),\n\t30: (\"GPSDifferential\", [1], None, \"Indicates whether differential correction is applied to the GPS receiver\"),\n}\n\n_TAG_FAMILIES = [bTT, xTT, pTT, exfT, gpsT]\n_TAG_FAMILIES_2TAG = [dict((v[0], t) for t,v in dic.items()) for dic in _TAG_FAMILIES]\n_TAG_FAMILIES_2KEY = [dict((v, k) for k,v in dic.items()) for dic in _TAG_FAMILIES_2TAG]\n\ndef get(tag):\n\tidx = 0\n\tfor dic in _TAG_FAMILIES:\n\t\tif isinstance(tag, (bytes, str)):\n\t\t\ttag = _TAG_FAMILIES_2TAG[idx][tag]\n\t\tif tag in dic:\n\t\t\treturn dic[tag]\n\treturn (\"Unknown\", [4], None, \"Undefined tag 0x%x\"%tag)\n\ndef _2tag(tag, family=None):\n\tif family != None:\n\t\tidx = _TAG_FAMILIES.index(family)\n\t\tif isinstance(tag, (bytes, str)):\n\t\t\tif tag in _TAG_FAMILIES_2TAG[idx]:\n\t\t\t\treturn _TAG_FAMILIES_2TAG[idx][tag]\n\t\t\treturn tag\n\t\telse:\n\t\t\treturn tag\n\telif isinstance(tag, (bytes, str)):\n\t\tfor dic in _TAG_FAMILIES_2TAG:\n\t\t\tif tag in dic:\n\t\t\t\treturn dic[tag]\n\t\treturn tag\n\telse:\n\t\treturn tag\n"
  },
  {
    "path": "core/lib/Tyf/values.py",
    "content": "# -*- encoding:utf-8 -*-\n# Copyright 2012-2015, THOORENS Bruno - http://bruno.thoorens.free.fr/licences/tyf.html\n\n## Tiff tag values\nNewSubfileType = {\n\t0: \"bit flag 000\",\n\t1: \"bit flag 001\",\n\t2: \"bit flag 010\",\n\t3: \"bit flag 011\",\n\t4: \"bit flag 100\",\n\t5: \"bit flag 101\",\n\t6: \"bit flag 110\",\n\t7: \"bit flag 111\"\n}\n\nSubfileType = {\n\t1: \"Full-resolution image data\",\n\t2: \"Reduced-resolution image data\",\n\t3: \"Single page of a multi-page image\"\n}\n\nCompression = {\n\t1:     \"Uncompressed\",\n\t2:     \"CCITT 1d\",\n\t3:     \"Group 3 Fax\",\n\t4:     \"Group 4 Fax\",\n\t5:     \"LZW\",\n\t6:     \"JPEG\",\n\t7:     \"JPEG ('new-style' JPEG)\",\n\t8:     \"Deflate ('Adobe-style')\",\n\t9:     \"TIFF-F and TIFF-FX standard (RFC 2301) B&W\",\n\t10:    \"TIFF-F and TIFF-FX standard (RFC 2301) RGB\",\n\t32771: \"CCITTRLEW\",  # 16-bit padding\n\t32773: \"PACKBITS\",\n\t32809: \"THUNDERSCAN\",\n\t32895: \"IT8CTPAD\",\n\t32896: \"IT8LW\",\n\t32897: \"IT8MP\",\n\t32908: \"PIXARFILM\",\n\t32909: \"PIXARLOG\",\n\t32946: \"DEFLATE\",\n\t32947: \"DCS\",\n\t34661: \"JBIG\",\n\t34676: \"SGILOG\",\n\t34677: \"SGILOG24\",\n\t34712: \"JP2000\",\n}\n\nPhotometricInterpretation = {\n\t0:     \"WhiteIsZero\",\n\t1:     \"BlackIsZero\",\n\t2:     \"RGB\",\n\t3:     \"RGB Palette\",\n\t4:     \"Transparency Mask\",\n\t5:     \"CMYK\",\n\t6:     \"YCbCr\",\n\t8:     \"CIE L*a*b*\",\n\t9:     \"ICC L*a*b*\",\n\t10:    \"ITU L*a*b*\",\n\t32803: \"CFA\",       # TIFF/EP, Adobe DNG\n\t32892: \"LinearRaw\"  # Adobe DNG\n}\n\nThresholding = {\n\t1: \"No dithering or halftoning has been applied to the image data\",\n\t2: \"An ordered dither or halftone technique has been applied to the image data\",\n\t3: \"A randomized process such as error diffusion has been applied to the image data\"\n}\n\nFillOrder = {\n\t1: \"Values stored in the higher-order bits of the byte\",\n\t2: \"Values stored in the lower-order bits of the byte\"\n}\n\nOrientation = {\n#   1        2       3      4         5            6           7          8\n# 888888  888888      88  88      8888888888  88                  88  8888888888\n# 88          88      88  88      88  88      88  88          88  88      88  88\n# 8888      8888    8888  8888    88          8888888888  8888888888          88\n# 88          88      88  88\n# 88          88  888888  888888\n\t1: \"Normal\",\n\t2: \"Fliped left to right\",\n\t3: \"Rotated 180 deg\",\n\t4: \"Fliped top to bottom\",\n\t5: \"Fliped left to right + rotated 90 deg counter clockwise\",\n\t6: \"Rotated 90 deg counter clockwise\",\n\t7: \"Fliped left to right + rotated 90 deg clockwise\",\n\t8: \"Rotated 90 deg clockwise\"\n}\n\nPlanarConfiguration = {\n\t1: \"Chunky\", #, format: RGBARGBARGBA....RGBA \n\t2: \"Planar\"  #, format: RRR.RGGG.GBBB.BAAA.A\n}\n\nGrayResponseUnit = {\n\t1: \"Number represents tenths of a unit\",\n\t2: \"Number represents hundredths of a unit\",\n\t3: \"Number represents thousandths of a unit\",\n\t4: \"Number represents ten-thousandths of a unit\",\n\t5: \"Number represents hundred-thousandths of a unit\"\n}\n\nResolutionUnit = {\n\t1:\"No unit\",\n\t2:\"Inch\",\n\t3:\"Centimeter\"\n}\n\nT4Options = {\n\t0: \"bit flag 000\",\n\t1: \"bit flag 001\",\n\t2: \"bit flag 010\",\n\t3: \"bit flag 011\",\n\t4: \"bit flag 100\",\n\t5: \"bit flag 101\",\n\t6: \"bit flag 110\",\n\t7: \"bit flag 111\"\n}\n\nT6Options = {\n\t0: \"bit flag 00\",\n\t2: \"bit flag 10\",\n}\n\nPredictor = {\n\t1: \"No prediction\",\n\t2: \"Horizontal differencing\",\n\t3: \"Floating point horizontal differencing\"\n}\n\nCleanFaxData = {\n\t0: \"No 'bad' lines\",\n\t1: \"'bad' lines exist, but were regenerated by the receiver\",\n\t2: \"'bad' lines exist, but have not been regenerated\"\n}\n\nInkSet = {\n\t1:\"CMYK\",\n\t2:\"Not CMYK\"\n}\n\nSampleFormat = {\n\t1: \"Unsigned integer data\",\n\t2: \"Two's complement signed integer data\",\n\t3: \"IEEE floating point data [IEEE]\",\n\t4: \"Undefined data format\"\n}\n\nIndexed = {\n\t0: \"Not indexed\",\n\t1: \"Indexed\"\n}\n\nOPIProxy = {\n\t0: \"A higher-resolution version of this image does not exist\",\n\t1: \"A higher-resolution version of this image exists, and the name of that image is found in the ImageID tag\"\n}\n\nProfileType = {\n\t0: \"Unspecified\",\n\t1: \"Group 3 fax\"\n}\n\nFaxProfile = {\n\t0: \"Does not conform to a profile defined for TIFF for facsimile\",\n\t1: \"Minimal black & white lossless, Profile S\",\n\t2: \"Extended black & white lossless, Profile F\",\n\t3: \"Lossless JBIG black & white, Profile J\",\n\t4: \"Lossy color and grayscale, Profile C\",\n\t5: \"Lossless color and grayscale, Profile L\",\n\t6: \"Mixed Raster Content, Profile M\"\n}\n\nCodingMethods = {\n\t0b1       : \"Unspecified compression\",\n\t0b10      : \"1-dimensional coding, ITU-T Rec. T.4 (MH - Modified Huffman)\",\n\t0b100     : \"2-dimensional coding, ITU-T Rec. T.4 (MR - Modified Read)\",\n\t0b1000    : \"2-dimensional coding, ITU-T Rec. T.6 (MMR - Modified MR)\",\n\t0b10000   : \"ITU-T Rec. T.82 coding, using ITU-T Rec. T.85 (JBIG)\",\n\t0b100000  : \"ITU-T Rec. T.81 (Baseline JPEG)\",\n\t0b1000000 : \"ITU-T Rec. T.82 coding, using ITU-T Rec. T.43 (JBIG color)\"\n}\n\nJPEGProc = {\n\t1:  \"Baseline sequential process\",\n\t14: \"Lossless process with Huffman coding\"\n}\n\nJPEGLosslessPredictors = {\n\t1: \"A\",\n\t2: \"B\",\n\t3: \"C\",\n\t4: \"A+B-C\",\n\t5: \"A+((B-C)/2)\",\n\t6: \"B+((A-C)/2)\",\n\t7: \"(A+B)/2\"\n}\n\nYCbCrSubSampling = {\n\t(0,1): \"YCbCrSubsampleHoriz : ImageWidth of this chroma image is equal to the ImageWidth of the associated luma image\",\n\t(0,2): \"YCbCrSubsampleHoriz : ImageWidth of this chroma image is half the ImageWidth of the associated luma image\",\n\t(0,4): \"YCbCrSubsampleHoriz : ImageWidth of this chroma image is one-quarter the ImageWidth of the associated luma image\",\n\t(1,1): \"YCbCrSubsampleVert : ImageLength (height) of this chroma image is equal to the ImageLength of the associated luma image\",\n\t(2,2): \"YCbCrSubsampleVert : ImageLength (height) of this chroma image is half the ImageLength of the associated luma image\",\n\t(4,4): \"YCbCrSubsampleVert : ImageLength (height) of this chroma image is one-quarter the ImageLength of the associated luma image\"\n}\n\nYCbCrPositioning = {\n\t1: \"Centered\", \n\t2: \"Co-sited\"\n}\n\n## EXIF tag values\nExposureProgram = {\n\t0: \"Not defined\",\n\t1: \"Manual\",\n\t2: \"Normal program\",\n\t3: \"Aperture priority\",\n\t4: \"Shutter priority\",\n\t5: \"Creative program (biased toward depth of field)\",\n\t6: \"Action program (biased toward fast shutter speed)\",\n\t7: \"Portrait mode (for closeup photos with the background out of focus)\",\n\t8: \"Landscape mode (for landscape photos with the background in focus)\"\n}\n\nMeteringMode = {\n\t0:   \"Unknown\",\n\t1:   \"Average\",\n\t2:   \"Center Weighted Average\",\n\t3:   \"Spot\",\n\t4:   \"MultiSpot\",\n\t5:   \"Pattern\",\n\t6:   \"Partial\",\n\t255: \"other\"\n}\n\nLightSource = {\n\t0:   \"Unknown\",\n\t1:   \"Daylight\",\n\t2:   \"Fluorescent\",\n\t3:   \"Tungsten (incandescent light)\",\n\t4:   \"Flash\",\n\t9:   \"Fine weather\",\n\t10:  \"Cloudy weather\",\n\t11:  \"Shade\",\n\t12:  \"Daylight fluorescent (D 5700 - 7100K)\",\n\t13:  \"Day white fluorescent (N 4600 - 5400K)\",\n\t14:  \"Cool white fluorescent (W 3900 - 4500K)\",\n\t15:  \"White fluorescent (WW 3200 - 3700K)\",\n\t17:  \"Standard light A\",\n\t18:  \"Standard light B\",\n\t19:  \"Standard light C\",\n\t20:  \"D55\",\n\t21:  \"D65\",\n\t22:  \"D75\",\n\t23:  \"D50\",\n\t24:  \"ISO studio tungsten\",\n\t255: \"Other light source\"\n}\n\nColorSpace = {\n\t1:     \"RGB\",\n\t65535: \"Uncalibrated\"\n}\n\nFlash = {\n\t0x0000: \"Flash did not fire\",\n\t0x0001: \"Flash fired\",\n\t0x0005: \"Strobe return light not detected\",\n\t0x0007: \"Strobe return light detected\",\n\t0x0008: \"On, did not fire\",\n\t0x0009: \"Flash fired, compulsory flash mode\",\n\t0x000D: \"Flash fired, compulsory flash mode, return light not detected\",\n\t0x000F: \"Flash fired, compulsory flash mode, return light detected\",\n\t0x0010: \"Flash did not fire, compulsory flash mode\",\n\t0x0014: \"Off, did not fire, return not detected\",\n\t0x0018: \"Flash did not fire, auto mode\",\n\t0x0019: \"Flash fired, auto mode\",\n\t0x001D: \"Flash fired, auto mode, return light not detected\",\n\t0x001F: \"Flash fired, auto mode, return light detected\",\n\t0x0020: \"No flash function\",\n\t0x0030: \"Off, no flash function\",\n\t0x0041: \"Flash fired, red-eye reduction mode\",\n\t0x0045: \"Flash fired, red-eye reduction mode, return light not detected\",\n\t0x0047: \"Flash fired, red-eye reduction mode, return light detected\",\n\t0x0049: \"Flash fired, compulsory flash mode, red-eye reduction mode\",\n\t0x004D: \"Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected\",\n\t0x004F: \"Flash fired, compulsory flash mode, red-eye reduction mode, return light detected\",\n\t0x0050: \"Off, red-eye reduction\",\n\t0x0058: \"Auto, Did not fire, red-eye reduction\",\n\t0x0059: \"Flash fired, auto mode, red-eye reduction mode\",\n\t0x005D: \"Flash fired, auto mode, return light not detected, red-eye reduction mode\",\n\t0x005F: \"Flash fired, auto mode, return light detected, red-eye reduction mode\"\n}\n\nFocalPlaneResolutionUnit = {\n\t1: \"No absolute unit of measurement\",\n\t2: \"Inch\",\n\t3: \"Centimeter\"\n}\n\nSensingMethod = {\n\t1: \"Not defined\",\n\t2: \"One-chip color area sensor\",\n\t3: \"Two-chip color area sensor\",\n\t4: \"Three-chip color area sensor\",\n\t5: \"Color sequential area sensor\",\n\t7: \"Trilinear sensor\",\n\t8: \"Color sequential linear sensor\"\n}\n\nCustomRendered = {\n\t0: \"Normal process\",\n\t1: \"Custom process\"\n}\n\nExposureMode = {\n\t0: \"Auto exposure\",\n\t1: \"Manual exposure\",\n\t2: \"Auto bracket\"\n}\n\nWhiteBalance = {\n\t0: \"Auto white balance\",\n\t1: \"Manual white balance\"\n}\n\nSceneCaptureType = {\n\t0: \"Standard\",\n\t1: \"Landscape\",\n\t2: \"Portrait\",\n\t3: \"Night scene\"\n}\n\nGainControl = {\n\t0: \"None\",\n\t1: \"Low gain up\",\n\t2: \"High gain up\",\n\t3: \"Low gain down\",\n\t4: \"High gain down\"\n}\n\nContrast = {\n\t0: \"Normal\",\n\t1: \"Soft\",\n\t2: \"Hard\"\n}\n\nSaturation = {\n\t0: \"Normal\",\n\t1: \"Low saturation\",\n\t2: \"High saturation\"\n}\n\nSharpness = Contrast\n\nSubjectDistanceRange = {\n\t0: \"Unknown\",\n\t1: \"Macro\",\n\t2: \"Close view\",\n\t3: \"Distant view\"\n}\n\n## GPS tag values\nGPSAltitudeRef = {\n\t0: \"Above sea level\",\n\t1: \"Below sea level\"\n}\n\nGPSMeasureMode = {\n\tb'2':   \"2-dimensional measurement\",\n\tb'3':   \"3-dimensional measurement\",\n\tb'2\\x00': \"2-dimensional measurement\",\n\tb'3\\x00': \"3-dimensional measurement\"\n}\n\nGPSSpeedRef = {\n\tb'K':     \"Kilometers per hour\",\n\tb'M':     \"Miles per hour\",\n\tb'N':     \"Knots\",\n\tb'K\\x00': \"Kilometers per hour\",\n\tb'M\\x00': \"Miles per hour\",\n\tb'N\\x00': \"Knots\"\n}\n\nGPSTrackRef = {\n\tb'T':     \"True direction\",\n\tb'M':     \"Magnetic direction\",\n\tb'T\\x00': \"True direction\",\n\tb'M\\x00': \"Magnetic direction\"\n}\n\nGPSImgDirectionRef = GPSTrackRef\n\nGPSLatitudeRef = {\n\tb'N':     \"North latitude\",\n\tb'S':     \"South latitude\",\n\tb'N\\x00': \"North latitude\",\n\tb'S\\x00': \"South latitude\"\n}\nGPSDestLatitudeRef = GPSLatitudeRef\n\nGPSLongitudeRef = {\n\tb'E':     \"East longitude\",\n\tb'W':     \"West longitude\",\n\tb'E\\x00': \"East longitude\",\n\tb'W\\x00': \"West longitude\"\n}\nGPSDestLongitudeRef = GPSLongitudeRef\n\nGPSDestBearingRef = GPSTrackRef\nGPSDestDistanceRef = GPSSpeedRef\n\nGPSDifferential = {\n\t0: \"Measurement without differential correction\",\n\t1: \"Differential correction applied\"\n}\n\n## Geotiff tag values\nGTModelTypeGeoKey = {\n\t0: \"Undefined\",\n\t1: \"Projection Coordinate System\",\n\t2: \"Geographic (latitude,longitude) System\",\n\t3: \"Geocentric (X,Y,Z) Coordinate System\",\n}\n\nGTRasterTypeGeoKey = {\n\t\t1: \"Raster pixel is area\",\n\t\t2: \"Raster pixel is point\",\n}\n\nProjCoordTransGeoKey = {\n\t1:  \"CT_TransverseMercator\",\n\t2:  \"CT_TransvMercator_Modified_Alaska\",\n\t3:  \"CT_ObliqueMercator\",\n\t4:  \"CT_ObliqueMercator_Laborde\",\n\t5:  \"CT_ObliqueMercator_Rosenmund\",\n\t6:  \"CT_ObliqueMercator_Spherical\",\n\t7:  \"CT_Mercator\",\n\t8:  \"CT_LambertConfConic_2SP\",\n\t9:  \"CT_LambertConfConic_Helmert\",\n\t10: \"CT_LambertAzimEqualArea\",\n\t11: \"CT_AlbersEqualArea\",\n\t12: \"CT_AzimuthalEquidistant\",\n\t13: \"CT_EquidistantConic\",\n\t14: \"CT_Stereographic\",\n\t15: \"CT_PolarStereographic\",\n\t16: \"CT_ObliqueStereographic\",\n\t17: \"CT_Equirectangular\",\n\t18: \"CT_CassiniSoldner\",\n\t19: \"CT_Gnomonic\",\n\t20: \"CT_MillerCylindrical\",\n\t21: \"CT_Orthographic\",\n\t22: \"CT_Polyconic\",\n\t23: \"CT_Robinson\",\n\t24: \"CT_Sinusoidal\",\n\t25: \"CT_VanDerGrinten\",\n\t26: \"CT_NewZealandMapGrid\",\n\t27: \"CT_TransvMercator_SouthOriented\",\n\t28: \"User-defined\",\n\t32767: \"User-defined\"\n}\n\nGeogPrimeMeridianGeoKey = {\n\t8901: \"Greenwich\",\n\t8902: \"Lisbon\",\n\t8903: \"Paris\",\n\t8904: \"Bogota\",\n\t8905: \"Madrid\",\n\t8906: \"Rome\",\n\t8907: \"Bern\",\n\t8908: \"Jakarta\",\n\t8909: \"Ferro\",\n\t8910: \"Brussels\",\n\t8911: \"Stockholm\",\n\t8912: \"Athens\",\n\t8913: \"Oslo\",\n\t8914: \"Paris RGS\"\n}\n\n\nGeogLinearUnitsGeoKey = {\n\t1025: \"millimetre\",\n\t1026: \"metres per second\",\n\t1027: \"millimetres per year\",\n\t1033: \"centimetre\",\n\t1034: \"centimetres per year\",\n\t1042: \"metres per year\",\n\t9001: \"metre\",\n\t9002: \"foot\",\n\t9003: \"US survey foot\",\n\t9005: \"Clarke's foot\",\n\t9014: \"fathom\",\n\t9030: \"nautical mile\",\n\t9031: \"German legal metre\",\n\t9033: \"US survey chain\",\n\t9034: \"US survey link\",\n\t9035: \"US survey mile\",\n\t9036: \"kilometre\",\n\t9037: \"Clarke's yard\",\n\t9038: \"Clarke's chain\",\n\t9039: \"Clarke's link\",\n\t9040: \"British yard (Sears 1922)\",\n\t9041: \"British foot (Sears 1922)\",\n\t9042: \"British chain (Sears 1922)\",\n\t9043: \"British link (Sears 1922)\",\n\t9050: \"British yard (Benoit 1895 A)\",\n\t9051: \"British foot (Benoit 1895 A)\",\n\t9052: \"British chain (Benoit 1895 A)\",\n\t9053: \"British link (Benoit 1895 A)\",\n\t9060: \"British yard (Benoit 1895 B)\",\n\t9061: \"British foot (Benoit 1895 B)\",\n\t9062: \"British chain (Benoit 1895 B)\",\n\t9063: \"British link (Benoit 1895 B)\",\n\t9070: \"British foot (1865)\",\n\t9080: \"Indian foot\",\n\t9081: \"Indian foot (1937)\",\n\t9082: \"Indian foot (1962)\",\n\t9083: \"Indian foot (1975)\",\n\t9084: \"Indian yard\",\n\t9085: \"Indian yard (1937)\",\n\t9086: \"Indian yard (1962)\",\n\t9087: \"Indian yard (1975)\",\n\t9093: \"Statute mile\",\n\t9094: \"Gold Coast foot\",\n\t9095: \"British foot (1936)\",\n\t9096: \"yard\",\n\t9097: \"chain\",\n\t9098: \"link\",\n\t9099: \"British yard (Sears 1922 truncated)\",\n\t9204: \"Bin width 330 US survey feet\",\n\t9205: \"Bin width 165 US survey feet\",\n\t9206: \"Bin width 82.5 US survey feet\",\n\t9207: \"Bin width 37.5 metres\",\n\t9208: \"Bin width 25 metres\",\n\t9209: \"Bin width 12.5 metres\",\n\t9210: \"Bin width 6.25 metres\",\n\t9211: \"Bin width 3.125 metres\",\n\t9300: \"British foot (Sears 1922 truncated)\",\n\t9301: \"British chain (Sears 1922 truncated)\",\n\t9302: \"British link (Sears 1922 truncated)\"\n}\n\n\nGeogAngularUnitsGeoKey = {\n\t1031: \"milliarc-second\",\n\t1032: \"milliarc-seconds per year\",\n\t1035: \"radians per second\",\n\t1043: \"arc-seconds per year\",\n\t9101: \"radian\",\n\t9102: \"degree\",\n\t9103: \"arc-minute\",\n\t9104: \"arc-second\",\n\t9105: \"grad\",\n\t9106: \"gon\",\n\t9107: \"degree minute second\",\n\t9108: \"degree minute second hemisphere\",\n\t9109: \"microradian\",\n\t9110: \"sexagesimal DMS\",\n\t9111: \"sexagesimal DM\",\n\t9112: \"centesimal minute\",\n\t9113: \"centesimal second\",\n\t9114: \"mil_6400\",\n\t9115: \"degree minute\",\n\t9116: \"degree hemisphere\",\n\t9117: \"hemisphere degree\",\n\t9118: \"degree minute hemisphere\",\n\t9119: \"hemisphere degree minute\",\n\t9120: \"hemisphere degree minute second\",\n\t9121: \"sexagesimal DMS.s\",\n\t9122: \"degree (supplier to define representation)\"\n}\n\n\nGeogAzimuthUnitsGeoKey = GeogAngularUnitsGeoKey\n\nProjLinearUnitsGeoKey = GeogLinearUnitsGeoKey\n\nVerticalUnitsGeoKey = GeogLinearUnitsGeoKey\n\n\nGeogEllipsoidGeoKey = {\n\t1024: \"CGCS2000\",\n\t7001: \"Airy 1830\",\n\t7002: \"Airy Modified 1849\",\n\t7003: \"Australian National Spheroid\",\n\t7004: \"Bessel 1841\",\n\t7005: \"Bessel Modified\",\n\t7006: \"Bessel Namibia\",\n\t7007: \"Clarke 1858\",\n\t7008: \"Clarke 1866\",\n\t7009: \"Clarke 1866 Michigan\",\n\t7010: \"Clarke 1880 (Benoit)\",\n\t7011: \"Clarke 1880 (IGN)\",\n\t7012: \"Clarke 1880 (RGS)\",\n\t7013: \"Clarke 1880 (Arc)\",\n\t7014: \"Clarke 1880 (SGA 1922)\",\n\t7015: \"Everest 1830 (1937 Adjustment)\",\n\t7016: \"Everest 1830 (1967 Definition)\",\n\t7018: \"Everest 1830 Modified\",\n\t7019: \"GRS 1980\",\n\t7020: \"Helmert 1906\",\n\t7021: \"Indonesian National Spheroid\",\n\t7022: \"International 1924\",\n\t7024: \"Krassowsky 1940\",\n\t7025: \"NWL 9D\",\n\t7027: \"Plessis 1817\",\n\t7028: \"Struve 1860\",\n\t7029: \"War Office\",\n\t7030: \"WGS 84\",\n\t7031: \"GEM 10C\",\n\t7032: \"OSU86F\",\n\t7033: \"OSU91A\",\n\t7034: \"Clarke 1880\",\n\t7035: \"Sphere\",\n\t7036: \"GRS 1967\",\n\t7041: \"Average Terrestrial System 1977\",\n\t7042: \"Everest (1830 Definition)\",\n\t7043: \"WGS 72\",\n\t7044: \"Everest 1830 (1962 Definition)\",\n\t7045: \"Everest 1830 (1975 Definition)\",\n\t7046: \"Bessel Namibia (GLM)\",\n\t7047: \"GRS 1980 Authalic Sphere\",\n\t7048: \"GRS 1980 Authalic Sphere\",\n\t7049: \"IAG 1975\",\n\t7050: \"GRS 1967 Modified\",\n\t7051: \"Danish 1876\",\n\t7052: \"Clarke 1866 Authalic Sphere\",\n\t7053: \"Hough 1960\",\n\t7054: \"PZ-90\",\n\t7055: \"Clarke 1880 (international foot)\",\n\t7056: \"Everest 1830 (RSO 1969)\",\n\t7057: \"International 1924 Authalic Sphere\",\n\t7058: \"Hughes 1980\",\n\t7059: \"Popular Visualisation Sphere\",\n\t32767: \"User-defined\"\n}\n\n\nGeogGeodeticDatumGeoKey = {\n\t1024: \"Hungarian Datum 1909\",\n\t1025: \"Taiwan Datum 1967\",\n\t1026: \"Taiwan Datum 1997\",\n\t1029: \"Iraqi Geospatial Reference System\",\n\t1031: \"MGI 1901\",\n\t1032: \"MOLDREF99\",\n\t1033: \"Reseau Geodesique de la RDC 2005\",\n\t1034: \"Serbian Reference Network 1998\",\n\t1035: \"Red Geodesica de Canarias 1995\",\n\t1036: \"Reseau Geodesique de Mayotte 2004\",\n\t1037: \"Cadastre 1997\",\n\t1038: \"Reseau Geodesique de Saint Pierre et Miquelon 2006\",\n\t1041: \"Autonomous Regions of Portugal 2008\",\n\t1042: \"Mexico ITRF92\",\n\t1043: \"China 2000\",\n\t1044: \"Sao Tome\",\n\t1045: \"New Beijing\",\n\t1046: \"Principe\",\n\t1047: \"Reseau de Reference des Antilles Francaises 1991\",\n\t1048: \"Tokyo 1892\",\n\t1052: \"System Jednotne Trigonometricke Site Katastralni/05\",\n\t1053: \"Sri Lanka Datum 1999\",\n\t1055: \"System Jednotne Trigonometricke Site Katastralni/05 (Ferro)\",\n\t1056: \"Geocentric Datum Brunei Darussalam 2009\",\n\t1057: \"Turkish National Reference Frame\",\n\t1058: \"Bhutan National Geodetic Datum\",\n\t1060: \"Islands Net 2004\",\n\t1061: \"International Terrestrial Reference Frame 2008\",\n\t1062: \"Posiciones Geodesicas Argentinas 2007\",\n\t1063: \"Marco Geodesico Nacional\",\n\t1064: \"SIRGAS-Chile\",\n\t1065: \"Costa Rica 2005\",\n\t1066: \"Sistema Geodesico Nacional de Panama MACARIO SOLIS\",\n\t1067: \"Peru96\",\n\t1068: \"SIRGAS-ROU98\",\n\t1069: \"SIRGAS_ES2007.8\",\n\t1070: \"Ocotepeque 1935\",\n\t1071: \"Sibun Gorge 1922\",\n\t1072: \"Panama-Colon 1911\",\n\t1073: \"Reseau Geodesique des Antilles Francaises 2009\",\n\t1074: \"Corrego Alegre 1961\",\n\t1075: \"South American Datum 1969(96)\",\n\t1076: \"Papua New Guinea Geodetic Datum 1994\",\n\t1077: \"Ukraine 2000\",\n\t1078: \"Fehmarnbelt Datum 2010\",\n\t1081: \"Deutsche Bahn Reference System\",\n\t1095: \"Tonga Geodetic Datum 2005\",\n\t1100: \"Cayman Islands Geodetic Datum 2011\",\n\t1111: \"Nepal 1981\",\n\t1112: \"Cyprus Geodetic Reference System 1993\",\n\t1113: \"Reseau Geodesique des Terres Australes et Antarctiques Francaises 2007\",\n\t1114: \"Israeli Geodetic Datum 2005\",\n\t1115: \"Israeli Geodetic Datum 2005(2012)\",\n\t1116: \"NAD83 (National Spatial Reference System 2011)\",\n\t1117: \"NAD83 (National Spatial Reference System PA11)\",\n\t1118: \"NAD83 (National Spatial Reference System MA11)\",\n\t1120: \"Mexico ITRF2008\",\n\t1128: \"Japanese Geodetic Datum 2011\",\n\t1132: \"Rete Dinamica Nazionale 2008\",\n\t1133: \"NAD83 (Continuously Operating Reference Station 1996)\",\n\t1135: \"Aden 1925\",\n\t1136: \"Bioko\",\n\t1137: \"Bekaa Valley 1920\",\n\t1138: \"South East Island 1943\",\n\t1139: \"Gambia\",\n\t1141: \"IGS08\",\n\t1142: \"IG05 Intermediate Datum\",\n\t1143: \"Israeli Geodetic Datum 2005\",\n\t1144: \"IG05/12 Intermediate Datum\",\n\t1145: \"Israeli Geodetic Datum 2005(2012)\",\n\t1147: \"Oman National Geodetic Datum 2014\",\n\t1160: \"Kyrgyzstan Geodetic Datum 2006\",\n\t6001: \"Not specified (based on Airy 1830 ellipsoid)\",\n\t6002: \"Not specified (based on Airy Modified 1849 ellipsoid)\",\n\t6003: \"Not specified (based on Australian National Spheroid)\",\n\t6004: \"Not specified (based on Bessel 1841 ellipsoid)\",\n\t6005: \"Not specified (based on Bessel Modified ellipsoid)\",\n\t6006: \"Not specified (based on Bessel Namibia ellipsoid)\",\n\t6007: \"Not specified (based on Clarke 1858 ellipsoid)\",\n\t6008: \"Not specified (based on Clarke 1866 ellipsoid)\",\n\t6009: \"Not specified (based on Clarke 1866 Michigan ellipsoid)\",\n\t6010: \"Not specified (based on Clarke 1880 (Benoit) ellipsoid)\",\n\t6011: \"Not specified (based on Clarke 1880 (IGN) ellipsoid)\",\n\t6012: \"Not specified (based on Clarke 1880 (RGS) ellipsoid)\",\n\t6013: \"Not specified (based on Clarke 1880 (Arc) ellipsoid)\",\n\t6014: \"Not specified (based on Clarke 1880 (SGA 1922) ellipsoid)\",\n\t6015: \"Not specified (based on Everest 1830 (1937 Adjustment) ellipsoid)\",\n\t6016: \"Not specified (based on Everest 1830 (1967 Definition) ellipsoid)\",\n\t6018: \"Not specified (based on Everest 1830 Modified ellipsoid)\",\n\t6019: \"Not specified (based on GRS 1980 ellipsoid)\",\n\t6020: \"Not specified (based on Helmert 1906 ellipsoid)\",\n\t6021: \"Not specified (based on Indonesian National Spheroid)\",\n\t6022: \"Not specified (based on International 1924 ellipsoid)\",\n\t6024: \"Not specified (based on Krassowsky 1940 ellipsoid)\",\n\t6025: \"Not specified (based on NWL 9D ellipsoid)\",\n\t6027: \"Not specified (based on Plessis 1817 ellipsoid)\",\n\t6028: \"Not specified (based on Struve 1860 ellipsoid)\",\n\t6029: \"Not specified (based on War Office ellipsoid)\",\n\t6030: \"Not specified (based on WGS 84 ellipsoid)\",\n\t6031: \"Not specified (based on GEM 10C ellipsoid)\",\n\t6032: \"Not specified (based on OSU86F ellipsoid)\",\n\t6033: \"Not specified (based on OSU91A ellipsoid)\",\n\t6034: \"Not specified (based on Clarke 1880 ellipsoid)\",\n\t6035: \"Not specified (based on Authalic Sphere)\",\n\t6036: \"Not specified (based on GRS 1967 ellipsoid)\",\n\t6041: \"Not specified (based on Average Terrestrial System 1977 ellipsoid)\",\n\t6042: \"Not specified (based on Everest (1830 Definition) ellipsoid)\",\n\t6043: \"Not specified (based on WGS 72 ellipsoid)\",\n\t6044: \"Not specified (based on Everest 1830 (1962 Definition) ellipsoid)\",\n\t6045: \"Not specified (based on Everest 1830 (1975 Definition) ellipsoid)\",\n\t6047: \"Not specified (based on GRS 1980 Authalic Sphere)\",\n\t6052: \"Not specified (based on Clarke 1866 Authalic Sphere)\",\n\t6053: \"Not specified (based on International 1924 Authalic Sphere)\",\n\t6054: \"Not specified (based on Hughes 1980 ellipsoid)\",\n\t6055: \"Popular Visualisation Datum\",\n\t6120: \"Greek\",\n\t6121: \"Greek Geodetic Reference System 1987\",\n\t6122: \"Average Terrestrial System 1977\",\n\t6123: \"Kartastokoordinaattijarjestelma (1966)\",\n\t6124: \"Rikets koordinatsystem 1990\",\n\t6125: \"Samboja\",\n\t6126: \"Lithuania 1994 (ETRS89)\",\n\t6127: \"Tete\",\n\t6128: \"Madzansua\",\n\t6129: \"Observatario\",\n\t6130: \"Moznet (ITRF94)\",\n\t6131: \"Indian 1960\",\n\t6132: \"Final Datum 1958\",\n\t6133: \"Estonia 1992\",\n\t6134: \"PDO Survey Datum 1993\",\n\t6135: \"Old Hawaiian\",\n\t6136: \"St. Lawrence Island\",\n\t6137: \"St. Paul Island\",\n\t6138: \"St. George Island\",\n\t6139: \"Puerto Rico\",\n\t6140: \"NAD83 Canadian Spatial Reference System\",\n\t6141: \"Israel 1993\",\n\t6142: \"Locodjo 1965\",\n\t6143: \"Abidjan 1987\",\n\t6144: \"Kalianpur 1937\",\n\t6145: \"Kalianpur 1962\",\n\t6146: \"Kalianpur 1975\",\n\t6147: \"Hanoi 1972\",\n\t6148: \"Hartebeesthoek94\",\n\t6149: \"CH1903\",\n\t6150: \"CH1903+\",\n\t6151: \"Swiss Terrestrial Reference Frame 1995\",\n\t6152: \"NAD83 (High Accuracy Reference Network)\",\n\t6153: \"Rassadiran\",\n\t6154: \"European Datum 1950(1977)\",\n\t6155: \"Dabola 1981\",\n\t6156: \"System Jednotne Trigonometricke Site Katastralni\",\n\t6157: \"Mount Dillon\",\n\t6158: \"Naparima 1955\",\n\t6159: \"European Libyan Datum 1979\",\n\t6160: \"Chos Malal 1914\",\n\t6161: \"Pampa del Castillo\",\n\t6162: \"Korean Datum 1985\",\n\t6163: \"Yemen National Geodetic Network 1996\",\n\t6164: \"South Yemen\",\n\t6165: \"Bissau\",\n\t6166: \"Korean Datum 1995\",\n\t6167: \"New Zealand Geodetic Datum 2000\",\n\t6168: \"Accra\",\n\t6169: \"American Samoa 1962\",\n\t6170: \"Sistema de Referencia Geocentrico para America del Sur 1995\",\n\t6171: \"Reseau Geodesique Francais 1993\",\n\t6172: \"Posiciones Geodesicas Argentinas\",\n\t6173: \"IRENET95\",\n\t6174: \"Sierra Leone Colony 1924\",\n\t6175: \"Sierra Leone 1968\",\n\t6176: \"Australian Antarctic Datum 1998\",\n\t6178: \"Pulkovo 1942(83)\",\n\t6179: \"Pulkovo 1942(58)\",\n\t6180: \"Estonia 1997\",\n\t6181: \"Luxembourg 1930\",\n\t6182: \"Azores Occidental Islands 1939\",\n\t6183: \"Azores Central Islands 1948\",\n\t6184: \"Azores Oriental Islands 1940\",\n\t6185: \"Madeira 1936\",\n\t6188: \"OSNI 1952\",\n\t6189: \"Red Geodesica Venezolana\",\n\t6190: \"Posiciones Geodesicas Argentinas 1998\",\n\t6191: \"Albanian 1987\",\n\t6192: \"Douala 1948\",\n\t6193: \"Manoca 1962\",\n\t6194: \"Qornoq 1927\",\n\t6195: \"Scoresbysund 1952\",\n\t6196: \"Ammassalik 1958\",\n\t6197: \"Garoua\",\n\t6198: \"Kousseri\",\n\t6199: \"Egypt 1930\",\n\t6200: \"Pulkovo 1995\",\n\t6201: \"Adindan\",\n\t6202: \"Australian Geodetic Datum 1966\",\n\t6203: \"Australian Geodetic Datum 1984\",\n\t6204: \"Ain el Abd 1970\",\n\t6205: \"Afgooye\",\n\t6206: \"Agadez\",\n\t6207: \"Lisbon 1937\",\n\t6208: \"Aratu\",\n\t6209: \"Arc 1950\",\n\t6210: \"Arc 1960\",\n\t6211: \"Batavia\",\n\t6212: \"Barbados 1938\",\n\t6213: \"Beduaram\",\n\t6214: \"Beijing 1954\",\n\t6215: \"Reseau National Belge 1950\",\n\t6216: \"Bermuda 1957\",\n\t6218: \"Bogota 1975\",\n\t6219: \"Bukit Rimpah\",\n\t6220: \"Camacupa\",\n\t6221: \"Campo Inchauspe\",\n\t6222: \"Cape\",\n\t6223: \"Carthage\",\n\t6224: \"Chua\",\n\t6225: \"Corrego Alegre 1970-72\",\n\t6226: \"Cote d'Ivoire\",\n\t6227: \"Deir ez Zor\",\n\t6228: \"Douala\",\n\t6229: \"Egypt 1907\",\n\t6230: \"European Datum 1950\",\n\t6231: \"European Datum 1987\",\n\t6232: \"Fahud\",\n\t6233: \"Gandajika 1970\",\n\t6234: \"Garoua\",\n\t6235: \"Guyane Francaise\",\n\t6236: \"Hu Tzu Shan 1950\",\n\t6237: \"Hungarian Datum 1972\",\n\t6238: \"Indonesian Datum 1974\",\n\t6239: \"Indian 1954\",\n\t6240: \"Indian 1975\",\n\t6241: \"Jamaica 1875\",\n\t6242: \"Jamaica 1969\",\n\t6243: \"Kalianpur 1880\",\n\t6244: \"Kandawala\",\n\t6245: \"Kertau 1968\",\n\t6246: \"Kuwait Oil Company\",\n\t6247: \"La Canoa\",\n\t6248: \"Provisional South American Datum 1956\",\n\t6249: \"Lake\",\n\t6250: \"Leigon\",\n\t6251: \"Liberia 1964\",\n\t6252: \"Lome\",\n\t6253: \"Luzon 1911\",\n\t6254: \"Hito XVIII 1963\",\n\t6255: \"Herat North\",\n\t6256: \"Mahe 1971\",\n\t6257: \"Makassar\",\n\t6258: \"European Terrestrial Reference System 1989\",\n\t6259: \"Malongo 1987\",\n\t6260: \"Manoca\",\n\t6261: \"Merchich\",\n\t6262: \"Massawa\",\n\t6263: \"Minna\",\n\t6264: \"Mhast\",\n\t6265: \"Monte Mario\",\n\t6266: \"M'poraloko\",\n\t6267: \"North American Datum 1927\",\n\t6268: \"NAD27 Michigan\",\n\t6269: \"North American Datum 1983\",\n\t6270: \"Nahrwan 1967\",\n\t6271: \"Naparima 1972\",\n\t6272: \"New Zealand Geodetic Datum 1949\",\n\t6273: \"NGO 1948\",\n\t6274: \"Datum 73\",\n\t6275: \"Nouvelle Triangulation Francaise\",\n\t6276: \"NSWC 9Z-2\",\n\t6277: \"OSGB 1936\",\n\t6278: \"OSGB 1970 (SN)\",\n\t6279: \"OS (SN) 1980\",\n\t6280: \"Padang 1884\",\n\t6281: \"Palestine 1923\",\n\t6282: \"Congo 1960 Pointe Noire\",\n\t6283: \"Geocentric Datum of Australia 1994\",\n\t6284: \"Pulkovo 1942\",\n\t6285: \"Qatar 1974\",\n\t6286: \"Qatar 1948\",\n\t6287: \"Qornoq\",\n\t6288: \"Loma Quintana\",\n\t6289: \"Amersfoort\",\n\t6291: \"South American Datum 1969\",\n\t6292: \"Sapper Hill 1943\",\n\t6293: \"Schwarzeck\",\n\t6294: \"Segora\",\n\t6295: \"Serindung\",\n\t6296: \"Sudan\",\n\t6297: \"Tananarive 1925\",\n\t6298: \"Timbalai 1948\",\n\t6299: \"TM65\",\n\t6300: \"Geodetic Datum of 1965\",\n\t6301: \"Tokyo\",\n\t6302: \"Trinidad 1903\",\n\t6303: \"Trucial Coast 1948\",\n\t6304: \"Voirol 1875\",\n\t6306: \"Bern 1938\",\n\t6307: \"Nord Sahara 1959\",\n\t6308: \"Stockholm 1938\",\n\t6309: \"Yacare\",\n\t6310: \"Yoff\",\n\t6311: \"Zanderij\",\n\t6312: \"Militar-Geographische Institut\",\n\t6313: \"Reseau National Belge 1972\",\n\t6314: \"Deutsches Hauptdreiecksnetz\",\n\t6315: \"Conakry 1905\",\n\t6316: \"Dealul Piscului 1930\",\n\t6317: \"Dealul Piscului 1970\",\n\t6318: \"National Geodetic Network\",\n\t6319: \"Kuwait Utility\",\n\t6322: \"World Geodetic System 1972\",\n\t6324: \"WGS 72 Transit Broadcast Ephemeris\",\n\t6326: \"World Geodetic System 1984\",\n\t6600: \"Anguilla 1957\",\n\t6601: \"Antigua 1943\",\n\t6602: \"Dominica 1945\",\n\t6603: \"Grenada 1953\",\n\t6604: \"Montserrat 1958\",\n\t6605: \"St. Kitts 1955\",\n\t6606: \"St. Lucia 1955\",\n\t6607: \"St. Vincent 1945\",\n\t6608: \"North American Datum 1927 (1976)\",\n\t6609: \"North American Datum 1927 (CGQ77)\",\n\t6610: \"Xian 1980\",\n\t6611: \"Hong Kong 1980\",\n\t6612: \"Japanese Geodetic Datum 2000\",\n\t6613: \"Gunung Segara\",\n\t6614: \"Qatar National Datum 1995\",\n\t6615: \"Porto Santo 1936\",\n\t6616: \"Selvagem Grande\",\n\t6618: \"South American Datum 1969\",\n\t6619: \"SWEREF99\",\n\t6620: \"Point 58\",\n\t6621: \"Fort Marigot\",\n\t6622: \"Guadeloupe 1948\",\n\t6623: \"Centre Spatial Guyanais 1967\",\n\t6624: \"Reseau Geodesique Francais Guyane 1995\",\n\t6625: \"Martinique 1938\",\n\t6626: \"Reunion 1947\",\n\t6627: \"Reseau Geodesique de la Reunion 1992\",\n\t6628: \"Tahiti 52\",\n\t6629: \"Tahaa 54\",\n\t6630: \"IGN72 Nuku Hiva\",\n\t6631: \"K0 1949\",\n\t6632: \"Combani 1950\",\n\t6633: \"IGN56 Lifou\",\n\t6634: \"IGN72 Grande Terre\",\n\t6635: \"ST87 Ouvea\",\n\t6636: \"Petrels 1972\",\n\t6637: \"Pointe Geologie Perroud 1950\",\n\t6638: \"Saint Pierre et Miquelon 1950\",\n\t6639: \"MOP78\",\n\t6640: \"Reseau de Reference des Antilles Francaises 1991\",\n\t6641: \"IGN53 Mare\",\n\t6642: \"ST84 Ile des Pins\",\n\t6643: \"ST71 Belep\",\n\t6644: \"NEA74 Noumea\",\n\t6645: \"Reseau Geodesique Nouvelle Caledonie 1991\",\n\t6646: \"Grand Comoros\",\n\t6647: \"International Terrestrial Reference Frame 1988\",\n\t6648: \"International Terrestrial Reference Frame 1989\",\n\t6649: \"International Terrestrial Reference Frame 1990\",\n\t6650: \"International Terrestrial Reference Frame 1991\",\n\t6651: \"International Terrestrial Reference Frame 1992\",\n\t6652: \"International Terrestrial Reference Frame 1993\",\n\t6653: \"International Terrestrial Reference Frame 1994\",\n\t6654: \"International Terrestrial Reference Frame 1996\",\n\t6655: \"International Terrestrial Reference Frame 1997\",\n\t6656: \"International Terrestrial Reference Frame 2000\",\n\t6657: \"Reykjavik 1900\",\n\t6658: \"Hjorsey 1955\",\n\t6659: \"Islands Net 1993\",\n\t6660: \"Helle 1954\",\n\t6661: \"Latvia 1992\",\n\t6663: \"Porto Santo 1995\",\n\t6664: \"Azores Oriental Islands 1995\",\n\t6665: \"Azores Central Islands 1995\",\n\t6666: \"Lisbon 1890\",\n\t6667: \"Iraq-Kuwait Boundary Datum 1992\",\n\t6668: \"European Datum 1979\",\n\t6670: \"Istituto Geografico Militaire 1995\",\n\t6671: \"Voirol 1879\",\n\t6672: \"Chatham Islands Datum 1971\",\n\t6673: \"Chatham Islands Datum 1979\",\n\t6674: \"Sistema de Referencia Geocentrico para las AmericaS 2000\",\n\t6675: \"Guam 1963\",\n\t6676: \"Vientiane 1982\",\n\t6677: \"Lao 1993\",\n\t6678: \"Lao National Datum 1997\",\n\t6679: \"Jouik 1961\",\n\t6680: \"Nouakchott 1965\",\n\t6681: \"Mauritania 1999\",\n\t6682: \"Gulshan 303\",\n\t6683: \"Philippine Reference System 1992\",\n\t6684: \"Gan 1970\",\n\t6685: \"Gandajika\",\n\t6686: \"Marco Geocentrico Nacional de Referencia\",\n\t6687: \"Reseau Geodesique de la Polynesie Francaise\",\n\t6688: \"Fatu Iva 72\",\n\t6689: \"IGN63 Hiva Oa\",\n\t6690: \"Tahiti 79\",\n\t6691: \"Moorea 87\",\n\t6692: \"Maupiti 83\",\n\t6693: \"Nakhl-e Ghanem\",\n\t6694: \"Posiciones Geodesicas Argentinas 1994\",\n\t6695: \"Katanga 1955\",\n\t6696: \"Kasai 1953\",\n\t6697: \"IGC 1962 Arc of the 6th Parallel South\",\n\t6698: \"IGN 1962 Kerguelen\",\n\t6699: \"Le Pouce 1934\",\n\t6700: \"IGN Astro 1960\",\n\t6701: \"Institut Geographique du Congo Belge 1955\",\n\t6702: \"Mauritania 1999\",\n\t6703: \"Missao Hidrografico Angola y Sao Tome 1951\",\n\t6704: \"Mhast (onshore)\",\n\t6705: \"Mhast (offshore)\",\n\t6706: \"Egypt Gulf of Suez S-650 TL\",\n\t6707: \"Tern Island 1961\",\n\t6708: \"Cocos Islands 1965\",\n\t6709: \"Iwo Jima 1945\",\n\t6710: \"St. Helena 1971\",\n\t6711: \"Marcus Island 1952\",\n\t6712: \"Ascension Island 1958\",\n\t6713: \"Ayabelle Lighthouse\",\n\t6714: \"Bellevue\",\n\t6715: \"Camp Area Astro\",\n\t6716: \"Phoenix Islands 1966\",\n\t6717: \"Cape Canaveral\",\n\t6718: \"Solomon 1968\",\n\t6719: \"Easter Island 1967\",\n\t6720: \"Fiji Geodetic Datum 1986\",\n\t6721: \"Fiji 1956\",\n\t6722: \"South Georgia 1968\",\n\t6723: \"Grand Cayman Geodetic Datum 1959\",\n\t6724: \"Diego Garcia 1969\",\n\t6725: \"Johnston Island 1961\",\n\t6726: \"Sister Islands Geodetic Datum 1961\",\n\t6727: \"Midway 1961\",\n\t6728: \"Pico de las Nieves 1984\",\n\t6729: \"Pitcairn 1967\",\n\t6730: \"Santo 1965\",\n\t6731: \"Viti Levu 1916\",\n\t6732: \"Marshall Islands 1960\",\n\t6733: \"Wake Island 1952\",\n\t6734: \"Tristan 1968\",\n\t6735: \"Kusaie 1951\",\n\t6736: \"Deception Island\",\n\t6737: \"Geocentric datum of Korea\",\n\t6738: \"Hong Kong 1963\",\n\t6739: \"Hong Kong 1963(67)\",\n\t6740: \"Parametrop Zemp 1990\",\n\t6741: \"Faroe Datum 1954\",\n\t6742: \"Geodetic Datum of Malaysia 2000\",\n\t6743: \"Karbala 1979\",\n\t6744: \"Nahrwan 1934\",\n\t6745: \"Rauenberg Datum/83\",\n\t6746: \"Potsdam Datum/83\",\n\t6747: \"Greenland 1996\",\n\t6748: \"Vanua Levu 1915\",\n\t6749: \"Reseau Geodesique de Nouvelle Caledonie 91-93\",\n\t6750: \"ST87 Ouvea\",\n\t6751: \"Kertau (RSO)\",\n\t6752: \"Viti Levu 1912\",\n\t6753: \"fk89\",\n\t6754: \"Libyan Geodetic Datum 2006\",\n\t6755: \"Datum Geodesi Nasional 1995\",\n\t6756: \"Vietnam 2000\",\n\t6757: \"SVY21\",\n\t6758: \"Jamaica 2001\",\n\t6759: \"NAD83 (National Spatial Reference System 2007)\",\n\t6760: \"World Geodetic System 1966\",\n\t6761: \"Croatian Terrestrial Reference System\",\n\t6762: \"Bermuda 2000\",\n\t6763: \"Pitcairn 2006\",\n\t6764: \"Ross Sea Region Geodetic Datum 2000\",\n\t6765: \"Slovenia Geodetic Datum 1996\",\n\t6801: \"CH1903 (Bern)\",\n\t6802: \"Bogota 1975 (Bogota)\",\n\t6803: \"Lisbon 1937 (Lisbon)\",\n\t6804: \"Makassar (Jakarta)\",\n\t6805: \"Militar-Geographische Institut (Ferro)\",\n\t6806: \"Monte Mario (Rome)\",\n\t6807: \"Nouvelle Triangulation Francaise (Paris)\",\n\t6808: \"Padang 1884 (Jakarta)\",\n\t6809: \"Reseau National Belge 1950 (Brussels)\",\n\t6810: \"Tananarive 1925 (Paris)\",\n\t6811: \"Voirol 1875 (Paris)\",\n\t6813: \"Batavia (Jakarta)\",\n\t6814: \"Stockholm 1938 (Stockholm)\",\n\t6815: \"Greek (Athens)\",\n\t6816: \"Carthage (Paris)\",\n\t6817: \"NGO 1948 (Oslo)\",\n\t6818: \"System Jednotne Trigonometricke Site Katastralni (Ferro)\",\n\t6819: \"Nord Sahara 1959 (Paris)\",\n\t6820: \"Gunung Segara (Jakarta)\",\n\t6821: \"Voirol 1879 (Paris)\",\n\t6896: \"International Terrestrial Reference Frame 2005\",\n\t6901: \"Ancienne Triangulation Francaise (Paris)\",\n\t6902: \"Nord de Guerre (Paris)\",\n\t6903: \"Madrid 1870 (Madrid)\",\n\t6904: \"Lisbon 1890 (Lisbon)\",\n\t32767: \"User-defined\"\n}\n\n\nVerticalDatumGeoKey = {\n\t1027: \"EGM2008 geoid\",\n\t1028: \"Fao 1979\",\n\t1030: \"N2000\",\n\t1039: \"New Zealand Vertical Datum 2009\",\n\t1040: \"Dunedin-Bluff 1960\",\n\t1049: \"Incheon\",\n\t1050: \"Trieste\",\n\t1051: \"Genoa\",\n\t1054: \"Sri Lanka Vertical Datum\",\n\t1059: \"Faroe Islands Vertical Reference 2009\",\n\t1079: \"Fehmarnbelt Vertical Reference 2010\",\n\t1080: \"Lowest Astronomic Tide\",\n\t1082: \"Highest Astronomic Tide\",\n\t1083: \"Lower Low Water Large Tide\",\n\t1084: \"Higher High Water Large Tide\",\n\t1085: \"Indian Spring Low Water\",\n\t1086: \"Mean Lower Low Water Spring Tides\",\n\t1087: \"Mean Low Water Spring Tides\",\n\t1088: \"Mean High Water Spring Tides\",\n\t1089: \"Mean Lower Low Water\",\n\t1090: \"Mean Higher High Water\",\n\t1091: \"Mean Low Water\",\n\t1092: \"Mean High Water\",\n\t1093: \"Low Water\",\n\t1094: \"High Water\",\n\t1096: \"Norway Normal Null 2000\",\n\t1097: \"Grand Cayman Vertical Datum 1954\",\n\t1098: \"Little Cayman Vertical Datum 1961\",\n\t1099: \"Cayman Brac Vertical Datum 1961\",\n\t1101: \"Cais da Pontinha - Funchal\",\n\t1102: \"Cais da Vila - Porto Santo\",\n\t1103: \"Cais das Velas\",\n\t1104: \"Horta\",\n\t1105: \"Cais da Madalena\",\n\t1106: \"Santa Cruz da Graciosa\",\n\t1107: \"Cais da Figueirinha - Angra do Heroismo\",\n\t1108: \"Santa Cruz das Flores\",\n\t1109: \"Cais da Vila do Porto\",\n\t1110: \"Ponta Delgada\",\n\t1119: \"Northern Marianas Vertical Datum of 2003\",\n\t1121: \"Tutuila Vertical Datum of 1962\",\n\t1122: \"Guam Vertical Datum of 1963\",\n\t1123: \"Puerto Rico Vertical Datum of 2002\",\n\t1124: \"Virgin Islands Vertical Datum of 2009\",\n\t1125: \"American Samoa Vertical Datum of 2002\",\n\t1126: \"Guam Vertical Datum of 2004\",\n\t1127: \"Canadian Geodetic Vertical Datum of 2013\",\n\t1129: \"Japanese Standard Levelling Datum 1972\",\n\t1130: \"Japanese Geodetic Datum 2000 (vertical)\",\n\t1131: \"Japanese Geodetic Datum 2011 (vertical)\",\n\t1140: \"Singapore Height Datum\",\n\t1146: \"Ras Ghumays\",\n\t1148: \"Famagusta 1960\",\n\t1149: \"PNG08\",\n\t1150: \"Kumul 34\",\n\t1151: \"Kiunga\",\n\t1161: \"Deutsches Haupthoehennetz 1912\",\n\t1162: \"Latvian Height System 2000\",\n\t5100: \"Mean Sea Level\",\n\t5101: \"Ordnance Datum Newlyn\",\n\t5102: \"National Geodetic Vertical Datum 1929\",\n\t5103: \"North American Vertical Datum 1988\",\n\t5104: \"Yellow Sea 1956\",\n\t5105: \"Baltic Sea\",\n\t5106: \"Caspian Sea\",\n\t5107: \"Nivellement general de la France\",\n\t5109: \"Normaal Amsterdams Peil\",\n\t5110: \"Ostend\",\n\t5111: \"Australian Height Datum\",\n\t5112: \"Australian Height Datum (Tasmania)\",\n\t5113: \"Instantaneous Water Level\",\n\t5114: \"Canadian Geodetic Vertical Datum of 1928\",\n\t5115: \"Piraeus Harbour 1986\",\n\t5116: \"Helsinki 1960\",\n\t5117: \"Rikets hojdsystem 1970\",\n\t5118: \"Nivellement General de la France - Lallemand\",\n\t5119: \"Nivellement General de la France - IGN69\",\n\t5120: \"Nivellement General de la France - IGN78\",\n\t5121: \"Maputo\",\n\t5122: \"Japanese Standard Levelling Datum 1969\",\n\t5123: \"PDO Height Datum 1993\",\n\t5124: \"Fahud Height Datum\",\n\t5125: \"Ha Tien 1960\",\n\t5126: \"Hon Dau 1992\",\n\t5127: \"Landesnivellement 1902\",\n\t5128: \"Landeshohennetz 1995\",\n\t5129: \"European Vertical Reference Frame 2000\",\n\t5130: \"Malin Head\",\n\t5131: \"Belfast Lough\",\n\t5132: \"Dansk Normal Nul\",\n\t5133: \"AIOC 1995\",\n\t5134: \"Black Sea\",\n\t5135: \"Hong Kong Principal Datum\",\n\t5136: \"Hong Kong Chart Datum\",\n\t5137: \"Yellow Sea 1985\",\n\t5138: \"Ordnance Datum Newlyn (Orkney Isles)\",\n\t5139: \"Fair Isle\",\n\t5140: \"Lerwick\",\n\t5141: \"Foula\",\n\t5142: \"Sule Skerry\",\n\t5143: \"North Rona\",\n\t5144: \"Stornoway\",\n\t5145: \"St Kilda\",\n\t5146: \"Flannan Isles\",\n\t5147: \"St Marys\",\n\t5148: \"Douglas\",\n\t5149: \"Fao\",\n\t5150: \"Bandar Abbas\",\n\t5151: \"Nivellement General de Nouvelle Caledonie\",\n\t5152: \"Poolbeg\",\n\t5153: \"Nivellement General Guyanais 1977\",\n\t5154: \"Martinique 1987\",\n\t5155: \"Guadeloupe 1988\",\n\t5156: \"Reunion 1989\",\n\t5157: \"Auckland 1946\",\n\t5158: \"Bluff 1955\",\n\t5159: \"Dunedin 1958\",\n\t5160: \"Gisborne 1926\",\n\t5161: \"Lyttelton 1937\",\n\t5162: \"Moturiki 1953\",\n\t5163: \"Napier 1962\",\n\t5164: \"Nelson 1955\",\n\t5165: \"One Tree Point 1964\",\n\t5166: \"Tararu 1952\",\n\t5167: \"Taranaki 1970\",\n\t5168: \"Wellington 1953\",\n\t5169: \"Waitangi (Chatham Island) 1959\",\n\t5170: \"Stewart Island 1977\",\n\t5171: \"EGM96 geoid\",\n\t5172: \"Nivellement General du Luxembourg\",\n\t5173: \"Antalya\",\n\t5174: \"Norway Normal Null 1954\",\n\t5175: \"Durres\",\n\t5176: \"Gebrauchshohen ADRIA\",\n\t5177: \"National Vertical Network 1999\",\n\t5178: \"Cascais\",\n\t5179: \"Constanta\",\n\t5180: \"Alicante\",\n\t5181: \"Deutsches Haupthoehennetz 1992\",\n\t5182: \"Deutsches Haupthoehennetz 1985\",\n\t5183: \"Staatlichen Nivellementnetzes 1976\",\n\t5184: \"Baltic 1982\",\n\t5185: \"Baltic 1980\",\n\t5186: \"Kuwait PWD\",\n\t5187: \"KOC Well Datum\",\n\t5188: \"KOC Construction Datum\",\n\t5189: \"Nivellement General de la Corse 1948\",\n\t5190: \"Danger 1950\",\n\t5191: \"Mayotte 1950\",\n\t5192: \"Martinique 1955\",\n\t5193: \"Guadeloupe 1951\",\n\t5194: \"Lagos 1955\",\n\t5195: \"Nivellement General de Polynesie Francaise\",\n\t5196: \"IGN 1966\",\n\t5197: \"Moorea SAU 1981\",\n\t5198: \"Raiatea SAU 2001\",\n\t5199: \"Maupiti SAU 2001\",\n\t5200: \"Huahine SAU 2001\",\n\t5201: \"Tahaa SAU 2001\",\n\t5202: \"Bora Bora SAU 2001\",\n\t5203: \"EGM84 geoid\",\n\t5204: \"International Great Lakes Datum 1955\",\n\t5205: \"International Great Lakes Datum 1985\",\n\t5206: \"Dansk Vertikal Reference 1990\",\n\t5207: \"Croatian Vertical Reference System 1971\",\n\t5208: \"Rikets hojdsystem 2000\",\n\t5209: \"Rikets hojdsystem 1900\",\n\t5210: \"IGN 1988 LS\",\n\t5211: \"IGN 1988 MG\",\n\t5212: \"IGN 1992 LD\",\n\t5213: \"IGN 1988 SB\",\n\t5214: \"IGN 1988 SM\",\n\t5215: \"European Vertical Reference Frame 2007\",\n\t32767: \"User-defined\"\n}\n\n\nVerticalCSTypeGeoKey = {\n\t3855: \"EGM2008 height\",\n\t3886: \"Fao 1979 height\",\n\t3900: \"N2000 height\",\n\t4440: \"NZVD2009 height\",\n\t4458: \"Dunedin-Bluff 1960 height\",\n\t5193: \"Incheon height\",\n\t5195: \"Trieste height\",\n\t5214: \"Genoa height\",\n\t5237: \"SLVD height\",\n\t5317: \"FVR09 height\",\n\t5336: \"Black Sea depth\",\n\t5597: \"FCSVR10 height\",\n\t5600: \"NGPF height\",\n\t5601: \"IGN 1966 height\",\n\t5602: \"Moorea SAU 1981 height\",\n\t5603: \"Raiatea SAU 2001 height\",\n\t5604: \"Maupiti SAU 2001 height\",\n\t5605: \"Huahine SAU 2001 height\",\n\t5606: \"Tahaa SAU 2001 height\",\n\t5607: \"Bora Bora SAU 2001 height\",\n\t5608: \"IGLD 1955 height\",\n\t5609: \"IGLD 1985 height\",\n\t5610: \"HVRS71 height\",\n\t5611: \"Caspian height\",\n\t5612: \"Baltic depth\",\n\t5613: \"RH2000 height\",\n\t5614: \"KOC WD depth (ft)\",\n\t5615: \"RH00 height\",\n\t5616: \"IGN 1988 LS height\",\n\t5617: \"IGN 1988 MG height\",\n\t5618: \"IGN 1992 LD height\",\n\t5619: \"IGN 1988 SB height\",\n\t5620: \"IGN 1988 SM height\",\n\t5621: \"EVRF2007 height\",\n\t5701: \"ODN height\",\n\t5702: \"NGVD29 height\",\n\t5703: \"NAVD88 height\",\n\t5704: \"Yellow Sea\",\n\t5705: \"Baltic height\",\n\t5706: \"Caspian depth\",\n\t5709: \"NAP height\",\n\t5710: \"Ostend height\",\n\t5711: \"AHD height\",\n\t5712: \"AHD (Tasmania) height\",\n\t5713: \"CGVD28 height\",\n\t5714: \"MSL height\",\n\t5715: \"MSL depth\",\n\t5716: \"Piraeus height\",\n\t5717: \"N60 height\",\n\t5718: \"RH70 height\",\n\t5719: \"NGF Lallemand height\",\n\t5720: \"NGF-IGN69 height\",\n\t5721: \"NGF-IGN78 height\",\n\t5722: \"Maputo height\",\n\t5723: \"JSLD69 height\",\n\t5724: \"PHD93 height\",\n\t5725: \"Fahud HD height\",\n\t5726: \"Ha Tien 1960 height\",\n\t5727: \"Hon Dau 1992 height\",\n\t5728: \"LN02 height\",\n\t5729: \"LHN95 height\",\n\t5730: \"EVRF2000 height\",\n\t5731: \"Malin Head height\",\n\t5732: \"Belfast height\",\n\t5733: \"DNN height\",\n\t5734: \"AIOC95 depth\",\n\t5735: \"Black Sea height\",\n\t5736: \"Yellow Sea 1956 height\",\n\t5737: \"Yellow Sea 1985 height\",\n\t5738: \"HKPD height\",\n\t5739: \"HKCD depth\",\n\t5740: \"ODN Orkney height\",\n\t5741: \"Fair Isle height\",\n\t5742: \"Lerwick height\",\n\t5743: \"Foula height\",\n\t5744: \"Sule Skerry height\",\n\t5745: \"North Rona height\",\n\t5746: \"Stornoway height\",\n\t5747: \"St Kilda height\",\n\t5748: \"Flannan Isles height\",\n\t5749: \"St Marys height\",\n\t5750: \"Douglas height\",\n\t5751: \"Fao height\",\n\t5752: \"Bandar Abbas height\",\n\t5753: \"NGNC height\",\n\t5754: \"Poolbeg height\",\n\t5755: \"NGG1977 height\",\n\t5756: \"Martinique 1987 height\",\n\t5757: \"Guadeloupe 1988 height\",\n\t5758: \"Reunion 1989 height\",\n\t5759: \"Auckland 1946 height\",\n\t5760: \"Bluff 1955 height\",\n\t5761: \"Dunedin 1958 height\",\n\t5762: \"Gisborne 1926 height\",\n\t5763: \"Lyttelton 1937 height\",\n\t5764: \"Moturiki 1953 height\",\n\t5765: \"Napier 1962 height\",\n\t5766: \"Nelson 1955 height\",\n\t5767: \"One Tree Point 1964 height\",\n\t5768: \"Tararu 1952 height\",\n\t5769: \"Taranaki 1970 height\",\n\t5770: \"Wellington 1953 height\",\n\t5771: \"Chatham Island 1959 height\",\n\t5772: \"Stewart Island 1977 height\",\n\t5773: \"EGM96 height\",\n\t5774: \"NG-L height\",\n\t5775: \"Antalya height\",\n\t5776: \"NN54 height\",\n\t5777: \"Durres height\",\n\t5778: \"GHA height\",\n\t5779: \"NVN99 height\",\n\t5780: \"Cascais height\",\n\t5781: \"Constanta height\",\n\t5782: \"Alicante height\",\n\t5783: \"DHHN92 height\",\n\t5784: \"DHHN85 height\",\n\t5785: \"SNN76 height\",\n\t5786: \"Baltic 1982 height\",\n\t5787: \"EOMA 1980 height\",\n\t5788: \"Kuwait PWD height\",\n\t5789: \"KOC WD depth\",\n\t5790: \"KOC CD height\",\n\t5791: \"NGC 1948 height\",\n\t5792: \"Danger 1950 height\",\n\t5793: \"Mayotte 1950 height\",\n\t5794: \"Martinique 1955 height\",\n\t5795: \"Guadeloupe 1951 height\",\n\t5796: \"Lagos 1955 height\",\n\t5797: \"AIOC95 height\",\n\t5798: \"EGM84 height\",\n\t5799: \"DVR90 height\",\n\t5829: \"Instantaneous Water Level height\",\n\t5831: \"Instantaneous Water Level depth\",\n\t5843: \"Ras Ghumays height\",\n\t5861: \"LAT depth\",\n\t5862: \"LLWLT depth\",\n\t5863: \"ISLW depth\",\n\t5864: \"MLLWS depth\",\n\t5865: \"MLWS depth\",\n\t5866: \"MLLW depth\",\n\t5867: \"MLW depth\",\n\t5868: \"MHW height\",\n\t5869: \"MHHW height\",\n\t5870: \"MHWS height\",\n\t5871: \"HHWLT height\",\n\t5872: \"HAT height\",\n\t5873: \"Low Water depth\",\n\t5874: \"High Water height\",\n\t5941: \"NN2000 height\",\n\t6130: \"GCVD54 height\",\n\t6131: \"LCVD61 height\",\n\t6132: \"CBVD61 height\",\n\t6178: \"Cais da Pontinha - Funchal height\",\n\t6179: \"Cais da Vila - Porto Santo height\",\n\t6180: \"Cais das Velas height\",\n\t6181: \"Horta height\",\n\t6182: \"Cais da Madalena height\",\n\t6183: \"Santa Cruz da Graciosa height\",\n\t6184: \"Cais da Figueirinha - Angra do Heroismo height\",\n\t6185: \"Santa Cruz das Flores height\",\n\t6186: \"Cais da Vila do Porto height\",\n\t6187: \"Ponta Delgada height\",\n\t6357: \"NAVD88 depth\",\n\t6358: \"NAVD88 depth (ftUS)\",\n\t6359: \"NGVD29 depth\",\n\t6360: \"NAVD88 height (ftUS)\",\n\t6638: \"Tutuila 1962 height\",\n\t6639: \"Guam 1963 height\",\n\t6640: \"NMVD03 height\",\n\t6641: \"PRVD02 height\",\n\t6642: \"VIVD09 height\",\n\t6643: \"ASVD02 height\",\n\t6644: \"GUVD04 height\",\n\t6647: \"CGVD2013 height\",\n\t6693: \"JSLD72 height\",\n\t6694: \"JGD2000 (vertical) height\",\n\t6695: \"JGD2011 (vertical) height\",\n\t6916: \"SHD height\",\n\t7446: \"Famagusta 1960 height\",\n\t7447: \"PNG08 height\",\n\t7651: \"Kumul 34 height\",\n\t7652: \"Kiunga height\",\n\t7699: \"DHHN12 height\",\n\t7700: \"Latvia 2000 height\",\n\t32767: \"User-defined\"\n}\n\n\nGeographicTypeGeoKey = {\n\t3819: \"HD1909\",\n\t3821: \"TWD67\",\n\t3824: \"TWD97\",\n\t3889: \"IGRS\",\n\t3906: \"MGI 1901\",\n\t4001: \"Unknown datum based upon the Airy 1830 ellipsoid\",\n\t4002: \"Unknown datum based upon the Airy Modified 1849 ellipsoid\",\n\t4003: \"Unknown datum based upon the Australian National Spheroid\",\n\t4004: \"Unknown datum based upon the Bessel 1841 ellipsoid\",\n\t4005: \"Unknown datum based upon the Bessel Modified ellipsoid\",\n\t4006: \"Unknown datum based upon the Bessel Namibia ellipsoid\",\n\t4007: \"Unknown datum based upon the Clarke 1858 ellipsoid\",\n\t4008: \"Unknown datum based upon the Clarke 1866 ellipsoid\",\n\t4009: \"Unknown datum based upon the Clarke 1866 Michigan ellipsoid\",\n\t4010: \"Unknown datum based upon the Clarke 1880 (Benoit) ellipsoid\",\n\t4011: \"Unknown datum based upon the Clarke 1880 (IGN) ellipsoid\",\n\t4012: \"Unknown datum based upon the Clarke 1880 (RGS) ellipsoid\",\n\t4013: \"Unknown datum based upon the Clarke 1880 (Arc) ellipsoid\",\n\t4014: \"Unknown datum based upon the Clarke 1880 (SGA 1922) ellipsoid\",\n\t4015: \"Unknown datum based upon the Everest 1830 (1937 Adjustment) ellipsoid\",\n\t4016: \"Unknown datum based upon the Everest 1830 (1967 Definition) ellipsoid\",\n\t4018: \"Unknown datum based upon the Everest 1830 Modified ellipsoid\",\n\t4019: \"Unknown datum based upon the GRS 1980 ellipsoid\",\n\t4020: \"Unknown datum based upon the Helmert 1906 ellipsoid\",\n\t4021: \"Unknown datum based upon the Indonesian National Spheroid\",\n\t4022: \"Unknown datum based upon the International 1924 ellipsoid\",\n\t4023: \"MOLDREF99\",\n\t4024: \"Unknown datum based upon the Krassowsky 1940 ellipsoid\",\n\t4025: \"Unknown datum based upon the NWL 9D ellipsoid\",\n\t4027: \"Unknown datum based upon the Plessis 1817 ellipsoid\",\n\t4028: \"Unknown datum based upon the Struve 1860 ellipsoid\",\n\t4029: \"Unknown datum based upon the War Office ellipsoid\",\n\t4030: \"Unknown datum based upon the WGS 84 ellipsoid\",\n\t4031: \"Unknown datum based upon the GEM 10C ellipsoid\",\n\t4032: \"Unknown datum based upon the OSU86F ellipsoid\",\n\t4033: \"Unknown datum based upon the OSU91A ellipsoid\",\n\t4034: \"Unknown datum based upon the Clarke 1880 ellipsoid\",\n\t4035: \"Unknown datum based upon the Authalic Sphere\",\n\t4036: \"Unknown datum based upon the GRS 1967 ellipsoid\",\n\t4041: \"Unknown datum based upon the Average Terrestrial System 1977 ellipsoid\",\n\t4042: \"Unknown datum based upon the Everest (1830 Definition) ellipsoid\",\n\t4043: \"Unknown datum based upon the WGS 72 ellipsoid\",\n\t4044: \"Unknown datum based upon the Everest 1830 (1962 Definition) ellipsoid\",\n\t4045: \"Unknown datum based upon the Everest 1830 (1975 Definition) ellipsoid\",\n\t4046: \"RGRDC 2005\",\n\t4047: \"Unspecified datum based upon the GRS 1980 Authalic Sphere\",\n\t4052: \"Unspecified datum based upon the Clarke 1866 Authalic Sphere\",\n\t4053: \"Unspecified datum based upon the International 1924 Authalic Sphere\",\n\t4054: \"Unspecified datum based upon the Hughes 1980 ellipsoid\",\n\t4055: \"Popular Visualisation CRS\",\n\t4075: \"SREF98\",\n\t4081: \"REGCAN95\",\n\t4120: \"Greek\",\n\t4121: \"GGRS87\",\n\t4122: \"ATS77\",\n\t4123: \"KKJ\",\n\t4124: \"RT90\",\n\t4125: \"Samboja\",\n\t4126: \"LKS94 (ETRS89)\",\n\t4127: \"Tete\",\n\t4128: \"Madzansua\",\n\t4129: \"Observatario\",\n\t4130: \"Moznet\",\n\t4131: \"Indian 1960\",\n\t4132: \"FD58\",\n\t4133: \"EST92\",\n\t4134: \"PSD93\",\n\t4135: \"Old Hawaiian\",\n\t4136: \"St. Lawrence Island\",\n\t4137: \"St. Paul Island\",\n\t4138: \"St. George Island\",\n\t4139: \"Puerto Rico\",\n\t4140: \"NAD83(CSRS98)\",\n\t4141: \"Israel 1993\",\n\t4142: \"Locodjo 1965\",\n\t4143: \"Abidjan 1987\",\n\t4144: \"Kalianpur 1937\",\n\t4145: \"Kalianpur 1962\",\n\t4146: \"Kalianpur 1975\",\n\t4147: \"Hanoi 1972\",\n\t4148: \"Hartebeesthoek94\",\n\t4149: \"CH1903\",\n\t4150: \"CH1903+\",\n\t4151: \"CHTRF95\",\n\t4152: \"NAD83(HARN)\",\n\t4153: \"Rassadiran\",\n\t4154: \"ED50(ED77)\",\n\t4155: \"Dabola 1981\",\n\t4156: \"S-JTSK\",\n\t4157: \"Mount Dillon\",\n\t4158: \"Naparima 1955\",\n\t4159: \"ELD79\",\n\t4160: \"Chos Malal 1914\",\n\t4161: \"Pampa del Castillo\",\n\t4162: \"Korean 1985\",\n\t4163: \"Yemen NGN96\",\n\t4164: \"South Yemen\",\n\t4165: \"Bissau\",\n\t4166: \"Korean 1995\",\n\t4167: \"NZGD2000\",\n\t4168: \"Accra\",\n\t4169: \"American Samoa 1962\",\n\t4170: \"SIRGAS 1995\",\n\t4171: \"RGF93\",\n\t4172: \"POSGAR\",\n\t4173: \"IRENET95\",\n\t4174: \"Sierra Leone 1924\",\n\t4175: \"Sierra Leone 1968\",\n\t4176: \"Australian Antarctic\",\n\t4178: \"Pulkovo 1942(83)\",\n\t4179: \"Pulkovo 1942(58)\",\n\t4180: \"EST97\",\n\t4181: \"Luxembourg 1930\",\n\t4182: \"Azores Occidental 1939\",\n\t4183: \"Azores Central 1948\",\n\t4184: \"Azores Oriental 1940\",\n\t4185: \"Madeira 1936\",\n\t4188: \"OSNI 1952\",\n\t4189: \"REGVEN\",\n\t4190: \"POSGAR 98\",\n\t4191: \"Albanian 1987\",\n\t4192: \"Douala 1948\",\n\t4193: \"Manoca 1962\",\n\t4194: \"Qornoq 1927\",\n\t4195: \"Scoresbysund 1952\",\n\t4196: \"Ammassalik 1958\",\n\t4197: \"Garoua\",\n\t4198: \"Kousseri\",\n\t4199: \"Egypt 1930\",\n\t4200: \"Pulkovo 1995\",\n\t4201: \"Adindan\",\n\t4202: \"AGD66\",\n\t4203: \"AGD84\",\n\t4204: \"Ain el Abd\",\n\t4205: \"Afgooye\",\n\t4206: \"Agadez\",\n\t4207: \"Lisbon\",\n\t4208: \"Aratu\",\n\t4209: \"Arc 1950\",\n\t4210: \"Arc 1960\",\n\t4211: \"Batavia\",\n\t4212: \"Barbados 1938\",\n\t4213: \"Beduaram\",\n\t4214: \"Beijing 1954\",\n\t4215: \"Belge 1950\",\n\t4216: \"Bermuda 1957\",\n\t4218: \"Bogota 1975\",\n\t4219: \"Bukit Rimpah\",\n\t4220: \"Camacupa\",\n\t4221: \"Campo Inchauspe\",\n\t4222: \"Cape\",\n\t4223: \"Carthage\",\n\t4224: \"Chua\",\n\t4225: \"Corrego Alegre 1970-72\",\n\t4226: \"Cote d'Ivoire\",\n\t4227: \"Deir ez Zor\",\n\t4228: \"Douala\",\n\t4229: \"Egypt 1907\",\n\t4230: \"ED50\",\n\t4231: \"ED87\",\n\t4232: \"Fahud\",\n\t4233: \"Gandajika 1970\",\n\t4234: \"Garoua\",\n\t4235: \"Guyane Francaise\",\n\t4236: \"Hu Tzu Shan 1950\",\n\t4237: \"HD72\",\n\t4238: \"ID74\",\n\t4239: \"Indian 1954\",\n\t4240: \"Indian 1975\",\n\t4241: \"Jamaica 1875\",\n\t4242: \"JAD69\",\n\t4243: \"Kalianpur 1880\",\n\t4244: \"Kandawala\",\n\t4245: \"Kertau 1968\",\n\t4246: \"KOC\",\n\t4247: \"La Canoa\",\n\t4248: \"PSAD56\",\n\t4249: \"Lake\",\n\t4250: \"Leigon\",\n\t4251: \"Liberia 1964\",\n\t4252: \"Lome\",\n\t4253: \"Luzon 1911\",\n\t4254: \"Hito XVIII 1963\",\n\t4255: \"Herat North\",\n\t4256: \"Mahe 1971\",\n\t4257: \"Makassar\",\n\t4258: \"ETRS89\",\n\t4259: \"Malongo 1987\",\n\t4260: \"Manoca\",\n\t4261: \"Merchich\",\n\t4262: \"Massawa\",\n\t4263: \"Minna\",\n\t4264: \"Mhast\",\n\t4265: \"Monte Mario\",\n\t4266: \"M'poraloko\",\n\t4267: \"NAD27\",\n\t4268: \"NAD27 Michigan\",\n\t4269: \"NAD83\",\n\t4270: \"Nahrwan 1967\",\n\t4271: \"Naparima 1972\",\n\t4272: \"NZGD49\",\n\t4273: \"NGO 1948\",\n\t4274: \"Datum 73\",\n\t4275: \"NTF\",\n\t4276: \"NSWC 9Z-2\",\n\t4277: \"OSGB 1936\",\n\t4278: \"OSGB70\",\n\t4279: \"OS(SN)80\",\n\t4280: \"Padang\",\n\t4281: \"Palestine 1923\",\n\t4282: \"Pointe Noire\",\n\t4283: \"GDA94\",\n\t4284: \"Pulkovo 1942\",\n\t4285: \"Qatar 1974\",\n\t4286: \"Qatar 1948\",\n\t4287: \"Qornoq\",\n\t4288: \"Loma Quintana\",\n\t4289: \"Amersfoort\",\n\t4291: \"SAD69\",\n\t4292: \"Sapper Hill 1943\",\n\t4293: \"Schwarzeck\",\n\t4294: \"Segora\",\n\t4295: \"Serindung\",\n\t4296: \"Sudan\",\n\t4297: \"Tananarive\",\n\t4298: \"Timbalai 1948\",\n\t4299: \"TM65\",\n\t4300: \"TM75\",\n\t4301: \"Tokyo\",\n\t4302: \"Trinidad 1903\",\n\t4303: \"TC(1948)\",\n\t4304: \"Voirol 1875\",\n\t4306: \"Bern 1938\",\n\t4307: \"Nord Sahara 1959\",\n\t4308: \"RT38\",\n\t4309: \"Yacare\",\n\t4310: \"Yoff\",\n\t4311: \"Zanderij\",\n\t4312: \"MGI\",\n\t4313: \"Belge 1972\",\n\t4314: \"DHDN\",\n\t4315: \"Conakry 1905\",\n\t4316: \"Dealul Piscului 1930\",\n\t4317: \"Dealul Piscului 1970\",\n\t4318: \"NGN\",\n\t4319: \"KUDAMS\",\n\t4322: \"WGS 72\",\n\t4324: \"WGS 72BE\",\n\t4326: \"WGS 84\",\n\t4463: \"RGSPM06\",\n\t4470: \"RGM04\",\n\t4475: \"Cadastre 1997\",\n\t4483: \"Mexico ITRF92\",\n\t4490: \"China Geodetic Coordinate System 2000\",\n\t4555: \"New Beijing\",\n\t4558: \"RRAF 1991\",\n\t4600: \"Anguilla 1957\",\n\t4601: \"Antigua 1943\",\n\t4602: \"Dominica 1945\",\n\t4603: \"Grenada 1953\",\n\t4604: \"Montserrat 1958\",\n\t4605: \"St. Kitts 1955\",\n\t4606: \"St. Lucia 1955\",\n\t4607: \"St. Vincent 1945\",\n\t4608: \"NAD27(76)\",\n\t4609: \"NAD27(CGQ77)\",\n\t4610: \"Xian 1980\",\n\t4611: \"Hong Kong 1980\",\n\t4612: \"JGD2000\",\n\t4613: \"Segara\",\n\t4614: \"QND95\",\n\t4615: \"Porto Santo\",\n\t4616: \"Selvagem Grande\",\n\t4617: \"NAD83(CSRS)\",\n\t4618: \"SAD69\",\n\t4619: \"SWEREF99\",\n\t4620: \"Point 58\",\n\t4621: \"Fort Marigot\",\n\t4622: \"Guadeloupe 1948\",\n\t4623: \"CSG67\",\n\t4624: \"RGFG95\",\n\t4625: \"Martinique 1938\",\n\t4626: \"Reunion 1947\",\n\t4627: \"RGR92\",\n\t4628: \"Tahiti 52\",\n\t4629: \"Tahaa 54\",\n\t4630: \"IGN72 Nuku Hiva\",\n\t4631: \"K0 1949\",\n\t4632: \"Combani 1950\",\n\t4633: \"IGN56 Lifou\",\n\t4634: \"IGN72 Grand Terre\",\n\t4635: \"ST87 Ouvea\",\n\t4636: \"Petrels 1972\",\n\t4637: \"Perroud 1950\",\n\t4638: \"Saint Pierre et Miquelon 1950\",\n\t4639: \"MOP78\",\n\t4640: \"RRAF 1991\",\n\t4641: \"IGN53 Mare\",\n\t4642: \"ST84 Ile des Pins\",\n\t4643: \"ST71 Belep\",\n\t4644: \"NEA74 Noumea\",\n\t4645: \"RGNC 1991\",\n\t4646: \"Grand Comoros\",\n\t4657: \"Reykjavik 1900\",\n\t4658: \"Hjorsey 1955\",\n\t4659: \"ISN93\",\n\t4660: \"Helle 1954\",\n\t4661: \"LKS92\",\n\t4662: \"IGN72 Grande Terre\",\n\t4663: \"Porto Santo 1995\",\n\t4664: \"Azores Oriental 1995\",\n\t4665: \"Azores Central 1995\",\n\t4666: \"Lisbon 1890\",\n\t4667: \"IKBD-92\",\n\t4668: \"ED79\",\n\t4669: \"LKS94\",\n\t4670: \"IGM95\",\n\t4671: \"Voirol 1879\",\n\t4672: \"Chatham Islands 1971\",\n\t4673: \"Chatham Islands 1979\",\n\t4674: \"SIRGAS 2000\",\n\t4675: \"Guam 1963\",\n\t4676: \"Vientiane 1982\",\n\t4677: \"Lao 1993\",\n\t4678: \"Lao 1997\",\n\t4679: \"Jouik 1961\",\n\t4680: \"Nouakchott 1965\",\n\t4681: \"Mauritania 1999\",\n\t4682: \"Gulshan 303\",\n\t4683: \"PRS92\",\n\t4684: \"Gan 1970\",\n\t4685: \"Gandajika\",\n\t4686: \"MAGNA-SIRGAS\",\n\t4687: \"RGPF\",\n\t4688: \"Fatu Iva 72\",\n\t4689: \"IGN63 Hiva Oa\",\n\t4690: \"Tahiti 79\",\n\t4691: \"Moorea 87\",\n\t4692: \"Maupiti 83\",\n\t4693: \"Nakhl-e Ghanem\",\n\t4694: \"POSGAR 94\",\n\t4695: \"Katanga 1955\",\n\t4696: \"Kasai 1953\",\n\t4697: \"IGC 1962 6th Parallel South\",\n\t4698: \"IGN 1962 Kerguelen\",\n\t4699: \"Le Pouce 1934\",\n\t4700: \"IGN Astro 1960\",\n\t4701: \"IGCB 1955\",\n\t4702: \"Mauritania 1999\",\n\t4703: \"Mhast 1951\",\n\t4704: \"Mhast (onshore)\",\n\t4705: \"Mhast (offshore)\",\n\t4706: \"Egypt Gulf of Suez S-650 TL\",\n\t4707: \"Tern Island 1961\",\n\t4708: \"Cocos Islands 1965\",\n\t4709: \"Iwo Jima 1945\",\n\t4710: \"St. Helena 1971\",\n\t4711: \"Marcus Island 1952\",\n\t4712: \"Ascension Island 1958\",\n\t4713: \"Ayabelle Lighthouse\",\n\t4714: \"Bellevue\",\n\t4715: \"Camp Area Astro\",\n\t4716: \"Phoenix Islands 1966\",\n\t4717: \"Cape Canaveral\",\n\t4718: \"Solomon 1968\",\n\t4719: \"Easter Island 1967\",\n\t4720: \"Fiji 1986\",\n\t4721: \"Fiji 1956\",\n\t4722: \"South Georgia 1968\",\n\t4723: \"GCGD59\",\n\t4724: \"Diego Garcia 1969\",\n\t4725: \"Johnston Island 1961\",\n\t4726: \"SIGD61\",\n\t4727: \"Midway 1961\",\n\t4728: \"Pico de las Nieves 1984\",\n\t4729: \"Pitcairn 1967\",\n\t4730: \"Santo 1965\",\n\t4731: \"Viti Levu 1916\",\n\t4732: \"Marshall Islands 1960\",\n\t4733: \"Wake Island 1952\",\n\t4734: \"Tristan 1968\",\n\t4735: \"Kusaie 1951\",\n\t4736: \"Deception Island\",\n\t4737: \"Korea 2000\",\n\t4738: \"Hong Kong 1963\",\n\t4739: \"Hong Kong 1963(67)\",\n\t4740: \"PZ-90\",\n\t4741: \"FD54\",\n\t4742: \"GDM2000\",\n\t4743: \"Karbala 1979\",\n\t4744: \"Nahrwan 1934\",\n\t4745: \"RD/83\",\n\t4746: \"PD/83\",\n\t4747: \"GR96\",\n\t4748: \"Vanua Levu 1915\",\n\t4749: \"RGNC91-93\",\n\t4750: \"ST87 Ouvea\",\n\t4751: \"Kertau (RSO)\",\n\t4752: \"Viti Levu 1912\",\n\t4753: \"fk89\",\n\t4754: \"LGD2006\",\n\t4755: \"DGN95\",\n\t4756: \"VN-2000\",\n\t4757: \"SVY21\",\n\t4758: \"JAD2001\",\n\t4759: \"NAD83(NSRS2007)\",\n\t4760: \"WGS 66\",\n\t4761: \"HTRS96\",\n\t4762: \"BDA2000\",\n\t4763: \"Pitcairn 2006\",\n\t4764: \"RSRGD2000\",\n\t4765: \"Slovenia 1996\",\n\t4801: \"Bern 1898 (Bern)\",\n\t4802: \"Bogota 1975 (Bogota)\",\n\t4803: \"Lisbon (Lisbon)\",\n\t4804: \"Makassar (Jakarta)\",\n\t4805: \"MGI (Ferro)\",\n\t4806: \"Monte Mario (Rome)\",\n\t4807: \"NTF (Paris)\",\n\t4808: \"Padang (Jakarta)\",\n\t4809: \"Belge 1950 (Brussels)\",\n\t4810: \"Tananarive (Paris)\",\n\t4811: \"Voirol 1875 (Paris)\",\n\t4813: \"Batavia (Jakarta)\",\n\t4814: \"RT38 (Stockholm)\",\n\t4815: \"Greek (Athens)\",\n\t4816: \"Carthage (Paris)\",\n\t4817: \"NGO 1948 (Oslo)\",\n\t4818: \"S-JTSK (Ferro)\",\n\t4819: \"Nord Sahara 1959 (Paris)\",\n\t4820: \"Segara (Jakarta)\",\n\t4821: \"Voirol 1879 (Paris)\",\n\t4823: \"Sao Tome\",\n\t4824: \"Principe\",\n\t4901: \"ATF (Paris)\",\n\t4902: \"NDG (Paris)\",\n\t4903: \"Madrid 1870 (Madrid)\",\n\t4904: \"Lisbon 1890 (Lisbon)\",\n\t5013: \"PTRA08\",\n\t5132: \"Tokyo 1892\",\n\t5228: \"S-JTSK/05\",\n\t5229: \"S-JTSK/05 (Ferro)\",\n\t5233: \"SLD99\",\n\t5246: \"GDBD2009\",\n\t5252: \"TUREF\",\n\t5264: \"DRUKREF 03\",\n\t5324: \"ISN2004\",\n\t5340: \"POSGAR 2007\",\n\t5354: \"MARGEN\",\n\t5360: \"SIRGAS-Chile\",\n\t5365: \"CR05\",\n\t5371: \"MACARIO SOLIS\",\n\t5373: \"Peru96\",\n\t5381: \"SIRGAS-ROU98\",\n\t5393: \"SIRGAS_ES2007.8\",\n\t5451: \"Ocotepeque 1935\",\n\t5464: \"Sibun Gorge 1922\",\n\t5467: \"Panama-Colon 1911\",\n\t5489: \"RGAF09\",\n\t5524: \"Corrego Alegre 1961\",\n\t5527: \"SAD69(96)\",\n\t5546: \"PNG94\",\n\t5561: \"UCS-2000\",\n\t5593: \"FEH2010\",\n\t5681: \"DB_REF\",\n\t5886: \"TGD2005\",\n\t6135: \"CIGD11\",\n\t6207: \"Nepal 1981\",\n\t6311: \"CGRS93\",\n\t6318: \"NAD83(2011)\",\n\t6322: \"NAD83(PA11)\",\n\t6325: \"NAD83(MA11)\",\n\t6365: \"Mexico ITRF2008\",\n\t6668: \"JGD2011\",\n\t6706: \"RDN2008\",\n\t6783: \"NAD83(CORS96)\",\n\t6881: \"Aden 1925\",\n\t6882: \"Bekaa Valley 1920\",\n\t6883: \"Bioko\",\n\t6892: \"South East Island 1943\",\n\t6894: \"Gambia\",\n\t6980: \"IGD05\",\n\t6983: \"IG05 Intermediate CRS\",\n\t6987: \"IGD05/12\",\n\t6990: \"IG05/12 Intermediate CRS\",\n\t7035: \"RGSPM06 (lon-lat)\",\n\t7037: \"RGR92 (lon-lat)\",\n\t7039: \"RGM04 (lon-lat)\",\n\t7041: \"RGFG95 (lon-lat)\",\n\t7073: \"RGTAAF07\",\n\t7084: \"RGF93 (lon-lat)\",\n\t7086: \"RGAF09 (lon-lat)\",\n\t7088: \"RGTAAF07 (lon-lat)\",\n\t7133: \"RGTAAF07 (lon-lat)\",\n\t7136: \"IGD05\",\n\t7139: \"IGD05/12\",\n\t7373: \"ONGD14\",\n\t7686: \"Kyrg-06\",\n\t32767: \"User-defined\",\n\t61206405: \"Greek (deg)\",\n\t61216405: \"GGRS87 (deg)\",\n\t61226405: \"ATS77 (deg)\",\n\t61236405: \"KKJ (deg)\",\n\t61246405: \"RT90 (deg)\",\n\t61266405: \"LKS94 (ETRS89) (deg)\",\n\t61266413: \"LKS94 (ETRS89) (3D deg)\",\n\t61276405: \"Tete (deg)\",\n\t61286405: \"Madzansua (deg)\",\n\t61296405: \"Observatario (deg)\",\n\t61306405: \"Moznet (deg)\",\n\t61316405: \"Indian 1960 (deg)\",\n\t61326405: \"FD58 (deg)\",\n\t61336405: \"EST92 (deg)\",\n\t61346405: \"PDO Survey Datum 1993 (deg)\",\n\t61356405: \"Old Hawaiian (deg)\",\n\t61366405: \"St. Lawrence Island (deg)\",\n\t61376405: \"St. Paul Island (deg)\",\n\t61386405: \"St. George Island (deg)\",\n\t61396405: \"Puerto Rico (deg)\",\n\t61406405: \"NAD83(CSRS) (deg)\",\n\t61416405: \"Israel (deg)\",\n\t61426405: \"Locodjo 1965 (deg)\",\n\t61436405: \"Abidjan 1987 (deg)\",\n\t61446405: \"Kalianpur 1937 (deg)\",\n\t61456405: \"Kalianpur 1962 (deg)\",\n\t61466405: \"Kalianpur 1975 (deg)\",\n\t61476405: \"Hanoi 1972 (deg)\",\n\t61486405: \"Hartebeesthoek94 (deg)\",\n\t61496405: \"CH1903 (deg)\",\n\t61506405: \"CH1903+ (deg)\",\n\t61516405: \"CHTRF95 (deg)\",\n\t61526405: \"NAD83(HARN) (deg)\",\n\t61536405: \"Rassadiran (deg)\",\n\t61546405: \"ED50(ED77) (deg)\",\n\t61556405: \"Dabola 1981 (deg)\",\n\t61566405: \"S-JTSK (deg)\",\n\t61576405: \"Mount Dillon (deg)\",\n\t61586405: \"Naparima 1955 (deg)\",\n\t61596405: \"ELD79 (deg)\",\n\t61606405: \"Chos Malal 1914 (deg)\",\n\t61616405: \"Pampa del Castillo (deg)\",\n\t61626405: \"Korean 1985 (deg)\",\n\t61636405: \"Yemen NGN96 (deg)\",\n\t61646405: \"South Yemen (deg)\",\n\t61656405: \"Bissau (deg)\",\n\t61666405: \"Korean 1995 (deg)\",\n\t61676405: \"NZGD2000 (deg)\",\n\t61686405: \"Accra (deg)\",\n\t61696405: \"American Samoa 1962 (deg)\",\n\t61706405: \"SIRGAS (deg)\",\n\t61716405: \"RGF93 (deg)\",\n\t61736405: \"IRENET95 (deg)\",\n\t61746405: \"Sierra Leone 1924 (deg)\",\n\t61756405: \"Sierra Leone 1968 (deg)\",\n\t61766405: \"Australian Antarctic (deg)\",\n\t61786405: \"Pulkovo 1942(83) (deg)\",\n\t61796405: \"Pulkovo 1942(58) (deg)\",\n\t61806405: \"EST97 (deg)\",\n\t61816405: \"Luxembourg 1930 (deg)\",\n\t61826405: \"Azores Occidental 1939 (deg)\",\n\t61836405: \"Azores Central 1948 (deg)\",\n\t61846405: \"Azores Oriental 1940 (deg)\",\n\t61886405: \"OSNI 1952 (deg)\",\n\t61896405: \"REGVEN (deg)\",\n\t61906405: \"POSGAR 98 (deg)\",\n\t61916405: \"Albanian 1987 (deg)\",\n\t61926405: \"Douala 1948 (deg)\",\n\t61936405: \"Manoca 1962 (deg)\",\n\t61946405: \"Qornoq 1927 (deg)\",\n\t61956405: \"Scoresbysund 1952 (deg)\",\n\t61966405: \"Ammassalik 1958 (deg)\",\n\t61976405: \"Garoua (deg)\",\n\t61986405: \"Kousseri (deg)\",\n\t61996405: \"Egypt 1930 (deg)\",\n\t62006405: \"Pulkovo 1995 (deg)\",\n\t62016405: \"Adindan (deg)\",\n\t62026405: \"AGD66 (deg)\",\n\t62036405: \"AGD84 (deg)\",\n\t62046405: \"Ain el Abd (deg)\",\n\t62056405: \"Afgooye (deg)\",\n\t62066405: \"Agadez (deg)\",\n\t62076405: \"Lisbon (deg)\",\n\t62086405: \"Aratu (deg)\",\n\t62096405: \"Arc 1950 (deg)\",\n\t62106405: \"Arc 1960 (deg)\",\n\t62116405: \"Batavia (deg)\",\n\t62126405: \"Barbados 1938 (deg)\",\n\t62136405: \"Beduaram (deg)\",\n\t62146405: \"Beijing 1954 (deg)\",\n\t62156405: \"Belge 1950 (deg)\",\n\t62166405: \"Bermuda 1957 (deg)\",\n\t62186405: \"Bogota 1975 (deg)\",\n\t62196405: \"Bukit Rimpah (deg)\",\n\t62206405: \"Camacupa (deg)\",\n\t62216405: \"Campo Inchauspe (deg)\",\n\t62226405: \"Cape (deg)\",\n\t62236405: \"Carthage (deg)\",\n\t62246405: \"Chua (deg)\",\n\t62256405: \"Corrego Alegre (deg)\",\n\t62276405: \"Deir ez Zor (deg)\",\n\t62296405: \"Egypt 1907 (deg)\",\n\t62306405: \"ED50 (deg)\",\n\t62316405: \"ED87 (deg)\",\n\t62326405: \"Fahud (deg)\",\n\t62336405: \"Gandajika 1970 (deg)\",\n\t62366405: \"Hu Tzu Shan (deg)\",\n\t62376405: \"HD72 (deg)\",\n\t62386405: \"ID74 (deg)\",\n\t62396405: \"Indian 1954 (deg)\",\n\t62406405: \"Indian 1975 (deg)\",\n\t62416405: \"Jamaica 1875 (deg)\",\n\t62426405: \"JAD69 (deg)\",\n\t62436405: \"Kalianpur 1880 (deg)\",\n\t62446405: \"Kandawala (deg)\",\n\t62456405: \"Kertau (deg)\",\n\t62466405: \"KOC (deg)\",\n\t62476405: \"La Canoa (deg)\",\n\t62486405: \"PSAD56 (deg)\",\n\t62496405: \"Lake (deg)\",\n\t62506405: \"Leigon (deg)\",\n\t62516405: \"Liberia 1964 (deg)\",\n\t62526405: \"Lome (deg)\",\n\t62536405: \"Luzon 1911 (deg)\",\n\t62546405: \"Hito XVIII 1963 (deg)\",\n\t62556405: \"Herat North (deg)\",\n\t62566405: \"Mahe 1971 (deg)\",\n\t62576405: \"Makassar (deg)\",\n\t62586405: \"ETRS89 (deg)\",\n\t62596405: \"Malongo 1987 (deg)\",\n\t62616405: \"Merchich (deg)\",\n\t62626405: \"Massawa (deg)\",\n\t62636405: \"Minna (deg)\",\n\t62646405: \"Mhast (deg)\",\n\t62656405: \"Monte Mario (deg)\",\n\t62666405: \"M'poraloko (deg)\",\n\t62676405: \"NAD27 (deg)\",\n\t62686405: \"NAD27 Michigan (deg)\",\n\t62696405: \"NAD83 (deg)\",\n\t62706405: \"Nahrwan 1967 (deg)\",\n\t62716405: \"Naparima 1972 (deg)\",\n\t62726405: \"NZGD49 (deg)\",\n\t62736405: \"NGO 1948 (deg)\",\n\t62746405: \"Datum 73 (deg)\",\n\t62756405: \"NTF (deg)\",\n\t62766405: \"NSWC 9Z-2 (deg)\",\n\t62776405: \"OSGB 1936 (deg)\",\n\t62786405: \"OSGB70 (deg)\",\n\t62796405: \"OS(SN)80 (deg)\",\n\t62806405: \"Padang (deg)\",\n\t62816405: \"Palestine 1923 (deg)\",\n\t62826405: \"Pointe Noire (deg)\",\n\t62836405: \"GDA94 (deg)\",\n\t62846405: \"Pulkovo 1942 (deg)\",\n\t62856405: \"Qatar 1974 (deg)\",\n\t62866405: \"Qatar 1948 (deg)\",\n\t62886405: \"Loma Quintana (deg)\",\n\t62896405: \"Amersfoort (deg)\",\n\t62926405: \"Sapper Hill 1943 (deg)\",\n\t62936405: \"Schwarzeck (deg)\",\n\t62956405: \"Serindung (deg)\",\n\t62976405: \"Tananarive (deg)\",\n\t62986405: \"Timbalai 1948 (deg)\",\n\t62996405: \"TM65 (deg)\",\n\t63006405: \"TM75 (deg)\",\n\t63016405: \"Tokyo (deg)\",\n\t63026405: \"Trinidad 1903 (deg)\",\n\t63036405: \"TC(1948) (deg)\",\n\t63046405: \"Voirol 1875 (deg)\",\n\t63066405: \"Bern 1938 (deg)\",\n\t63076405: \"Nord Sahara 1959 (deg)\",\n\t63086405: \"RT38 (deg)\",\n\t63096405: \"Yacare (deg)\",\n\t63106405: \"Yoff (deg)\",\n\t63116405: \"Zanderij (deg)\",\n\t63126405: \"MGI (deg)\",\n\t63136405: \"Belge 1972 (deg)\",\n\t63146405: \"DHDN (deg)\",\n\t63156405: \"Conakry 1905 (deg)\",\n\t63166405: \"Dealul Piscului 1933 (deg)\",\n\t63176405: \"Dealul Piscului 1970 (deg)\",\n\t63186405: \"NGN (deg)\",\n\t63196405: \"KUDAMS (deg)\",\n\t63226405: \"WGS 72 (deg)\",\n\t63246405: \"WGS 72BE (deg)\",\n\t63266405: \"WGS 84 (deg)\",\n\t63266406: \"WGS 84 (degH)\",\n\t63266407: \"WGS 84 (Hdeg)\",\n\t63266408: \"WGS 84 (DM)\",\n\t63266409: \"WGS 84 (DMH)\",\n\t63266410: \"WGS 84 (HDM)\",\n\t63266411: \"WGS 84 (DMS)\",\n\t63266412: \"WGS 84 (HDMS)\",\n\t66006405: \"Anguilla 1957 (deg)\",\n\t66016405: \"Antigua 1943 (deg)\",\n\t66026405: \"Dominica 1945 (deg)\",\n\t66036405: \"Grenada 1953 (deg)\",\n\t66046405: \"Montserrat 1958 (deg)\",\n\t66056405: \"St. Kitts 1955 (deg)\",\n\t66066405: \"St. Lucia 1955 (deg)\",\n\t66076405: \"St. Vincent 1945 (deg)\",\n\t66086405: \"NAD27(76) (deg)\",\n\t66096405: \"NAD27(CGQ77) (deg)\",\n\t66106405: \"Xian 1980 (deg)\",\n\t66116405: \"Hong Kong 1980 (deg)\",\n\t66126405: \"JGD2000 (deg)\",\n\t66136405: \"Segara (deg)\",\n\t66146405: \"QND95 (deg)\",\n\t66156405: \"Porto Santo (deg)\",\n\t66166405: \"Selvagem Grande (deg)\",\n\t66186405: \"SAD69 (deg)\",\n\t66196405: \"SWEREF99 (deg)\",\n\t66206405: \"Point 58 (deg)\",\n\t66216405: \"Fort Marigot (deg)\",\n\t66226405: \"Sainte Anne (deg)\",\n\t66236405: \"CSG67 (deg)\",\n\t66246405: \"RGFG95 (deg)\",\n\t66256405: \"Fort Desaix (deg)\",\n\t66266405: \"Piton des Neiges (deg)\",\n\t66276405: \"RGR92 (deg)\",\n\t66286405: \"Tahiti (deg)\",\n\t66296405: \"Tahaa (deg)\",\n\t66306405: \"IGN72 Nuku Hiva (deg)\",\n\t66316405: \"K0 1949 (deg)\",\n\t66326405: \"Combani 1950 (deg)\",\n\t66336405: \"IGN56 Lifou (deg)\",\n\t66346405: \"IGN72 Grande Terre (deg)\",\n\t66356405: \"ST87 Ouvea (deg)\",\n\t66366405: \"Petrels 1972 (deg)\",\n\t66376405: \"Perroud 1950 (deg)\",\n\t66386405: \"Saint Pierre et Miquelon 1950 (deg)\",\n\t66396405: \"MOP78 (deg)\",\n\t66406405: \"RRAF 1991 (deg)\",\n\t66416405: \"IGN53 Mare (deg)\",\n\t66426405: \"ST84 Ile des Pins (deg)\",\n\t66436405: \"ST71 Belep (deg)\",\n\t66446405: \"NEA74 Noumea (deg)\",\n\t66456405: \"RGNC 1991 (deg)\",\n\t66466405: \"Grand Comoros (deg)\",\n\t66576405: \"Reykjavik 1900 (deg)\",\n\t66586405: \"Hjorsey 1955 (deg)\",\n\t66596405: \"ISN93 (deg)\",\n\t66606405: \"Helle 1954 (deg)\",\n\t66616405: \"LKS92 (deg)\",\n\t66636405: \"Porto Santo 1995 (deg)\",\n\t66646405: \"Azores Oriental 1995 (deg)\",\n\t66656405: \"Azores Central 1995 (deg)\",\n\t66666405: \"Lisbon 1890 (deg)\",\n\t66676405: \"IKBD-92 (deg)\",\n\t68016405: \"Bern 1898 (Bern) (deg)\",\n\t68026405: \"Bogota 1975 (Bogota) (deg)\",\n\t68036405: \"Lisbon (Lisbon) (deg)\",\n\t68046405: \"Makassar (Jakarta) (deg)\",\n\t68056405: \"MGI (Ferro) (deg)\",\n\t68066405: \"Monte Mario (Rome) (deg)\",\n\t68086405: \"Padang (Jakarta) (deg)\",\n\t68096405: \"Belge 1950 (Brussels) (deg)\",\n\t68136405: \"Batavia (Jakarta) (deg)\",\n\t68146405: \"RT38 (Stockholm) (deg)\",\n\t68156405: \"Greek (Athens) (deg)\",\n\t68186405: \"S-JTSK (Ferro) (deg)\",\n\t68206405: \"Segara (Jakarta) (deg)\",\n\t69036405: \"Madrid 1870 (Madrid) (deg)\"\n}\n\n\nProjectedCSTypeGeoKey = {\n\t2000: \"Anguilla 1957 / British West Indies Grid\",\n\t2001: \"Antigua 1943 / British West Indies Grid\",\n\t2002: \"Dominica 1945 / British West Indies Grid\",\n\t2003: \"Grenada 1953 / British West Indies Grid\",\n\t2004: \"Montserrat 1958 / British West Indies Grid\",\n\t2005: \"St. Kitts 1955 / British West Indies Grid\",\n\t2006: \"St. Lucia 1955 / British West Indies Grid\",\n\t2007: \"St. Vincent 45 / British West Indies Grid\",\n\t2008: \"NAD27(CGQ77) / SCoPQ zone 2\",\n\t2009: \"NAD27(CGQ77) / SCoPQ zone 3\",\n\t2010: \"NAD27(CGQ77) / SCoPQ zone 4\",\n\t2011: \"NAD27(CGQ77) / SCoPQ zone 5\",\n\t2012: \"NAD27(CGQ77) / SCoPQ zone 6\",\n\t2013: \"NAD27(CGQ77) / SCoPQ zone 7\",\n\t2014: \"NAD27(CGQ77) / SCoPQ zone 8\",\n\t2015: \"NAD27(CGQ77) / SCoPQ zone 9\",\n\t2016: \"NAD27(CGQ77) / SCoPQ zone 10\",\n\t2017: \"NAD27(76) / MTM zone 8\",\n\t2018: \"NAD27(76) / MTM zone 9\",\n\t2019: \"NAD27(76) / MTM zone 10\",\n\t2020: \"NAD27(76) / MTM zone 11\",\n\t2021: \"NAD27(76) / MTM zone 12\",\n\t2022: \"NAD27(76) / MTM zone 13\",\n\t2023: \"NAD27(76) / MTM zone 14\",\n\t2024: \"NAD27(76) / MTM zone 15\",\n\t2025: \"NAD27(76) / MTM zone 16\",\n\t2026: \"NAD27(76) / MTM zone 17\",\n\t2027: \"NAD27(76) / UTM zone 15N\",\n\t2028: \"NAD27(76) / UTM zone 16N\",\n\t2029: \"NAD27(76) / UTM zone 17N\",\n\t2030: \"NAD27(76) / UTM zone 18N\",\n\t2031: \"NAD27(CGQ77) / UTM zone 17N\",\n\t2032: \"NAD27(CGQ77) / UTM zone 18N\",\n\t2033: \"NAD27(CGQ77) / UTM zone 19N\",\n\t2034: \"NAD27(CGQ77) / UTM zone 20N\",\n\t2035: \"NAD27(CGQ77) / UTM zone 21N\",\n\t2036: \"NAD83(CSRS98) / New Brunswick Stereo\",\n\t2037: \"NAD83(CSRS98) / UTM zone 19N\",\n\t2038: \"NAD83(CSRS98) / UTM zone 20N\",\n\t2039: \"Israel 1993 / Israeli TM Grid\",\n\t2040: \"Locodjo 1965 / UTM zone 30N\",\n\t2041: \"Abidjan 1987 / UTM zone 30N\",\n\t2042: \"Locodjo 1965 / UTM zone 29N\",\n\t2043: \"Abidjan 1987 / UTM zone 29N\",\n\t2044: \"Hanoi 1972 / Gauss-Kruger zone 18\",\n\t2045: \"Hanoi 1972 / Gauss-Kruger zone 19\",\n\t2046: \"Hartebeesthoek94 / Lo15\",\n\t2047: \"Hartebeesthoek94 / Lo17\",\n\t2048: \"Hartebeesthoek94 / Lo19\",\n\t2049: \"Hartebeesthoek94 / Lo21\",\n\t2050: \"Hartebeesthoek94 / Lo23\",\n\t2051: \"Hartebeesthoek94 / Lo25\",\n\t2052: \"Hartebeesthoek94 / Lo27\",\n\t2053: \"Hartebeesthoek94 / Lo29\",\n\t2054: \"Hartebeesthoek94 / Lo31\",\n\t2055: \"Hartebeesthoek94 / Lo33\",\n\t2056: \"CH1903+ / LV95\",\n\t2057: \"Rassadiran / Nakhl e Taqi\",\n\t2058: \"ED50(ED77) / UTM zone 38N\",\n\t2059: \"ED50(ED77) / UTM zone 39N\",\n\t2060: \"ED50(ED77) / UTM zone 40N\",\n\t2061: \"ED50(ED77) / UTM zone 41N\",\n\t2062: \"Madrid 1870 (Madrid) / Spain\",\n\t2063: \"Dabola 1981 / UTM zone 28N\",\n\t2064: \"Dabola 1981 / UTM zone 29N\",\n\t2065: \"S-JTSK (Ferro) / Krovak\",\n\t2066: \"Mount Dillon / Tobago Grid\",\n\t2067: \"Naparima 1955 / UTM zone 20N\",\n\t2068: \"ELD79 / Libya zone 5\",\n\t2069: \"ELD79 / Libya zone 6\",\n\t2070: \"ELD79 / Libya zone 7\",\n\t2071: \"ELD79 / Libya zone 8\",\n\t2072: \"ELD79 / Libya zone 9\",\n\t2073: \"ELD79 / Libya zone 10\",\n\t2074: \"ELD79 / Libya zone 11\",\n\t2075: \"ELD79 / Libya zone 12\",\n\t2076: \"ELD79 / Libya zone 13\",\n\t2077: \"ELD79 / UTM zone 32N\",\n\t2078: \"ELD79 / UTM zone 33N\",\n\t2079: \"ELD79 / UTM zone 34N\",\n\t2080: \"ELD79 / UTM zone 35N\",\n\t2081: \"Chos Malal 1914 / Argentina 2\",\n\t2082: \"Pampa del Castillo / Argentina 2\",\n\t2083: \"Hito XVIII 1963 / Argentina 2\",\n\t2084: \"Hito XVIII 1963 / UTM zone 19S\",\n\t2085: \"NAD27 / Cuba Norte\",\n\t2086: \"NAD27 / Cuba Sur\",\n\t2087: \"ELD79 / TM 12 NE\",\n\t2088: \"Carthage / TM 11 NE\",\n\t2089: \"Yemen NGN96 / UTM zone 38N\",\n\t2090: \"Yemen NGN96 / UTM zone 39N\",\n\t2091: \"South Yemen / Gauss Kruger zone 8\",\n\t2092: \"South Yemen / Gauss Kruger zone 9\",\n\t2093: \"Hanoi 1972 / GK 106 NE\",\n\t2094: \"WGS 72BE / TM 106 NE\",\n\t2095: \"Bissau / UTM zone 28N\",\n\t2096: \"Korean 1985 / East Belt\",\n\t2097: \"Korean 1985 / Central Belt\",\n\t2098: \"Korean 1985 / West Belt\",\n\t2099: \"Qatar 1948 / Qatar Grid\",\n\t2100: \"GGRS87 / Greek Grid\",\n\t2101: \"Lake / Maracaibo Grid M1\",\n\t2102: \"Lake / Maracaibo Grid\",\n\t2103: \"Lake / Maracaibo Grid M3\",\n\t2104: \"Lake / Maracaibo La Rosa Grid\",\n\t2105: \"NZGD2000 / Mount Eden 2000\",\n\t2106: \"NZGD2000 / Bay of Plenty 2000\",\n\t2107: \"NZGD2000 / Poverty Bay 2000\",\n\t2108: \"NZGD2000 / Hawkes Bay 2000\",\n\t2109: \"NZGD2000 / Taranaki 2000\",\n\t2110: \"NZGD2000 / Tuhirangi 2000\",\n\t2111: \"NZGD2000 / Wanganui 2000\",\n\t2112: \"NZGD2000 / Wairarapa 2000\",\n\t2113: \"NZGD2000 / Wellington 2000\",\n\t2114: \"NZGD2000 / Collingwood 2000\",\n\t2115: \"NZGD2000 / Nelson 2000\",\n\t2116: \"NZGD2000 / Karamea 2000\",\n\t2117: \"NZGD2000 / Buller 2000\",\n\t2118: \"NZGD2000 / Grey 2000\",\n\t2119: \"NZGD2000 / Amuri 2000\",\n\t2120: \"NZGD2000 / Marlborough 2000\",\n\t2121: \"NZGD2000 / Hokitika 2000\",\n\t2122: \"NZGD2000 / Okarito 2000\",\n\t2123: \"NZGD2000 / Jacksons Bay 2000\",\n\t2124: \"NZGD2000 / Mount Pleasant 2000\",\n\t2125: \"NZGD2000 / Gawler 2000\",\n\t2126: \"NZGD2000 / Timaru 2000\",\n\t2127: \"NZGD2000 / Lindis Peak 2000\",\n\t2128: \"NZGD2000 / Mount Nicholas 2000\",\n\t2129: \"NZGD2000 / Mount York 2000\",\n\t2130: \"NZGD2000 / Observation Point 2000\",\n\t2131: \"NZGD2000 / North Taieri 2000\",\n\t2132: \"NZGD2000 / Bluff 2000\",\n\t2133: \"NZGD2000 / UTM zone 58S\",\n\t2134: \"NZGD2000 / UTM zone 59S\",\n\t2135: \"NZGD2000 / UTM zone 60S\",\n\t2136: \"Accra / Ghana National Grid\",\n\t2137: \"Accra / TM 1 NW\",\n\t2138: \"NAD27(CGQ77) / Quebec Lambert\",\n\t2139: \"NAD83(CSRS98) / SCoPQ zone 2\",\n\t2140: \"NAD83(CSRS98) / MTM zone 3\",\n\t2141: \"NAD83(CSRS98) / MTM zone 4\",\n\t2142: \"NAD83(CSRS98) / MTM zone 5\",\n\t2143: \"NAD83(CSRS98) / MTM zone 6\",\n\t2144: \"NAD83(CSRS98) / MTM zone 7\",\n\t2145: \"NAD83(CSRS98) / MTM zone 8\",\n\t2146: \"NAD83(CSRS98) / MTM zone 9\",\n\t2147: \"NAD83(CSRS98) / MTM zone 10\",\n\t2148: \"NAD83(CSRS98) / UTM zone 21N\",\n\t2149: \"NAD83(CSRS98) / UTM zone 18N\",\n\t2150: \"NAD83(CSRS98) / UTM zone 17N\",\n\t2151: \"NAD83(CSRS98) / UTM zone 13N\",\n\t2152: \"NAD83(CSRS98) / UTM zone 12N\",\n\t2153: \"NAD83(CSRS98) / UTM zone 11N\",\n\t2154: \"RGF93 / Lambert-93\",\n\t2155: \"American Samoa 1962 / American Samoa Lambert\",\n\t2156: \"NAD83(HARN) / UTM zone 59S\",\n\t2157: \"IRENET95 / Irish Transverse Mercator\",\n\t2158: \"IRENET95 / UTM zone 29N\",\n\t2159: \"Sierra Leone 1924 / New Colony Grid\",\n\t2160: \"Sierra Leone 1924 / New War Office Grid\",\n\t2161: \"Sierra Leone 1968 / UTM zone 28N\",\n\t2162: \"Sierra Leone 1968 / UTM zone 29N\",\n\t2163: \"US National Atlas Equal Area\",\n\t2164: \"Locodjo 1965 / TM 5 NW\",\n\t2165: \"Abidjan 1987 / TM 5 NW\",\n\t2166: \"Pulkovo 1942(83) / Gauss Kruger zone 3\",\n\t2167: \"Pulkovo 1942(83) / Gauss Kruger zone 4\",\n\t2168: \"Pulkovo 1942(83) / Gauss Kruger zone 5\",\n\t2169: \"Luxembourg 1930 / Gauss\",\n\t2170: \"MGI / Slovenia Grid\",\n\t2171: \"Pulkovo 1942(58) / Poland zone I\",\n\t2172: \"Pulkovo 1942(58) / Poland zone II\",\n\t2173: \"Pulkovo 1942(58) / Poland zone III\",\n\t2174: \"Pulkovo 1942(58) / Poland zone IV\",\n\t2175: \"Pulkovo 1942(58) / Poland zone V\",\n\t2176: \"ETRS89 / Poland CS2000 zone 5\",\n\t2177: \"ETRS89 / Poland CS2000 zone 6\",\n\t2178: \"ETRS89 / Poland CS2000 zone 7\",\n\t2179: \"ETRS89 / Poland CS2000 zone 8\",\n\t2180: \"ETRS89 / Poland CS92\",\n\t2188: \"Azores Occidental 1939 / UTM zone 25N\",\n\t2189: \"Azores Central 1948 / UTM zone 26N\",\n\t2190: \"Azores Oriental 1940 / UTM zone 26N\",\n\t2191: \"Madeira 1936 / UTM zone 28N\",\n\t2192: \"ED50 / France EuroLambert\",\n\t2193: \"NZGD2000 / New Zealand Transverse Mercator 2000\",\n\t2194: \"American Samoa 1962 / American Samoa Lambert\",\n\t2195: \"NAD83(HARN) / UTM zone 2S\",\n\t2196: \"ETRS89 / Kp2000 Jutland\",\n\t2197: \"ETRS89 / Kp2000 Zealand\",\n\t2198: \"ETRS89 / Kp2000 Bornholm\",\n\t2199: \"Albanian 1987 / Gauss Kruger zone 4\",\n\t2200: \"ATS77 / New Brunswick Stereographic (ATS77)\",\n\t2201: \"REGVEN / UTM zone 18N\",\n\t2202: \"REGVEN / UTM zone 19N\",\n\t2203: \"REGVEN / UTM zone 20N\",\n\t2204: \"NAD27 / Tennessee\",\n\t2205: \"NAD83 / Kentucky North\",\n\t2206: \"ED50 / 3-degree Gauss-Kruger zone 9\",\n\t2207: \"ED50 / 3-degree Gauss-Kruger zone 10\",\n\t2208: \"ED50 / 3-degree Gauss-Kruger zone 11\",\n\t2209: \"ED50 / 3-degree Gauss-Kruger zone 12\",\n\t2210: \"ED50 / 3-degree Gauss-Kruger zone 13\",\n\t2211: \"ED50 / 3-degree Gauss-Kruger zone 14\",\n\t2212: \"ED50 / 3-degree Gauss-Kruger zone 15\",\n\t2213: \"ETRS89 / TM 30 NE\",\n\t2214: \"Douala 1948 / AOF west\",\n\t2215: \"Manoca 1962 / UTM zone 32N\",\n\t2216: \"Qornoq 1927 / UTM zone 22N\",\n\t2217: \"Qornoq 1927 / UTM zone 23N\",\n\t2218: \"Scoresbysund 1952 / Greenland zone 5 east\",\n\t2219: \"ATS77 / UTM zone 19N\",\n\t2220: \"ATS77 / UTM zone 20N\",\n\t2221: \"Scoresbysund 1952 / Greenland zone 6 east\",\n\t2222: \"NAD83 / Arizona East (ft)\",\n\t2223: \"NAD83 / Arizona Central (ft)\",\n\t2224: \"NAD83 / Arizona West (ft)\",\n\t2225: \"NAD83 / California zone 1 (ftUS)\",\n\t2226: \"NAD83 / California zone 2 (ftUS)\",\n\t2227: \"NAD83 / California zone 3 (ftUS)\",\n\t2228: \"NAD83 / California zone 4 (ftUS)\",\n\t2229: \"NAD83 / California zone 5 (ftUS)\",\n\t2230: \"NAD83 / California zone 6 (ftUS)\",\n\t2231: \"NAD83 / Colorado North (ftUS)\",\n\t2232: \"NAD83 / Colorado Central (ftUS)\",\n\t2233: \"NAD83 / Colorado South (ftUS)\",\n\t2234: \"NAD83 / Connecticut (ftUS)\",\n\t2235: \"NAD83 / Delaware (ftUS)\",\n\t2236: \"NAD83 / Florida East (ftUS)\",\n\t2237: \"NAD83 / Florida West (ftUS)\",\n\t2238: \"NAD83 / Florida North (ftUS)\",\n\t2239: \"NAD83 / Georgia East (ftUS)\",\n\t2240: \"NAD83 / Georgia West (ftUS)\",\n\t2241: \"NAD83 / Idaho East (ftUS)\",\n\t2242: \"NAD83 / Idaho Central (ftUS)\",\n\t2243: \"NAD83 / Idaho West (ftUS)\",\n\t2244: \"NAD83 / Indiana East (ftUS)\",\n\t2245: \"NAD83 / Indiana West (ftUS)\",\n\t2246: \"NAD83 / Kentucky North (ftUS)\",\n\t2247: \"NAD83 / Kentucky South (ftUS)\",\n\t2248: \"NAD83 / Maryland (ftUS)\",\n\t2249: \"NAD83 / Massachusetts Mainland (ftUS)\",\n\t2250: \"NAD83 / Massachusetts Island (ftUS)\",\n\t2251: \"NAD83 / Michigan North (ft)\",\n\t2252: \"NAD83 / Michigan Central (ft)\",\n\t2253: \"NAD83 / Michigan South (ft)\",\n\t2254: \"NAD83 / Mississippi East (ftUS)\",\n\t2255: \"NAD83 / Mississippi West (ftUS)\",\n\t2256: \"NAD83 / Montana (ft)\",\n\t2257: \"NAD83 / New Mexico East (ftUS)\",\n\t2258: \"NAD83 / New Mexico Central (ftUS)\",\n\t2259: \"NAD83 / New Mexico West (ftUS)\",\n\t2260: \"NAD83 / New York East (ftUS)\",\n\t2261: \"NAD83 / New York Central (ftUS)\",\n\t2262: \"NAD83 / New York West (ftUS)\",\n\t2263: \"NAD83 / New York Long Island (ftUS)\",\n\t2264: \"NAD83 / North Carolina (ftUS)\",\n\t2265: \"NAD83 / North Dakota North (ft)\",\n\t2266: \"NAD83 / North Dakota South (ft)\",\n\t2267: \"NAD83 / Oklahoma North (ftUS)\",\n\t2268: \"NAD83 / Oklahoma South (ftUS)\",\n\t2269: \"NAD83 / Oregon North (ft)\",\n\t2270: \"NAD83 / Oregon South (ft)\",\n\t2271: \"NAD83 / Pennsylvania North (ftUS)\",\n\t2272: \"NAD83 / Pennsylvania South (ftUS)\",\n\t2273: \"NAD83 / South Carolina (ft)\",\n\t2274: \"NAD83 / Tennessee (ftUS)\",\n\t2275: \"NAD83 / Texas North (ftUS)\",\n\t2276: \"NAD83 / Texas North Central (ftUS)\",\n\t2277: \"NAD83 / Texas Central (ftUS)\",\n\t2278: \"NAD83 / Texas South Central (ftUS)\",\n\t2279: \"NAD83 / Texas South (ftUS)\",\n\t2280: \"NAD83 / Utah North (ft)\",\n\t2281: \"NAD83 / Utah Central (ft)\",\n\t2282: \"NAD83 / Utah South (ft)\",\n\t2283: \"NAD83 / Virginia North (ftUS)\",\n\t2284: \"NAD83 / Virginia South (ftUS)\",\n\t2285: \"NAD83 / Washington North (ftUS)\",\n\t2286: \"NAD83 / Washington South (ftUS)\",\n\t2287: \"NAD83 / Wisconsin North (ftUS)\",\n\t2288: \"NAD83 / Wisconsin Central (ftUS)\",\n\t2289: \"NAD83 / Wisconsin South (ftUS)\",\n\t2290: \"ATS77 / Prince Edward Isl. Stereographic (ATS77)\",\n\t2291: \"NAD83(CSRS98) / Prince Edward Isl. Stereographic (NAD83)\",\n\t2292: \"NAD83(CSRS98) / Prince Edward Isl. Stereographic (NAD83)\",\n\t2294: \"ATS77 / MTM Nova Scotia zone 4\",\n\t2295: \"ATS77 / MTM Nova Scotia zone 5\",\n\t2296: \"Ammassalik 1958 / Greenland zone 7 east\",\n\t2297: \"Qornoq 1927 / Greenland zone 1 east\",\n\t2298: \"Qornoq 1927 / Greenland zone 2 east\",\n\t2299: \"Qornoq 1927 / Greenland zone 2 west\",\n\t2300: \"Qornoq 1927 / Greenland zone 3 east\",\n\t2301: \"Qornoq 1927 / Greenland zone 3 west\",\n\t2302: \"Qornoq 1927 / Greenland zone 4 east\",\n\t2303: \"Qornoq 1927 / Greenland zone 4 west\",\n\t2304: \"Qornoq 1927 / Greenland zone 5 west\",\n\t2305: \"Qornoq 1927 / Greenland zone 6 west\",\n\t2306: \"Qornoq 1927 / Greenland zone 7 west\",\n\t2307: \"Qornoq 1927 / Greenland zone 8 east\",\n\t2308: \"Batavia / TM 109 SE\",\n\t2309: \"WGS 84 / TM 116 SE\",\n\t2310: \"WGS 84 / TM 132 SE\",\n\t2311: \"WGS 84 / TM 6 NE\",\n\t2312: \"Garoua / UTM zone 33N\",\n\t2313: \"Kousseri / UTM zone 33N\",\n\t2314: \"Trinidad 1903 / Trinidad Grid (ftCla)\",\n\t2315: \"Campo Inchauspe / UTM zone 19S\",\n\t2316: \"Campo Inchauspe / UTM zone 20S\",\n\t2317: \"PSAD56 / ICN Regional\",\n\t2318: \"Ain el Abd / Aramco Lambert\",\n\t2319: \"ED50 / TM27\",\n\t2320: \"ED50 / TM30\",\n\t2321: \"ED50 / TM33\",\n\t2322: \"ED50 / TM36\",\n\t2323: \"ED50 / TM39\",\n\t2324: \"ED50 / TM42\",\n\t2325: \"ED50 / TM45\",\n\t2326: \"Hong Kong 1980 Grid System\",\n\t2327: \"Xian 1980 / Gauss-Kruger zone 13\",\n\t2328: \"Xian 1980 / Gauss-Kruger zone 14\",\n\t2329: \"Xian 1980 / Gauss-Kruger zone 15\",\n\t2330: \"Xian 1980 / Gauss-Kruger zone 16\",\n\t2331: \"Xian 1980 / Gauss-Kruger zone 17\",\n\t2332: \"Xian 1980 / Gauss-Kruger zone 18\",\n\t2333: \"Xian 1980 / Gauss-Kruger zone 19\",\n\t2334: \"Xian 1980 / Gauss-Kruger zone 20\",\n\t2335: \"Xian 1980 / Gauss-Kruger zone 21\",\n\t2336: \"Xian 1980 / Gauss-Kruger zone 22\",\n\t2337: \"Xian 1980 / Gauss-Kruger zone 23\",\n\t2338: \"Xian 1980 / Gauss-Kruger CM 75E\",\n\t2339: \"Xian 1980 / Gauss-Kruger CM 81E\",\n\t2340: \"Xian 1980 / Gauss-Kruger CM 87E\",\n\t2341: \"Xian 1980 / Gauss-Kruger CM 93E\",\n\t2342: \"Xian 1980 / Gauss-Kruger CM 99E\",\n\t2343: \"Xian 1980 / Gauss-Kruger CM 105E\",\n\t2344: \"Xian 1980 / Gauss-Kruger CM 111E\",\n\t2345: \"Xian 1980 / Gauss-Kruger CM 117E\",\n\t2346: \"Xian 1980 / Gauss-Kruger CM 123E\",\n\t2347: \"Xian 1980 / Gauss-Kruger CM 129E\",\n\t2348: \"Xian 1980 / Gauss-Kruger CM 135E\",\n\t2349: \"Xian 1980 / 3-degree Gauss-Kruger zone 25\",\n\t2350: \"Xian 1980 / 3-degree Gauss-Kruger zone 26\",\n\t2351: \"Xian 1980 / 3-degree Gauss-Kruger zone 27\",\n\t2352: \"Xian 1980 / 3-degree Gauss-Kruger zone 28\",\n\t2353: \"Xian 1980 / 3-degree Gauss-Kruger zone 29\",\n\t2354: \"Xian 1980 / 3-degree Gauss-Kruger zone 30\",\n\t2355: \"Xian 1980 / 3-degree Gauss-Kruger zone 31\",\n\t2356: \"Xian 1980 / 3-degree Gauss-Kruger zone 32\",\n\t2357: \"Xian 1980 / 3-degree Gauss-Kruger zone 33\",\n\t2358: \"Xian 1980 / 3-degree Gauss-Kruger zone 34\",\n\t2359: \"Xian 1980 / 3-degree Gauss-Kruger zone 35\",\n\t2360: \"Xian 1980 / 3-degree Gauss-Kruger zone 36\",\n\t2361: \"Xian 1980 / 3-degree Gauss-Kruger zone 37\",\n\t2362: \"Xian 1980 / 3-degree Gauss-Kruger zone 38\",\n\t2363: \"Xian 1980 / 3-degree Gauss-Kruger zone 39\",\n\t2364: \"Xian 1980 / 3-degree Gauss-Kruger zone 40\",\n\t2365: \"Xian 1980 / 3-degree Gauss-Kruger zone 41\",\n\t2366: \"Xian 1980 / 3-degree Gauss-Kruger zone 42\",\n\t2367: \"Xian 1980 / 3-degree Gauss-Kruger zone 43\",\n\t2368: \"Xian 1980 / 3-degree Gauss-Kruger zone 44\",\n\t2369: \"Xian 1980 / 3-degree Gauss-Kruger zone 45\",\n\t2370: \"Xian 1980 / 3-degree Gauss-Kruger CM 75E\",\n\t2371: \"Xian 1980 / 3-degree Gauss-Kruger CM 78E\",\n\t2372: \"Xian 1980 / 3-degree Gauss-Kruger CM 81E\",\n\t2373: \"Xian 1980 / 3-degree Gauss-Kruger CM 84E\",\n\t2374: \"Xian 1980 / 3-degree Gauss-Kruger CM 87E\",\n\t2375: \"Xian 1980 / 3-degree Gauss-Kruger CM 90E\",\n\t2376: \"Xian 1980 / 3-degree Gauss-Kruger CM 93E\",\n\t2377: \"Xian 1980 / 3-degree Gauss-Kruger CM 96E\",\n\t2378: \"Xian 1980 / 3-degree Gauss-Kruger CM 99E\",\n\t2379: \"Xian 1980 / 3-degree Gauss-Kruger CM 102E\",\n\t2380: \"Xian 1980 / 3-degree Gauss-Kruger CM 105E\",\n\t2381: \"Xian 1980 / 3-degree Gauss-Kruger CM 108E\",\n\t2382: \"Xian 1980 / 3-degree Gauss-Kruger CM 111E\",\n\t2383: \"Xian 1980 / 3-degree Gauss-Kruger CM 114E\",\n\t2384: \"Xian 1980 / 3-degree Gauss-Kruger CM 117E\",\n\t2385: \"Xian 1980 / 3-degree Gauss-Kruger CM 120E\",\n\t2386: \"Xian 1980 / 3-degree Gauss-Kruger CM 123E\",\n\t2387: \"Xian 1980 / 3-degree Gauss-Kruger CM 126E\",\n\t2388: \"Xian 1980 / 3-degree Gauss-Kruger CM 129E\",\n\t2389: \"Xian 1980 / 3-degree Gauss-Kruger CM 132E\",\n\t2390: \"Xian 1980 / 3-degree Gauss-Kruger CM 135E\",\n\t2391: \"KKJ / Finland zone 1\",\n\t2392: \"KKJ / Finland zone 2\",\n\t2393: \"KKJ / Finland Uniform Coordinate System\",\n\t2394: \"KKJ / Finland zone 4\",\n\t2395: \"South Yemen / Gauss-Kruger zone 8\",\n\t2396: \"South Yemen / Gauss-Kruger zone 9\",\n\t2397: \"Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 3\",\n\t2398: \"Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 4\",\n\t2399: \"Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 5\",\n\t2400: \"RT90 2.5 gon W\",\n\t2401: \"Beijing 1954 / 3-degree Gauss-Kruger zone 25\",\n\t2402: \"Beijing 1954 / 3-degree Gauss-Kruger zone 26\",\n\t2403: \"Beijing 1954 / 3-degree Gauss-Kruger zone 27\",\n\t2404: \"Beijing 1954 / 3-degree Gauss-Kruger zone 28\",\n\t2405: \"Beijing 1954 / 3-degree Gauss-Kruger zone 29\",\n\t2406: \"Beijing 1954 / 3-degree Gauss-Kruger zone 30\",\n\t2407: \"Beijing 1954 / 3-degree Gauss-Kruger zone 31\",\n\t2408: \"Beijing 1954 / 3-degree Gauss-Kruger zone 32\",\n\t2409: \"Beijing 1954 / 3-degree Gauss-Kruger zone 33\",\n\t2410: \"Beijing 1954 / 3-degree Gauss-Kruger zone 34\",\n\t2411: \"Beijing 1954 / 3-degree Gauss-Kruger zone 35\",\n\t2412: \"Beijing 1954 / 3-degree Gauss-Kruger zone 36\",\n\t2413: \"Beijing 1954 / 3-degree Gauss-Kruger zone 37\",\n\t2414: \"Beijing 1954 / 3-degree Gauss-Kruger zone 38\",\n\t2415: \"Beijing 1954 / 3-degree Gauss-Kruger zone 39\",\n\t2416: \"Beijing 1954 / 3-degree Gauss-Kruger zone 40\",\n\t2417: \"Beijing 1954 / 3-degree Gauss-Kruger zone 41\",\n\t2418: \"Beijing 1954 / 3-degree Gauss-Kruger zone 42\",\n\t2419: \"Beijing 1954 / 3-degree Gauss-Kruger zone 43\",\n\t2420: \"Beijing 1954 / 3-degree Gauss-Kruger zone 44\",\n\t2421: \"Beijing 1954 / 3-degree Gauss-Kruger zone 45\",\n\t2422: \"Beijing 1954 / 3-degree Gauss-Kruger CM 75E\",\n\t2423: \"Beijing 1954 / 3-degree Gauss-Kruger CM 78E\",\n\t2424: \"Beijing 1954 / 3-degree Gauss-Kruger CM 81E\",\n\t2425: \"Beijing 1954 / 3-degree Gauss-Kruger CM 84E\",\n\t2426: \"Beijing 1954 / 3-degree Gauss-Kruger CM 87E\",\n\t2427: \"Beijing 1954 / 3-degree Gauss-Kruger CM 90E\",\n\t2428: \"Beijing 1954 / 3-degree Gauss-Kruger CM 93E\",\n\t2429: \"Beijing 1954 / 3-degree Gauss-Kruger CM 96E\",\n\t2430: \"Beijing 1954 / 3-degree Gauss-Kruger CM 99E\",\n\t2431: \"Beijing 1954 / 3-degree Gauss-Kruger CM 102E\",\n\t2432: \"Beijing 1954 / 3-degree Gauss-Kruger CM 105E\",\n\t2433: \"Beijing 1954 / 3-degree Gauss-Kruger CM 108E\",\n\t2434: \"Beijing 1954 / 3-degree Gauss-Kruger CM 111E\",\n\t2435: \"Beijing 1954 / 3-degree Gauss-Kruger CM 114E\",\n\t2436: \"Beijing 1954 / 3-degree Gauss-Kruger CM 117E\",\n\t2437: \"Beijing 1954 / 3-degree Gauss-Kruger CM 120E\",\n\t2438: \"Beijing 1954 / 3-degree Gauss-Kruger CM 123E\",\n\t2439: \"Beijing 1954 / 3-degree Gauss-Kruger CM 126E\",\n\t2440: \"Beijing 1954 / 3-degree Gauss-Kruger CM 129E\",\n\t2441: \"Beijing 1954 / 3-degree Gauss-Kruger CM 132E\",\n\t2442: \"Beijing 1954 / 3-degree Gauss-Kruger CM 135E\",\n\t2443: \"JGD2000 / Japan Plane Rectangular CS I\",\n\t2444: \"JGD2000 / Japan Plane Rectangular CS II\",\n\t2445: \"JGD2000 / Japan Plane Rectangular CS III\",\n\t2446: \"JGD2000 / Japan Plane Rectangular CS IV\",\n\t2447: \"JGD2000 / Japan Plane Rectangular CS V\",\n\t2448: \"JGD2000 / Japan Plane Rectangular CS VI\",\n\t2449: \"JGD2000 / Japan Plane Rectangular CS VII\",\n\t2450: \"JGD2000 / Japan Plane Rectangular CS VIII\",\n\t2451: \"JGD2000 / Japan Plane Rectangular CS IX\",\n\t2452: \"JGD2000 / Japan Plane Rectangular CS X\",\n\t2453: \"JGD2000 / Japan Plane Rectangular CS XI\",\n\t2454: \"JGD2000 / Japan Plane Rectangular CS XII\",\n\t2455: \"JGD2000 / Japan Plane Rectangular CS XIII\",\n\t2456: \"JGD2000 / Japan Plane Rectangular CS XIV\",\n\t2457: \"JGD2000 / Japan Plane Rectangular CS XV\",\n\t2458: \"JGD2000 / Japan Plane Rectangular CS XVI\",\n\t2459: \"JGD2000 / Japan Plane Rectangular CS XVII\",\n\t2460: \"JGD2000 / Japan Plane Rectangular CS XVIII\",\n\t2461: \"JGD2000 / Japan Plane Rectangular CS XIX\",\n\t2462: \"Albanian 1987 / Gauss-Kruger zone 4\",\n\t2463: \"Pulkovo 1995 / Gauss-Kruger CM 21E\",\n\t2464: \"Pulkovo 1995 / Gauss-Kruger CM 27E\",\n\t2465: \"Pulkovo 1995 / Gauss-Kruger CM 33E\",\n\t2466: \"Pulkovo 1995 / Gauss-Kruger CM 39E\",\n\t2467: \"Pulkovo 1995 / Gauss-Kruger CM 45E\",\n\t2468: \"Pulkovo 1995 / Gauss-Kruger CM 51E\",\n\t2469: \"Pulkovo 1995 / Gauss-Kruger CM 57E\",\n\t2470: \"Pulkovo 1995 / Gauss-Kruger CM 63E\",\n\t2471: \"Pulkovo 1995 / Gauss-Kruger CM 69E\",\n\t2472: \"Pulkovo 1995 / Gauss-Kruger CM 75E\",\n\t2473: \"Pulkovo 1995 / Gauss-Kruger CM 81E\",\n\t2474: \"Pulkovo 1995 / Gauss-Kruger CM 87E\",\n\t2475: \"Pulkovo 1995 / Gauss-Kruger CM 93E\",\n\t2476: \"Pulkovo 1995 / Gauss-Kruger CM 99E\",\n\t2477: \"Pulkovo 1995 / Gauss-Kruger CM 105E\",\n\t2478: \"Pulkovo 1995 / Gauss-Kruger CM 111E\",\n\t2479: \"Pulkovo 1995 / Gauss-Kruger CM 117E\",\n\t2480: \"Pulkovo 1995 / Gauss-Kruger CM 123E\",\n\t2481: \"Pulkovo 1995 / Gauss-Kruger CM 129E\",\n\t2482: \"Pulkovo 1995 / Gauss-Kruger CM 135E\",\n\t2483: \"Pulkovo 1995 / Gauss-Kruger CM 141E\",\n\t2484: \"Pulkovo 1995 / Gauss-Kruger CM 147E\",\n\t2485: \"Pulkovo 1995 / Gauss-Kruger CM 153E\",\n\t2486: \"Pulkovo 1995 / Gauss-Kruger CM 159E\",\n\t2487: \"Pulkovo 1995 / Gauss-Kruger CM 165E\",\n\t2488: \"Pulkovo 1995 / Gauss-Kruger CM 171E\",\n\t2489: \"Pulkovo 1995 / Gauss-Kruger CM 177E\",\n\t2490: \"Pulkovo 1995 / Gauss-Kruger CM 177W\",\n\t2491: \"Pulkovo 1995 / Gauss-Kruger CM 171W\",\n\t2492: \"Pulkovo 1942 / Gauss-Kruger CM 9E\",\n\t2493: \"Pulkovo 1942 / Gauss-Kruger CM 15E\",\n\t2494: \"Pulkovo 1942 / Gauss-Kruger CM 21E\",\n\t2495: \"Pulkovo 1942 / Gauss-Kruger CM 27E\",\n\t2496: \"Pulkovo 1942 / Gauss-Kruger CM 33E\",\n\t2497: \"Pulkovo 1942 / Gauss-Kruger CM 39E\",\n\t2498: \"Pulkovo 1942 / Gauss-Kruger CM 45E\",\n\t2499: \"Pulkovo 1942 / Gauss-Kruger CM 51E\",\n\t2500: \"Pulkovo 1942 / Gauss-Kruger CM 57E\",\n\t2501: \"Pulkovo 1942 / Gauss-Kruger CM 63E\",\n\t2502: \"Pulkovo 1942 / Gauss-Kruger CM 69E\",\n\t2503: \"Pulkovo 1942 / Gauss-Kruger CM 75E\",\n\t2504: \"Pulkovo 1942 / Gauss-Kruger CM 81E\",\n\t2505: \"Pulkovo 1942 / Gauss-Kruger CM 87E\",\n\t2506: \"Pulkovo 1942 / Gauss-Kruger CM 93E\",\n\t2507: \"Pulkovo 1942 / Gauss-Kruger CM 99E\",\n\t2508: \"Pulkovo 1942 / Gauss-Kruger CM 105E\",\n\t2509: \"Pulkovo 1942 / Gauss-Kruger CM 111E\",\n\t2510: \"Pulkovo 1942 / Gauss-Kruger CM 117E\",\n\t2511: \"Pulkovo 1942 / Gauss-Kruger CM 123E\",\n\t2512: \"Pulkovo 1942 / Gauss-Kruger CM 129E\",\n\t2513: \"Pulkovo 1942 / Gauss-Kruger CM 135E\",\n\t2514: \"Pulkovo 1942 / Gauss-Kruger CM 141E\",\n\t2515: \"Pulkovo 1942 / Gauss-Kruger CM 147E\",\n\t2516: \"Pulkovo 1942 / Gauss-Kruger CM 153E\",\n\t2517: \"Pulkovo 1942 / Gauss-Kruger CM 159E\",\n\t2518: \"Pulkovo 1942 / Gauss-Kruger CM 165E\",\n\t2519: \"Pulkovo 1942 / Gauss-Kruger CM 171E\",\n\t2520: \"Pulkovo 1942 / Gauss-Kruger CM 177E\",\n\t2521: \"Pulkovo 1942 / Gauss-Kruger CM 177W\",\n\t2522: \"Pulkovo 1942 / Gauss-Kruger CM 171W\",\n\t2523: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 7\",\n\t2524: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 8\",\n\t2525: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 9\",\n\t2526: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 10\",\n\t2527: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 11\",\n\t2528: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 12\",\n\t2529: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 13\",\n\t2530: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 14\",\n\t2531: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 15\",\n\t2532: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 16\",\n\t2533: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 17\",\n\t2534: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 18\",\n\t2535: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 19\",\n\t2536: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 20\",\n\t2537: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 21\",\n\t2538: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 22\",\n\t2539: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 23\",\n\t2540: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 24\",\n\t2541: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 25\",\n\t2542: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 26\",\n\t2543: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 27\",\n\t2544: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 28\",\n\t2545: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 29\",\n\t2546: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 30\",\n\t2547: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 31\",\n\t2548: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 32\",\n\t2549: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 33\",\n\t2550: \"Samboja / UTM zone 50S\",\n\t2551: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 34\",\n\t2552: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 35\",\n\t2553: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 36\",\n\t2554: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 37\",\n\t2555: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 38\",\n\t2556: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 39\",\n\t2557: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 40\",\n\t2558: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 41\",\n\t2559: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 42\",\n\t2560: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 43\",\n\t2561: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 44\",\n\t2562: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 45\",\n\t2563: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 46\",\n\t2564: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 47\",\n\t2565: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 48\",\n\t2566: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 49\",\n\t2567: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 50\",\n\t2568: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 51\",\n\t2569: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 52\",\n\t2570: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 53\",\n\t2571: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 54\",\n\t2572: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 55\",\n\t2573: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 56\",\n\t2574: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 57\",\n\t2575: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 58\",\n\t2576: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 59\",\n\t2577: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 60\",\n\t2578: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 61\",\n\t2579: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 62\",\n\t2580: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 63\",\n\t2581: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 64\",\n\t2582: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 21E\",\n\t2583: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 24E\",\n\t2584: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 27E\",\n\t2585: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 30E\",\n\t2586: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 33E\",\n\t2587: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 36E\",\n\t2588: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 39E\",\n\t2589: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 42E\",\n\t2590: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 45E\",\n\t2591: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 48E\",\n\t2592: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 51E\",\n\t2593: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 54E\",\n\t2594: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 57E\",\n\t2595: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 60E\",\n\t2596: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 63E\",\n\t2597: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 66E\",\n\t2598: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 69E\",\n\t2599: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 72E\",\n\t2600: \"Lietuvos Koordinoei Sistema 1994\",\n\t2601: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 75E\",\n\t2602: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 78E\",\n\t2603: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 81E\",\n\t2604: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 84E\",\n\t2605: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 87E\",\n\t2606: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 90E\",\n\t2607: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 93E\",\n\t2608: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 96E\",\n\t2609: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 99E\",\n\t2610: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 102E\",\n\t2611: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 105E\",\n\t2612: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 108E\",\n\t2613: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 111E\",\n\t2614: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 114E\",\n\t2615: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 117E\",\n\t2616: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 120E\",\n\t2617: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 123E\",\n\t2618: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 126E\",\n\t2619: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 129E\",\n\t2620: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 132E\",\n\t2621: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 135E\",\n\t2622: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 138E\",\n\t2623: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 141E\",\n\t2624: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 144E\",\n\t2625: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 147E\",\n\t2626: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 150E\",\n\t2627: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 153E\",\n\t2628: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 156E\",\n\t2629: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 159E\",\n\t2630: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 162E\",\n\t2631: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 165E\",\n\t2632: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 168E\",\n\t2633: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 171E\",\n\t2634: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 174E\",\n\t2635: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 177E\",\n\t2636: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 180E\",\n\t2637: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 177W\",\n\t2638: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 174W\",\n\t2639: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 171W\",\n\t2640: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 168W\",\n\t2641: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 7\",\n\t2642: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 8\",\n\t2643: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 9\",\n\t2644: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 10\",\n\t2645: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 11\",\n\t2646: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 12\",\n\t2647: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 13\",\n\t2648: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 14\",\n\t2649: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 15\",\n\t2650: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 16\",\n\t2651: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 17\",\n\t2652: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 18\",\n\t2653: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 19\",\n\t2654: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 20\",\n\t2655: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 21\",\n\t2656: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 22\",\n\t2657: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 23\",\n\t2658: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 24\",\n\t2659: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 25\",\n\t2660: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 26\",\n\t2661: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 27\",\n\t2662: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 28\",\n\t2663: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 29\",\n\t2664: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 30\",\n\t2665: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 31\",\n\t2666: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 32\",\n\t2667: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 33\",\n\t2668: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 34\",\n\t2669: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 35\",\n\t2670: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 36\",\n\t2671: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 37\",\n\t2672: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 38\",\n\t2673: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 39\",\n\t2674: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 40\",\n\t2675: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 41\",\n\t2676: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 42\",\n\t2677: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 43\",\n\t2678: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 44\",\n\t2679: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 45\",\n\t2680: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 46\",\n\t2681: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 47\",\n\t2682: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 48\",\n\t2683: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 49\",\n\t2684: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 50\",\n\t2685: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 51\",\n\t2686: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 52\",\n\t2687: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 53\",\n\t2688: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 54\",\n\t2689: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 55\",\n\t2690: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 56\",\n\t2691: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 57\",\n\t2692: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 58\",\n\t2693: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 59\",\n\t2694: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 60\",\n\t2695: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 61\",\n\t2696: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 62\",\n\t2697: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 63\",\n\t2698: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 64\",\n\t2699: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 21E\",\n\t2700: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 24E\",\n\t2701: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 27E\",\n\t2702: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 30E\",\n\t2703: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 33E\",\n\t2704: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 36E\",\n\t2705: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 39E\",\n\t2706: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 42E\",\n\t2707: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 45E\",\n\t2708: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 48E\",\n\t2709: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 51E\",\n\t2710: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 54E\",\n\t2711: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 57E\",\n\t2712: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 60E\",\n\t2713: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 63E\",\n\t2714: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 66E\",\n\t2715: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 69E\",\n\t2716: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 72E\",\n\t2717: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 75E\",\n\t2718: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 78E\",\n\t2719: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 81E\",\n\t2720: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 84E\",\n\t2721: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 87E\",\n\t2722: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 90E\",\n\t2723: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 93E\",\n\t2724: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 96E\",\n\t2725: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 99E\",\n\t2726: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 102E\",\n\t2727: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 105E\",\n\t2728: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 108E\",\n\t2729: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 111E\",\n\t2730: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 114E\",\n\t2731: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 117E\",\n\t2732: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 120E\",\n\t2733: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 123E\",\n\t2734: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 126E\",\n\t2735: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 129E\",\n\t2736: \"Tete / UTM zone 36S\",\n\t2737: \"Tete / UTM zone 37S\",\n\t2738: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 132E\",\n\t2739: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 135E\",\n\t2740: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 138E\",\n\t2741: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 141E\",\n\t2742: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 144E\",\n\t2743: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 147E\",\n\t2744: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 150E\",\n\t2745: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 153E\",\n\t2746: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 156E\",\n\t2747: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 159E\",\n\t2748: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 162E\",\n\t2749: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 165E\",\n\t2750: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 168E\",\n\t2751: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 171E\",\n\t2752: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 174E\",\n\t2753: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 177E\",\n\t2754: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 180E\",\n\t2755: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 177W\",\n\t2756: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 174W\",\n\t2757: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 171W\",\n\t2758: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 168W\",\n\t2759: \"NAD83(HARN) / Alabama East\",\n\t2760: \"NAD83(HARN) / Alabama West\",\n\t2761: \"NAD83(HARN) / Arizona East\",\n\t2762: \"NAD83(HARN) / Arizona Central\",\n\t2763: \"NAD83(HARN) / Arizona West\",\n\t2764: \"NAD83(HARN) / Arkansas North\",\n\t2765: \"NAD83(HARN) / Arkansas South\",\n\t2766: \"NAD83(HARN) / California zone 1\",\n\t2767: \"NAD83(HARN) / California zone 2\",\n\t2768: \"NAD83(HARN) / California zone 3\",\n\t2769: \"NAD83(HARN) / California zone 4\",\n\t2770: \"NAD83(HARN) / California zone 5\",\n\t2771: \"NAD83(HARN) / California zone 6\",\n\t2772: \"NAD83(HARN) / Colorado North\",\n\t2773: \"NAD83(HARN) / Colorado Central\",\n\t2774: \"NAD83(HARN) / Colorado South\",\n\t2775: \"NAD83(HARN) / Connecticut\",\n\t2776: \"NAD83(HARN) / Delaware\",\n\t2777: \"NAD83(HARN) / Florida East\",\n\t2778: \"NAD83(HARN) / Florida West\",\n\t2779: \"NAD83(HARN) / Florida North\",\n\t2780: \"NAD83(HARN) / Georgia East\",\n\t2781: \"NAD83(HARN) / Georgia West\",\n\t2782: \"NAD83(HARN) / Hawaii zone 1\",\n\t2783: \"NAD83(HARN) / Hawaii zone 2\",\n\t2784: \"NAD83(HARN) / Hawaii zone 3\",\n\t2785: \"NAD83(HARN) / Hawaii zone 4\",\n\t2786: \"NAD83(HARN) / Hawaii zone 5\",\n\t2787: \"NAD83(HARN) / Idaho East\",\n\t2788: \"NAD83(HARN) / Idaho Central\",\n\t2789: \"NAD83(HARN) / Idaho West\",\n\t2790: \"NAD83(HARN) / Illinois East\",\n\t2791: \"NAD83(HARN) / Illinois West\",\n\t2792: \"NAD83(HARN) / Indiana East\",\n\t2793: \"NAD83(HARN) / Indiana West\",\n\t2794: \"NAD83(HARN) / Iowa North\",\n\t2795: \"NAD83(HARN) / Iowa South\",\n\t2796: \"NAD83(HARN) / Kansas North\",\n\t2797: \"NAD83(HARN) / Kansas South\",\n\t2798: \"NAD83(HARN) / Kentucky North\",\n\t2799: \"NAD83(HARN) / Kentucky South\",\n\t2800: \"NAD83(HARN) / Louisiana North\",\n\t2801: \"NAD83(HARN) / Louisiana South\",\n\t2802: \"NAD83(HARN) / Maine East\",\n\t2803: \"NAD83(HARN) / Maine West\",\n\t2804: \"NAD83(HARN) / Maryland\",\n\t2805: \"NAD83(HARN) / Massachusetts Mainland\",\n\t2806: \"NAD83(HARN) / Massachusetts Island\",\n\t2807: \"NAD83(HARN) / Michigan North\",\n\t2808: \"NAD83(HARN) / Michigan Central\",\n\t2809: \"NAD83(HARN) / Michigan South\",\n\t2810: \"NAD83(HARN) / Minnesota North\",\n\t2811: \"NAD83(HARN) / Minnesota Central\",\n\t2812: \"NAD83(HARN) / Minnesota South\",\n\t2813: \"NAD83(HARN) / Mississippi East\",\n\t2814: \"NAD83(HARN) / Mississippi West\",\n\t2815: \"NAD83(HARN) / Missouri East\",\n\t2816: \"NAD83(HARN) / Missouri Central\",\n\t2817: \"NAD83(HARN) / Missouri West\",\n\t2818: \"NAD83(HARN) / Montana\",\n\t2819: \"NAD83(HARN) / Nebraska\",\n\t2820: \"NAD83(HARN) / Nevada East\",\n\t2821: \"NAD83(HARN) / Nevada Central\",\n\t2822: \"NAD83(HARN) / Nevada West\",\n\t2823: \"NAD83(HARN) / New Hampshire\",\n\t2824: \"NAD83(HARN) / New Jersey\",\n\t2825: \"NAD83(HARN) / New Mexico East\",\n\t2826: \"NAD83(HARN) / New Mexico Central\",\n\t2827: \"NAD83(HARN) / New Mexico West\",\n\t2828: \"NAD83(HARN) / New York East\",\n\t2829: \"NAD83(HARN) / New York Central\",\n\t2830: \"NAD83(HARN) / New York West\",\n\t2831: \"NAD83(HARN) / New York Long Island\",\n\t2832: \"NAD83(HARN) / North Dakota North\",\n\t2833: \"NAD83(HARN) / North Dakota South\",\n\t2834: \"NAD83(HARN) / Ohio North\",\n\t2835: \"NAD83(HARN) / Ohio South\",\n\t2836: \"NAD83(HARN) / Oklahoma North\",\n\t2837: \"NAD83(HARN) / Oklahoma South\",\n\t2838: \"NAD83(HARN) / Oregon North\",\n\t2839: \"NAD83(HARN) / Oregon South\",\n\t2840: \"NAD83(HARN) / Rhode Island\",\n\t2841: \"NAD83(HARN) / South Dakota North\",\n\t2842: \"NAD83(HARN) / South Dakota South\",\n\t2843: \"NAD83(HARN) / Tennessee\",\n\t2844: \"NAD83(HARN) / Texas North\",\n\t2845: \"NAD83(HARN) / Texas North Central\",\n\t2846: \"NAD83(HARN) / Texas Central\",\n\t2847: \"NAD83(HARN) / Texas South Central\",\n\t2848: \"NAD83(HARN) / Texas South\",\n\t2849: \"NAD83(HARN) / Utah North\",\n\t2850: \"NAD83(HARN) / Utah Central\",\n\t2851: \"NAD83(HARN) / Utah South\",\n\t2852: \"NAD83(HARN) / Vermont\",\n\t2853: \"NAD83(HARN) / Virginia North\",\n\t2854: \"NAD83(HARN) / Virginia South\",\n\t2855: \"NAD83(HARN) / Washington North\",\n\t2856: \"NAD83(HARN) / Washington South\",\n\t2857: \"NAD83(HARN) / West Virginia North\",\n\t2858: \"NAD83(HARN) / West Virginia South\",\n\t2859: \"NAD83(HARN) / Wisconsin North\",\n\t2860: \"NAD83(HARN) / Wisconsin Central\",\n\t2861: \"NAD83(HARN) / Wisconsin South\",\n\t2862: \"NAD83(HARN) / Wyoming East\",\n\t2863: \"NAD83(HARN) / Wyoming East Central\",\n\t2864: \"NAD83(HARN) / Wyoming West Central\",\n\t2865: \"NAD83(HARN) / Wyoming West\",\n\t2866: \"NAD83(HARN) / Puerto Rico and Virgin Is.\",\n\t2867: \"NAD83(HARN) / Arizona East (ft)\",\n\t2868: \"NAD83(HARN) / Arizona Central (ft)\",\n\t2869: \"NAD83(HARN) / Arizona West (ft)\",\n\t2870: \"NAD83(HARN) / California zone 1 (ftUS)\",\n\t2871: \"NAD83(HARN) / California zone 2 (ftUS)\",\n\t2872: \"NAD83(HARN) / California zone 3 (ftUS)\",\n\t2873: \"NAD83(HARN) / California zone 4 (ftUS)\",\n\t2874: \"NAD83(HARN) / California zone 5 (ftUS)\",\n\t2875: \"NAD83(HARN) / California zone 6 (ftUS)\",\n\t2876: \"NAD83(HARN) / Colorado North (ftUS)\",\n\t2877: \"NAD83(HARN) / Colorado Central (ftUS)\",\n\t2878: \"NAD83(HARN) / Colorado South (ftUS)\",\n\t2879: \"NAD83(HARN) / Connecticut (ftUS)\",\n\t2880: \"NAD83(HARN) / Delaware (ftUS)\",\n\t2881: \"NAD83(HARN) / Florida East (ftUS)\",\n\t2882: \"NAD83(HARN) / Florida West (ftUS)\",\n\t2883: \"NAD83(HARN) / Florida North (ftUS)\",\n\t2884: \"NAD83(HARN) / Georgia East (ftUS)\",\n\t2885: \"NAD83(HARN) / Georgia West (ftUS)\",\n\t2886: \"NAD83(HARN) / Idaho East (ftUS)\",\n\t2887: \"NAD83(HARN) / Idaho Central (ftUS)\",\n\t2888: \"NAD83(HARN) / Idaho West (ftUS)\",\n\t2889: \"NAD83(HARN) / Indiana East (ftUS)\",\n\t2890: \"NAD83(HARN) / Indiana West (ftUS)\",\n\t2891: \"NAD83(HARN) / Kentucky North (ftUS)\",\n\t2892: \"NAD83(HARN) / Kentucky South (ftUS)\",\n\t2893: \"NAD83(HARN) / Maryland (ftUS)\",\n\t2894: \"NAD83(HARN) / Massachusetts Mainland (ftUS)\",\n\t2895: \"NAD83(HARN) / Massachusetts Island (ftUS)\",\n\t2896: \"NAD83(HARN) / Michigan North (ft)\",\n\t2897: \"NAD83(HARN) / Michigan Central (ft)\",\n\t2898: \"NAD83(HARN) / Michigan South (ft)\",\n\t2899: \"NAD83(HARN) / Mississippi East (ftUS)\",\n\t2900: \"NAD83(HARN) / Mississippi West (ftUS)\",\n\t2901: \"NAD83(HARN) / Montana (ft)\",\n\t2902: \"NAD83(HARN) / New Mexico East (ftUS)\",\n\t2903: \"NAD83(HARN) / New Mexico Central (ftUS)\",\n\t2904: \"NAD83(HARN) / New Mexico West (ftUS)\",\n\t2905: \"NAD83(HARN) / New York East (ftUS)\",\n\t2906: \"NAD83(HARN) / New York Central (ftUS)\",\n\t2907: \"NAD83(HARN) / New York West (ftUS)\",\n\t2908: \"NAD83(HARN) / New York Long Island (ftUS)\",\n\t2909: \"NAD83(HARN) / North Dakota North (ft)\",\n\t2910: \"NAD83(HARN) / North Dakota South (ft)\",\n\t2911: \"NAD83(HARN) / Oklahoma North (ftUS)\",\n\t2912: \"NAD83(HARN) / Oklahoma South (ftUS)\",\n\t2913: \"NAD83(HARN) / Oregon North (ft)\",\n\t2914: \"NAD83(HARN) / Oregon South (ft)\",\n\t2915: \"NAD83(HARN) / Tennessee (ftUS)\",\n\t2916: \"NAD83(HARN) / Texas North (ftUS)\",\n\t2917: \"NAD83(HARN) / Texas North Central (ftUS)\",\n\t2918: \"NAD83(HARN) / Texas Central (ftUS)\",\n\t2919: \"NAD83(HARN) / Texas South Central (ftUS)\",\n\t2920: \"NAD83(HARN) / Texas South (ftUS)\",\n\t2921: \"NAD83(HARN) / Utah North (ft)\",\n\t2922: \"NAD83(HARN) / Utah Central (ft)\",\n\t2923: \"NAD83(HARN) / Utah South (ft)\",\n\t2924: \"NAD83(HARN) / Virginia North (ftUS)\",\n\t2925: \"NAD83(HARN) / Virginia South (ftUS)\",\n\t2926: \"NAD83(HARN) / Washington North (ftUS)\",\n\t2927: \"NAD83(HARN) / Washington South (ftUS)\",\n\t2928: \"NAD83(HARN) / Wisconsin North (ftUS)\",\n\t2929: \"NAD83(HARN) / Wisconsin Central (ftUS)\",\n\t2930: \"NAD83(HARN) / Wisconsin South (ftUS)\",\n\t2931: \"Beduaram / TM 13 NE\",\n\t2932: \"QND95 / Qatar National Grid\",\n\t2933: \"Segara / UTM zone 50S\",\n\t2934: \"Segara (Jakarta) / NEIEZ\",\n\t2935: \"Pulkovo 1942 / CS63 zone A1\",\n\t2936: \"Pulkovo 1942 / CS63 zone A2\",\n\t2937: \"Pulkovo 1942 / CS63 zone A3\",\n\t2938: \"Pulkovo 1942 / CS63 zone A4\",\n\t2939: \"Pulkovo 1942 / CS63 zone K2\",\n\t2940: \"Pulkovo 1942 / CS63 zone K3\",\n\t2941: \"Pulkovo 1942 / CS63 zone K4\",\n\t2942: \"Porto Santo / UTM zone 28N\",\n\t2943: \"Selvagem Grande / UTM zone 28N\",\n\t2944: \"NAD83(CSRS) / SCoPQ zone 2\",\n\t2945: \"NAD83(CSRS) / MTM zone 3\",\n\t2946: \"NAD83(CSRS) / MTM zone 4\",\n\t2947: \"NAD83(CSRS) / MTM zone 5\",\n\t2948: \"NAD83(CSRS) / MTM zone 6\",\n\t2949: \"NAD83(CSRS) / MTM zone 7\",\n\t2950: \"NAD83(CSRS) / MTM zone 8\",\n\t2951: \"NAD83(CSRS) / MTM zone 9\",\n\t2952: \"NAD83(CSRS) / MTM zone 10\",\n\t2953: \"NAD83(CSRS) / New Brunswick Stereographic\",\n\t2954: \"NAD83(CSRS) / Prince Edward Isl. Stereographic (NAD83)\",\n\t2955: \"NAD83(CSRS) / UTM zone 11N\",\n\t2956: \"NAD83(CSRS) / UTM zone 12N\",\n\t2957: \"NAD83(CSRS) / UTM zone 13N\",\n\t2958: \"NAD83(CSRS) / UTM zone 17N\",\n\t2959: \"NAD83(CSRS) / UTM zone 18N\",\n\t2960: \"NAD83(CSRS) / UTM zone 19N\",\n\t2961: \"NAD83(CSRS) / UTM zone 20N\",\n\t2962: \"NAD83(CSRS) / UTM zone 21N\",\n\t2963: \"Lisbon 1890 (Lisbon) / Portugal Bonne\",\n\t2964: \"NAD27 / Alaska Albers\",\n\t2965: \"NAD83 / Indiana East (ftUS)\",\n\t2966: \"NAD83 / Indiana West (ftUS)\",\n\t2967: \"NAD83(HARN) / Indiana East (ftUS)\",\n\t2968: \"NAD83(HARN) / Indiana West (ftUS)\",\n\t2969: \"Fort Marigot / UTM zone 20N\",\n\t2970: \"Guadeloupe 1948 / UTM zone 20N\",\n\t2971: \"CSG67 / UTM zone 22N\",\n\t2972: \"RGFG95 / UTM zone 22N\",\n\t2973: \"Martinique 1938 / UTM zone 20N\",\n\t2975: \"RGR92 / UTM zone 40S\",\n\t2976: \"Tahiti 52 / UTM zone 6S\",\n\t2977: \"Tahaa 54 / UTM zone 5S\",\n\t2978: \"IGN72 Nuku Hiva / UTM zone 7S\",\n\t2979: \"K0 1949 / UTM zone 42S\",\n\t2980: \"Combani 1950 / UTM zone 38S\",\n\t2981: \"IGN56 Lifou / UTM zone 58S\",\n\t2982: \"IGN72 Grand Terre / UTM zone 58S\",\n\t2983: \"ST87 Ouvea / UTM zone 58S\",\n\t2984: \"RGNC 1991 / Lambert New Caledonia\",\n\t2985: \"Petrels 1972 / Terre Adelie Polar Stereographic\",\n\t2986: \"Perroud 1950 / Terre Adelie Polar Stereographic\",\n\t2987: \"Saint Pierre et Miquelon 1950 / UTM zone 21N\",\n\t2988: \"MOP78 / UTM zone 1S\",\n\t2989: \"RRAF 1991 / UTM zone 20N\",\n\t2990: \"Reunion 1947 / TM Reunion\",\n\t2991: \"NAD83 / Oregon LCC (m)\",\n\t2992: \"NAD83 / Oregon GIC Lambert (ft)\",\n\t2993: \"NAD83(HARN) / Oregon LCC (m)\",\n\t2994: \"NAD83(HARN) / Oregon GIC Lambert (ft)\",\n\t2995: \"IGN53 Mare / UTM zone 58S\",\n\t2996: \"ST84 Ile des Pins / UTM zone 58S\",\n\t2997: \"ST71 Belep / UTM zone 58S\",\n\t2998: \"NEA74 Noumea / UTM zone 58S\",\n\t2999: \"Grand Comoros / UTM zone 38S\",\n\t3000: \"Segara / NEIEZ\",\n\t3001: \"Batavia / NEIEZ\",\n\t3002: \"Makassar / NEIEZ\",\n\t3003: \"Monte Mario / Italy zone 1\",\n\t3004: \"Monte Mario / Italy zone 2\",\n\t3005: \"NAD83 / BC Albers\",\n\t3006: \"SWEREF99 TM\",\n\t3007: \"SWEREF99 12 00\",\n\t3008: \"SWEREF99 13 30\",\n\t3009: \"SWEREF99 15 00\",\n\t3010: \"SWEREF99 16 30\",\n\t3011: \"SWEREF99 18 00\",\n\t3012: \"SWEREF99 14 15\",\n\t3013: \"SWEREF99 15 45\",\n\t3014: \"SWEREF99 17 15\",\n\t3015: \"SWEREF99 18 45\",\n\t3016: \"SWEREF99 20 15\",\n\t3017: \"SWEREF99 21 45\",\n\t3018: \"SWEREF99 23 15\",\n\t3019: \"RT90 7.5 gon V\",\n\t3020: \"RT90 5 gon V\",\n\t3021: \"RT90 2.5 gon V\",\n\t3022: \"RT90 0 gon\",\n\t3023: \"RT90 2.5 gon O\",\n\t3024: \"RT90 5 gon O\",\n\t3025: \"RT38 7.5 gon V\",\n\t3026: \"RT38 5 gon V\",\n\t3027: \"RT38 2.5 gon V\",\n\t3028: \"RT38 0 gon\",\n\t3029: \"RT38 2.5 gon O\",\n\t3030: \"RT38 5 gon O\",\n\t3031: \"WGS 84 / Antarctic Polar Stereographic\",\n\t3032: \"WGS 84 / Australian Antarctic Polar Stereographic\",\n\t3033: \"WGS 84 / Australian Antarctic Lambert\",\n\t3034: \"ETRS89 / LCC Europe\",\n\t3035: \"ETRS89 / LAEA Europe\",\n\t3036: \"Moznet / UTM zone 36S\",\n\t3037: \"Moznet / UTM zone 37S\",\n\t3038: \"ETRS89 / TM26\",\n\t3039: \"ETRS89 / TM27\",\n\t3040: \"ETRS89 / UTM zone 28N (N-E)\",\n\t3041: \"ETRS89 / UTM zone 29N (N-E)\",\n\t3042: \"ETRS89 / UTM zone 30N (N-E)\",\n\t3043: \"ETRS89 / UTM zone 31N (N-E)\",\n\t3044: \"ETRS89 / UTM zone 32N (N-E)\",\n\t3045: \"ETRS89 / UTM zone 33N (N-E)\",\n\t3046: \"ETRS89 / UTM zone 34N (N-E)\",\n\t3047: \"ETRS89 / UTM zone 35N (N-E)\",\n\t3048: \"ETRS89 / UTM zone 36N (N-E)\",\n\t3049: \"ETRS89 / UTM zone 37N (N-E)\",\n\t3050: \"ETRS89 / TM38\",\n\t3051: \"ETRS89 / TM39\",\n\t3052: \"Reykjavik 1900 / Lambert 1900\",\n\t3053: \"Hjorsey 1955 / Lambert 1955\",\n\t3054: \"Hjorsey 1955 / UTM zone 26N\",\n\t3055: \"Hjorsey 1955 / UTM zone 27N\",\n\t3056: \"Hjorsey 1955 / UTM zone 28N\",\n\t3057: \"ISN93 / Lambert 1993\",\n\t3058: \"Helle 1954 / Jan Mayen Grid\",\n\t3059: \"LKS92 / Latvia TM\",\n\t3060: \"IGN72 Grande Terre / UTM zone 58S\",\n\t3061: \"Porto Santo 1995 / UTM zone 28N\",\n\t3062: \"Azores Oriental 1995 / UTM zone 26N\",\n\t3063: \"Azores Central 1995 / UTM zone 26N\",\n\t3064: \"IGM95 / UTM zone 32N\",\n\t3065: \"IGM95 / UTM zone 33N\",\n\t3066: \"ED50 / Jordan TM\",\n\t3067: \"ETRS89 / TM35FIN(E,N)\",\n\t3068: \"DHDN / Soldner Berlin\",\n\t3069: \"NAD27 / Wisconsin Transverse Mercator\",\n\t3070: \"NAD83 / Wisconsin Transverse Mercator\",\n\t3071: \"NAD83(HARN) / Wisconsin Transverse Mercator\",\n\t3072: \"NAD83 / Maine CS2000 East\",\n\t3073: \"NAD83 / Maine CS2000 Central\",\n\t3074: \"NAD83 / Maine CS2000 West\",\n\t3075: \"NAD83(HARN) / Maine CS2000 East\",\n\t3076: \"NAD83(HARN) / Maine CS2000 Central\",\n\t3077: \"NAD83(HARN) / Maine CS2000 West\",\n\t3078: \"NAD83 / Michigan Oblique Mercator\",\n\t3079: \"NAD83(HARN) / Michigan Oblique Mercator\",\n\t3080: \"NAD27 / Shackleford\",\n\t3081: \"NAD83 / Texas State Mapping System\",\n\t3082: \"NAD83 / Texas Centric Lambert Conformal\",\n\t3083: \"NAD83 / Texas Centric Albers Equal Area\",\n\t3084: \"NAD83(HARN) / Texas Centric Lambert Conformal\",\n\t3085: \"NAD83(HARN) / Texas Centric Albers Equal Area\",\n\t3086: \"NAD83 / Florida GDL Albers\",\n\t3087: \"NAD83(HARN) / Florida GDL Albers\",\n\t3088: \"NAD83 / Kentucky Single Zone\",\n\t3089: \"NAD83 / Kentucky Single Zone (ftUS)\",\n\t3090: \"NAD83(HARN) / Kentucky Single Zone\",\n\t3091: \"NAD83(HARN) / Kentucky Single Zone (ftUS)\",\n\t3092: \"Tokyo / UTM zone 51N\",\n\t3093: \"Tokyo / UTM zone 52N\",\n\t3094: \"Tokyo / UTM zone 53N\",\n\t3095: \"Tokyo / UTM zone 54N\",\n\t3096: \"Tokyo / UTM zone 55N\",\n\t3097: \"JGD2000 / UTM zone 51N\",\n\t3098: \"JGD2000 / UTM zone 52N\",\n\t3099: \"JGD2000 / UTM zone 53N\",\n\t3100: \"JGD2000 / UTM zone 54N\",\n\t3101: \"JGD2000 / UTM zone 55N\",\n\t3102: \"American Samoa 1962 / American Samoa Lambert\",\n\t3103: \"Mauritania 1999 / UTM zone 28N\",\n\t3104: \"Mauritania 1999 / UTM zone 29N\",\n\t3105: \"Mauritania 1999 / UTM zone 30N\",\n\t3106: \"Gulshan 303 / Bangladesh Transverse Mercator\",\n\t3107: \"GDA94 / SA Lambert\",\n\t3108: \"ETRS89 / Guernsey Grid\",\n\t3109: \"ETRS89 / Jersey Transverse Mercator\",\n\t3110: \"AGD66 / Vicgrid66\",\n\t3111: \"GDA94 / Vicgrid94\",\n\t3112: \"GDA94 / Geoscience Australia Lambert\",\n\t3113: \"GDA94 / BCSG02\",\n\t3114: \"MAGNA-SIRGAS / Colombia Far West zone\",\n\t3115: \"MAGNA-SIRGAS / Colombia West zone\",\n\t3116: \"MAGNA-SIRGAS / Colombia Bogota zone\",\n\t3117: \"MAGNA-SIRGAS / Colombia East Central zone\",\n\t3118: \"MAGNA-SIRGAS / Colombia East zone\",\n\t3119: \"Douala 1948 / AEF west\",\n\t3120: \"Pulkovo 1942(58) / Poland zone I\",\n\t3121: \"PRS92 / Philippines zone 1\",\n\t3122: \"PRS92 / Philippines zone 2\",\n\t3123: \"PRS92 / Philippines zone 3\",\n\t3124: \"PRS92 / Philippines zone 4\",\n\t3125: \"PRS92 / Philippines zone 5\",\n\t3126: \"ETRS89 / ETRS-GK19FIN\",\n\t3127: \"ETRS89 / ETRS-GK20FIN\",\n\t3128: \"ETRS89 / ETRS-GK21FIN\",\n\t3129: \"ETRS89 / ETRS-GK22FIN\",\n\t3130: \"ETRS89 / ETRS-GK23FIN\",\n\t3131: \"ETRS89 / ETRS-GK24FIN\",\n\t3132: \"ETRS89 / ETRS-GK25FIN\",\n\t3133: \"ETRS89 / ETRS-GK26FIN\",\n\t3134: \"ETRS89 / ETRS-GK27FIN\",\n\t3135: \"ETRS89 / ETRS-GK28FIN\",\n\t3136: \"ETRS89 / ETRS-GK29FIN\",\n\t3137: \"ETRS89 / ETRS-GK30FIN\",\n\t3138: \"ETRS89 / ETRS-GK31FIN\",\n\t3139: \"Vanua Levu 1915 / Vanua Levu Grid\",\n\t3140: \"Viti Levu 1912 / Viti Levu Grid\",\n\t3141: \"Fiji 1956 / UTM zone 60S\",\n\t3142: \"Fiji 1956 / UTM zone 1S\",\n\t3143: \"Fiji 1986 / Fiji Map Grid\",\n\t3144: \"FD54 / Faroe Lambert\",\n\t3145: \"ETRS89 / Faroe Lambert\",\n\t3146: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 6\",\n\t3147: \"Pulkovo 1942 / 3-degree Gauss-Kruger CM 18E\",\n\t3148: \"Indian 1960 / UTM zone 48N\",\n\t3149: \"Indian 1960 / UTM zone 49N\",\n\t3150: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 6\",\n\t3151: \"Pulkovo 1995 / 3-degree Gauss-Kruger CM 18E\",\n\t3152: \"ST74\",\n\t3153: \"NAD83(CSRS) / BC Albers\",\n\t3154: \"NAD83(CSRS) / UTM zone 7N\",\n\t3155: \"NAD83(CSRS) / UTM zone 8N\",\n\t3156: \"NAD83(CSRS) / UTM zone 9N\",\n\t3157: \"NAD83(CSRS) / UTM zone 10N\",\n\t3158: \"NAD83(CSRS) / UTM zone 14N\",\n\t3159: \"NAD83(CSRS) / UTM zone 15N\",\n\t3160: \"NAD83(CSRS) / UTM zone 16N\",\n\t3161: \"NAD83 / Ontario MNR Lambert\",\n\t3162: \"NAD83(CSRS) / Ontario MNR Lambert\",\n\t3163: \"RGNC91-93 / Lambert New Caledonia\",\n\t3164: \"ST87 Ouvea / UTM zone 58S\",\n\t3165: \"NEA74 Noumea / Noumea Lambert\",\n\t3166: \"NEA74 Noumea / Noumea Lambert 2\",\n\t3167: \"Kertau (RSO) / RSO Malaya (ch)\",\n\t3168: \"Kertau (RSO) / RSO Malaya (m)\",\n\t3169: \"RGNC91-93 / UTM zone 57S\",\n\t3170: \"RGNC91-93 / UTM zone 58S\",\n\t3171: \"RGNC91-93 / UTM zone 59S\",\n\t3172: \"IGN53 Mare / UTM zone 59S\",\n\t3173: \"fk89 / Faroe Lambert FK89\",\n\t3174: \"NAD83 / Great Lakes Albers\",\n\t3175: \"NAD83 / Great Lakes and St Lawrence Albers\",\n\t3176: \"Indian 1960 / TM 106 NE\",\n\t3177: \"LGD2006 / Libya TM\",\n\t3178: \"GR96 / UTM zone 18N\",\n\t3179: \"GR96 / UTM zone 19N\",\n\t3180: \"GR96 / UTM zone 20N\",\n\t3181: \"GR96 / UTM zone 21N\",\n\t3182: \"GR96 / UTM zone 22N\",\n\t3183: \"GR96 / UTM zone 23N\",\n\t3184: \"GR96 / UTM zone 24N\",\n\t3185: \"GR96 / UTM zone 25N\",\n\t3186: \"GR96 / UTM zone 26N\",\n\t3187: \"GR96 / UTM zone 27N\",\n\t3188: \"GR96 / UTM zone 28N\",\n\t3189: \"GR96 / UTM zone 29N\",\n\t3190: \"LGD2006 / Libya TM zone 5\",\n\t3191: \"LGD2006 / Libya TM zone 6\",\n\t3192: \"LGD2006 / Libya TM zone 7\",\n\t3193: \"LGD2006 / Libya TM zone 8\",\n\t3194: \"LGD2006 / Libya TM zone 9\",\n\t3195: \"LGD2006 / Libya TM zone 10\",\n\t3196: \"LGD2006 / Libya TM zone 11\",\n\t3197: \"LGD2006 / Libya TM zone 12\",\n\t3198: \"LGD2006 / Libya TM zone 13\",\n\t3199: \"LGD2006 / UTM zone 32N\",\n\t3200: \"FD58 / Iraq zone\",\n\t3201: \"LGD2006 / UTM zone 33N\",\n\t3202: \"LGD2006 / UTM zone 34N\",\n\t3203: \"LGD2006 / UTM zone 35N\",\n\t3204: \"WGS 84 / SCAR IMW SP19-20\",\n\t3205: \"WGS 84 / SCAR IMW SP21-22\",\n\t3206: \"WGS 84 / SCAR IMW SP23-24\",\n\t3207: \"WGS 84 / SCAR IMW SQ01-02\",\n\t3208: \"WGS 84 / SCAR IMW SQ19-20\",\n\t3209: \"WGS 84 / SCAR IMW SQ21-22\",\n\t3210: \"WGS 84 / SCAR IMW SQ37-38\",\n\t3211: \"WGS 84 / SCAR IMW SQ39-40\",\n\t3212: \"WGS 84 / SCAR IMW SQ41-42\",\n\t3213: \"WGS 84 / SCAR IMW SQ43-44\",\n\t3214: \"WGS 84 / SCAR IMW SQ45-46\",\n\t3215: \"WGS 84 / SCAR IMW SQ47-48\",\n\t3216: \"WGS 84 / SCAR IMW SQ49-50\",\n\t3217: \"WGS 84 / SCAR IMW SQ51-52\",\n\t3218: \"WGS 84 / SCAR IMW SQ53-54\",\n\t3219: \"WGS 84 / SCAR IMW SQ55-56\",\n\t3220: \"WGS 84 / SCAR IMW SQ57-58\",\n\t3221: \"WGS 84 / SCAR IMW SR13-14\",\n\t3222: \"WGS 84 / SCAR IMW SR15-16\",\n\t3223: \"WGS 84 / SCAR IMW SR17-18\",\n\t3224: \"WGS 84 / SCAR IMW SR19-20\",\n\t3225: \"WGS 84 / SCAR IMW SR27-28\",\n\t3226: \"WGS 84 / SCAR IMW SR29-30\",\n\t3227: \"WGS 84 / SCAR IMW SR31-32\",\n\t3228: \"WGS 84 / SCAR IMW SR33-34\",\n\t3229: \"WGS 84 / SCAR IMW SR35-36\",\n\t3230: \"WGS 84 / SCAR IMW SR37-38\",\n\t3231: \"WGS 84 / SCAR IMW SR39-40\",\n\t3232: \"WGS 84 / SCAR IMW SR41-42\",\n\t3233: \"WGS 84 / SCAR IMW SR43-44\",\n\t3234: \"WGS 84 / SCAR IMW SR45-46\",\n\t3235: \"WGS 84 / SCAR IMW SR47-48\",\n\t3236: \"WGS 84 / SCAR IMW SR49-50\",\n\t3237: \"WGS 84 / SCAR IMW SR51-52\",\n\t3238: \"WGS 84 / SCAR IMW SR53-54\",\n\t3239: \"WGS 84 / SCAR IMW SR55-56\",\n\t3240: \"WGS 84 / SCAR IMW SR57-58\",\n\t3241: \"WGS 84 / SCAR IMW SR59-60\",\n\t3242: \"WGS 84 / SCAR IMW SS04-06\",\n\t3243: \"WGS 84 / SCAR IMW SS07-09\",\n\t3244: \"WGS 84 / SCAR IMW SS10-12\",\n\t3245: \"WGS 84 / SCAR IMW SS13-15\",\n\t3246: \"WGS 84 / SCAR IMW SS16-18\",\n\t3247: \"WGS 84 / SCAR IMW SS19-21\",\n\t3248: \"WGS 84 / SCAR IMW SS25-27\",\n\t3249: \"WGS 84 / SCAR IMW SS28-30\",\n\t3250: \"WGS 84 / SCAR IMW SS31-33\",\n\t3251: \"WGS 84 / SCAR IMW SS34-36\",\n\t3252: \"WGS 84 / SCAR IMW SS37-39\",\n\t3253: \"WGS 84 / SCAR IMW SS40-42\",\n\t3254: \"WGS 84 / SCAR IMW SS43-45\",\n\t3255: \"WGS 84 / SCAR IMW SS46-48\",\n\t3256: \"WGS 84 / SCAR IMW SS49-51\",\n\t3257: \"WGS 84 / SCAR IMW SS52-54\",\n\t3258: \"WGS 84 / SCAR IMW SS55-57\",\n\t3259: \"WGS 84 / SCAR IMW SS58-60\",\n\t3260: \"WGS 84 / SCAR IMW ST01-04\",\n\t3261: \"WGS 84 / SCAR IMW ST05-08\",\n\t3262: \"WGS 84 / SCAR IMW ST09-12\",\n\t3263: \"WGS 84 / SCAR IMW ST13-16\",\n\t3264: \"WGS 84 / SCAR IMW ST17-20\",\n\t3265: \"WGS 84 / SCAR IMW ST21-24\",\n\t3266: \"WGS 84 / SCAR IMW ST25-28\",\n\t3267: \"WGS 84 / SCAR IMW ST29-32\",\n\t3268: \"WGS 84 / SCAR IMW ST33-36\",\n\t3269: \"WGS 84 / SCAR IMW ST37-40\",\n\t3270: \"WGS 84 / SCAR IMW ST41-44\",\n\t3271: \"WGS 84 / SCAR IMW ST45-48\",\n\t3272: \"WGS 84 / SCAR IMW ST49-52\",\n\t3273: \"WGS 84 / SCAR IMW ST53-56\",\n\t3274: \"WGS 84 / SCAR IMW ST57-60\",\n\t3275: \"WGS 84 / SCAR IMW SU01-05\",\n\t3276: \"WGS 84 / SCAR IMW SU06-10\",\n\t3277: \"WGS 84 / SCAR IMW SU11-15\",\n\t3278: \"WGS 84 / SCAR IMW SU16-20\",\n\t3279: \"WGS 84 / SCAR IMW SU21-25\",\n\t3280: \"WGS 84 / SCAR IMW SU26-30\",\n\t3281: \"WGS 84 / SCAR IMW SU31-35\",\n\t3282: \"WGS 84 / SCAR IMW SU36-40\",\n\t3283: \"WGS 84 / SCAR IMW SU41-45\",\n\t3284: \"WGS 84 / SCAR IMW SU46-50\",\n\t3285: \"WGS 84 / SCAR IMW SU51-55\",\n\t3286: \"WGS 84 / SCAR IMW SU56-60\",\n\t3287: \"WGS 84 / SCAR IMW SV01-10\",\n\t3288: \"WGS 84 / SCAR IMW SV11-20\",\n\t3289: \"WGS 84 / SCAR IMW SV21-30\",\n\t3290: \"WGS 84 / SCAR IMW SV31-40\",\n\t3291: \"WGS 84 / SCAR IMW SV41-50\",\n\t3292: \"WGS 84 / SCAR IMW SV51-60\",\n\t3293: \"WGS 84 / SCAR IMW SW01-60\",\n\t3294: \"WGS 84 / USGS Transantarctic Mountains\",\n\t3295: \"Guam 1963 / Yap Islands\",\n\t3296: \"RGPF / UTM zone 5S\",\n\t3297: \"RGPF / UTM zone 6S\",\n\t3298: \"RGPF / UTM zone 7S\",\n\t3299: \"RGPF / UTM zone 8S\",\n\t3300: \"Estonian Coordinate System of 1992\",\n\t3301: \"Estonian Coordinate System of 1997\",\n\t3302: \"IGN63 Hiva Oa / UTM zone 7S\",\n\t3303: \"Fatu Iva 72 / UTM zone 7S\",\n\t3304: \"Tahiti 79 / UTM zone 6S\",\n\t3305: \"Moorea 87 / UTM zone 6S\",\n\t3306: \"Maupiti 83 / UTM zone 5S\",\n\t3307: \"Nakhl-e Ghanem / UTM zone 39N\",\n\t3308: \"GDA94 / NSW Lambert\",\n\t3309: \"NAD27 / California Albers\",\n\t3310: \"NAD83 / California Albers\",\n\t3311: \"NAD83(HARN) / California Albers\",\n\t3312: \"CSG67 / UTM zone 21N\",\n\t3313: \"RGFG95 / UTM zone 21N\",\n\t3314: \"Katanga 1955 / Katanga Lambert\",\n\t3315: \"Katanga 1955 / Katanga TM\",\n\t3316: \"Kasai 1953 / Congo TM zone 22\",\n\t3317: \"Kasai 1953 / Congo TM zone 24\",\n\t3318: \"IGC 1962 / Congo TM zone 12\",\n\t3319: \"IGC 1962 / Congo TM zone 14\",\n\t3320: \"IGC 1962 / Congo TM zone 16\",\n\t3321: \"IGC 1962 / Congo TM zone 18\",\n\t3322: \"IGC 1962 / Congo TM zone 20\",\n\t3323: \"IGC 1962 / Congo TM zone 22\",\n\t3324: \"IGC 1962 / Congo TM zone 24\",\n\t3325: \"IGC 1962 / Congo TM zone 26\",\n\t3326: \"IGC 1962 / Congo TM zone 28\",\n\t3327: \"IGC 1962 / Congo TM zone 30\",\n\t3328: \"Pulkovo 1942(58) / GUGiK-80\",\n\t3329: \"Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 5\",\n\t3330: \"Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 6\",\n\t3331: \"Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 7\",\n\t3332: \"Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 8\",\n\t3333: \"Pulkovo 1942(58) / Gauss-Kruger zone 3\",\n\t3334: \"Pulkovo 1942(58) / Gauss-Kruger zone 4\",\n\t3335: \"Pulkovo 1942(58) / Gauss-Kruger zone 5\",\n\t3336: \"IGN 1962 Kerguelen / UTM zone 42S\",\n\t3337: \"Le Pouce 1934 / Mauritius Grid\",\n\t3338: \"NAD83 / Alaska Albers\",\n\t3339: \"IGCB 1955 / Congo TM zone 12\",\n\t3340: \"IGCB 1955 / Congo TM zone 14\",\n\t3341: \"IGCB 1955 / Congo TM zone 16\",\n\t3342: \"IGCB 1955 / UTM zone 33S\",\n\t3343: \"Mauritania 1999 / UTM zone 28N\",\n\t3344: \"Mauritania 1999 / UTM zone 29N\",\n\t3345: \"Mauritania 1999 / UTM zone 30N\",\n\t3346: \"LKS94 / Lithuania TM\",\n\t3347: \"NAD83 / Statistics Canada Lambert\",\n\t3348: \"NAD83(CSRS) / Statistics Canada Lambert\",\n\t3349: \"WGS 84 / PDC Mercator\",\n\t3350: \"Pulkovo 1942 / CS63 zone C0\",\n\t3351: \"Pulkovo 1942 / CS63 zone C1\",\n\t3352: \"Pulkovo 1942 / CS63 zone C2\",\n\t3353: \"Mhast (onshore) / UTM zone 32S\",\n\t3354: \"Mhast (offshore) / UTM zone 32S\",\n\t3355: \"Egypt Gulf of Suez S-650 TL / Red Belt\",\n\t3356: \"Grand Cayman 1959 / UTM zone 17N\",\n\t3357: \"Little Cayman 1961 / UTM zone 17N\",\n\t3358: \"NAD83(HARN) / North Carolina\",\n\t3359: \"NAD83(HARN) / North Carolina (ftUS)\",\n\t3360: \"NAD83(HARN) / South Carolina\",\n\t3361: \"NAD83(HARN) / South Carolina (ft)\",\n\t3362: \"NAD83(HARN) / Pennsylvania North\",\n\t3363: \"NAD83(HARN) / Pennsylvania North (ftUS)\",\n\t3364: \"NAD83(HARN) / Pennsylvania South\",\n\t3365: \"NAD83(HARN) / Pennsylvania South (ftUS)\",\n\t3366: \"Hong Kong 1963 Grid System\",\n\t3367: \"IGN Astro 1960 / UTM zone 28N\",\n\t3368: \"IGN Astro 1960 / UTM zone 29N\",\n\t3369: \"IGN Astro 1960 / UTM zone 30N\",\n\t3370: \"NAD27 / UTM zone 59N\",\n\t3371: \"NAD27 / UTM zone 60N\",\n\t3372: \"NAD83 / UTM zone 59N\",\n\t3373: \"NAD83 / UTM zone 60N\",\n\t3374: \"FD54 / UTM zone 29N\",\n\t3375: \"GDM2000 / Peninsula RSO\",\n\t3376: \"GDM2000 / East Malaysia BRSO\",\n\t3377: \"GDM2000 / Johor Grid\",\n\t3378: \"GDM2000 / Sembilan and Melaka Grid\",\n\t3379: \"GDM2000 / Pahang Grid\",\n\t3380: \"GDM2000 / Selangor Grid\",\n\t3381: \"GDM2000 / Terengganu Grid\",\n\t3382: \"GDM2000 / Pinang Grid\",\n\t3383: \"GDM2000 / Kedah and Perlis Grid\",\n\t3384: \"GDM2000 / Perak Grid\",\n\t3385: \"GDM2000 / Kelantan Grid\",\n\t3386: \"KKJ / Finland zone 0\",\n\t3387: \"KKJ / Finland zone 5\",\n\t3388: \"Pulkovo 1942 / Caspian Sea Mercator\",\n\t3389: \"Pulkovo 1942 / 3-degree Gauss-Kruger zone 60\",\n\t3390: \"Pulkovo 1995 / 3-degree Gauss-Kruger zone 60\",\n\t3391: \"Karbala 1979 / UTM zone 37N\",\n\t3392: \"Karbala 1979 / UTM zone 38N\",\n\t3393: \"Karbala 1979 / UTM zone 39N\",\n\t3394: \"Nahrwan 1934 / Iraq zone\",\n\t3395: \"WGS 84 / World Mercator\",\n\t3396: \"PD/83 / 3-degree Gauss-Kruger zone 3\",\n\t3397: \"PD/83 / 3-degree Gauss-Kruger zone 4\",\n\t3398: \"RD/83 / 3-degree Gauss-Kruger zone 4\",\n\t3399: \"RD/83 / 3-degree Gauss-Kruger zone 5\",\n\t3400: \"NAD83 / Alberta 10-TM (Forest)\",\n\t3401: \"NAD83 / Alberta 10-TM (Resource)\",\n\t3402: \"NAD83(CSRS) / Alberta 10-TM (Forest)\",\n\t3403: \"NAD83(CSRS) / Alberta 10-TM (Resource)\",\n\t3404: \"NAD83(HARN) / North Carolina (ftUS)\",\n\t3405: \"VN-2000 / UTM zone 48N\",\n\t3406: \"VN-2000 / UTM zone 49N\",\n\t3407: \"Hong Kong 1963 Grid System\",\n\t3408: \"NSIDC EASE-Grid North\",\n\t3409: \"NSIDC EASE-Grid South\",\n\t3410: \"NSIDC EASE-Grid Global\",\n\t3411: \"NSIDC Sea Ice Polar Stereographic North\",\n\t3412: \"NSIDC Sea Ice Polar Stereographic South\",\n\t3413: \"WGS 84 / NSIDC Sea Ice Polar Stereographic North\",\n\t3414: \"SVY21 / Singapore TM\",\n\t3415: \"WGS 72BE / South China Sea Lambert\",\n\t3416: \"ETRS89 / Austria Lambert\",\n\t3417: \"NAD83 / Iowa North (ftUS)\",\n\t3418: \"NAD83 / Iowa South (ftUS)\",\n\t3419: \"NAD83 / Kansas North (ftUS)\",\n\t3420: \"NAD83 / Kansas South (ftUS)\",\n\t3421: \"NAD83 / Nevada East (ftUS)\",\n\t3422: \"NAD83 / Nevada Central (ftUS)\",\n\t3423: \"NAD83 / Nevada West (ftUS)\",\n\t3424: \"NAD83 / New Jersey (ftUS)\",\n\t3425: \"NAD83(HARN) / Iowa North (ftUS)\",\n\t3426: \"NAD83(HARN) / Iowa South (ftUS)\",\n\t3427: \"NAD83(HARN) / Kansas North (ftUS)\",\n\t3428: \"NAD83(HARN) / Kansas South (ftUS)\",\n\t3429: \"NAD83(HARN) / Nevada East (ftUS)\",\n\t3430: \"NAD83(HARN) / Nevada Central (ftUS)\",\n\t3431: \"NAD83(HARN) / Nevada West (ftUS)\",\n\t3432: \"NAD83(HARN) / New Jersey (ftUS)\",\n\t3433: \"NAD83 / Arkansas North (ftUS)\",\n\t3434: \"NAD83 / Arkansas South (ftUS)\",\n\t3435: \"NAD83 / Illinois East (ftUS)\",\n\t3436: \"NAD83 / Illinois West (ftUS)\",\n\t3437: \"NAD83 / New Hampshire (ftUS)\",\n\t3438: \"NAD83 / Rhode Island (ftUS)\",\n\t3439: \"PSD93 / UTM zone 39N\",\n\t3440: \"PSD93 / UTM zone 40N\",\n\t3441: \"NAD83(HARN) / Arkansas North (ftUS)\",\n\t3442: \"NAD83(HARN) / Arkansas South (ftUS)\",\n\t3443: \"NAD83(HARN) / Illinois East (ftUS)\",\n\t3444: \"NAD83(HARN) / Illinois West (ftUS)\",\n\t3445: \"NAD83(HARN) / New Hampshire (ftUS)\",\n\t3446: \"NAD83(HARN) / Rhode Island (ftUS)\",\n\t3447: \"ETRS89 / Belgian Lambert 2005\",\n\t3448: \"JAD2001 / Jamaica Metric Grid\",\n\t3449: \"JAD2001 / UTM zone 17N\",\n\t3450: \"JAD2001 / UTM zone 18N\",\n\t3451: \"NAD83 / Louisiana North (ftUS)\",\n\t3452: \"NAD83 / Louisiana South (ftUS)\",\n\t3453: \"NAD83 / Louisiana Offshore (ftUS)\",\n\t3454: \"NAD83 / South Dakota North (ftUS)\",\n\t3455: \"NAD83 / South Dakota South (ftUS)\",\n\t3456: \"NAD83(HARN) / Louisiana North (ftUS)\",\n\t3457: \"NAD83(HARN) / Louisiana South (ftUS)\",\n\t3458: \"NAD83(HARN) / South Dakota North (ftUS)\",\n\t3459: \"NAD83(HARN) / South Dakota South (ftUS)\",\n\t3460: \"Fiji 1986 / Fiji Map Grid\",\n\t3461: \"Dabola 1981 / UTM zone 28N\",\n\t3462: \"Dabola 1981 / UTM zone 29N\",\n\t3463: \"NAD83 / Maine CS2000 Central\",\n\t3464: \"NAD83(HARN) / Maine CS2000 Central\",\n\t3465: \"NAD83(NSRS2007) / Alabama East\",\n\t3466: \"NAD83(NSRS2007) / Alabama West\",\n\t3467: \"NAD83(NSRS2007) / Alaska Albers\",\n\t3468: \"NAD83(NSRS2007) / Alaska zone 1\",\n\t3469: \"NAD83(NSRS2007) / Alaska zone 2\",\n\t3470: \"NAD83(NSRS2007) / Alaska zone 3\",\n\t3471: \"NAD83(NSRS2007) / Alaska zone 4\",\n\t3472: \"NAD83(NSRS2007) / Alaska zone 5\",\n\t3473: \"NAD83(NSRS2007) / Alaska zone 6\",\n\t3474: \"NAD83(NSRS2007) / Alaska zone 7\",\n\t3475: \"NAD83(NSRS2007) / Alaska zone 8\",\n\t3476: \"NAD83(NSRS2007) / Alaska zone 9\",\n\t3477: \"NAD83(NSRS2007) / Alaska zone 10\",\n\t3478: \"NAD83(NSRS2007) / Arizona Central\",\n\t3479: \"NAD83(NSRS2007) / Arizona Central (ft)\",\n\t3480: \"NAD83(NSRS2007) / Arizona East\",\n\t3481: \"NAD83(NSRS2007) / Arizona East (ft)\",\n\t3482: \"NAD83(NSRS2007) / Arizona West\",\n\t3483: \"NAD83(NSRS2007) / Arizona West (ft)\",\n\t3484: \"NAD83(NSRS2007) / Arkansas North\",\n\t3485: \"NAD83(NSRS2007) / Arkansas North (ftUS)\",\n\t3486: \"NAD83(NSRS2007) / Arkansas South\",\n\t3487: \"NAD83(NSRS2007) / Arkansas South (ftUS)\",\n\t3488: \"NAD83(NSRS2007) / California Albers\",\n\t3489: \"NAD83(NSRS2007) / California zone 1\",\n\t3490: \"NAD83(NSRS2007) / California zone 1 (ftUS)\",\n\t3491: \"NAD83(NSRS2007) / California zone 2\",\n\t3492: \"NAD83(NSRS2007) / California zone 2 (ftUS)\",\n\t3493: \"NAD83(NSRS2007) / California zone 3\",\n\t3494: \"NAD83(NSRS2007) / California zone 3 (ftUS)\",\n\t3495: \"NAD83(NSRS2007) / California zone 4\",\n\t3496: \"NAD83(NSRS2007) / California zone 4 (ftUS)\",\n\t3497: \"NAD83(NSRS2007) / California zone 5\",\n\t3498: \"NAD83(NSRS2007) / California zone 5 (ftUS)\",\n\t3499: \"NAD83(NSRS2007) / California zone 6\",\n\t3500: \"NAD83(NSRS2007) / California zone 6 (ftUS)\",\n\t3501: \"NAD83(NSRS2007) / Colorado Central\",\n\t3502: \"NAD83(NSRS2007) / Colorado Central (ftUS)\",\n\t3503: \"NAD83(NSRS2007) / Colorado North\",\n\t3504: \"NAD83(NSRS2007) / Colorado North (ftUS)\",\n\t3505: \"NAD83(NSRS2007) / Colorado South\",\n\t3506: \"NAD83(NSRS2007) / Colorado South (ftUS)\",\n\t3507: \"NAD83(NSRS2007) / Connecticut\",\n\t3508: \"NAD83(NSRS2007) / Connecticut (ftUS)\",\n\t3509: \"NAD83(NSRS2007) / Delaware\",\n\t3510: \"NAD83(NSRS2007) / Delaware (ftUS)\",\n\t3511: \"NAD83(NSRS2007) / Florida East\",\n\t3512: \"NAD83(NSRS2007) / Florida East (ftUS)\",\n\t3513: \"NAD83(NSRS2007) / Florida GDL Albers\",\n\t3514: \"NAD83(NSRS2007) / Florida North\",\n\t3515: \"NAD83(NSRS2007) / Florida North (ftUS)\",\n\t3516: \"NAD83(NSRS2007) / Florida West\",\n\t3517: \"NAD83(NSRS2007) / Florida West (ftUS)\",\n\t3518: \"NAD83(NSRS2007) / Georgia East\",\n\t3519: \"NAD83(NSRS2007) / Georgia East (ftUS)\",\n\t3520: \"NAD83(NSRS2007) / Georgia West\",\n\t3521: \"NAD83(NSRS2007) / Georgia West (ftUS)\",\n\t3522: \"NAD83(NSRS2007) / Idaho Central\",\n\t3523: \"NAD83(NSRS2007) / Idaho Central (ftUS)\",\n\t3524: \"NAD83(NSRS2007) / Idaho East\",\n\t3525: \"NAD83(NSRS2007) / Idaho East (ftUS)\",\n\t3526: \"NAD83(NSRS2007) / Idaho West\",\n\t3527: \"NAD83(NSRS2007) / Idaho West (ftUS)\",\n\t3528: \"NAD83(NSRS2007) / Illinois East\",\n\t3529: \"NAD83(NSRS2007) / Illinois East (ftUS)\",\n\t3530: \"NAD83(NSRS2007) / Illinois West\",\n\t3531: \"NAD83(NSRS2007) / Illinois West (ftUS)\",\n\t3532: \"NAD83(NSRS2007) / Indiana East\",\n\t3533: \"NAD83(NSRS2007) / Indiana East (ftUS)\",\n\t3534: \"NAD83(NSRS2007) / Indiana West\",\n\t3535: \"NAD83(NSRS2007) / Indiana West (ftUS)\",\n\t3536: \"NAD83(NSRS2007) / Iowa North\",\n\t3537: \"NAD83(NSRS2007) / Iowa North (ftUS)\",\n\t3538: \"NAD83(NSRS2007) / Iowa South\",\n\t3539: \"NAD83(NSRS2007) / Iowa South (ftUS)\",\n\t3540: \"NAD83(NSRS2007) / Kansas North\",\n\t3541: \"NAD83(NSRS2007) / Kansas North (ftUS)\",\n\t3542: \"NAD83(NSRS2007) / Kansas South\",\n\t3543: \"NAD83(NSRS2007) / Kansas South (ftUS)\",\n\t3544: \"NAD83(NSRS2007) / Kentucky North\",\n\t3545: \"NAD83(NSRS2007) / Kentucky North (ftUS)\",\n\t3546: \"NAD83(NSRS2007) / Kentucky Single Zone\",\n\t3547: \"NAD83(NSRS2007) / Kentucky Single Zone (ftUS)\",\n\t3548: \"NAD83(NSRS2007) / Kentucky South\",\n\t3549: \"NAD83(NSRS2007) / Kentucky South (ftUS)\",\n\t3550: \"NAD83(NSRS2007) / Louisiana North\",\n\t3551: \"NAD83(NSRS2007) / Louisiana North (ftUS)\",\n\t3552: \"NAD83(NSRS2007) / Louisiana South\",\n\t3553: \"NAD83(NSRS2007) / Louisiana South (ftUS)\",\n\t3554: \"NAD83(NSRS2007) / Maine CS2000 Central\",\n\t3555: \"NAD83(NSRS2007) / Maine CS2000 East\",\n\t3556: \"NAD83(NSRS2007) / Maine CS2000 West\",\n\t3557: \"NAD83(NSRS2007) / Maine East\",\n\t3558: \"NAD83(NSRS2007) / Maine West\",\n\t3559: \"NAD83(NSRS2007) / Maryland\",\n\t3560: \"NAD83 / Utah North (ftUS)\",\n\t3561: \"Old Hawaiian / Hawaii zone 1\",\n\t3562: \"Old Hawaiian / Hawaii zone 2\",\n\t3563: \"Old Hawaiian / Hawaii zone 3\",\n\t3564: \"Old Hawaiian / Hawaii zone 4\",\n\t3565: \"Old Hawaiian / Hawaii zone 5\",\n\t3566: \"NAD83 / Utah Central (ftUS)\",\n\t3567: \"NAD83 / Utah South (ftUS)\",\n\t3568: \"NAD83(HARN) / Utah North (ftUS)\",\n\t3569: \"NAD83(HARN) / Utah Central (ftUS)\",\n\t3570: \"NAD83(HARN) / Utah South (ftUS)\",\n\t3571: \"WGS 84 / North Pole LAEA Bering Sea\",\n\t3572: \"WGS 84 / North Pole LAEA Alaska\",\n\t3573: \"WGS 84 / North Pole LAEA Canada\",\n\t3574: \"WGS 84 / North Pole LAEA Atlantic\",\n\t3575: \"WGS 84 / North Pole LAEA Europe\",\n\t3576: \"WGS 84 / North Pole LAEA Russia\",\n\t3577: \"GDA94 / Australian Albers\",\n\t3578: \"NAD83 / Yukon Albers\",\n\t3579: \"NAD83(CSRS) / Yukon Albers\",\n\t3580: \"NAD83 / NWT Lambert\",\n\t3581: \"NAD83(CSRS) / NWT Lambert\",\n\t3582: \"NAD83(NSRS2007) / Maryland (ftUS)\",\n\t3583: \"NAD83(NSRS2007) / Massachusetts Island\",\n\t3584: \"NAD83(NSRS2007) / Massachusetts Island (ftUS)\",\n\t3585: \"NAD83(NSRS2007) / Massachusetts Mainland\",\n\t3586: \"NAD83(NSRS2007) / Massachusetts Mainland (ftUS)\",\n\t3587: \"NAD83(NSRS2007) / Michigan Central\",\n\t3588: \"NAD83(NSRS2007) / Michigan Central (ft)\",\n\t3589: \"NAD83(NSRS2007) / Michigan North\",\n\t3590: \"NAD83(NSRS2007) / Michigan North (ft)\",\n\t3591: \"NAD83(NSRS2007) / Michigan Oblique Mercator\",\n\t3592: \"NAD83(NSRS2007) / Michigan South\",\n\t3593: \"NAD83(NSRS2007) / Michigan South (ft)\",\n\t3594: \"NAD83(NSRS2007) / Minnesota Central\",\n\t3595: \"NAD83(NSRS2007) / Minnesota North\",\n\t3596: \"NAD83(NSRS2007) / Minnesota South\",\n\t3597: \"NAD83(NSRS2007) / Mississippi East\",\n\t3598: \"NAD83(NSRS2007) / Mississippi East (ftUS)\",\n\t3599: \"NAD83(NSRS2007) / Mississippi West\",\n\t3600: \"NAD83(NSRS2007) / Mississippi West (ftUS)\",\n\t3601: \"NAD83(NSRS2007) / Missouri Central\",\n\t3602: \"NAD83(NSRS2007) / Missouri East\",\n\t3603: \"NAD83(NSRS2007) / Missouri West\",\n\t3604: \"NAD83(NSRS2007) / Montana\",\n\t3605: \"NAD83(NSRS2007) / Montana (ft)\",\n\t3606: \"NAD83(NSRS2007) / Nebraska\",\n\t3607: \"NAD83(NSRS2007) / Nevada Central\",\n\t3608: \"NAD83(NSRS2007) / Nevada Central (ftUS)\",\n\t3609: \"NAD83(NSRS2007) / Nevada East\",\n\t3610: \"NAD83(NSRS2007) / Nevada East (ftUS)\",\n\t3611: \"NAD83(NSRS2007) / Nevada West\",\n\t3612: \"NAD83(NSRS2007) / Nevada West (ftUS)\",\n\t3613: \"NAD83(NSRS2007) / New Hampshire\",\n\t3614: \"NAD83(NSRS2007) / New Hampshire (ftUS)\",\n\t3615: \"NAD83(NSRS2007) / New Jersey\",\n\t3616: \"NAD83(NSRS2007) / New Jersey (ftUS)\",\n\t3617: \"NAD83(NSRS2007) / New Mexico Central\",\n\t3618: \"NAD83(NSRS2007) / New Mexico Central (ftUS)\",\n\t3619: \"NAD83(NSRS2007) / New Mexico East\",\n\t3620: \"NAD83(NSRS2007) / New Mexico East (ftUS)\",\n\t3621: \"NAD83(NSRS2007) / New Mexico West\",\n\t3622: \"NAD83(NSRS2007) / New Mexico West (ftUS)\",\n\t3623: \"NAD83(NSRS2007) / New York Central\",\n\t3624: \"NAD83(NSRS2007) / New York Central (ftUS)\",\n\t3625: \"NAD83(NSRS2007) / New York East\",\n\t3626: \"NAD83(NSRS2007) / New York East (ftUS)\",\n\t3627: \"NAD83(NSRS2007) / New York Long Island\",\n\t3628: \"NAD83(NSRS2007) / New York Long Island (ftUS)\",\n\t3629: \"NAD83(NSRS2007) / New York West\",\n\t3630: \"NAD83(NSRS2007) / New York West (ftUS)\",\n\t3631: \"NAD83(NSRS2007) / North Carolina\",\n\t3632: \"NAD83(NSRS2007) / North Carolina (ftUS)\",\n\t3633: \"NAD83(NSRS2007) / North Dakota North\",\n\t3634: \"NAD83(NSRS2007) / North Dakota North (ft)\",\n\t3635: \"NAD83(NSRS2007) / North Dakota South\",\n\t3636: \"NAD83(NSRS2007) / North Dakota South (ft)\",\n\t3637: \"NAD83(NSRS2007) / Ohio North\",\n\t3638: \"NAD83(NSRS2007) / Ohio South\",\n\t3639: \"NAD83(NSRS2007) / Oklahoma North\",\n\t3640: \"NAD83(NSRS2007) / Oklahoma North (ftUS)\",\n\t3641: \"NAD83(NSRS2007) / Oklahoma South\",\n\t3642: \"NAD83(NSRS2007) / Oklahoma South (ftUS)\",\n\t3643: \"NAD83(NSRS2007) / Oregon LCC (m)\",\n\t3644: \"NAD83(NSRS2007) / Oregon GIC Lambert (ft)\",\n\t3645: \"NAD83(NSRS2007) / Oregon North\",\n\t3646: \"NAD83(NSRS2007) / Oregon North (ft)\",\n\t3647: \"NAD83(NSRS2007) / Oregon South\",\n\t3648: \"NAD83(NSRS2007) / Oregon South (ft)\",\n\t3649: \"NAD83(NSRS2007) / Pennsylvania North\",\n\t3650: \"NAD83(NSRS2007) / Pennsylvania North (ftUS)\",\n\t3651: \"NAD83(NSRS2007) / Pennsylvania South\",\n\t3652: \"NAD83(NSRS2007) / Pennsylvania South (ftUS)\",\n\t3653: \"NAD83(NSRS2007) / Rhode Island\",\n\t3654: \"NAD83(NSRS2007) / Rhode Island (ftUS)\",\n\t3655: \"NAD83(NSRS2007) / South Carolina\",\n\t3656: \"NAD83(NSRS2007) / South Carolina (ft)\",\n\t3657: \"NAD83(NSRS2007) / South Dakota North\",\n\t3658: \"NAD83(NSRS2007) / South Dakota North (ftUS)\",\n\t3659: \"NAD83(NSRS2007) / South Dakota South\",\n\t3660: \"NAD83(NSRS2007) / South Dakota South (ftUS)\",\n\t3661: \"NAD83(NSRS2007) / Tennessee\",\n\t3662: \"NAD83(NSRS2007) / Tennessee (ftUS)\",\n\t3663: \"NAD83(NSRS2007) / Texas Central\",\n\t3664: \"NAD83(NSRS2007) / Texas Central (ftUS)\",\n\t3665: \"NAD83(NSRS2007) / Texas Centric Albers Equal Area\",\n\t3666: \"NAD83(NSRS2007) / Texas Centric Lambert Conformal\",\n\t3667: \"NAD83(NSRS2007) / Texas North\",\n\t3668: \"NAD83(NSRS2007) / Texas North (ftUS)\",\n\t3669: \"NAD83(NSRS2007) / Texas North Central\",\n\t3670: \"NAD83(NSRS2007) / Texas North Central (ftUS)\",\n\t3671: \"NAD83(NSRS2007) / Texas South\",\n\t3672: \"NAD83(NSRS2007) / Texas South (ftUS)\",\n\t3673: \"NAD83(NSRS2007) / Texas South Central\",\n\t3674: \"NAD83(NSRS2007) / Texas South Central (ftUS)\",\n\t3675: \"NAD83(NSRS2007) / Utah Central\",\n\t3676: \"NAD83(NSRS2007) / Utah Central (ft)\",\n\t3677: \"NAD83(NSRS2007) / Utah Central (ftUS)\",\n\t3678: \"NAD83(NSRS2007) / Utah North\",\n\t3679: \"NAD83(NSRS2007) / Utah North (ft)\",\n\t3680: \"NAD83(NSRS2007) / Utah North (ftUS)\",\n\t3681: \"NAD83(NSRS2007) / Utah South\",\n\t3682: \"NAD83(NSRS2007) / Utah South (ft)\",\n\t3683: \"NAD83(NSRS2007) / Utah South (ftUS)\",\n\t3684: \"NAD83(NSRS2007) / Vermont\",\n\t3685: \"NAD83(NSRS2007) / Virginia North\",\n\t3686: \"NAD83(NSRS2007) / Virginia North (ftUS)\",\n\t3687: \"NAD83(NSRS2007) / Virginia South\",\n\t3688: \"NAD83(NSRS2007) / Virginia South (ftUS)\",\n\t3689: \"NAD83(NSRS2007) / Washington North\",\n\t3690: \"NAD83(NSRS2007) / Washington North (ftUS)\",\n\t3691: \"NAD83(NSRS2007) / Washington South\",\n\t3692: \"NAD83(NSRS2007) / Washington South (ftUS)\",\n\t3693: \"NAD83(NSRS2007) / West Virginia North\",\n\t3694: \"NAD83(NSRS2007) / West Virginia South\",\n\t3695: \"NAD83(NSRS2007) / Wisconsin Central\",\n\t3696: \"NAD83(NSRS2007) / Wisconsin Central (ftUS)\",\n\t3697: \"NAD83(NSRS2007) / Wisconsin North\",\n\t3698: \"NAD83(NSRS2007) / Wisconsin North (ftUS)\",\n\t3699: \"NAD83(NSRS2007) / Wisconsin South\",\n\t3700: \"NAD83(NSRS2007) / Wisconsin South (ftUS)\",\n\t3701: \"NAD83(NSRS2007) / Wisconsin Transverse Mercator\",\n\t3702: \"NAD83(NSRS2007) / Wyoming East\",\n\t3703: \"NAD83(NSRS2007) / Wyoming East Central\",\n\t3704: \"NAD83(NSRS2007) / Wyoming West Central\",\n\t3705: \"NAD83(NSRS2007) / Wyoming West\",\n\t3706: \"NAD83(NSRS2007) / UTM zone 59N\",\n\t3707: \"NAD83(NSRS2007) / UTM zone 60N\",\n\t3708: \"NAD83(NSRS2007) / UTM zone 1N\",\n\t3709: \"NAD83(NSRS2007) / UTM zone 2N\",\n\t3710: \"NAD83(NSRS2007) / UTM zone 3N\",\n\t3711: \"NAD83(NSRS2007) / UTM zone 4N\",\n\t3712: \"NAD83(NSRS2007) / UTM zone 5N\",\n\t3713: \"NAD83(NSRS2007) / UTM zone 6N\",\n\t3714: \"NAD83(NSRS2007) / UTM zone 7N\",\n\t3715: \"NAD83(NSRS2007) / UTM zone 8N\",\n\t3716: \"NAD83(NSRS2007) / UTM zone 9N\",\n\t3717: \"NAD83(NSRS2007) / UTM zone 10N\",\n\t3718: \"NAD83(NSRS2007) / UTM zone 11N\",\n\t3719: \"NAD83(NSRS2007) / UTM zone 12N\",\n\t3720: \"NAD83(NSRS2007) / UTM zone 13N\",\n\t3721: \"NAD83(NSRS2007) / UTM zone 14N\",\n\t3722: \"NAD83(NSRS2007) / UTM zone 15N\",\n\t3723: \"NAD83(NSRS2007) / UTM zone 16N\",\n\t3724: \"NAD83(NSRS2007) / UTM zone 17N\",\n\t3725: \"NAD83(NSRS2007) / UTM zone 18N\",\n\t3726: \"NAD83(NSRS2007) / UTM zone 19N\",\n\t3727: \"Reunion 1947 / TM Reunion\",\n\t3728: \"NAD83(NSRS2007) / Ohio North (ftUS)\",\n\t3729: \"NAD83(NSRS2007) / Ohio South (ftUS)\",\n\t3730: \"NAD83(NSRS2007) / Wyoming East (ftUS)\",\n\t3731: \"NAD83(NSRS2007) / Wyoming East Central (ftUS)\",\n\t3732: \"NAD83(NSRS2007) / Wyoming West Central (ftUS)\",\n\t3733: \"NAD83(NSRS2007) / Wyoming West (ftUS)\",\n\t3734: \"NAD83 / Ohio North (ftUS)\",\n\t3735: \"NAD83 / Ohio South (ftUS)\",\n\t3736: \"NAD83 / Wyoming East (ftUS)\",\n\t3737: \"NAD83 / Wyoming East Central (ftUS)\",\n\t3738: \"NAD83 / Wyoming West Central (ftUS)\",\n\t3739: \"NAD83 / Wyoming West (ftUS)\",\n\t3740: \"NAD83(HARN) / UTM zone 10N\",\n\t3741: \"NAD83(HARN) / UTM zone 11N\",\n\t3742: \"NAD83(HARN) / UTM zone 12N\",\n\t3743: \"NAD83(HARN) / UTM zone 13N\",\n\t3744: \"NAD83(HARN) / UTM zone 14N\",\n\t3745: \"NAD83(HARN) / UTM zone 15N\",\n\t3746: \"NAD83(HARN) / UTM zone 16N\",\n\t3747: \"NAD83(HARN) / UTM zone 17N\",\n\t3748: \"NAD83(HARN) / UTM zone 18N\",\n\t3749: \"NAD83(HARN) / UTM zone 19N\",\n\t3750: \"NAD83(HARN) / UTM zone 4N\",\n\t3751: \"NAD83(HARN) / UTM zone 5N\",\n\t3752: \"WGS 84 / Mercator 41\",\n\t3753: \"NAD83(HARN) / Ohio North (ftUS)\",\n\t3754: \"NAD83(HARN) / Ohio South (ftUS)\",\n\t3755: \"NAD83(HARN) / Wyoming East (ftUS)\",\n\t3756: \"NAD83(HARN) / Wyoming East Central (ftUS)\",\n\t3757: \"NAD83(HARN) / Wyoming West Central (ftUS)\",\n\t3758: \"NAD83(HARN) / Wyoming West (ftUS)\",\n\t3759: \"NAD83 / Hawaii zone 3 (ftUS)\",\n\t3760: \"NAD83(HARN) / Hawaii zone 3 (ftUS)\",\n\t3761: \"NAD83(CSRS) / UTM zone 22N\",\n\t3762: \"WGS 84 / South Georgia Lambert\",\n\t3763: \"ETRS89 / Portugal TM06\",\n\t3764: \"NZGD2000 / Chatham Island Circuit 2000\",\n\t3765: \"HTRS96 / Croatia TM\",\n\t3766: \"HTRS96 / Croatia LCC\",\n\t3767: \"HTRS96 / UTM zone 33N\",\n\t3768: \"HTRS96 / UTM zone 34N\",\n\t3769: \"Bermuda 1957 / UTM zone 20N\",\n\t3770: \"BDA2000 / Bermuda 2000 National Grid\",\n\t3771: \"NAD27 / Alberta 3TM ref merid 111 W\",\n\t3772: \"NAD27 / Alberta 3TM ref merid 114 W\",\n\t3773: \"NAD27 / Alberta 3TM ref merid 117 W\",\n\t3774: \"NAD27 / Alberta 3TM ref merid 120 W\",\n\t3775: \"NAD83 / Alberta 3TM ref merid 111 W\",\n\t3776: \"NAD83 / Alberta 3TM ref merid 114 W\",\n\t3777: \"NAD83 / Alberta 3TM ref merid 117 W\",\n\t3778: \"NAD83 / Alberta 3TM ref merid 120 W\",\n\t3779: \"NAD83(CSRS) / Alberta 3TM ref merid 111 W\",\n\t3780: \"NAD83(CSRS) / Alberta 3TM ref merid 114 W\",\n\t3781: \"NAD83(CSRS) / Alberta 3TM ref merid 117 W\",\n\t3782: \"NAD83(CSRS) / Alberta 3TM ref merid 120 W\",\n\t3783: \"Pitcairn 2006 / Pitcairn TM 2006\",\n\t3784: \"Pitcairn 1967 / UTM zone 9S\",\n\t3785: \"Popular Visualisation CRS / Mercator\",\n\t3786: \"World Equidistant Cylindrical (Sphere)\",\n\t3787: \"MGI / Slovene National Grid\",\n\t3788: \"NZGD2000 / Auckland Islands TM 2000\",\n\t3789: \"NZGD2000 / Campbell Island TM 2000\",\n\t3790: \"NZGD2000 / Antipodes Islands TM 2000\",\n\t3791: \"NZGD2000 / Raoul Island TM 2000\",\n\t3793: \"NZGD2000 / Chatham Islands TM 2000\",\n\t3794: \"Slovenia 1996 / Slovene National Grid\",\n\t3795: \"NAD27 / Cuba Norte\",\n\t3796: \"NAD27 / Cuba Sur\",\n\t3797: \"NAD27 / MTQ Lambert\",\n\t3798: \"NAD83 / MTQ Lambert\",\n\t3799: \"NAD83(CSRS) / MTQ Lambert\",\n\t3800: \"NAD27 / Alberta 3TM ref merid 120 W\",\n\t3801: \"NAD83 / Alberta 3TM ref merid 120 W\",\n\t3802: \"NAD83(CSRS) / Alberta 3TM ref merid 120 W\",\n\t3812: \"ETRS89 / Belgian Lambert 2008\",\n\t3814: \"NAD83 / Mississippi TM\",\n\t3815: \"NAD83(HARN) / Mississippi TM\",\n\t3816: \"NAD83(NSRS2007) / Mississippi TM\",\n\t3825: \"TWD97 / TM2 zone 119\",\n\t3826: \"TWD97 / TM2 zone 121\",\n\t3827: \"TWD67 / TM2 zone 119\",\n\t3828: \"TWD67 / TM2 zone 121\",\n\t3829: \"Hu Tzu Shan 1950 / UTM zone 51N\",\n\t3832: \"WGS 84 / PDC Mercator\",\n\t3833: \"Pulkovo 1942(58) / Gauss-Kruger zone 2\",\n\t3834: \"Pulkovo 1942(83) / Gauss-Kruger zone 2\",\n\t3835: \"Pulkovo 1942(83) / Gauss-Kruger zone 3\",\n\t3836: \"Pulkovo 1942(83) / Gauss-Kruger zone 4\",\n\t3837: \"Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 3\",\n\t3838: \"Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 4\",\n\t3839: \"Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 9\",\n\t3840: \"Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 10\",\n\t3841: \"Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 6\",\n\t3842: \"Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 7\",\n\t3843: \"Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 8\",\n\t3844: \"Pulkovo 1942(58) / Stereo70\",\n\t3845: \"SWEREF99 / RT90 7.5 gon V emulation\",\n\t3846: \"SWEREF99 / RT90 5 gon V emulation\",\n\t3847: \"SWEREF99 / RT90 2.5 gon V emulation\",\n\t3848: \"SWEREF99 / RT90 0 gon emulation\",\n\t3849: \"SWEREF99 / RT90 2.5 gon O emulation\",\n\t3850: \"SWEREF99 / RT90 5 gon O emulation\",\n\t3851: \"NZGD2000 / NZCS2000\",\n\t3852: \"RSRGD2000 / DGLC2000\",\n\t3854: \"County ST74\",\n\t3857: \"WGS 84 / Pseudo-Mercator\",\n\t3873: \"ETRS89 / GK19FIN\",\n\t3874: \"ETRS89 / GK20FIN\",\n\t3875: \"ETRS89 / GK21FIN\",\n\t3876: \"ETRS89 / GK22FIN\",\n\t3877: \"ETRS89 / GK23FIN\",\n\t3878: \"ETRS89 / GK24FIN\",\n\t3879: \"ETRS89 / GK25FIN\",\n\t3880: \"ETRS89 / GK26FIN\",\n\t3881: \"ETRS89 / GK27FIN\",\n\t3882: \"ETRS89 / GK28FIN\",\n\t3883: \"ETRS89 / GK29FIN\",\n\t3884: \"ETRS89 / GK30FIN\",\n\t3885: \"ETRS89 / GK31FIN\",\n\t3890: \"IGRS / UTM zone 37N\",\n\t3891: \"IGRS / UTM zone 38N\",\n\t3892: \"IGRS / UTM zone 39N\",\n\t3893: \"ED50 / Iraq National Grid\",\n\t3907: \"MGI 1901 / Balkans zone 5\",\n\t3908: \"MGI 1901 / Balkans zone 6\",\n\t3909: \"MGI 1901 / Balkans zone 7\",\n\t3910: \"MGI 1901 / Balkans zone 8\",\n\t3911: \"MGI 1901 / Slovenia Grid\",\n\t3912: \"MGI 1901 / Slovene National Grid\",\n\t3920: \"Puerto Rico / UTM zone 20N\",\n\t3942: \"RGF93 / CC42\",\n\t3943: \"RGF93 / CC43\",\n\t3944: \"RGF93 / CC44\",\n\t3945: \"RGF93 / CC45\",\n\t3946: \"RGF93 / CC46\",\n\t3947: \"RGF93 / CC47\",\n\t3948: \"RGF93 / CC48\",\n\t3949: \"RGF93 / CC49\",\n\t3950: \"RGF93 / CC50\",\n\t3968: \"NAD83 / Virginia Lambert\",\n\t3969: \"NAD83(HARN) / Virginia Lambert\",\n\t3970: \"NAD83(NSRS2007) / Virginia Lambert\",\n\t3973: \"WGS 84 / NSIDC EASE-Grid North\",\n\t3974: \"WGS 84 / NSIDC EASE-Grid South\",\n\t3975: \"WGS 84 / NSIDC EASE-Grid Global\",\n\t3976: \"WGS 84 / NSIDC Sea Ice Polar Stereographic South\",\n\t3978: \"NAD83 / Canada Atlas Lambert\",\n\t3979: \"NAD83(CSRS) / Canada Atlas Lambert\",\n\t3985: \"Katanga 1955 / Katanga Lambert\",\n\t3986: \"Katanga 1955 / Katanga Gauss zone A\",\n\t3987: \"Katanga 1955 / Katanga Gauss zone B\",\n\t3988: \"Katanga 1955 / Katanga Gauss zone C\",\n\t3989: \"Katanga 1955 / Katanga Gauss zone D\",\n\t3991: \"Puerto Rico State Plane CS of 1927\",\n\t3992: \"Puerto Rico / St. Croix\",\n\t3993: \"Guam 1963 / Guam SPCS\",\n\t3994: \"WGS 84 / Mercator 41\",\n\t3995: \"WGS 84 / Arctic Polar Stereographic\",\n\t3996: \"WGS 84 / IBCAO Polar Stereographic\",\n\t3997: \"WGS 84 / Dubai Local TM\",\n\t4026: \"MOLDREF99 / Moldova TM\",\n\t4037: \"WGS 84 / TMzn35N\",\n\t4038: \"WGS 84 / TMzn36N\",\n\t4048: \"RGRDC 2005 / Congo TM zone 12\",\n\t4049: \"RGRDC 2005 / Congo TM zone 14\",\n\t4050: \"RGRDC 2005 / Congo TM zone 16\",\n\t4051: \"RGRDC 2005 / Congo TM zone 18\",\n\t4056: \"RGRDC 2005 / Congo TM zone 20\",\n\t4057: \"RGRDC 2005 / Congo TM zone 22\",\n\t4058: \"RGRDC 2005 / Congo TM zone 24\",\n\t4059: \"RGRDC 2005 / Congo TM zone 26\",\n\t4060: \"RGRDC 2005 / Congo TM zone 28\",\n\t4061: \"RGRDC 2005 / UTM zone 33S\",\n\t4062: \"RGRDC 2005 / UTM zone 34S\",\n\t4063: \"RGRDC 2005 / UTM zone 35S\",\n\t4071: \"Chua / UTM zone 23S\",\n\t4082: \"REGCAN95 / UTM zone 27N\",\n\t4083: \"REGCAN95 / UTM zone 28N\",\n\t4087: \"WGS 84 / World Equidistant Cylindrical\",\n\t4088: \"World Equidistant Cylindrical (Sphere)\",\n\t4093: \"ETRS89 / DKTM1\",\n\t4094: \"ETRS89 / DKTM2\",\n\t4095: \"ETRS89 / DKTM3\",\n\t4096: \"ETRS89 / DKTM4\",\n\t4217: \"NAD83 / BLM 59N (ftUS)\",\n\t4390: \"Kertau 1968 / Johor Grid\",\n\t4391: \"Kertau 1968 / Sembilan and Melaka Grid\",\n\t4392: \"Kertau 1968 / Pahang Grid\",\n\t4393: \"Kertau 1968 / Selangor Grid\",\n\t4394: \"Kertau 1968 / Terengganu Grid\",\n\t4395: \"Kertau 1968 / Pinang Grid\",\n\t4396: \"Kertau 1968 / Kedah and Perlis Grid\",\n\t4397: \"Kertau 1968 / Perak Revised Grid\",\n\t4398: \"Kertau 1968 / Kelantan Grid\",\n\t4399: \"NAD27 / BLM 59N (ftUS)\",\n\t4400: \"NAD27 / BLM 60N (ftUS)\",\n\t4401: \"NAD27 / BLM 1N (ftUS)\",\n\t4402: \"NAD27 / BLM 2N (ftUS)\",\n\t4403: \"NAD27 / BLM 3N (ftUS)\",\n\t4404: \"NAD27 / BLM 4N (ftUS)\",\n\t4405: \"NAD27 / BLM 5N (ftUS)\",\n\t4406: \"NAD27 / BLM 6N (ftUS)\",\n\t4407: \"NAD27 / BLM 7N (ftUS)\",\n\t4408: \"NAD27 / BLM 8N (ftUS)\",\n\t4409: \"NAD27 / BLM 9N (ftUS)\",\n\t4410: \"NAD27 / BLM 10N (ftUS)\",\n\t4411: \"NAD27 / BLM 11N (ftUS)\",\n\t4412: \"NAD27 / BLM 12N (ftUS)\",\n\t4413: \"NAD27 / BLM 13N (ftUS)\",\n\t4414: \"NAD83(HARN) / Guam Map Grid\",\n\t4415: \"Katanga 1955 / Katanga Lambert\",\n\t4417: \"Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 7\",\n\t4418: \"NAD27 / BLM 18N (ftUS)\",\n\t4419: \"NAD27 / BLM 19N (ftUS)\",\n\t4420: \"NAD83 / BLM 60N (ftUS)\",\n\t4421: \"NAD83 / BLM 1N (ftUS)\",\n\t4422: \"NAD83 / BLM 2N (ftUS)\",\n\t4423: \"NAD83 / BLM 3N (ftUS)\",\n\t4424: \"NAD83 / BLM 4N (ftUS)\",\n\t4425: \"NAD83 / BLM 5N (ftUS)\",\n\t4426: \"NAD83 / BLM 6N (ftUS)\",\n\t4427: \"NAD83 / BLM 7N (ftUS)\",\n\t4428: \"NAD83 / BLM 8N (ftUS)\",\n\t4429: \"NAD83 / BLM 9N (ftUS)\",\n\t4430: \"NAD83 / BLM 10N (ftUS)\",\n\t4431: \"NAD83 / BLM 11N (ftUS)\",\n\t4432: \"NAD83 / BLM 12N (ftUS)\",\n\t4433: \"NAD83 / BLM 13N (ftUS)\",\n\t4434: \"Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 8\",\n\t4437: \"NAD83(NSRS2007) / Puerto Rico and Virgin Is.\",\n\t4438: \"NAD83 / BLM 18N (ftUS)\",\n\t4439: \"NAD83 / BLM 19N (ftUS)\",\n\t4455: \"NAD27 / Pennsylvania South\",\n\t4456: \"NAD27 / New York Long Island\",\n\t4457: \"NAD83 / South Dakota North (ftUS)\",\n\t4462: \"WGS 84 / Australian Centre for Remote Sensing Lambert\",\n\t4467: \"RGSPM06 / UTM zone 21N\",\n\t4471: \"RGM04 / UTM zone 38S\",\n\t4474: \"Cadastre 1997 / UTM zone 38S\",\n\t4484: \"Mexico ITRF92 / UTM zone 11N\",\n\t4485: \"Mexico ITRF92 / UTM zone 12N\",\n\t4486: \"Mexico ITRF92 / UTM zone 13N\",\n\t4487: \"Mexico ITRF92 / UTM zone 14N\",\n\t4488: \"Mexico ITRF92 / UTM zone 15N\",\n\t4489: \"Mexico ITRF92 / UTM zone 16N\",\n\t4491: \"CGCS2000 / Gauss-Kruger zone 13\",\n\t4492: \"CGCS2000 / Gauss-Kruger zone 14\",\n\t4493: \"CGCS2000 / Gauss-Kruger zone 15\",\n\t4494: \"CGCS2000 / Gauss-Kruger zone 16\",\n\t4495: \"CGCS2000 / Gauss-Kruger zone 17\",\n\t4496: \"CGCS2000 / Gauss-Kruger zone 18\",\n\t4497: \"CGCS2000 / Gauss-Kruger zone 19\",\n\t4498: \"CGCS2000 / Gauss-Kruger zone 20\",\n\t4499: \"CGCS2000 / Gauss-Kruger zone 21\",\n\t4500: \"CGCS2000 / Gauss-Kruger zone 22\",\n\t4501: \"CGCS2000 / Gauss-Kruger zone 23\",\n\t4502: \"CGCS2000 / Gauss-Kruger CM 75E\",\n\t4503: \"CGCS2000 / Gauss-Kruger CM 81E\",\n\t4504: \"CGCS2000 / Gauss-Kruger CM 87E\",\n\t4505: \"CGCS2000 / Gauss-Kruger CM 93E\",\n\t4506: \"CGCS2000 / Gauss-Kruger CM 99E\",\n\t4507: \"CGCS2000 / Gauss-Kruger CM 105E\",\n\t4508: \"CGCS2000 / Gauss-Kruger CM 111E\",\n\t4509: \"CGCS2000 / Gauss-Kruger CM 117E\",\n\t4510: \"CGCS2000 / Gauss-Kruger CM 123E\",\n\t4511: \"CGCS2000 / Gauss-Kruger CM 129E\",\n\t4512: \"CGCS2000 / Gauss-Kruger CM 135E\",\n\t4513: \"CGCS2000 / 3-degree Gauss-Kruger zone 25\",\n\t4514: \"CGCS2000 / 3-degree Gauss-Kruger zone 26\",\n\t4515: \"CGCS2000 / 3-degree Gauss-Kruger zone 27\",\n\t4516: \"CGCS2000 / 3-degree Gauss-Kruger zone 28\",\n\t4517: \"CGCS2000 / 3-degree Gauss-Kruger zone 29\",\n\t4518: \"CGCS2000 / 3-degree Gauss-Kruger zone 30\",\n\t4519: \"CGCS2000 / 3-degree Gauss-Kruger zone 31\",\n\t4520: \"CGCS2000 / 3-degree Gauss-Kruger zone 32\",\n\t4521: \"CGCS2000 / 3-degree Gauss-Kruger zone 33\",\n\t4522: \"CGCS2000 / 3-degree Gauss-Kruger zone 34\",\n\t4523: \"CGCS2000 / 3-degree Gauss-Kruger zone 35\",\n\t4524: \"CGCS2000 / 3-degree Gauss-Kruger zone 36\",\n\t4525: \"CGCS2000 / 3-degree Gauss-Kruger zone 37\",\n\t4526: \"CGCS2000 / 3-degree Gauss-Kruger zone 38\",\n\t4527: \"CGCS2000 / 3-degree Gauss-Kruger zone 39\",\n\t4528: \"CGCS2000 / 3-degree Gauss-Kruger zone 40\",\n\t4529: \"CGCS2000 / 3-degree Gauss-Kruger zone 41\",\n\t4530: \"CGCS2000 / 3-degree Gauss-Kruger zone 42\",\n\t4531: \"CGCS2000 / 3-degree Gauss-Kruger zone 43\",\n\t4532: \"CGCS2000 / 3-degree Gauss-Kruger zone 44\",\n\t4533: \"CGCS2000 / 3-degree Gauss-Kruger zone 45\",\n\t4534: \"CGCS2000 / 3-degree Gauss-Kruger CM 75E\",\n\t4535: \"CGCS2000 / 3-degree Gauss-Kruger CM 78E\",\n\t4536: \"CGCS2000 / 3-degree Gauss-Kruger CM 81E\",\n\t4537: \"CGCS2000 / 3-degree Gauss-Kruger CM 84E\",\n\t4538: \"CGCS2000 / 3-degree Gauss-Kruger CM 87E\",\n\t4539: \"CGCS2000 / 3-degree Gauss-Kruger CM 90E\",\n\t4540: \"CGCS2000 / 3-degree Gauss-Kruger CM 93E\",\n\t4541: \"CGCS2000 / 3-degree Gauss-Kruger CM 96E\",\n\t4542: \"CGCS2000 / 3-degree Gauss-Kruger CM 99E\",\n\t4543: \"CGCS2000 / 3-degree Gauss-Kruger CM 102E\",\n\t4544: \"CGCS2000 / 3-degree Gauss-Kruger CM 105E\",\n\t4545: \"CGCS2000 / 3-degree Gauss-Kruger CM 108E\",\n\t4546: \"CGCS2000 / 3-degree Gauss-Kruger CM 111E\",\n\t4547: \"CGCS2000 / 3-degree Gauss-Kruger CM 114E\",\n\t4548: \"CGCS2000 / 3-degree Gauss-Kruger CM 117E\",\n\t4549: \"CGCS2000 / 3-degree Gauss-Kruger CM 120E\",\n\t4550: \"CGCS2000 / 3-degree Gauss-Kruger CM 123E\",\n\t4551: \"CGCS2000 / 3-degree Gauss-Kruger CM 126E\",\n\t4552: \"CGCS2000 / 3-degree Gauss-Kruger CM 129E\",\n\t4553: \"CGCS2000 / 3-degree Gauss-Kruger CM 132E\",\n\t4554: \"CGCS2000 / 3-degree Gauss-Kruger CM 135E\",\n\t4559: \"RRAF 1991 / UTM zone 20N\",\n\t4568: \"New Beijing / Gauss-Kruger zone 13\",\n\t4569: \"New Beijing / Gauss-Kruger zone 14\",\n\t4570: \"New Beijing / Gauss-Kruger zone 15\",\n\t4571: \"New Beijing / Gauss-Kruger zone 16\",\n\t4572: \"New Beijing / Gauss-Kruger zone 17\",\n\t4573: \"New Beijing / Gauss-Kruger zone 18\",\n\t4574: \"New Beijing / Gauss-Kruger zone 19\",\n\t4575: \"New Beijing / Gauss-Kruger zone 20\",\n\t4576: \"New Beijing / Gauss-Kruger zone 21\",\n\t4577: \"New Beijing / Gauss-Kruger zone 22\",\n\t4578: \"New Beijing / Gauss-Kruger zone 23\",\n\t4579: \"New Beijing / Gauss-Kruger CM 75E\",\n\t4580: \"New Beijing / Gauss-Kruger CM 81E\",\n\t4581: \"New Beijing / Gauss-Kruger CM 87E\",\n\t4582: \"New Beijing / Gauss-Kruger CM 93E\",\n\t4583: \"New Beijing / Gauss-Kruger CM 99E\",\n\t4584: \"New Beijing / Gauss-Kruger CM 105E\",\n\t4585: \"New Beijing / Gauss-Kruger CM 111E\",\n\t4586: \"New Beijing / Gauss-Kruger CM 117E\",\n\t4587: \"New Beijing / Gauss-Kruger CM 123E\",\n\t4588: \"New Beijing / Gauss-Kruger CM 129E\",\n\t4589: \"New Beijing / Gauss-Kruger CM 135E\",\n\t4647: \"ETRS89 / UTM zone 32N (zE-N)\",\n\t4652: \"New Beijing / 3-degree Gauss-Kruger zone 25\",\n\t4653: \"New Beijing / 3-degree Gauss-Kruger zone 26\",\n\t4654: \"New Beijing / 3-degree Gauss-Kruger zone 27\",\n\t4655: \"New Beijing / 3-degree Gauss-Kruger zone 28\",\n\t4656: \"New Beijing / 3-degree Gauss-Kruger zone 29\",\n\t4766: \"New Beijing / 3-degree Gauss-Kruger zone 30\",\n\t4767: \"New Beijing / 3-degree Gauss-Kruger zone 31\",\n\t4768: \"New Beijing / 3-degree Gauss-Kruger zone 32\",\n\t4769: \"New Beijing / 3-degree Gauss-Kruger zone 33\",\n\t4770: \"New Beijing / 3-degree Gauss-Kruger zone 34\",\n\t4771: \"New Beijing / 3-degree Gauss-Kruger zone 35\",\n\t4772: \"New Beijing / 3-degree Gauss-Kruger zone 36\",\n\t4773: \"New Beijing / 3-degree Gauss-Kruger zone 37\",\n\t4774: \"New Beijing / 3-degree Gauss-Kruger zone 38\",\n\t4775: \"New Beijing / 3-degree Gauss-Kruger zone 39\",\n\t4776: \"New Beijing / 3-degree Gauss-Kruger zone 40\",\n\t4777: \"New Beijing / 3-degree Gauss-Kruger zone 41\",\n\t4778: \"New Beijing / 3-degree Gauss-Kruger zone 42\",\n\t4779: \"New Beijing / 3-degree Gauss-Kruger zone 43\",\n\t4780: \"New Beijing / 3-degree Gauss-Kruger zone 44\",\n\t4781: \"New Beijing / 3-degree Gauss-Kruger zone 45\",\n\t4782: \"New Beijing / 3-degree Gauss-Kruger CM 75E\",\n\t4783: \"New Beijing / 3-degree Gauss-Kruger CM 78E\",\n\t4784: \"New Beijing / 3-degree Gauss-Kruger CM 81E\",\n\t4785: \"New Beijing / 3-degree Gauss-Kruger CM 84E\",\n\t4786: \"New Beijing / 3-degree Gauss-Kruger CM 87E\",\n\t4787: \"New Beijing / 3-degree Gauss-Kruger CM 90E\",\n\t4788: \"New Beijing / 3-degree Gauss-Kruger CM 93E\",\n\t4789: \"New Beijing / 3-degree Gauss-Kruger CM 96E\",\n\t4790: \"New Beijing / 3-degree Gauss-Kruger CM 99E\",\n\t4791: \"New Beijing / 3-degree Gauss-Kruger CM 102E\",\n\t4792: \"New Beijing / 3-degree Gauss-Kruger CM 105E\",\n\t4793: \"New Beijing / 3-degree Gauss-Kruger CM 108E\",\n\t4794: \"New Beijing / 3-degree Gauss-Kruger CM 111E\",\n\t4795: \"New Beijing / 3-degree Gauss-Kruger CM 114E\",\n\t4796: \"New Beijing / 3-degree Gauss-Kruger CM 117E\",\n\t4797: \"New Beijing / 3-degree Gauss-Kruger CM 120E\",\n\t4798: \"New Beijing / 3-degree Gauss-Kruger CM 123E\",\n\t4799: \"New Beijing / 3-degree Gauss-Kruger CM 126E\",\n\t4800: \"New Beijing / 3-degree Gauss-Kruger CM 129E\",\n\t4812: \"New Beijing / 3-degree Gauss-Kruger CM 132E\",\n\t4822: \"New Beijing / 3-degree Gauss-Kruger CM 135E\",\n\t4826: \"WGS 84 / Cape Verde National\",\n\t4839: \"ETRS89 / LCC Germany (N-E)\",\n\t4855: \"ETRS89 / NTM zone 5\",\n\t4856: \"ETRS89 / NTM zone 6\",\n\t4857: \"ETRS89 / NTM zone 7\",\n\t4858: \"ETRS89 / NTM zone 8\",\n\t4859: \"ETRS89 / NTM zone 9\",\n\t4860: \"ETRS89 / NTM zone 10\",\n\t4861: \"ETRS89 / NTM zone 11\",\n\t4862: \"ETRS89 / NTM zone 12\",\n\t4863: \"ETRS89 / NTM zone 13\",\n\t4864: \"ETRS89 / NTM zone 14\",\n\t4865: \"ETRS89 / NTM zone 15\",\n\t4866: \"ETRS89 / NTM zone 16\",\n\t4867: \"ETRS89 / NTM zone 17\",\n\t4868: \"ETRS89 / NTM zone 18\",\n\t4869: \"ETRS89 / NTM zone 19\",\n\t4870: \"ETRS89 / NTM zone 20\",\n\t4871: \"ETRS89 / NTM zone 21\",\n\t4872: \"ETRS89 / NTM zone 22\",\n\t4873: \"ETRS89 / NTM zone 23\",\n\t4874: \"ETRS89 / NTM zone 24\",\n\t4875: \"ETRS89 / NTM zone 25\",\n\t4876: \"ETRS89 / NTM zone 26\",\n\t4877: \"ETRS89 / NTM zone 27\",\n\t4878: \"ETRS89 / NTM zone 28\",\n\t4879: \"ETRS89 / NTM zone 29\",\n\t4880: \"ETRS89 / NTM zone 30\",\n\t5014: \"PTRA08 / UTM zone 25N\",\n\t5015: \"PTRA08 / UTM zone 26N\",\n\t5016: \"PTRA08 / UTM zone 28N\",\n\t5017: \"Lisbon 1890 / Portugal Bonne New\",\n\t5018: \"Lisbon / Portuguese Grid New\",\n\t5041: \"WGS 84 / UPS North (E,N)\",\n\t5042: \"WGS 84 / UPS South (E,N)\",\n\t5048: \"ETRS89 / TM35FIN(N,E)\",\n\t5069: \"NAD27 / Conus Albers\",\n\t5070: \"NAD83 / Conus Albers\",\n\t5071: \"NAD83(HARN) / Conus Albers\",\n\t5072: \"NAD83(NSRS2007) / Conus Albers\",\n\t5105: \"ETRS89 / NTM zone 5\",\n\t5106: \"ETRS89 / NTM zone 6\",\n\t5107: \"ETRS89 / NTM zone 7\",\n\t5108: \"ETRS89 / NTM zone 8\",\n\t5109: \"ETRS89 / NTM zone 9\",\n\t5110: \"ETRS89 / NTM zone 10\",\n\t5111: \"ETRS89 / NTM zone 11\",\n\t5112: \"ETRS89 / NTM zone 12\",\n\t5113: \"ETRS89 / NTM zone 13\",\n\t5114: \"ETRS89 / NTM zone 14\",\n\t5115: \"ETRS89 / NTM zone 15\",\n\t5116: \"ETRS89 / NTM zone 16\",\n\t5117: \"ETRS89 / NTM zone 17\",\n\t5118: \"ETRS89 / NTM zone 18\",\n\t5119: \"ETRS89 / NTM zone 19\",\n\t5120: \"ETRS89 / NTM zone 20\",\n\t5121: \"ETRS89 / NTM zone 21\",\n\t5122: \"ETRS89 / NTM zone 22\",\n\t5123: \"ETRS89 / NTM zone 23\",\n\t5124: \"ETRS89 / NTM zone 24\",\n\t5125: \"ETRS89 / NTM zone 25\",\n\t5126: \"ETRS89 / NTM zone 26\",\n\t5127: \"ETRS89 / NTM zone 27\",\n\t5128: \"ETRS89 / NTM zone 28\",\n\t5129: \"ETRS89 / NTM zone 29\",\n\t5130: \"ETRS89 / NTM zone 30\",\n\t5167: \"Korean 1985 / East Sea Belt\",\n\t5168: \"Korean 1985 / Central Belt Jeju\",\n\t5169: \"Tokyo 1892 / Korea West Belt\",\n\t5170: \"Tokyo 1892 / Korea Central Belt\",\n\t5171: \"Tokyo 1892 / Korea East Belt\",\n\t5172: \"Tokyo 1892 / Korea East Sea Belt\",\n\t5173: \"Korean 1985 / Modified West Belt\",\n\t5174: \"Korean 1985 / Modified Central Belt\",\n\t5175: \"Korean 1985 / Modified Central Belt Jeju\",\n\t5176: \"Korean 1985 / Modified East Belt\",\n\t5177: \"Korean 1985 / Modified East Sea Belt\",\n\t5178: \"Korean 1985 / Unified CS\",\n\t5179: \"Korea 2000 / Unified CS\",\n\t5180: \"Korea 2000 / West Belt\",\n\t5181: \"Korea 2000 / Central Belt\",\n\t5182: \"Korea 2000 / Central Belt Jeju\",\n\t5183: \"Korea 2000 / East Belt\",\n\t5184: \"Korea 2000 / East Sea Belt\",\n\t5185: \"Korea 2000 / West Belt 2010\",\n\t5186: \"Korea 2000 / Central Belt 2010\",\n\t5187: \"Korea 2000 / East Belt 2010\",\n\t5188: \"Korea 2000 / East Sea Belt 2010\",\n\t5221: \"S-JTSK (Ferro) / Krovak East North\",\n\t5223: \"WGS 84 / Gabon TM\",\n\t5224: \"S-JTSK/05 (Ferro) / Modified Krovak\",\n\t5225: \"S-JTSK/05 (Ferro) / Modified Krovak East North\",\n\t5234: \"Kandawala / Sri Lanka Grid\",\n\t5235: \"SLD99 / Sri Lanka Grid 1999\",\n\t5243: \"ETRS89 / LCC Germany (E-N)\",\n\t5247: \"GDBD2009 / Brunei BRSO\",\n\t5253: \"TUREF / TM27\",\n\t5254: \"TUREF / TM30\",\n\t5255: \"TUREF / TM33\",\n\t5256: \"TUREF / TM36\",\n\t5257: \"TUREF / TM39\",\n\t5258: \"TUREF / TM42\",\n\t5259: \"TUREF / TM45\",\n\t5266: \"DRUKREF 03 / Bhutan National Grid\",\n\t5269: \"TUREF / 3-degree Gauss-Kruger zone 9\",\n\t5270: \"TUREF / 3-degree Gauss-Kruger zone 10\",\n\t5271: \"TUREF / 3-degree Gauss-Kruger zone 11\",\n\t5272: \"TUREF / 3-degree Gauss-Kruger zone 12\",\n\t5273: \"TUREF / 3-degree Gauss-Kruger zone 13\",\n\t5274: \"TUREF / 3-degree Gauss-Kruger zone 14\",\n\t5275: \"TUREF / 3-degree Gauss-Kruger zone 15\",\n\t5292: \"DRUKREF 03 / Bumthang TM\",\n\t5293: \"DRUKREF 03 / Chhukha TM\",\n\t5294: \"DRUKREF 03 / Dagana TM\",\n\t5295: \"DRUKREF 03 / Gasa TM\",\n\t5296: \"DRUKREF 03 / Ha TM\",\n\t5297: \"DRUKREF 03 / Lhuentse TM\",\n\t5298: \"DRUKREF 03 / Mongar TM\",\n\t5299: \"DRUKREF 03 / Paro TM\",\n\t5300: \"DRUKREF 03 / Pemagatshel TM\",\n\t5301: \"DRUKREF 03 / Punakha TM\",\n\t5302: \"DRUKREF 03 / Samdrup Jongkhar TM\",\n\t5303: \"DRUKREF 03 / Samtse TM\",\n\t5304: \"DRUKREF 03 / Sarpang TM\",\n\t5305: \"DRUKREF 03 / Thimphu TM\",\n\t5306: \"DRUKREF 03 / Trashigang TM\",\n\t5307: \"DRUKREF 03 / Trongsa TM\",\n\t5308: \"DRUKREF 03 / Tsirang TM\",\n\t5309: \"DRUKREF 03 / Wangdue Phodrang TM\",\n\t5310: \"DRUKREF 03 / Yangtse TM\",\n\t5311: \"DRUKREF 03 / Zhemgang TM\",\n\t5316: \"ETRS89 / Faroe TM\",\n\t5320: \"NAD83 / Teranet Ontario Lambert\",\n\t5321: \"NAD83(CSRS) / Teranet Ontario Lambert\",\n\t5325: \"ISN2004 / Lambert 2004\",\n\t5329: \"Segara (Jakarta) / NEIEZ\",\n\t5330: \"Batavia (Jakarta) / NEIEZ\",\n\t5331: \"Makassar (Jakarta) / NEIEZ\",\n\t5337: \"Aratu / UTM zone 25S\",\n\t5343: \"POSGAR 2007 / Argentina 1\",\n\t5344: \"POSGAR 2007 / Argentina 2\",\n\t5345: \"POSGAR 2007 / Argentina 3\",\n\t5346: \"POSGAR 2007 / Argentina 4\",\n\t5347: \"POSGAR 2007 / Argentina 5\",\n\t5348: \"POSGAR 2007 / Argentina 6\",\n\t5349: \"POSGAR 2007 / Argentina 7\",\n\t5355: \"MARGEN / UTM zone 20S\",\n\t5356: \"MARGEN / UTM zone 19S\",\n\t5357: \"MARGEN / UTM zone 21S\",\n\t5361: \"SIRGAS-Chile / UTM zone 19S\",\n\t5362: \"SIRGAS-Chile / UTM zone 18S\",\n\t5367: \"CR05 / CRTM05\",\n\t5382: \"SIRGAS-ROU98 / UTM zone 21S\",\n\t5383: \"SIRGAS-ROU98 / UTM zone 22S\",\n\t5387: \"Peru96 / UTM zone 18S\",\n\t5388: \"Peru96 / UTM zone 17S\",\n\t5389: \"Peru96 / UTM zone 19S\",\n\t5396: \"SIRGAS 2000 / UTM zone 26S\",\n\t5456: \"Ocotepeque 1935 / Costa Rica Norte\",\n\t5457: \"Ocotepeque 1935 / Costa Rica Sur\",\n\t5458: \"Ocotepeque 1935 / Guatemala Norte\",\n\t5459: \"Ocotepeque 1935 / Guatemala Sur\",\n\t5460: \"Ocotepeque 1935 / El Salvador Lambert\",\n\t5461: \"Ocotepeque 1935 / Nicaragua Norte\",\n\t5462: \"Ocotepeque 1935 / Nicaragua Sur\",\n\t5463: \"SAD69 / UTM zone 17N\",\n\t5466: \"Sibun Gorge 1922 / Colony Grid\",\n\t5469: \"Panama-Colon 1911 / Panama Lambert\",\n\t5472: \"Panama-Colon 1911 / Panama Polyconic\",\n\t5479: \"RSRGD2000 / MSLC2000\",\n\t5480: \"RSRGD2000 / BCLC2000\",\n\t5481: \"RSRGD2000 / PCLC2000\",\n\t5482: \"RSRGD2000 / RSPS2000\",\n\t5490: \"RGAF09 / UTM zone 20N\",\n\t5513: \"S-JTSK / Krovak\",\n\t5514: \"S-JTSK / Krovak East North\",\n\t5515: \"S-JTSK/05 / Modified Krovak\",\n\t5516: \"S-JTSK/05 / Modified Krovak East North\",\n\t5518: \"CI1971 / Chatham Islands Map Grid\",\n\t5519: \"CI1979 / Chatham Islands Map Grid\",\n\t5520: \"DHDN / 3-degree Gauss-Kruger zone 1\",\n\t5523: \"WGS 84 / Gabon TM 2011\",\n\t5530: \"SAD69(96) / Brazil Polyconic\",\n\t5531: \"SAD69(96) / UTM zone 21S\",\n\t5532: \"SAD69(96) / UTM zone 22S\",\n\t5533: \"SAD69(96) / UTM zone 23S\",\n\t5534: \"SAD69(96) / UTM zone 24S\",\n\t5535: \"SAD69(96) / UTM zone 25S\",\n\t5536: \"Corrego Alegre 1961 / UTM zone 21S\",\n\t5537: \"Corrego Alegre 1961 / UTM zone 22S\",\n\t5538: \"Corrego Alegre 1961 / UTM zone 23S\",\n\t5539: \"Corrego Alegre 1961 / UTM zone 24S\",\n\t5550: \"PNG94 / PNGMG94 zone 54\",\n\t5551: \"PNG94 / PNGMG94 zone 55\",\n\t5552: \"PNG94 / PNGMG94 zone 56\",\n\t5559: \"Ocotepeque 1935 / Guatemala Norte\",\n\t5562: \"UCS-2000 / Gauss-Kruger zone 4\",\n\t5563: \"UCS-2000 / Gauss-Kruger zone 5\",\n\t5564: \"UCS-2000 / Gauss-Kruger zone 6\",\n\t5565: \"UCS-2000 / Gauss-Kruger zone 7\",\n\t5566: \"UCS-2000 / Gauss-Kruger CM 21E\",\n\t5567: \"UCS-2000 / Gauss-Kruger CM 27E\",\n\t5568: \"UCS-2000 / Gauss-Kruger CM 33E\",\n\t5569: \"UCS-2000 / Gauss-Kruger CM 39E\",\n\t5570: \"UCS-2000 / 3-degree Gauss-Kruger zone 7\",\n\t5571: \"UCS-2000 / 3-degree Gauss-Kruger zone 8\",\n\t5572: \"UCS-2000 / 3-degree Gauss-Kruger zone 9\",\n\t5573: \"UCS-2000 / 3-degree Gauss-Kruger zone 10\",\n\t5574: \"UCS-2000 / 3-degree Gauss-Kruger zone 11\",\n\t5575: \"UCS-2000 / 3-degree Gauss-Kruger zone 12\",\n\t5576: \"UCS-2000 / 3-degree Gauss-Kruger zone 13\",\n\t5577: \"UCS-2000 / 3-degree Gauss-Kruger CM 21E\",\n\t5578: \"UCS-2000 / 3-degree Gauss-Kruger CM 24E\",\n\t5579: \"UCS-2000 / 3-degree Gauss-Kruger CM 27E\",\n\t5580: \"UCS-2000 / 3-degree Gauss-Kruger CM 30E\",\n\t5581: \"UCS-2000 / 3-degree Gauss-Kruger CM 33E\",\n\t5582: \"UCS-2000 / 3-degree Gauss-Kruger CM 36E\",\n\t5583: \"UCS-2000 / 3-degree Gauss-Kruger CM 39E\",\n\t5588: \"NAD27 / New Brunswick Stereographic (NAD27)\",\n\t5589: \"Sibun Gorge 1922 / Colony Grid\",\n\t5596: \"FEH2010 / Fehmarnbelt TM\",\n\t5623: \"NAD27 / Michigan East\",\n\t5624: \"NAD27 / Michigan Old Central\",\n\t5625: \"NAD27 / Michigan West\",\n\t5627: \"ED50 / TM 6 NE\",\n\t5629: \"Moznet / UTM zone 38S\",\n\t5631: \"Pulkovo 1942(58) / Gauss-Kruger zone 2 (E-N)\",\n\t5632: \"PTRA08 / LCC Europe\",\n\t5633: \"PTRA08 / LAEA Europe\",\n\t5634: \"REGCAN95 / LCC Europe\",\n\t5635: \"REGCAN95 / LAEA Europe\",\n\t5636: \"TUREF / LAEA Europe\",\n\t5637: \"TUREF / LCC Europe\",\n\t5638: \"ISN2004 / LAEA Europe\",\n\t5639: \"ISN2004 / LCC Europe\",\n\t5641: \"SIRGAS 2000 / Brazil Mercator\",\n\t5643: \"ED50 / SPBA LCC\",\n\t5644: \"RGR92 / UTM zone 39S\",\n\t5646: \"NAD83 / Vermont (ftUS)\",\n\t5649: \"ETRS89 / UTM zone 31N (zE-N)\",\n\t5650: \"ETRS89 / UTM zone 33N (zE-N)\",\n\t5651: \"ETRS89 / UTM zone 31N (N-zE)\",\n\t5652: \"ETRS89 / UTM zone 32N (N-zE)\",\n\t5653: \"ETRS89 / UTM zone 33N (N-zE)\",\n\t5654: \"NAD83(HARN) / Vermont (ftUS)\",\n\t5655: \"NAD83(NSRS2007) / Vermont (ftUS)\",\n\t5659: \"Monte Mario / TM Emilia-Romagna\",\n\t5663: \"Pulkovo 1942(58) / Gauss-Kruger zone 3 (E-N)\",\n\t5664: \"Pulkovo 1942(83) / Gauss-Kruger zone 2 (E-N)\",\n\t5665: \"Pulkovo 1942(83) / Gauss-Kruger zone 3 (E-N)\",\n\t5666: \"PD/83 / 3-degree Gauss-Kruger zone 3 (E-N)\",\n\t5667: \"PD/83 / 3-degree Gauss-Kruger zone 4 (E-N)\",\n\t5668: \"RD/83 / 3-degree Gauss-Kruger zone 4 (E-N)\",\n\t5669: \"RD/83 / 3-degree Gauss-Kruger zone 5 (E-N)\",\n\t5670: \"Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 3 (E-N)\",\n\t5671: \"Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 4 (E-N)\",\n\t5672: \"Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 5 (E-N)\",\n\t5673: \"Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 3 (E-N)\",\n\t5674: \"Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 4 (E-N)\",\n\t5675: \"Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 5 (E-N)\",\n\t5676: \"DHDN / 3-degree Gauss-Kruger zone 2 (E-N)\",\n\t5677: \"DHDN / 3-degree Gauss-Kruger zone 3 (E-N)\",\n\t5678: \"DHDN / 3-degree Gauss-Kruger zone 4 (E-N)\",\n\t5679: \"DHDN / 3-degree Gauss-Kruger zone 5 (E-N)\",\n\t5680: \"DHDN / 3-degree Gauss-Kruger zone 1 (E-N)\",\n\t5682: \"DB_REF / 3-degree Gauss-Kruger zone 2 (E-N)\",\n\t5683: \"DB_REF / 3-degree Gauss-Kruger zone 3 (E-N)\",\n\t5684: \"DB_REF / 3-degree Gauss-Kruger zone 4 (E-N)\",\n\t5685: \"DB_REF / 3-degree Gauss-Kruger zone 5 (E-N)\",\n\t5700: \"NZGD2000 / UTM zone 1S\",\n\t5819: \"EPSG topocentric example A\",\n\t5820: \"EPSG topocentric example B\",\n\t5821: \"EPSG vertical perspective example\",\n\t5825: \"AGD66 / ACT Standard Grid\",\n\t5836: \"Yemen NGN96 / UTM zone 37N\",\n\t5837: \"Yemen NGN96 / UTM zone 40N\",\n\t5839: \"Peru96 / UTM zone 17S\",\n\t5842: \"WGS 84 / TM 12 SE\",\n\t5844: \"RGRDC 2005 / Congo TM zone 30\",\n\t5858: \"SAD69(96) / UTM zone 22S\",\n\t5875: \"SAD69(96) / UTM zone 18S\",\n\t5876: \"SAD69(96) / UTM zone 19S\",\n\t5877: \"SAD69(96) / UTM zone 20S\",\n\t5879: \"Cadastre 1997 / UTM zone 38S\",\n\t5880: \"SIRGAS 2000 / Brazil Polyconic\",\n\t5887: \"TGD2005 / Tonga Map Grid\",\n\t5890: \"JAXA Snow Depth Polar Stereographic North\",\n\t5921: \"WGS 84 / EPSG Arctic Regional zone A1\",\n\t5922: \"WGS 84 / EPSG Arctic Regional zone A2\",\n\t5923: \"WGS 84 / EPSG Arctic Regional zone A3\",\n\t5924: \"WGS 84 / EPSG Arctic Regional zone A4\",\n\t5925: \"WGS 84 / EPSG Arctic Regional zone A5\",\n\t5926: \"WGS 84 / EPSG Arctic Regional zone B1\",\n\t5927: \"WGS 84 / EPSG Arctic Regional zone B2\",\n\t5928: \"WGS 84 / EPSG Arctic Regional zone B3\",\n\t5929: \"WGS 84 / EPSG Arctic Regional zone B4\",\n\t5930: \"WGS 84 / EPSG Arctic Regional zone B5\",\n\t5931: \"WGS 84 / EPSG Arctic Regional zone C1\",\n\t5932: \"WGS 84 / EPSG Arctic Regional zone C2\",\n\t5933: \"WGS 84 / EPSG Arctic Regional zone C3\",\n\t5934: \"WGS 84 / EPSG Arctic Regional zone C4\",\n\t5935: \"WGS 84 / EPSG Arctic Regional zone C5\",\n\t5936: \"WGS 84 / EPSG Alaska Polar Stereographic\",\n\t5937: \"WGS 84 / EPSG Canada Polar Stereographic\",\n\t5938: \"WGS 84 / EPSG Greenland Polar Stereographic\",\n\t5939: \"WGS 84 / EPSG Norway Polar Stereographic\",\n\t5940: \"WGS 84 / EPSG Russia Polar Stereographic\",\n\t6050: \"GR96 / EPSG Arctic zone 1-25\",\n\t6051: \"GR96 / EPSG Arctic zone 2-18\",\n\t6052: \"GR96 / EPSG Arctic zone 2-20\",\n\t6053: \"GR96 / EPSG Arctic zone 3-29\",\n\t6054: \"GR96 / EPSG Arctic zone 3-31\",\n\t6055: \"GR96 / EPSG Arctic zone 3-33\",\n\t6056: \"GR96 / EPSG Arctic zone 4-20\",\n\t6057: \"GR96 / EPSG Arctic zone 4-22\",\n\t6058: \"GR96 / EPSG Arctic zone 4-24\",\n\t6059: \"GR96 / EPSG Arctic zone 5-41\",\n\t6060: \"GR96 / EPSG Arctic zone 5-43\",\n\t6061: \"GR96 / EPSG Arctic zone 5-45\",\n\t6062: \"GR96 / EPSG Arctic zone 6-26\",\n\t6063: \"GR96 / EPSG Arctic zone 6-28\",\n\t6064: \"GR96 / EPSG Arctic zone 6-30\",\n\t6065: \"GR96 / EPSG Arctic zone 7-11\",\n\t6066: \"GR96 / EPSG Arctic zone 7-13\",\n\t6067: \"GR96 / EPSG Arctic zone 8-20\",\n\t6068: \"GR96 / EPSG Arctic zone 8-22\",\n\t6069: \"ETRS89 / EPSG Arctic zone 2-22\",\n\t6070: \"ETRS89 / EPSG Arctic zone 3-11\",\n\t6071: \"ETRS89 / EPSG Arctic zone 4-26\",\n\t6072: \"ETRS89 / EPSG Arctic zone 4-28\",\n\t6073: \"ETRS89 / EPSG Arctic zone 5-11\",\n\t6074: \"ETRS89 / EPSG Arctic zone 5-13\",\n\t6075: \"WGS 84 / EPSG Arctic zone 2-24\",\n\t6076: \"WGS 84 / EPSG Arctic zone 2-26\",\n\t6077: \"WGS 84 / EPSG Arctic zone 3-13\",\n\t6078: \"WGS 84 / EPSG Arctic zone 3-15\",\n\t6079: \"WGS 84 / EPSG Arctic zone 3-17\",\n\t6080: \"WGS 84 / EPSG Arctic zone 3-19\",\n\t6081: \"WGS 84 / EPSG Arctic zone 4-30\",\n\t6082: \"WGS 84 / EPSG Arctic zone 4-32\",\n\t6083: \"WGS 84 / EPSG Arctic zone 4-34\",\n\t6084: \"WGS 84 / EPSG Arctic zone 4-36\",\n\t6085: \"WGS 84 / EPSG Arctic zone 4-38\",\n\t6086: \"WGS 84 / EPSG Arctic zone 4-40\",\n\t6087: \"WGS 84 / EPSG Arctic zone 5-15\",\n\t6088: \"WGS 84 / EPSG Arctic zone 5-17\",\n\t6089: \"WGS 84 / EPSG Arctic zone 5-19\",\n\t6090: \"WGS 84 / EPSG Arctic zone 5-21\",\n\t6091: \"WGS 84 / EPSG Arctic zone 5-23\",\n\t6092: \"WGS 84 / EPSG Arctic zone 5-25\",\n\t6093: \"WGS 84 / EPSG Arctic zone 5-27\",\n\t6094: \"NAD83(NSRS2007) / EPSG Arctic zone 5-29\",\n\t6095: \"NAD83(NSRS2007) / EPSG Arctic zone 5-31\",\n\t6096: \"NAD83(NSRS2007) / EPSG Arctic zone 6-14\",\n\t6097: \"NAD83(NSRS2007) / EPSG Arctic zone 6-16\",\n\t6098: \"NAD83(CSRS) / EPSG Arctic zone 1-23\",\n\t6099: \"NAD83(CSRS) / EPSG Arctic zone 2-14\",\n\t6100: \"NAD83(CSRS) / EPSG Arctic zone 2-16\",\n\t6101: \"NAD83(CSRS) / EPSG Arctic zone 3-25\",\n\t6102: \"NAD83(CSRS) / EPSG Arctic zone 3-27\",\n\t6103: \"NAD83(CSRS) / EPSG Arctic zone 3-29\",\n\t6104: \"NAD83(CSRS) / EPSG Arctic zone 4-14\",\n\t6105: \"NAD83(CSRS) / EPSG Arctic zone 4-16\",\n\t6106: \"NAD83(CSRS) / EPSG Arctic zone 4-18\",\n\t6107: \"NAD83(CSRS) / EPSG Arctic zone 5-33\",\n\t6108: \"NAD83(CSRS) / EPSG Arctic zone 5-35\",\n\t6109: \"NAD83(CSRS) / EPSG Arctic zone 5-37\",\n\t6110: \"NAD83(CSRS) / EPSG Arctic zone 5-39\",\n\t6111: \"NAD83(CSRS) / EPSG Arctic zone 6-18\",\n\t6112: \"NAD83(CSRS) / EPSG Arctic zone 6-20\",\n\t6113: \"NAD83(CSRS) / EPSG Arctic zone 6-22\",\n\t6114: \"NAD83(CSRS) / EPSG Arctic zone 6-24\",\n\t6115: \"WGS 84 / EPSG Arctic zone 1-27\",\n\t6116: \"WGS 84 / EPSG Arctic zone 1-29\",\n\t6117: \"WGS 84 / EPSG Arctic zone 1-31\",\n\t6118: \"WGS 84 / EPSG Arctic zone 1-21\",\n\t6119: \"WGS 84 / EPSG Arctic zone 2-28\",\n\t6120: \"WGS 84 / EPSG Arctic zone 2-10\",\n\t6121: \"WGS 84 / EPSG Arctic zone 2-12\",\n\t6122: \"WGS 84 / EPSG Arctic zone 3-21\",\n\t6123: \"WGS 84 / EPSG Arctic zone 3-23\",\n\t6124: \"WGS 84 / EPSG Arctic zone 4-12\",\n\t6125: \"ETRS89 / EPSG Arctic zone 5-47\",\n\t6128: \"Grand Cayman National Grid 1959\",\n\t6129: \"Sister Islands National Grid 1961\",\n\t6141: \"Cayman Islands National Grid 2011\",\n\t6200: \"NAD27 / Michigan North\",\n\t6201: \"NAD27 / Michigan Central\",\n\t6202: \"NAD27 / Michigan South\",\n\t6204: \"Macedonia State Coordinate System\",\n\t6210: \"SIRGAS 2000 / UTM zone 23N\",\n\t6211: \"SIRGAS 2000 / UTM zone 24N\",\n\t6244: \"MAGNA-SIRGAS / Arauca urban grid\",\n\t6245: \"MAGNA-SIRGAS / Armenia urban grid\",\n\t6246: \"MAGNA-SIRGAS / Barranquilla urban grid\",\n\t6247: \"MAGNA-SIRGAS / Bogota urban grid\",\n\t6248: \"MAGNA-SIRGAS / Bucaramanga urban grid\",\n\t6249: \"MAGNA-SIRGAS / Cali urban grid\",\n\t6250: \"MAGNA-SIRGAS / Cartagena urban grid\",\n\t6251: \"MAGNA-SIRGAS / Cucuta urban grid\",\n\t6252: \"MAGNA-SIRGAS / Florencia urban grid\",\n\t6253: \"MAGNA-SIRGAS / Ibague urban grid\",\n\t6254: \"MAGNA-SIRGAS / Inirida urban grid\",\n\t6255: \"MAGNA-SIRGAS / Leticia urban grid\",\n\t6256: \"MAGNA-SIRGAS / Manizales urban grid\",\n\t6257: \"MAGNA-SIRGAS / Medellin urban grid\",\n\t6258: \"MAGNA-SIRGAS / Mitu urban grid\",\n\t6259: \"MAGNA-SIRGAS / Mocoa urban grid\",\n\t6260: \"MAGNA-SIRGAS / Monteria urban grid\",\n\t6261: \"MAGNA-SIRGAS / Neiva urban grid\",\n\t6262: \"MAGNA-SIRGAS / Pasto urban grid\",\n\t6263: \"MAGNA-SIRGAS / Pereira urban grid\",\n\t6264: \"MAGNA-SIRGAS / Popayan urban grid\",\n\t6265: \"MAGNA-SIRGAS / Puerto Carreno urban grid\",\n\t6266: \"MAGNA-SIRGAS / Quibdo urban grid\",\n\t6267: \"MAGNA-SIRGAS / Riohacha urban grid\",\n\t6268: \"MAGNA-SIRGAS / San Andres urban grid\",\n\t6269: \"MAGNA-SIRGAS / San Jose del Guaviare urban grid\",\n\t6270: \"MAGNA-SIRGAS / Santa Marta urban grid\",\n\t6271: \"MAGNA-SIRGAS / Sucre urban grid\",\n\t6272: \"MAGNA-SIRGAS / Tunja urban grid\",\n\t6273: \"MAGNA-SIRGAS / Valledupar urban grid\",\n\t6274: \"MAGNA-SIRGAS / Villavicencio urban grid\",\n\t6275: \"MAGNA-SIRGAS / Yopal urban grid\",\n\t6307: \"NAD83(CORS96) / Puerto Rico and Virgin Is.\",\n\t6312: \"CGRS93 / Cyprus Local Transverse Mercator\",\n\t6316: \"Macedonia State Coordinate System zone 7\",\n\t6328: \"NAD83(2011) / UTM zone 59N\",\n\t6329: \"NAD83(2011) / UTM zone 60N\",\n\t6330: \"NAD83(2011) / UTM zone 1N\",\n\t6331: \"NAD83(2011) / UTM zone 2N\",\n\t6332: \"NAD83(2011) / UTM zone 3N\",\n\t6333: \"NAD83(2011) / UTM zone 4N\",\n\t6334: \"NAD83(2011) / UTM zone 5N\",\n\t6335: \"NAD83(2011) / UTM zone 6N\",\n\t6336: \"NAD83(2011) / UTM zone 7N\",\n\t6337: \"NAD83(2011) / UTM zone 8N\",\n\t6338: \"NAD83(2011) / UTM zone 9N\",\n\t6339: \"NAD83(2011) / UTM zone 10N\",\n\t6340: \"NAD83(2011) / UTM zone 11N\",\n\t6341: \"NAD83(2011) / UTM zone 12N\",\n\t6342: \"NAD83(2011) / UTM zone 13N\",\n\t6343: \"NAD83(2011) / UTM zone 14N\",\n\t6344: \"NAD83(2011) / UTM zone 15N\",\n\t6345: \"NAD83(2011) / UTM zone 16N\",\n\t6346: \"NAD83(2011) / UTM zone 17N\",\n\t6347: \"NAD83(2011) / UTM zone 18N\",\n\t6348: \"NAD83(2011) / UTM zone 19N\",\n\t6350: \"NAD83(2011) / Conus Albers\",\n\t6351: \"NAD83(2011) / EPSG Arctic zone 5-29\",\n\t6352: \"NAD83(2011) / EPSG Arctic zone 5-31\",\n\t6353: \"NAD83(2011) / EPSG Arctic zone 6-14\",\n\t6354: \"NAD83(2011) / EPSG Arctic zone 6-16\",\n\t6355: \"NAD83(2011) / Alabama East\",\n\t6356: \"NAD83(2011) / Alabama West\",\n\t6362: \"Mexico ITRF92 / LCC\",\n\t6366: \"Mexico ITRF2008 / UTM zone 11N\",\n\t6367: \"Mexico ITRF2008 / UTM zone 12N\",\n\t6368: \"Mexico ITRF2008 / UTM zone 13N\",\n\t6369: \"Mexico ITRF2008 / UTM zone 14N\",\n\t6370: \"Mexico ITRF2008 / UTM zone 15N\",\n\t6371: \"Mexico ITRF2008 / UTM zone 16N\",\n\t6372: \"Mexico ITRF2008 / LCC\",\n\t6381: \"UCS-2000 / Ukraine TM zone 7\",\n\t6382: \"UCS-2000 / Ukraine TM zone 8\",\n\t6383: \"UCS-2000 / Ukraine TM zone 9\",\n\t6384: \"UCS-2000 / Ukraine TM zone 10\",\n\t6385: \"UCS-2000 / Ukraine TM zone 11\",\n\t6386: \"UCS-2000 / Ukraine TM zone 12\",\n\t6387: \"UCS-2000 / Ukraine TM zone 13\",\n\t6391: \"Cayman Islands National Grid 2011\",\n\t6393: \"NAD83(2011) / Alaska Albers\",\n\t6394: \"NAD83(2011) / Alaska zone 1\",\n\t6395: \"NAD83(2011) / Alaska zone 2\",\n\t6396: \"NAD83(2011) / Alaska zone 3\",\n\t6397: \"NAD83(2011) / Alaska zone 4\",\n\t6398: \"NAD83(2011) / Alaska zone 5\",\n\t6399: \"NAD83(2011) / Alaska zone 6\",\n\t6400: \"NAD83(2011) / Alaska zone 7\",\n\t6401: \"NAD83(2011) / Alaska zone 8\",\n\t6402: \"NAD83(2011) / Alaska zone 9\",\n\t6403: \"NAD83(2011) / Alaska zone 10\",\n\t6404: \"NAD83(2011) / Arizona Central\",\n\t6405: \"NAD83(2011) / Arizona Central (ft)\",\n\t6406: \"NAD83(2011) / Arizona East\",\n\t6407: \"NAD83(2011) / Arizona East (ft)\",\n\t6408: \"NAD83(2011) / Arizona West\",\n\t6409: \"NAD83(2011) / Arizona West (ft)\",\n\t6410: \"NAD83(2011) / Arkansas North\",\n\t6411: \"NAD83(2011) / Arkansas North (ftUS)\",\n\t6412: \"NAD83(2011) / Arkansas South\",\n\t6413: \"NAD83(2011) / Arkansas South (ftUS)\",\n\t6414: \"NAD83(2011) / California Albers\",\n\t6415: \"NAD83(2011) / California zone 1\",\n\t6416: \"NAD83(2011) / California zone 1 (ftUS)\",\n\t6417: \"NAD83(2011) / California zone 2\",\n\t6418: \"NAD83(2011) / California zone 2 (ftUS)\",\n\t6419: \"NAD83(2011) / California zone 3\",\n\t6420: \"NAD83(2011) / California zone 3 (ftUS)\",\n\t6421: \"NAD83(2011) / California zone 4\",\n\t6422: \"NAD83(2011) / California zone 4 (ftUS)\",\n\t6423: \"NAD83(2011) / California zone 5\",\n\t6424: \"NAD83(2011) / California zone 5 (ftUS)\",\n\t6425: \"NAD83(2011) / California zone 6\",\n\t6426: \"NAD83(2011) / California zone 6 (ftUS)\",\n\t6427: \"NAD83(2011) / Colorado Central\",\n\t6428: \"NAD83(2011) / Colorado Central (ftUS)\",\n\t6429: \"NAD83(2011) / Colorado North\",\n\t6430: \"NAD83(2011) / Colorado North (ftUS)\",\n\t6431: \"NAD83(2011) / Colorado South\",\n\t6432: \"NAD83(2011) / Colorado South (ftUS)\",\n\t6433: \"NAD83(2011) / Connecticut\",\n\t6434: \"NAD83(2011) / Connecticut (ftUS)\",\n\t6435: \"NAD83(2011) / Delaware\",\n\t6436: \"NAD83(2011) / Delaware (ftUS)\",\n\t6437: \"NAD83(2011) / Florida East\",\n\t6438: \"NAD83(2011) / Florida East (ftUS)\",\n\t6439: \"NAD83(2011) / Florida GDL Albers\",\n\t6440: \"NAD83(2011) / Florida North\",\n\t6441: \"NAD83(2011) / Florida North (ftUS)\",\n\t6442: \"NAD83(2011) / Florida West\",\n\t6443: \"NAD83(2011) / Florida West (ftUS)\",\n\t6444: \"NAD83(2011) / Georgia East\",\n\t6445: \"NAD83(2011) / Georgia East (ftUS)\",\n\t6446: \"NAD83(2011) / Georgia West\",\n\t6447: \"NAD83(2011) / Georgia West (ftUS)\",\n\t6448: \"NAD83(2011) / Idaho Central\",\n\t6449: \"NAD83(2011) / Idaho Central (ftUS)\",\n\t6450: \"NAD83(2011) / Idaho East\",\n\t6451: \"NAD83(2011) / Idaho East (ftUS)\",\n\t6452: \"NAD83(2011) / Idaho West\",\n\t6453: \"NAD83(2011) / Idaho West (ftUS)\",\n\t6454: \"NAD83(2011) / Illinois East\",\n\t6455: \"NAD83(2011) / Illinois East (ftUS)\",\n\t6456: \"NAD83(2011) / Illinois West\",\n\t6457: \"NAD83(2011) / Illinois West (ftUS)\",\n\t6458: \"NAD83(2011) / Indiana East\",\n\t6459: \"NAD83(2011) / Indiana East (ftUS)\",\n\t6460: \"NAD83(2011) / Indiana West\",\n\t6461: \"NAD83(2011) / Indiana West (ftUS)\",\n\t6462: \"NAD83(2011) / Iowa North\",\n\t6463: \"NAD83(2011) / Iowa North (ftUS)\",\n\t6464: \"NAD83(2011) / Iowa South\",\n\t6465: \"NAD83(2011) / Iowa South (ftUS)\",\n\t6466: \"NAD83(2011) / Kansas North\",\n\t6467: \"NAD83(2011) / Kansas North (ftUS)\",\n\t6468: \"NAD83(2011) / Kansas South\",\n\t6469: \"NAD83(2011) / Kansas South (ftUS)\",\n\t6470: \"NAD83(2011) / Kentucky North\",\n\t6471: \"NAD83(2011) / Kentucky North (ftUS)\",\n\t6472: \"NAD83(2011) / Kentucky Single Zone\",\n\t6473: \"NAD83(2011) / Kentucky Single Zone (ftUS)\",\n\t6474: \"NAD83(2011) / Kentucky South\",\n\t6475: \"NAD83(2011) / Kentucky South (ftUS)\",\n\t6476: \"NAD83(2011) / Louisiana North\",\n\t6477: \"NAD83(2011) / Louisiana North (ftUS)\",\n\t6478: \"NAD83(2011) / Louisiana South\",\n\t6479: \"NAD83(2011) / Louisiana South (ftUS)\",\n\t6480: \"NAD83(2011) / Maine CS2000 Central\",\n\t6481: \"NAD83(2011) / Maine CS2000 East\",\n\t6482: \"NAD83(2011) / Maine CS2000 West\",\n\t6483: \"NAD83(2011) / Maine East\",\n\t6484: \"NAD83(2011) / Maine East (ftUS)\",\n\t6485: \"NAD83(2011) / Maine West\",\n\t6486: \"NAD83(2011) / Maine West (ftUS)\",\n\t6487: \"NAD83(2011) / Maryland\",\n\t6488: \"NAD83(2011) / Maryland (ftUS)\",\n\t6489: \"NAD83(2011) / Massachusetts Island\",\n\t6490: \"NAD83(2011) / Massachusetts Island (ftUS)\",\n\t6491: \"NAD83(2011) / Massachusetts Mainland\",\n\t6492: \"NAD83(2011) / Massachusetts Mainland (ftUS)\",\n\t6493: \"NAD83(2011) / Michigan Central\",\n\t6494: \"NAD83(2011) / Michigan Central (ft)\",\n\t6495: \"NAD83(2011) / Michigan North\",\n\t6496: \"NAD83(2011) / Michigan North (ft)\",\n\t6497: \"NAD83(2011) / Michigan Oblique Mercator\",\n\t6498: \"NAD83(2011) / Michigan South\",\n\t6499: \"NAD83(2011) / Michigan South (ft)\",\n\t6500: \"NAD83(2011) / Minnesota Central\",\n\t6501: \"NAD83(2011) / Minnesota Central (ftUS)\",\n\t6502: \"NAD83(2011) / Minnesota North\",\n\t6503: \"NAD83(2011) / Minnesota North (ftUS)\",\n\t6504: \"NAD83(2011) / Minnesota South\",\n\t6505: \"NAD83(2011) / Minnesota South (ftUS)\",\n\t6506: \"NAD83(2011) / Mississippi East\",\n\t6507: \"NAD83(2011) / Mississippi East (ftUS)\",\n\t6508: \"NAD83(2011) / Mississippi TM\",\n\t6509: \"NAD83(2011) / Mississippi West\",\n\t6510: \"NAD83(2011) / Mississippi West (ftUS)\",\n\t6511: \"NAD83(2011) / Missouri Central\",\n\t6512: \"NAD83(2011) / Missouri East\",\n\t6513: \"NAD83(2011) / Missouri West\",\n\t6514: \"NAD83(2011) / Montana\",\n\t6515: \"NAD83(2011) / Montana (ft)\",\n\t6516: \"NAD83(2011) / Nebraska\",\n\t6517: \"NAD83(2011) / Nebraska (ftUS)\",\n\t6518: \"NAD83(2011) / Nevada Central\",\n\t6519: \"NAD83(2011) / Nevada Central (ftUS)\",\n\t6520: \"NAD83(2011) / Nevada East\",\n\t6521: \"NAD83(2011) / Nevada East (ftUS)\",\n\t6522: \"NAD83(2011) / Nevada West\",\n\t6523: \"NAD83(2011) / Nevada West (ftUS)\",\n\t6524: \"NAD83(2011) / New Hampshire\",\n\t6525: \"NAD83(2011) / New Hampshire (ftUS)\",\n\t6526: \"NAD83(2011) / New Jersey\",\n\t6527: \"NAD83(2011) / New Jersey (ftUS)\",\n\t6528: \"NAD83(2011) / New Mexico Central\",\n\t6529: \"NAD83(2011) / New Mexico Central (ftUS)\",\n\t6530: \"NAD83(2011) / New Mexico East\",\n\t6531: \"NAD83(2011) / New Mexico East (ftUS)\",\n\t6532: \"NAD83(2011) / New Mexico West\",\n\t6533: \"NAD83(2011) / New Mexico West (ftUS)\",\n\t6534: \"NAD83(2011) / New York Central\",\n\t6535: \"NAD83(2011) / New York Central (ftUS)\",\n\t6536: \"NAD83(2011) / New York East\",\n\t6537: \"NAD83(2011) / New York East (ftUS)\",\n\t6538: \"NAD83(2011) / New York Long Island\",\n\t6539: \"NAD83(2011) / New York Long Island (ftUS)\",\n\t6540: \"NAD83(2011) / New York West\",\n\t6541: \"NAD83(2011) / New York West (ftUS)\",\n\t6542: \"NAD83(2011) / North Carolina\",\n\t6543: \"NAD83(2011) / North Carolina (ftUS)\",\n\t6544: \"NAD83(2011) / North Dakota North\",\n\t6545: \"NAD83(2011) / North Dakota North (ft)\",\n\t6546: \"NAD83(2011) / North Dakota South\",\n\t6547: \"NAD83(2011) / North Dakota South (ft)\",\n\t6548: \"NAD83(2011) / Ohio North\",\n\t6549: \"NAD83(2011) / Ohio North (ftUS)\",\n\t6550: \"NAD83(2011) / Ohio South\",\n\t6551: \"NAD83(2011) / Ohio South (ftUS)\",\n\t6552: \"NAD83(2011) / Oklahoma North\",\n\t6553: \"NAD83(2011) / Oklahoma North (ftUS)\",\n\t6554: \"NAD83(2011) / Oklahoma South\",\n\t6555: \"NAD83(2011) / Oklahoma South (ftUS)\",\n\t6556: \"NAD83(2011) / Oregon LCC (m)\",\n\t6557: \"NAD83(2011) / Oregon GIC Lambert (ft)\",\n\t6558: \"NAD83(2011) / Oregon North\",\n\t6559: \"NAD83(2011) / Oregon North (ft)\",\n\t6560: \"NAD83(2011) / Oregon South\",\n\t6561: \"NAD83(2011) / Oregon South (ft)\",\n\t6562: \"NAD83(2011) / Pennsylvania North\",\n\t6563: \"NAD83(2011) / Pennsylvania North (ftUS)\",\n\t6564: \"NAD83(2011) / Pennsylvania South\",\n\t6565: \"NAD83(2011) / Pennsylvania South (ftUS)\",\n\t6566: \"NAD83(2011) / Puerto Rico and Virgin Is.\",\n\t6567: \"NAD83(2011) / Rhode Island\",\n\t6568: \"NAD83(2011) / Rhode Island (ftUS)\",\n\t6569: \"NAD83(2011) / South Carolina\",\n\t6570: \"NAD83(2011) / South Carolina (ft)\",\n\t6571: \"NAD83(2011) / South Dakota North\",\n\t6572: \"NAD83(2011) / South Dakota North (ftUS)\",\n\t6573: \"NAD83(2011) / South Dakota South\",\n\t6574: \"NAD83(2011) / South Dakota South (ftUS)\",\n\t6575: \"NAD83(2011) / Tennessee\",\n\t6576: \"NAD83(2011) / Tennessee (ftUS)\",\n\t6577: \"NAD83(2011) / Texas Central\",\n\t6578: \"NAD83(2011) / Texas Central (ftUS)\",\n\t6579: \"NAD83(2011) / Texas Centric Albers Equal Area\",\n\t6580: \"NAD83(2011) / Texas Centric Lambert Conformal\",\n\t6581: \"NAD83(2011) / Texas North\",\n\t6582: \"NAD83(2011) / Texas North (ftUS)\",\n\t6583: \"NAD83(2011) / Texas North Central\",\n\t6584: \"NAD83(2011) / Texas North Central (ftUS)\",\n\t6585: \"NAD83(2011) / Texas South\",\n\t6586: \"NAD83(2011) / Texas South (ftUS)\",\n\t6587: \"NAD83(2011) / Texas South Central\",\n\t6588: \"NAD83(2011) / Texas South Central (ftUS)\",\n\t6589: \"NAD83(2011) / Vermont\",\n\t6590: \"NAD83(2011) / Vermont (ftUS)\",\n\t6591: \"NAD83(2011) / Virginia Lambert\",\n\t6592: \"NAD83(2011) / Virginia North\",\n\t6593: \"NAD83(2011) / Virginia North (ftUS)\",\n\t6594: \"NAD83(2011) / Virginia South\",\n\t6595: \"NAD83(2011) / Virginia South (ftUS)\",\n\t6596: \"NAD83(2011) / Washington North\",\n\t6597: \"NAD83(2011) / Washington North (ftUS)\",\n\t6598: \"NAD83(2011) / Washington South\",\n\t6599: \"NAD83(2011) / Washington South (ftUS)\",\n\t6600: \"NAD83(2011) / West Virginia North\",\n\t6601: \"NAD83(2011) / West Virginia North (ftUS)\",\n\t6602: \"NAD83(2011) / West Virginia South\",\n\t6603: \"NAD83(2011) / West Virginia South (ftUS)\",\n\t6604: \"NAD83(2011) / Wisconsin Central\",\n\t6605: \"NAD83(2011) / Wisconsin Central (ftUS)\",\n\t6606: \"NAD83(2011) / Wisconsin North\",\n\t6607: \"NAD83(2011) / Wisconsin North (ftUS)\",\n\t6608: \"NAD83(2011) / Wisconsin South\",\n\t6609: \"NAD83(2011) / Wisconsin South (ftUS)\",\n\t6610: \"NAD83(2011) / Wisconsin Transverse Mercator\",\n\t6611: \"NAD83(2011) / Wyoming East\",\n\t6612: \"NAD83(2011) / Wyoming East (ftUS)\",\n\t6613: \"NAD83(2011) / Wyoming East Central\",\n\t6614: \"NAD83(2011) / Wyoming East Central (ftUS)\",\n\t6615: \"NAD83(2011) / Wyoming West\",\n\t6616: \"NAD83(2011) / Wyoming West (ftUS)\",\n\t6617: \"NAD83(2011) / Wyoming West Central\",\n\t6618: \"NAD83(2011) / Wyoming West Central (ftUS)\",\n\t6619: \"NAD83(2011) / Utah Central\",\n\t6620: \"NAD83(2011) / Utah North\",\n\t6621: \"NAD83(2011) / Utah South\",\n\t6622: \"NAD83(CSRS) / Quebec Lambert\",\n\t6623: \"NAD83 / Quebec Albers\",\n\t6624: \"NAD83(CSRS) / Quebec Albers\",\n\t6625: \"NAD83(2011) / Utah Central (ftUS)\",\n\t6626: \"NAD83(2011) / Utah North (ftUS)\",\n\t6627: \"NAD83(2011) / Utah South (ftUS)\",\n\t6628: \"NAD83(PA11) / Hawaii zone 1\",\n\t6629: \"NAD83(PA11) / Hawaii zone 2\",\n\t6630: \"NAD83(PA11) / Hawaii zone 3\",\n\t6631: \"NAD83(PA11) / Hawaii zone 4\",\n\t6632: \"NAD83(PA11) / Hawaii zone 5\",\n\t6633: \"NAD83(PA11) / Hawaii zone 3 (ftUS)\",\n\t6634: \"NAD83(PA11) / UTM zone 4N\",\n\t6635: \"NAD83(PA11) / UTM zone 5N\",\n\t6636: \"NAD83(PA11) / UTM zone 2S\",\n\t6637: \"NAD83(MA11) / Guam Map Grid\",\n\t6646: \"Karbala 1979 / Iraq National Grid\",\n\t6669: \"JGD2011 / Japan Plane Rectangular CS I\",\n\t6670: \"JGD2011 / Japan Plane Rectangular CS II\",\n\t6671: \"JGD2011 / Japan Plane Rectangular CS III\",\n\t6672: \"JGD2011 / Japan Plane Rectangular CS IV\",\n\t6673: \"JGD2011 / Japan Plane Rectangular CS V\",\n\t6674: \"JGD2011 / Japan Plane Rectangular CS VI\",\n\t6675: \"JGD2011 / Japan Plane Rectangular CS VII\",\n\t6676: \"JGD2011 / Japan Plane Rectangular CS VIII\",\n\t6677: \"JGD2011 / Japan Plane Rectangular CS IX\",\n\t6678: \"JGD2011 / Japan Plane Rectangular CS X\",\n\t6679: \"JGD2011 / Japan Plane Rectangular CS XI\",\n\t6680: \"JGD2011 / Japan Plane Rectangular CS XII\",\n\t6681: \"JGD2011 / Japan Plane Rectangular CS XIII\",\n\t6682: \"JGD2011 / Japan Plane Rectangular CS XIV\",\n\t6683: \"JGD2011 / Japan Plane Rectangular CS XV\",\n\t6684: \"JGD2011 / Japan Plane Rectangular CS XVI\",\n\t6685: \"JGD2011 / Japan Plane Rectangular CS XVII\",\n\t6686: \"JGD2011 / Japan Plane Rectangular CS XVIII\",\n\t6687: \"JGD2011 / Japan Plane Rectangular CS XIX\",\n\t6688: \"JGD2011 / UTM zone 51N\",\n\t6689: \"JGD2011 / UTM zone 52N\",\n\t6690: \"JGD2011 / UTM zone 53N\",\n\t6691: \"JGD2011 / UTM zone 54N\",\n\t6692: \"JGD2011 / UTM zone 55N\",\n\t6703: \"WGS 84 / TM 60 SW\",\n\t6707: \"RDN2008 / TM32\",\n\t6708: \"RDN2008 / TM33\",\n\t6709: \"RDN2008 / TM34\",\n\t6720: \"WGS 84 / CIG92\",\n\t6721: \"GDA94 / CIG94\",\n\t6722: \"WGS 84 / CKIG92\",\n\t6723: \"GDA94 / CKIG94\",\n\t6732: \"GDA94 / MGA zone 41\",\n\t6733: \"GDA94 / MGA zone 42\",\n\t6734: \"GDA94 / MGA zone 43\",\n\t6735: \"GDA94 / MGA zone 44\",\n\t6736: \"GDA94 / MGA zone 46\",\n\t6737: \"GDA94 / MGA zone 47\",\n\t6738: \"GDA94 / MGA zone 59\",\n\t6784: \"NAD83(CORS96) / Oregon Baker zone (m)\",\n\t6785: \"NAD83(CORS96) / Oregon Baker zone (ft)\",\n\t6786: \"NAD83(2011) / Oregon Baker zone (m)\",\n\t6787: \"NAD83(2011) / Oregon Baker zone (ft)\",\n\t6788: \"NAD83(CORS96) / Oregon Bend-Klamath Falls zone (m)\",\n\t6789: \"NAD83(CORS96) / Oregon Bend-Klamath Falls zone (ft)\",\n\t6790: \"NAD83(2011) / Oregon Bend-Klamath Falls zone (m)\",\n\t6791: \"NAD83(2011) / Oregon Bend-Klamath Falls zone (ft)\",\n\t6792: \"NAD83(CORS96) / Oregon Bend-Redmond-Prineville zone (m)\",\n\t6793: \"NAD83(CORS96) / Oregon Bend-Redmond-Prineville zone (ft)\",\n\t6794: \"NAD83(2011) / Oregon Bend-Redmond-Prineville zone (m)\",\n\t6795: \"NAD83(2011) / Oregon Bend-Redmond-Prineville zone (ft)\",\n\t6796: \"NAD83(CORS96) / Oregon Bend-Burns zone (m)\",\n\t6797: \"NAD83(CORS96) / Oregon Bend-Burns zone (ft)\",\n\t6798: \"NAD83(2011) / Oregon Bend-Burns zone (m)\",\n\t6799: \"NAD83(2011) / Oregon Bend-Burns zone (ft)\",\n\t6800: \"NAD83(CORS96) / Oregon Canyonville-Grants Pass zone (m)\",\n\t6801: \"NAD83(CORS96) / Oregon Canyonville-Grants Pass zone (ft)\",\n\t6802: \"NAD83(2011) / Oregon Canyonville-Grants Pass zone (m)\",\n\t6803: \"NAD83(2011) / Oregon Canyonville-Grants Pass zone (ft)\",\n\t6804: \"NAD83(CORS96) / Oregon Columbia River East zone (m)\",\n\t6805: \"NAD83(CORS96) / Oregon Columbia River East zone (ft)\",\n\t6806: \"NAD83(2011) / Oregon Columbia River East zone (m)\",\n\t6807: \"NAD83(2011) / Oregon Columbia River East zone (ft)\",\n\t6808: \"NAD83(CORS96) / Oregon Columbia River West zone (m)\",\n\t6809: \"NAD83(CORS96) / Oregon Columbia River West zone (ft)\",\n\t6810: \"NAD83(2011) / Oregon Columbia River West zone (m)\",\n\t6811: \"NAD83(2011) / Oregon Columbia River West zone (ft)\",\n\t6812: \"NAD83(CORS96) / Oregon Cottage Grove-Canyonville zone (m)\",\n\t6813: \"NAD83(CORS96) / Oregon Cottage Grove-Canyonville zone (ft)\",\n\t6814: \"NAD83(2011) / Oregon Cottage Grove-Canyonville zone (m)\",\n\t6815: \"NAD83(2011) / Oregon Cottage Grove-Canyonville zone (ft)\",\n\t6816: \"NAD83(CORS96) / Oregon Dufur-Madras zone (m)\",\n\t6817: \"NAD83(CORS96) / Oregon Dufur-Madras zone (ft)\",\n\t6818: \"NAD83(2011) / Oregon Dufur-Madras zone (m)\",\n\t6819: \"NAD83(2011) / Oregon Dufur-Madras zone (ft)\",\n\t6820: \"NAD83(CORS96) / Oregon Eugene zone (m)\",\n\t6821: \"NAD83(CORS96) / Oregon Eugene zone (ft)\",\n\t6822: \"NAD83(2011) / Oregon Eugene zone (m)\",\n\t6823: \"NAD83(2011) / Oregon Eugene zone (ft)\",\n\t6824: \"NAD83(CORS96) / Oregon Grants Pass-Ashland zone (m)\",\n\t6825: \"NAD83(CORS96) / Oregon Grants Pass-Ashland zone (ft)\",\n\t6826: \"NAD83(2011) / Oregon Grants Pass-Ashland zone (m)\",\n\t6827: \"NAD83(2011) / Oregon Grants Pass-Ashland zone (ft)\",\n\t6828: \"NAD83(CORS96) / Oregon Gresham-Warm Springs zone (m)\",\n\t6829: \"NAD83(CORS96) / Oregon Gresham-Warm Springs zone (ft)\",\n\t6830: \"NAD83(2011) / Oregon Gresham-Warm Springs zone (m)\",\n\t6831: \"NAD83(2011) / Oregon Gresham-Warm Springs zone (ft)\",\n\t6832: \"NAD83(CORS96) / Oregon La Grande zone (m)\",\n\t6833: \"NAD83(CORS96) / Oregon La Grande zone (ft)\",\n\t6834: \"NAD83(2011) / Oregon La Grande zone (m)\",\n\t6835: \"NAD83(2011) / Oregon La Grande zone (ft)\",\n\t6836: \"NAD83(CORS96) / Oregon Ontario zone (m)\",\n\t6837: \"NAD83(CORS96) / Oregon Ontario zone (ft)\",\n\t6838: \"NAD83(2011) / Oregon Ontario zone (m)\",\n\t6839: \"NAD83(2011) / Oregon Ontario zone (ft)\",\n\t6840: \"NAD83(CORS96) / Oregon Coast zone (m)\",\n\t6841: \"NAD83(CORS96) / Oregon Coast zone (ft)\",\n\t6842: \"NAD83(2011) / Oregon Coast zone (m)\",\n\t6843: \"NAD83(2011) / Oregon Coast zone (ft)\",\n\t6844: \"NAD83(CORS96) / Oregon Pendleton zone (m)\",\n\t6845: \"NAD83(CORS96) / Oregon Pendleton zone (ft)\",\n\t6846: \"NAD83(2011) / Oregon Pendleton zone (m)\",\n\t6847: \"NAD83(2011) / Oregon Pendleton zone (ft)\",\n\t6848: \"NAD83(CORS96) / Oregon Pendleton-La Grande zone (m)\",\n\t6849: \"NAD83(CORS96) / Oregon Pendleton-La Grande zone (ft)\",\n\t6850: \"NAD83(2011) / Oregon Pendleton-La Grande zone (m)\",\n\t6851: \"NAD83(2011) / Oregon Pendleton-La Grande zone (ft)\",\n\t6852: \"NAD83(CORS96) / Oregon Portland zone (m)\",\n\t6853: \"NAD83(CORS96) / Oregon Portland zone (ft)\",\n\t6854: \"NAD83(2011) / Oregon Portland zone (m)\",\n\t6855: \"NAD83(2011) / Oregon Portland zone (ft)\",\n\t6856: \"NAD83(CORS96) / Oregon Salem zone (m)\",\n\t6857: \"NAD83(CORS96) / Oregon Salem zone (ft)\",\n\t6858: \"NAD83(2011) / Oregon Salem zone (m)\",\n\t6859: \"NAD83(2011) / Oregon Salem zone (ft)\",\n\t6860: \"NAD83(CORS96) / Oregon Santiam Pass zone (m)\",\n\t6861: \"NAD83(CORS96) / Oregon Santiam Pass zone (ft)\",\n\t6862: \"NAD83(2011) / Oregon Santiam Pass zone (m)\",\n\t6863: \"NAD83(2011) / Oregon Santiam Pass (ft)\",\n\t6867: \"NAD83(CORS96) / Oregon LCC (m)\",\n\t6868: \"NAD83(CORS96) / Oregon GIC Lambert (ft)\",\n\t6870: \"ETRS89 / Albania TM 2010\",\n\t6875: \"RDN2008 / Italy zone\",\n\t6876: \"RDN2008 / Zone 12\",\n\t6879: \"NAD83(2011) / Wisconsin Central\",\n\t6880: \"NAD83(2011) / Nebraska (ftUS)\",\n\t6884: \"NAD83(CORS96) / Oregon North\",\n\t6885: \"NAD83(CORS96) / Oregon North (ft)\",\n\t6886: \"NAD83(CORS96) / Oregon South\",\n\t6887: \"NAD83(CORS96) / Oregon South (ft)\",\n\t6915: \"South East Island 1943 / UTM zone 40N\",\n\t6922: \"NAD83 / Kansas LCC\",\n\t6923: \"NAD83 / Kansas LCC (ftUS)\",\n\t6924: \"NAD83(2011) / Kansas LCC\",\n\t6925: \"NAD83(2011) / Kansas LCC (ftUS)\",\n\t6931: \"WGS 84 / NSIDC EASE-Grid 2.0 North\",\n\t6932: \"WGS 84 / NSIDC EASE-Grid 2.0 South\",\n\t6933: \"WGS 84 / NSIDC EASE-Grid 2.0 Global\",\n\t6956: \"VN-2000 / TM-3 zone 481\",\n\t6957: \"VN-2000 / TM-3 zone 482\",\n\t6958: \"VN-2000 / TM-3 zone 491\",\n\t6959: \"VN-2000 / TM-3 Da Nang zone\",\n\t6962: \"ETRS89 / Albania LCC 2010\",\n\t6966: \"NAD27 / Michigan North\",\n\t6984: \"Israeli Grid 05\",\n\t6991: \"Israeli Grid 05/12\",\n\t6996: \"NAD83(2011) / San Francisco CS13\",\n\t6997: \"NAD83(2011) / San Francisco CS13 (ftUS)\",\n\t7005: \"Nahrwan 1934 / UTM zone 37N\",\n\t7006: \"Nahrwan 1934 / UTM zone 38N\",\n\t7007: \"Nahrwan 1934 / UTM zone 39N\",\n\t7057: \"NAD83(2011) / IaRCS zone 1\",\n\t7058: \"NAD83(2011) / IaRCS zone 2\",\n\t7059: \"NAD83(2011) / IaRCS zone 3\",\n\t7060: \"NAD83(2011) / IaRCS zone 4\",\n\t7061: \"NAD83(2011) / IaRCS zone 5\",\n\t7062: \"NAD83(2011) / IaRCS zone 6\",\n\t7063: \"NAD83(2011) / IaRCS zone 7\",\n\t7064: \"NAD83(2011) / IaRCS zone 8\",\n\t7065: \"NAD83(2011) / IaRCS zone 9\",\n\t7066: \"NAD83(2011) / IaRCS zone 10\",\n\t7067: \"NAD83(2011) / IaRCS zone 11\",\n\t7068: \"NAD83(2011) / IaRCS zone 12\",\n\t7069: \"NAD83(2011) / IaRCS zone 13\",\n\t7070: \"NAD83(2011) / IaRCS zone 14\",\n\t7074: \"RGTAAF07 / UTM zone 37S\",\n\t7075: \"RGTAAF07 / UTM zone 38S\",\n\t7076: \"RGTAAF07 / UTM zone 39S\",\n\t7077: \"RGTAAF07 / UTM zone 40S\",\n\t7078: \"RGTAAF07 / UTM zone 41S\",\n\t7079: \"RGTAAF07 / UTM zone 42S\",\n\t7080: \"RGTAAF07 / UTM zone 43S\",\n\t7081: \"RGTAAF07 / UTM zone 44S\",\n\t7082: \"RGTAAF07 / Terre Adelie Polar Stereographic\",\n\t7109: \"NAD83(2011) / RMTCRS St Mary (m)\",\n\t7110: \"NAD83(2011) / RMTCRS Blackfeet (m)\",\n\t7111: \"NAD83(2011) / RMTCRS Milk River (m)\",\n\t7112: \"NAD83(2011) / RMTCRS Fort Belknap (m)\",\n\t7113: \"NAD83(2011) / RMTCRS Fort Peck Assiniboine (m)\",\n\t7114: \"NAD83(2011) / RMTCRS Fort Peck Sioux (m)\",\n\t7115: \"NAD83(2011) / RMTCRS Crow (m)\",\n\t7116: \"NAD83(2011) / RMTCRS Bobcat (m)\",\n\t7117: \"NAD83(2011) / RMTCRS Billings (m)\",\n\t7118: \"NAD83(2011) / RMTCRS Wind River (m)\",\n\t7119: \"NAD83(2011) / RMTCRS St Mary (ft)\",\n\t7120: \"NAD83(2011) / RMTCRS Blackfeet (ft)\",\n\t7121: \"NAD83(2011) / RMTCRS Milk River (ft)\",\n\t7122: \"NAD83(2011) / RMTCRS Fort Belknap (ft)\",\n\t7123: \"NAD83(2011) / RMTCRS Fort Peck Assiniboine (ft)\",\n\t7124: \"NAD83(2011) / RMTCRS Fort Peck Sioux (ft)\",\n\t7125: \"NAD83(2011) / RMTCRS Crow (ft)\",\n\t7126: \"NAD83(2011) / RMTCRS Bobcat (ft)\",\n\t7127: \"NAD83(2011) / RMTCRS Billings (ft)\",\n\t7128: \"NAD83(2011) / RMTCRS Wind River (ftUS)\",\n\t7131: \"NAD83(2011) / San Francisco CS13\",\n\t7132: \"NAD83(2011) / San Francisco CS13 (ftUS)\",\n\t7142: \"Palestine 1923 / Palestine Grid modified\",\n\t7257: \"NAD83(2011) / InGCS Adams (m)\",\n\t7258: \"NAD83(2011) / InGCS Adams (ftUS)\",\n\t7259: \"NAD83(2011) / InGCS Allen (m)\",\n\t7260: \"NAD83(2011) / InGCS Allen (ftUS)\",\n\t7261: \"NAD83(2011) / InGCS Bartholomew (m)\",\n\t7262: \"NAD83(2011) / InGCS Bartholomew (ftUS)\",\n\t7263: \"NAD83(2011) / InGCS Benton (m)\",\n\t7264: \"NAD83(2011) / InGCS Benton (ftUS)\",\n\t7265: \"NAD83(2011) / InGCS Blackford-Delaware (m)\",\n\t7266: \"NAD83(2011) / InGCS Blackford-Delaware (ftUS)\",\n\t7267: \"NAD83(2011) / InGCS Boone-Hendricks (m)\",\n\t7268: \"NAD83(2011) / InGCS Boone-Hendricks (ftUS)\",\n\t7269: \"NAD83(2011) / InGCS Brown (m)\",\n\t7270: \"NAD83(2011) / InGCS Brown (ftUS)\",\n\t7271: \"NAD83(2011) / InGCS Carroll (m)\",\n\t7272: \"NAD83(2011) / InGCS Carroll (ftUS)\",\n\t7273: \"NAD83(2011) / InGCS Cass (m)\",\n\t7274: \"NAD83(2011) / InGCS Cass (ftUS)\",\n\t7275: \"NAD83(2011) / InGCS Clark-Floyd-Scott (m)\",\n\t7276: \"NAD83(2011) / InGCS Clark-Floyd-Scott (ftUS)\",\n\t7277: \"NAD83(2011) / InGCS Clay (m)\",\n\t7278: \"NAD83(2011) / InGCS Clay (ftUS)\",\n\t7279: \"NAD83(2011) / InGCS Clinton (m)\",\n\t7280: \"NAD83(2011) / InGCS Clinton (ftUS)\",\n\t7281: \"NAD83(2011) / InGCS Crawford-Lawrence-Orange (m)\",\n\t7282: \"NAD83(2011) / InGCS Crawford-Lawrence-Orange (ftUS)\",\n\t7283: \"NAD83(2011) / InGCS Daviess-Greene (m)\",\n\t7284: \"NAD83(2011) / InGCS Daviess-Greene (ftUS)\",\n\t7285: \"NAD83(2011) / InGCS Dearborn-Ohio-Switzerland (m)\",\n\t7286: \"NAD83(2011) / InGCS Dearborn-Ohio-Switzerland (ftUS)\",\n\t7287: \"NAD83(2011) / InGCS Decatur-Rush (m)\",\n\t7288: \"NAD83(2011) / InGCS Decatur-Rush (ftUS)\",\n\t7289: \"NAD83(2011) / InGCS DeKalb (m)\",\n\t7290: \"NAD83(2011) / InGCS DeKalb (ftUS)\",\n\t7291: \"NAD83(2011) / InGCS Dubois-Martin (m)\",\n\t7292: \"NAD83(2011) / InGCS Dubois-Martin (ftUS)\",\n\t7293: \"NAD83(2011) / InGCS Elkhart-Kosciusko-Wabash (m)\",\n\t7294: \"NAD83(2011) / InGCS Elkhart-Kosciusko-Wabash (ftUS)\",\n\t7295: \"NAD83(2011) / InGCS Fayette-Franklin-Union (m)\",\n\t7296: \"NAD83(2011) / InGCS Fayette-Franklin-Union (ftUS)\",\n\t7297: \"NAD83(2011) / InGCS Fountain-Warren (m)\",\n\t7298: \"NAD83(2011) / InGCS Fountain-Warren (ftUS)\",\n\t7299: \"NAD83(2011) / InGCS Fulton-Marshall-St. Joseph (m)\",\n\t7300: \"NAD83(2011) / InGCS Fulton-Marshall-St. Joseph (ftUS)\",\n\t7301: \"NAD83(2011) / InGCS Gibson (m)\",\n\t7302: \"NAD83(2011) / InGCS Gibson (ftUS)\",\n\t7303: \"NAD83(2011) / InGCS Grant (m)\",\n\t7304: \"NAD83(2011) / InGCS Grant (ftUS)\",\n\t7305: \"NAD83(2011) / InGCS Hamilton-Tipton (m)\",\n\t7306: \"NAD83(2011) / InGCS Hamilton-Tipton (ftUS)\",\n\t7307: \"NAD83(2011) / InGCS Hancock-Madison (m)\",\n\t7308: \"NAD83(2011) / InGCS Hancock-Madison (ftUS)\",\n\t7309: \"NAD83(2011) / InGCS Harrison-Washington (m)\",\n\t7310: \"NAD83(2011) / InGCS Harrison-Washington (ftUS)\",\n\t7311: \"NAD83(2011) / InGCS Henry (m)\",\n\t7312: \"NAD83(2011) / InGCS Henry (ftUS)\",\n\t7313: \"NAD83(2011) / InGCS Howard-Miami (m)\",\n\t7314: \"NAD83(2011) / InGCS Howard-Miami (ftUS)\",\n\t7315: \"NAD83(2011) / InGCS Huntington-Whitley (m)\",\n\t7316: \"NAD83(2011) / InGCS Huntington-Whitley (ftUS)\",\n\t7317: \"NAD83(2011) / InGCS Jackson (m)\",\n\t7318: \"NAD83(2011) / InGCS Jackson (ftUS)\",\n\t7319: \"NAD83(2011) / InGCS Jasper-Porter (m)\",\n\t7320: \"NAD83(2011) / InGCS Jasper-Porter (ftUS)\",\n\t7321: \"NAD83(2011) / InGCS Jay (m)\",\n\t7322: \"NAD83(2011) / InGCS Jay (ftUS)\",\n\t7323: \"NAD83(2011) / InGCS Jefferson (m)\",\n\t7324: \"NAD83(2011) / InGCS Jefferson (ftUS)\",\n\t7325: \"NAD83(2011) / InGCS Jennings (m)\",\n\t7326: \"NAD83(2011) / InGCS Jennings (ftUS)\",\n\t7327: \"NAD83(2011) / InGCS Johnson-Marion (m)\",\n\t7328: \"NAD83(2011) / InGCS Johnson-Marion (ftUS)\",\n\t7329: \"NAD83(2011) / InGCS Knox (m)\",\n\t7330: \"NAD83(2011) / InGCS Knox (ftUS)\",\n\t7331: \"NAD83(2011) / InGCS LaGrange-Noble (m)\",\n\t7332: \"NAD83(2011) / InGCS LaGrange-Noble (ftUS)\",\n\t7333: \"NAD83(2011) / InGCS Lake-Newton (m)\",\n\t7334: \"NAD83(2011) / InGCS Lake-Newton (ftUS)\",\n\t7335: \"NAD83(2011) / InGCS LaPorte-Pulaski-Starke (m)\",\n\t7336: \"NAD83(2011) / InGCS LaPorte-Pulaski-Starke (ftUS)\",\n\t7337: \"NAD83(2011) / InGCS Monroe-Morgan (m)\",\n\t7338: \"NAD83(2011) / InGCS Monroe-Morgan (ftUS)\",\n\t7339: \"NAD83(2011) / InGCS Montgomery-Putnam (m)\",\n\t7340: \"NAD83(2011) / InGCS Montgomery-Putnam (ftUS)\",\n\t7341: \"NAD83(2011) / InGCS Owen (m)\",\n\t7342: \"NAD83(2011) / InGCS Owen (ftUS)\",\n\t7343: \"NAD83(2011) / InGCS Parke-Vermillion (m)\",\n\t7344: \"NAD83(2011) / InGCS Parke-Vermillion (ftUS)\",\n\t7345: \"NAD83(2011) / InGCS Perry (m)\",\n\t7346: \"NAD83(2011) / InGCS Perry (ftUS)\",\n\t7347: \"NAD83(2011) / InGCS Pike-Warrick (m)\",\n\t7348: \"NAD83(2011) / InGCS Pike-Warrick (ftUS)\",\n\t7349: \"NAD83(2011) / InGCS Posey (m)\",\n\t7350: \"NAD83(2011) / InGCS Posey (ftUS)\",\n\t7351: \"NAD83(2011) / InGCS Randolph-Wayne (m)\",\n\t7352: \"NAD83(2011) / InGCS Randolph-Wayne (ftUS)\",\n\t7353: \"NAD83(2011) / InGCS Ripley (m)\",\n\t7354: \"NAD83(2011) / InGCS Ripley (ftUS)\",\n\t7355: \"NAD83(2011) / InGCS Shelby (m)\",\n\t7356: \"NAD83(2011) / InGCS Shelby (ftUS)\",\n\t7357: \"NAD83(2011) / InGCS Spencer (m)\",\n\t7358: \"NAD83(2011) / InGCS Spencer (ftUS)\",\n\t7359: \"NAD83(2011) / InGCS Steuben (m)\",\n\t7360: \"NAD83(2011) / InGCS Steuben (ftUS)\",\n\t7361: \"NAD83(2011) / InGCS Sullivan (m)\",\n\t7362: \"NAD83(2011) / InGCS Sullivan (ftUS)\",\n\t7363: \"NAD83(2011) / InGCS Tippecanoe-White (m)\",\n\t7364: \"NAD83(2011) / InGCS Tippecanoe-White (ftUS)\",\n\t7365: \"NAD83(2011) / InGCS Vanderburgh (m)\",\n\t7366: \"NAD83(2011) / InGCS Vanderburgh (ftUS)\",\n\t7367: \"NAD83(2011) / InGCS Vigo (m)\",\n\t7368: \"NAD83(2011) / InGCS Vigo (ftUS)\",\n\t7369: \"NAD83(2011) / InGCS Wells (m)\",\n\t7370: \"NAD83(2011) / InGCS Wells (ftUS)\",\n\t7374: \"ONGD14 / UTM zone 39N\",\n\t7375: \"ONGD14 / UTM zone 40N\",\n\t7376: \"ONGD14 / UTM zone 41N\",\n\t7528: \"NAD83(2011) / WISCRS Adams and Juneau (m)\",\n\t7529: \"NAD83(2011) / WISCRS Ashland (m)\",\n\t7530: \"NAD83(2011) / WISCRS Barron (m)\",\n\t7531: \"NAD83(2011) / WISCRS Bayfield (m)\",\n\t7532: \"NAD83(2011) / WISCRS Brown (m)\",\n\t7533: \"NAD83(2011) / WISCRS Buffalo (m)\",\n\t7534: \"NAD83(2011) / WISCRS Burnett (m)\",\n\t7535: \"NAD83(2011) / WISCRS Calumet, Fond du Lac, Outagamie and Winnebago (m)\",\n\t7536: \"NAD83(2011) / WISCRS Chippewa (m)\",\n\t7537: \"NAD83(2011) / WISCRS Clark (m)\",\n\t7538: \"NAD83(2011) / WISCRS Columbia (m)\",\n\t7539: \"NAD83(2011) / WISCRS Crawford (m)\",\n\t7540: \"NAD83(2011) / WISCRS Dane (m)\",\n\t7541: \"NAD83(2011) / WISCRS Dodge and Jefferson (m)\",\n\t7542: \"NAD83(2011) / WISCRS Door (m)\",\n\t7543: \"NAD83(2011) / WISCRS Douglas (m)\",\n\t7544: \"NAD83(2011) / WISCRS Dunn (m)\",\n\t7545: \"NAD83(2011) / WISCRS Eau Claire (m)\",\n\t7546: \"NAD83(2011) / WISCRS Florence (m)\",\n\t7547: \"NAD83(2011) / WISCRS Forest (m)\",\n\t7548: \"NAD83(2011) / WISCRS Grant (m)\",\n\t7549: \"NAD83(2011) / WISCRS Green and Lafayette (m)\",\n\t7550: \"NAD83(2011) / WISCRS Green Lake and Marquette (m)\",\n\t7551: \"NAD83(2011) / WISCRS Iowa (m)\",\n\t7552: \"NAD83(2011) / WISCRS Iron (m)\",\n\t7553: \"NAD83(2011) / WISCRS Jackson (m)\",\n\t7554: \"NAD83(2011) / WISCRS Kenosha, Milwaukee, Ozaukee and Racine (m)\",\n\t7555: \"NAD83(2011) / WISCRS Kewaunee, Manitowoc and Sheboygan (m)\",\n\t7556: \"NAD83(2011) / WISCRS La Crosse (m)\",\n\t7557: \"NAD83(2011) / WISCRS Langlade (m)\",\n\t7558: \"NAD83(2011) / WISCRS Lincoln (m)\",\n\t7559: \"NAD83(2011) / WISCRS Marathon (m)\",\n\t7560: \"NAD83(2011) / WISCRS Marinette (m)\",\n\t7561: \"NAD83(2011) / WISCRS Menominee (m)\",\n\t7562: \"NAD83(2011) / WISCRS Monroe (m)\",\n\t7563: \"NAD83(2011) / WISCRS Oconto (m)\",\n\t7564: \"NAD83(2011) / WISCRS Oneida (m)\",\n\t7565: \"NAD83(2011) / WISCRS Pepin and Pierce (m)\",\n\t7566: \"NAD83(2011) / WISCRS Polk (m)\",\n\t7567: \"NAD83(2011) / WISCRS Portage (m)\",\n\t7568: \"NAD83(2011) / WISCRS Price (m)\",\n\t7569: \"NAD83(2011) / WISCRS Richland (m)\",\n\t7570: \"NAD83(2011) / WISCRS Rock (m)\",\n\t7571: \"NAD83(2011) / WISCRS Rusk (m)\",\n\t7572: \"NAD83(2011) / WISCRS Sauk (m)\",\n\t7573: \"NAD83(2011) / WISCRS Sawyer (m)\",\n\t7574: \"NAD83(2011) / WISCRS Shawano (m)\",\n\t7575: \"NAD83(2011) / WISCRS St. Croix (m)\",\n\t7576: \"NAD83(2011) / WISCRS Taylor (m)\",\n\t7577: \"NAD83(2011) / WISCRS Trempealeau (m)\",\n\t7578: \"NAD83(2011) / WISCRS Vernon (m)\",\n\t7579: \"NAD83(2011) / WISCRS Vilas (m)\",\n\t7580: \"NAD83(2011) / WISCRS Walworth (m)\",\n\t7581: \"NAD83(2011) / WISCRS Washburn (m)\",\n\t7582: \"NAD83(2011) / WISCRS Washington (m)\",\n\t7583: \"NAD83(2011) / WISCRS Waukesha (m)\",\n\t7584: \"NAD83(2011) / WISCRS Waupaca (m)\",\n\t7585: \"NAD83(2011) / WISCRS Waushara (m)\",\n\t7586: \"NAD83(2011) / WISCRS Wood (m)\",\n\t7587: \"NAD83(2011) / WISCRS Adams and Juneau (ftUS)\",\n\t7588: \"NAD83(2011) / WISCRS Ashland (ftUS)\",\n\t7589: \"NAD83(2011) / WISCRS Barron (ftUS)\",\n\t7590: \"NAD83(2011) / WISCRS Bayfield (ftUS)\",\n\t7591: \"NAD83(2011) / WISCRS Brown (ftUS)\",\n\t7592: \"NAD83(2011) / WISCRS Buffalo (ftUS)\",\n\t7593: \"NAD83(2011) / WISCRS Burnett (ftUS)\",\n\t7594: \"NAD83(2011) / WISCRS Calumet, Fond du Lac, Outagamie and Winnebago (ftUS)\",\n\t7595: \"NAD83(2011) / WISCRS Chippewa (ftUS)\",\n\t7596: \"NAD83(2011) / WISCRS Clark (ftUS)\",\n\t7597: \"NAD83(2011) / WISCRS Columbia (ftUS)\",\n\t7598: \"NAD83(2011) / WISCRS Crawford (ftUS)\",\n\t7599: \"NAD83(2011) / WISCRS Dane (ftUS)\",\n\t7600: \"NAD83(2011) / WISCRS Dodge and Jefferson (ftUS)\",\n\t7601: \"NAD83(2011) / WISCRS Door (ftUS)\",\n\t7602: \"NAD83(2011) / WISCRS Douglas (ftUS)\",\n\t7603: \"NAD83(2011) / WISCRS Dunn (ftUS)\",\n\t7604: \"NAD83(2011) / WISCRS Eau Claire (ftUS)\",\n\t7605: \"NAD83(2011) / WISCRS Florence (ftUS)\",\n\t7606: \"NAD83(2011) / WISCRS Forest (ftUS)\",\n\t7607: \"NAD83(2011) / WISCRS Grant (ftUS)\",\n\t7608: \"NAD83(2011) / WISCRS Green and Lafayette (ftUS)\",\n\t7609: \"NAD83(2011) / WISCRS Green Lake and Marquette (ftUS)\",\n\t7610: \"NAD83(2011) / WISCRS Iowa (ftUS)\",\n\t7611: \"NAD83(2011) / WISCRS Iron (ftUS)\",\n\t7612: \"NAD83(2011) / WISCRS Jackson (ftUS)\",\n\t7613: \"NAD83(2011) / WISCRS Kenosha, Milwaukee, Ozaukee and Racine (ftUS)\",\n\t7614: \"NAD83(2011) / WISCRS Kewaunee, Manitowoc and Sheboygan (ftUS)\",\n\t7615: \"NAD83(2011) / WISCRS La Crosse (ftUS)\",\n\t7616: \"NAD83(2011) / WISCRS Langlade (ftUS)\",\n\t7617: \"NAD83(2011) / WISCRS Lincoln (ftUS)\",\n\t7618: \"NAD83(2011) / WISCRS Marathon (ftUS)\",\n\t7619: \"NAD83(2011) / WISCRS Marinette (ftUS)\",\n\t7620: \"NAD83(2011) / WISCRS Menominee (ftUS)\",\n\t7621: \"NAD83(2011) / WISCRS Monroe (ftUS)\",\n\t7622: \"NAD83(2011) / WISCRS Oconto (ftUS)\",\n\t7623: \"NAD83(2011) / WISCRS Oneida (ftUS)\",\n\t7624: \"NAD83(2011) / WISCRS Pepin and Pierce (ftUS)\",\n\t7625: \"NAD83(2011) / WISCRS Polk (ftUS)\",\n\t7626: \"NAD83(2011) / WISCRS Portage (ftUS)\",\n\t7627: \"NAD83(2011) / WISCRS Price (ftUS)\",\n\t7628: \"NAD83(2011) / WISCRS Richland (ftUS)\",\n\t7629: \"NAD83(2011) / WISCRS Rock (ftUS)\",\n\t7630: \"NAD83(2011) / WISCRS Rusk (ftUS)\",\n\t7631: \"NAD83(2011) / WISCRS Sauk (ftUS)\",\n\t7632: \"NAD83(2011) / WISCRS Sawyer (ftUS)\",\n\t7633: \"NAD83(2011) / WISCRS Shawano (ftUS)\",\n\t7634: \"NAD83(2011) / WISCRS St. Croix (ftUS)\",\n\t7635: \"NAD83(2011) / WISCRS Taylor (ftUS)\",\n\t7636: \"NAD83(2011) / WISCRS Trempealeau (ftUS)\",\n\t7637: \"NAD83(2011) / WISCRS Vernon (ftUS)\",\n\t7638: \"NAD83(2011) / WISCRS Vilas (ftUS)\",\n\t7639: \"NAD83(2011) / WISCRS Walworth (ftUS)\",\n\t7640: \"NAD83(2011) / WISCRS Washburn (ftUS)\",\n\t7641: \"NAD83(2011) / WISCRS Washington (ftUS)\",\n\t7642: \"NAD83(2011) / WISCRS Waukesha (ftUS)\",\n\t7643: \"NAD83(2011) / WISCRS Waupaca (ftUS)\",\n\t7644: \"NAD83(2011) / WISCRS Waushara (ftUS)\",\n\t7645: \"NAD83(2011) / WISCRS Wood (ftUS)\",\n\t7692: \"Kyrg-06 / zone 1\",\n\t7693: \"Kyrg-06 / zone 2\",\n\t7694: \"Kyrg-06 / zone 3\",\n\t7695: \"Kyrg-06 / zone 4\",\n\t7696: \"Kyrg-06 / zone 5\",\n\t20004: \"Pulkovo 1995 / Gauss-Kruger zone 4\",\n\t20005: \"Pulkovo 1995 / Gauss-Kruger zone 5\",\n\t20006: \"Pulkovo 1995 / Gauss-Kruger zone 6\",\n\t20007: \"Pulkovo 1995 / Gauss-Kruger zone 7\",\n\t20008: \"Pulkovo 1995 / Gauss-Kruger zone 8\",\n\t20009: \"Pulkovo 1995 / Gauss-Kruger zone 9\",\n\t20010: \"Pulkovo 1995 / Gauss-Kruger zone 10\",\n\t20011: \"Pulkovo 1995 / Gauss-Kruger zone 11\",\n\t20012: \"Pulkovo 1995 / Gauss-Kruger zone 12\",\n\t20013: \"Pulkovo 1995 / Gauss-Kruger zone 13\",\n\t20014: \"Pulkovo 1995 / Gauss-Kruger zone 14\",\n\t20015: \"Pulkovo 1995 / Gauss-Kruger zone 15\",\n\t20016: \"Pulkovo 1995 / Gauss-Kruger zone 16\",\n\t20017: \"Pulkovo 1995 / Gauss-Kruger zone 17\",\n\t20018: \"Pulkovo 1995 / Gauss-Kruger zone 18\",\n\t20019: \"Pulkovo 1995 / Gauss-Kruger zone 19\",\n\t20020: \"Pulkovo 1995 / Gauss-Kruger zone 20\",\n\t20021: \"Pulkovo 1995 / Gauss-Kruger zone 21\",\n\t20022: \"Pulkovo 1995 / Gauss-Kruger zone 22\",\n\t20023: \"Pulkovo 1995 / Gauss-Kruger zone 23\",\n\t20024: \"Pulkovo 1995 / Gauss-Kruger zone 24\",\n\t20025: \"Pulkovo 1995 / Gauss-Kruger zone 25\",\n\t20026: \"Pulkovo 1995 / Gauss-Kruger zone 26\",\n\t20027: \"Pulkovo 1995 / Gauss-Kruger zone 27\",\n\t20028: \"Pulkovo 1995 / Gauss-Kruger zone 28\",\n\t20029: \"Pulkovo 1995 / Gauss-Kruger zone 29\",\n\t20030: \"Pulkovo 1995 / Gauss-Kruger zone 30\",\n\t20031: \"Pulkovo 1995 / Gauss-Kruger zone 31\",\n\t20032: \"Pulkovo 1995 / Gauss-Kruger zone 32\",\n\t20064: \"Pulkovo 1995 / Gauss-Kruger 4N\",\n\t20065: \"Pulkovo 1995 / Gauss-Kruger 5N\",\n\t20066: \"Pulkovo 1995 / Gauss-Kruger 6N\",\n\t20067: \"Pulkovo 1995 / Gauss-Kruger 7N\",\n\t20068: \"Pulkovo 1995 / Gauss-Kruger 8N\",\n\t20069: \"Pulkovo 1995 / Gauss-Kruger 9N\",\n\t20070: \"Pulkovo 1995 / Gauss-Kruger 10N\",\n\t20071: \"Pulkovo 1995 / Gauss-Kruger 11N\",\n\t20072: \"Pulkovo 1995 / Gauss-Kruger 12N\",\n\t20073: \"Pulkovo 1995 / Gauss-Kruger 13N\",\n\t20074: \"Pulkovo 1995 / Gauss-Kruger 14N\",\n\t20075: \"Pulkovo 1995 / Gauss-Kruger 15N\",\n\t20076: \"Pulkovo 1995 / Gauss-Kruger 16N\",\n\t20077: \"Pulkovo 1995 / Gauss-Kruger 17N\",\n\t20078: \"Pulkovo 1995 / Gauss-Kruger 18N\",\n\t20079: \"Pulkovo 1995 / Gauss-Kruger 19N\",\n\t20080: \"Pulkovo 1995 / Gauss-Kruger 20N\",\n\t20081: \"Pulkovo 1995 / Gauss-Kruger 21N\",\n\t20082: \"Pulkovo 1995 / Gauss-Kruger 22N\",\n\t20083: \"Pulkovo 1995 / Gauss-Kruger 23N\",\n\t20084: \"Pulkovo 1995 / Gauss-Kruger 24N\",\n\t20085: \"Pulkovo 1995 / Gauss-Kruger 25N\",\n\t20086: \"Pulkovo 1995 / Gauss-Kruger 26N\",\n\t20087: \"Pulkovo 1995 / Gauss-Kruger 27N\",\n\t20088: \"Pulkovo 1995 / Gauss-Kruger 28N\",\n\t20089: \"Pulkovo 1995 / Gauss-Kruger 29N\",\n\t20090: \"Pulkovo 1995 / Gauss-Kruger 30N\",\n\t20091: \"Pulkovo 1995 / Gauss-Kruger 31N\",\n\t20092: \"Pulkovo 1995 / Gauss-Kruger 32N\",\n\t20135: \"Adindan / UTM zone 35N\",\n\t20136: \"Adindan / UTM zone 36N\",\n\t20137: \"Adindan / UTM zone 37N\",\n\t20138: \"Adindan / UTM zone 38N\",\n\t20248: \"AGD66 / AMG zone 48\",\n\t20249: \"AGD66 / AMG zone 49\",\n\t20250: \"AGD66 / AMG zone 50\",\n\t20251: \"AGD66 / AMG zone 51\",\n\t20252: \"AGD66 / AMG zone 52\",\n\t20253: \"AGD66 / AMG zone 53\",\n\t20254: \"AGD66 / AMG zone 54\",\n\t20255: \"AGD66 / AMG zone 55\",\n\t20256: \"AGD66 / AMG zone 56\",\n\t20257: \"AGD66 / AMG zone 57\",\n\t20258: \"AGD66 / AMG zone 58\",\n\t20348: \"AGD84 / AMG zone 48\",\n\t20349: \"AGD84 / AMG zone 49\",\n\t20350: \"AGD84 / AMG zone 50\",\n\t20351: \"AGD84 / AMG zone 51\",\n\t20352: \"AGD84 / AMG zone 52\",\n\t20353: \"AGD84 / AMG zone 53\",\n\t20354: \"AGD84 / AMG zone 54\",\n\t20355: \"AGD84 / AMG zone 55\",\n\t20356: \"AGD84 / AMG zone 56\",\n\t20357: \"AGD84 / AMG zone 57\",\n\t20358: \"AGD84 / AMG zone 58\",\n\t20436: \"Ain el Abd / UTM zone 36N\",\n\t20437: \"Ain el Abd / UTM zone 37N\",\n\t20438: \"Ain el Abd / UTM zone 38N\",\n\t20439: \"Ain el Abd / UTM zone 39N\",\n\t20440: \"Ain el Abd / UTM zone 40N\",\n\t20499: \"Ain el Abd / Bahrain Grid\",\n\t20538: \"Afgooye / UTM zone 38N\",\n\t20539: \"Afgooye / UTM zone 39N\",\n\t20790: \"Lisbon (Lisbon) / Portuguese National Grid\",\n\t20791: \"Lisbon (Lisbon) / Portuguese Grid\",\n\t20822: \"Aratu / UTM zone 22S\",\n\t20823: \"Aratu / UTM zone 23S\",\n\t20824: \"Aratu / UTM zone 24S\",\n\t20934: \"Arc 1950 / UTM zone 34S\",\n\t20935: \"Arc 1950 / UTM zone 35S\",\n\t20936: \"Arc 1950 / UTM zone 36S\",\n\t21035: \"Arc 1960 / UTM zone 35S\",\n\t21036: \"Arc 1960 / UTM zone 36S\",\n\t21037: \"Arc 1960 / UTM zone 37S\",\n\t21095: \"Arc 1960 / UTM zone 35N\",\n\t21096: \"Arc 1960 / UTM zone 36N\",\n\t21097: \"Arc 1960 / UTM zone 37N\",\n\t21100: \"Batavia (Jakarta) / NEIEZ\",\n\t21148: \"Batavia / UTM zone 48S\",\n\t21149: \"Batavia / UTM zone 49S\",\n\t21150: \"Batavia / UTM zone 50S\",\n\t21291: \"Barbados 1938 / British West Indies Grid\",\n\t21292: \"Barbados 1938 / Barbados National Grid\",\n\t21413: \"Beijing 1954 / Gauss-Kruger zone 13\",\n\t21414: \"Beijing 1954 / Gauss-Kruger zone 14\",\n\t21415: \"Beijing 1954 / Gauss-Kruger zone 15\",\n\t21416: \"Beijing 1954 / Gauss-Kruger zone 16\",\n\t21417: \"Beijing 1954 / Gauss-Kruger zone 17\",\n\t21418: \"Beijing 1954 / Gauss-Kruger zone 18\",\n\t21419: \"Beijing 1954 / Gauss-Kruger zone 19\",\n\t21420: \"Beijing 1954 / Gauss-Kruger zone 20\",\n\t21421: \"Beijing 1954 / Gauss-Kruger zone 21\",\n\t21422: \"Beijing 1954 / Gauss-Kruger zone 22\",\n\t21423: \"Beijing 1954 / Gauss-Kruger zone 23\",\n\t21453: \"Beijing 1954 / Gauss-Kruger CM 75E\",\n\t21454: \"Beijing 1954 / Gauss-Kruger CM 81E\",\n\t21455: \"Beijing 1954 / Gauss-Kruger CM 87E\",\n\t21456: \"Beijing 1954 / Gauss-Kruger CM 93E\",\n\t21457: \"Beijing 1954 / Gauss-Kruger CM 99E\",\n\t21458: \"Beijing 1954 / Gauss-Kruger CM 105E\",\n\t21459: \"Beijing 1954 / Gauss-Kruger CM 111E\",\n\t21460: \"Beijing 1954 / Gauss-Kruger CM 117E\",\n\t21461: \"Beijing 1954 / Gauss-Kruger CM 123E\",\n\t21462: \"Beijing 1954 / Gauss-Kruger CM 129E\",\n\t21463: \"Beijing 1954 / Gauss-Kruger CM 135E\",\n\t21473: \"Beijing 1954 / Gauss-Kruger 13N\",\n\t21474: \"Beijing 1954 / Gauss-Kruger 14N\",\n\t21475: \"Beijing 1954 / Gauss-Kruger 15N\",\n\t21476: \"Beijing 1954 / Gauss-Kruger 16N\",\n\t21477: \"Beijing 1954 / Gauss-Kruger 17N\",\n\t21478: \"Beijing 1954 / Gauss-Kruger 18N\",\n\t21479: \"Beijing 1954 / Gauss-Kruger 19N\",\n\t21480: \"Beijing 1954 / Gauss-Kruger 20N\",\n\t21481: \"Beijing 1954 / Gauss-Kruger 21N\",\n\t21482: \"Beijing 1954 / Gauss-Kruger 22N\",\n\t21483: \"Beijing 1954 / Gauss-Kruger 23N\",\n\t21500: \"Belge 1950 (Brussels) / Belge Lambert 50\",\n\t21780: \"Bern 1898 (Bern) / LV03C\",\n\t21781: \"CH1903 / LV03\",\n\t21782: \"CH1903 / LV03C-G\",\n\t21817: \"Bogota 1975 / UTM zone 17N\",\n\t21818: \"Bogota 1975 / UTM zone 18N\",\n\t21891: \"Bogota 1975 / Colombia West zone\",\n\t21892: \"Bogota 1975 / Colombia Bogota zone\",\n\t21893: \"Bogota 1975 / Colombia East Central zone\",\n\t21894: \"Bogota 1975 / Colombia East\",\n\t21896: \"Bogota 1975 / Colombia West zone\",\n\t21897: \"Bogota 1975 / Colombia Bogota zone\",\n\t21898: \"Bogota 1975 / Colombia East Central zone\",\n\t21899: \"Bogota 1975 / Colombia East\",\n\t22032: \"Camacupa / UTM zone 32S\",\n\t22033: \"Camacupa / UTM zone 33S\",\n\t22091: \"Camacupa / TM 11.30 SE\",\n\t22092: \"Camacupa / TM 12 SE\",\n\t22171: \"POSGAR 98 / Argentina 1\",\n\t22172: \"POSGAR 98 / Argentina 2\",\n\t22173: \"POSGAR 98 / Argentina 3\",\n\t22174: \"POSGAR 98 / Argentina 4\",\n\t22175: \"POSGAR 98 / Argentina 5\",\n\t22176: \"POSGAR 98 / Argentina 6\",\n\t22177: \"POSGAR 98 / Argentina 7\",\n\t22181: \"POSGAR 94 / Argentina 1\",\n\t22182: \"POSGAR 94 / Argentina 2\",\n\t22183: \"POSGAR 94 / Argentina 3\",\n\t22184: \"POSGAR 94 / Argentina 4\",\n\t22185: \"POSGAR 94 / Argentina 5\",\n\t22186: \"POSGAR 94 / Argentina 6\",\n\t22187: \"POSGAR 94 / Argentina 7\",\n\t22191: \"Campo Inchauspe / Argentina 1\",\n\t22192: \"Campo Inchauspe / Argentina 2\",\n\t22193: \"Campo Inchauspe / Argentina 3\",\n\t22194: \"Campo Inchauspe / Argentina 4\",\n\t22195: \"Campo Inchauspe / Argentina 5\",\n\t22196: \"Campo Inchauspe / Argentina 6\",\n\t22197: \"Campo Inchauspe / Argentina 7\",\n\t22234: \"Cape / UTM zone 34S\",\n\t22235: \"Cape / UTM zone 35S\",\n\t22236: \"Cape / UTM zone 36S\",\n\t22275: \"Cape / Lo15\",\n\t22277: \"Cape / Lo17\",\n\t22279: \"Cape / Lo19\",\n\t22281: \"Cape / Lo21\",\n\t22283: \"Cape / Lo23\",\n\t22285: \"Cape / Lo25\",\n\t22287: \"Cape / Lo27\",\n\t22289: \"Cape / Lo29\",\n\t22291: \"Cape / Lo31\",\n\t22293: \"Cape / Lo33\",\n\t22300: \"Carthage (Paris) / Tunisia Mining Grid\",\n\t22332: \"Carthage / UTM zone 32N\",\n\t22391: \"Carthage / Nord Tunisie\",\n\t22392: \"Carthage / Sud Tunisie\",\n\t22521: \"Corrego Alegre 1970-72 / UTM zone 21S\",\n\t22522: \"Corrego Alegre 1970-72 / UTM zone 22S\",\n\t22523: \"Corrego Alegre 1970-72 / UTM zone 23S\",\n\t22524: \"Corrego Alegre 1970-72 / UTM zone 24S\",\n\t22525: \"Corrego Alegre 1970-72 / UTM zone 25S\",\n\t22700: \"Deir ez Zor / Levant Zone\",\n\t22770: \"Deir ez Zor / Syria Lambert\",\n\t22780: \"Deir ez Zor / Levant Stereographic\",\n\t22832: \"Douala / UTM zone 32N\",\n\t22991: \"Egypt 1907 / Blue Belt\",\n\t22992: \"Egypt 1907 / Red Belt\",\n\t22993: \"Egypt 1907 / Purple Belt\",\n\t22994: \"Egypt 1907 / Extended Purple Belt\",\n\t23028: \"ED50 / UTM zone 28N\",\n\t23029: \"ED50 / UTM zone 29N\",\n\t23030: \"ED50 / UTM zone 30N\",\n\t23031: \"ED50 / UTM zone 31N\",\n\t23032: \"ED50 / UTM zone 32N\",\n\t23033: \"ED50 / UTM zone 33N\",\n\t23034: \"ED50 / UTM zone 34N\",\n\t23035: \"ED50 / UTM zone 35N\",\n\t23036: \"ED50 / UTM zone 36N\",\n\t23037: \"ED50 / UTM zone 37N\",\n\t23038: \"ED50 / UTM zone 38N\",\n\t23090: \"ED50 / TM 0 N\",\n\t23095: \"ED50 / TM 5 NE\",\n\t23239: \"Fahud / UTM zone 39N\",\n\t23240: \"Fahud / UTM zone 40N\",\n\t23433: \"Garoua / UTM zone 33N\",\n\t23700: \"HD72 / EOV\",\n\t23830: \"DGN95 / Indonesia TM-3 zone 46.2\",\n\t23831: \"DGN95 / Indonesia TM-3 zone 47.1\",\n\t23832: \"DGN95 / Indonesia TM-3 zone 47.2\",\n\t23833: \"DGN95 / Indonesia TM-3 zone 48.1\",\n\t23834: \"DGN95 / Indonesia TM-3 zone 48.2\",\n\t23835: \"DGN95 / Indonesia TM-3 zone 49.1\",\n\t23836: \"DGN95 / Indonesia TM-3 zone 49.2\",\n\t23837: \"DGN95 / Indonesia TM-3 zone 50.1\",\n\t23838: \"DGN95 / Indonesia TM-3 zone 50.2\",\n\t23839: \"DGN95 / Indonesia TM-3 zone 51.1\",\n\t23840: \"DGN95 / Indonesia TM-3 zone 51.2\",\n\t23841: \"DGN95 / Indonesia TM-3 zone 52.1\",\n\t23842: \"DGN95 / Indonesia TM-3 zone 52.2\",\n\t23843: \"DGN95 / Indonesia TM-3 zone 53.1\",\n\t23844: \"DGN95 / Indonesia TM-3 zone 53.2\",\n\t23845: \"DGN95 / Indonesia TM-3 zone 54.1\",\n\t23846: \"ID74 / UTM zone 46N\",\n\t23847: \"ID74 / UTM zone 47N\",\n\t23848: \"ID74 / UTM zone 48N\",\n\t23849: \"ID74 / UTM zone 49N\",\n\t23850: \"ID74 / UTM zone 50N\",\n\t23851: \"ID74 / UTM zone 51N\",\n\t23852: \"ID74 / UTM zone 52N\",\n\t23853: \"ID74 / UTM zone 53N\",\n\t23866: \"DGN95 / UTM zone 46N\",\n\t23867: \"DGN95 / UTM zone 47N\",\n\t23868: \"DGN95 / UTM zone 48N\",\n\t23869: \"DGN95 / UTM zone 49N\",\n\t23870: \"DGN95 / UTM zone 50N\",\n\t23871: \"DGN95 / UTM zone 51N\",\n\t23872: \"DGN95 / UTM zone 52N\",\n\t23877: \"DGN95 / UTM zone 47S\",\n\t23878: \"DGN95 / UTM zone 48S\",\n\t23879: \"DGN95 / UTM zone 49S\",\n\t23880: \"DGN95 / UTM zone 50S\",\n\t23881: \"DGN95 / UTM zone 51S\",\n\t23882: \"DGN95 / UTM zone 52S\",\n\t23883: \"DGN95 / UTM zone 53S\",\n\t23884: \"DGN95 / UTM zone 54S\",\n\t23886: \"ID74 / UTM zone 46S\",\n\t23887: \"ID74 / UTM zone 47S\",\n\t23888: \"ID74 / UTM zone 48S\",\n\t23889: \"ID74 / UTM zone 49S\",\n\t23890: \"ID74 / UTM zone 50S\",\n\t23891: \"ID74 / UTM zone 51S\",\n\t23892: \"ID74 / UTM zone 52S\",\n\t23893: \"ID74 / UTM zone 53S\",\n\t23894: \"ID74 / UTM zone 54S\",\n\t23946: \"Indian 1954 / UTM zone 46N\",\n\t23947: \"Indian 1954 / UTM zone 47N\",\n\t23948: \"Indian 1954 / UTM zone 48N\",\n\t24047: \"Indian 1975 / UTM zone 47N\",\n\t24048: \"Indian 1975 / UTM zone 48N\",\n\t24100: \"Jamaica 1875 / Jamaica (Old Grid)\",\n\t24200: \"JAD69 / Jamaica National Grid\",\n\t24305: \"Kalianpur 1937 / UTM zone 45N\",\n\t24306: \"Kalianpur 1937 / UTM zone 46N\",\n\t24311: \"Kalianpur 1962 / UTM zone 41N\",\n\t24312: \"Kalianpur 1962 / UTM zone 42N\",\n\t24313: \"Kalianpur 1962 / UTM zone 43N\",\n\t24342: \"Kalianpur 1975 / UTM zone 42N\",\n\t24343: \"Kalianpur 1975 / UTM zone 43N\",\n\t24344: \"Kalianpur 1975 / UTM zone 44N\",\n\t24345: \"Kalianpur 1975 / UTM zone 45N\",\n\t24346: \"Kalianpur 1975 / UTM zone 46N\",\n\t24347: \"Kalianpur 1975 / UTM zone 47N\",\n\t24370: \"Kalianpur 1880 / India zone 0\",\n\t24371: \"Kalianpur 1880 / India zone I\",\n\t24372: \"Kalianpur 1880 / India zone IIa\",\n\t24373: \"Kalianpur 1880 / India zone IIIa\",\n\t24374: \"Kalianpur 1880 / India zone IVa\",\n\t24375: \"Kalianpur 1937 / India zone IIb\",\n\t24376: \"Kalianpur 1962 / India zone I\",\n\t24377: \"Kalianpur 1962 / India zone IIa\",\n\t24378: \"Kalianpur 1975 / India zone I\",\n\t24379: \"Kalianpur 1975 / India zone IIa\",\n\t24380: \"Kalianpur 1975 / India zone IIb\",\n\t24381: \"Kalianpur 1975 / India zone IIIa\",\n\t24382: \"Kalianpur 1880 / India zone IIb\",\n\t24383: \"Kalianpur 1975 / India zone IVa\",\n\t24500: \"Kertau 1968 / Singapore Grid\",\n\t24547: \"Kertau 1968 / UTM zone 47N\",\n\t24548: \"Kertau 1968 / UTM zone 48N\",\n\t24571: \"Kertau / R.S.O. Malaya (ch)\",\n\t24600: \"KOC Lambert\",\n\t24718: \"La Canoa / UTM zone 18N\",\n\t24719: \"La Canoa / UTM zone 19N\",\n\t24720: \"La Canoa / UTM zone 20N\",\n\t24817: \"PSAD56 / UTM zone 17N\",\n\t24818: \"PSAD56 / UTM zone 18N\",\n\t24819: \"PSAD56 / UTM zone 19N\",\n\t24820: \"PSAD56 / UTM zone 20N\",\n\t24821: \"PSAD56 / UTM zone 21N\",\n\t24877: \"PSAD56 / UTM zone 17S\",\n\t24878: \"PSAD56 / UTM zone 18S\",\n\t24879: \"PSAD56 / UTM zone 19S\",\n\t24880: \"PSAD56 / UTM zone 20S\",\n\t24881: \"PSAD56 / UTM zone 21S\",\n\t24882: \"PSAD56 / UTM zone 22S\",\n\t24891: \"PSAD56 / Peru west zone\",\n\t24892: \"PSAD56 / Peru central zone\",\n\t24893: \"PSAD56 / Peru east zone\",\n\t25000: \"Leigon / Ghana Metre Grid\",\n\t25231: \"Lome / UTM zone 31N\",\n\t25391: \"Luzon 1911 / Philippines zone I\",\n\t25392: \"Luzon 1911 / Philippines zone II\",\n\t25393: \"Luzon 1911 / Philippines zone III\",\n\t25394: \"Luzon 1911 / Philippines zone IV\",\n\t25395: \"Luzon 1911 / Philippines zone V\",\n\t25700: \"Makassar (Jakarta) / NEIEZ\",\n\t25828: \"ETRS89 / UTM zone 28N\",\n\t25829: \"ETRS89 / UTM zone 29N\",\n\t25830: \"ETRS89 / UTM zone 30N\",\n\t25831: \"ETRS89 / UTM zone 31N\",\n\t25832: \"ETRS89 / UTM zone 32N\",\n\t25833: \"ETRS89 / UTM zone 33N\",\n\t25834: \"ETRS89 / UTM zone 34N\",\n\t25835: \"ETRS89 / UTM zone 35N\",\n\t25836: \"ETRS89 / UTM zone 36N\",\n\t25837: \"ETRS89 / UTM zone 37N\",\n\t25838: \"ETRS89 / UTM zone 38N\",\n\t25884: \"ETRS89 / TM Baltic93\",\n\t25932: \"Malongo 1987 / UTM zone 32S\",\n\t26191: \"Merchich / Nord Maroc\",\n\t26192: \"Merchich / Sud Maroc\",\n\t26193: \"Merchich / Sahara\",\n\t26194: \"Merchich / Sahara Nord\",\n\t26195: \"Merchich / Sahara Sud\",\n\t26237: \"Massawa / UTM zone 37N\",\n\t26331: \"Minna / UTM zone 31N\",\n\t26332: \"Minna / UTM zone 32N\",\n\t26391: \"Minna / Nigeria West Belt\",\n\t26392: \"Minna / Nigeria Mid Belt\",\n\t26393: \"Minna / Nigeria East Belt\",\n\t26432: \"Mhast / UTM zone 32S\",\n\t26591: \"Monte Mario (Rome) / Italy zone 1\",\n\t26592: \"Monte Mario (Rome) / Italy zone 2\",\n\t26632: \"M'poraloko / UTM zone 32N\",\n\t26692: \"M'poraloko / UTM zone 32S\",\n\t26701: \"NAD27 / UTM zone 1N\",\n\t26702: \"NAD27 / UTM zone 2N\",\n\t26703: \"NAD27 / UTM zone 3N\",\n\t26704: \"NAD27 / UTM zone 4N\",\n\t26705: \"NAD27 / UTM zone 5N\",\n\t26706: \"NAD27 / UTM zone 6N\",\n\t26707: \"NAD27 / UTM zone 7N\",\n\t26708: \"NAD27 / UTM zone 8N\",\n\t26709: \"NAD27 / UTM zone 9N\",\n\t26710: \"NAD27 / UTM zone 10N\",\n\t26711: \"NAD27 / UTM zone 11N\",\n\t26712: \"NAD27 / UTM zone 12N\",\n\t26713: \"NAD27 / UTM zone 13N\",\n\t26714: \"NAD27 / UTM zone 14N\",\n\t26715: \"NAD27 / UTM zone 15N\",\n\t26716: \"NAD27 / UTM zone 16N\",\n\t26717: \"NAD27 / UTM zone 17N\",\n\t26718: \"NAD27 / UTM zone 18N\",\n\t26719: \"NAD27 / UTM zone 19N\",\n\t26720: \"NAD27 / UTM zone 20N\",\n\t26721: \"NAD27 / UTM zone 21N\",\n\t26722: \"NAD27 / UTM zone 22N\",\n\t26729: \"NAD27 / Alabama East\",\n\t26730: \"NAD27 / Alabama West\",\n\t26731: \"NAD27 / Alaska zone 1\",\n\t26732: \"NAD27 / Alaska zone 2\",\n\t26733: \"NAD27 / Alaska zone 3\",\n\t26734: \"NAD27 / Alaska zone 4\",\n\t26735: \"NAD27 / Alaska zone 5\",\n\t26736: \"NAD27 / Alaska zone 6\",\n\t26737: \"NAD27 / Alaska zone 7\",\n\t26738: \"NAD27 / Alaska zone 8\",\n\t26739: \"NAD27 / Alaska zone 9\",\n\t26740: \"NAD27 / Alaska zone 10\",\n\t26741: \"NAD27 / California zone I\",\n\t26742: \"NAD27 / California zone II\",\n\t26743: \"NAD27 / California zone III\",\n\t26744: \"NAD27 / California zone IV\",\n\t26745: \"NAD27 / California zone V\",\n\t26746: \"NAD27 / California zone VI\",\n\t26747: \"NAD27 / California zone VII\",\n\t26748: \"NAD27 / Arizona East\",\n\t26749: \"NAD27 / Arizona Central\",\n\t26750: \"NAD27 / Arizona West\",\n\t26751: \"NAD27 / Arkansas North\",\n\t26752: \"NAD27 / Arkansas South\",\n\t26753: \"NAD27 / Colorado North\",\n\t26754: \"NAD27 / Colorado Central\",\n\t26755: \"NAD27 / Colorado South\",\n\t26756: \"NAD27 / Connecticut\",\n\t26757: \"NAD27 / Delaware\",\n\t26758: \"NAD27 / Florida East\",\n\t26759: \"NAD27 / Florida West\",\n\t26760: \"NAD27 / Florida North\",\n\t26766: \"NAD27 / Georgia East\",\n\t26767: \"NAD27 / Georgia West\",\n\t26768: \"NAD27 / Idaho East\",\n\t26769: \"NAD27 / Idaho Central\",\n\t26770: \"NAD27 / Idaho West\",\n\t26771: \"NAD27 / Illinois East\",\n\t26772: \"NAD27 / Illinois West\",\n\t26773: \"NAD27 / Indiana East\",\n\t26774: \"NAD27 / Indiana West\",\n\t26775: \"NAD27 / Iowa North\",\n\t26776: \"NAD27 / Iowa South\",\n\t26777: \"NAD27 / Kansas North\",\n\t26778: \"NAD27 / Kansas South\",\n\t26779: \"NAD27 / Kentucky North\",\n\t26780: \"NAD27 / Kentucky South\",\n\t26781: \"NAD27 / Louisiana North\",\n\t26782: \"NAD27 / Louisiana South\",\n\t26783: \"NAD27 / Maine East\",\n\t26784: \"NAD27 / Maine West\",\n\t26785: \"NAD27 / Maryland\",\n\t26786: \"NAD27 / Massachusetts Mainland\",\n\t26787: \"NAD27 / Massachusetts Island\",\n\t26791: \"NAD27 / Minnesota North\",\n\t26792: \"NAD27 / Minnesota Central\",\n\t26793: \"NAD27 / Minnesota South\",\n\t26794: \"NAD27 / Mississippi East\",\n\t26795: \"NAD27 / Mississippi West\",\n\t26796: \"NAD27 / Missouri East\",\n\t26797: \"NAD27 / Missouri Central\",\n\t26798: \"NAD27 / Missouri West\",\n\t26799: \"NAD27 / California zone VII\",\n\t26801: \"NAD Michigan / Michigan East\",\n\t26802: \"NAD Michigan / Michigan Old Central\",\n\t26803: \"NAD Michigan / Michigan West\",\n\t26811: \"NAD Michigan / Michigan North\",\n\t26812: \"NAD Michigan / Michigan Central\",\n\t26813: \"NAD Michigan / Michigan South\",\n\t26814: \"NAD83 / Maine East (ftUS)\",\n\t26815: \"NAD83 / Maine West (ftUS)\",\n\t26819: \"NAD83 / Minnesota North (ftUS)\",\n\t26820: \"NAD83 / Minnesota Central (ftUS)\",\n\t26821: \"NAD83 / Minnesota South (ftUS)\",\n\t26822: \"NAD83 / Nebraska (ftUS)\",\n\t26823: \"NAD83 / West Virginia North (ftUS)\",\n\t26824: \"NAD83 / West Virginia South (ftUS)\",\n\t26825: \"NAD83(HARN) / Maine East (ftUS)\",\n\t26826: \"NAD83(HARN) / Maine West (ftUS)\",\n\t26830: \"NAD83(HARN) / Minnesota North (ftUS)\",\n\t26831: \"NAD83(HARN) / Minnesota Central (ftUS)\",\n\t26832: \"NAD83(HARN) / Minnesota South (ftUS)\",\n\t26833: \"NAD83(HARN) / Nebraska (ftUS)\",\n\t26834: \"NAD83(HARN) / West Virginia North (ftUS)\",\n\t26835: \"NAD83(HARN) / West Virginia South (ftUS)\",\n\t26836: \"NAD83(NSRS2007) / Maine East (ftUS)\",\n\t26837: \"NAD83(NSRS2007) / Maine West (ftUS)\",\n\t26841: \"NAD83(NSRS2007) / Minnesota North (ftUS)\",\n\t26842: \"NAD83(NSRS2007) / Minnesota Central (ftUS)\",\n\t26843: \"NAD83(NSRS2007) / Minnesota South (ftUS)\",\n\t26844: \"NAD83(NSRS2007) / Nebraska (ftUS)\",\n\t26845: \"NAD83(NSRS2007) / West Virginia North (ftUS)\",\n\t26846: \"NAD83(NSRS2007) / West Virginia South (ftUS)\",\n\t26847: \"NAD83 / Maine East (ftUS)\",\n\t26848: \"NAD83 / Maine West (ftUS)\",\n\t26849: \"NAD83 / Minnesota North (ftUS)\",\n\t26850: \"NAD83 / Minnesota Central (ftUS)\",\n\t26851: \"NAD83 / Minnesota South (ftUS)\",\n\t26852: \"NAD83 / Nebraska (ftUS)\",\n\t26853: \"NAD83 / West Virginia North (ftUS)\",\n\t26854: \"NAD83 / West Virginia South (ftUS)\",\n\t26855: \"NAD83(HARN) / Maine East (ftUS)\",\n\t26856: \"NAD83(HARN) / Maine West (ftUS)\",\n\t26857: \"NAD83(HARN) / Minnesota North (ftUS)\",\n\t26858: \"NAD83(HARN) / Minnesota Central (ftUS)\",\n\t26859: \"NAD83(HARN) / Minnesota South (ftUS)\",\n\t26860: \"NAD83(HARN) / Nebraska (ftUS)\",\n\t26861: \"NAD83(HARN) / West Virginia North (ftUS)\",\n\t26862: \"NAD83(HARN) / West Virginia South (ftUS)\",\n\t26863: \"NAD83(NSRS2007) / Maine East (ftUS)\",\n\t26864: \"NAD83(NSRS2007) / Maine West (ftUS)\",\n\t26865: \"NAD83(NSRS2007) / Minnesota North (ftUS)\",\n\t26866: \"NAD83(NSRS2007) / Minnesota Central (ftUS)\",\n\t26867: \"NAD83(NSRS2007) / Minnesota South (ftUS)\",\n\t26868: \"NAD83(NSRS2007) / Nebraska (ftUS)\",\n\t26869: \"NAD83(NSRS2007) / West Virginia North (ftUS)\",\n\t26870: \"NAD83(NSRS2007) / West Virginia South (ftUS)\",\n\t26891: \"NAD83(CSRS) / MTM zone 11\",\n\t26892: \"NAD83(CSRS) / MTM zone 12\",\n\t26893: \"NAD83(CSRS) / MTM zone 13\",\n\t26894: \"NAD83(CSRS) / MTM zone 14\",\n\t26895: \"NAD83(CSRS) / MTM zone 15\",\n\t26896: \"NAD83(CSRS) / MTM zone 16\",\n\t26897: \"NAD83(CSRS) / MTM zone 17\",\n\t26898: \"NAD83(CSRS) / MTM zone 1\",\n\t26899: \"NAD83(CSRS) / MTM zone 2\",\n\t26901: \"NAD83 / UTM zone 1N\",\n\t26902: \"NAD83 / UTM zone 2N\",\n\t26903: \"NAD83 / UTM zone 3N\",\n\t26904: \"NAD83 / UTM zone 4N\",\n\t26905: \"NAD83 / UTM zone 5N\",\n\t26906: \"NAD83 / UTM zone 6N\",\n\t26907: \"NAD83 / UTM zone 7N\",\n\t26908: \"NAD83 / UTM zone 8N\",\n\t26909: \"NAD83 / UTM zone 9N\",\n\t26910: \"NAD83 / UTM zone 10N\",\n\t26911: \"NAD83 / UTM zone 11N\",\n\t26912: \"NAD83 / UTM zone 12N\",\n\t26913: \"NAD83 / UTM zone 13N\",\n\t26914: \"NAD83 / UTM zone 14N\",\n\t26915: \"NAD83 / UTM zone 15N\",\n\t26916: \"NAD83 / UTM zone 16N\",\n\t26917: \"NAD83 / UTM zone 17N\",\n\t26918: \"NAD83 / UTM zone 18N\",\n\t26919: \"NAD83 / UTM zone 19N\",\n\t26920: \"NAD83 / UTM zone 20N\",\n\t26921: \"NAD83 / UTM zone 21N\",\n\t26922: \"NAD83 / UTM zone 22N\",\n\t26923: \"NAD83 / UTM zone 23N\",\n\t26929: \"NAD83 / Alabama East\",\n\t26930: \"NAD83 / Alabama West\",\n\t26931: \"NAD83 / Alaska zone 1\",\n\t26932: \"NAD83 / Alaska zone 2\",\n\t26933: \"NAD83 / Alaska zone 3\",\n\t26934: \"NAD83 / Alaska zone 4\",\n\t26935: \"NAD83 / Alaska zone 5\",\n\t26936: \"NAD83 / Alaska zone 6\",\n\t26937: \"NAD83 / Alaska zone 7\",\n\t26938: \"NAD83 / Alaska zone 8\",\n\t26939: \"NAD83 / Alaska zone 9\",\n\t26940: \"NAD83 / Alaska zone 10\",\n\t26941: \"NAD83 / California zone 1\",\n\t26942: \"NAD83 / California zone 2\",\n\t26943: \"NAD83 / California zone 3\",\n\t26944: \"NAD83 / California zone 4\",\n\t26945: \"NAD83 / California zone 5\",\n\t26946: \"NAD83 / California zone 6\",\n\t26948: \"NAD83 / Arizona East\",\n\t26949: \"NAD83 / Arizona Central\",\n\t26950: \"NAD83 / Arizona West\",\n\t26951: \"NAD83 / Arkansas North\",\n\t26952: \"NAD83 / Arkansas South\",\n\t26953: \"NAD83 / Colorado North\",\n\t26954: \"NAD83 / Colorado Central\",\n\t26955: \"NAD83 / Colorado South\",\n\t26956: \"NAD83 / Connecticut\",\n\t26957: \"NAD83 / Delaware\",\n\t26958: \"NAD83 / Florida East\",\n\t26959: \"NAD83 / Florida West\",\n\t26960: \"NAD83 / Florida North\",\n\t26961: \"NAD83 / Hawaii zone 1\",\n\t26962: \"NAD83 / Hawaii zone 2\",\n\t26963: \"NAD83 / Hawaii zone 3\",\n\t26964: \"NAD83 / Hawaii zone 4\",\n\t26965: \"NAD83 / Hawaii zone 5\",\n\t26966: \"NAD83 / Georgia East\",\n\t26967: \"NAD83 / Georgia West\",\n\t26968: \"NAD83 / Idaho East\",\n\t26969: \"NAD83 / Idaho Central\",\n\t26970: \"NAD83 / Idaho West\",\n\t26971: \"NAD83 / Illinois East\",\n\t26972: \"NAD83 / Illinois West\",\n\t26973: \"NAD83 / Indiana East\",\n\t26974: \"NAD83 / Indiana West\",\n\t26975: \"NAD83 / Iowa North\",\n\t26976: \"NAD83 / Iowa South\",\n\t26977: \"NAD83 / Kansas North\",\n\t26978: \"NAD83 / Kansas South\",\n\t26979: \"NAD83 / Kentucky North\",\n\t26980: \"NAD83 / Kentucky South\",\n\t26981: \"NAD83 / Louisiana North\",\n\t26982: \"NAD83 / Louisiana South\",\n\t26983: \"NAD83 / Maine East\",\n\t26984: \"NAD83 / Maine West\",\n\t26985: \"NAD83 / Maryland\",\n\t26986: \"NAD83 / Massachusetts Mainland\",\n\t26987: \"NAD83 / Massachusetts Island\",\n\t26988: \"NAD83 / Michigan North\",\n\t26989: \"NAD83 / Michigan Central\",\n\t26990: \"NAD83 / Michigan South\",\n\t26991: \"NAD83 / Minnesota North\",\n\t26992: \"NAD83 / Minnesota Central\",\n\t26993: \"NAD83 / Minnesota South\",\n\t26994: \"NAD83 / Mississippi East\",\n\t26995: \"NAD83 / Mississippi West\",\n\t26996: \"NAD83 / Missouri East\",\n\t26997: \"NAD83 / Missouri Central\",\n\t26998: \"NAD83 / Missouri West\",\n\t27037: \"Nahrwan 1967 / UTM zone 37N\",\n\t27038: \"Nahrwan 1967 / UTM zone 38N\",\n\t27039: \"Nahrwan 1967 / UTM zone 39N\",\n\t27040: \"Nahrwan 1967 / UTM zone 40N\",\n\t27120: \"Naparima 1972 / UTM zone 20N\",\n\t27200: \"NZGD49 / New Zealand Map Grid\",\n\t27205: \"NZGD49 / Mount Eden Circuit\",\n\t27206: \"NZGD49 / Bay of Plenty Circuit\",\n\t27207: \"NZGD49 / Poverty Bay Circuit\",\n\t27208: \"NZGD49 / Hawkes Bay Circuit\",\n\t27209: \"NZGD49 / Taranaki Circuit\",\n\t27210: \"NZGD49 / Tuhirangi Circuit\",\n\t27211: \"NZGD49 / Wanganui Circuit\",\n\t27212: \"NZGD49 / Wairarapa Circuit\",\n\t27213: \"NZGD49 / Wellington Circuit\",\n\t27214: \"NZGD49 / Collingwood Circuit\",\n\t27215: \"NZGD49 / Nelson Circuit\",\n\t27216: \"NZGD49 / Karamea Circuit\",\n\t27217: \"NZGD49 / Buller Circuit\",\n\t27218: \"NZGD49 / Grey Circuit\",\n\t27219: \"NZGD49 / Amuri Circuit\",\n\t27220: \"NZGD49 / Marlborough Circuit\",\n\t27221: \"NZGD49 / Hokitika Circuit\",\n\t27222: \"NZGD49 / Okarito Circuit\",\n\t27223: \"NZGD49 / Jacksons Bay Circuit\",\n\t27224: \"NZGD49 / Mount Pleasant Circuit\",\n\t27225: \"NZGD49 / Gawler Circuit\",\n\t27226: \"NZGD49 / Timaru Circuit\",\n\t27227: \"NZGD49 / Lindis Peak Circuit\",\n\t27228: \"NZGD49 / Mount Nicholas Circuit\",\n\t27229: \"NZGD49 / Mount York Circuit\",\n\t27230: \"NZGD49 / Observation Point Circuit\",\n\t27231: \"NZGD49 / North Taieri Circuit\",\n\t27232: \"NZGD49 / Bluff Circuit\",\n\t27258: \"NZGD49 / UTM zone 58S\",\n\t27259: \"NZGD49 / UTM zone 59S\",\n\t27260: \"NZGD49 / UTM zone 60S\",\n\t27291: \"NZGD49 / North Island Grid\",\n\t27292: \"NZGD49 / South Island Grid\",\n\t27391: \"NGO 1948 (Oslo) / NGO zone I\",\n\t27392: \"NGO 1948 (Oslo) / NGO zone II\",\n\t27393: \"NGO 1948 (Oslo) / NGO zone III\",\n\t27394: \"NGO 1948 (Oslo) / NGO zone IV\",\n\t27395: \"NGO 1948 (Oslo) / NGO zone V\",\n\t27396: \"NGO 1948 (Oslo) / NGO zone VI\",\n\t27397: \"NGO 1948 (Oslo) / NGO zone VII\",\n\t27398: \"NGO 1948 (Oslo) / NGO zone VIII\",\n\t27429: \"Datum 73 / UTM zone 29N\",\n\t27492: \"Datum 73 / Modified Portuguese Grid\",\n\t27493: \"Datum 73 / Modified Portuguese Grid\",\n\t27500: \"ATF (Paris) / Nord de Guerre\",\n\t27561: \"NTF (Paris) / Lambert Nord France\",\n\t27562: \"NTF (Paris) / Lambert Centre France\",\n\t27563: \"NTF (Paris) / Lambert Sud France\",\n\t27564: \"NTF (Paris) / Lambert Corse\",\n\t27571: \"NTF (Paris) / Lambert zone I\",\n\t27572: \"NTF (Paris) / Lambert zone II\",\n\t27573: \"NTF (Paris) / Lambert zone III\",\n\t27574: \"NTF (Paris) / Lambert zone IV\",\n\t27581: \"NTF (Paris) / France I\",\n\t27582: \"NTF (Paris) / France II\",\n\t27583: \"NTF (Paris) / France III\",\n\t27584: \"NTF (Paris) / France IV\",\n\t27591: \"NTF (Paris) / Nord France\",\n\t27592: \"NTF (Paris) / Centre France\",\n\t27593: \"NTF (Paris) / Sud France\",\n\t27594: \"NTF (Paris) / Corse\",\n\t27700: \"OSGB 1936 / British National Grid\",\n\t28191: \"Palestine 1923 / Palestine Grid\",\n\t28192: \"Palestine 1923 / Palestine Belt\",\n\t28193: \"Palestine 1923 / Israeli CS Grid\",\n\t28232: \"Pointe Noire / UTM zone 32S\",\n\t28348: \"GDA94 / MGA zone 48\",\n\t28349: \"GDA94 / MGA zone 49\",\n\t28350: \"GDA94 / MGA zone 50\",\n\t28351: \"GDA94 / MGA zone 51\",\n\t28352: \"GDA94 / MGA zone 52\",\n\t28353: \"GDA94 / MGA zone 53\",\n\t28354: \"GDA94 / MGA zone 54\",\n\t28355: \"GDA94 / MGA zone 55\",\n\t28356: \"GDA94 / MGA zone 56\",\n\t28357: \"GDA94 / MGA zone 57\",\n\t28358: \"GDA94 / MGA zone 58\",\n\t28402: \"Pulkovo 1942 / Gauss-Kruger zone 2\",\n\t28403: \"Pulkovo 1942 / Gauss-Kruger zone 3\",\n\t28404: \"Pulkovo 1942 / Gauss-Kruger zone 4\",\n\t28405: \"Pulkovo 1942 / Gauss-Kruger zone 5\",\n\t28406: \"Pulkovo 1942 / Gauss-Kruger zone 6\",\n\t28407: \"Pulkovo 1942 / Gauss-Kruger zone 7\",\n\t28408: \"Pulkovo 1942 / Gauss-Kruger zone 8\",\n\t28409: \"Pulkovo 1942 / Gauss-Kruger zone 9\",\n\t28410: \"Pulkovo 1942 / Gauss-Kruger zone 10\",\n\t28411: \"Pulkovo 1942 / Gauss-Kruger zone 11\",\n\t28412: \"Pulkovo 1942 / Gauss-Kruger zone 12\",\n\t28413: \"Pulkovo 1942 / Gauss-Kruger zone 13\",\n\t28414: \"Pulkovo 1942 / Gauss-Kruger zone 14\",\n\t28415: \"Pulkovo 1942 / Gauss-Kruger zone 15\",\n\t28416: \"Pulkovo 1942 / Gauss-Kruger zone 16\",\n\t28417: \"Pulkovo 1942 / Gauss-Kruger zone 17\",\n\t28418: \"Pulkovo 1942 / Gauss-Kruger zone 18\",\n\t28419: \"Pulkovo 1942 / Gauss-Kruger zone 19\",\n\t28420: \"Pulkovo 1942 / Gauss-Kruger zone 20\",\n\t28421: \"Pulkovo 1942 / Gauss-Kruger zone 21\",\n\t28422: \"Pulkovo 1942 / Gauss-Kruger zone 22\",\n\t28423: \"Pulkovo 1942 / Gauss-Kruger zone 23\",\n\t28424: \"Pulkovo 1942 / Gauss-Kruger zone 24\",\n\t28425: \"Pulkovo 1942 / Gauss-Kruger zone 25\",\n\t28426: \"Pulkovo 1942 / Gauss-Kruger zone 26\",\n\t28427: \"Pulkovo 1942 / Gauss-Kruger zone 27\",\n\t28428: \"Pulkovo 1942 / Gauss-Kruger zone 28\",\n\t28429: \"Pulkovo 1942 / Gauss-Kruger zone 29\",\n\t28430: \"Pulkovo 1942 / Gauss-Kruger zone 30\",\n\t28431: \"Pulkovo 1942 / Gauss-Kruger zone 31\",\n\t28432: \"Pulkovo 1942 / Gauss-Kruger zone 32\",\n\t28462: \"Pulkovo 1942 / Gauss-Kruger 2N\",\n\t28463: \"Pulkovo 1942 / Gauss-Kruger 3N\",\n\t28464: \"Pulkovo 1942 / Gauss-Kruger 4N\",\n\t28465: \"Pulkovo 1942 / Gauss-Kruger 5N\",\n\t28466: \"Pulkovo 1942 / Gauss-Kruger 6N\",\n\t28467: \"Pulkovo 1942 / Gauss-Kruger 7N\",\n\t28468: \"Pulkovo 1942 / Gauss-Kruger 8N\",\n\t28469: \"Pulkovo 1942 / Gauss-Kruger 9N\",\n\t28470: \"Pulkovo 1942 / Gauss-Kruger 10N\",\n\t28471: \"Pulkovo 1942 / Gauss-Kruger 11N\",\n\t28472: \"Pulkovo 1942 / Gauss-Kruger 12N\",\n\t28473: \"Pulkovo 1942 / Gauss-Kruger 13N\",\n\t28474: \"Pulkovo 1942 / Gauss-Kruger 14N\",\n\t28475: \"Pulkovo 1942 / Gauss-Kruger 15N\",\n\t28476: \"Pulkovo 1942 / Gauss-Kruger 16N\",\n\t28477: \"Pulkovo 1942 / Gauss-Kruger 17N\",\n\t28478: \"Pulkovo 1942 / Gauss-Kruger 18N\",\n\t28479: \"Pulkovo 1942 / Gauss-Kruger 19N\",\n\t28480: \"Pulkovo 1942 / Gauss-Kruger 20N\",\n\t28481: \"Pulkovo 1942 / Gauss-Kruger 21N\",\n\t28482: \"Pulkovo 1942 / Gauss-Kruger 22N\",\n\t28483: \"Pulkovo 1942 / Gauss-Kruger 23N\",\n\t28484: \"Pulkovo 1942 / Gauss-Kruger 24N\",\n\t28485: \"Pulkovo 1942 / Gauss-Kruger 25N\",\n\t28486: \"Pulkovo 1942 / Gauss-Kruger 26N\",\n\t28487: \"Pulkovo 1942 / Gauss-Kruger 27N\",\n\t28488: \"Pulkovo 1942 / Gauss-Kruger 28N\",\n\t28489: \"Pulkovo 1942 / Gauss-Kruger 29N\",\n\t28490: \"Pulkovo 1942 / Gauss-Kruger 30N\",\n\t28491: \"Pulkovo 1942 / Gauss-Kruger 31N\",\n\t28492: \"Pulkovo 1942 / Gauss-Kruger 32N\",\n\t28600: \"Qatar 1974 / Qatar National Grid\",\n\t28991: \"Amersfoort / RD Old\",\n\t28992: \"Amersfoort / RD New\",\n\t29100: \"SAD69 / Brazil Polyconic\",\n\t29101: \"SAD69 / Brazil Polyconic\",\n\t29118: \"SAD69 / UTM zone 18N\",\n\t29119: \"SAD69 / UTM zone 19N\",\n\t29120: \"SAD69 / UTM zone 20N\",\n\t29121: \"SAD69 / UTM zone 21N\",\n\t29122: \"SAD69 / UTM zone 22N\",\n\t29168: \"SAD69 / UTM zone 18N\",\n\t29169: \"SAD69 / UTM zone 19N\",\n\t29170: \"SAD69 / UTM zone 20N\",\n\t29171: \"SAD69 / UTM zone 21N\",\n\t29172: \"SAD69 / UTM zone 22N\",\n\t29177: \"SAD69 / UTM zone 17S\",\n\t29178: \"SAD69 / UTM zone 18S\",\n\t29179: \"SAD69 / UTM zone 19S\",\n\t29180: \"SAD69 / UTM zone 20S\",\n\t29181: \"SAD69 / UTM zone 21S\",\n\t29182: \"SAD69 / UTM zone 22S\",\n\t29183: \"SAD69 / UTM zone 23S\",\n\t29184: \"SAD69 / UTM zone 24S\",\n\t29185: \"SAD69 / UTM zone 25S\",\n\t29187: \"SAD69 / UTM zone 17S\",\n\t29188: \"SAD69 / UTM zone 18S\",\n\t29189: \"SAD69 / UTM zone 19S\",\n\t29190: \"SAD69 / UTM zone 20S\",\n\t29191: \"SAD69 / UTM zone 21S\",\n\t29192: \"SAD69 / UTM zone 22S\",\n\t29193: \"SAD69 / UTM zone 23S\",\n\t29194: \"SAD69 / UTM zone 24S\",\n\t29195: \"SAD69 / UTM zone 25S\",\n\t29220: \"Sapper Hill 1943 / UTM zone 20S\",\n\t29221: \"Sapper Hill 1943 / UTM zone 21S\",\n\t29333: \"Schwarzeck / UTM zone 33S\",\n\t29371: \"Schwarzeck / Lo22/11\",\n\t29373: \"Schwarzeck / Lo22/13\",\n\t29375: \"Schwarzeck / Lo22/15\",\n\t29377: \"Schwarzeck / Lo22/17\",\n\t29379: \"Schwarzeck / Lo22/19\",\n\t29381: \"Schwarzeck / Lo22/21\",\n\t29383: \"Schwarzeck / Lo22/23\",\n\t29385: \"Schwarzeck / Lo22/25\",\n\t29635: \"Sudan / UTM zone 35N\",\n\t29636: \"Sudan / UTM zone 36N\",\n\t29700: \"Tananarive (Paris) / Laborde Grid\",\n\t29701: \"Tananarive (Paris) / Laborde Grid\",\n\t29702: \"Tananarive (Paris) / Laborde Grid approximation\",\n\t29738: \"Tananarive / UTM zone 38S\",\n\t29739: \"Tananarive / UTM zone 39S\",\n\t29849: \"Timbalai 1948 / UTM zone 49N\",\n\t29850: \"Timbalai 1948 / UTM zone 50N\",\n\t29871: \"Timbalai 1948 / RSO Borneo (ch)\",\n\t29872: \"Timbalai 1948 / RSO Borneo (ft)\",\n\t29873: \"Timbalai 1948 / RSO Borneo (m)\",\n\t29900: \"TM65 / Irish National Grid\",\n\t29901: \"OSNI 1952 / Irish National Grid\",\n\t29902: \"TM65 / Irish Grid\",\n\t29903: \"TM75 / Irish Grid\",\n\t30161: \"Tokyo / Japan Plane Rectangular CS I\",\n\t30162: \"Tokyo / Japan Plane Rectangular CS II\",\n\t30163: \"Tokyo / Japan Plane Rectangular CS III\",\n\t30164: \"Tokyo / Japan Plane Rectangular CS IV\",\n\t30165: \"Tokyo / Japan Plane Rectangular CS V\",\n\t30166: \"Tokyo / Japan Plane Rectangular CS VI\",\n\t30167: \"Tokyo / Japan Plane Rectangular CS VII\",\n\t30168: \"Tokyo / Japan Plane Rectangular CS VIII\",\n\t30169: \"Tokyo / Japan Plane Rectangular CS IX\",\n\t30170: \"Tokyo / Japan Plane Rectangular CS X\",\n\t30171: \"Tokyo / Japan Plane Rectangular CS XI\",\n\t30172: \"Tokyo / Japan Plane Rectangular CS XII\",\n\t30173: \"Tokyo / Japan Plane Rectangular CS XIII\",\n\t30174: \"Tokyo / Japan Plane Rectangular CS XIV\",\n\t30175: \"Tokyo / Japan Plane Rectangular CS XV\",\n\t30176: \"Tokyo / Japan Plane Rectangular CS XVI\",\n\t30177: \"Tokyo / Japan Plane Rectangular CS XVII\",\n\t30178: \"Tokyo / Japan Plane Rectangular CS XVIII\",\n\t30179: \"Tokyo / Japan Plane Rectangular CS XIX\",\n\t30200: \"Trinidad 1903 / Trinidad Grid\",\n\t30339: \"TC(1948) / UTM zone 39N\",\n\t30340: \"TC(1948) / UTM zone 40N\",\n\t30491: \"Voirol 1875 / Nord Algerie (ancienne)\",\n\t30492: \"Voirol 1875 / Sud Algerie (ancienne)\",\n\t30493: \"Voirol 1879 / Nord Algerie (ancienne)\",\n\t30494: \"Voirol 1879 / Sud Algerie (ancienne)\",\n\t30729: \"Nord Sahara 1959 / UTM zone 29N\",\n\t30730: \"Nord Sahara 1959 / UTM zone 30N\",\n\t30731: \"Nord Sahara 1959 / UTM zone 31N\",\n\t30732: \"Nord Sahara 1959 / UTM zone 32N\",\n\t30791: \"Nord Sahara 1959 / Nord Algerie\",\n\t30792: \"Nord Sahara 1959 / Sud Algerie\",\n\t30800: \"RT38 2.5 gon W\",\n\t31028: \"Yoff / UTM zone 28N\",\n\t31121: \"Zanderij / UTM zone 21N\",\n\t31154: \"Zanderij / TM 54 NW\",\n\t31170: \"Zanderij / Suriname Old TM\",\n\t31171: \"Zanderij / Suriname TM\",\n\t31251: \"MGI (Ferro) / Austria GK West Zone\",\n\t31252: \"MGI (Ferro) / Austria GK Central Zone\",\n\t31253: \"MGI (Ferro) / Austria GK East Zone\",\n\t31254: \"MGI / Austria GK West\",\n\t31255: \"MGI / Austria GK Central\",\n\t31256: \"MGI / Austria GK East\",\n\t31257: \"MGI / Austria GK M28\",\n\t31258: \"MGI / Austria GK M31\",\n\t31259: \"MGI / Austria GK M34\",\n\t31265: \"MGI / 3-degree Gauss zone 5\",\n\t31266: \"MGI / 3-degree Gauss zone 6\",\n\t31267: \"MGI / 3-degree Gauss zone 7\",\n\t31268: \"MGI / 3-degree Gauss zone 8\",\n\t31275: \"MGI / Balkans zone 5\",\n\t31276: \"MGI / Balkans zone 6\",\n\t31277: \"MGI / Balkans zone 7\",\n\t31278: \"MGI / Balkans zone 8\",\n\t31279: \"MGI / Balkans zone 8\",\n\t31281: \"MGI (Ferro) / Austria West Zone\",\n\t31282: \"MGI (Ferro) / Austria Central Zone\",\n\t31283: \"MGI (Ferro) / Austria East Zone\",\n\t31284: \"MGI / Austria M28\",\n\t31285: \"MGI / Austria M31\",\n\t31286: \"MGI / Austria M34\",\n\t31287: \"MGI / Austria Lambert\",\n\t31288: \"MGI (Ferro) / M28\",\n\t31289: \"MGI (Ferro) / M31\",\n\t31290: \"MGI (Ferro) / M34\",\n\t31291: \"MGI (Ferro) / Austria West Zone\",\n\t31292: \"MGI (Ferro) / Austria Central Zone\",\n\t31293: \"MGI (Ferro) / Austria East Zone\",\n\t31294: \"MGI / M28\",\n\t31295: \"MGI / M31\",\n\t31296: \"MGI / M34\",\n\t31297: \"MGI / Austria Lambert\",\n\t31300: \"Belge 1972 / Belge Lambert 72\",\n\t31370: \"Belge 1972 / Belgian Lambert 72\",\n\t31461: \"DHDN / 3-degree Gauss zone 1\",\n\t31462: \"DHDN / 3-degree Gauss zone 2\",\n\t31463: \"DHDN / 3-degree Gauss zone 3\",\n\t31464: \"DHDN / 3-degree Gauss zone 4\",\n\t31465: \"DHDN / 3-degree Gauss zone 5\",\n\t31466: \"DHDN / 3-degree Gauss-Kruger zone 2\",\n\t31467: \"DHDN / 3-degree Gauss-Kruger zone 3\",\n\t31468: \"DHDN / 3-degree Gauss-Kruger zone 4\",\n\t31469: \"DHDN / 3-degree Gauss-Kruger zone 5\",\n\t31528: \"Conakry 1905 / UTM zone 28N\",\n\t31529: \"Conakry 1905 / UTM zone 29N\",\n\t31600: \"Dealul Piscului 1930 / Stereo 33\",\n\t31700: \"Dealul Piscului 1970/ Stereo 70\",\n\t31838: \"NGN / UTM zone 38N\",\n\t31839: \"NGN / UTM zone 39N\",\n\t31900: \"KUDAMS / KTM\",\n\t31901: \"KUDAMS / KTM\",\n\t31965: \"SIRGAS 2000 / UTM zone 11N\",\n\t31966: \"SIRGAS 2000 / UTM zone 12N\",\n\t31967: \"SIRGAS 2000 / UTM zone 13N\",\n\t31968: \"SIRGAS 2000 / UTM zone 14N\",\n\t31969: \"SIRGAS 2000 / UTM zone 15N\",\n\t31970: \"SIRGAS 2000 / UTM zone 16N\",\n\t31971: \"SIRGAS 2000 / UTM zone 17N\",\n\t31972: \"SIRGAS 2000 / UTM zone 18N\",\n\t31973: \"SIRGAS 2000 / UTM zone 19N\",\n\t31974: \"SIRGAS 2000 / UTM zone 20N\",\n\t31975: \"SIRGAS 2000 / UTM zone 21N\",\n\t31976: \"SIRGAS 2000 / UTM zone 22N\",\n\t31977: \"SIRGAS 2000 / UTM zone 17S\",\n\t31978: \"SIRGAS 2000 / UTM zone 18S\",\n\t31979: \"SIRGAS 2000 / UTM zone 19S\",\n\t31980: \"SIRGAS 2000 / UTM zone 20S\",\n\t31981: \"SIRGAS 2000 / UTM zone 21S\",\n\t31982: \"SIRGAS 2000 / UTM zone 22S\",\n\t31983: \"SIRGAS 2000 / UTM zone 23S\",\n\t31984: \"SIRGAS 2000 / UTM zone 24S\",\n\t31985: \"SIRGAS 2000 / UTM zone 25S\",\n\t31986: \"SIRGAS 1995 / UTM zone 17N\",\n\t31987: \"SIRGAS 1995 / UTM zone 18N\",\n\t31988: \"SIRGAS 1995 / UTM zone 19N\",\n\t31989: \"SIRGAS 1995 / UTM zone 20N\",\n\t31990: \"SIRGAS 1995 / UTM zone 21N\",\n\t31991: \"SIRGAS 1995 / UTM zone 22N\",\n\t31992: \"SIRGAS 1995 / UTM zone 17S\",\n\t31993: \"SIRGAS 1995 / UTM zone 18S\",\n\t31994: \"SIRGAS 1995 / UTM zone 19S\",\n\t31995: \"SIRGAS 1995 / UTM zone 20S\",\n\t31996: \"SIRGAS 1995 / UTM zone 21S\",\n\t31997: \"SIRGAS 1995 / UTM zone 22S\",\n\t31998: \"SIRGAS 1995 / UTM zone 23S\",\n\t31999: \"SIRGAS 1995 / UTM zone 24S\",\n\t32000: \"SIRGAS 1995 / UTM zone 25S\",\n\t32001: \"NAD27 / Montana North\",\n\t32002: \"NAD27 / Montana Central\",\n\t32003: \"NAD27 / Montana South\",\n\t32005: \"NAD27 / Nebraska North\",\n\t32006: \"NAD27 / Nebraska South\",\n\t32007: \"NAD27 / Nevada East\",\n\t32008: \"NAD27 / Nevada Central\",\n\t32009: \"NAD27 / Nevada West\",\n\t32010: \"NAD27 / New Hampshire\",\n\t32011: \"NAD27 / New Jersey\",\n\t32012: \"NAD27 / New Mexico East\",\n\t32013: \"NAD27 / New Mexico Central\",\n\t32014: \"NAD27 / New Mexico West\",\n\t32015: \"NAD27 / New York East\",\n\t32016: \"NAD27 / New York Central\",\n\t32017: \"NAD27 / New York West\",\n\t32018: \"NAD27 / New York Long Island\",\n\t32019: \"NAD27 / North Carolina\",\n\t32020: \"NAD27 / North Dakota North\",\n\t32021: \"NAD27 / North Dakota South\",\n\t32022: \"NAD27 / Ohio North\",\n\t32023: \"NAD27 / Ohio South\",\n\t32024: \"NAD27 / Oklahoma North\",\n\t32025: \"NAD27 / Oklahoma South\",\n\t32026: \"NAD27 / Oregon North\",\n\t32027: \"NAD27 / Oregon South\",\n\t32028: \"NAD27 / Pennsylvania North\",\n\t32029: \"NAD27 / Pennsylvania South\",\n\t32030: \"NAD27 / Rhode Island\",\n\t32031: \"NAD27 / South Carolina North\",\n\t32033: \"NAD27 / South Carolina South\",\n\t32034: \"NAD27 / South Dakota North\",\n\t32035: \"NAD27 / South Dakota South\",\n\t32036: \"NAD27 / Tennessee\",\n\t32037: \"NAD27 / Texas North\",\n\t32038: \"NAD27 / Texas North Central\",\n\t32039: \"NAD27 / Texas Central\",\n\t32040: \"NAD27 / Texas South Central\",\n\t32041: \"NAD27 / Texas South\",\n\t32042: \"NAD27 / Utah North\",\n\t32043: \"NAD27 / Utah Central\",\n\t32044: \"NAD27 / Utah South\",\n\t32045: \"NAD27 / Vermont\",\n\t32046: \"NAD27 / Virginia North\",\n\t32047: \"NAD27 / Virginia South\",\n\t32048: \"NAD27 / Washington North\",\n\t32049: \"NAD27 / Washington South\",\n\t32050: \"NAD27 / West Virginia North\",\n\t32051: \"NAD27 / West Virginia South\",\n\t32052: \"NAD27 / Wisconsin North\",\n\t32053: \"NAD27 / Wisconsin Central\",\n\t32054: \"NAD27 / Wisconsin South\",\n\t32055: \"NAD27 / Wyoming East\",\n\t32056: \"NAD27 / Wyoming East Central\",\n\t32057: \"NAD27 / Wyoming West Central\",\n\t32058: \"NAD27 / Wyoming West\",\n\t32061: \"NAD27 / Guatemala Norte\",\n\t32062: \"NAD27 / Guatemala Sur\",\n\t32064: \"NAD27 / BLM 14N (ftUS)\",\n\t32065: \"NAD27 / BLM 15N (ftUS)\",\n\t32066: \"NAD27 / BLM 16N (ftUS)\",\n\t32067: \"NAD27 / BLM 17N (ftUS)\",\n\t32074: \"NAD27 / BLM 14N (feet)\",\n\t32075: \"NAD27 / BLM 15N (feet)\",\n\t32076: \"NAD27 / BLM 16N (feet)\",\n\t32077: \"NAD27 / BLM 17N (feet)\",\n\t32081: \"NAD27 / MTM zone 1\",\n\t32082: \"NAD27 / MTM zone 2\",\n\t32083: \"NAD27 / MTM zone 3\",\n\t32084: \"NAD27 / MTM zone 4\",\n\t32085: \"NAD27 / MTM zone 5\",\n\t32086: \"NAD27 / MTM zone 6\",\n\t32098: \"NAD27 / Quebec Lambert\",\n\t32099: \"NAD27 / Louisiana Offshore\",\n\t32100: \"NAD83 / Montana\",\n\t32104: \"NAD83 / Nebraska\",\n\t32107: \"NAD83 / Nevada East\",\n\t32108: \"NAD83 / Nevada Central\",\n\t32109: \"NAD83 / Nevada West\",\n\t32110: \"NAD83 / New Hampshire\",\n\t32111: \"NAD83 / New Jersey\",\n\t32112: \"NAD83 / New Mexico East\",\n\t32113: \"NAD83 / New Mexico Central\",\n\t32114: \"NAD83 / New Mexico West\",\n\t32115: \"NAD83 / New York East\",\n\t32116: \"NAD83 / New York Central\",\n\t32117: \"NAD83 / New York West\",\n\t32118: \"NAD83 / New York Long Island\",\n\t32119: \"NAD83 / North Carolina\",\n\t32120: \"NAD83 / North Dakota North\",\n\t32121: \"NAD83 / North Dakota South\",\n\t32122: \"NAD83 / Ohio North\",\n\t32123: \"NAD83 / Ohio South\",\n\t32124: \"NAD83 / Oklahoma North\",\n\t32125: \"NAD83 / Oklahoma South\",\n\t32126: \"NAD83 / Oregon North\",\n\t32127: \"NAD83 / Oregon South\",\n\t32128: \"NAD83 / Pennsylvania North\",\n\t32129: \"NAD83 / Pennsylvania South\",\n\t32130: \"NAD83 / Rhode Island\",\n\t32133: \"NAD83 / South Carolina\",\n\t32134: \"NAD83 / South Dakota North\",\n\t32135: \"NAD83 / South Dakota South\",\n\t32136: \"NAD83 / Tennessee\",\n\t32137: \"NAD83 / Texas North\",\n\t32138: \"NAD83 / Texas North Central\",\n\t32139: \"NAD83 / Texas Central\",\n\t32140: \"NAD83 / Texas South Central\",\n\t32141: \"NAD83 / Texas South\",\n\t32142: \"NAD83 / Utah North\",\n\t32143: \"NAD83 / Utah Central\",\n\t32144: \"NAD83 / Utah South\",\n\t32145: \"NAD83 / Vermont\",\n\t32146: \"NAD83 / Virginia North\",\n\t32147: \"NAD83 / Virginia South\",\n\t32148: \"NAD83 / Washington North\",\n\t32149: \"NAD83 / Washington South\",\n\t32150: \"NAD83 / West Virginia North\",\n\t32151: \"NAD83 / West Virginia South\",\n\t32152: \"NAD83 / Wisconsin North\",\n\t32153: \"NAD83 / Wisconsin Central\",\n\t32154: \"NAD83 / Wisconsin South\",\n\t32155: \"NAD83 / Wyoming East\",\n\t32156: \"NAD83 / Wyoming East Central\",\n\t32157: \"NAD83 / Wyoming West Central\",\n\t32158: \"NAD83 / Wyoming West\",\n\t32161: \"NAD83 / Puerto Rico & Virgin Is.\",\n\t32164: \"NAD83 / BLM 14N (ftUS)\",\n\t32165: \"NAD83 / BLM 15N (ftUS)\",\n\t32166: \"NAD83 / BLM 16N (ftUS)\",\n\t32167: \"NAD83 / BLM 17N (ftUS)\",\n\t32180: \"NAD83 / SCoPQ zone 2\",\n\t32181: \"NAD83 / MTM zone 1\",\n\t32182: \"NAD83 / MTM zone 2\",\n\t32183: \"NAD83 / MTM zone 3\",\n\t32184: \"NAD83 / MTM zone 4\",\n\t32185: \"NAD83 / MTM zone 5\",\n\t32186: \"NAD83 / MTM zone 6\",\n\t32187: \"NAD83 / MTM zone 7\",\n\t32188: \"NAD83 / MTM zone 8\",\n\t32189: \"NAD83 / MTM zone 9\",\n\t32190: \"NAD83 / MTM zone 10\",\n\t32191: \"NAD83 / MTM zone 11\",\n\t32192: \"NAD83 / MTM zone 12\",\n\t32193: \"NAD83 / MTM zone 13\",\n\t32194: \"NAD83 / MTM zone 14\",\n\t32195: \"NAD83 / MTM zone 15\",\n\t32196: \"NAD83 / MTM zone 16\",\n\t32197: \"NAD83 / MTM zone 17\",\n\t32198: \"NAD83 / Quebec Lambert\",\n\t32199: \"NAD83 / Louisiana Offshore\",\n\t32201: \"WGS 72 / UTM zone 1N\",\n\t32202: \"WGS 72 / UTM zone 2N\",\n\t32203: \"WGS 72 / UTM zone 3N\",\n\t32204: \"WGS 72 / UTM zone 4N\",\n\t32205: \"WGS 72 / UTM zone 5N\",\n\t32206: \"WGS 72 / UTM zone 6N\",\n\t32207: \"WGS 72 / UTM zone 7N\",\n\t32208: \"WGS 72 / UTM zone 8N\",\n\t32209: \"WGS 72 / UTM zone 9N\",\n\t32210: \"WGS 72 / UTM zone 10N\",\n\t32211: \"WGS 72 / UTM zone 11N\",\n\t32212: \"WGS 72 / UTM zone 12N\",\n\t32213: \"WGS 72 / UTM zone 13N\",\n\t32214: \"WGS 72 / UTM zone 14N\",\n\t32215: \"WGS 72 / UTM zone 15N\",\n\t32216: \"WGS 72 / UTM zone 16N\",\n\t32217: \"WGS 72 / UTM zone 17N\",\n\t32218: \"WGS 72 / UTM zone 18N\",\n\t32219: \"WGS 72 / UTM zone 19N\",\n\t32220: \"WGS 72 / UTM zone 20N\",\n\t32221: \"WGS 72 / UTM zone 21N\",\n\t32222: \"WGS 72 / UTM zone 22N\",\n\t32223: \"WGS 72 / UTM zone 23N\",\n\t32224: \"WGS 72 / UTM zone 24N\",\n\t32225: \"WGS 72 / UTM zone 25N\",\n\t32226: \"WGS 72 / UTM zone 26N\",\n\t32227: \"WGS 72 / UTM zone 27N\",\n\t32228: \"WGS 72 / UTM zone 28N\",\n\t32229: \"WGS 72 / UTM zone 29N\",\n\t32230: \"WGS 72 / UTM zone 30N\",\n\t32231: \"WGS 72 / UTM zone 31N\",\n\t32232: \"WGS 72 / UTM zone 32N\",\n\t32233: \"WGS 72 / UTM zone 33N\",\n\t32234: \"WGS 72 / UTM zone 34N\",\n\t32235: \"WGS 72 / UTM zone 35N\",\n\t32236: \"WGS 72 / UTM zone 36N\",\n\t32237: \"WGS 72 / UTM zone 37N\",\n\t32238: \"WGS 72 / UTM zone 38N\",\n\t32239: \"WGS 72 / UTM zone 39N\",\n\t32240: \"WGS 72 / UTM zone 40N\",\n\t32241: \"WGS 72 / UTM zone 41N\",\n\t32242: \"WGS 72 / UTM zone 42N\",\n\t32243: \"WGS 72 / UTM zone 43N\",\n\t32244: \"WGS 72 / UTM zone 44N\",\n\t32245: \"WGS 72 / UTM zone 45N\",\n\t32246: \"WGS 72 / UTM zone 46N\",\n\t32247: \"WGS 72 / UTM zone 47N\",\n\t32248: \"WGS 72 / UTM zone 48N\",\n\t32249: \"WGS 72 / UTM zone 49N\",\n\t32250: \"WGS 72 / UTM zone 50N\",\n\t32251: \"WGS 72 / UTM zone 51N\",\n\t32252: \"WGS 72 / UTM zone 52N\",\n\t32253: \"WGS 72 / UTM zone 53N\",\n\t32254: \"WGS 72 / UTM zone 54N\",\n\t32255: \"WGS 72 / UTM zone 55N\",\n\t32256: \"WGS 72 / UTM zone 56N\",\n\t32257: \"WGS 72 / UTM zone 57N\",\n\t32258: \"WGS 72 / UTM zone 58N\",\n\t32259: \"WGS 72 / UTM zone 59N\",\n\t32260: \"WGS 72 / UTM zone 60N\",\n\t32301: \"WGS 72 / UTM zone 1S\",\n\t32302: \"WGS 72 / UTM zone 2S\",\n\t32303: \"WGS 72 / UTM zone 3S\",\n\t32304: \"WGS 72 / UTM zone 4S\",\n\t32305: \"WGS 72 / UTM zone 5S\",\n\t32306: \"WGS 72 / UTM zone 6S\",\n\t32307: \"WGS 72 / UTM zone 7S\",\n\t32308: \"WGS 72 / UTM zone 8S\",\n\t32309: \"WGS 72 / UTM zone 9S\",\n\t32310: \"WGS 72 / UTM zone 10S\",\n\t32311: \"WGS 72 / UTM zone 11S\",\n\t32312: \"WGS 72 / UTM zone 12S\",\n\t32313: \"WGS 72 / UTM zone 13S\",\n\t32314: \"WGS 72 / UTM zone 14S\",\n\t32315: \"WGS 72 / UTM zone 15S\",\n\t32316: \"WGS 72 / UTM zone 16S\",\n\t32317: \"WGS 72 / UTM zone 17S\",\n\t32318: \"WGS 72 / UTM zone 18S\",\n\t32319: \"WGS 72 / UTM zone 19S\",\n\t32320: \"WGS 72 / UTM zone 20S\",\n\t32321: \"WGS 72 / UTM zone 21S\",\n\t32322: \"WGS 72 / UTM zone 22S\",\n\t32323: \"WGS 72 / UTM zone 23S\",\n\t32324: \"WGS 72 / UTM zone 24S\",\n\t32325: \"WGS 72 / UTM zone 25S\",\n\t32326: \"WGS 72 / UTM zone 26S\",\n\t32327: \"WGS 72 / UTM zone 27S\",\n\t32328: \"WGS 72 / UTM zone 28S\",\n\t32329: \"WGS 72 / UTM zone 29S\",\n\t32330: \"WGS 72 / UTM zone 30S\",\n\t32331: \"WGS 72 / UTM zone 31S\",\n\t32332: \"WGS 72 / UTM zone 32S\",\n\t32333: \"WGS 72 / UTM zone 33S\",\n\t32334: \"WGS 72 / UTM zone 34S\",\n\t32335: \"WGS 72 / UTM zone 35S\",\n\t32336: \"WGS 72 / UTM zone 36S\",\n\t32337: \"WGS 72 / UTM zone 37S\",\n\t32338: \"WGS 72 / UTM zone 38S\",\n\t32339: \"WGS 72 / UTM zone 39S\",\n\t32340: \"WGS 72 / UTM zone 40S\",\n\t32341: \"WGS 72 / UTM zone 41S\",\n\t32342: \"WGS 72 / UTM zone 42S\",\n\t32343: \"WGS 72 / UTM zone 43S\",\n\t32344: \"WGS 72 / UTM zone 44S\",\n\t32345: \"WGS 72 / UTM zone 45S\",\n\t32346: \"WGS 72 / UTM zone 46S\",\n\t32347: \"WGS 72 / UTM zone 47S\",\n\t32348: \"WGS 72 / UTM zone 48S\",\n\t32349: \"WGS 72 / UTM zone 49S\",\n\t32350: \"WGS 72 / UTM zone 50S\",\n\t32351: \"WGS 72 / UTM zone 51S\",\n\t32352: \"WGS 72 / UTM zone 52S\",\n\t32353: \"WGS 72 / UTM zone 53S\",\n\t32354: \"WGS 72 / UTM zone 54S\",\n\t32355: \"WGS 72 / UTM zone 55S\",\n\t32356: \"WGS 72 / UTM zone 56S\",\n\t32357: \"WGS 72 / UTM zone 57S\",\n\t32358: \"WGS 72 / UTM zone 58S\",\n\t32359: \"WGS 72 / UTM zone 59S\",\n\t32360: \"WGS 72 / UTM zone 60S\",\n\t32401: \"WGS 72BE / UTM zone 1N\",\n\t32402: \"WGS 72BE / UTM zone 2N\",\n\t32403: \"WGS 72BE / UTM zone 3N\",\n\t32404: \"WGS 72BE / UTM zone 4N\",\n\t32405: \"WGS 72BE / UTM zone 5N\",\n\t32406: \"WGS 72BE / UTM zone 6N\",\n\t32407: \"WGS 72BE / UTM zone 7N\",\n\t32408: \"WGS 72BE / UTM zone 8N\",\n\t32409: \"WGS 72BE / UTM zone 9N\",\n\t32410: \"WGS 72BE / UTM zone 10N\",\n\t32411: \"WGS 72BE / UTM zone 11N\",\n\t32412: \"WGS 72BE / UTM zone 12N\",\n\t32413: \"WGS 72BE / UTM zone 13N\",\n\t32414: \"WGS 72BE / UTM zone 14N\",\n\t32415: \"WGS 72BE / UTM zone 15N\",\n\t32416: \"WGS 72BE / UTM zone 16N\",\n\t32417: \"WGS 72BE / UTM zone 17N\",\n\t32418: \"WGS 72BE / UTM zone 18N\",\n\t32419: \"WGS 72BE / UTM zone 19N\",\n\t32420: \"WGS 72BE / UTM zone 20N\",\n\t32421: \"WGS 72BE / UTM zone 21N\",\n\t32422: \"WGS 72BE / UTM zone 22N\",\n\t32423: \"WGS 72BE / UTM zone 23N\",\n\t32424: \"WGS 72BE / UTM zone 24N\",\n\t32425: \"WGS 72BE / UTM zone 25N\",\n\t32426: \"WGS 72BE / UTM zone 26N\",\n\t32427: \"WGS 72BE / UTM zone 27N\",\n\t32428: \"WGS 72BE / UTM zone 28N\",\n\t32429: \"WGS 72BE / UTM zone 29N\",\n\t32430: \"WGS 72BE / UTM zone 30N\",\n\t32431: \"WGS 72BE / UTM zone 31N\",\n\t32432: \"WGS 72BE / UTM zone 32N\",\n\t32433: \"WGS 72BE / UTM zone 33N\",\n\t32434: \"WGS 72BE / UTM zone 34N\",\n\t32435: \"WGS 72BE / UTM zone 35N\",\n\t32436: \"WGS 72BE / UTM zone 36N\",\n\t32437: \"WGS 72BE / UTM zone 37N\",\n\t32438: \"WGS 72BE / UTM zone 38N\",\n\t32439: \"WGS 72BE / UTM zone 39N\",\n\t32440: \"WGS 72BE / UTM zone 40N\",\n\t32441: \"WGS 72BE / UTM zone 41N\",\n\t32442: \"WGS 72BE / UTM zone 42N\",\n\t32443: \"WGS 72BE / UTM zone 43N\",\n\t32444: \"WGS 72BE / UTM zone 44N\",\n\t32445: \"WGS 72BE / UTM zone 45N\",\n\t32446: \"WGS 72BE / UTM zone 46N\",\n\t32447: \"WGS 72BE / UTM zone 47N\",\n\t32448: \"WGS 72BE / UTM zone 48N\",\n\t32449: \"WGS 72BE / UTM zone 49N\",\n\t32450: \"WGS 72BE / UTM zone 50N\",\n\t32451: \"WGS 72BE / UTM zone 51N\",\n\t32452: \"WGS 72BE / UTM zone 52N\",\n\t32453: \"WGS 72BE / UTM zone 53N\",\n\t32454: \"WGS 72BE / UTM zone 54N\",\n\t32455: \"WGS 72BE / UTM zone 55N\",\n\t32456: \"WGS 72BE / UTM zone 56N\",\n\t32457: \"WGS 72BE / UTM zone 57N\",\n\t32458: \"WGS 72BE / UTM zone 58N\",\n\t32459: \"WGS 72BE / UTM zone 59N\",\n\t32460: \"WGS 72BE / UTM zone 60N\",\n\t32501: \"WGS 72BE / UTM zone 1S\",\n\t32502: \"WGS 72BE / UTM zone 2S\",\n\t32503: \"WGS 72BE / UTM zone 3S\",\n\t32504: \"WGS 72BE / UTM zone 4S\",\n\t32505: \"WGS 72BE / UTM zone 5S\",\n\t32506: \"WGS 72BE / UTM zone 6S\",\n\t32507: \"WGS 72BE / UTM zone 7S\",\n\t32508: \"WGS 72BE / UTM zone 8S\",\n\t32509: \"WGS 72BE / UTM zone 9S\",\n\t32510: \"WGS 72BE / UTM zone 10S\",\n\t32511: \"WGS 72BE / UTM zone 11S\",\n\t32512: \"WGS 72BE / UTM zone 12S\",\n\t32513: \"WGS 72BE / UTM zone 13S\",\n\t32514: \"WGS 72BE / UTM zone 14S\",\n\t32515: \"WGS 72BE / UTM zone 15S\",\n\t32516: \"WGS 72BE / UTM zone 16S\",\n\t32517: \"WGS 72BE / UTM zone 17S\",\n\t32518: \"WGS 72BE / UTM zone 18S\",\n\t32519: \"WGS 72BE / UTM zone 19S\",\n\t32520: \"WGS 72BE / UTM zone 20S\",\n\t32521: \"WGS 72BE / UTM zone 21S\",\n\t32522: \"WGS 72BE / UTM zone 22S\",\n\t32523: \"WGS 72BE / UTM zone 23S\",\n\t32524: \"WGS 72BE / UTM zone 24S\",\n\t32525: \"WGS 72BE / UTM zone 25S\",\n\t32526: \"WGS 72BE / UTM zone 26S\",\n\t32527: \"WGS 72BE / UTM zone 27S\",\n\t32528: \"WGS 72BE / UTM zone 28S\",\n\t32529: \"WGS 72BE / UTM zone 29S\",\n\t32530: \"WGS 72BE / UTM zone 30S\",\n\t32531: \"WGS 72BE / UTM zone 31S\",\n\t32532: \"WGS 72BE / UTM zone 32S\",\n\t32533: \"WGS 72BE / UTM zone 33S\",\n\t32534: \"WGS 72BE / UTM zone 34S\",\n\t32535: \"WGS 72BE / UTM zone 35S\",\n\t32536: \"WGS 72BE / UTM zone 36S\",\n\t32537: \"WGS 72BE / UTM zone 37S\",\n\t32538: \"WGS 72BE / UTM zone 38S\",\n\t32539: \"WGS 72BE / UTM zone 39S\",\n\t32540: \"WGS 72BE / UTM zone 40S\",\n\t32541: \"WGS 72BE / UTM zone 41S\",\n\t32542: \"WGS 72BE / UTM zone 42S\",\n\t32543: \"WGS 72BE / UTM zone 43S\",\n\t32544: \"WGS 72BE / UTM zone 44S\",\n\t32545: \"WGS 72BE / UTM zone 45S\",\n\t32546: \"WGS 72BE / UTM zone 46S\",\n\t32547: \"WGS 72BE / UTM zone 47S\",\n\t32548: \"WGS 72BE / UTM zone 48S\",\n\t32549: \"WGS 72BE / UTM zone 49S\",\n\t32550: \"WGS 72BE / UTM zone 50S\",\n\t32551: \"WGS 72BE / UTM zone 51S\",\n\t32552: \"WGS 72BE / UTM zone 52S\",\n\t32553: \"WGS 72BE / UTM zone 53S\",\n\t32554: \"WGS 72BE / UTM zone 54S\",\n\t32555: \"WGS 72BE / UTM zone 55S\",\n\t32556: \"WGS 72BE / UTM zone 56S\",\n\t32557: \"WGS 72BE / UTM zone 57S\",\n\t32558: \"WGS 72BE / UTM zone 58S\",\n\t32559: \"WGS 72BE / UTM zone 59S\",\n\t32560: \"WGS 72BE / UTM zone 60S\",\n\t32600: \"WGS 84 / UTM grid system (northern hemisphere)\",\n\t32601: \"WGS 84 / UTM zone 1N\",\n\t32602: \"WGS 84 / UTM zone 2N\",\n\t32603: \"WGS 84 / UTM zone 3N\",\n\t32604: \"WGS 84 / UTM zone 4N\",\n\t32605: \"WGS 84 / UTM zone 5N\",\n\t32606: \"WGS 84 / UTM zone 6N\",\n\t32607: \"WGS 84 / UTM zone 7N\",\n\t32608: \"WGS 84 / UTM zone 8N\",\n\t32609: \"WGS 84 / UTM zone 9N\",\n\t32610: \"WGS 84 / UTM zone 10N\",\n\t32611: \"WGS 84 / UTM zone 11N\",\n\t32612: \"WGS 84 / UTM zone 12N\",\n\t32613: \"WGS 84 / UTM zone 13N\",\n\t32614: \"WGS 84 / UTM zone 14N\",\n\t32615: \"WGS 84 / UTM zone 15N\",\n\t32616: \"WGS 84 / UTM zone 16N\",\n\t32617: \"WGS 84 / UTM zone 17N\",\n\t32618: \"WGS 84 / UTM zone 18N\",\n\t32619: \"WGS 84 / UTM zone 19N\",\n\t32620: \"WGS 84 / UTM zone 20N\",\n\t32621: \"WGS 84 / UTM zone 21N\",\n\t32622: \"WGS 84 / UTM zone 22N\",\n\t32623: \"WGS 84 / UTM zone 23N\",\n\t32624: \"WGS 84 / UTM zone 24N\",\n\t32625: \"WGS 84 / UTM zone 25N\",\n\t32626: \"WGS 84 / UTM zone 26N\",\n\t32627: \"WGS 84 / UTM zone 27N\",\n\t32628: \"WGS 84 / UTM zone 28N\",\n\t32629: \"WGS 84 / UTM zone 29N\",\n\t32630: \"WGS 84 / UTM zone 30N\",\n\t32631: \"WGS 84 / UTM zone 31N\",\n\t32632: \"WGS 84 / UTM zone 32N\",\n\t32633: \"WGS 84 / UTM zone 33N\",\n\t32634: \"WGS 84 / UTM zone 34N\",\n\t32635: \"WGS 84 / UTM zone 35N\",\n\t32636: \"WGS 84 / UTM zone 36N\",\n\t32637: \"WGS 84 / UTM zone 37N\",\n\t32638: \"WGS 84 / UTM zone 38N\",\n\t32639: \"WGS 84 / UTM zone 39N\",\n\t32640: \"WGS 84 / UTM zone 40N\",\n\t32641: \"WGS 84 / UTM zone 41N\",\n\t32642: \"WGS 84 / UTM zone 42N\",\n\t32643: \"WGS 84 / UTM zone 43N\",\n\t32644: \"WGS 84 / UTM zone 44N\",\n\t32645: \"WGS 84 / UTM zone 45N\",\n\t32646: \"WGS 84 / UTM zone 46N\",\n\t32647: \"WGS 84 / UTM zone 47N\",\n\t32648: \"WGS 84 / UTM zone 48N\",\n\t32649: \"WGS 84 / UTM zone 49N\",\n\t32650: \"WGS 84 / UTM zone 50N\",\n\t32651: \"WGS 84 / UTM zone 51N\",\n\t32652: \"WGS 84 / UTM zone 52N\",\n\t32653: \"WGS 84 / UTM zone 53N\",\n\t32654: \"WGS 84 / UTM zone 54N\",\n\t32655: \"WGS 84 / UTM zone 55N\",\n\t32656: \"WGS 84 / UTM zone 56N\",\n\t32657: \"WGS 84 / UTM zone 57N\",\n\t32658: \"WGS 84 / UTM zone 58N\",\n\t32659: \"WGS 84 / UTM zone 59N\",\n\t32660: \"WGS 84 / UTM zone 60N\",\n\t32661: \"WGS 84 / UPS North (N,E)\",\n\t32662: \"WGS 84 / Plate Carree\",\n\t32663: \"WGS 84 / World Equidistant Cylindrical\",\n\t32664: \"WGS 84 / BLM 14N (ftUS)\",\n\t32665: \"WGS 84 / BLM 15N (ftUS)\",\n\t32666: \"WGS 84 / BLM 16N (ftUS)\",\n\t32667: \"WGS 84 / BLM 17N (ftUS)\",\n\t32700: \"WGS 84 / UTM grid system (southern hemisphere)\",\n\t32701: \"WGS 84 / UTM zone 1S\",\n\t32702: \"WGS 84 / UTM zone 2S\",\n\t32703: \"WGS 84 / UTM zone 3S\",\n\t32704: \"WGS 84 / UTM zone 4S\",\n\t32705: \"WGS 84 / UTM zone 5S\",\n\t32706: \"WGS 84 / UTM zone 6S\",\n\t32707: \"WGS 84 / UTM zone 7S\",\n\t32708: \"WGS 84 / UTM zone 8S\",\n\t32709: \"WGS 84 / UTM zone 9S\",\n\t32710: \"WGS 84 / UTM zone 10S\",\n\t32711: \"WGS 84 / UTM zone 11S\",\n\t32712: \"WGS 84 / UTM zone 12S\",\n\t32713: \"WGS 84 / UTM zone 13S\",\n\t32714: \"WGS 84 / UTM zone 14S\",\n\t32715: \"WGS 84 / UTM zone 15S\",\n\t32716: \"WGS 84 / UTM zone 16S\",\n\t32717: \"WGS 84 / UTM zone 17S\",\n\t32718: \"WGS 84 / UTM zone 18S\",\n\t32719: \"WGS 84 / UTM zone 19S\",\n\t32720: \"WGS 84 / UTM zone 20S\",\n\t32721: \"WGS 84 / UTM zone 21S\",\n\t32722: \"WGS 84 / UTM zone 22S\",\n\t32723: \"WGS 84 / UTM zone 23S\",\n\t32724: \"WGS 84 / UTM zone 24S\",\n\t32725: \"WGS 84 / UTM zone 25S\",\n\t32726: \"WGS 84 / UTM zone 26S\",\n\t32727: \"WGS 84 / UTM zone 27S\",\n\t32728: \"WGS 84 / UTM zone 28S\",\n\t32729: \"WGS 84 / UTM zone 29S\",\n\t32730: \"WGS 84 / UTM zone 30S\",\n\t32731: \"WGS 84 / UTM zone 31S\",\n\t32732: \"WGS 84 / UTM zone 32S\",\n\t32733: \"WGS 84 / UTM zone 33S\",\n\t32734: \"WGS 84 / UTM zone 34S\",\n\t32735: \"WGS 84 / UTM zone 35S\",\n\t32736: \"WGS 84 / UTM zone 36S\",\n\t32737: \"WGS 84 / UTM zone 37S\",\n\t32738: \"WGS 84 / UTM zone 38S\",\n\t32739: \"WGS 84 / UTM zone 39S\",\n\t32740: \"WGS 84 / UTM zone 40S\",\n\t32741: \"WGS 84 / UTM zone 41S\",\n\t32742: \"WGS 84 / UTM zone 42S\",\n\t32743: \"WGS 84 / UTM zone 43S\",\n\t32744: \"WGS 84 / UTM zone 44S\",\n\t32745: \"WGS 84 / UTM zone 45S\",\n\t32746: \"WGS 84 / UTM zone 46S\",\n\t32747: \"WGS 84 / UTM zone 47S\",\n\t32748: \"WGS 84 / UTM zone 48S\",\n\t32749: \"WGS 84 / UTM zone 49S\",\n\t32750: \"WGS 84 / UTM zone 50S\",\n\t32751: \"WGS 84 / UTM zone 51S\",\n\t32752: \"WGS 84 / UTM zone 52S\",\n\t32753: \"WGS 84 / UTM zone 53S\",\n\t32754: \"WGS 84 / UTM zone 54S\",\n\t32755: \"WGS 84 / UTM zone 55S\",\n\t32756: \"WGS 84 / UTM zone 56S\",\n\t32757: \"WGS 84 / UTM zone 57S\",\n\t32758: \"WGS 84 / UTM zone 58S\",\n\t32759: \"WGS 84 / UTM zone 59S\",\n\t32760: \"WGS 84 / UTM zone 60S\",\n\t32761: \"WGS 84 / UPS South (N,E)\",\n\t32766: \"WGS 84 / TM 36 SE\",\n\t32767: \"User-defined\"\n}\n\n\nProjectionGeoKey = {\n\t10101: \"Proj_Alabama_CS27_East\",\n\t10102: \"Proj_Alabama_CS27_West\",\n\t10131: \"Proj_Alabama_CS83_East\",\n\t10132: \"Proj_Alabama_CS83_West\",\n\t10201: \"Proj_Arizona_Coordinate_System_east\",\n\t10202: \"Proj_Arizona_Coordinate_System_Central\",\n\t10203: \"Proj_Arizona_Coordinate_System_west\",\n\t10231: \"Proj_Arizona_CS83_east\",\n\t10232: \"Proj_Arizona_CS83_Central\",\n\t10233: \"Proj_Arizona_CS83_west\",\n\t10301: \"Proj_Arkansas_CS27_North\",\n\t10302: \"Proj_Arkansas_CS27_South\",\n\t10331: \"Proj_Arkansas_CS83_North\",\n\t10332: \"Proj_Arkansas_CS83_South\",\n\t10401: \"Proj_California_CS27_I\",\n\t10402: \"Proj_California_CS27_II\",\n\t10403: \"Proj_California_CS27_III\",\n\t10404: \"Proj_California_CS27_IV\",\n\t10405: \"Proj_California_CS27_V\",\n\t10406: \"Proj_California_CS27_VI\",\n\t10407: \"Proj_California_CS27_VII\",\n\t10431: \"Proj_California_CS83_1\",\n\t10432: \"Proj_California_CS83_2\",\n\t10433: \"Proj_California_CS83_3\",\n\t10434: \"Proj_California_CS83_4\",\n\t10435: \"Proj_California_CS83_5\",\n\t10436: \"Proj_California_CS83_6\",\n\t10501: \"Proj_Colorado_CS27_North\",\n\t10502: \"Proj_Colorado_CS27_Central\",\n\t10503: \"Proj_Colorado_CS27_South\",\n\t10531: \"Proj_Colorado_CS83_North\",\n\t10532: \"Proj_Colorado_CS83_Central\",\n\t10533: \"Proj_Colorado_CS83_South\",\n\t10600: \"Proj_Connecticut_CS27\",\n\t10630: \"Proj_Connecticut_CS83\",\n\t10700: \"Proj_Delaware_CS27\",\n\t10730: \"Proj_Delaware_CS83\",\n\t10901: \"Proj_Florida_CS27_East\",\n\t10902: \"Proj_Florida_CS27_West\",\n\t10903: \"Proj_Florida_CS27_North\",\n\t10931: \"Proj_Florida_CS83_East\",\n\t10932: \"Proj_Florida_CS83_West\",\n\t10933: \"Proj_Florida_CS83_North\",\n\t11001: \"Proj_Georgia_CS27_East\",\n\t11002: \"Proj_Georgia_CS27_West\",\n\t11031: \"Proj_Georgia_CS83_East\",\n\t11032: \"Proj_Georgia_CS83_West\",\n\t11101: \"Proj_Idaho_CS27_East\",\n\t11102: \"Proj_Idaho_CS27_Central\",\n\t11103: \"Proj_Idaho_CS27_West\",\n\t11131: \"Proj_Idaho_CS83_East\",\n\t11132: \"Proj_Idaho_CS83_Central\",\n\t11133: \"Proj_Idaho_CS83_West\",\n\t11201: \"Proj_Illinois_CS27_East\",\n\t11202: \"Proj_Illinois_CS27_West\",\n\t11231: \"Proj_Illinois_CS83_East\",\n\t11232: \"Proj_Illinois_CS83_West\",\n\t11301: \"Proj_Indiana_CS27_East\",\n\t11302: \"Proj_Indiana_CS27_West\",\n\t11331: \"Proj_Indiana_CS83_East\",\n\t11332: \"Proj_Indiana_CS83_West\",\n\t11401: \"Proj_Iowa_CS27_North\",\n\t11402: \"Proj_Iowa_CS27_South\",\n\t11431: \"Proj_Iowa_CS83_North\",\n\t11432: \"Proj_Iowa_CS83_South\",\n\t11501: \"Proj_Kansas_CS27_North\",\n\t11502: \"Proj_Kansas_CS27_South\",\n\t11531: \"Proj_Kansas_CS83_North\",\n\t11532: \"Proj_Kansas_CS83_South\",\n\t11601: \"Proj_Kentucky_CS27_North\",\n\t11602: \"Proj_Kentucky_CS27_South\",\n\t11631: \"Proj_Kentucky_CS83_North\",\n\t11632: \"Proj_Kentucky_CS83_South\",\n\t11701: \"Proj_Louisiana_CS27_North\",\n\t11702: \"Proj_Louisiana_CS27_South\",\n\t11731: \"Proj_Louisiana_CS83_North\",\n\t11732: \"Proj_Louisiana_CS83_South\",\n\t11801: \"Proj_Maine_CS27_East\",\n\t11802: \"Proj_Maine_CS27_West\",\n\t11831: \"Proj_Maine_CS83_East\",\n\t11832: \"Proj_Maine_CS83_West\",\n\t11900: \"Proj_Maryland_CS27\",\n\t11930: \"Proj_Maryland_CS83\",\n\t12001: \"Proj_Massachusetts_CS27_Mainland\",\n\t12002: \"Proj_Massachusetts_CS27_Island\",\n\t12031: \"Proj_Massachusetts_CS83_Mainland\",\n\t12032: \"Proj_Massachusetts_CS83_Island\",\n\t12101: \"Proj_Michigan_State_Plane_East\",\n\t12102: \"Proj_Michigan_State_Plane_Old_Central\",\n\t12103: \"Proj_Michigan_State_Plane_West\",\n\t12111: \"Proj_Michigan_CS27_North\",\n\t12112: \"Proj_Michigan_CS27_Central\",\n\t12113: \"Proj_Michigan_CS27_South\",\n\t12141: \"Proj_Michigan_CS83_North\",\n\t12142: \"Proj_Michigan_CS83_Central\",\n\t12143: \"Proj_Michigan_CS83_South\",\n\t12201: \"Proj_Minnesota_CS27_North\",\n\t12202: \"Proj_Minnesota_CS27_Central\",\n\t12203: \"Proj_Minnesota_CS27_South\",\n\t12231: \"Proj_Minnesota_CS83_North\",\n\t12232: \"Proj_Minnesota_CS83_Central\",\n\t12233: \"Proj_Minnesota_CS83_South\",\n\t12301: \"Proj_Mississippi_CS27_East\",\n\t12302: \"Proj_Mississippi_CS27_West\",\n\t12331: \"Proj_Mississippi_CS83_East\",\n\t12332: \"Proj_Mississippi_CS83_West\",\n\t12401: \"Proj_Missouri_CS27_East\",\n\t12402: \"Proj_Missouri_CS27_Central\",\n\t12403: \"Proj_Missouri_CS27_West\",\n\t12431: \"Proj_Missouri_CS83_East\",\n\t12432: \"Proj_Missouri_CS83_Central\",\n\t12433: \"Proj_Missouri_CS83_West\",\n\t12501: \"Proj_Montana_CS27_North\",\n\t12502: \"Proj_Montana_CS27_Central\",\n\t12503: \"Proj_Montana_CS27_South\",\n\t12530: \"Proj_Montana_CS83\",\n\t12601: \"Proj_Nebraska_CS27_North\",\n\t12602: \"Proj_Nebraska_CS27_South\",\n\t12630: \"Proj_Nebraska_CS83\",\n\t12701: \"Proj_Nevada_CS27_East\",\n\t12702: \"Proj_Nevada_CS27_Central\",\n\t12703: \"Proj_Nevada_CS27_West\",\n\t12731: \"Proj_Nevada_CS83_East\",\n\t12732: \"Proj_Nevada_CS83_Central\",\n\t12733: \"Proj_Nevada_CS83_West\",\n\t12800: \"Proj_New_Hampshire_CS27\",\n\t12830: \"Proj_New_Hampshire_CS83\",\n\t12900: \"Proj_New_Jersey_CS27\",\n\t12930: \"Proj_New_Jersey_CS83\",\n\t13001: \"Proj_New_Mexico_CS27_East\",\n\t13002: \"Proj_New_Mexico_CS27_Central\",\n\t13003: \"Proj_New_Mexico_CS27_West\",\n\t13031: \"Proj_New_Mexico_CS83_East\",\n\t13032: \"Proj_New_Mexico_CS83_Central\",\n\t13033: \"Proj_New_Mexico_CS83_West\",\n\t13101: \"Proj_New_York_CS27_East\",\n\t13102: \"Proj_New_York_CS27_Central\",\n\t13103: \"Proj_New_York_CS27_West\",\n\t13104: \"Proj_New_York_CS27_Long_Island\",\n\t13131: \"Proj_New_York_CS83_East\",\n\t13132: \"Proj_New_York_CS83_Central\",\n\t13133: \"Proj_New_York_CS83_West\",\n\t13134: \"Proj_New_York_CS83_Long_Island\",\n\t13200: \"Proj_North_Carolina_CS27\",\n\t13230: \"Proj_North_Carolina_CS83\",\n\t13301: \"Proj_North_Dakota_CS27_North\",\n\t13302: \"Proj_North_Dakota_CS27_South\",\n\t13331: \"Proj_North_Dakota_CS83_North\",\n\t13332: \"Proj_North_Dakota_CS83_South\",\n\t13401: \"Proj_Ohio_CS27_North\",\n\t13402: \"Proj_Ohio_CS27_South\",\n\t13431: \"Proj_Ohio_CS83_North\",\n\t13432: \"Proj_Ohio_CS83_South\",\n\t13501: \"Proj_Oklahoma_CS27_North\",\n\t13502: \"Proj_Oklahoma_CS27_South\",\n\t13531: \"Proj_Oklahoma_CS83_North\",\n\t13532: \"Proj_Oklahoma_CS83_South\",\n\t13601: \"Proj_Oregon_CS27_North\",\n\t13602: \"Proj_Oregon_CS27_South\",\n\t13631: \"Proj_Oregon_CS83_North\",\n\t13632: \"Proj_Oregon_CS83_South\",\n\t13701: \"Proj_Pennsylvania_CS27_North\",\n\t13702: \"Proj_Pennsylvania_CS27_South\",\n\t13731: \"Proj_Pennsylvania_CS83_North\",\n\t13732: \"Proj_Pennsylvania_CS83_South\",\n\t13800: \"Proj_Rhode_Island_CS27\",\n\t13830: \"Proj_Rhode_Island_CS83\",\n\t13901: \"Proj_South_Carolina_CS27_North\",\n\t13902: \"Proj_South_Carolina_CS27_South\",\n\t13930: \"Proj_South_Carolina_CS83\",\n\t14001: \"Proj_South_Dakota_CS27_North\",\n\t14002: \"Proj_South_Dakota_CS27_South\",\n\t14031: \"Proj_South_Dakota_CS83_North\",\n\t14032: \"Proj_South_Dakota_CS83_South\",\n\t14100: \"Proj_Tennessee_CS27\",\n\t14130: \"Proj_Tennessee_CS83\",\n\t14201: \"Proj_Texas_CS27_North\",\n\t14202: \"Proj_Texas_CS27_North_Central\",\n\t14203: \"Proj_Texas_CS27_Central\",\n\t14204: \"Proj_Texas_CS27_South_Central\",\n\t14205: \"Proj_Texas_CS27_South\",\n\t14231: \"Proj_Texas_CS83_North\",\n\t14232: \"Proj_Texas_CS83_North_Central\",\n\t14233: \"Proj_Texas_CS83_Central\",\n\t14234: \"Proj_Texas_CS83_South_Central\",\n\t14235: \"Proj_Texas_CS83_South\",\n\t14301: \"Proj_Utah_CS27_North\",\n\t14302: \"Proj_Utah_CS27_Central\",\n\t14303: \"Proj_Utah_CS27_South\",\n\t14331: \"Proj_Utah_CS83_North\",\n\t14332: \"Proj_Utah_CS83_Central\",\n\t14333: \"Proj_Utah_CS83_South\",\n\t14400: \"Proj_Vermont_CS27\",\n\t14430: \"Proj_Vermont_CS83\",\n\t14501: \"Proj_Virginia_CS27_North\",\n\t14502: \"Proj_Virginia_CS27_South\",\n\t14531: \"Proj_Virginia_CS83_North\",\n\t14532: \"Proj_Virginia_CS83_South\",\n\t14601: \"Proj_Washington_CS27_North\",\n\t14602: \"Proj_Washington_CS27_South\",\n\t14631: \"Proj_Washington_CS83_North\",\n\t14632: \"Proj_Washington_CS83_South\",\n\t14701: \"Proj_West_Virginia_CS27_North\",\n\t14702: \"Proj_West_Virginia_CS27_South\",\n\t14731: \"Proj_West_Virginia_CS83_North\",\n\t14732: \"Proj_West_Virginia_CS83_South\",\n\t14801: \"Proj_Wisconsin_CS27_North\",\n\t14802: \"Proj_Wisconsin_CS27_Central\",\n\t14803: \"Proj_Wisconsin_CS27_South\",\n\t14831: \"Proj_Wisconsin_CS83_North\",\n\t14832: \"Proj_Wisconsin_CS83_Central\",\n\t14833: \"Proj_Wisconsin_CS83_South\",\n\t14901: \"Proj_Wyoming_CS27_East\",\n\t14902: \"Proj_Wyoming_CS27_East_Central\",\n\t14903: \"Proj_Wyoming_CS27_West_Central\",\n\t14904: \"Proj_Wyoming_CS27_West\",\n\t14931: \"Proj_Wyoming_CS83_East\",\n\t14932: \"Proj_Wyoming_CS83_East_Central\",\n\t14933: \"Proj_Wyoming_CS83_West_Central\",\n\t14934: \"Proj_Wyoming_CS83_West\",\n\t15001: \"Proj_Alaska_CS27_1\",\n\t15002: \"Proj_Alaska_CS27_2\",\n\t15003: \"Proj_Alaska_CS27_3\",\n\t15004: \"Proj_Alaska_CS27_4\",\n\t15005: \"Proj_Alaska_CS27_5\",\n\t15006: \"Proj_Alaska_CS27_6\",\n\t15007: \"Proj_Alaska_CS27_7\",\n\t15008: \"Proj_Alaska_CS27_8\",\n\t15009: \"Proj_Alaska_CS27_9\",\n\t15010: \"Proj_Alaska_CS27_10\",\n\t15031: \"Proj_Alaska_CS83_1\",\n\t15032: \"Proj_Alaska_CS83_2\",\n\t15033: \"Proj_Alaska_CS83_3\",\n\t15034: \"Proj_Alaska_CS83_4\",\n\t15035: \"Proj_Alaska_CS83_5\",\n\t15036: \"Proj_Alaska_CS83_6\",\n\t15037: \"Proj_Alaska_CS83_7\",\n\t15038: \"Proj_Alaska_CS83_8\",\n\t15039: \"Proj_Alaska_CS83_9\",\n\t15040: \"Proj_Alaska_CS83_10\",\n\t15101: \"Proj_Hawaii_CS27_1\",\n\t15102: \"Proj_Hawaii_CS27_2\",\n\t15103: \"Proj_Hawaii_CS27_3\",\n\t15104: \"Proj_Hawaii_CS27_4\",\n\t15105: \"Proj_Hawaii_CS27_5\",\n\t15131: \"Proj_Hawaii_CS83_1\",\n\t15132: \"Proj_Hawaii_CS83_2\",\n\t15133: \"Proj_Hawaii_CS83_3\",\n\t15134: \"Proj_Hawaii_CS83_4\",\n\t15135: \"Proj_Hawaii_CS83_5\",\n\t15201: \"Proj_Puerto_Rico_CS27\",\n\t15202: \"Proj_St_Croix\",\n\t15230: \"Proj_Puerto_Rico_Virgin_Is\",\n\t15914: \"Proj_BLM_14N_feet\",\n\t15915: \"Proj_BLM_15N_feet\",\n\t15916: \"Proj_BLM_16N_feet\",\n\t15917: \"Proj_BLM_17N_feet\",\n\t17348: \"Proj_Map_Grid_of_Australia_48\",\n\t17349: \"Proj_Map_Grid_of_Australia_49\",\n\t17350: \"Proj_Map_Grid_of_Australia_50\",\n\t17351: \"Proj_Map_Grid_of_Australia_51\",\n\t17352: \"Proj_Map_Grid_of_Australia_52\",\n\t17353: \"Proj_Map_Grid_of_Australia_53\",\n\t17354: \"Proj_Map_Grid_of_Australia_54\",\n\t17355: \"Proj_Map_Grid_of_Australia_55\",\n\t17356: \"Proj_Map_Grid_of_Australia_56\",\n\t17357: \"Proj_Map_Grid_of_Australia_57\",\n\t17358: \"Proj_Map_Grid_of_Australia_58\",\n\t17448: \"Proj_Australian_Map_Grid_48\",\n\t17449: \"Proj_Australian_Map_Grid_49\",\n\t17450: \"Proj_Australian_Map_Grid_50\",\n\t17451: \"Proj_Australian_Map_Grid_51\",\n\t17452: \"Proj_Australian_Map_Grid_52\",\n\t17453: \"Proj_Australian_Map_Grid_53\",\n\t17454: \"Proj_Australian_Map_Grid_54\",\n\t17455: \"Proj_Australian_Map_Grid_55\",\n\t17456: \"Proj_Australian_Map_Grid_56\",\n\t17457: \"Proj_Australian_Map_Grid_57\",\n\t17458: \"Proj_Australian_Map_Grid_58\",\n\t18031: \"Proj_Argentina_1\",\n\t18032: \"Proj_Argentina_2\",\n\t18033: \"Proj_Argentina_3\",\n\t18034: \"Proj_Argentina_4\",\n\t18035: \"Proj_Argentina_5\",\n\t18036: \"Proj_Argentina_6\",\n\t18037: \"Proj_Argentina_7\",\n\t18051: \"Proj_Colombia_3W\",\n\t18052: \"Proj_Colombia_Bogota\",\n\t18053: \"Proj_Colombia_3E\",\n\t18054: \"Proj_Colombia_6E\",\n\t18072: \"Proj_Egypt_Red_Belt\",\n\t18073: \"Proj_Egypt_Purple_Belt\",\n\t18074: \"Proj_Extended_Purple_Belt\",\n\t18141: \"Proj_New_Zealand_North_Island_Nat_Grid\",\n\t18142: \"Proj_New_Zealand_South_Island_Nat_Grid\",\n\t19900: \"Proj_Bahrain_Grid\",\n\t19905: \"Proj_Netherlands_E_Indies_Equatorial\",\n\t19912: \"Proj_RSO_Borneo\",\n\t32767: \"User-defined\"\n}\n"
  },
  {
    "path": "core/lib/imageio/README.md",
    "content": "This is the Python package that is installed on the user's system.\n\nIt consists of a `core` module, which implements the basis of imageio.\nThe `plugins` module contains the code to actually import/export images,\norganised in plugins.\n\nThe `freeze` module provides functionality for freezing apps that make \nuse of imageio.\n"
  },
  {
    "path": "core/lib/imageio/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2015, imageio contributors\n# imageio is distributed under the terms of the (new) BSD License.\n\n# This docstring is used at the index of the documentation pages, and\n# gets inserted into a slightly larger description (in setup.py) for\n# the page on Pypi:\n\"\"\" \nImageio is a Python library that provides an easy interface to read and\nwrite a wide range of image data, including animated images, volumetric\ndata, and scientific formats. It is cross-platform, runs on Python 2.x\nand 3.x, and is easy to install.\n\nMain website: http://imageio.github.io\n\"\"\"\n\n__version__ = '1.5' \n\n# Load some bits from core\nfrom .core import FormatManager, RETURN_BYTES  # noqa\n\n# Instantiate format manager\nformats = FormatManager()\n\n# Load the functions\nfrom .core.functions import help  # noqa\nfrom .core.functions import get_reader, get_writer  # noqa\nfrom .core.functions import imread, mimread, volread, mvolread  # noqa\nfrom .core.functions import imwrite, mimwrite, volwrite, mvolwrite  # noqa\n\n# Load function aliases\nfrom .core.functions import read, save  # noqa\nfrom .core.functions import imsave, mimsave, volsave, mvolsave  # noqa\n\n# Load all the plugins\nfrom . import plugins  # noqa\n\n# expose the show method of formats\nshow_formats = formats.show\n\n# Clean up some names\ndel FormatManager\n"
  },
  {
    "path": "core/lib/imageio/core/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2015, imageio contributors\n# Distributed under the (new) BSD License. See LICENSE.txt for more info.\n\n\"\"\" This subpackage provides the core functionality of imageio\n(everything but the plugins).\n\"\"\"\n\nfrom .util import Image, Dict, asarray, image_as_uint, urlopen  # noqa\nfrom .util import BaseProgressIndicator, StdoutProgressIndicator  # noqa\nfrom .util import string_types, text_type, binary_type, IS_PYPY  # noqa\nfrom .util import get_platform, appdata_dir, resource_dirs, has_module  # noqa\nfrom .findlib import load_lib  # noqa\nfrom .fetching import get_remote_file, InternetNotAllowedError  # noqa\nfrom .request import Request, read_n_bytes, RETURN_BYTES  # noqa\nfrom .format import Format, FormatManager  # noqa\n"
  },
  {
    "path": "core/lib/imageio/core/fetching.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2015, imageio contributors\n# Based on code from the vispy project\n# Distributed under the (new) BSD License. See LICENSE.txt for more info.\n\n\"\"\"Data downloading and reading functions\n\"\"\"\n\nfrom __future__ import absolute_import, print_function, division\n\nfrom math import log\nimport os\nfrom os import path as op\nimport sys\nimport shutil\nimport time\n\nfrom . import appdata_dir, resource_dirs\nfrom . import StdoutProgressIndicator, string_types, urlopen\n\n\nclass InternetNotAllowedError(IOError):\n    \"\"\" Plugins that need resources can just use get_remote_file(), but\n    should catch this error and silently ignore it.\n    \"\"\"\n    pass\n\n\ndef get_remote_file(fname, directory=None, force_download=False):\n    \"\"\" Get a the filename for the local version of a file from the web\n\n    Parameters\n    ----------\n    fname : str\n        The relative filename on the remote data repository to download.\n        These correspond to paths on\n        ``https://github.com/imageio/imageio-binaries/``.\n    directory : str | None\n        The directory where the file will be cached if a download was\n        required to obtain the file. By default, the appdata directory\n        is used. This is also the first directory that is checked for\n        a local version of the file.\n    force_download : bool | str\n        If True, the file will be downloaded even if a local copy exists\n        (and this copy will be overwritten). Can also be a YYYY-MM-DD date\n        to ensure a file is up-to-date (modified date of a file on disk,\n        if present, is checked).\n\n    Returns\n    -------\n    fname : str\n        The path to the file on the local system.\n    \"\"\"\n    #_url_root = 'https://github.com/imageio/imageio-binaries/raw/master/'\n    _url_root = 'https://github.com/domlysz/freeimage_bin/raw/master/'\n    url = _url_root + fname\n    fname = op.normcase(fname)  # convert to native\n    # Get dirs to look for the resource\n    directory = directory or appdata_dir('imageio')\n    dirs = resource_dirs()\n    dirs.insert(0, directory)  # Given dir has preference\n    # Try to find the resource locally\n    for dir in dirs:\n        filename = op.join(dir, fname)\n        if op.isfile(filename):\n            if not force_download:  # we're done\n                return filename\n            if isinstance(force_download, string_types):\n                ntime = time.strptime(force_download, '%Y-%m-%d')\n                ftime = time.gmtime(op.getctime(filename))\n                if ftime >= ntime:\n                    return filename\n                else:\n                    print('File older than %s, updating...' % force_download)\n                    break\n    \n    # If we get here, we're going to try to download the file\n    if os.getenv('IMAGEIO_NO_INTERNET', '').lower() in ('1', 'true', 'yes'):\n        raise InternetNotAllowedError('Will not download resource from the '\n                                      'internet because enironment variable '\n                                      'IMAGEIO_NO_INTERNET is set.')\n    # Get filename to store to and make sure the dir exists\n    filename = op.join(directory, fname)\n    if not op.isdir(op.dirname(filename)):\n        os.makedirs(op.abspath(op.dirname(filename)))\n    # let's go get the file\n    if os.getenv('CONTINUOUS_INTEGRATION', False):  # pragma: no cover\n        # On Travis, we retry a few times ...\n        for i in range(2):\n            try:\n                _fetch_file(url, filename)\n                return filename\n            except IOError:\n                time.sleep(0.5)\n        else:\n            _fetch_file(url, filename)\n            return filename\n    else:  # pragma: no cover\n        _fetch_file(url, filename)\n        return filename\n\n\ndef _fetch_file(url, file_name, print_destination=True):\n    \"\"\"Load requested file, downloading it if needed or requested\n\n    Parameters\n    ----------\n    url: string\n        The url of file to be downloaded.\n    file_name: string\n        Name, along with the path, of where downloaded file will be saved.\n    print_destination: bool, optional\n        If true, destination of where file was saved will be printed after\n        download finishes.\n    resume: bool, optional\n        If true, try to resume partially downloaded files.\n    \"\"\"\n    # Adapted from NISL:\n    # https://github.com/nisl/tutorial/blob/master/nisl/datasets.py\n    \n    print('Imageio: %r was not found on your computer; '\n          'downloading it now.' % os.path.basename(file_name))\n    \n    temp_file_name = file_name + \".part\"\n    local_file = None\n    initial_size = 0\n    errors = []\n    for tries in range(4):\n        try:\n            # Checking file size and displaying it alongside the download url\n            remote_file = urlopen(url, timeout=5.)\n            file_size = int(remote_file.headers['Content-Length'].strip())\n            size_str = _sizeof_fmt(file_size)\n            print('Try %i. Download from %s (%s)' % (tries+1, url, size_str))\n            # Downloading data (can be extended to resume if need be)\n            local_file = open(temp_file_name, \"wb\")\n            _chunk_read(remote_file, local_file, initial_size=initial_size)\n            # temp file must be closed prior to the move\n            if not local_file.closed:\n                local_file.close()\n            shutil.move(temp_file_name, file_name)\n            if print_destination is True:\n                sys.stdout.write('File saved as %s.\\n' % file_name)\n            break\n        except Exception as e:\n            errors.append(e)\n            print('Error while fetching file: %s.' % str(e))\n        finally:\n            if local_file is not None:\n                if not local_file.closed:\n                    local_file.close()\n    else:\n        raise IOError('Unable to download %r. Perhaps there is a no internet '\n                      'connection? If there is, please report this problem.' %\n                      os.path.basename(file_name))\n\n\ndef _chunk_read(response, local_file, chunk_size=8192, initial_size=0):\n    \"\"\"Download a file chunk by chunk and show advancement\n\n    Can also be used when resuming downloads over http.\n\n    Parameters\n    ----------\n    response: urllib.response.addinfourl\n        Response to the download request in order to get file size.\n    local_file: file\n        Hard disk file where data should be written.\n    chunk_size: integer, optional\n        Size of downloaded chunks. Default: 8192\n    initial_size: int, optional\n        If resuming, indicate the initial size of the file.\n    \"\"\"\n    # Adapted from NISL:\n    # https://github.com/nisl/tutorial/blob/master/nisl/datasets.py\n\n    bytes_so_far = initial_size\n    # Returns only amount left to download when resuming, not the size of the\n    # entire file\n    total_size = int(response.headers['Content-Length'].strip())\n    total_size += initial_size\n    \n    progress = StdoutProgressIndicator('Downloading')\n    progress.start('', 'bytes', total_size)\n    \n    while True:\n        chunk = response.read(chunk_size)\n        bytes_so_far += len(chunk)\n        if not chunk:\n            break\n        _chunk_write(chunk, local_file, progress)\n    progress.finish('Done')\n\n\ndef _chunk_write(chunk, local_file, progress):\n    \"\"\"Write a chunk to file and update the progress bar\"\"\"\n    local_file.write(chunk)\n    progress.increase_progress(len(chunk))\n    time.sleep(0.0001)\n\n\ndef _sizeof_fmt(num):\n    \"\"\"Turn number of bytes into human-readable str\"\"\"\n    units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB']\n    decimals = [0, 0, 1, 2, 2, 2]\n    \"\"\"Human friendly file size\"\"\"\n    if num > 1:\n        exponent = min(int(log(num, 1024)), len(units) - 1)\n        quotient = float(num) / 1024 ** exponent\n        unit = units[exponent]\n        num_decimals = decimals[exponent]\n        format_string = '{0:.%sf} {1}' % (num_decimals)\n        return format_string.format(quotient, unit)\n    return '0 bytes' if num == 0 else '1 byte'\n"
  },
  {
    "path": "core/lib/imageio/core/findlib.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2015, imageio contributors\n# Copyright (C) 2013, Zach Pincus, Almar Klein and others\n\n\"\"\" This module contains generic code to find and load a dynamic library.\n\"\"\"\n\nfrom __future__ import absolute_import, print_function, division\n\nimport os\nimport sys\nimport ctypes\n\n\nLOCALDIR = os.path.abspath(os.path.dirname(__file__))\n\n\n# More generic:\n# def get_local_lib_dirs(*libdirs):\n#     \"\"\" Get a list of existing directories that end with one of the given\n#     subdirs, and that are in the (sub)package that this modules is part of.\n#     \"\"\"\n#     dirs = []\n#     parts = __name__.split('.')\n#     for i in reversed(range(len(parts))):\n#         package_name = '.'.join(parts[:i])\n#         package = sys.modules.get(package_name, None)\n#         if package:\n#             dirs.append(os.path.abspath(os.path.dirname(package.__file__)))\n#     dirs = [os.path.join(d, sub) for sub in libdirs for d in dirs]\n#     return [d for d in dirs if os.path.isdir(d)]\n\n\ndef looks_lib(fname):\n    \"\"\" Returns True if the given filename looks like a dynamic library.\n    Based on extension, but cross-platform and more flexible. \n    \"\"\"\n    fname = fname.lower()\n    if sys.platform.startswith('win'):\n        return fname.endswith('.dll')\n    elif sys.platform.startswith('darwin'):\n        return fname.endswith('.dylib')\n    else:\n        return fname.endswith('.so') or '.so.' in fname\n\n\ndef generate_candidate_libs(lib_names, lib_dirs=None):\n    \"\"\" Generate a list of candidate filenames of what might be the dynamic\n    library corresponding with the given list of names.\n    Returns (lib_dirs, lib_paths)\n    \"\"\"\n    lib_dirs = lib_dirs or []\n    \n    # Get system dirs to search\n    sys_lib_dirs = ['/lib', \n                    '/usr/lib', \n                    '/usr/lib/x86_64-linux-gnu',\n                    '/usr/local/lib', \n                    '/opt/local/lib', ]\n    \n    # Get Python dirs to search (shared if for Pyzo)\n    py_sub_dirs = ['lib', 'DLLs', 'Library/bin', 'shared']    \n    py_lib_dirs = [os.path.join(sys.prefix, d) for d in py_sub_dirs]\n    if hasattr(sys, 'base_prefix'):\n        py_lib_dirs += [os.path.join(sys.base_prefix, d) for d in py_sub_dirs]\n    \n    # Get user dirs to search (i.e. HOME)\n    home_dir = os.path.expanduser('~')\n    user_lib_dirs = [os.path.join(home_dir, d) for d in ['lib']]\n    \n    # Select only the dirs for which a directory exists, and remove duplicates\n    potential_lib_dirs = lib_dirs + sys_lib_dirs + py_lib_dirs + user_lib_dirs\n    lib_dirs = []\n    for ld in potential_lib_dirs:\n        if os.path.isdir(ld) and ld not in lib_dirs:\n            lib_dirs.append(ld)\n    \n    # Now attempt to find libraries of that name in the given directory\n    # (case-insensitive)\n    lib_paths = []\n    for lib_dir in lib_dirs:\n        # Get files, prefer short names, last version\n        files = os.listdir(lib_dir)\n        files = reversed(sorted(files))\n        files = sorted(files, key=len)\n        for lib_name in lib_names:\n            # Test all filenames for name and ext\n            for fname in files:\n                if fname.lower().startswith(lib_name) and looks_lib(fname):\n                    lib_paths.append(os.path.join(lib_dir, fname))\n    \n    # Return (only the items which are files)\n    lib_paths = [lp for lp in lib_paths if os.path.isfile(lp)]\n    return lib_dirs, lib_paths\n\n\ndef load_lib(exact_lib_names, lib_names, lib_dirs=None):\n    \"\"\" load_lib(exact_lib_names, lib_names, lib_dirs=None)\n    \n    Load a dynamic library. \n    \n    This function first tries to load the library from the given exact\n    names. When that fails, it tries to find the library in common\n    locations. It searches for files that start with one of the names\n    given in lib_names (case insensitive). The search is performed in\n    the given lib_dirs and a set of common library dirs.\n    \n    Returns ``(ctypes_library, library_path)``\n    \"\"\"\n    \n    # Checks\n    assert isinstance(exact_lib_names, list)\n    assert isinstance(lib_names, list)\n    if lib_dirs is not None:\n        assert isinstance(lib_dirs, list)\n    exact_lib_names = [n for n in exact_lib_names if n]\n    lib_names = [n for n in lib_names if n]\n    \n    # Get reference name (for better messages)\n    if lib_names:\n        the_lib_name = lib_names[0]\n    elif exact_lib_names:\n        the_lib_name = exact_lib_names[0]\n    else:\n        raise ValueError(\"No library name given.\")\n    \n    # Collect filenames of potential libraries\n    # First try a few bare library names that ctypes might be able to find\n    # in the default locations for each platform. \n    lib_dirs, lib_paths = generate_candidate_libs(lib_names, lib_dirs)\n    lib_paths = exact_lib_names + lib_paths\n    \n    # Select loader \n    if sys.platform.startswith('win'):\n        loader = ctypes.windll\n    else:\n        loader = ctypes.cdll\n    \n    # Try to load until success\n    the_lib = None\n    errors = []\n    for fname in lib_paths:\n        try:\n            the_lib = loader.LoadLibrary(fname)\n            break\n        except Exception:\n            # Don't record errors when it couldn't load the library from an\n            # exact name -- this fails often, and doesn't provide any useful\n            # debugging information anyway, beyond \"couldn't find library...\"\n            if fname not in exact_lib_names:\n                # Get exception instance in Python 2.x/3.x compatible manner\n                e_type, e_value, e_tb = sys.exc_info()\n                del e_tb\n                errors.append((fname, e_value))\n    \n    # No success ...\n    if the_lib is None:\n        if errors:\n            # No library loaded, and load-errors reported for some\n            # candidate libs\n            err_txt = ['%s:\\n%s' % (l, str(e)) for l, e in errors]\n            msg = ('One or more %s libraries were found, but ' + \n                   'could not be loaded due to the following errors:\\n%s')\n            raise OSError(msg % (the_lib_name, '\\n\\n'.join(err_txt)))\n        else:\n            # No errors, because no potential libraries found at all!\n            msg = 'Could not find a %s library in any of:\\n%s'\n            raise OSError(msg % (the_lib_name, '\\n'.join(lib_dirs)))\n    \n    # Done\n    return the_lib, fname\n"
  },
  {
    "path": "core/lib/imageio/core/format.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2015, imageio contributors\n# imageio is distributed under the terms of the (new) BSD License.\n\n\"\"\" \n\n.. note::\n    imageio is under construction, some details with regard to the \n    Reader and Writer classes may change. \n\nThese are the main classes of imageio. They expose an interface for\nadvanced users and plugin developers. A brief overview:\n  \n  * imageio.FormatManager - for keeping track of registered formats.\n  * imageio.Format - representation of a file format reader/writer\n  * imageio.Format.Reader - object used during the reading of a file.\n  * imageio.Format.Writer - object used during saving a file.\n  * imageio.Request - used to store the filename and other info.\n\nPlugins need to implement a Format class and register\na format object using ``imageio.formats.add_format()``.\n\n\"\"\"\n\nfrom __future__ import absolute_import, print_function, division\n\n# todo: do we even use the known extensions?\n\n# Some notes:\n#\n# The classes in this module use the Request object to pass filename and\n# related info around. This request object is instantiated in\n# imageio.get_reader and imageio.get_writer.\n\nfrom __future__ import with_statement\n\nimport os\n\nimport numpy as np\n\nfrom . import Image, asarray\nfrom . import string_types, text_type, binary_type  # noqa\n\n\nclass Format:\n    \"\"\" Represents an implementation to read/write a particular file format\n    \n    A format instance is responsible for 1) providing information about\n    a format; 2) determining whether a certain file can be read/written\n    with this format; 3) providing a reader/writer class.\n    \n    Generally, imageio will select the right format and use that to\n    read/write an image. A format can also be explicitly chosen in all\n    read/write functions. Use ``print(format)``, or ``help(format_name)``\n    to see its documentation.\n    \n    To implement a specific format, one should create a subclass of\n    Format and the Format.Reader and Format.Writer classes. see\n    :doc:`plugins` for details.\n    \n    Parameters\n    ----------\n    name : str\n        A short name of this format. Users can select a format using its name.\n    description : str\n        A one-line description of the format.\n    extensions : str | list | None\n        List of filename extensions that this format supports. If a\n        string is passed it should be space or comma separated. The\n        extensions are used in the documentation and to allow users to\n        select a format by file extension. It is not used to determine\n        what format to use for reading/saving a file.\n    modes : str\n        A string containing the modes that this format can handle ('iIvV').\n        This attribute is used in the documentation and to select the\n        formats when reading/saving a file.\n    \"\"\"\n    \n    def __init__(self, name, description, extensions=None, modes=None):\n        \n        # Store name and description\n        self._name = name.upper()\n        self._description = description\n        \n        # Store extensions, do some effort to normalize them.\n        # They are stored as a list of lowercase strings without leading dots.\n        if extensions is None:\n            extensions = []\n        elif isinstance(extensions, string_types):\n            extensions = extensions.replace(',', ' ').split(' ')\n        #\n        if isinstance(extensions, (tuple, list)):\n            self._extensions = tuple(['.' + e.strip('.').lower() \n                                      for e in extensions if e])\n        else:\n            raise ValueError('Invalid value for extensions given.')\n        \n        # Store mode\n        self._modes = modes or ''\n        if not isinstance(self._modes, string_types):\n            raise ValueError('Invalid value for modes given.')\n        for m in self._modes:\n            if m not in 'iIvV?':\n                raise ValueError('Invalid value for mode given.')\n    \n    def __repr__(self):\n        # Short description\n        return '<Format %s - %s>' % (self.name, self.description)\n    \n    def __str__(self):\n        return self.doc\n    \n    @property\n    def doc(self):\n        \"\"\" The documentation for this format (name + description + docstring).\n        \"\"\"\n        # Our docsring is assumed to be indented by four spaces. The\n        # first line needs special attention.\n        return '%s - %s\\n\\n    %s\\n' % (self.name, self.description, \n                                        self.__doc__.strip())\n    \n    @property\n    def name(self):\n        \"\"\" The name of this format.\n        \"\"\"\n        return self._name\n    \n    @property\n    def description(self):\n        \"\"\" A short description of this format.\n        \"\"\" \n        return self._description\n    \n    @property\n    def extensions(self):\n        \"\"\" A list of file extensions supported by this plugin.\n        These are all lowercase with a leading dot.\n        \"\"\"\n        return self._extensions\n    \n    @property\n    def modes(self):\n        \"\"\" A string specifying the modes that this format can handle.\n        \"\"\"\n        return self._modes\n    \n    def get_reader(self, request):\n        \"\"\" get_reader(request)\n        \n        Return a reader object that can be used to read data and info\n        from the given file. Users are encouraged to use\n        imageio.get_reader() instead.\n        \"\"\"\n        select_mode = request.mode[1] if request.mode[1] in 'iIvV' else ''\n        if select_mode not in self.modes:\n            raise RuntimeError('Format %s cannot read in mode %r' % \n                               (self.name, select_mode))\n        return self.Reader(self, request)\n    \n    def get_writer(self, request):\n        \"\"\" get_writer(request)\n        \n        Return a writer object that can be used to write data and info\n        to the given file. Users are encouraged to use\n        imageio.get_writer() instead.\n        \"\"\"\n        select_mode = request.mode[1] if request.mode[1] in 'iIvV' else ''\n        if select_mode not in self.modes:\n            raise RuntimeError('Format %s cannot write in mode %r' % \n                               (self.name, select_mode))\n        return self.Writer(self, request)\n    \n    def can_read(self, request):\n        \"\"\" can_read(request)\n        \n        Get whether this format can read data from the specified uri.\n        \"\"\"\n        return self._can_read(request)\n    \n    def can_write(self, request):\n        \"\"\" can_write(request)\n        \n        Get whether this format can write data to the speciefed uri.\n        \"\"\"\n        return self._can_write(request)\n    \n    def _can_read(self, request):  # pragma: no cover\n        return None  # Plugins must implement this\n    \n    def _can_write(self, request):  # pragma: no cover\n        return None  # Plugins must implement this\n\n    # -----\n    \n    class _BaseReaderWriter(object):\n        \"\"\" Base class for the Reader and Writer class to implement common \n        functionality. It implements a similar approach for opening/closing\n        and context management as Python's file objects.\n        \"\"\"\n        \n        def __init__(self, format, request):\n            self.__closed = False\n            self._BaseReaderWriter_last_index = -1\n            self._format = format\n            self._request = request\n            # Open the reader/writer\n            self._open(**self.request.kwargs.copy())\n        \n        @property\n        def format(self):\n            \"\"\" The :class:`.Format` object corresponding to the current\n            read/write operation.\n            \"\"\"\n            return self._format\n        \n        @property\n        def request(self):\n            \"\"\" The :class:`.Request` object corresponding to the\n            current read/write operation.\n            \"\"\"\n            return self._request\n        \n        def __enter__(self):\n            self._checkClosed()\n            return self\n        \n        def __exit__(self, type, value, traceback):\n            if value is None:\n                # Otherwise error in close hide the real error.\n                self.close() \n        \n        def __del__(self):\n            try:\n                self.close()\n            except Exception:  # pragma: no cover\n                pass  # Supress noise when called during interpreter shutdown\n        \n        def close(self):\n            \"\"\" Flush and close the reader/writer.\n            This method has no effect if it is already closed.\n            \"\"\"\n            if self.__closed:\n                return\n            self.__closed = True\n            self._close()\n            # Process results and clean request object\n            self.request.finish()\n        \n        @property\n        def closed(self):\n            \"\"\" Whether the reader/writer is closed.\n            \"\"\"\n            return self.__closed\n        \n        def _checkClosed(self, msg=None):\n            \"\"\"Internal: raise an ValueError if reader/writer is closed\n            \"\"\"\n            if self.closed:\n                what = self.__class__.__name__\n                msg = msg or (\"I/O operation on closed %s.\" % what)\n                raise RuntimeError(msg)\n        \n        # To implement\n        \n        def _open(self, **kwargs):\n            \"\"\" _open(**kwargs)\n            \n            Plugins should probably implement this.\n            \n            It is called when reader/writer is created. Here the\n            plugin can do its initialization. The given keyword arguments\n            are those that were given by the user at imageio.read() or\n            imageio.write().\n            \"\"\" \n            raise NotImplementedError()\n        \n        def _close(self):\n            \"\"\" _close()\n            \n            Plugins should probably implement this.\n            \n            It is called when the reader/writer is closed. Here the plugin\n            can do a cleanup, flush, etc.\n            \n            \"\"\" \n            raise NotImplementedError()\n    \n    # -----\n    \n    class Reader(_BaseReaderWriter):\n        \"\"\"\n        The purpose of a reader object is to read data from an image\n        resource, and should be obtained by calling :func:`.get_reader`. \n        \n        A reader can be used as an iterator to read multiple images,\n        and (if the format permits) only reads data from the file when\n        new data is requested (i.e. streaming). A reader can also be\n        used as a context manager so that it is automatically closed.\n        \n        Plugins implement Reader's for different formats. Though rare,\n        plugins may provide additional functionality (beyond what is\n        provided by the base reader class).\n        \"\"\"\n        \n        def get_length(self):\n            \"\"\" get_length()\n            \n            Get the number of images in the file. (Note: you can also\n            use ``len(reader_object)``.)\n            \n            The result can be:\n                * 0 for files that only have meta data\n                * 1 for singleton images (e.g. in PNG, JPEG, etc.)\n                * N for image series\n                * inf for streams (series of unknown length)\n            \"\"\" \n            return self._get_length()\n        \n        def get_data(self, index, **kwargs):\n            \"\"\" get_data(index, **kwargs)\n            \n            Read image data from the file, using the image index. The\n            returned image has a 'meta' attribute with the meta data.\n            \n            Some formats may support additional keyword arguments. These are\n            listed in the documentation of those formats.\n            \"\"\"\n            self._checkClosed()\n            self._BaseReaderWriter_last_index = index\n            im, meta = self._get_data(index, **kwargs)\n            return Image(im, meta)  # Image tests im and meta \n        \n        def get_next_data(self, **kwargs):\n            \"\"\" get_next_data(**kwargs)\n            \n            Read the next image from the series.\n            \n            Some formats may support additional keyword arguments. These are\n            listed in the documentation of those formats.\n            \"\"\"\n            return self.get_data(self._BaseReaderWriter_last_index+1, **kwargs)\n        \n        def get_meta_data(self, index=None):\n            \"\"\" get_meta_data(index=None)\n            \n            Read meta data from the file. using the image index. If the\n            index is omitted or None, return the file's (global) meta data.\n            \n            Note that ``get_data`` also provides the meta data for the returned\n            image as an atrribute of that image.\n            \n            The meta data is a dict, which shape depends on the format.\n            E.g. for JPEG, the dict maps group names to subdicts and each\n            group is a dict with name-value pairs. The groups represent\n            the different metadata formats (EXIF, XMP, etc.).\n            \"\"\"\n            self._checkClosed()\n            meta = self._get_meta_data(index)\n            if not isinstance(meta, dict):\n                raise ValueError('Meta data must be a dict, not %r' % \n                                 meta.__class__.__name__)\n            return meta\n        \n        def iter_data(self):\n            \"\"\" iter_data()\n            \n            Iterate over all images in the series. (Note: you can also\n            iterate over the reader object.)\n            \n            \"\"\" \n            self._checkClosed()\n            i, n = 0, self.get_length()\n            while i < n:\n                try:\n                    im, meta = self._get_data(i)\n                except IndexError:\n                    if n == float('inf'):\n                        return\n                    raise\n                yield Image(im, meta)\n                i += 1\n        \n        # Compatibility\n        \n        def __iter__(self):\n            return self.iter_data()\n        \n        def __len__(self):\n            return self.get_length()\n        \n        # To implement\n        \n        def _get_length(self):\n            \"\"\" _get_length()\n            \n            Plugins must implement this.\n            \n            The retured scalar specifies the number of images in the series.\n            See Reader.get_length for more information.\n            \"\"\" \n            raise NotImplementedError() \n        \n        def _get_data(self, index):\n            \"\"\" _get_data()\n            \n            Plugins must implement this, but may raise an IndexError in\n            case the plugin does not support random access.\n            \n            It should return the image and meta data: (ndarray, dict).\n            \"\"\" \n            raise NotImplementedError() \n        \n        def _get_meta_data(self, index):\n            \"\"\" _get_meta_data(index)\n            \n            Plugins must implement this. \n            \n            It should return the meta data as a dict, corresponding to the\n            given index, or to the file's (global) meta data if index is\n            None.\n            \"\"\" \n            raise NotImplementedError() \n    \n    # -----\n    \n    class Writer(_BaseReaderWriter):\n        \"\"\" \n        The purpose of a writer object is to write data to an image\n        resource, and should be obtained by calling :func:`.get_writer`. \n        \n        A writer will (if the format permits) write data to the file\n        as soon as new data is provided (i.e. streaming). A writer can\n        also be used as a context manager so that it is automatically\n        closed.\n        \n        Plugins implement Writer's for different formats. Though rare,\n        plugins may provide additional functionality (beyond what is\n        provided by the base writer class).\n        \"\"\"\n        \n        def append_data(self, im, meta=None):\n            \"\"\" append_data(im, meta={})\n            \n            Append an image (and meta data) to the file. The final meta\n            data that is used consists of the meta data on the given\n            image (if applicable), updated with the given meta data.\n            \"\"\" \n            self._checkClosed()\n            # Check image data\n            if not isinstance(im, np.ndarray):\n                raise ValueError('append_data requires ndarray as first arg')\n            # Get total meta dict\n            total_meta = {}\n            if hasattr(im, 'meta') and isinstance(im.meta, dict):\n                total_meta.update(im.meta)\n            if meta is None:\n                pass\n            elif not isinstance(meta, dict):\n                raise ValueError('Meta must be a dict.')\n            else:\n                total_meta.update(meta)        \n            \n            # Decouple meta info\n            im = asarray(im)\n            # Call\n            return self._append_data(im, total_meta)\n        \n        def set_meta_data(self, meta):\n            \"\"\" set_meta_data(meta)\n            \n            Sets the file's (global) meta data. The meta data is a dict which\n            shape depends on the format. E.g. for JPEG the dict maps\n            group names to subdicts, and each group is a dict with\n            name-value pairs. The groups represents the different\n            metadata formats (EXIF, XMP, etc.). \n            \n            Note that some meta formats may not be supported for\n            writing, and individual fields may be ignored without\n            warning if they are invalid.\n            \"\"\" \n            self._checkClosed()\n            if not isinstance(meta, dict):\n                raise ValueError('Meta must be a dict.')\n            else:\n                return self._set_meta_data(meta)\n        \n        # To implement\n        \n        def _append_data(self, im, meta):\n            # Plugins must implement this\n            raise NotImplementedError() \n        \n        def _set_meta_data(self, meta):\n            # Plugins must implement this\n            raise NotImplementedError() \n\n\nclass FormatManager:\n    \"\"\" \n    There is exactly one FormatManager object in imageio: ``imageio.formats``.\n    Its purpose it to keep track of the registered formats.\n    \n    The format manager supports getting a format object using indexing (by \n    format name or extension). When used as an iterator, this object\n    yields all registered format objects.\n    \n    See also :func:`.help`.\n    \"\"\"\n    \n    def __init__(self):\n        self._formats = []\n    \n    def __repr__(self):\n        return '<imageio.FormatManager with %i registered formats>' % len(self)\n    \n    def __iter__(self):\n        return iter(self._formats)\n    \n    def __len__(self):\n        return len(self._formats)\n    \n    def __str__(self):\n        ss = []\n        for format in self._formats: \n            ext = ', '.join(format.extensions)\n            s = '%s - %s [%s]' % (format.name, format.description, ext)\n            ss.append(s)\n        return '\\n'.join(ss)\n    \n    def __getitem__(self, name):\n        # Check\n        if not isinstance(name, string_types):\n            raise ValueError('Looking up a format should be done by name '\n                             'or by extension.')\n        \n        # Test if name is existing file\n        if os.path.isfile(name):\n            from . import Request\n            format = self.search_read_format(Request(name, 'r?'))\n            if format is not None:\n                return format\n        \n        if '.' in name:\n            # Look for extension\n            e1, e2 = os.path.splitext(name.lower())\n            name = e2 or e1\n            # Search for format that supports this extension\n            for format in self._formats:\n                if name in format.extensions:\n                    return format\n        else:\n            # Look for name\n            name = name.upper()\n            for format in self._formats:\n                if name == format.name:\n                    return format\n            else:\n                # Maybe the user meant to specify an extension\n                return self['.'+name.lower()]\n        \n        # Nothing found ...\n        raise IndexError('No format known by name %s.' % name)\n    \n    def add_format(self, format, overwrite=False):\n        \"\"\" add_format(format, overwrite=False)\n        \n        Register a format, so that imageio can use it. If a format with the\n        same name already exists, an error is raised, unless overwrite is True,\n        in which case the current format is replaced.\n        \"\"\"\n        if not isinstance(format, Format):\n            raise ValueError('add_format needs argument to be a Format object')\n        elif format in self._formats:\n            raise ValueError('Given Format instance is already registered')\n        elif format.name in self.get_format_names():\n            if overwrite:\n                self._formats.remove(self[format.name])\n            else:\n                raise ValueError('A Format named %r is already registered, use'\n                                 ' overwrite=True to replace.' % format.name)\n        self._formats.append(format)\n    \n    def search_read_format(self, request):\n        \"\"\" search_read_format(request)\n        \n        Search a format that can read a file according to the given request.\n        Returns None if no appropriate format was found. (used internally)\n        \"\"\"\n        select_mode = request.mode[1] if request.mode[1] in 'iIvV' else ''\n        select_ext = request.filename.lower()\n        \n        # Select formats that seem to be able to read it\n        selected_formats = []\n        for format in self._formats:\n            if select_mode in format.modes:\n                if select_ext.endswith(format.extensions):\n                    selected_formats.append(format)\n        \n        # Select the first that can\n        for format in selected_formats:\n            if format.can_read(request):\n                return format\n        \n        # If no format could read it, it could be that file has no or\n        # the wrong extension. We ask all formats again.\n        for format in self._formats:\n            if format not in selected_formats:\n                if format.can_read(request):\n                    return format\n    \n    def search_write_format(self, request):\n        \"\"\" search_write_format(request)\n        \n        Search a format that can write a file according to the given request. \n        Returns None if no appropriate format was found. (used internally)\n        \"\"\"\n        select_mode = request.mode[1] if request.mode[1] in 'iIvV' else ''\n        select_ext = request.filename.lower()\n        \n        # Select formats that seem to be able to write it\n        selected_formats = []\n        for format in self._formats:\n            if select_mode in format.modes:\n                if select_ext.endswith(format.extensions):\n                    selected_formats.append(format)\n        \n        # Select the first that can\n        for format in selected_formats:\n            if format.can_write(request):\n                return format\n        \n        # If none of the selected formats could write it, maybe another\n        # format can still write it. It might prefer a different mode,\n        # or be able to handle more formats than it says by its extensions.\n        for format in self._formats:\n            if format not in selected_formats:\n                if format.can_write(request):\n                    return format\n    \n    def get_format_names(self):\n        \"\"\" Get the names of all registered formats.\n        \"\"\"\n        return [f.name for f in self._formats]\n    \n    def show(self):\n        \"\"\" Show a nicely formatted list of available formats\n        \"\"\"\n        print(self)\n"
  },
  {
    "path": "core/lib/imageio/core/functions.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2015, imageio contributors\n# imageio is distributed under the terms of the (new) BSD License.\n\n\"\"\" \nThese functions represent imageio's main interface for the user. They\nprovide a common API to read and write image data for a large\nvariety of formats. All read and write functions accept keyword\narguments, which are passed on to the format that does the actual work.\nTo see what keyword arguments are supported by a specific format, use\nthe :func:`.help` function.\n\nFunctions for reading:\n\n  * :func:`.imread` - read an image from the specified uri\n  * :func:`.mimread` - read a series of images from the specified uri\n  * :func:`.volread` - read a volume from the specified uri\n  * :func:`.mvolread` - read a series of volumes from the specified uri\n\nFunctions for saving:\n\n  * :func:`.imwrite` - write an image to the specified uri\n  * :func:`.mimwrite` - write a series of images to the specified uri\n  * :func:`.volwrite` - write a volume to the specified uri\n  * :func:`.mvolwrite` - write a series of volumes to the specified uri\n\nMore control:\n\nFor a larger degree of control, imageio provides functions\n:func:`.get_reader` and :func:`.get_writer`. They respectively return an\n:class:`.Reader` and an :class:`.Writer` object, which can\nbe used to read/write data and meta data in a more controlled manner.\nThis also allows specific scientific formats to be exposed in a way\nthat best suits that file-format.\n\n\n.. note::\n    \n    Some of these functions were renamed in v1.1 to realize a more clear\n    and consistent API. The old functions are still available for\n    backward compatibility (and will be in the foreseeable future).\n\n\"\"\"\n\nfrom __future__ import absolute_import, print_function, division\n\nimport numpy as np\n\nfrom . import Request\nfrom .. import formats\n\n\ndef help(name=None):\n    \"\"\" help(name=None)\n    \n    Print the documentation of the format specified by name, or a list\n    of supported formats if name is omitted. \n    \n    Parameters\n    ----------\n    name : str\n        Can be the name of a format, a filename extension, or a full\n        filename. See also the :doc:`formats page <formats>`.\n    \"\"\"\n    if not name:\n        print(formats)\n    else:\n        print(formats[name])\n\n\n## Base functions that return a reader/writer\n\n\ndef get_reader(uri, format=None, mode='?', **kwargs):\n    \"\"\" get_reader(uri, format=None, mode='?', **kwargs)\n    \n    Returns a :class:`.Reader` object which can be used to read data\n    and meta data from the specified file.\n    \n    Parameters\n    ----------\n    uri : {str, bytes, file}\n        The resource to load the image from. This can be a normal\n        filename, a file in a zipfile, an http/ftp address, a file\n        object, or the raw bytes.\n    format : str\n        The format to use to read the file. By default imageio selects\n        the appropriate for you based on the filename and its contents.\n    mode : {'i', 'I', 'v', 'V', '?'}\n        Used to give the reader a hint on what the user expects (default \"?\"):\n        \"i\" for an image, \"I\" for multiple images, \"v\" for a volume,\n        \"V\" for multiple volumes, \"?\" for don't care.\n    kwargs : ...\n        Further keyword arguments are passed to the reader. See :func:`.help`\n        to see what arguments are available for a particular format.\n    \"\"\" \n    \n    # Create request object\n    request = Request(uri, 'r' + mode, **kwargs)\n    \n    # Get format\n    if format is not None:\n        format = formats[format]\n    else:\n        format = formats.search_read_format(request)\n    if format is None:\n        raise ValueError('Could not find a format to read the specified file '\n                         'in mode %r' % mode)\n    \n    # Return its reader object\n    return format.get_reader(request)\n\n\ndef get_writer(uri, format=None, mode='?', **kwargs):\n    \"\"\" get_writer(uri, format=None, mode='?', **kwargs)\n    \n    Returns a :class:`.Writer` object which can be used to write data\n    and meta data to the specified file.\n    \n    Parameters\n    ----------\n    uri : {str, file}\n        The resource to write the image to. This can be a normal\n        filename, a file in a zipfile, a file object, or\n        ``imageio.RETURN_BYTES``, in which case the raw bytes are\n        returned.\n    format : str\n        The format to use to read the file. By default imageio selects\n        the appropriate for you based on the filename.\n    mode : {'i', 'I', 'v', 'V', '?'}\n        Used to give the writer a hint on what the user expects (default '?'):\n        \"i\" for an image, \"I\" for multiple images, \"v\" for a volume,\n        \"V\" for multiple volumes, \"?\" for don't care.\n    kwargs : ...\n        Further keyword arguments are passed to the writer. See :func:`.help`\n        to see what arguments are available for a particular format.\n    \"\"\" \n    \n    # Create request object\n    request = Request(uri, 'w' + mode, **kwargs)\n    \n    # Get format\n    if format is not None:\n        format = formats[format]\n    else:\n        format = formats.search_write_format(request)\n    if format is None:\n        raise ValueError('Could not find a format to write the specified file '\n                         'in mode %r' % mode)\n    \n    # Return its writer object\n    return format.get_writer(request)\n\n\n## Images\n\ndef imread(uri, format=None, **kwargs):\n    \"\"\" imread(uri, format=None, **kwargs)\n    \n    Reads an image from the specified file. Returns a numpy array, which\n    comes with a dict of meta data at its 'meta' attribute.\n    \n    Note that the image data is returned as-is, and may not always have\n    a dtype of uint8 (and thus may differ from what e.g. PIL returns).\n    \n    Parameters\n    ----------\n    uri : {str, bytes, file}\n        The resource to load the image from. This can be a normal\n        filename, a file in a zipfile, an http/ftp address, a file\n        object, or the raw bytes.\n    format : str\n        The format to use to read the file. By default imageio selects\n        the appropriate for you based on the filename and its contents.\n    kwargs : ...\n        Further keyword arguments are passed to the reader. See :func:`.help`\n        to see what arguments are available for a particular format.\n    \"\"\" \n    \n    # Get reader and read first\n    reader = read(uri, format, 'i', **kwargs)\n    with reader:\n        return reader.get_data(0)\n\n\ndef imwrite(uri, im, format=None, **kwargs):\n    \"\"\" imwrite(uri, im, format=None, **kwargs)\n    \n    Write an image to the specified file.\n    \n    Parameters\n    ----------\n    uri : {str, file}\n        The resource to write the image to. This can be a normal\n        filename, a file in a zipfile, a file object, or\n        ``imageio.RETURN_BYTES``, in which case the raw bytes are\n        returned.\n    im : numpy.ndarray\n        The image data. Must be NxM, NxMx3 or NxMx4.\n    format : str\n        The format to use to read the file. By default imageio selects\n        the appropriate for you based on the filename and its contents.\n    kwargs : ...\n        Further keyword arguments are passed to the writer. See :func:`.help`\n        to see what arguments are available for a particular format.\n    \"\"\" \n    \n    # Test image\n    if isinstance(im, np.ndarray):\n        if im.ndim == 2:\n            pass\n        elif im.ndim == 3 and im.shape[2] in [1, 3, 4]:\n            pass\n        else:\n            raise ValueError('Image must be 2D (grayscale, RGB, or RGBA).')\n    else:\n        raise ValueError('Image must be a numpy array.')\n    \n    # Get writer and write first\n    writer = get_writer(uri, format, 'i', **kwargs)\n    with writer:\n        writer.append_data(im)\n    \n    # Return a result if there is any\n    return writer.request.get_result()\n\n\n## Multiple images\n\ndef mimread(uri, format=None, **kwargs):\n    \"\"\" mimread(uri, format=None, **kwargs)\n    \n    Reads multiple images from the specified file. Returns a list of\n    numpy arrays, each with a dict of meta data at its 'meta' attribute.\n    \n    Parameters\n    ----------\n    uri : {str, bytes, file}\n        The resource to load the images from. This can be a normal\n        filename, a file in a zipfile, an http/ftp address, a file\n        object, or the raw bytes.\n    format : str\n        The format to use to read the file. By default imageio selects\n        the appropriate for you based on the filename and its contents.\n    kwargs : ...\n        Further keyword arguments are passed to the reader. See :func:`.help`\n        to see what arguments are available for a particular format.\n    \n    Memory consumption\n    ------------------\n    This function will raise a RuntimeError when the read data consumes\n    over 256 MB of memory. This is to protect the system using so much\n    memory that it needs to resort to swapping, and thereby stall the\n    computer. E.g. ``mimread('hunger_games.avi')``.\n    \"\"\" \n    \n    # Get reader\n    reader = read(uri, format, 'I', **kwargs)\n    \n    # Read\n    ims = []\n    nbytes = 0\n    for im in reader:\n        ims.append(im)\n        # Memory check\n        nbytes += im.nbytes\n        if nbytes > 256 * 1024 * 1024:\n            ims[:] = []  # clear to free the memory\n            raise RuntimeError('imageio.mimread() has read over 256 MiB of '\n                               'image data.\\nStopped to avoid memory problems.'\n                               ' Use imageio.get_reader() instead.')\n    \n    return ims\n\n\ndef mimwrite(uri, ims, format=None, **kwargs):\n    \"\"\" mimwrite(uri, ims, format=None, **kwargs)\n    \n    Write multiple images to the specified file.\n    \n    Parameters\n    ----------\n    uri : {str, file}\n        The resource to write the images to. This can be a normal\n        filename, a file in a zipfile, a file object, or\n        ``imageio.RETURN_BYTES``, in which case the raw bytes are\n        returned.\n    ims : sequence of numpy arrays\n        The image data. Each array must be NxM, NxMx3 or NxMx4.\n    format : str\n        The format to use to read the file. By default imageio selects\n        the appropriate for you based on the filename and its contents.\n    kwargs : ...\n        Further keyword arguments are passed to the writer. See :func:`.help`\n        to see what arguments are available for a particular format.\n    \"\"\" \n    \n    # Get writer\n    writer = get_writer(uri, format, 'I', **kwargs)\n    with writer:\n        \n        # Iterate over images (ims may be a generator)\n        for im in ims:\n            \n            # Test image\n            if isinstance(im, np.ndarray):\n                if im.ndim == 2:\n                    pass\n                elif im.ndim == 3 and im.shape[2] in [1, 3, 4]:\n                    pass\n                else:\n                    raise ValueError('Image must be 2D '\n                                     '(grayscale, RGB, or RGBA).')\n            else:\n                raise ValueError('Image must be a numpy array.')\n            \n            # Add image\n            writer.append_data(im)\n    \n    # Return a result if there is any\n    return writer.request.get_result()\n\n\n## Volumes\n\ndef volread(uri, format=None, **kwargs):\n    \"\"\" volread(uri, format=None, **kwargs)\n    \n    Reads a volume from the specified file. Returns a numpy array, which\n    comes with a dict of meta data at its 'meta' attribute.\n    \n    Parameters\n    ----------\n    uri : {str, bytes, file}\n        The resource to load the volume from. This can be a normal\n        filename, a file in a zipfile, an http/ftp address, a file\n        object, or the raw bytes.\n    format : str\n        The format to use to read the file. By default imageio selects\n        the appropriate for you based on the filename and its contents.\n    kwargs : ...\n        Further keyword arguments are passed to the reader. See :func:`.help`\n        to see what arguments are available for a particular format.\n    \"\"\" \n    \n    # Get reader and read first\n    reader = read(uri, format, 'v', **kwargs)\n    with reader:\n        return reader.get_data(0)\n\n\ndef volwrite(uri, im, format=None, **kwargs):\n    \"\"\" volwrite(uri, vol, format=None, **kwargs)\n    \n    Write a volume to the specified file.\n    \n    Parameters\n    ----------\n    uri : {str, file}\n        The resource to write the image to. This can be a normal\n        filename, a file in a zipfile, a file object, or\n        ``imageio.RETURN_BYTES``, in which case the raw bytes are\n        returned.\n    vol : numpy.ndarray\n        The image data. Must be NxMxL (or NxMxLxK if each voxel is a tuple).\n    format : str\n        The format to use to read the file. By default imageio selects\n        the appropriate for you based on the filename and its contents.\n    kwargs : ...\n        Further keyword arguments are passed to the writer. See :func:`.help`\n        to see what arguments are available for a particular format.\n    \"\"\" \n    \n    # Test image\n    if isinstance(im, np.ndarray):\n        if im.ndim == 3:\n            pass\n        elif im.ndim == 4 and im.shape[3] < 32:  # How large can a tuple be?\n            pass\n        else:\n            raise ValueError('Image must be 3D, or 4D if each voxel is '\n                             'a tuple.')\n    else:\n        raise ValueError('Image must be a numpy array.')\n    \n    # Get writer and write first\n    writer = get_writer(uri, format, 'v', **kwargs)\n    with writer:\n        writer.append_data(im)\n    \n    # Return a result if there is any\n    return writer.request.get_result()\n\n\n## Multiple volumes\n\ndef mvolread(uri, format=None, **kwargs):\n    \"\"\" mvolread(uri, format=None, **kwargs)\n    \n    Reads multiple volumes from the specified file. Returns a list of\n    numpy arrays, each with a dict of meta data at its 'meta' attribute.\n    \n    Parameters\n    ----------\n    uri : {str, bytes, file}\n        The resource to load the volumes from. This can be a normal\n        filename, a file in a zipfile, an http/ftp address, a file\n        object, or the raw bytes.\n    format : str\n        The format to use to read the file. By default imageio selects\n        the appropriate for you based on the filename and its contents.\n    kwargs : ...\n        Further keyword arguments are passed to the reader. See :func:`.help`\n        to see what arguments are available for a particular format.\n    \"\"\" \n    \n    # Get reader and read all\n    reader = read(uri, format, 'V', **kwargs)\n    \n    ims = []\n    nbytes = 0\n    for im in reader:\n        ims.append(im)\n        # Memory check\n        nbytes += im.nbytes\n        if nbytes > 1024 * 1024 * 1024:  # pragma: no cover\n            ims[:] = []  # clear to free the memory\n            raise RuntimeError('imageio.mvolread() has read over 1 GiB of '\n                               'image data.\\nStopped to avoid memory problems.'\n                               ' Use imageio.get_reader() instead.')\n    \n    return ims\n\n\ndef mvolwrite(uri, ims, format=None, **kwargs):\n    \"\"\" mvolwrite(uri, vols, format=None, **kwargs)\n    \n    Write multiple volumes to the specified file.\n    \n    Parameters\n    ----------\n    uri : {str, file}\n        The resource to write the volumes to. This can be a normal\n        filename, a file in a zipfile, a file object, or\n        ``imageio.RETURN_BYTES``, in which case the raw bytes are\n        returned.\n    ims : sequence of numpy arrays\n        The image data. Each array must be NxMxL (or NxMxLxK if each\n        voxel is a tuple).\n    format : str\n        The format to use to read the file. By default imageio selects\n        the appropriate for you based on the filename and its contents.\n    kwargs : ...\n        Further keyword arguments are passed to the writer. See :func:`.help`\n        to see what arguments are available for a particular format.\n    \"\"\" \n    \n    # Get writer\n    writer = get_writer(uri, format, 'V', **kwargs)\n    with writer:\n        \n        # Iterate over images (ims may be a generator)\n        for im in ims:\n            \n            # Test image\n            if isinstance(im, np.ndarray):\n                if im.ndim == 3:\n                    pass\n                elif im.ndim == 4 and im.shape[3] < 32:\n                    pass  # How large can a tuple be?\n                else:\n                    raise ValueError('Image must be 3D, or 4D if each voxel is'\n                                     'a tuple.')\n            else:\n                raise ValueError('Image must be a numpy array.')\n            \n            # Add image\n            writer.append_data(im)\n    \n    # Return a result if there is any\n    return writer.request.get_result()\n\n\n## Aliases\n\nread = get_reader\nsave = get_writer\nimsave = imwrite\nmimsave = mimwrite\nvolsave = volwrite\nmvolsave = mvolwrite\n"
  },
  {
    "path": "core/lib/imageio/core/request.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2015, imageio contributors\n# imageio is distributed under the terms of the (new) BSD License.\n\n\"\"\" \nDefinition of the Request object, which acts as a kind of bridge between\nwhat the user wants and what the plugins can.\n\"\"\"\n\nfrom __future__ import absolute_import, print_function, division\n\nimport sys\nimport os\nfrom io import BytesIO\nimport zipfile\nimport tempfile\nimport shutil\n\nfrom ..core import string_types, binary_type, urlopen, get_remote_file\n\n# URI types\nURI_BYTES = 1\nURI_FILE = 2\nURI_FILENAME = 3\nURI_ZIPPED = 4\nURI_HTTP = 5\nURI_FTP = 6\n\n# The user can use this string in a write call to get the data back as bytes.\nRETURN_BYTES = '<bytes>'\n\n# Example images that will be auto-downloaded\nEXAMPLE_IMAGES = {\n    'astronaut.png': 'Image of the astronaut Eileen Collins',\n    'camera.png': 'Classic grayscale image of a photographer',\n    'checkerboard.png': 'Black and white image of a chekerboard',\n    'clock.png': 'Photo of a clock with motion blur (Stefan van der Walt)',\n    'coffee.png': 'Image of a cup of coffee (Rachel Michetti)',\n    \n    'chelsea.png': 'Image of Stefan\\'s cat',\n    'wikkie.png': 'Image of Almar\\'s cat',\n    \n    'coins.png': 'Image showing greek coins from Pompeii',\n    'horse.png': 'Image showing the silhouette of a horse (Andreas Preuss)',\n    'hubble_deep_field.png': 'Photograph taken by Hubble telescope (NASA)',\n    'immunohistochemistry.png': 'Immunohistochemical (IHC) staining',\n    'lena.png': 'Classic but sometimes controversioal Lena test image', \n    'moon.png': 'Image showing a portion of the surface of the moon',\n    'page.png': 'A scanned page of text',\n    'text.png': 'A photograph of handdrawn text',\n    \n    'chelsea.zip': 'The chelsea.png in a zipfile (for testing)',\n    'newtonscradle.gif': 'Animated GIF of a newton\\'s cradle',\n    'cockatoo.mp4': 'Video file of a cockatoo',\n    'stent.npz': 'Volumetric image showing a stented abdominal aorta',\n}\n\n\nclass Request(object):\n    \"\"\" Request(uri, mode, **kwargs)\n    \n    Represents a request for reading or saving an image resource. This\n    object wraps information to that request and acts as an interface\n    for the plugins to several resources; it allows the user to read\n    from filenames, files, http, zipfiles, raw bytes, etc., but offer\n    a simple interface to the plugins via ``get_file()`` and\n    ``get_local_filename()``.\n    \n    For each read/write operation a single Request instance is used and passed\n    to the can_read/can_write method of a format, and subsequently to\n    the Reader/Writer class. This allows rudimentary passing of\n    information between different formats and between a format and\n    associated reader/writer.\n\n    parameters\n    ----------\n    uri : {str, bytes, file}\n        The resource to load the image from.\n    mode : str\n        The first character is \"r\" or \"w\", indicating a read or write\n        request. The second character is used to indicate the kind of data:\n        \"i\" for an image, \"I\" for multiple images, \"v\" for a volume,\n        \"V\" for multiple volumes, \"?\" for don't care.\n    \"\"\"\n    \n    def __init__(self, uri, mode, **kwargs):\n        \n        # General        \n        self._uri_type = None\n        self._filename = None\n        self._kwargs = kwargs\n        self._result = None         # Some write actions may have a result\n        \n        # To handle the user-side\n        self._filename_zip = None   # not None if a zipfile is used\n        self._bytes = None          # Incoming bytes\n        self._zipfile = None        # To store a zipfile instance (if used)\n        \n        # To handle the plugin side\n        self._file = None               # To store the file instance\n        self._filename_local = None     # not None if using tempfile on this FS\n        self._firstbytes = None         # For easy header parsing\n        \n        # To store formats that may be able to fulfil this request\n        #self._potential_formats = []\n        \n        # Check mode\n        self._mode = mode\n        if not isinstance(mode, string_types):\n            raise ValueError('Request requires mode must be a string')\n        if not len(mode) == 2:\n            raise ValueError('Request requires mode to have two chars')\n        if mode[0] not in 'rw':\n            raise ValueError('Request requires mode[0] to be \"r\" or \"w\"')\n        if mode[1] not in 'iIvV?':\n            raise ValueError('Request requires mode[1] to be in \"iIvV?\"')\n        \n        # Parse what was given\n        self._parse_uri(uri)\n    \n    def _parse_uri(self, uri):\n        \"\"\" Try to figure our what we were given\n        \"\"\"\n        py3k = sys.version_info[0] == 3\n        is_read_request = self.mode[0] == 'r'\n        is_write_request = self.mode[0] == 'w'\n        \n        if isinstance(uri, string_types):\n            # Explicit\n            if uri.startswith('http://') or uri.startswith('https://'):\n                self._uri_type = URI_HTTP\n                self._filename = uri\n            elif uri.startswith('ftp://') or uri.startswith('ftps://'):\n                self._uri_type = URI_FTP\n                self._filename = uri\n            elif uri.startswith('file://'):\n                self._uri_type = URI_FILENAME\n                self._filename = uri[7:]\n            elif uri.startswith('<video') and is_read_request:\n                self._uri_type = URI_BYTES\n                self._filename = uri\n            elif uri == RETURN_BYTES and is_write_request:\n                self._uri_type = URI_BYTES\n                self._filename = '<bytes>'\n            # Less explicit (particularly on py 2.x)\n            elif py3k:\n                self._uri_type = URI_FILENAME\n                self._filename = uri\n            else:  # pragma: no cover - our ref for coverage is py3k\n                try:\n                    isfile = os.path.isfile(uri)\n                except Exception:\n                    isfile = False  # If checking does not even work ...\n                if isfile:\n                    self._uri_type = URI_FILENAME\n                    self._filename = uri\n                elif len(uri) < 256:  # Can go wrong with veeery tiny images\n                    self._uri_type = URI_FILENAME\n                    self._filename = uri\n                elif isinstance(uri, binary_type) and is_read_request:\n                    self._uri_type = URI_BYTES\n                    self._filename = '<bytes>'\n                    self._bytes = uri\n                else:\n                    self._uri_type = URI_FILENAME\n                    self._filename = uri\n        elif py3k and isinstance(uri, binary_type) and is_read_request:\n            self._uri_type = URI_BYTES\n            self._filename = '<bytes>'\n            self._bytes = uri\n        # Files\n        elif is_read_request:\n            if hasattr(uri, 'read') and hasattr(uri, 'close'):\n                self._uri_type = URI_FILE\n                self._filename = '<file>'\n                self._file = uri\n        elif is_write_request:\n            if hasattr(uri, 'write') and hasattr(uri, 'close'):\n                self._uri_type = URI_FILE\n                self._filename = '<file>'\n                self._file = uri\n        \n        # Expand user dir\n        if self._uri_type == URI_FILENAME and self._filename.startswith('~'):\n            self._filename = os.path.expanduser(self._filename)\n        \n        # Check if a zipfile\n        if self._uri_type == URI_FILENAME:\n            # Search for zip extension followed by a path separater\n            for needle in ['.zip/', '.zip\\\\']:\n                zip_i = self._filename.lower().find(needle)\n                if zip_i > 0:                    \n                    zip_i += 4\n                    self._uri_type = URI_ZIPPED\n                    self._filename_zip = (self._filename[:zip_i], \n                                          self._filename[zip_i:].lstrip('/\\\\'))\n                    break\n        \n        # Check if we could read it\n        if self._uri_type is None:\n            uri_r = repr(uri)\n            if len(uri_r) > 60:\n                uri_r = uri_r[:57] + '...'\n            raise IOError(\"Cannot understand given URI: %s.\" % uri_r)\n        \n        # Check if this is supported\n        noWriting = [URI_HTTP, URI_FTP]\n        if is_write_request and self._uri_type in noWriting:\n            raise IOError('imageio does not support writing to http/ftp.')\n        \n        # Check if an example image\n        if is_read_request and self._uri_type in [URI_FILENAME, URI_ZIPPED]:\n            fn = self._filename\n            if self._filename_zip:\n                fn = self._filename_zip[0]\n            if (not os.path.exists(fn)) and (fn in EXAMPLE_IMAGES):\n                fn = get_remote_file('images/' + fn)\n                self._filename = fn\n                if self._filename_zip:\n                    self._filename_zip = fn, self._filename_zip[1]\n                    self._filename = fn + '/' + self._filename_zip[1]\n        \n        # Make filename absolute \n        if self._uri_type in [URI_FILENAME, URI_ZIPPED]:\n            if self._filename_zip:\n                self._filename_zip = (os.path.abspath(self._filename_zip[0]),\n                                      self._filename_zip[1])\n            else:\n                self._filename = os.path.abspath(self._filename)\n        \n        # Check wether file name is valid\n        if self._uri_type in [URI_FILENAME, URI_ZIPPED]:\n            fn = self._filename\n            if self._filename_zip:\n                fn = self._filename_zip[0]\n            if is_read_request:\n                # Reading: check that the file exists (but is allowed a dir)\n                if not os.path.exists(fn):\n                    raise IOError(\"No such file: '%s'\" % fn)\n            else:\n                # Writing: check that the directory to write to does exist\n                dn = os.path.dirname(fn)\n                if not os.path.exists(dn):\n                    raise IOError(\"The directory %r does not exist\" % dn)\n    \n    @property\n    def filename(self):\n        \"\"\" The uri for which reading/saving was requested. This\n        can be a filename, an http address, or other resource\n        identifier. Do not rely on the filename to obtain the data,\n        but use ``get_file()`` or ``get_local_filename()`` instead.\n        \"\"\"\n        return self._filename\n    \n    @property\n    def mode(self):\n        \"\"\" The mode of the request. The first character is \"r\" or \"w\",\n        indicating a read or write request. The second character is\n        used to indicate the kind of data:\n        \"i\" for an image, \"I\" for multiple images, \"v\" for a volume,\n        \"V\" for multiple volumes, \"?\" for don't care.\n        \"\"\"\n        return self._mode\n    \n    @property\n    def kwargs(self):\n        \"\"\" The dict of keyword arguments supplied by the user.\n        \"\"\"\n        return self._kwargs\n    \n    ## For obtaining data\n    \n    def get_file(self):\n        \"\"\" get_file()\n        Get a file object for the resource associated with this request.\n        If this is a reading request, the file is in read mode,\n        otherwise in write mode. This method is not thread safe. Plugins\n        do not need to close the file when done.\n        \n        This is the preferred way to read/write the data. But if a\n        format cannot handle file-like objects, they should use\n        ``get_local_filename()``.\n        \"\"\"\n        want_to_write = self.mode[0] == 'w'\n        \n        # Is there already a file?\n        # Either _uri_type == URI_FILE, or we already opened the file, \n        # e.g. by using firstbytes\n        if self._file is not None:\n            self._file.seek(0)\n            return self._file\n        \n        if self._uri_type == URI_BYTES:\n            if want_to_write:                          \n                self._file = BytesIO()\n            else:\n                self._file = BytesIO(self._bytes)\n        \n        elif self._uri_type == URI_FILENAME:\n            if want_to_write:\n                self._file = open(self.filename, 'wb')\n            else:\n                self._file = open(self.filename, 'rb')\n        \n        elif self._uri_type == URI_ZIPPED:\n            # Get the correct filename\n            filename, name = self._filename_zip\n            if want_to_write:\n                # Create new file object, we catch the bytes in finish()\n                self._file = BytesIO()\n            else:\n                # Open zipfile and open new file object for specific file\n                self._zipfile = zipfile.ZipFile(filename, 'r')\n                self._file = self._zipfile.open(name, 'r')\n        \n        elif self._uri_type in [URI_HTTP or URI_FTP]:\n            assert not want_to_write  # This should have been tested in init\n            self._file = urlopen(self.filename, timeout=5)\n        \n        return self._file\n    \n    def get_local_filename(self):\n        \"\"\" get_local_filename()\n        If the filename is an existing file on this filesystem, return\n        that. Otherwise a temporary file is created on the local file\n        system which can be used by the format to read from or write to.\n        \"\"\"\n        \n        if self._uri_type == URI_FILENAME:\n            return self._filename\n        else:\n            # Get filename\n            ext = os.path.splitext(self._filename)[1]\n            self._filename_local = tempfile.mktemp(ext, 'imageio_')\n            # Write stuff to it?\n            if self.mode[0] == 'r':\n                with open(self._filename_local, 'wb') as file:\n                    shutil.copyfileobj(self.get_file(), file)\n            return self._filename_local\n    \n    def finish(self):\n        \"\"\" finish()\n        For internal use (called when the context of the reader/writer\n        exits). Finishes this request. Close open files and process\n        results.\n        \"\"\"\n        \n        # Init\n        bytes = None\n        \n        # Collect bytes from temp file\n        if self.mode[0] == 'w' and self._filename_local:\n            with open(self._filename_local, 'rb') as file:\n                bytes = file.read()\n        \n        # Collect bytes from BytesIO file object.\n        written = (self.mode[0] == 'w') and self._file\n        if written and self._uri_type in [URI_BYTES, URI_ZIPPED]:\n            bytes = self._file.getvalue()\n        \n        # Close open files that we know of (and are responsible for)\n        if self._file and self._uri_type != URI_FILE:\n            self._file.close()\n            self._file = None\n        if self._zipfile:\n            self._zipfile.close()\n            self._zipfile = None\n        # Remove temp file\n        if self._filename_local:\n            try:\n                os.remove(self._filename_local)\n            except Exception:  # pragma: no cover\n                pass\n            self._filename_local = None\n        \n        # Handle bytes that we collected\n        if bytes is not None:\n            if self._uri_type == URI_BYTES:\n                self._result = bytes  # Picked up by imread function\n            elif self._uri_type == URI_ZIPPED:\n                zf = zipfile.ZipFile(self._filename_zip[0], 'a')\n                zf.writestr(self._filename_zip[1], bytes)\n                zf.close()\n        \n        # Detach so gc can clean even if a reference of self lingers\n        self._bytes = None\n    \n    def get_result(self):\n        \"\"\" For internal use. In some situations a write action can have\n        a result (bytes data). That is obtained with this function.\n        \"\"\"\n        self._result, res = None, self._result\n        return res\n    \n    @property\n    def firstbytes(self):\n        \"\"\" The first 256 bytes of the file. These can be used to \n        parse the header to determine the file-format.\n        \"\"\"\n        if self._firstbytes is None:\n            self._read_first_bytes()\n        return self._firstbytes\n    \n    def _read_first_bytes(self, N=256):\n        if self._bytes is not None:\n            self._firstbytes = self._bytes[:N]\n        else:\n            # Prepare\n            f = self.get_file()\n            try:\n                i = f.tell()\n            except Exception:\n                i = None\n            # Read\n            self._firstbytes = read_n_bytes(f, N)\n            # Set back\n            try:\n                if i is None:\n                    raise Exception('cannot seek with None')\n                f.seek(i)\n            except Exception:\n                # Prevent get_file() from reusing the file\n                self._file = None\n                # If the given URI was a file object, we have a problem,\n                # but that should be tested in get_file(), because we\n                # seek() there.\n                assert self._uri_type != URI_FILE\n\n\ndef read_n_bytes(f, N):\n    \"\"\" read_n_bytes(file, n)\n    \n    Read n bytes from the given file, or less if the file has less\n    bytes. Returns zero bytes if the file is closed.\n    \"\"\"\n    bb = binary_type()\n    while len(bb) < N:\n        extra_bytes = f.read(N-len(bb))\n        if not extra_bytes:\n            break\n        bb += extra_bytes\n    return bb\n"
  },
  {
    "path": "core/lib/imageio/core/util.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2015, imageio contributors\n# imageio is distributed under the terms of the (new) BSD License.\n\n\"\"\" \nVarious utilities for imageio\n\"\"\"\n\nfrom __future__ import absolute_import, print_function, division\n\nimport re\nimport os\nimport sys\nimport time\nimport struct\nfrom warnings import warn\nimport platform\n\nimport numpy as np\n\nIS_PYPY = '__pypy__' in sys.builtin_module_names\nTHIS_DIR = os.path.abspath(os.path.dirname(__file__))\n\n# Taken from six.py\nPY3 = sys.version_info[0] == 3\nif PY3:\n    string_types = str,\n    text_type = str\n    binary_type = bytes\nelse:  # pragma: no cover\n    string_types = basestring,  # noqa\n    text_type = unicode  # noqa\n    binary_type = str\n\n\ndef urlopen(*args, **kwargs):\n    \"\"\" Compatibility function for the urlopen function. Raises an\n    RuntimeError if urlopen could not be imported (which can occur in\n    frozen applications.\n    \"\"\" \n    try:\n        from urllib2 import urlopen\n    except ImportError:\n        try:\n            from urllib.request import urlopen  # Py3k\n        except ImportError:\n            raise RuntimeError('Could not import urlopen.')\n    return urlopen(*args, **kwargs)\n\n\ndef image_as_uint(im, bitdepth=None):\n    \"\"\" Convert the given image to uint (default: uint8)\n    \n    If the dtype already matches the desired format, it is returned\n    as-is. If the image is float, and all values are between 0 and 1,\n    the values are multiplied by np.power(2.0, bitdepth). In all other\n    situations, the values are scaled such that the minimum value\n    becomes 0 and the maximum value becomes np.power(2.0, bitdepth)-1\n    (255 for 8-bit and 65535 for 16-bit).\n    \"\"\"\n    if not bitdepth:\n        bitdepth = 8\n    if not isinstance(im, np.ndarray):\n        raise ValueError('Image must be a numpy array')\n    if bitdepth == 8:\n        out_type = np.uint8\n    elif bitdepth == 16:\n        out_type = np.uint16\n    else:\n        raise ValueError('Bitdepth must be either 8 or 16')\n    dtype_str = str(im.dtype)\n    if ((im.dtype == np.uint8 and bitdepth == 8) or\n       (im.dtype == np.uint16 and bitdepth == 16)):\n        # Already the correct format? Return as-is\n        return im\n    if (dtype_str.startswith('float') and\n       np.nanmin(im) >= 0 and np.nanmax(im) <= 1):\n        warn('Lossy conversion from {0} to {1}, range [0, 1]'.format(\n             dtype_str, out_type.__name__))\n        im = im.astype(np.float64) * (np.power(2.0, bitdepth)-1)\n    elif im.dtype == np.uint16 and bitdepth == 8:\n        warn('Lossy conversion from uint16 to uint8, '\n             'loosing 8 bits of resolution')\n        im = np.right_shift(im, 8)\n    elif im.dtype == np.uint32:\n        warn('Lossy conversion from uint32 to {0}, '\n             'loosing {1} bits of resolution'.format(out_type.__name__,\n                                                     32-bitdepth))\n        im = np.right_shift(im, 32-bitdepth)\n    elif im.dtype == np.uint64:\n        warn('Lossy conversion from uint64 to {0}, '\n             'loosing {1} bits of resolution'.format(out_type.__name__,\n                                                     64-bitdepth))\n        im = np.right_shift(im, 64-bitdepth)\n    else:\n        mi = np.nanmin(im)\n        ma = np.nanmax(im)\n        if not np.isfinite(mi):\n            raise ValueError('Minimum image value is not finite')\n        if not np.isfinite(ma):\n            raise ValueError('Maximum image value is not finite')\n        if ma == mi:\n            raise ValueError('Max value == min value, ambiguous given dtype')\n        warn('Conversion from {0} to {1}, '\n             'range [{2}, {3}]'.format(dtype_str, out_type.__name__, mi, ma))\n        # Now make float copy before we scale\n        im = im.astype('float64')\n        # Scale the values between 0 and 1 then multiply by the max value\n        im = (im - mi) / (ma - mi) * (np.power(2.0, bitdepth)-1)\n    assert np.nanmin(im) >= 0\n    assert np.nanmax(im) < np.power(2.0, bitdepth)\n    return im.astype(out_type)\n\n\n# currently not used ... the only use it to easily provide the global meta info\nclass ImageList(list):\n    def __init__(self, meta=None):\n        list.__init__(self)\n        # Check\n        if not (meta is None or isinstance(meta, dict)):\n            raise ValueError('ImageList expects meta data to be a dict.')\n        # Convert and return\n        self._meta = meta if meta is not None else {}\n    \n    @property\n    def meta(self):\n        \"\"\" The dict with the meta data of this image.\n        \"\"\" \n        return self._meta\n\n\nclass Image(np.ndarray):\n    \"\"\" Image(array, meta=None)\n    \n    A subclass of np.ndarray that has a meta attribute.\n    Following scikit-image, we leave this as a normal numpy array as much \n    as we can.\n    \"\"\"\n    \n    def __new__(cls, array, meta=None):\n        # Check\n        if not isinstance(array, np.ndarray):\n            raise ValueError('Image expects a numpy array.')\n        if not (meta is None or isinstance(meta, dict)):\n            raise ValueError('Image expects meta data to be a dict.')\n        # Convert and return\n        meta = meta if meta is not None else {}\n        try:\n            ob = array.view(cls)\n        except AttributeError:  # pragma: no cover\n            # Just return the original; no metadata on the array in Pypy!\n            return array\n        ob._copy_meta(meta)\n        return ob\n    \n    def _copy_meta(self, meta):\n        \"\"\" Make a 2-level deep copy of the meta dictionary.\n        \"\"\"\n        self._meta = Dict()\n        for key, val in meta.items():\n            if isinstance(val, dict):\n                val = Dict(val)  # Copy this level\n            self._meta[key] = val\n    \n    @property\n    def meta(self):\n        \"\"\" The dict with the meta data of this image.\n        \"\"\" \n        return self._meta\n    \n    def __array_finalize__(self, ob):\n        \"\"\" So the meta info is maintained when doing calculations with\n        the array. \n        \"\"\"\n        if isinstance(ob, Image):\n            self._copy_meta(ob.meta)\n        else:\n            self._copy_meta({})\n    \n    def __array_wrap__(self, out, context=None):\n        \"\"\" So that we return a native numpy array (or scalar) when a\n        reducting ufunc is applied (such as sum(), std(), etc.)\n        \"\"\"\n        if not out.shape:\n            return out.dtype.type(out)  # Scalar\n        elif out.shape != self.shape:\n            return out.view(type=np.ndarray)\n        else:\n            return out  # Type Image\n\n\ndef asarray(a):\n    \"\"\" Pypy-safe version of np.asarray. Pypy's np.asarray consumes a\n    *lot* of memory if the given array is an ndarray subclass. This\n    function does not.\n    \"\"\"\n    if isinstance(a, np.ndarray):\n        if IS_PYPY:  # pragma: no cover \n            a = a.copy()  # pypy has issues with base views\n        plain = a.view(type=np.ndarray)\n        return plain\n    return np.asarray(a)\n\n\ntry:\n    from collections import OrderedDict as _dict\nexcept ImportError:\n    _dict = dict\n\n\nclass Dict(_dict):\n    \"\"\" A dict in which the keys can be get and set as if they were\n    attributes. Very convenient in combination with autocompletion.\n    \n    This Dict still behaves as much as possible as a normal dict, and\n    keys can be anything that are otherwise valid keys. However, \n    keys that are not valid identifiers or that are names of the dict\n    class (such as 'items' and 'copy') cannot be get/set as attributes.\n    \"\"\"\n    \n    __reserved_names__ = dir(_dict())  # Also from OrderedDict\n    __pure_names__ = dir(dict())\n    \n    def __getattribute__(self, key):\n        try:\n            return object.__getattribute__(self, key)\n        except AttributeError:\n            if key in self:\n                return self[key]\n            else:\n                raise\n    \n    def __setattr__(self, key, val):\n        if key in Dict.__reserved_names__:\n            # Either let OrderedDict do its work, or disallow\n            if key not in Dict.__pure_names__:\n                return _dict.__setattr__(self, key, val)\n            else:\n                raise AttributeError('Reserved name, this key can only ' +\n                                     'be set via ``d[%r] = X``' % key)\n        else:\n            # if isinstance(val, dict): val = Dict(val) -> no, makes a copy!\n            self[key] = val\n    \n    def __dir__(self):\n        isidentifier = lambda x: bool(re.match(r'[a-z_]\\w*$', x, re.I))\n        names = [k for k in self.keys() if \n                 (isinstance(k, string_types) and isidentifier(k))]\n        return Dict.__reserved_names__ + names\n\n    \nclass BaseProgressIndicator:\n    \"\"\" BaseProgressIndicator(name)\n    \n    A progress indicator helps display the progres of a task to the\n    user. Progress can be pending, running, finished or failed.\n    \n    Each task has:\n      * a name - a short description of what needs to be done.\n      * an action - the current action in performing the task (e.g. a subtask)\n      * progress - how far the task is completed\n      * max - max number of progress units. If 0, the progress is indefinite\n      * unit - the units in which the progress is counted\n      * status - 0: pending, 1: in progress, 2: finished, 3: failed\n    \n    This class defines an abstract interface. Subclasses should implement\n    _start, _stop, _update_progress(progressText), _write(message).\n    \"\"\"\n    \n    def __init__(self, name):\n        self._name = name\n        self._action = ''\n        self._unit = ''\n        self._max = 0\n        self._status = 0\n        self._last_progress_update = 0\n    \n    def start(self,  action='', unit='', max=0):\n        \"\"\" start(action='', unit='', max=0)\n        \n        Start the progress. Optionally specify an action, a unit,\n        and a maxium progress value.\n        \"\"\"\n        if self._status == 1:\n            self.finish() \n        self._action = action\n        self._unit = unit\n        self._max = max\n        #\n        self._progress = 0 \n        self._status = 1\n        self._start()\n    \n    def status(self):\n        \"\"\" status()\n        \n        Get the status of the progress - 0: pending, 1: in progress,\n        2: finished, 3: failed\n        \"\"\"\n        return self._status\n    \n    def set_progress(self, progress=0, force=False):\n        \"\"\" set_progress(progress=0, force=False)\n        \n        Set the current progress. To avoid unnecessary progress updates\n        this will only have a visual effect if the time since the last\n        update is > 0.1 seconds, or if force is True.\n        \"\"\"\n        self._progress = progress\n        # Update or not?\n        if not (force or (time.time() - self._last_progress_update > 0.1)):\n            return\n        self._last_progress_update = time.time()\n        # Compose new string\n        unit = self._unit or ''\n        progressText = ''\n        if unit == '%':\n            progressText = '%2.1f%%' % progress\n        elif self._max > 0:\n            percent = 100 * float(progress) / self._max\n            progressText = '%i/%i %s (%2.1f%%)' % (progress, self._max, unit, \n                                                   percent)\n        elif progress > 0:\n            if isinstance(progress, float):\n                progressText = '%0.4g %s' % (progress, unit)\n            else:\n                progressText = '%i %s' % (progress, unit)\n        # Update\n        self._update_progress(progressText)\n    \n    def increase_progress(self, extra_progress):\n        \"\"\" increase_progress(extra_progress)\n        \n        Increase the progress by a certain amount.\n        \"\"\"\n        self.set_progress(self._progress + extra_progress)\n    \n    def finish(self, message=None):\n        \"\"\" finish(message=None)\n        \n        Finish the progress, optionally specifying a message. This will\n        not set the progress to the maximum.\n        \"\"\"\n        self.set_progress(self._progress, True)  # fore update\n        self._status = 2\n        self._stop()\n        if message is not None:\n            self._write(message)\n    \n    def fail(self, message=None):\n        \"\"\" fail(message=None)\n        \n        Stop the progress with a failure, optionally specifying a message.\n        \"\"\"\n        self.set_progress(self._progress, True)  # fore update\n        self._status = 3\n        self._stop()\n        message = 'FAIL ' + (message or '')\n        self._write(message)\n    \n    def write(self, message):\n        \"\"\" write(message)\n        \n        Write a message during progress (such as a warning).\n        \"\"\"\n        if self.__class__ == BaseProgressIndicator:\n            # When this class is used as a dummy, print explicit message\n            print(message)\n        else:\n            return self._write(message)\n    \n    # Implementing classes should implement these\n    \n    def _start(self):\n        pass\n        \n    def _stop(self):\n        pass\n    \n    def _update_progress(self, progressText):\n        pass\n    \n    def _write(self, message):\n        pass\n\n\nclass StdoutProgressIndicator(BaseProgressIndicator):\n    \"\"\" StdoutProgressIndicator(name)\n    \n    A progress indicator that shows the progress in stdout. It\n    assumes that the tty can appropriately deal with backspace\n    characters.\n    \"\"\"\n    def _start(self):\n        self._chars_prefix, self._chars = '', ''\n        # Write message\n        if self._action:\n            self._chars_prefix = '%s (%s): ' % (self._name, self._action)\n        else:\n            self._chars_prefix = '%s: ' % self._name\n        sys.stdout.write(self._chars_prefix)\n        sys.stdout.flush()\n    \n    def _update_progress(self, progressText):\n        # If progress is unknown, at least make something move\n        if not progressText:\n            i1, i2, i3, i4 = '-\\\\|/'\n            M = {i1: i2, i2: i3, i3: i4, i4: i1}\n            progressText = M.get(self._chars, i1)\n        # Store new string and write\n        delChars = '\\b'*len(self._chars)\n        self._chars = progressText\n        sys.stdout.write(delChars+self._chars)\n        sys.stdout.flush()\n    \n    def _stop(self):\n        self._chars = self._chars_prefix = ''\n        sys.stdout.write('\\n')\n        sys.stdout.flush()\n    \n    def _write(self, message):\n        # Write message\n        delChars = '\\b'*len(self._chars_prefix+self._chars)\n        sys.stdout.write(delChars+'  '+message+'\\n')\n        # Reprint progress text\n        sys.stdout.write(self._chars_prefix+self._chars)\n        sys.stdout.flush()\n\n\n# From pyzolib/paths.py (https://bitbucket.org/pyzo/pyzolib/src/tip/paths.py)\ndef appdata_dir(appname=None, roaming=False):\n    \"\"\" appdata_dir(appname=None, roaming=False)\n    \n    Get the path to the application directory, where applications are allowed\n    to write user specific files (e.g. configurations). For non-user specific\n    data, consider using common_appdata_dir().\n    If appname is given, a subdir is appended (and created if necessary). \n    If roaming is True, will prefer a roaming directory (Windows Vista/7).\n    \"\"\"\n    \n    # Define default user directory\n    userDir = os.path.expanduser('~')\n    if not os.path.isdir(userDir):  # pragma: no cover\n        userDir = '/var/tmp'  # issue #54\n    \n    # Get system app data dir\n    path = None\n    if sys.platform.startswith('win'):\n        path1, path2 = os.getenv('LOCALAPPDATA'), os.getenv('APPDATA')\n        path = (path2 or path1) if roaming else (path1 or path2)\n    elif sys.platform.startswith('darwin'):\n        path = os.path.join(userDir, 'Library', 'Application Support')\n    # On Linux and as fallback\n    if not (path and os.path.isdir(path)):\n        path = userDir\n    \n    # Maybe we should store things local to the executable (in case of a \n    # portable distro or a frozen application that wants to be portable)\n    prefix = sys.prefix\n    if getattr(sys, 'frozen', None):\n        prefix = os.path.abspath(os.path.dirname(sys.path[0]))\n    for reldir in ('settings', '../settings'):\n        localpath = os.path.abspath(os.path.join(prefix, reldir))\n        if os.path.isdir(localpath):  # pragma: no cover\n            try:\n                open(os.path.join(localpath, 'test.write'), 'wb').close()\n                os.remove(os.path.join(localpath, 'test.write'))\n            except IOError:\n                pass  # We cannot write in this directory\n            else:\n                path = localpath\n                break\n    \n    # Get path specific for this app\n    if appname:\n        if path == userDir:\n            appname = '.' + appname.lstrip('.')  # Make it a hidden directory\n        path = os.path.join(path, appname)\n        if not os.path.isdir(path):  # pragma: no cover\n            os.mkdir(path)\n    \n    # Done\n    return path\n\n\ndef resource_dirs():\n    \"\"\" resource_dirs()\n    \n    Get a list of directories where imageio resources may be located.\n    The first directory in this list is the \"resources\" directory in\n    the package itself. The second directory is the appdata directory\n    (~/.imageio on Linux). The list further contains the application\n    directory (for frozen apps), and may include additional directories\n    in the future.\n    \"\"\"\n    dirs = []\n    # Resource dir baked in the package\n    dirs.append(os.path.abspath(os.path.join(THIS_DIR, '..', 'resources')))\n    # Appdata directory\n    try:\n        dirs.append(appdata_dir('imageio'))\n    except Exception:  # pragma: no cover\n        pass  # The home dir may not be writable\n    # Directory where the app is located (mainly for frozen apps)\n    if sys.path and sys.path[0]:\n        # Get the path. If frozen, sys.path[0] is the name of the executable,\n        # otherwise it is the path to the directory that contains the script.\n        thepath = sys.path[0]\n        if getattr(sys, 'frozen', None):\n            thepath = os.path.dirname(thepath)\n        dirs.append(os.path.abspath(thepath))\n    return dirs\n\n\ndef get_platform():\n    \"\"\" get_platform()\n    \n    Get a string that specifies the platform more specific than\n    sys.platform does. The result can be: linux32, linux64, win32,\n    win64, osx32, osx64, osx-arm64. Other platforms may be added in the future.\n    \"\"\"\n    # Get platform\n    if sys.platform.startswith('linux'):\n        plat = 'linux%i'\n    elif sys.platform.startswith('win'):\n        plat = 'win%i'\n    elif sys.platform.startswith('darwin'):\n        if platform.machine() == 'arm64':\n            plat = 'osx-arm64'\n        else:\n            plat = 'osx%i'\n    else:  # pragma: no cover\n        return None\n    \n    # Only perform string formatting when plat contains '%i'\n    if '%i' in plat:\n        return plat % (struct.calcsize('P') * 8)  # 32 or 64 bits\n    else:\n        return plat\n\n\n\ndef has_module(module_name):\n    \"\"\"Check to see if a python module is available.\n    \"\"\"\n    if sys.version_info > (3, ):\n        import importlib\n        return importlib.find_loader(module_name) is not None\n    else:  # pragma: no cover\n        import imp\n        try:\n            imp.find_module(module_name)\n        except ImportError:\n            return False\n        return True\n"
  },
  {
    "path": "core/lib/imageio/freeze.py",
    "content": "\"\"\" \nHelper functions for freezing imageio.\n\"\"\"\n\nimport sys\n\n\ndef get_includes():\n    if sys.version_info[0] == 3:\n        urllib = ['email', 'urllib.request', ]\n    else:\n        urllib = ['urllib2']\n    return urllib + ['numpy', 'zipfile', 'io']\n\n\ndef get_excludes():\n    return []\n"
  },
  {
    "path": "core/lib/imageio/plugins/__init__.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2015, imageio contributors\n# imageio is distributed under the terms of the (new) BSD License.\n\n\"\"\"\n\nImagio is plugin-based. Every supported format is provided with a\nplugin. You can write your own plugins to make imageio support\nadditional formats. And we would be interested in adding such code to the\nimageio codebase!\n\n\nWhat is a plugin\n----------------\n\nIn imageio, a plugin provides one or more :class:`.Format` objects, and \ncorresponding :class:`.Reader` and :class:`.Writer` classes.\nEach Format object represents an implementation to read/write a \nparticular file format. Its Reader and Writer classes do the actual\nreading/saving.\n\nThe reader and writer objects have a ``request`` attribute that can be\nused to obtain information about the read or write :class:`.Request`, such as\nuser-provided keyword arguments, as well get access to the raw image\ndata.\n\n\nRegistering\n-----------\n\nStrictly speaking a format can be used stand alone. However, to allow \nimageio to automatically select it for a specific file, the format must\nbe registered using ``imageio.formats.add_format()``. \n\nNote that a plugin is not required to be part of the imageio package; as\nlong as a format is registered, imageio can use it. This makes imageio very \neasy to extend.\n\n\nWhat methods to implement\n--------------------------\n\nImageio is designed such that plugins only need to implement a few\nprivate methods. The public API is implemented by the base classes.\nIn effect, the public methods can be given a descent docstring which\ndoes not have to be repeated at the plugins.\n\nFor the Format class, the following needs to be implemented/specified:\n\n  * The format needs a short name, a description, and a list of file\n    extensions that are common for the file-format in question.\n    These ase set when instantiation the Format object.\n  * Use a docstring to provide more detailed information about the\n    format/plugin, such as parameters for reading and saving that the user\n    can supply via keyword arguments.\n  * Implement ``_can_read(request)``, return a bool. \n    See also the :class:`.Request` class.\n  * Implement ``_can_write(request)``, dito.\n\nFor the Format.Reader class:\n  \n  * Implement ``_open(**kwargs)`` to initialize the reader. Deal with the\n    user-provided keyword arguments here.\n  * Implement ``_close()`` to clean up.\n  * Implement ``_get_length()`` to provide a suitable length based on what\n    the user expects. Can be ``inf`` for streaming data.\n  * Implement ``_get_data(index)`` to return an array and a meta-data dict.\n  * Implement ``_get_meta_data(index)`` to return a meta-data dict. If index\n    is None, it should return the 'global' meta-data.\n\nFor the Format.Writer class:\n    \n  * Implement ``_open(**kwargs)`` to initialize the writer. Deal with the\n    user-provided keyword arguments here.\n  * Implement ``_close()`` to clean up.\n  * Implement ``_append_data(im, meta)`` to add data (and meta-data).\n  * Implement ``_set_meta_data(meta)`` to set the global meta-data.\n\n\"\"\"\n\n# First import plugins that we want to take precedence over freeimage\nfrom . import freeimage  # noqa\n\n"
  },
  {
    "path": "core/lib/imageio/plugins/_freeimage.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2015, imageio contributors\n# imageio is distributed under the terms of the (new) BSD License.\n\n# styletest: ignore E261\n\n\"\"\" Module imageio/freeimage.py\n\nThis module contains the wrapper code for the freeimage library.\nThe functions defined in this module are relatively thin; just thin\nenough so that arguments and results are native Python/numpy data\ntypes.\n\n\"\"\"\n\nfrom __future__ import absolute_import, print_function, with_statement\n\nimport os\nimport sys\nimport ctypes\nimport threading\nfrom logging import warn\nimport numpy\n\nfrom ..core import (get_remote_file, load_lib, Dict, resource_dirs, \n                    string_types, binary_type, IS_PYPY, get_platform,\n                    InternetNotAllowedError)\n\nTEST_NUMPY_NO_STRIDES = False  # To test pypy fallback\n\nFNAME_PER_PLATFORM = {\n    'osx32': 'libfreeimage-3.16.0-osx10.6.dylib',  # universal library\n    'osx64': 'libfreeimage-3.16.0-osx10.6.dylib',\n    'osx-arm64': 'libfreeimage.3.18.0.dylib',\n    'win32': 'FreeImage-3.18.0-win32.dll',\n    'win64': 'FreeImage-3.18.0-win64.dll',\n    'linux32': 'libfreeimage-3.16.0-linux32.so',\n    'linux64': 'libfreeimage-3.16.0-linux64.so',\n}\n\n\ndef get_freeimage_lib():\n    \"\"\" Ensure we have our version of the binary freeimage lib.\n    \"\"\" \n    \n    lib = os.getenv('IMAGEIO_FREEIMAGE_LIB', None)\n    if lib:  # pragma: no cover\n        return lib\n    \n    # Get filename to load\n    # If we do not provide a binary, the system may still do ...\n    plat = get_platform()\n    if plat and plat in FNAME_PER_PLATFORM:\n        try:\n            #return get_remote_file('freeimage/' + FNAME_PER_PLATFORM[plat])\n            return get_remote_file(FNAME_PER_PLATFORM[plat])\n        except InternetNotAllowedError:\n            pass\n        except RuntimeError as e:  # pragma: no cover\n            warn(str(e))\n\n\n# Define function to encode a filename to bytes (for the current system)\nefn = lambda x: x.encode(sys.getfilesystemencoding())\n\n# 4-byte quads of 0,v,v,v from 0,0,0,0 to 0,255,255,255\nGREY_PALETTE = numpy.arange(0, 0x01000000, 0x00010101, dtype=numpy.uint32)\n\n\nclass FI_TYPES(object):\n    FIT_UNKNOWN = 0\n    FIT_BITMAP = 1\n    FIT_UINT16 = 2\n    FIT_INT16 = 3\n    FIT_UINT32 = 4\n    FIT_INT32 = 5\n    FIT_FLOAT = 6\n    FIT_DOUBLE = 7\n    FIT_COMPLEX = 8\n    FIT_RGB16 = 9\n    FIT_RGBA16 = 10\n    FIT_RGBF = 11\n    FIT_RGBAF = 12\n\n    dtypes = {\n        FIT_BITMAP: numpy.uint8,\n        FIT_UINT16: numpy.uint16,\n        FIT_INT16: numpy.int16,\n        FIT_UINT32: numpy.uint32,\n        FIT_INT32: numpy.int32,\n        FIT_FLOAT: numpy.float32,\n        FIT_DOUBLE: numpy.float64,\n        FIT_COMPLEX: numpy.complex128,\n        FIT_RGB16: numpy.uint16,\n        FIT_RGBA16: numpy.uint16,\n        FIT_RGBF: numpy.float32,\n        FIT_RGBAF: numpy.float32\n    }\n\n    fi_types = {\n        (numpy.uint8, 1): FIT_BITMAP,\n        (numpy.uint8, 3): FIT_BITMAP,\n        (numpy.uint8, 4): FIT_BITMAP,\n        (numpy.uint16, 1): FIT_UINT16,\n        (numpy.int16, 1): FIT_INT16,\n        (numpy.uint32, 1): FIT_UINT32,\n        (numpy.int32, 1): FIT_INT32,\n        (numpy.float32, 1): FIT_FLOAT,\n        (numpy.float64, 1): FIT_DOUBLE,\n        (numpy.complex128, 1): FIT_COMPLEX,\n        (numpy.uint16, 3): FIT_RGB16,\n        (numpy.uint16, 4): FIT_RGBA16,\n        (numpy.float32, 3): FIT_RGBF,\n        (numpy.float32, 4): FIT_RGBAF\n    }\n\n    extra_dims = {\n        FIT_UINT16: [],\n        FIT_INT16: [],\n        FIT_UINT32: [],\n        FIT_INT32: [],\n        FIT_FLOAT: [],\n        FIT_DOUBLE: [],\n        FIT_COMPLEX: [],\n        FIT_RGB16: [3],\n        FIT_RGBA16: [4],\n        FIT_RGBF: [3],\n        FIT_RGBAF: [4]\n    }\n\n\nclass IO_FLAGS(object):\n    FIF_LOAD_NOPIXELS = 0x8000 # loading: load the image header only\n    #                          # (not supported by all plugins)\n    BMP_DEFAULT = 0\n    BMP_SAVE_RLE = 1\n    CUT_DEFAULT = 0\n    DDS_DEFAULT = 0\n    EXR_DEFAULT = 0 # save data as half with piz-based wavelet compression\n    EXR_FLOAT = 0x0001 # save data as float instead of half (not recommended)\n    EXR_NONE = 0x0002 # save with no compression\n    EXR_ZIP = 0x0004 # save with zlib compression, in blocks of 16 scan lines\n    EXR_PIZ = 0x0008 # save with piz-based wavelet compression\n    EXR_PXR24 = 0x0010 # save with lossy 24-bit float compression\n    EXR_B44 = 0x0020 # save with lossy 44% float compression\n    #                # - goes to 22% when combined with EXR_LC\n    EXR_LC = 0x0040 # save images with one luminance and two chroma channels,\n    #               # rather than as RGB (lossy compression)\n    FAXG3_DEFAULT = 0\n    GIF_DEFAULT = 0\n    GIF_LOAD256 = 1 # Load the image as a 256 color image with ununsed\n    #               # palette entries, if it's 16 or 2 color\n    GIF_PLAYBACK = 2 # 'Play' the GIF to generate each frame (as 32bpp)\n    #                # instead of returning raw frame data when loading\n    HDR_DEFAULT = 0\n    ICO_DEFAULT = 0\n    ICO_MAKEALPHA = 1 # convert to 32bpp and create an alpha channel from the\n    #                 # AND-mask when loading\n    IFF_DEFAULT = 0\n    J2K_DEFAULT = 0 # save with a 16:1 rate\n    JP2_DEFAULT = 0 # save with a 16:1 rate\n    JPEG_DEFAULT = 0 # loading (see JPEG_FAST);\n    #                # saving (see JPEG_QUALITYGOOD|JPEG_SUBSAMPLING_420)\n    JPEG_FAST = 0x0001 # load the file as fast as possible,\n    #                  # sacrificing some quality\n    JPEG_ACCURATE = 0x0002 # load the file with the best quality,\n    #                      # sacrificing some speed\n    JPEG_CMYK = 0x0004 # load separated CMYK \"as is\"\n    #                  # (use | to combine with other load flags)\n    JPEG_EXIFROTATE = 0x0008 # load and rotate according to\n    #                        # Exif 'Orientation' tag if available\n    JPEG_QUALITYSUPERB = 0x80 # save with superb quality (100:1)\n    JPEG_QUALITYGOOD = 0x0100 # save with good quality (75:1)\n    JPEG_QUALITYNORMAL = 0x0200 # save with normal quality (50:1)\n    JPEG_QUALITYAVERAGE = 0x0400 # save with average quality (25:1)\n    JPEG_QUALITYBAD = 0x0800 # save with bad quality (10:1)\n    JPEG_PROGRESSIVE = 0x2000 # save as a progressive-JPEG\n    #                         # (use | to combine with other save flags)\n    JPEG_SUBSAMPLING_411 = 0x1000 # save with high 4x1 chroma\n    #                             # subsampling (4:1:1)\n    JPEG_SUBSAMPLING_420 = 0x4000 # save with medium 2x2 medium chroma\n    #                             # subsampling (4:2:0) - default value\n    JPEG_SUBSAMPLING_422 = 0x8000 # save /w low 2x1 chroma subsampling (4:2:2)\n    JPEG_SUBSAMPLING_444 = 0x10000 # save with no chroma subsampling (4:4:4)\n    JPEG_OPTIMIZE = 0x20000 # on saving, compute optimal Huffman coding tables\n    #                       # (can reduce a few percent of file size)\n    JPEG_BASELINE = 0x40000 # save basic JPEG, without metadata or any markers\n    KOALA_DEFAULT = 0\n    LBM_DEFAULT = 0\n    MNG_DEFAULT = 0\n    PCD_DEFAULT = 0\n    PCD_BASE = 1 # load the bitmap sized 768 x 512\n    PCD_BASEDIV4 = 2 # load the bitmap sized 384 x 256\n    PCD_BASEDIV16 = 3 # load the bitmap sized 192 x 128\n    PCX_DEFAULT = 0\n    PFM_DEFAULT = 0\n    PICT_DEFAULT = 0\n    PNG_DEFAULT = 0\n    PNG_IGNOREGAMMA = 1 # loading: avoid gamma correction\n    PNG_Z_BEST_SPEED = 0x0001 # save using ZLib level 1 compression flag\n    #                         # (default value is 6)\n    PNG_Z_DEFAULT_COMPRESSION = 0x0006 # save using ZLib level 6 compression\n    #                                  # flag (default recommended value)\n    PNG_Z_BEST_COMPRESSION = 0x0009 # save using ZLib level 9 compression flag\n    #                               # (default value is 6)\n    PNG_Z_NO_COMPRESSION = 0x0100 # save without ZLib compression\n    PNG_INTERLACED = 0x0200 # save using Adam7 interlacing (use | to combine\n    #                       # with other save flags)\n    PNM_DEFAULT = 0\n    PNM_SAVE_RAW = 0 # Writer saves in RAW format (i.e. P4, P5 or P6)\n    PNM_SAVE_ASCII = 1 # Writer saves in ASCII format (i.e. P1, P2 or P3)\n    PSD_DEFAULT = 0\n    PSD_CMYK = 1 # reads tags for separated CMYK (default is conversion to RGB)\n    PSD_LAB = 2 # reads tags for CIELab (default is conversion to RGB)\n    RAS_DEFAULT = 0\n    RAW_DEFAULT = 0 # load the file as linear RGB 48-bit\n    RAW_PREVIEW = 1 # try to load the embedded JPEG preview with included\n    #               # Exif Data or default to RGB 24-bit\n    RAW_DISPLAY = 2 # load the file as RGB 24-bit\n    SGI_DEFAULT = 0\n    TARGA_DEFAULT = 0\n    TARGA_LOAD_RGB888 = 1 # Convert RGB555 and ARGB8888 -> RGB888.\n    TARGA_SAVE_RLE = 2 # Save with RLE compression\n    TIFF_DEFAULT = 0\n    TIFF_CMYK = 0x0001 # reads/stores tags for separated CMYK\n    #                  # (use | to combine with compression flags)\n    TIFF_PACKBITS = 0x0100 # save using PACKBITS compression\n    TIFF_DEFLATE = 0x0200 # save using DEFLATE (a.k.a. ZLIB) compression\n    TIFF_ADOBE_DEFLATE = 0x0400 # save using ADOBE DEFLATE compression\n    TIFF_NONE = 0x0800 # save without any compression\n    TIFF_CCITTFAX3 = 0x1000 # save using CCITT Group 3 fax encoding\n    TIFF_CCITTFAX4 = 0x2000 # save using CCITT Group 4 fax encoding\n    TIFF_LZW = 0x4000 # save using LZW compression\n    TIFF_JPEG = 0x8000 # save using JPEG compression\n    TIFF_LOGLUV = 0x10000 # save using LogLuv compression\n    WBMP_DEFAULT = 0\n    XBM_DEFAULT = 0\n    XPM_DEFAULT = 0\n\n\nclass METADATA_MODELS(object):\n    FIMD_COMMENTS = 0\n    FIMD_EXIF_MAIN = 1\n    FIMD_EXIF_EXIF = 2\n    FIMD_EXIF_GPS = 3\n    FIMD_EXIF_MAKERNOTE = 4\n    FIMD_EXIF_INTEROP = 5\n    FIMD_IPTC = 6\n    FIMD_XMP = 7\n    FIMD_GEOTIFF = 8\n    FIMD_ANIMATION = 9\n\n\nclass METADATA_DATATYPE(object):\n    FIDT_BYTE = 1 # 8-bit unsigned integer\n    FIDT_ASCII = 2 # 8-bit bytes w/ last byte null\n    FIDT_SHORT = 3 # 16-bit unsigned integer\n    FIDT_LONG = 4 # 32-bit unsigned integer\n    FIDT_RATIONAL = 5 # 64-bit unsigned fraction\n    FIDT_SBYTE = 6 # 8-bit signed integer\n    FIDT_UNDEFINED = 7 # 8-bit untyped data\n    FIDT_SSHORT = 8 # 16-bit signed integer\n    FIDT_SLONG = 9 # 32-bit signed integer\n    FIDT_SRATIONAL = 10 # 64-bit signed fraction\n    FIDT_FLOAT = 11 # 32-bit IEEE floating point\n    FIDT_DOUBLE = 12 # 64-bit IEEE floating point\n    FIDT_IFD = 13 # 32-bit unsigned integer (offset)\n    FIDT_PALETTE = 14 # 32-bit RGBQUAD\n    FIDT_LONG8 = 16 # 64-bit unsigned integer\n    FIDT_SLONG8 = 17 # 64-bit signed integer\n    FIDT_IFD8 = 18 # 64-bit unsigned integer (offset)\n    \n    dtypes = {\n        FIDT_BYTE: numpy.uint8,\n        FIDT_SHORT: numpy.uint16,\n        FIDT_LONG: numpy.uint32,\n        FIDT_RATIONAL:  [('numerator', numpy.uint32),\n                         ('denominator', numpy.uint32)],\n        FIDT_LONG8: numpy.uint64,\n        FIDT_SLONG8: numpy.int64,\n        FIDT_IFD8: numpy.uint64,\n        FIDT_SBYTE: numpy.int8,\n        FIDT_UNDEFINED: numpy.uint8,\n        FIDT_SSHORT: numpy.int16,\n        FIDT_SLONG: numpy.int32,\n        FIDT_SRATIONAL: [('numerator', numpy.int32),\n                         ('denominator', numpy.int32)],\n        FIDT_FLOAT: numpy.float32,\n        FIDT_DOUBLE: numpy.float64,\n        FIDT_IFD: numpy.uint32,\n        FIDT_PALETTE:   [('R', numpy.uint8), ('G', numpy.uint8),\n                         ('B', numpy.uint8), ('A', numpy.uint8)],\n    }\n\n\nclass Freeimage(object):\n    \"\"\" Class to represent an interface to the FreeImage library.\n    This class is relatively thin. It provides a Pythonic API that converts\n    Freeimage objects to Python objects, but that's about it. \n    The actual implementation should be provided by the plugins.\n    \n    The recommended way to call into the Freeimage library (so that\n    errors and warnings show up in the right moment) is to use this \n    object as a context manager:\n    with imageio.fi as lib:\n        lib.FreeImage_GetPalette()\n    \n    \"\"\"\n    \n    _API = {\n        # All we're doing here is telling ctypes that some of the\n        # FreeImage functions return pointers instead of integers. (On\n        # 64-bit systems, without this information the pointers get\n        # truncated and crashes result). There's no need to list\n        # functions that return ints, or the types of the parameters\n        # to these or other functions -- that's fine to do implicitly.\n          \n        # Note that the ctypes immediately converts the returned void_p\n        # back to a python int again! This is really not helpful,\n        # because then passing it back to another library call will\n        # cause truncation-to-32-bits on 64-bit systems. Thanks, ctypes!\n        # So after these calls one must immediately re-wrap the int as\n        # a c_void_p if it is to be passed back into FreeImage.\n        'FreeImage_AllocateT': (ctypes.c_void_p, None),\n        'FreeImage_FindFirstMetadata': (ctypes.c_void_p, None),\n        'FreeImage_GetBits': (ctypes.c_void_p, None),\n        'FreeImage_GetPalette': (ctypes.c_void_p, None),\n        'FreeImage_GetTagKey': (ctypes.c_char_p, None),\n        'FreeImage_GetTagValue': (ctypes.c_void_p, None),\n        'FreeImage_CreateTag': (ctypes.c_void_p, None),\n\n        'FreeImage_Save': (ctypes.c_void_p, None),\n        'FreeImage_Load': (ctypes.c_void_p, None),\n        'FreeImage_LoadFromMemory': (ctypes.c_void_p, None),\n        \n        'FreeImage_OpenMultiBitmap': (ctypes.c_void_p, None),\n        'FreeImage_LoadMultiBitmapFromMemory': (ctypes.c_void_p, None),\n        'FreeImage_LockPage': (ctypes.c_void_p, None),\n        \n        'FreeImage_OpenMemory': (ctypes.c_void_p, None),\n        #'FreeImage_ReadMemory': (ctypes.c_void_p, None),\n        #'FreeImage_CloseMemory': (ctypes.c_void_p, None),\n        \n        'FreeImage_GetVersion': (ctypes.c_char_p, None),\n        'FreeImage_GetFIFExtensionList': (ctypes.c_char_p, None),\n        'FreeImage_GetFormatFromFIF': (ctypes.c_char_p, None),\n        'FreeImage_GetFIFDescription': (ctypes.c_char_p, None),\n        \n        'FreeImage_ColorQuantizeEx': (ctypes.c_void_p, None),\n\n        # Pypy wants some extra definitions, so here we go ...\n        'FreeImage_IsLittleEndian': (ctypes.c_int, None),\n        'FreeImage_SetOutputMessage': (ctypes.c_void_p, None),\n        'FreeImage_GetFIFCount': (ctypes.c_int, None),\n        'FreeImage_IsPluginEnabled': (ctypes.c_int, None),\n        'FreeImage_GetFileType': (ctypes.c_int, None),\n        #\n        'FreeImage_GetTagType': (ctypes.c_int, None),\n        'FreeImage_GetTagLength': (ctypes.c_int, None),\n        'FreeImage_FindNextMetadata': (ctypes.c_int, None),\n        'FreeImage_FindCloseMetadata': (ctypes.c_void_p, None),\n        #\n        'FreeImage_GetFIFFromFilename': (ctypes.c_int, None),\n        'FreeImage_FIFSupportsReading': (ctypes.c_int, None),\n        'FreeImage_FIFSupportsWriting': (ctypes.c_int, None),\n        'FreeImage_FIFSupportsExportType': (ctypes.c_int, None),\n        'FreeImage_FIFSupportsExportBPP': (ctypes.c_int, None),\n        'FreeImage_GetHeight': (ctypes.c_int, None),\n        'FreeImage_GetWidth': (ctypes.c_int, None),\n        'FreeImage_GetImageType': (ctypes.c_int, None),\n        'FreeImage_GetBPP': (ctypes.c_int, None),\n        'FreeImage_GetColorsUsed': (ctypes.c_int, None),\n        'FreeImage_ConvertTo32Bits': (ctypes.c_void_p, None),\n        'FreeImage_GetPitch': (ctypes.c_int, None),\n        'FreeImage_Unload': (ctypes.c_void_p, None),\n    }\n    \n    def __init__(self):\n        \n        # Initialize freeimage lib as None\n        self._lib = None\n        \n        # A lock to create thread-safety\n        self._lock = threading.RLock()\n        \n        # Init log messages lists\n        self._messages = []\n        \n        # Select functype for error handler\n        if sys.platform.startswith('win'): \n            functype = ctypes.WINFUNCTYPE\n        else: \n            functype = ctypes.CFUNCTYPE\n        \n        # Create output message handler\n        @functype(None, ctypes.c_int, ctypes.c_char_p)\n        def error_handler(fif, message):\n            message = message.decode('utf-8')\n            self._messages.append(message)\n            while (len(self._messages)) > 256:\n                self._messages.pop(0)\n        \n        # Make sure to keep a ref to function\n        self._error_handler = error_handler\n    \n    @property\n    def lib(self):\n        if self._lib is None:\n            try:\n                self.load_freeimage()\n            except OSError as err:\n                self._lib = 'The freeimage library could not be loaded: '\n                self._lib += str(err)\n        if isinstance(self._lib, str):\n            raise RuntimeError(self._lib)\n        return self._lib\n    \n    def has_lib(self):\n        try:\n            self.lib\n        except Exception:\n            return False\n        return True\n    \n    def load_freeimage(self):\n        \"\"\" Try to load the freeimage lib from the system. If not successful,\n        try to download the imageio version and try again.\n        \"\"\"\n        # Load library and register API\n        success = False\n        try:\n            # Try without forcing a download, but giving preference\n            # to the imageio-provided lib (if previously downloaded)\n            self._load_freeimage()\n            self._register_api()\n            if self.lib.FreeImage_GetVersion().decode('utf-8') >= '3.15':\n                success = True\n        except OSError:\n            pass\n        \n        if not success:\n            # Ensure we have our own lib, try again\n            get_freeimage_lib()\n            self._load_freeimage()\n            self._register_api()\n        \n        # Wrap up\n        self.lib.FreeImage_SetOutputMessage(self._error_handler)\n        self.lib_version = self.lib.FreeImage_GetVersion().decode('utf-8')\n    \n    def _load_freeimage(self):\n        \n        # Define names\n        lib_names = ['freeimage', 'libfreeimage']\n        exact_lib_names = ['FreeImage', 'libfreeimage.dylib', \n                           'libfreeimage.so', 'libfreeimage.so.3']\n        # Add names of libraries that we provide (that file may not exist)\n        res_dirs = resource_dirs()\n        plat = get_platform()\n        if plat:  # Can be None on e.g. FreeBSD\n            fname = FNAME_PER_PLATFORM[plat]\n            for dir in res_dirs:\n                exact_lib_names.insert(0, \n                                       os.path.join(dir, 'freeimage', fname))\n\n        # Add the path specified with IMAGEIO_FREEIMAGE_LIB:\n        lib = os.getenv('IMAGEIO_FREEIMAGE_LIB', None)\n        if lib is not None:\n            exact_lib_names.insert(0, lib)\n        \n        # Load\n        try:\n            lib, fname = load_lib(exact_lib_names, lib_names, res_dirs)\n        except OSError as err:  # pragma: no cover\n            err_msg = str(err) + '\\nPlease install the FreeImage library.'\n            raise OSError(err_msg)\n        \n        # Store\n        self._lib = lib\n        self.lib_fname = fname\n    \n    def _register_api(self):\n        # Albert's ctypes pattern    \n        for f, (restype, argtypes) in self._API.items():\n            func = getattr(self.lib, f)\n            func.restype = restype\n            func.argtypes = argtypes\n    \n    ## Handling of output messages\n    \n    def __enter__(self):\n        self._lock.acquire()\n        return self.lib\n    \n    def __exit__(self, *args):\n        self._show_any_warnings()\n        self._lock.release()\n    \n    def _reset_log(self):\n        \"\"\" Reset the list of output messages. Call this before \n        loading or saving an image with the FreeImage API.\n        \"\"\"\n        self._messages = []\n    \n    def _get_error_message(self):\n        \"\"\" Get the output messages produced since the last reset as \n        one string. Returns 'No known reason.' if there are no messages. \n        Also resets the log.\n        \"\"\" \n        if self._messages:\n            res = ' '.join(self._messages)\n            self._reset_log()\n            return res\n        else:\n            return 'No known reason.'\n    \n    def _show_any_warnings(self):\n        \"\"\" If there were any messages since the last reset, show them\n        as a warning. Otherwise do nothing. Also resets the messages.\n        \"\"\" \n        if self._messages:\n            warn('imageio.freeimage warning: ' + self._get_error_message())\n            self._reset_log()\n    \n    def get_output_log(self):\n        \"\"\" Return a list of the last 256 output messages \n        (warnings and errors) produced by the FreeImage library.\n        \"\"\" \n        # This message log is not cleared/reset, but kept to 256 elements.\n        return [m for m in self._messages]\n\n    def getFIF(self, filename, mode, bytes=None):\n        \"\"\" Get the freeimage Format (FIF) from a given filename.\n        If mode is 'r', will try to determine the format by reading\n        the file, otherwise only the filename is used.\n        \n        This function also tests whether the format supports reading/writing.\n        \"\"\"\n        with self as lib:\n        \n            # Init\n            ftype = -1\n            if mode not in 'rw':\n                raise ValueError('Invalid mode (must be \"r\" or \"w\").')\n            \n            # Try getting format from the content. Note that some files\n            # do not have a header that allows reading the format from\n            # the file.\n            if mode == 'r':\n                if bytes is not None:\n                    fimemory = lib.FreeImage_OpenMemory(\n                        ctypes.c_char_p(bytes), len(bytes))\n                    ftype = lib.FreeImage_GetFileTypeFromMemory(\n                        ctypes.c_void_p(fimemory), len(bytes))\n                    lib.FreeImage_CloseMemory(ctypes.c_void_p(fimemory))\n                if (ftype == -1) and os.path.isfile(filename):\n                    ftype = lib.FreeImage_GetFileType(efn(filename), 0)\n            # Try getting the format from the extension\n            if ftype == -1:\n                ftype = lib.FreeImage_GetFIFFromFilename(efn(filename))\n            \n            # Test if ok\n            if ftype == -1:\n                raise ValueError('Cannot determine format of file \"%s\"' %\n                                 filename)\n            elif mode == 'w' and not lib.FreeImage_FIFSupportsWriting(ftype):\n                raise ValueError('Cannot write the format of file \"%s\"' %\n                                 filename)\n            elif mode == 'r' and not lib.FreeImage_FIFSupportsReading(ftype):\n                raise ValueError('Cannot read the format of file \"%s\"' %\n                                 filename)\n            return ftype\n    \n    def create_bitmap(self, filename, ftype, flags=0):\n        \"\"\" create_bitmap(filename, ftype, flags=0)\n        Create a wrapped bitmap object.\n        \"\"\" \n        return FIBitmap(self, filename, ftype, flags)\n    \n    def create_multipage_bitmap(self, filename, ftype, flags=0):\n        \"\"\" create_multipage_bitmap(filename, ftype, flags=0)\n        Create a wrapped multipage bitmap object.\n        \"\"\" \n        return FIMultipageBitmap(self, filename, ftype, flags)\n\n\nclass FIBaseBitmap(object):\n    def __init__(self, fi, filename, ftype, flags):\n        self._fi = fi\n        self._filename = filename\n        self._ftype = ftype\n        self._flags = flags\n        self._bitmap = None\n        self._close_funcs = []\n    \n    def __del__(self):\n        self.close()\n    \n    def close(self):\n        if (self._bitmap is not None) and self._close_funcs:\n            for close_func in self._close_funcs:\n                try:\n                    with self._fi:\n                        fun = close_func[0]\n                        fun(*close_func[1:])\n                except Exception:  # pragma: no cover\n                    pass\n            self._close_funcs = []\n            self._bitmap = None\n    \n    def _set_bitmap(self, bitmap, close_func=None):\n        \"\"\" Function to set the bitmap and specify the function to unload it.\n        \"\"\" \n        if self._bitmap is not None:\n            pass  # bitmap is converted\n        if close_func is None:\n            close_func = self._fi.lib.FreeImage_Unload, bitmap\n        \n        self._bitmap = bitmap\n        if close_func:\n            self._close_funcs.append(close_func)\n    \n    def get_meta_data(self):\n        \n        # todo: there is also FreeImage_TagToString, is that useful?\n        # and would that work well when reading and then saving?\n        \n        # Create a list of (model_name, number) tuples\n        models = [(name[5:], number) for name, number in\n                  METADATA_MODELS.__dict__.items() if name.startswith('FIMD_')]\n        \n        # Prepare\n        metadata = Dict()\n        tag = ctypes.c_void_p()\n        \n        with self._fi as lib:\n            \n            # Iterate over all FreeImage meta models\n            for model_name, number in models:\n                                \n                # Find beginning, get search handle\n                mdhandle = lib.FreeImage_FindFirstMetadata(number, \n                                                           self._bitmap,\n                                                           ctypes.byref(tag))\n                mdhandle = ctypes.c_void_p(mdhandle)\n                if mdhandle:\n                    \n                    # Iterate over all tags in this model\n                    more = True\n                    while more:\n                        # Get info about tag\n                        tag_name = lib.FreeImage_GetTagKey(tag).decode('utf-8')\n                        tag_type = lib.FreeImage_GetTagType(tag)\n                        byte_size = lib.FreeImage_GetTagLength(tag)\n                        char_ptr = ctypes.c_char * byte_size\n                        data = char_ptr.from_address(\n                            lib.FreeImage_GetTagValue(tag))\n                        # Convert in a way compatible with Pypy\n                        tag_bytes = binary_type(bytearray(data))\n                        # The default value is the raw bytes\n                        tag_val = tag_bytes\n                        # Convert to a Python value in the metadata dict\n                        if tag_type == METADATA_DATATYPE.FIDT_ASCII:\n                            tag_val = tag_bytes.decode('utf-8', 'replace')\n                        elif tag_type in METADATA_DATATYPE.dtypes:\n                            dtype = METADATA_DATATYPE.dtypes[tag_type]\n                            if IS_PYPY and isinstance(dtype, (list, tuple)):\n                                pass  # pragma: no cover - or we get a segfault\n                            else:\n                                try:\n                                    tag_val = numpy.fromstring(tag_bytes, \n                                                               dtype=dtype)\n                                    if len(tag_val) == 1:\n                                        tag_val = tag_val[0]\n                                except Exception:  # pragma: no cover\n                                    pass\n                        # Store data in dict\n                        subdict = metadata.setdefault(model_name, Dict())\n                        subdict[tag_name] = tag_val\n                        # Next\n                        more = lib.FreeImage_FindNextMetadata(\n                            mdhandle, ctypes.byref(tag))\n                    \n                    # Close search handle for current meta model\n                    lib.FreeImage_FindCloseMetadata(mdhandle)\n            \n            # Done\n            return metadata\n    \n    def set_meta_data(self, metadata):\n        \n        # Create a dict mapping model_name to number\n        models = {}\n        for name, number in METADATA_MODELS.__dict__.items():\n            if name.startswith('FIMD_'):\n                models[name[5:]] = number\n        \n        # Create a mapping from numpy.dtype to METADATA_DATATYPE\n        def get_tag_type_number(dtype):\n            for number, numpy_dtype in METADATA_DATATYPE.dtypes.items():\n                if dtype == numpy_dtype:\n                    return number\n            else:\n                return None\n        \n        with self._fi as lib:\n            \n            for model_name, subdict in metadata.items():\n                \n                # Get model number\n                number = models.get(model_name, None)\n                if number is None:\n                    continue # Unknown model, silent ignore\n                \n                for tag_name, tag_val in subdict.items():\n                \n                    # Create new tag\n                    tag = lib.FreeImage_CreateTag()\n                    tag = ctypes.c_void_p(tag)\n                    \n                    try:\n                        # Convert Python value to FI type, val\n                        is_ascii = False\n                        if isinstance(tag_val, string_types):\n                            try:\n                                tag_bytes = tag_val.encode('ascii')\n                                is_ascii = True\n                            except UnicodeError:\n                                pass\n                        if is_ascii:\n                            tag_type = METADATA_DATATYPE.FIDT_ASCII\n                            tag_count = len(tag_bytes)\n                        else:\n                            if not hasattr(tag_val, 'dtype'):\n                                tag_val = numpy.array([tag_val])\n                            tag_type = get_tag_type_number(tag_val.dtype)\n                            if tag_type is None:\n                                warn('imageio.freeimage warning: Could not '\n                                     'determine tag type of %r.' % tag_name)\n                                continue\n                            tag_bytes = tag_val.tostring()\n                            tag_count = tag_val.size\n                        # Set properties\n                        lib.FreeImage_SetTagKey(tag, tag_name.encode('utf-8'))\n                        lib.FreeImage_SetTagType(tag, tag_type)\n                        lib.FreeImage_SetTagCount(tag, tag_count)\n                        lib.FreeImage_SetTagLength(tag, len(tag_bytes))\n                        lib.FreeImage_SetTagValue(tag, tag_bytes)\n                        # Store tag\n                        tag_key = lib.FreeImage_GetTagKey(tag)\n                        lib.FreeImage_SetMetadata(number, self._bitmap, \n                                                  tag_key, tag)\n                    \n                    except Exception as err:  # pragma: no cover\n                        warn('imagio.freeimage warning: Could not set tag '\n                             '%r: %s, %s' % (tag_name, \n                                             self._fi._get_error_message(), \n                                             str(err)))\n                    finally:\n                        lib.FreeImage_DeleteTag(tag)\n\n\nclass FIBitmap(FIBaseBitmap):\n    \"\"\" Wrapper for the FI bitmap object.\n    \"\"\" \n    \n    def allocate(self, array):\n        \n        # Prepare array\n        assert isinstance(array, numpy.ndarray)\n        shape = array.shape\n        dtype = array.dtype\n        \n        # Get shape and channel info\n        r, c = shape[:2]\n        if len(shape) == 2:\n            n_channels = 1\n        elif len(shape) == 3:\n            n_channels = shape[2]\n        else:\n            n_channels = shape[0]\n        \n        # Get fi_type\n        try:\n            fi_type = FI_TYPES.fi_types[(dtype.type, n_channels)]\n            self._fi_type = fi_type\n        except KeyError:\n            raise ValueError('Cannot write arrays of given type and shape.')\n        \n        # Allocate bitmap\n        with self._fi as lib:\n            bpp = 8 * dtype.itemsize * n_channels\n            bitmap = lib.FreeImage_AllocateT(fi_type, c, r, bpp, 0, 0, 0)\n            bitmap = ctypes.c_void_p(bitmap)\n            \n            # Check and store\n            if not bitmap:  # pragma: no cover\n                raise RuntimeError('Could not allocate bitmap for storage: %s'\n                                   % self._fi._get_error_message())\n            self._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap))\n    \n    def load_from_filename(self, filename=None):\n        if filename is None:\n            filename = self._filename\n        \n        with self._fi as lib: \n            # Create bitmap\n            bitmap = lib.FreeImage_Load(self._ftype, efn(filename), \n                                        self._flags)\n            bitmap = ctypes.c_void_p(bitmap)\n            \n            # Check and store\n            if not bitmap:  # pragma: no cover\n                raise ValueError('Could not load bitmap \"%s\": %s' % \n                                 (self._filename, \n                                  self._fi._get_error_message()))\n            self._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap))\n    \n# def load_from_bytes(self, bytes):\n#     with self._fi as lib: \n#         # Create bitmap\n#         fimemory = lib.FreeImage_OpenMemory(\n#                                         ctypes.c_char_p(bytes), len(bytes))\n#         bitmap = lib.FreeImage_LoadFromMemory(\n#                         self._ftype, ctypes.c_void_p(fimemory), self._flags)\n#         bitmap = ctypes.c_void_p(bitmap)\n#         lib.FreeImage_CloseMemory(ctypes.c_void_p(fimemory))\n#         \n#         # Check\n#         if not bitmap:\n#             raise ValueError('Could not load bitmap \"%s\": %s' \n#                         % (self._filename, self._fi._get_error_message()))\n#         else:\n#             self._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap))\n    \n    def save_to_filename(self, filename=None):\n        if filename is None:\n            filename = self._filename\n        \n        ftype = self._ftype\n        bitmap = self._bitmap\n        fi_type = self._fi_type # element type\n        \n        with self._fi as lib:\n            # Check if can write\n            if fi_type == FI_TYPES.FIT_BITMAP:\n                can_write = lib.FreeImage_FIFSupportsExportBPP(\n                    ftype, lib.FreeImage_GetBPP(bitmap))\n            else:\n                can_write = lib.FreeImage_FIFSupportsExportType(ftype, fi_type)\n            if not can_write:\n                raise TypeError('Cannot save image of this format '\n                                'to this file type')\n            \n            # Save to file\n            res = lib.FreeImage_Save(ftype, bitmap, efn(filename), self._flags)\n            # Check\n            if not res:  # pragma: no cover, we do so many checks, this is rare\n                raise RuntimeError('Could not save file \"%s\": %s' % \n                                   (self._filename, \n                                    self._fi._get_error_message()))\n    \n# def save_to_bytes(self):\n#     ftype = self._ftype\n#     bitmap = self._bitmap\n#     fi_type = self._fi_type # element type\n#     \n#     with self._fi as lib:\n#         # Check if can write\n#         if fi_type == FI_TYPES.FIT_BITMAP:\n#             can_write = lib.FreeImage_FIFSupportsExportBPP(ftype,\n#                                     lib.FreeImage_GetBPP(bitmap))\n#         else:\n#             can_write = lib.FreeImage_FIFSupportsExportType(ftype, fi_type)\n#         if not can_write:\n#             raise TypeError('Cannot save image of this format '\n#                             'to this file type')\n#         \n#         # Extract the bytes\n#         fimemory = lib.FreeImage_OpenMemory(0, 0)\n#         res = lib.FreeImage_SaveToMemory(ftype, bitmap, \n#                                          ctypes.c_void_p(fimemory), \n#                                          self._flags)\n#         if res:\n#             N = lib.FreeImage_TellMemory(ctypes.c_void_p(fimemory))\n#             result = ctypes.create_string_buffer(N)\n#             lib.FreeImage_SeekMemory(ctypes.c_void_p(fimemory), 0)\n#             lib.FreeImage_ReadMemory(result, 1, N, ctypes.c_void_p(fimemory))\n#             result = result.raw\n#         lib.FreeImage_CloseMemory(ctypes.c_void_p(fimemory))\n#         \n#         # Check\n#         if not res:\n#             raise RuntimeError('Could not save file \"%s\": %s' \n#                     % (self._filename, self._fi._get_error_message()))\n#     \n#     # Done\n#     return result\n    \n    def get_image_data(self):\n        dtype, shape, bpp = self._get_type_and_shape()\n        array = self._wrap_bitmap_bits_in_array(shape, dtype, False)\n        with self._fi as lib:\n            isle = lib.FreeImage_IsLittleEndian()\n        \n        # swizzle the color components and flip the scanlines to go from\n        # FreeImage's BGR[A] and upside-down internal memory format to \n        # something more normal\n        def n(arr):\n            #return arr[..., ::-1].T  # Does not work on numpypy yet\n            if arr.ndim == 1:  # pragma: no cover\n                return arr[::-1].T\n            elif arr.ndim == 2:  # Always the case here ...\n                return arr[:, ::-1].T\n            elif arr.ndim == 3:  # pragma: no cover\n                return arr[:, :, ::-1].T\n            elif arr.ndim == 4:  # pragma: no cover\n                return arr[:, :, :, ::-1].T\n        \n        if len(shape) == 3 and isle and dtype.type == numpy.uint8:\n            b = n(array[0])\n            g = n(array[1]) \n            r = n(array[2])\n            if shape[0] == 3:\n                return numpy.dstack((r, g, b))\n            elif shape[0] == 4:\n                a = n(array[3])\n                return numpy.dstack((r, g, b, a))\n            else:  # pragma: no cover - we check this earlier\n                raise ValueError('Cannot handle images of shape %s' % shape)\n        \n        # We need to copy because array does *not* own its memory\n        # after bitmap is freed.\n        a = n(array).copy()\n        return a\n    \n    def set_image_data(self, array):\n        \n        # Prepare array\n        assert isinstance(array, numpy.ndarray)\n        shape = array.shape\n        dtype = array.dtype\n        with self._fi as lib:\n            isle = lib.FreeImage_IsLittleEndian()\n        \n        # Calculate shape and channels\n        r, c = shape[:2]\n        if len(shape) == 2:\n            n_channels = 1\n            w_shape = (c, r)\n        elif len(shape) == 3:\n            n_channels = shape[2]\n            w_shape = (n_channels, c, r)\n        else:\n            n_channels = shape[0]\n        \n        def n(arr):  # normalise to freeimage's in-memory format\n            return arr.T[:, ::-1]\n        wrapped_array = self._wrap_bitmap_bits_in_array(w_shape, dtype, True)\n        # swizzle the color components and flip the scanlines to go to\n        # FreeImage's BGR[A] and upside-down internal memory format\n        if len(shape) == 3:\n            R = array[:, :, 0]\n            G = array[:, :, 1]\n            B = array[:, :, 2]\n        \n            if isle:\n                if dtype.type == numpy.uint8:\n                    wrapped_array[0] = n(B)\n                    wrapped_array[1] = n(G)\n                    wrapped_array[2] = n(R)\n                elif dtype.type == numpy.uint16:\n                    wrapped_array[0] = n(R)\n                    wrapped_array[1] = n(G)\n                    wrapped_array[2] = n(B)\n                #\n                if shape[2] == 4:\n                    A = array[:, :, 3]\n                    wrapped_array[3] = n(A)\n        else:\n            wrapped_array[:] = n(array)\n        if self._need_finish:\n            self._finish_wrapped_array(wrapped_array)\n        \n        if len(shape) == 2 and dtype.type == numpy.uint8:\n            with self._fi as lib:\n                palette = lib.FreeImage_GetPalette(self._bitmap)\n            palette = ctypes.c_void_p(palette)\n            if not palette:\n                raise RuntimeError('Could not get image palette')\n            try:\n                palette_data = GREY_PALETTE.ctypes.data\n            except Exception:  # pragma: no cover - IS_PYPY\n                palette_data = GREY_PALETTE.__array_interface__['data'][0]\n            ctypes.memmove(palette, palette_data, 1024)\n    \n    def _wrap_bitmap_bits_in_array(self, shape, dtype, save):\n        \"\"\"Return an ndarray view on the data in a FreeImage bitmap. Only\n        valid for as long as the bitmap is loaded (if single page) / locked\n        in memory (if multipage). This is used in loading data, but\n        also during saving, to prepare a strided numpy array buffer.\n        \n        \"\"\"\n        # Get bitmap info\n        with self._fi as lib:\n            pitch = lib.FreeImage_GetPitch(self._bitmap)\n            bits = lib.FreeImage_GetBits(self._bitmap)\n        \n        # Get more info\n        height = shape[-1]\n        byte_size = height * pitch\n        itemsize = dtype.itemsize\n        \n        # Get strides\n        if len(shape) == 3:\n            strides = (itemsize, shape[0]*itemsize, pitch)\n        else:\n            strides = (itemsize, pitch)\n        \n        # Create numpy array and return\n        data = (ctypes.c_char*byte_size).from_address(bits)\n        try:\n            self._need_finish = False\n            if TEST_NUMPY_NO_STRIDES:\n                raise NotImplementedError()\n            return numpy.ndarray(shape, dtype=dtype, buffer=data, \n                                 strides=strides)\n        except NotImplementedError:\n            # IS_PYPY - not very efficient. We create a C-contiguous\n            # numpy array (because pypy does not support Fortran-order)\n            # and shape it such that the rest of the code can remain.\n            if save:\n                self._need_finish = True  # Flag to use _finish_wrapped_array\n                return numpy.zeros(shape, dtype=dtype)\n            else:\n                bytes = binary_type(bytearray(data))\n                array = numpy.fromstring(bytes, dtype=dtype)\n                # Deal with strides\n                if len(shape) == 3:\n                    array.shape = shape[2], strides[-1]/shape[0], shape[0]\n                    array2 = array[:shape[2], :shape[1], :shape[0]]\n                    array = numpy.zeros(shape, dtype=array.dtype)\n                    for i in range(shape[0]):\n                        array[i] = array2[:, :, i].T\n                else:\n                    array.shape = shape[1], strides[-1]\n                    array = array[:shape[1], :shape[0]].T\n                return array\n    \n    def _finish_wrapped_array(self, array):  # IS_PYPY\n        \"\"\" Hardcore way to inject numpy array in bitmap.\n        \"\"\"\n        # Get bitmap info\n        with self._fi as lib:\n            pitch = lib.FreeImage_GetPitch(self._bitmap)\n            bits = lib.FreeImage_GetBits(self._bitmap)\n            bpp = lib.FreeImage_GetBPP(self._bitmap)\n        # Get channels and realwidth\n        nchannels = bpp // 8 // array.itemsize\n        realwidth = pitch // nchannels\n        # Apply padding for pitch if necessary\n        extra = realwidth - array.shape[-2]\n        assert extra >= 0 and extra < 10\n        # Make sort of Fortran, also take padding (i.e. pitch) into account\n        newshape = array.shape[-1], realwidth, nchannels\n        array2 = numpy.zeros(newshape, array.dtype)\n        if nchannels == 1:\n            array2[:, :array.shape[-2], 0] = array.T\n        else:\n            for i in range(nchannels):\n                array2[:, :array.shape[-2], i] = array[i, :, :].T\n        # copy data\n        data_ptr = array2.__array_interface__['data'][0]\n        ctypes.memmove(bits, data_ptr, array2.nbytes)\n        del array2\n    \n    def _get_type_and_shape(self):\n        bitmap = self._bitmap\n        \n        # Get info on bitmap\n        with self._fi as lib:\n            w = lib.FreeImage_GetWidth(bitmap)\n            h = lib.FreeImage_GetHeight(bitmap)\n            self._fi_type = fi_type = lib.FreeImage_GetImageType(bitmap)\n            if not fi_type:\n                raise ValueError('Unknown image pixel type')\n        \n        # Determine required props for numpy array\n        bpp = None\n        dtype = FI_TYPES.dtypes[fi_type]\n        \n        if fi_type == FI_TYPES.FIT_BITMAP:\n            with self._fi as lib:\n                bpp = lib.FreeImage_GetBPP(bitmap)\n                has_pallette = lib.FreeImage_GetColorsUsed(bitmap)\n            if has_pallette:\n                # Examine the palette. If it is grayscale, we return as such\n                if has_pallette == 256:\n                    palette = lib.FreeImage_GetPalette(bitmap)\n                    palette = ctypes.c_void_p(palette)\n                    p = (ctypes.c_uint8*(256*4)).from_address(palette.value)\n                    p = numpy.frombuffer(p, numpy.uint32)\n                    if (GREY_PALETTE == p).all():\n                        extra_dims = []\n                        return numpy.dtype(dtype), extra_dims + [w, h], bpp\n                # Convert bitmap and call this method again\n                newbitmap = lib.FreeImage_ConvertTo32Bits(bitmap)\n                newbitmap = ctypes.c_void_p(newbitmap)\n                self._set_bitmap(newbitmap)\n                return self._get_type_and_shape()\n            elif bpp == 8:\n                extra_dims = []\n            elif bpp == 24:\n                extra_dims = [3]\n            elif bpp == 32:\n                extra_dims = [4]\n            else:  # pragma: no cover\n                #raise ValueError('Cannot convert %d BPP bitmap' % bpp)\n                # Convert bitmap and call this method again\n                newbitmap = lib.FreeImage_ConvertTo32Bits(bitmap)\n                newbitmap = ctypes.c_void_p(newbitmap)\n                self._set_bitmap(newbitmap)\n                return self._get_type_and_shape()\n        else:\n            extra_dims = FI_TYPES.extra_dims[fi_type]\n        \n        # Return dtype and shape\n        return numpy.dtype(dtype), extra_dims + [w, h], bpp\n    \n    def quantize(self, quantizer=0, palettesize=256):\n        \"\"\" Quantize the bitmap to make it 8-bit (paletted). Returns a new\n        FIBitmap object.\n        Only for 24 bit images.\n        \"\"\"\n        with self._fi as lib:\n            # New bitmap\n            bitmap = lib.FreeImage_ColorQuantizeEx(self._bitmap, quantizer,\n                                                   palettesize, 0, None)\n            bitmap = ctypes.c_void_p(bitmap)\n            \n            # Check and return\n            if not bitmap:\n                raise ValueError('Could not quantize bitmap \"%s\": %s' % \n                                 (self._filename, \n                                  self._fi._get_error_message()))\n\n            new = FIBitmap(self._fi, self._filename, self._ftype,\n                           self._flags)\n            new._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap))\n            new._fi_type = self._fi_type\n            return new\n    \n# def convert_to_32bit(self):\n#     \"\"\" Convert to 32bit image.\n#     \"\"\"\n#     with self._fi as lib:\n#         # New bitmap\n#         bitmap = lib.FreeImage_ConvertTo32Bits(self._bitmap)\n#         bitmap = ctypes.c_void_p(bitmap)\n#         \n#         # Check and return\n#         if not bitmap:\n#             raise ValueError('Could not convert bitmap to 32bit \"%s\": %s' %\n#                                 (self._filename, \n#                                 self._fi._get_error_message()))\n#         else:\n#             new = FIBitmap(self._fi, self._filename, self._ftype, \n#                             self._flags)\n#             new._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap))\n#             new._fi_type = self._fi_type\n#             return new\n\n\nclass FIMultipageBitmap(FIBaseBitmap):\n    \"\"\" Wrapper for the multipage FI bitmap object.\n    \"\"\" \n    \n    def load_from_filename(self, filename=None):\n        if filename is None:  # pragma: no cover\n            filename = self._filename\n        \n        # Prepare\n        create_new = False\n        read_only = True\n        keep_cache_in_memory = False\n        \n        # Try opening\n        with self._fi as lib:\n            \n            # Create bitmap\n            multibitmap = lib.FreeImage_OpenMultiBitmap(self._ftype, \n                                                        efn(filename), \n                                                        create_new, read_only, \n                                                        keep_cache_in_memory, \n                                                        self._flags)\n            multibitmap = ctypes.c_void_p(multibitmap)\n            \n            # Check\n            if not multibitmap:  # pragma: no cover\n                err = self._fi._get_error_message()\n                raise ValueError('Could not open file \"%s\" as multi-image: %s'\n                                 % (self._filename, err))\n            self._set_bitmap(multibitmap,\n                             (lib.FreeImage_CloseMultiBitmap, multibitmap))\n\n# def load_from_bytes(self, bytes):\n#     with self._fi as lib:\n#         # Create bitmap\n#         fimemory = lib.FreeImage_OpenMemory(\n#                                         ctypes.c_char_p(bytes), len(bytes))\n#         multibitmap = lib.FreeImage_LoadMultiBitmapFromMemory(\n#             self._ftype, ctypes.c_void_p(fimemory), self._flags)\n#         multibitmap = ctypes.c_void_p(multibitmap)\n#         #lib.FreeImage_CloseMemory(ctypes.c_void_p(fimemory))\n#         self._mem = fimemory\n#         self._bytes = bytes\n#         # Check\n#         if not multibitmap:\n#             raise ValueError('Could not load multibitmap \"%s\": %s' \n#                         % (self._filename, self._fi._get_error_message()))\n#         else:\n#             self._set_bitmap(multibitmap, \n#                              (lib.FreeImage_CloseMultiBitmap, multibitmap))\n        \n    def save_to_filename(self, filename=None):\n        if filename is None:  # pragma: no cover\n            filename = self._filename\n        \n        # Prepare\n        create_new = True\n        read_only = False\n        keep_cache_in_memory = False\n        \n        # Open the file\n        # todo: Set flags at close func\n        with self._fi as lib:\n            multibitmap = lib.FreeImage_OpenMultiBitmap(self._ftype, \n                                                        efn(filename),\n                                                        create_new, read_only, \n                                                        keep_cache_in_memory, \n                                                        0)\n            multibitmap = ctypes.c_void_p(multibitmap)\n        \n            # Check\n            if not multibitmap:  # pragma: no cover\n                msg = ('Could not open file \"%s\" for writing multi-image: %s' \n                       % (self._filename, self._fi._get_error_message()))\n                raise ValueError(msg)\n            self._set_bitmap(multibitmap,\n                             (lib.FreeImage_CloseMultiBitmap, multibitmap))\n    \n    def __len__(self):\n        with self._fi as lib:\n            return lib.FreeImage_GetPageCount(self._bitmap)\n    \n    def get_page(self, index):\n        \"\"\" Return the sub-bitmap for the given page index.\n        Please close the returned bitmap when done.\n        \"\"\" \n        with self._fi as lib:\n            \n            # Create low-level bitmap in freeimage\n            bitmap = lib.FreeImage_LockPage(self._bitmap, index)\n            bitmap = ctypes.c_void_p(bitmap)\n            if not bitmap:  # pragma: no cover\n                raise ValueError('Could not open sub-image %i in %r: %s' % \n                                 (index, self._filename, \n                                  self._fi._get_error_message()))\n            \n            # Get bitmap object to wrap this bitmap\n            bm = FIBitmap(self._fi, self._filename, self._ftype, self._flags)\n            bm._set_bitmap(bitmap, (lib.FreeImage_UnlockPage, self._bitmap, \n                           bitmap, False))\n            return bm\n    \n    def append_bitmap(self, bitmap):\n        \"\"\" Add a sub-bitmap to the multi-page bitmap.\n        \"\"\" \n        with self._fi as lib:\n            # no return value\n            lib.FreeImage_AppendPage(self._bitmap, bitmap._bitmap)\n\n\n# Create instance\nfi = Freeimage()\n"
  },
  {
    "path": "core/lib/imageio/plugins/freeimage.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2015, imageio contributors\n# imageio is distributed under the terms of the (new) BSD License.\n\n\"\"\" Plugin that wraps the freeimage lib. The wrapper for Freeimage is\npart of the core of imageio, but it's functionality is exposed via\nthe plugin system (therefore this plugin is very thin).\n\"\"\"\n\nfrom __future__ import absolute_import, print_function, division\n\nimport numpy as np\n\nfrom .. import formats\nfrom ..core import Format, image_as_uint\nfrom ._freeimage import fi, IO_FLAGS, FNAME_PER_PLATFORM  # noqa\n\n\n# todo: support files with only meta data\n\n\nclass FreeimageFormat(Format):\n    \"\"\" This is the default format used for FreeImage. Each Freeimage\n    format has the 'flags' keyword argument. See the Freeimage\n    documentation for more information.\n    \n    Parameters for reading\n    ----------------------\n    flags : int\n        A freeimage-specific option. In most cases we provide explicit\n        parameters for influencing image reading.\n    \n    Parameters for saving\n    ----------------------\n    flags : int\n        A freeimage-specific option. In most cases we provide explicit\n        parameters for influencing image saving.\n    \"\"\"\n    \n    _modes = 'i'\n    \n    @property\n    def fif(self):\n        return self._fif  # Set when format is created\n    \n    def _can_read(self, request):\n        # Ask freeimage if it can read it, maybe ext missing\n        if fi.has_lib():\n            if not hasattr(request, '_fif'):\n                try:\n                    request._fif = fi.getFIF(request.filename, 'r', \n                                             request.firstbytes)\n                except Exception:  # pragma: no cover\n                    request._fif = -1\n            if request._fif == self.fif:\n                return True\n    \n    def _can_write(self, request):\n        # Ask freeimage, because we are not aware of all formats\n        if fi.has_lib():\n            if not hasattr(request, '_fif'):\n                try:\n                    request._fif = fi.getFIF(request.filename, 'w')\n                except Exception:  # pragma: no cover\n                    request._fif = -1\n            if request._fif is self.fif:\n                return True\n    \n    # --\n    \n    class Reader(Format.Reader):\n        \n        def _get_length(self):\n            return 1\n        \n        def _open(self, flags=0):\n            self._bm = fi.create_bitmap(self.request.filename, \n                                        self.format.fif, flags)\n            self._bm.load_from_filename(self.request.get_local_filename())\n        \n        def _close(self):\n            self._bm.close()\n        \n        def _get_data(self, index):\n            if index != 0:\n                raise IndexError('This format only supports singleton images.')\n            return self._bm.get_image_data(), self._bm.get_meta_data()\n        \n        def _get_meta_data(self, index):\n            if not (index is None or index == 0):\n                raise IndexError()\n            return self._bm.get_meta_data()\n    \n    # --\n    \n    class Writer(Format.Writer):\n        \n        def _open(self, flags=0):        \n            self._flags = flags  # Store flags for later use\n            self._bm = None\n            self._is_set = False  # To prevent appending more than one image\n            self._meta = {}\n        \n        def _close(self):\n            # Set global meta data\n            self._bm.set_meta_data(self._meta)\n            # Write and close\n            self._bm.save_to_filename(self.request.get_local_filename())\n            self._bm.close()\n        \n        def _append_data(self, im, meta):    \n            # Check if set\n            if not self._is_set:\n                self._is_set = True\n            else:\n                raise RuntimeError('Singleton image; '\n                                   'can only append image data once.')\n            # Pop unit dimension for grayscale images\n            if im.ndim == 3 and im.shape[-1] == 1:\n                im = im[:, :, 0]\n            # Lazy instantaion of the bitmap, we need image data\n            if self._bm is None:\n                self._bm = fi.create_bitmap(self.request.filename, \n                                            self.format.fif, self._flags)\n                self._bm.allocate(im)\n            # Set data\n            self._bm.set_image_data(im)\n            # There is no distinction between global and per-image meta data \n            # for singleton images\n            self._meta = meta  \n        \n        def _set_meta_data(self, meta):\n            self._meta = meta\n\n\n## Special plugins\n\n# todo: there is also FIF_LOAD_NOPIXELS, \n# but perhaps that should be used with get_meta_data.\n\nclass FreeimageBmpFormat(FreeimageFormat):\n    \"\"\" A BMP format based on the Freeimage library.\n    \n    This format supports grayscale, RGB and RGBA images.\n    \n    Parameters for saving\n    ---------------------\n    compression : bool\n        Whether to compress the bitmap using RLE when saving. Default False.\n        It seems this does not always work, but who cares, you should use\n        PNG anyway.\n    \n    \"\"\"\n    \n    class Writer(FreeimageFormat.Writer):\n        def _open(self, flags=0, compression=False):\n            # Build flags from kwargs\n            flags = int(flags)\n            if compression:\n                flags |= IO_FLAGS.BMP_SAVE_RLE\n            else:\n                flags |= IO_FLAGS.BMP_DEFAULT\n            # Act as usual, but with modified flags\n            return FreeimageFormat.Writer._open(self, flags)\n        \n        def _append_data(self, im, meta):\n            im = image_as_uint(im, bitdepth=8)\n            return FreeimageFormat.Writer._append_data(self, im, meta)\n\n\nclass FreeimagePngFormat(FreeimageFormat):\n    \"\"\" A PNG format based on the Freeimage library.\n    \n    This format supports grayscale, RGB and RGBA images.\n    \n    Parameters for reading\n    ----------------------\n    ignoregamma : bool\n        Avoid gamma correction. Default False.\n    \n    Parameters for saving\n    ---------------------\n    compression : {0, 1, 6, 9}\n        The compression factor. Higher factors result in more\n        compression at the cost of speed. Note that PNG compression is\n        always lossless. Default 9.\n    quantize : int\n        If specified, turn the given RGB or RGBA image in a paletted image\n        for more efficient storage. The value should be between 2 and 256.\n        If the value of 0 the image is not quantized.\n    interlaced : bool\n        Save using Adam7 interlacing. Default False.\n    \"\"\"\n    \n    class Reader(FreeimageFormat.Reader):\n        def _open(self, flags=0, ignoregamma=False):\n            # Build flags from kwargs\n            flags = int(flags)        \n            if ignoregamma:\n                flags |= IO_FLAGS.PNG_IGNOREGAMMA\n            # Enter as usual, with modified flags\n            return FreeimageFormat.Reader._open(self, flags)\n    \n    # -- \n    \n    class Writer(FreeimageFormat.Writer):\n        def _open(self, flags=0, compression=9, quantize=0, interlaced=False):\n            compression_map = {0: IO_FLAGS.PNG_Z_NO_COMPRESSION,\n                               1: IO_FLAGS.PNG_Z_BEST_SPEED,\n                               6: IO_FLAGS.PNG_Z_DEFAULT_COMPRESSION,\n                               9: IO_FLAGS.PNG_Z_BEST_COMPRESSION, }\n            # Build flags from kwargs\n            flags = int(flags)\n            if interlaced:\n                flags |= IO_FLAGS.PNG_INTERLACED\n            try:\n                flags |= compression_map[compression]\n            except KeyError:\n                raise ValueError('Png compression must be 0, 1, 6, or 9.')\n            # Act as usual, but with modified flags\n            return FreeimageFormat.Writer._open(self, flags)\n        \n        def _append_data(self, im, meta):\n            if str(im.dtype) == 'uint16':\n                im = image_as_uint(im, bitdepth=16)\n            else:\n                im = image_as_uint(im, bitdepth=8)\n            FreeimageFormat.Writer._append_data(self, im, meta)\n            # Quantize?\n            q = int(self.request.kwargs.get('quantize', False))\n            if not q:\n                pass\n            elif not (im.ndim == 3 and im.shape[-1] == 3):\n                raise ValueError('Can only quantize RGB images')\n            elif q < 2 or q > 256:\n                raise ValueError('PNG quantize param must be 2..256')\n            else:\n                bm = self._bm.quantize(0, q)\n                self._bm.close()\n                self._bm = bm\n\n\nclass FreeimageJpegFormat(FreeimageFormat):\n    \"\"\" A JPEG format based on the Freeimage library.\n    \n    This format supports grayscale and RGB images.\n    \n    Parameters for reading\n    ----------------------\n    exifrotate : bool\n        Automatically rotate the image according to the exif flag.\n        Default True. If 2 is given, do the rotation in Python instead\n        of freeimage.\n    quickread : bool\n        Read the image more quickly, at the expense of quality. \n        Default False.\n    \n    Parameters for saving\n    ---------------------\n    quality : scalar\n        The compression factor of the saved image (1..100), higher\n        numbers result in higher quality but larger file size. Default 75.\n    progressive : bool\n        Save as a progressive JPEG file (e.g. for images on the web).\n        Default False.\n    optimize : bool\n        On saving, compute optimal Huffman coding tables (can reduce a\n        few percent of file size). Default False.\n    baseline : bool\n        Save basic JPEG, without metadata or any markers. Default False.\n    \n    \"\"\"\n    \n    class Reader(FreeimageFormat.Reader):\n        def _open(self, flags=0, exifrotate=True, quickread=False):\n            # Build flags from kwargs\n            flags = int(flags)        \n            if exifrotate and exifrotate != 2:\n                flags |= IO_FLAGS.JPEG_EXIFROTATE\n            if not quickread:\n                flags |= IO_FLAGS.JPEG_ACCURATE\n            # Enter as usual, with modified flags\n            return FreeimageFormat.Reader._open(self, flags)\n        \n        def _get_data(self, index):\n            im, meta = FreeimageFormat.Reader._get_data(self, index)\n            im = self._rotate(im, meta)\n            return im, meta\n        \n        def _rotate(self, im, meta):\n            \"\"\" Use Orientation information from EXIF meta data to \n            orient the image correctly. Freeimage is also supposed to\n            support that, and I am pretty sure it once did, but now it\n            does not, so let's just do it in Python.\n            Edit: and now it works again, just leave in place as a fallback.\n            \"\"\"\n            if self.request.kwargs.get('exifrotate', None) == 2:\n                try:\n                    ori = meta['EXIF_MAIN']['Orientation']\n                except KeyError:  # pragma: no cover\n                    pass  # Orientation not available\n                else:  # pragma: no cover - we cannot touch all cases\n                    # www.impulseadventure.com/photo/exif-orientation.html\n                    if ori in [1, 2]:\n                        pass\n                    if ori in [3, 4]:\n                        im = np.rot90(im, 2)\n                    if ori in [5, 6]:\n                        im = np.rot90(im, 3)\n                    if ori in [7, 8]:\n                        im = np.rot90(im)\n                    if ori in [2, 4, 5, 7]:  # Flipped cases (rare)\n                        im = np.fliplr(im)\n            return im\n    \n    # --\n        \n    class Writer(FreeimageFormat.Writer):\n        def _open(self, flags=0, quality=75, progressive=False, optimize=False,\n                  baseline=False):\n            # Test quality\n            quality = int(quality)\n            if quality < 1 or quality > 100:\n                raise ValueError('JPEG quality should be between 1 and 100.')\n            # Build flags from kwargs\n            flags = int(flags)\n            flags |= quality\n            if progressive:\n                flags |= IO_FLAGS.JPEG_PROGRESSIVE\n            if optimize:\n                flags |= IO_FLAGS.JPEG_OPTIMIZE\n            if baseline:\n                flags |= IO_FLAGS.JPEG_BASELINE\n            # Act as usual, but with modified flags\n            return FreeimageFormat.Writer._open(self, flags)\n        \n        def _append_data(self, im, meta):\n            if im.ndim == 3 and im.shape[-1] == 4:\n                raise IOError('JPEG does not support alpha channel.')\n            im = image_as_uint(im, bitdepth=8)\n            return FreeimageFormat.Writer._append_data(self, im, meta)\n\n\n## Create the formats\n\nSPECIAL_CLASSES = {'jpeg': FreeimageJpegFormat,\n                   'png': FreeimagePngFormat,\n                   'bmp': FreeimageBmpFormat,\n                   'gif': None,  # defined in freeimagemulti\n                   'ico': None,  # defined in freeimagemulti\n                   'mng': None,  # defined in freeimagemulti\n                   }\n\n# rename TIFF to make way for the tiffile plugin\nNAME_MAP = {'TIFF': 'FI_TIFF'}\n\n# This is a dump of supported FreeImage formats on Linux fi verion 3.16.0\n# > imageio.plugins.freeimage.create_freeimage_formats()\n# > for i in sorted(imageio.plugins.freeimage.fiformats): print('%r,' % (i, ))\nfiformats = [\n    ('BMP', 0, 'Windows or OS/2 Bitmap', 'bmp'),\n    ('CUT', 21, 'Dr. Halo', 'cut'),\n    ('DDS', 24, 'DirectX Surface', 'dds'),\n    ('EXR', 29, 'ILM OpenEXR', 'exr'),\n    ('G3', 27, 'Raw fax format CCITT G.3', 'g3'),\n    ('GIF', 25, 'Graphics Interchange Format', 'gif'),\n    ('HDR', 26, 'High Dynamic Range Image', 'hdr'),\n    ('ICO', 1, 'Windows Icon', 'ico'),\n    ('IFF', 5, 'IFF Interleaved Bitmap', 'iff,lbm'),\n    ('J2K', 30, 'JPEG-2000 codestream', 'j2k,j2c'),\n    ('JNG', 3, 'JPEG Network Graphics', 'jng'),\n    ('JP2', 31, 'JPEG-2000 File Format', 'jp2'),\n    ('JPEG', 2, 'JPEG - JFIF Compliant', 'jpg,jif,jpeg,jpe'),\n    ('JPEG-XR', 36, 'JPEG XR image format', 'jxr,wdp,hdp'),\n    ('KOALA', 4, 'C64 Koala Graphics', 'koa'),\n    ('MNG', 6, 'Multiple-image Network Graphics', 'mng'),\n    ('PBM', 7, 'Portable Bitmap (ASCII)', 'pbm'),\n    ('PBMRAW', 8, 'Portable Bitmap (RAW)', 'pbm'),\n    ('PCD', 9, 'Kodak PhotoCD', 'pcd'),\n    ('PCX', 10, 'Zsoft Paintbrush', 'pcx'),\n    ('PFM', 32, 'Portable floatmap', 'pfm'),\n    ('PGM', 11, 'Portable Greymap (ASCII)', 'pgm'),\n    ('PGMRAW', 12, 'Portable Greymap (RAW)', 'pgm'),\n    ('PICT', 33, 'Macintosh PICT', 'pct,pict,pic'),\n    ('PNG', 13, 'Portable Network Graphics', 'png'),\n    ('PPM', 14, 'Portable Pixelmap (ASCII)', 'ppm'),\n    ('PPMRAW', 15, 'Portable Pixelmap (RAW)', 'ppm'),\n    ('PSD', 20, 'Adobe Photoshop', 'psd'),\n    ('RAS', 16, 'Sun Raster Image', 'ras'),\n    ('RAW', 34, 'RAW camera image', '3fr,arw,bay,bmq,cap,cine,cr2,crw,cs1,dc2,'\n     'dcr,drf,dsc,dng,erf,fff,ia,iiq,k25,kc2,kdc,mdc,mef,mos,mrw,nef,nrw,orf,'\n     'pef,ptx,pxn,qtk,raf,raw,rdc,rw2,rwl,rwz,sr2,srf,srw,sti'),\n    ('SGI', 28, 'SGI Image Format', 'sgi,rgb,rgba,bw'),\n    ('TARGA', 17, 'Truevision Targa', 'tga,targa'),\n    ('TIFF', 18, 'Tagged Image File Format', 'tif,tiff'),\n    ('WBMP', 19, 'Wireless Bitmap', 'wap,wbmp,wbm'),\n    ('WebP', 35, 'Google WebP image format', 'webp'),\n    ('XBM', 22, 'X11 Bitmap Format', 'xbm'),\n    ('XPM', 23, 'X11 Pixmap Format', 'xpm'),\n]\n\n\ndef _create_predefined_freeimage_formats():\n    \n    for name, i, des, ext in fiformats:\n        name = NAME_MAP.get(name, name)\n        # Get class for format\n        FormatClass = SPECIAL_CLASSES.get(name.lower(), FreeimageFormat)\n        if FormatClass:\n            # Create Format and add\n            format = FormatClass(name, des, ext, FormatClass._modes)\n            format._fif = i\n            formats.add_format(format) \n\n\ndef create_freeimage_formats():\n    \"\"\" By default, imageio registers a list of predefined formats\n    that freeimage can handle. If your version of imageio can handle\n    more formats, you can call this function to register them.\n    \"\"\"\n    fiformats[:] = []\n    \n    # Freeimage available?\n    if fi is None:  # pragma: no cover\n        return \n    \n    # Init\n    lib = fi._lib\n    \n    # Create formats        \n    for i in range(lib.FreeImage_GetFIFCount()):\n        if lib.FreeImage_IsPluginEnabled(i):                \n            # Get info\n            name = lib.FreeImage_GetFormatFromFIF(i).decode('ascii')\n            des = lib.FreeImage_GetFIFDescription(i).decode('ascii')\n            ext = lib.FreeImage_GetFIFExtensionList(i).decode('ascii')\n            fiformats.append((name, i, des, ext))\n            name = NAME_MAP.get(name, name)\n            # Get class for format\n            FormatClass = SPECIAL_CLASSES.get(name.lower(), FreeimageFormat)\n            if FormatClass:\n                # Create Format and add\n                format = FormatClass(name, des, ext, FormatClass._modes)\n                format._fif = i\n                formats.add_format(format, overwrite=True)\n\n\n_create_predefined_freeimage_formats()\n"
  },
  {
    "path": "core/lib/imageio/resources/shipped_resources_go_here",
    "content": ""
  },
  {
    "path": "core/lib/imageio/testing.py",
    "content": "# -*- coding: utf-8 -*-\n# Copyright (c) 2015, imageio contributors\n# Distributed under the (new) BSD License. See LICENSE.txt for more info.\n\n\"\"\" Functionality used for testing. This code itself is not covered in tests.\n\"\"\"\n\nfrom __future__ import absolute_import, print_function, division\n\nimport os\nimport sys\nimport inspect\nimport shutil\nimport atexit\n\nimport pytest\n\n# Get root dir\nTHIS_DIR = os.path.abspath(os.path.dirname(__file__))\nROOT_DIR = THIS_DIR\nfor i in range(9):\n    ROOT_DIR = os.path.dirname(ROOT_DIR)\n    if os.path.isfile(os.path.join(ROOT_DIR, '.gitignore')):\n        break\n\n\nSTYLE_IGNORES = ['E226', \n                 'E241', \n                 'E265', \n                 'E266',  # too many leading '#' for block comment\n                 'E402',  # module level import not at top of file\n                 'E731',  # do not assign a lambda expression, use a def\n                 'W291', \n                 'W293',\n                 'W503',  # line break before binary operator\n                 ]\n\n\n## Functions to use in tests\n\ndef run_tests_if_main(show_coverage=False):\n    \"\"\" Run tests in a given file if it is run as a script\n    \n    Coverage is reported for running this single test. Set show_coverage to\n    launch the report in the web browser.\n    \"\"\"\n    local_vars = inspect.currentframe().f_back.f_locals\n    if not local_vars.get('__name__', '') == '__main__':\n        return\n    # we are in a \"__main__\"\n    os.chdir(ROOT_DIR)\n    fname = str(local_vars['__file__'])\n    _clear_imageio()\n    _enable_faulthandler()\n    pytest.main('-v -x --color=yes --cov imageio '\n                '--cov-config .coveragerc --cov-report html %s' % repr(fname))\n    if show_coverage:\n        import webbrowser\n        fname = os.path.join(ROOT_DIR, 'htmlcov', 'index.html')\n        webbrowser.open_new_tab(fname)\n\n\n_the_test_dir = None\n\n\ndef get_test_dir():\n    global _the_test_dir\n    if _the_test_dir is None:\n        # Define dir\n        from imageio.core import appdata_dir\n        _the_test_dir = os.path.join(appdata_dir('imageio'), 'testdir')\n        # Clear and create it now\n        clean_test_dir(True)\n        os.makedirs(_the_test_dir)\n        os.makedirs(os.path.join(_the_test_dir, 'images'))\n        # And later\n        atexit.register(clean_test_dir)\n    return _the_test_dir\n\n\ndef clean_test_dir(strict=False):\n    if os.path.isdir(_the_test_dir):\n        try:\n            shutil.rmtree(_the_test_dir)\n        except Exception:\n            if strict:\n                raise\n        \n\ndef need_internet():\n    if os.getenv('IMAGEIO_NO_INTERNET', '').lower() in ('1', 'true', 'yes'):\n        pytest.skip('No internet')\n\n\n## Functions to use from make\n\ndef test_unit(cov_report='term'):\n    \"\"\" Run all unit tests. Returns exit code.\n    \"\"\"\n    orig_dir = os.getcwd()\n    os.chdir(ROOT_DIR)\n    try:\n        _clear_imageio()\n        _enable_faulthandler()\n        return pytest.main('-v --cov imageio --cov-config .coveragerc '\n                           '--cov-report %s tests' % cov_report)\n    finally:\n        os.chdir(orig_dir)\n        import imageio\n        print('Tests were performed on', str(imageio))\n\n\ndef test_style():\n    \"\"\" Test style using flake8\n    \"\"\"\n    # Test if flake is there\n    try:\n        from flake8.main import main  # noqa\n    except ImportError:\n        print('Skipping flake8 test, flake8 not installed')\n        return\n    \n    # Reporting\n    print('Running flake8 on %s' % ROOT_DIR)\n    sys.stdout = FileForTesting(sys.stdout)\n    \n    # Init\n    ignores = STYLE_IGNORES.copy()\n    fail = False\n    count = 0\n    \n    # Iterate over files\n    for dir, dirnames, filenames in os.walk(ROOT_DIR):\n        dir = os.path.relpath(dir, ROOT_DIR)\n        # Skip this dir?\n        exclude_dirs = set(['.git', 'docs', 'build', 'dist', '__pycache__'])\n        if exclude_dirs.intersection(dir.split(os.path.sep)):\n            continue\n        # Check all files ...\n        for fname in filenames:\n            if fname.endswith('.py'):\n                # Get test options for this file\n                filename = os.path.join(ROOT_DIR, dir, fname)\n                skip, extra_ignores = _get_style_test_options(filename)\n                if skip:\n                    continue\n                # Test\n                count += 1\n                thisfail = _test_style(filename, ignores + extra_ignores)\n                if thisfail:\n                    fail = True\n                    print('----')\n                sys.stdout.flush()\n    \n    # Report result\n    sys.stdout.revert()\n    if not count:\n        raise RuntimeError('    Arg! flake8 did not check any files')\n    elif fail:\n        raise RuntimeError('    Arg! flake8 failed (checked %i files)' % count)\n    else:\n        print('    Hooray! flake8 passed (checked %i files)' % count)\n\n\n## Requirements\n\ndef _enable_faulthandler():\n    \"\"\" Enable faulthandler (if we can), so that we get tracebacks\n    on segfaults.\n    \"\"\"\n    try:\n        import faulthandler\n        faulthandler.enable()\n        print('Faulthandler enabled')\n    except Exception:\n        print('Could not enable faulthandler')\n\n\ndef _clear_imageio():\n    # Remove ourselves from sys.modules to force an import\n    for key in list(sys.modules.keys()):\n        if key.startswith('imageio'):\n            del sys.modules[key]\n\n\nclass FileForTesting(object):\n    \"\"\" Alternative to stdout that makes path relative to ROOT_DIR\n    \"\"\"\n    def __init__(self, original):\n        self._original = original\n    \n    def write(self, msg):\n        if msg.startswith(ROOT_DIR):\n            msg = os.path.relpath(msg, ROOT_DIR)\n        self._original.write(msg)\n        self._original.flush()\n    \n    def flush(self):\n        self._original.flush()\n    \n    def revert(self):\n        sys.stdout = self._original\n\n\ndef _get_style_test_options(filename):\n    \"\"\" Returns (skip, ignores) for the specifies source file.\n    \"\"\"\n    skip = False\n    ignores = []\n    text = open(filename, 'rb').read().decode('utf-8')\n    # Iterate over lines\n    for i, line in enumerate(text.splitlines()):\n        if i > 20:\n            break\n        if line.startswith('# styletest:'):\n            if 'skip' in line:\n                skip = True\n            elif 'ignore' in line:\n                words = line.replace(',', ' ').split(' ')\n                words = [w.strip() for w in words if w.strip()]\n                words = [w for w in words if \n                         (w[1:].isnumeric() and w[0] in 'EWFCN')]\n                ignores.extend(words)\n    return skip, ignores\n\n\ndef _test_style(filename, ignore):\n    \"\"\" Test style for a certain file.\n    \"\"\"\n    if isinstance(ignore, (list, tuple)):\n        ignore = ','.join(ignore)\n    \n    orig_dir = os.getcwd()\n    orig_argv = sys.argv\n    \n    os.chdir(ROOT_DIR)\n    sys.argv[1:] = [filename]\n    sys.argv.append('--ignore=' + ignore)\n    try:\n        from flake8.main import main\n        main()\n    except SystemExit as ex:\n        if ex.code in (None, 0):\n            return False\n        else:\n            return True\n    finally:\n        os.chdir(orig_dir)\n        sys.argv[:] = orig_argv\n"
  },
  {
    "path": "core/lib/imghdr.py",
    "content": "\"\"\"Recognize image file formats based on their first few bytes.\"\"\"\n\nfrom os import PathLike\n\n__all__ = [\"what\"]\n\n#-------------------------#\n# Recognize image headers #\n#-------------------------#\n\ndef what(file, h=None):\n    f = None\n    try:\n        if h is None:\n            if isinstance(file, (str, PathLike)):\n                f = open(file, 'rb')\n                h = f.read(32)\n            else:\n                location = file.tell()\n                h = file.read(32)\n                file.seek(location)\n        for tf in tests:\n            res = tf(h, f)\n            if res:\n                return res\n    finally:\n        if f: f.close()\n    return None\n\n\n#---------------------------------#\n# Subroutines per image file type #\n#---------------------------------#\n\ntests = []\n\ndef test_jpeg(h, f):\n    \"\"\"JPEG data in JFIF or Exif format\"\"\"\n    if h[6:10] in (b'JFIF', b'Exif'):\n        return 'jpeg'\n\ntests.append(test_jpeg)\n\ndef test_png(h, f):\n    if h.startswith(b'\\211PNG\\r\\n\\032\\n'):\n        return 'png'\n\ntests.append(test_png)\n\ndef test_gif(h, f):\n    \"\"\"GIF ('87 and '89 variants)\"\"\"\n    if h[:6] in (b'GIF87a', b'GIF89a'):\n        return 'gif'\n\ntests.append(test_gif)\n\ndef test_tiff(h, f):\n    \"\"\"TIFF (can be in Motorola or Intel byte order)\"\"\"\n    if h[:2] in (b'MM', b'II'):\n        return 'tiff'\n\ntests.append(test_tiff)\n\ndef test_rgb(h, f):\n    \"\"\"SGI image library\"\"\"\n    if h.startswith(b'\\001\\332'):\n        return 'rgb'\n\ntests.append(test_rgb)\n\ndef test_pbm(h, f):\n    \"\"\"PBM (portable bitmap)\"\"\"\n    if len(h) >= 3 and \\\n        h[0] == ord(b'P') and h[1] in b'14' and h[2] in b' \\t\\n\\r':\n        return 'pbm'\n\ntests.append(test_pbm)\n\ndef test_pgm(h, f):\n    \"\"\"PGM (portable graymap)\"\"\"\n    if len(h) >= 3 and \\\n        h[0] == ord(b'P') and h[1] in b'25' and h[2] in b' \\t\\n\\r':\n        return 'pgm'\n\ntests.append(test_pgm)\n\ndef test_ppm(h, f):\n    \"\"\"PPM (portable pixmap)\"\"\"\n    if len(h) >= 3 and \\\n        h[0] == ord(b'P') and h[1] in b'36' and h[2] in b' \\t\\n\\r':\n        return 'ppm'\n\ntests.append(test_ppm)\n\ndef test_rast(h, f):\n    \"\"\"Sun raster file\"\"\"\n    if h.startswith(b'\\x59\\xA6\\x6A\\x95'):\n        return 'rast'\n\ntests.append(test_rast)\n\ndef test_xbm(h, f):\n    \"\"\"X bitmap (X10 or X11)\"\"\"\n    if h.startswith(b'#define '):\n        return 'xbm'\n\ntests.append(test_xbm)\n\ndef test_bmp(h, f):\n    if h.startswith(b'BM'):\n        return 'bmp'\n\ntests.append(test_bmp)\n\ndef test_webp(h, f):\n    if h.startswith(b'RIFF') and h[8:12] == b'WEBP':\n        return 'webp'\n\ntests.append(test_webp)\n\ndef test_exr(h, f):\n    if h.startswith(b'\\x76\\x2f\\x31\\x01'):\n        return 'exr'\n\ntests.append(test_exr)\n\n#--------------------#\n# Small test program #\n#--------------------#\n\ndef test():\n    import sys\n    recursive = 0\n    if sys.argv[1:] and sys.argv[1] == '-r':\n        del sys.argv[1:2]\n        recursive = 1\n    try:\n        if sys.argv[1:]:\n            testall(sys.argv[1:], recursive, 1)\n        else:\n            testall(['.'], recursive, 1)\n    except KeyboardInterrupt:\n        sys.stderr.write('\\n[Interrupted]\\n')\n        sys.exit(1)\n\ndef testall(list, recursive, toplevel):\n    import sys\n    import os\n    for filename in list:\n        if os.path.isdir(filename):\n            print(filename + '/:', end=' ')\n            if recursive or toplevel:\n                print('recursing down:')\n                import glob\n                names = glob.glob(os.path.join(filename, '*'))\n                testall(names, recursive, 0)\n            else:\n                print('*** directory (use -r) ***')\n        else:\n            print(filename + ':', end=' ')\n            sys.stdout.flush()\n            try:\n                print(what(filename))\n            except OSError:\n                print('*** not found ***')\n\nif __name__ == '__main__':\n    test()\n"
  },
  {
    "path": "core/lib/shapefile.py",
    "content": "\"\"\"\r\nshapefile.py\r\nProvides read and write support for ESRI Shapefiles.\r\nauthor: jlawhead<at>geospatialpython.com\r\nversion: 2.1.0\r\nCompatible with Python versions 2.7-3.x\r\n\"\"\"\r\n\r\n__version__ = \"2.1.0\"\r\n\r\nfrom struct import pack, unpack, calcsize, error, Struct\r\nimport os\r\nimport sys\r\nimport time\r\nimport array\r\nimport tempfile\r\nimport warnings\r\nimport io\r\nfrom datetime import date\r\n\r\n\r\n# Constants for shape types\r\nNULL = 0\r\nPOINT = 1\r\nPOLYLINE = 3\r\nPOLYGON = 5\r\nMULTIPOINT = 8\r\nPOINTZ = 11\r\nPOLYLINEZ = 13\r\nPOLYGONZ = 15\r\nMULTIPOINTZ = 18\r\nPOINTM = 21\r\nPOLYLINEM = 23\r\nPOLYGONM = 25\r\nMULTIPOINTM = 28\r\nMULTIPATCH = 31\r\n\r\nSHAPETYPE_LOOKUP = {\r\n    0: 'NULL',\r\n    1: 'POINT',\r\n    3: 'POLYLINE',\r\n    5: 'POLYGON',\r\n    8: 'MULTIPOINT',\r\n    11: 'POINTZ',\r\n    13: 'POLYLINEZ',\r\n    15: 'POLYGONZ',\r\n    18: 'MULTIPOINTZ',\r\n    21: 'POINTM',\r\n    23: 'POLYLINEM',\r\n    25: 'POLYGONM',\r\n    28: 'MULTIPOINTM',\r\n    31: 'MULTIPATCH'}\r\n\r\nTRIANGLE_STRIP = 0\r\nTRIANGLE_FAN = 1\r\nOUTER_RING = 2\r\nINNER_RING = 3\r\nFIRST_RING = 4\r\nRING = 5\r\n\r\nPARTTYPE_LOOKUP = {\r\n    0: 'TRIANGLE_STRIP',\r\n    1: 'TRIANGLE_FAN',\r\n    2: 'OUTER_RING',\r\n    3: 'INNER_RING',\r\n    4: 'FIRST_RING',\r\n    5: 'RING'}\r\n\r\n\r\n# Python 2-3 handling\r\n\r\nPYTHON3 = sys.version_info[0] == 3\r\n\r\nif PYTHON3:\r\n    xrange = range\r\n    izip = zip\r\nelse:\r\n    from itertools import izip\r\n\r\n\r\n# Helpers\r\n\r\nMISSING = [None,'']\r\nNODATA = -10e38 # as per the ESRI shapefile spec, only used for m-values. \r\n\r\nif PYTHON3:\r\n    def b(v, encoding='utf-8', encodingErrors='strict'):\r\n        if isinstance(v, str):\r\n            # For python 3 encode str to bytes.\r\n            return v.encode(encoding, encodingErrors)\r\n        elif isinstance(v, bytes):\r\n            # Already bytes.\r\n            return v\r\n        elif v is None:\r\n            # Since we're dealing with text, interpret None as \"\"\r\n            return b\"\"\r\n        else:\r\n            # Force string representation.\r\n            return str(v).encode(encoding, encodingErrors)\r\n\r\n    def u(v, encoding='utf-8', encodingErrors='strict'):\r\n        if isinstance(v, bytes):\r\n            # For python 3 decode bytes to str.\r\n            return v.decode(encoding, encodingErrors)\r\n        elif isinstance(v, str):\r\n            # Already str.\r\n            return v\r\n        elif v is None:\r\n            # Since we're dealing with text, interpret None as \"\"\r\n            return \"\"\r\n        else:\r\n            # Force string representation.\r\n            return bytes(v).decode(encoding, encodingErrors)\r\n\r\n    def is_string(v):\r\n        return isinstance(v, str)\r\n\r\nelse:\r\n    def b(v, encoding='utf-8', encodingErrors='strict'):\r\n        if isinstance(v, unicode):\r\n            # For python 2 encode unicode to bytes.\r\n            return v.encode(encoding, encodingErrors)\r\n        elif isinstance(v, bytes):\r\n            # Already bytes.\r\n            return v\r\n        elif v is None:\r\n            # Since we're dealing with text, interpret None as \"\"\r\n            return \"\"\r\n        else:\r\n            # Force string representation.\r\n            return unicode(v).encode(encoding, encodingErrors)\r\n\r\n    def u(v, encoding='utf-8', encodingErrors='strict'):\r\n        if isinstance(v, bytes):\r\n            # For python 2 decode bytes to unicode.\r\n            return v.decode(encoding, encodingErrors)\r\n        elif isinstance(v, unicode):\r\n            # Already unicode.\r\n            return v\r\n        elif v is None:\r\n            # Since we're dealing with text, interpret None as \"\"\r\n            return u\"\"\r\n        else:\r\n            # Force string representation.\r\n            return bytes(v).decode(encoding, encodingErrors)\r\n\r\n    def is_string(v):\r\n        return isinstance(v, basestring)\r\n\r\n\r\n# Begin\r\n\r\nclass _Array(array.array):\r\n    \"\"\"Converts python tuples to lits of the appropritate type.\r\n    Used to unpack different shapefile header parts.\"\"\"\r\n    def __repr__(self):\r\n        return str(self.tolist())\r\n\r\ndef signed_area(coords):\r\n    \"\"\"Return the signed area enclosed by a ring using the linear time\r\n    algorithm. A value >= 0 indicates a counter-clockwise oriented ring.\r\n    \"\"\"\r\n    xs, ys = map(list, zip(*coords))\r\n    xs.append(xs[1])\r\n    ys.append(ys[1])\r\n    return sum(xs[i]*(ys[i+1]-ys[i-1]) for i in range(1, len(coords)))/2.0\r\n\r\nclass Shape(object):\r\n    def __init__(self, shapeType=NULL, points=None, parts=None, partTypes=None):\r\n        \"\"\"Stores the geometry of the different shape types\r\n        specified in the Shapefile spec. Shape types are\r\n        usually point, polyline, or polygons. Every shape type\r\n        except the \"Null\" type contains points at some level for\r\n        example verticies in a polygon. If a shape type has\r\n        multiple shapes containing points within a single\r\n        geometry record then those shapes are called parts. Parts\r\n        are designated by their starting index in geometry record's\r\n        list of shapes. For MultiPatch geometry, partTypes designates\r\n        the patch type of each of the parts. \r\n        \"\"\"\r\n        self.shapeType = shapeType\r\n        self.points = points or []\r\n        self.parts = parts or []\r\n        if partTypes:\r\n            self.partTypes = partTypes\r\n\r\n    @property\r\n    def __geo_interface__(self):\r\n        if not self.parts or not self.points:\r\n            Exception('Invalid shape, cannot create GeoJSON representation. Shape type is \"%s\" but does not contain any parts and/or points.' % SHAPETYPE_LOOKUP[self.shapeType])\r\n\r\n        if self.shapeType in [POINT, POINTM, POINTZ]:\r\n            return {\r\n            'type': 'Point',\r\n            'coordinates': tuple(self.points[0])\r\n            }\r\n        elif self.shapeType in [MULTIPOINT, MULTIPOINTM, MULTIPOINTZ]:\r\n            return {\r\n            'type': 'MultiPoint',\r\n            'coordinates': tuple([tuple(p) for p in self.points])\r\n            }\r\n        elif self.shapeType in [POLYLINE, POLYLINEM, POLYLINEZ]:\r\n            if len(self.parts) == 1:\r\n                return {\r\n                'type': 'LineString',\r\n                'coordinates': tuple([tuple(p) for p in self.points])\r\n                }\r\n            else:\r\n                ps = None\r\n                coordinates = []\r\n                for part in self.parts:\r\n                    if ps == None:\r\n                        ps = part\r\n                        continue\r\n                    else:\r\n                        coordinates.append(tuple([tuple(p) for p in self.points[ps:part]]))\r\n                        ps = part\r\n                else:\r\n                    coordinates.append(tuple([tuple(p) for p in self.points[part:]]))\r\n                return {\r\n                'type': 'MultiLineString',\r\n                'coordinates': tuple(coordinates)\r\n                }\r\n        elif self.shapeType in [POLYGON, POLYGONM, POLYGONZ]:\r\n            if len(self.parts) == 1:\r\n                return {\r\n                'type': 'Polygon',\r\n                'coordinates': (tuple([tuple(p) for p in self.points]),)\r\n                }\r\n            else:\r\n                ps = None\r\n                rings = []\r\n                for part in self.parts:\r\n                    if ps == None:\r\n                        ps = part\r\n                        continue\r\n                    else:\r\n                        rings.append(tuple([tuple(p) for p in self.points[ps:part]]))\r\n                        ps = part\r\n                else:\r\n                    rings.append(tuple([tuple(p) for p in self.points[part:]]))\r\n                polys = []\r\n                poly = [rings[0]]\r\n                for ring in rings[1:]:\r\n                    if signed_area(ring) < 0:\r\n                        polys.append(poly)\r\n                        poly = [ring]\r\n                    else:\r\n                        poly.append(ring)\r\n                polys.append(poly)\r\n                if len(polys) == 1:\r\n                    return {\r\n                    'type': 'Polygon',\r\n                    'coordinates': tuple(polys[0])\r\n                    }\r\n                elif len(polys) > 1:\r\n                    return {\r\n                    'type': 'MultiPolygon',\r\n                    'coordinates': polys\r\n                    }\r\n        else:\r\n            raise Exception('Shape type \"%s\" cannot be represented as GeoJSON.' % SHAPETYPE_LOOKUP[self.shapeType])\r\n\r\n    @staticmethod\r\n    def _from_geojson(geoj):\r\n        # create empty shape\r\n        shape = Shape()\r\n        # set shapeType\r\n        geojType = geoj[\"type\"] if geoj else \"Null\"\r\n        if geojType == \"Null\":\r\n            shapeType = NULL\r\n        elif geojType == \"Point\":\r\n            shapeType = POINT\r\n        elif geojType == \"LineString\":\r\n            shapeType = POLYLINE\r\n        elif geojType == \"Polygon\":\r\n            shapeType = POLYGON\r\n        elif geojType == \"MultiPoint\":\r\n            shapeType = MULTIPOINT\r\n        elif geojType == \"MultiLineString\":\r\n            shapeType = POLYLINE\r\n        elif geojType == \"MultiPolygon\":\r\n            shapeType = POLYGON\r\n        else:\r\n            raise Exception(\"Cannot create Shape from GeoJSON type '%s'\" % geojType)\r\n        shape.shapeType = shapeType\r\n        \r\n        # set points and parts\r\n        if geojType == \"Point\":\r\n            shape.points = [ geoj[\"coordinates\"] ]\r\n            shape.parts = [0]\r\n        elif geojType in (\"MultiPoint\",\"LineString\"):\r\n            shape.points = geoj[\"coordinates\"]\r\n            shape.parts = [0]\r\n        elif geojType in (\"Polygon\"):\r\n            points = []\r\n            parts = []\r\n            index = 0\r\n            for i,ext_or_hole in enumerate(geoj[\"coordinates\"]):\r\n                if i == 0 and not signed_area(ext_or_hole) < 0:\r\n                    # flip exterior direction\r\n                    ext_or_hole = list(reversed(ext_or_hole))\r\n                elif i > 0 and not signed_area(ext_or_hole) >= 0:\r\n                    # flip hole direction\r\n                    ext_or_hole = list(reversed(ext_or_hole))\r\n                points.extend(ext_or_hole)\r\n                parts.append(index)\r\n                index += len(ext_or_hole)\r\n            shape.points = points\r\n            shape.parts = parts\r\n        elif geojType in (\"MultiLineString\"):\r\n            points = []\r\n            parts = []\r\n            index = 0\r\n            for linestring in geoj[\"coordinates\"]:\r\n                points.extend(linestring)\r\n                parts.append(index)\r\n                index += len(linestring)\r\n            shape.points = points\r\n            shape.parts = parts\r\n        elif geojType in (\"MultiPolygon\"):\r\n            points = []\r\n            parts = []\r\n            index = 0\r\n            for polygon in geoj[\"coordinates\"]:\r\n                for i,ext_or_hole in enumerate(polygon):\r\n                    if i == 0 and not signed_area(ext_or_hole) < 0:\r\n                        # flip exterior direction\r\n                        ext_or_hole = list(reversed(ext_or_hole))\r\n                    elif i > 0 and not signed_area(ext_or_hole) >= 0:\r\n                        # flip hole direction\r\n                        ext_or_hole = list(reversed(ext_or_hole))\r\n                    points.extend(ext_or_hole)\r\n                    parts.append(index)\r\n                    index += len(ext_or_hole)\r\n            shape.points = points\r\n            shape.parts = parts\r\n        return shape\r\n\r\n    @property\r\n    def shapeTypeName(self):\r\n        return SHAPETYPE_LOOKUP[self.shapeType]\r\n\r\nclass _Record(list):\r\n    \"\"\"\r\n    A class to hold a record. Subclasses list to ensure compatibility with\r\n    former work and allows to use all the optimazations of the builtin list.\r\n    In addition to the list interface, the values of the record\r\n    can also be retrieved using the fields name. Eg. if the dbf contains\r\n    a field ID at position 0, the ID can be retrieved with the position, the field name\r\n    as a key or the field name as an attribute.\r\n\r\n    >>> # Create a Record with one field, normally the record is created by the Reader class\r\n    >>> r = _Record({'ID': 0}, [0])\r\n    >>> print(r[0])\r\n    >>> print(r['ID'])\r\n    >>> print(r.ID)\r\n    \"\"\"\r\n\r\n    def __init__(self, field_positions, values, oid=None):\r\n        \"\"\"\r\n        A Record should be created by the Reader class\r\n\r\n        :param field_positions: A dict mapping field names to field positions\r\n        :param values: A sequence of values\r\n        :param oid: The object id, an int (optional)\r\n        \"\"\"\r\n        self.__field_positions = field_positions\r\n        if oid is not None:\r\n            self.__oid = oid\r\n        else:\r\n            self.__oid = -1\r\n        list.__init__(self, values)\r\n\r\n    def __getattr__(self, item):\r\n        \"\"\"\r\n        __getattr__ is called if an attribute is used that does\r\n        not exist in the normal sense. Eg. r=Record(...), r.ID\r\n        calls r.__getattr__('ID'), but r.index(5) calls list.index(r, 5)\r\n        :param item: The field name, used as attribute\r\n        :return: Value of the field\r\n        :raises: Attribute error, if field does not exist\r\n                and IndexError, if field exists but not values in the Record\r\n        \"\"\"\r\n        try:\r\n            index = self.__field_positions[item]\r\n            return list.__getitem__(self, index)\r\n        except KeyError:\r\n            raise AttributeError('{} is not a field name'.format(item))\r\n        except IndexError:\r\n            raise IndexError('{} found as a field but not enough values available.'.format(item))\r\n\r\n    def __setattr__(self, key, value):\r\n        \"\"\"\r\n        Sets a value of a field attribute\r\n        :param key: The field name\r\n        :param value: the value of that field\r\n        :return: None\r\n        :raises: AttributeError, if key is not a field of the shapefile\r\n        \"\"\"\r\n        if key.startswith('_'):  # Prevent infinite loop when setting mangled attribute\r\n            return list.__setattr__(self, key, value)\r\n        try:\r\n            index = self.__field_positions[key]\r\n            return list.__setitem__(self, index, value)\r\n        except KeyError:\r\n            raise AttributeError('{} is not a field name'.format(key))\r\n\r\n    def __getitem__(self, item):\r\n        \"\"\"\r\n        Extends the normal list item access with\r\n        access using a fieldname\r\n\r\n        Eg. r['ID'], r[0]\r\n        :param item: Either the position of the value or the name of a field\r\n        :return: the value of the field\r\n        \"\"\"\r\n        try:\r\n            return list.__getitem__(self, item)\r\n        except TypeError:\r\n            try:\r\n                index = self.__field_positions[item]\r\n            except KeyError:\r\n                index = None\r\n        if index is not None:\r\n            return list.__getitem__(self, index)\r\n        else:\r\n            raise IndexError('\"{}\" is not a field name and not an int'.format(item))\r\n\r\n    def __setitem__(self, key, value):\r\n        \"\"\"\r\n        Extends the normal list item access with\r\n        access using a fieldname\r\n\r\n        Eg. r['ID']=2, r[0]=2\r\n        :param key: Either the position of the value or the name of a field\r\n        :param value: the new value of the field\r\n        \"\"\"\r\n        try:\r\n            return list.__setitem__(self, key, value)\r\n        except TypeError:\r\n            index = self.__field_positions.get(key)\r\n            if index is not None:\r\n                return list.__setitem__(self, index, value)\r\n            else:\r\n                raise IndexError('{} is not a field name and not an int'.format(key))\r\n\r\n    @property\r\n    def oid(self):\r\n        \"\"\"The index position of the record in the original shapefile\"\"\"\r\n        return self.__oid\r\n\r\n    def as_dict(self):\r\n        \"\"\"\r\n        Returns this Record as a dictionary using the field names as keys\r\n        :return: dict\r\n        \"\"\"\r\n        return dict((f, self[i]) for f, i in self.__field_positions.items())\r\n\r\n    def __repr__(self):\r\n        return 'Record #{}: {}'.format(self.__oid, list(self))\r\n\r\n    def __dir__(self):\r\n        \"\"\"\r\n        Helps to show the field names in an interactive environment like IPython.\r\n        See: http://ipython.readthedocs.io/en/stable/config/integrating.html\r\n\r\n        :return: List of method names and fields\r\n        \"\"\"\r\n        default = list(dir(type(self))) # default list methods and attributes of this class\r\n        fnames = list(self.__field_positions.keys()) # plus field names (random order)\r\n        return default + fnames \r\n        \r\nclass ShapeRecord(object):\r\n    \"\"\"A ShapeRecord object containing a shape along with its attributes.\r\n    Provides the GeoJSON __geo_interface__ to return a Feature dictionary.\"\"\"\r\n    def __init__(self, shape=None, record=None):\r\n        self.shape = shape\r\n        self.record = record\r\n\r\n    @property\r\n    def __geo_interface__(self):\r\n        return {'type': 'Feature',\r\n                'properties': self.record.as_dict(),\r\n                'geometry': self.shape.__geo_interface__}\r\n\r\nclass Shapes(list):\r\n    \"\"\"A class to hold a list of Shape objects. Subclasses list to ensure compatibility with\r\n    former work and allows to use all the optimazations of the builtin list.\r\n    In addition to the list interface, this also provides the GeoJSON __geo_interface__\r\n    to return a GeometryCollection dictionary. \"\"\"\r\n\r\n    def __repr__(self):\r\n        return 'Shapes: {}'.format(list(self))\r\n\r\n    @property\r\n    def __geo_interface__(self):\r\n        return {'type': 'GeometryCollection',\r\n                'geometries': [g.__geo_interface__ for g in self]}\r\n\r\nclass ShapeRecords(list):\r\n    \"\"\"A class to hold a list of ShapeRecord objects. Subclasses list to ensure compatibility with\r\n    former work and allows to use all the optimazations of the builtin list.\r\n    In addition to the list interface, this also provides the GeoJSON __geo_interface__\r\n    to return a FeatureCollection dictionary. \"\"\"\r\n\r\n    def __repr__(self):\r\n        return 'ShapeRecords: {}'.format(list(self))\r\n\r\n    @property\r\n    def __geo_interface__(self):\r\n        return {'type': 'FeatureCollection',\r\n                'features': [f.__geo_interface__ for f in self]}\r\n\r\nclass ShapefileException(Exception):\r\n    \"\"\"An exception to handle shapefile specific problems.\"\"\"\r\n    pass\r\n\r\nclass Reader(object):\r\n    \"\"\"Reads the three files of a shapefile as a unit or\r\n    separately.  If one of the three files (.shp, .shx,\r\n    .dbf) is missing no exception is thrown until you try\r\n    to call a method that depends on that particular file.\r\n    The .shx index file is used if available for efficiency\r\n    but is not required to read the geometry from the .shp\r\n    file. The \"shapefile\" argument in the constructor is the\r\n    name of the file you want to open.\r\n\r\n    You can instantiate a Reader without specifying a shapefile\r\n    and then specify one later with the load() method.\r\n\r\n    Only the shapefile headers are read upon loading. Content\r\n    within each file is only accessed when required and as\r\n    efficiently as possible. Shapefiles are usually not large\r\n    but they can be.\r\n    \"\"\"\r\n    def __init__(self, *args, **kwargs):\r\n        self.shp = None\r\n        self.shx = None\r\n        self.dbf = None\r\n        self.shapeName = \"Not specified\"\r\n        self._offsets = []\r\n        self.shpLength = None\r\n        self.numRecords = None\r\n        self.fields = []\r\n        self.__dbfHdrLength = 0\r\n        self.__fieldposition_lookup = {}\r\n        self.encoding = kwargs.pop('encoding', 'utf-8')\r\n        self.encodingErrors = kwargs.pop('encodingErrors', 'strict')\r\n        # See if a shapefile name was passed as an argument\r\n        if len(args) > 0:\r\n            if is_string(args[0]):\r\n                self.load(args[0])\r\n                return\r\n        if \"shp\" in kwargs.keys():\r\n            if hasattr(kwargs[\"shp\"], \"read\"):\r\n                self.shp = kwargs[\"shp\"]\r\n                # Copy if required\r\n                try:\r\n                    self.shp.seek(0)\r\n                except (NameError, io.UnsupportedOperation):\r\n                    self.shp = io.BytesIO(self.shp.read())\r\n            if \"shx\" in kwargs.keys():\r\n                if hasattr(kwargs[\"shx\"], \"read\"):\r\n                    self.shx = kwargs[\"shx\"]\r\n                    # Copy if required\r\n                    try:\r\n                        self.shx.seek(0)\r\n                    except (NameError, io.UnsupportedOperation):\r\n                        self.shx = io.BytesIO(self.shx.read())\r\n        if \"dbf\" in kwargs.keys():\r\n            if hasattr(kwargs[\"dbf\"], \"read\"):\r\n                self.dbf = kwargs[\"dbf\"]\r\n                # Copy if required\r\n                try:\r\n                    self.dbf.seek(0)\r\n                except (NameError, io.UnsupportedOperation):\r\n                    self.dbf = io.BytesIO(self.dbf.read())\r\n        if self.shp or self.dbf:        \r\n            self.load()\r\n        else:\r\n            raise ShapefileException(\"Shapefile Reader requires a shapefile or file-like object.\")\r\n\r\n    def __str__(self):\r\n        \"\"\"\r\n        Use some general info on the shapefile as __str__\r\n        \"\"\"\r\n        info = ['shapefile Reader']\r\n        if self.shp:\r\n            info.append(\"    {} shapes (type '{}')\".format(\r\n                len(self), SHAPETYPE_LOOKUP[self.shapeType]))\r\n        if self.dbf:\r\n            info.append('    {} records ({} fields)'.format(\r\n                len(self), len(self.fields)))\r\n        return '\\n'.join(info)\r\n\r\n    def __enter__(self):\r\n        \"\"\"\r\n        Enter phase of context manager.\r\n        \"\"\"\r\n        return self\r\n\r\n    def __exit__(self, exc_type, exc_val, exc_tb):\r\n        \"\"\"\r\n        Exit phase of context manager, close opened files.\r\n        \"\"\"\r\n        self.close()\r\n\r\n    def __len__(self):\r\n        \"\"\"Returns the number of shapes/records in the shapefile.\"\"\"\r\n        return self.numRecords\r\n\r\n    def __iter__(self):\r\n        \"\"\"Iterates through the shapes/records in the shapefile.\"\"\"\r\n        for shaperec in self.iterShapeRecords():\r\n            yield shaperec\r\n\r\n    @property\r\n    def __geo_interface__(self):\r\n        fieldnames = [f[0] for f in self.fields]\r\n        features = []\r\n        for feat in self.iterShapeRecords():\r\n            fdict = {'type': 'Feature',\r\n                     'properties': dict(zip(fieldnames,feat.record)),\r\n                     'geometry': feat.shape.__geo_interface__}\r\n            features.append(fdict)\r\n        return {'type': 'FeatureCollection',\r\n                'bbox': self.bbox,\r\n                'features': features}\r\n\r\n    @property\r\n    def shapeTypeName(self):\r\n        return SHAPETYPE_LOOKUP[self.shapeType]\r\n\r\n    def load(self, shapefile=None):\r\n        \"\"\"Opens a shapefile from a filename or file-like\r\n        object. Normally this method would be called by the\r\n        constructor with the file name as an argument.\"\"\"\r\n        if shapefile:\r\n            (shapeName, ext) = os.path.splitext(shapefile)\r\n            self.shapeName = shapeName\r\n            self.load_shp(shapeName)\r\n            self.load_shx(shapeName)\r\n            self.load_dbf(shapeName)\r\n            if not (self.shp or self.dbf):\r\n                raise ShapefileException(\"Unable to open %s.dbf or %s.shp.\" % (shapeName, shapeName))\r\n        if self.shp:\r\n            self.__shpHeader()\r\n        if self.dbf:\r\n            self.__dbfHeader()\r\n\r\n    def load_shp(self, shapefile_name):\r\n        \"\"\"\r\n        Attempts to load file with .shp extension as both lower and upper case\r\n        \"\"\"\r\n        shp_ext = 'shp'\r\n        try:\r\n            self.shp = open(\"%s.%s\" % (shapefile_name, shp_ext), \"rb\")\r\n        except IOError:\r\n            try:\r\n                self.shp = open(\"%s.%s\" % (shapefile_name, shp_ext.upper()), \"rb\")\r\n            except IOError:\r\n                pass\r\n\r\n    def load_shx(self, shapefile_name):\r\n        \"\"\"\r\n        Attempts to load file with .shx extension as both lower and upper case\r\n        \"\"\"\r\n        shx_ext = 'shx'\r\n        try:\r\n            self.shx = open(\"%s.%s\" % (shapefile_name, shx_ext), \"rb\")\r\n        except IOError:\r\n            try:\r\n                self.shx = open(\"%s.%s\" % (shapefile_name, shx_ext.upper()), \"rb\")\r\n            except IOError:\r\n                pass\r\n\r\n    def load_dbf(self, shapefile_name):\r\n        \"\"\"\r\n        Attempts to load file with .dbf extension as both lower and upper case\r\n        \"\"\"\r\n        dbf_ext = 'dbf'\r\n        try:\r\n            self.dbf = open(\"%s.%s\" % (shapefile_name, dbf_ext), \"rb\")\r\n        except IOError:\r\n            try:\r\n                self.dbf = open(\"%s.%s\" % (shapefile_name, dbf_ext.upper()), \"rb\")\r\n            except IOError:\r\n                pass\r\n\r\n    def __del__(self):\r\n        self.close()\r\n\r\n    def close(self):\r\n        for attribute in (self.shp, self.shx, self.dbf):\r\n            if hasattr(attribute, 'close'):\r\n                try:\r\n                    attribute.close()\r\n                except IOError:\r\n                    pass\r\n\r\n    def __getFileObj(self, f):\r\n        \"\"\"Checks to see if the requested shapefile file object is\r\n        available. If not a ShapefileException is raised.\"\"\"\r\n        if not f:\r\n            raise ShapefileException(\"Shapefile Reader requires a shapefile or file-like object.\")\r\n        if self.shp and self.shpLength is None:\r\n            self.load()\r\n        if self.dbf and len(self.fields) == 0:\r\n            self.load()\r\n        return f\r\n\r\n    def __restrictIndex(self, i):\r\n        \"\"\"Provides list-like handling of a record index with a clearer\r\n        error message if the index is out of bounds.\"\"\"\r\n        if self.numRecords:\r\n            rmax = self.numRecords - 1\r\n            if abs(i) > rmax:\r\n                raise IndexError(\"Shape or Record index out of range.\")\r\n            if i < 0: i = range(self.numRecords)[i]\r\n        return i\r\n\r\n    def __shpHeader(self):\r\n        \"\"\"Reads the header information from a .shp or .shx file.\"\"\"\r\n        if not self.shp:\r\n            raise ShapefileException(\"Shapefile Reader requires a shapefile or file-like object. (no shp file found\")\r\n        shp = self.shp\r\n        # File length (16-bit word * 2 = bytes)\r\n        shp.seek(24)\r\n        self.shpLength = unpack(\">i\", shp.read(4))[0] * 2\r\n        # Shape type\r\n        shp.seek(32)\r\n        self.shapeType= unpack(\"<i\", shp.read(4))[0]\r\n        # The shapefile's bounding box (lower left, upper right)\r\n        self.bbox = _Array('d', unpack(\"<4d\", shp.read(32)))\r\n        # Elevation\r\n        self.zbox = _Array('d', unpack(\"<2d\", shp.read(16)))\r\n        # Measure\r\n        self.mbox = []\r\n        for m in _Array('d', unpack(\"<2d\", shp.read(16))):\r\n            # Measure values less than -10e38 are nodata values according to the spec\r\n            if m > NODATA:\r\n                self.mbox.append(m)\r\n            else:\r\n                self.mbox.append(None)\r\n\r\n    def __shape(self):\r\n        \"\"\"Returns the header info and geometry for a single shape.\"\"\"\r\n        f = self.__getFileObj(self.shp)\r\n        record = Shape()\r\n        nParts = nPoints = zmin = zmax = mmin = mmax = None\r\n        (recNum, recLength) = unpack(\">2i\", f.read(8))\r\n        # Determine the start of the next record\r\n        next = f.tell() + (2 * recLength)\r\n        shapeType = unpack(\"<i\", f.read(4))[0]\r\n        record.shapeType = shapeType\r\n        # For Null shapes create an empty points list for consistency\r\n        if shapeType == 0:\r\n            record.points = []\r\n        # All shape types capable of having a bounding box\r\n        elif shapeType in (3,5,8,13,15,18,23,25,28,31):\r\n            record.bbox = _Array('d', unpack(\"<4d\", f.read(32)))\r\n        # Shape types with parts\r\n        if shapeType in (3,5,13,15,23,25,31):\r\n            nParts = unpack(\"<i\", f.read(4))[0]\r\n        # Shape types with points\r\n        if shapeType in (3,5,8,13,15,18,23,25,28,31):\r\n            nPoints = unpack(\"<i\", f.read(4))[0]\r\n        # Read parts\r\n        if nParts:\r\n            record.parts = _Array('i', unpack(\"<%si\" % nParts, f.read(nParts * 4)))\r\n        # Read part types for Multipatch - 31\r\n        if shapeType == 31:\r\n            record.partTypes = _Array('i', unpack(\"<%si\" % nParts, f.read(nParts * 4)))\r\n        # Read points - produces a list of [x,y] values\r\n        if nPoints:\r\n            flat = unpack(\"<%sd\" % (2 * nPoints), f.read(16*nPoints))\r\n            record.points = list(izip(*(iter(flat),) * 2))\r\n        # Read z extremes and values\r\n        if shapeType in (13,15,18,31):\r\n            (zmin, zmax) = unpack(\"<2d\", f.read(16))\r\n            record.z = _Array('d', unpack(\"<%sd\" % nPoints, f.read(nPoints * 8)))\r\n        # Read m extremes and values\r\n        if shapeType in (13,15,18,23,25,28,31):\r\n            if next - f.tell() >= 16:\r\n                (mmin, mmax) = unpack(\"<2d\", f.read(16))\r\n            # Measure values less than -10e38 are nodata values according to the spec\r\n            if next - f.tell() >= nPoints * 8:\r\n                record.m = []\r\n                for m in _Array('d', unpack(\"<%sd\" % nPoints, f.read(nPoints * 8))):\r\n                    if m > NODATA:\r\n                        record.m.append(m)\r\n                    else:\r\n                        record.m.append(None)\r\n            else:\r\n                record.m = [None for _ in range(nPoints)]\r\n        # Read a single point\r\n        if shapeType in (1,11,21):\r\n            record.points = [_Array('d', unpack(\"<2d\", f.read(16)))]\r\n        # Read a single Z value\r\n        if shapeType == 11:\r\n            record.z = list(unpack(\"<d\", f.read(8)))\r\n        # Read a single M value\r\n        if shapeType in (21,11):\r\n            if next - f.tell() >= 8:\r\n                (m,) = unpack(\"<d\", f.read(8))\r\n            else:\r\n                m = NODATA\r\n            # Measure values less than -10e38 are nodata values according to the spec\r\n            if m > NODATA:\r\n                record.m = [m]\r\n            else:\r\n                record.m = [None]\r\n        # Seek to the end of this record as defined by the record header because\r\n        # the shapefile spec doesn't require the actual content to meet the header\r\n        # definition.  Probably allowed for lazy feature deletion. \r\n        f.seek(next)\r\n        return record\r\n\r\n    def __shapeIndex(self, i=None):\r\n        \"\"\"Returns the offset in a .shp file for a shape based on information\r\n        in the .shx index file.\"\"\"\r\n        shx = self.shx\r\n        if not shx:\r\n            return None\r\n        if not self._offsets:\r\n            # File length (16-bit word * 2 = bytes) - header length\r\n            shx.seek(24)\r\n            shxRecordLength = (unpack(\">i\", shx.read(4))[0] * 2) - 100\r\n            numRecords = shxRecordLength // 8\r\n            # Jump to the first record.\r\n            shx.seek(100)\r\n            shxRecords = _Array('i')\r\n            # Each offset consists of two nrs, only the first one matters\r\n            shxRecords.fromfile(shx, 2 * numRecords)\r\n            if sys.byteorder != 'big':\r\n                 shxRecords.byteswap()\r\n            self._offsets = [2 * el for el in shxRecords[::2]]\r\n        if not i == None:\r\n            return self._offsets[i]\r\n\r\n    def shape(self, i=0):\r\n        \"\"\"Returns a shape object for a shape in the the geometry\r\n        record file.\"\"\"\r\n        shp = self.__getFileObj(self.shp)\r\n        i = self.__restrictIndex(i)\r\n        offset = self.__shapeIndex(i)\r\n        if not offset:\r\n            # Shx index not available so iterate the full list.\r\n            for j,k in enumerate(self.iterShapes()):\r\n                if j == i:\r\n                    return k\r\n        shp.seek(offset)\r\n        return self.__shape()\r\n\r\n    def shapes(self):\r\n        \"\"\"Returns all shapes in a shapefile.\"\"\"\r\n        shp = self.__getFileObj(self.shp)\r\n        # Found shapefiles which report incorrect\r\n        # shp file length in the header. Can't trust\r\n        # that so we seek to the end of the file\r\n        # and figure it out.\r\n        shp.seek(0,2)\r\n        self.shpLength = shp.tell()\r\n        shp.seek(100)\r\n        shapes = Shapes()\r\n        while shp.tell() < self.shpLength:\r\n            shapes.append(self.__shape())\r\n        return shapes\r\n\r\n    def iterShapes(self):\r\n        \"\"\"Serves up shapes in a shapefile as an iterator. Useful\r\n        for handling large shapefiles.\"\"\"\r\n        shp = self.__getFileObj(self.shp)\r\n        shp.seek(0,2)\r\n        self.shpLength = shp.tell()\r\n        shp.seek(100)\r\n        while shp.tell() < self.shpLength:\r\n            yield self.__shape()    \r\n\r\n    def __dbfHeader(self):\r\n        \"\"\"Reads a dbf header. Xbase-related code borrows heavily from ActiveState Python Cookbook Recipe 362715 by Raymond Hettinger\"\"\"\r\n        if not self.dbf:\r\n            raise ShapefileException(\"Shapefile Reader requires a shapefile or file-like object. (no dbf file found)\")\r\n        dbf = self.dbf\r\n        # read relevant header parts\r\n        self.numRecords, self.__dbfHdrLength, self.__recordLength = \\\r\n                unpack(\"<xxxxLHH20x\", dbf.read(32))\r\n        # read fields\r\n        numFields = (self.__dbfHdrLength - 33) // 32\r\n        for field in range(numFields):\r\n            fieldDesc = list(unpack(\"<11sc4xBB14x\", dbf.read(32)))\r\n            name = 0\r\n            idx = 0\r\n            if b\"\\x00\" in fieldDesc[name]:\r\n                idx = fieldDesc[name].index(b\"\\x00\")\r\n            else:\r\n                idx = len(fieldDesc[name]) - 1\r\n            fieldDesc[name] = fieldDesc[name][:idx]\r\n            fieldDesc[name] = u(fieldDesc[name], self.encoding, self.encodingErrors)\r\n            fieldDesc[name] = fieldDesc[name].lstrip()\r\n            fieldDesc[1] = u(fieldDesc[1], 'ascii')\r\n            self.fields.append(fieldDesc)\r\n        terminator = dbf.read(1)\r\n        if terminator != b\"\\r\":\r\n            raise ShapefileException(\"Shapefile dbf header lacks expected terminator. (likely corrupt?)\")\r\n        self.fields.insert(0, ('DeletionFlag', 'C', 1, 0))\r\n        fmt,fmtSize = self.__recordFmt()\r\n        self.__recStruct = Struct(fmt)\r\n\r\n        # Store the field positions\r\n        self.__fieldposition_lookup = dict((f[0], i) for i, f in enumerate(self.fields[1:]))\r\n\r\n    def __recordFmt(self):\r\n        \"\"\"Calculates the format and size of a .dbf record.\"\"\"\r\n        if self.numRecords is None:\r\n            self.__dbfHeader()\r\n        fmt = ''.join(['%ds' % fieldinfo[2] for fieldinfo in self.fields])\r\n        fmtSize = calcsize(fmt)\r\n        # total size of fields should add up to recordlength from the header\r\n        while fmtSize < self.__recordLength:\r\n            # if not, pad byte until reaches recordlength\r\n            fmt += \"x\" \r\n            fmtSize += 1\r\n        return (fmt, fmtSize)\r\n\r\n    def __record(self, oid=None):\r\n        \"\"\"Reads and returns a dbf record row as a list of values.\"\"\"\r\n        f = self.__getFileObj(self.dbf)\r\n        recordContents = self.__recStruct.unpack(f.read(self.__recStruct.size))\r\n        if recordContents[0] != b' ':\r\n            # deleted record\r\n            return None\r\n        record = []\r\n        for (name, typ, size, deci), value in zip(self.fields, recordContents):\r\n            if name == 'DeletionFlag':\r\n                continue\r\n            elif typ in (\"N\",\"F\"):\r\n                # numeric or float: number stored as a string, right justified, and padded with blanks to the width of the field. \r\n                value = value.split(b'\\0')[0]\r\n                value = value.replace(b'*', b'')  # QGIS NULL is all '*' chars\r\n                if value == b'':\r\n                    value = None\r\n                elif deci:\r\n                    try:\r\n                        value = float(value)\r\n                    except ValueError:\r\n                        #not parseable as float, set to None\r\n                        value = None\r\n                else:\r\n                    # force to int\r\n                    try:\r\n                        # first try to force directly to int.\r\n                        # forcing a large int to float and back to int\r\n                        # will lose information and result in wrong nr.\r\n                        value = int(value) \r\n                    except ValueError:\r\n                        # forcing directly to int failed, so was probably a float.\r\n                        try:\r\n                            value = int(float(value))\r\n                        except ValueError:\r\n                            #not parseable as int, set to None\r\n                            value = None\r\n            elif typ == 'D':\r\n                # date: 8 bytes - date stored as a string in the format YYYYMMDD.\r\n                if value.count(b'0') == len(value):  # QGIS NULL is all '0' chars\r\n                    value = None\r\n                else:\r\n                    try:\r\n                        y, m, d = int(value[:4]), int(value[4:6]), int(value[6:8])\r\n                        value = date(y, m, d)\r\n                    except:\r\n                        value = value.strip()\r\n            elif typ == 'L':\r\n                # logical: 1 byte - initialized to 0x20 (space) otherwise T or F.\r\n                if value == b\" \":\r\n                    value = None # space means missing or not yet set\r\n                else:\r\n                    if value in b'YyTt1':\r\n                        value = True\r\n                    elif value in b'NnFf0':\r\n                        value = False\r\n                    else:\r\n                        value = None # unknown value is set to missing\r\n            else:\r\n                # anything else is forced to string/unicode\r\n                value = u(value, self.encoding, self.encodingErrors)\r\n                value = value.strip()\r\n            record.append(value)\r\n\r\n        return _Record(self.__fieldposition_lookup, record, oid)\r\n\r\n    def record(self, i=0):\r\n        \"\"\"Returns a specific dbf record based on the supplied index.\"\"\"\r\n        f = self.__getFileObj(self.dbf)\r\n        if self.numRecords is None:\r\n            self.__dbfHeader()\r\n        i = self.__restrictIndex(i)\r\n        recSize = self.__recStruct.size\r\n        f.seek(0)\r\n        f.seek(self.__dbfHdrLength + (i * recSize))\r\n        return self.__record(oid=i)\r\n\r\n    def records(self):\r\n        \"\"\"Returns all records in a dbf file.\"\"\"\r\n        if self.numRecords is None:\r\n            self.__dbfHeader()\r\n        records = []\r\n        f = self.__getFileObj(self.dbf)\r\n        f.seek(self.__dbfHdrLength)\r\n        for i in range(self.numRecords):\r\n            r = self.__record(oid=i)\r\n            if r:\r\n                records.append(r)\r\n        return records\r\n\r\n    def iterRecords(self):\r\n        \"\"\"Serves up records in a dbf file as an iterator.\r\n        Useful for large shapefiles or dbf files.\"\"\"\r\n        if self.numRecords is None:\r\n            self.__dbfHeader()\r\n        f = self.__getFileObj(self.dbf)\r\n        f.seek(self.__dbfHdrLength)\r\n        for i in xrange(self.numRecords):\r\n            r = self.__record()\r\n            if r:\r\n                yield r\r\n\r\n    def shapeRecord(self, i=0):\r\n        \"\"\"Returns a combination geometry and attribute record for the\r\n        supplied record index.\"\"\"\r\n        i = self.__restrictIndex(i)\r\n        return ShapeRecord(shape=self.shape(i), record=self.record(i))\r\n\r\n    def shapeRecords(self):\r\n        \"\"\"Returns a list of combination geometry/attribute records for\r\n        all records in a shapefile.\"\"\"\r\n        return ShapeRecords([ShapeRecord(shape=rec[0], record=rec[1]) \\\r\n                                for rec in zip(self.shapes(), self.records())])\r\n\r\n    def iterShapeRecords(self):\r\n        \"\"\"Returns a generator of combination geometry/attribute records for\r\n        all records in a shapefile.\"\"\"\r\n        for shape, record in izip(self.iterShapes(), self.iterRecords()):\r\n            yield ShapeRecord(shape=shape, record=record)\r\n\r\n\r\nclass Writer(object):\r\n    \"\"\"Provides write support for ESRI Shapefiles.\"\"\"\r\n    def __init__(self, target=None, shapeType=None, autoBalance=False, **kwargs):\r\n        self.target = target\r\n        self.autoBalance = autoBalance\r\n        self.fields = []\r\n        self.shapeType = shapeType\r\n        self.shp = self.shx = self.dbf = None\r\n        if target:\r\n            self.shp = self.__getFileObj(os.path.splitext(target)[0] + '.shp')\r\n            self.shx = self.__getFileObj(os.path.splitext(target)[0] + '.shx')\r\n            self.dbf = self.__getFileObj(os.path.splitext(target)[0] + '.dbf')\r\n        elif kwargs.get('shp') or kwargs.get('shx') or kwargs.get('dbf'):\r\n            shp,shx,dbf = kwargs.get('shp'), kwargs.get('shx'), kwargs.get('dbf')\r\n            if shp:\r\n                self.shp = self.__getFileObj(shp)\r\n            if shx:\r\n                self.shx = self.__getFileObj(shx)\r\n            if dbf:\r\n                self.dbf = self.__getFileObj(dbf)\r\n        else:\r\n            raise Exception('Either the target filepath, or any of shp, shx, or dbf must be set to create a shapefile.')\r\n        # Initiate with empty headers, to be finalized upon closing\r\n        if self.shp: self.shp.write(b'9'*100) \r\n        if self.shx: self.shx.write(b'9'*100) \r\n        # Geometry record offsets and lengths for writing shx file.\r\n        self.recNum = 0\r\n        self.shpNum = 0\r\n        self._bbox = None\r\n        self._zbox = None\r\n        self._mbox = None\r\n        # Use deletion flags in dbf? Default is false (0).\r\n        self.deletionFlag = 0\r\n        # Encoding\r\n        self.encoding = kwargs.pop('encoding', 'utf-8')\r\n        self.encodingErrors = kwargs.pop('encodingErrors', 'strict')\r\n\r\n    def __len__(self):\r\n        \"\"\"Returns the current number of features written to the shapefile. \r\n        If shapes and records are unbalanced, the length is considered the highest\r\n        of the two.\"\"\"\r\n        return max(self.recNum, self.shpNum) \r\n\r\n    def __enter__(self):\r\n        \"\"\"\r\n        Enter phase of context manager.\r\n        \"\"\"\r\n        return self\r\n\r\n    def __exit__(self, exc_type, exc_val, exc_tb):\r\n        \"\"\"\r\n        Exit phase of context manager, finish writing and close the files.\r\n        \"\"\"\r\n        self.close()\r\n\r\n    def __del__(self):\r\n        self.close()\r\n\r\n    def close(self):\r\n        \"\"\"\r\n        Write final shp, shx, and dbf headers, close opened files.\r\n        \"\"\"\r\n        # Check if any of the files have already been closed\r\n        shp_open = self.shp and not (hasattr(self.shp, 'closed') and self.shp.closed)\r\n        shx_open = self.shx and not (hasattr(self.shx, 'closed') and self.shx.closed)\r\n        dbf_open = self.dbf and not (hasattr(self.dbf, 'closed') and self.dbf.closed)\r\n            \r\n        # Balance if already not balanced\r\n        if self.shp and shp_open and self.dbf and dbf_open:\r\n            if self.autoBalance:\r\n                self.balance()\r\n            if self.recNum != self.shpNum:\r\n                raise ShapefileException(\"When saving both the dbf and shp file, \"\r\n                                         \"the number of records (%s) must correspond \"\r\n                                         \"with the number of shapes (%s)\" % (self.recNum, self.shpNum))\r\n        # Fill in the blank headers\r\n        if self.shp and shp_open:\r\n            self.__shapefileHeader(self.shp, headerType='shp')\r\n        if self.shx and shx_open:\r\n            self.__shapefileHeader(self.shx, headerType='shx')\r\n\r\n        # Update the dbf header with final length etc\r\n        if self.dbf and dbf_open:\r\n            self.__dbfHeader()\r\n\r\n        # Close files, if target is a filepath\r\n        if self.target:\r\n            for attribute in (self.shp, self.shx, self.dbf):\r\n                if hasattr(attribute, 'close'):\r\n                    try:\r\n                        attribute.close()\r\n                    except IOError:\r\n                        pass\r\n\r\n    def __getFileObj(self, f):\r\n        \"\"\"Safety handler to verify file-like objects\"\"\"\r\n        if not f:\r\n            raise ShapefileException(\"No file-like object available.\")\r\n        elif hasattr(f, \"write\"):\r\n            return f\r\n        else:\r\n            pth = os.path.split(f)[0]\r\n            if pth and not os.path.exists(pth):\r\n                os.makedirs(pth)\r\n            return open(f, \"wb+\")\r\n\r\n    def __shpFileLength(self):\r\n        \"\"\"Calculates the file length of the shp file.\"\"\"\r\n        # Remember starting position\r\n        start = self.shp.tell()\r\n        # Calculate size of all shapes\r\n        self.shp.seek(0,2)\r\n        size = self.shp.tell()\r\n        # Calculate size as 16-bit words\r\n        size //= 2\r\n        # Return to start\r\n        self.shp.seek(start)\r\n        return size\r\n\r\n    def __bbox(self, s):\r\n        x = []\r\n        y = []\r\n        if len(s.points) > 0:\r\n            px, py = list(zip(*s.points))[:2]\r\n            x.extend(px)\r\n            y.extend(py)\r\n        else:\r\n            # this should not happen.\r\n            # any shape that is not null should have at least one point, and only those should be sent here. \r\n            # could also mean that earlier code failed to add points to a non-null shape. \r\n            raise Exception(\"Cannot create bbox. Expected a valid shape with at least one point. Got a shape of type '%s' and 0 points.\" % s.shapeType)\r\n        bbox = [min(x), min(y), max(x), max(y)]\r\n        # update global\r\n        if self._bbox:\r\n            # compare with existing\r\n            self._bbox = [min(bbox[0],self._bbox[0]), min(bbox[1],self._bbox[1]), max(bbox[2],self._bbox[2]), max(bbox[3],self._bbox[3])]\r\n        else:\r\n            # first time bbox is being set\r\n            self._bbox = bbox\r\n        return bbox\r\n\r\n    def __zbox(self, s):\r\n        z = []\r\n        for p in s.points:\r\n            try:\r\n                z.append(p[2])\r\n            except IndexError:\r\n                # point did not have z value\r\n                # setting it to 0 is probably ok, since it means all are on the same elavation\r\n                z.append(0)\r\n        zbox = [min(z), max(z)]\r\n        # update global\r\n        if self._zbox:\r\n            # compare with existing\r\n            self._zbox = [min(zbox[0],self._zbox[0]), max(zbox[1],self._zbox[1])]\r\n        else:\r\n            # first time zbox is being set\r\n            self._zbox = zbox\r\n        return zbox\r\n\r\n    def __mbox(self, s):\r\n        mpos = 3 if s.shapeType in (11,13,15,18,31) else 2\r\n        m = []\r\n        for p in s.points:\r\n            try:\r\n                if p[mpos] is not None:\r\n                    # mbox should only be calculated on valid m values\r\n                    m.append(p[mpos])\r\n            except IndexError:\r\n                # point did not have m value so is missing\r\n                # mbox should only be calculated on valid m values\r\n                pass\r\n        if not m:\r\n            # only if none of the shapes had m values, should mbox be set to missing m values\r\n            m.append(NODATA)\r\n        mbox = [min(m), max(m)]\r\n        # update global\r\n        if self._mbox:\r\n            # compare with existing\r\n            self._mbox = [min(mbox[0],self._mbox[0]), max(mbox[1],self._mbox[1])]\r\n        else:\r\n            # first time mbox is being set\r\n            self._mbox = mbox\r\n        return mbox\r\n\r\n    @property\r\n    def shapeTypeName(self):\r\n        return SHAPETYPE_LOOKUP[self.shapeType]\r\n\r\n    def bbox(self):\r\n        \"\"\"Returns the current bounding box for the shapefile which is\r\n        the lower-left and upper-right corners. It does not contain the\r\n        elevation or measure extremes.\"\"\"\r\n        return self._bbox\r\n\r\n    def zbox(self):\r\n        \"\"\"Returns the current z extremes for the shapefile.\"\"\"\r\n        return self._zbox\r\n\r\n    def mbox(self):\r\n        \"\"\"Returns the current m extremes for the shapefile.\"\"\"\r\n        return self._mbox\r\n\r\n    def __shapefileHeader(self, fileObj, headerType='shp'):\r\n        \"\"\"Writes the specified header type to the specified file-like object.\r\n        Several of the shapefile formats are so similar that a single generic\r\n        method to read or write them is warranted.\"\"\"\r\n        f = self.__getFileObj(fileObj)\r\n        f.seek(0)\r\n        # File code, Unused bytes\r\n        f.write(pack(\">6i\", 9994,0,0,0,0,0))\r\n        # File length (Bytes / 2 = 16-bit words)\r\n        if headerType == 'shp':\r\n            f.write(pack(\">i\", self.__shpFileLength()))\r\n        elif headerType == 'shx':\r\n            f.write(pack('>i', ((100 + (self.shpNum * 8)) // 2)))\r\n        # Version, Shape type\r\n        if self.shapeType is None:\r\n            self.shapeType = NULL\r\n        f.write(pack(\"<2i\", 1000, self.shapeType))\r\n        # The shapefile's bounding box (lower left, upper right)\r\n        if self.shapeType != 0:\r\n            try:\r\n                bbox = self.bbox()\r\n                if bbox is None:\r\n                    # The bbox is initialized with None, so this would mean the shapefile contains no valid geometries.\r\n                    # In such cases of empty shapefiles, ESRI spec says the bbox values are 'unspecified'.\r\n                    # Not sure what that means, so for now just setting to 0s, which is the same behavior as in previous versions.\r\n                    # This would also make sense since the Z and M bounds are similarly set to 0 for non-Z/M type shapefiles.\r\n                    bbox = [0,0,0,0] \r\n                f.write(pack(\"<4d\", *bbox))\r\n            except error:\r\n                raise ShapefileException(\"Failed to write shapefile bounding box. Floats required.\")\r\n        else:\r\n            f.write(pack(\"<4d\", 0,0,0,0))\r\n        # Elevation\r\n        if self.shapeType in (11,13,15,18):\r\n            # Z values are present in Z type\r\n            zbox = self.zbox()\r\n        else:\r\n            # As per the ESRI shapefile spec, the zbox for non-Z type shapefiles are set to 0s\r\n            zbox = [0,0]\r\n        # Measure\r\n        if self.shapeType in (11,13,15,18,21,23,25,28,31):\r\n            # M values are present in M or Z type\r\n            mbox = self.mbox()\r\n        else:\r\n            # As per the ESRI shapefile spec, the mbox for non-M type shapefiles are set to 0s\r\n            mbox = [0,0]\r\n        # Try writing\r\n        try:\r\n            f.write(pack(\"<4d\", zbox[0], zbox[1], mbox[0], mbox[1]))\r\n        except error:\r\n            raise ShapefileException(\"Failed to write shapefile elevation and measure values. Floats required.\")\r\n\r\n    def __dbfHeader(self):\r\n        \"\"\"Writes the dbf header and field descriptors.\"\"\"\r\n        f = self.__getFileObj(self.dbf)\r\n        f.seek(0)\r\n        version = 3\r\n        year, month, day = time.localtime()[:3]\r\n        year -= 1900\r\n        # Remove deletion flag placeholder from fields\r\n        for field in self.fields:\r\n            if field[0].startswith(\"Deletion\"):\r\n                self.fields.remove(field)\r\n        numRecs = self.recNum\r\n        numFields = len(self.fields)\r\n        headerLength = numFields * 32 + 33\r\n        if headerLength >= 65535:\r\n            raise ShapefileException(\r\n                    \"Shapefile dbf header length exceeds maximum length.\")\r\n        recordLength = sum([int(field[2]) for field in self.fields]) + 1\r\n        header = pack('<BBBBLHH20x', version, year, month, day, numRecs,\r\n                headerLength, recordLength)\r\n        f.write(header)\r\n        # Field descriptors\r\n        for field in self.fields:\r\n            name, fieldType, size, decimal = field\r\n            name = b(name, self.encoding, self.encodingErrors)\r\n            name = name.replace(b' ', b'_')\r\n            name = name.ljust(11).replace(b' ', b'\\x00')\r\n            fieldType = b(fieldType, 'ascii')\r\n            size = int(size)\r\n            fld = pack('<11sc4xBB14x', name, fieldType, size, decimal)\r\n            f.write(fld)\r\n        # Terminator\r\n        f.write(b'\\r')\r\n\r\n    def shape(self, s):\r\n        # Balance if already not balanced\r\n        if self.autoBalance and self.recNum < self.shpNum:\r\n            self.balance()\r\n        # Check is shape or import from geojson\r\n        if not isinstance(s, Shape):\r\n            if hasattr(s, \"__geo_interface__\"):\r\n                s = s.__geo_interface__\r\n            if isinstance(s, dict):\r\n                s = Shape._from_geojson(s)\r\n            else:\r\n                raise Exception(\"Can only write Shape objects, GeoJSON dictionaries, \"\r\n                                \"or objects with the __geo_interface__, \"\r\n                                \"not: %r\" % s)\r\n        # Write to file\r\n        offset,length = self.__shpRecord(s)\r\n        self.__shxRecord(offset, length)\r\n\r\n    def __shpRecord(self, s):\r\n        f = self.__getFileObj(self.shp)\r\n        offset = f.tell()\r\n        # Record number, Content length place holder\r\n        self.shpNum += 1\r\n        f.write(pack(\">2i\", self.shpNum, 0))\r\n        start = f.tell()\r\n        # Shape Type\r\n        if self.shapeType is None and s.shapeType != NULL:\r\n            self.shapeType = s.shapeType\r\n        if s.shapeType != NULL and s.shapeType != self.shapeType:\r\n            raise Exception(\"The shape's type (%s) must match the type of the shapefile (%s).\" % (s.shapeType, self.shapeType))\r\n        f.write(pack(\"<i\", s.shapeType))\r\n\r\n        # For point just update bbox of the whole shapefile\r\n        if s.shapeType in (1,11,21):\r\n            self.__bbox(s)\r\n        # All shape types capable of having a bounding box\r\n        if s.shapeType in (3,5,8,13,15,18,23,25,28,31):\r\n            try:\r\n                f.write(pack(\"<4d\", *self.__bbox(s)))\r\n            except error:\r\n                raise ShapefileException(\"Failed to write bounding box for record %s. Expected floats.\" % self.shpNum)\r\n        # Shape types with parts\r\n        if s.shapeType in (3,5,13,15,23,25,31):\r\n            # Number of parts\r\n            f.write(pack(\"<i\", len(s.parts)))\r\n        # Shape types with multiple points per record\r\n        if s.shapeType in (3,5,8,13,15,18,23,25,28,31):\r\n            # Number of points\r\n            f.write(pack(\"<i\", len(s.points)))\r\n        # Write part indexes\r\n        if s.shapeType in (3,5,13,15,23,25,31):\r\n            for p in s.parts:\r\n                f.write(pack(\"<i\", p))\r\n        # Part types for Multipatch (31)\r\n        if s.shapeType == 31:\r\n            for pt in s.partTypes:\r\n                f.write(pack(\"<i\", pt))\r\n        # Write points for multiple-point records\r\n        if s.shapeType in (3,5,8,13,15,18,23,25,28,31):\r\n            try:\r\n                [f.write(pack(\"<2d\", *p[:2])) for p in s.points]\r\n            except error:\r\n                raise ShapefileException(\"Failed to write points for record %s. Expected floats.\" % self.shpNum)\r\n        # Write z extremes and values\r\n        # Note: missing z values are autoset to 0, but not sure if this is ideal.\r\n        if s.shapeType in (13,15,18,31):\r\n            try:\r\n                f.write(pack(\"<2d\", *self.__zbox(s)))\r\n            except error:\r\n                raise ShapefileException(\"Failed to write elevation extremes for record %s. Expected floats.\" % self.shpNum)\r\n            try:\r\n                if hasattr(s,\"z\"):\r\n                    # if z values are stored in attribute\r\n                    f.write(pack(\"<%sd\" % len(s.z), *s.z))\r\n                else:\r\n                    # if z values are stored as 3rd dimension\r\n                    [f.write(pack(\"<d\", p[2] if len(p) > 2 else 0)) for p in s.points]  \r\n            except error:\r\n                raise ShapefileException(\"Failed to write elevation values for record %s. Expected floats.\" % self.shpNum)\r\n        # Write m extremes and values\r\n        # When reading a file, pyshp converts NODATA m values to None, so here we make sure to convert them back to NODATA\r\n        # Note: missing m values are autoset to NODATA.\r\n        if s.shapeType in (13,15,18,23,25,28,31):\r\n            try:\r\n                f.write(pack(\"<2d\", *self.__mbox(s)))\r\n            except error:\r\n                raise ShapefileException(\"Failed to write measure extremes for record %s. Expected floats\" % self.shpNum)\r\n            try:\r\n                if hasattr(s,\"m\"): \r\n                    # if m values are stored in attribute\r\n                    f.write(pack(\"<%sd\" % len(s.m), *[m if m is not None else NODATA for m in s.m]))\r\n                else:\r\n                    # if m values are stored as 3rd/4th dimension\r\n                    # 0-index position of m value is 3 if z type (x,y,z,m), or 2 if m type (x,y,m)\r\n                    mpos = 3 if s.shapeType in (13,15,18,31) else 2\r\n                    [f.write(pack(\"<d\", p[mpos] if len(p) > mpos and p[mpos] is not None else NODATA)) for p in s.points]\r\n            except error:\r\n                raise ShapefileException(\"Failed to write measure values for record %s. Expected floats\" % self.shpNum)\r\n        # Write a single point\r\n        if s.shapeType in (1,11,21):\r\n            try:\r\n                f.write(pack(\"<2d\", s.points[0][0], s.points[0][1]))\r\n            except error:\r\n                raise ShapefileException(\"Failed to write point for record %s. Expected floats.\" % self.shpNum)\r\n        # Write a single Z value\r\n        # Note: missing z values are autoset to 0, but not sure if this is ideal.\r\n        if s.shapeType == 11:\r\n            # update the global z box\r\n            self.__zbox(s)\r\n            # then write value\r\n            if hasattr(s, \"z\"):\r\n                # if z values are stored in attribute\r\n                try:\r\n                    if not s.z:\r\n                        s.z = (0,)\r\n                    f.write(pack(\"<d\", s.z[0]))\r\n                except error:\r\n                    raise ShapefileException(\"Failed to write elevation value for record %s. Expected floats.\" % self.shpNum)\r\n            else:\r\n                # if z values are stored as 3rd dimension\r\n                try:\r\n                    if len(s.points[0]) < 3:\r\n                        s.points[0].append(0)\r\n                    f.write(pack(\"<d\", s.points[0][2]))\r\n                except error:\r\n                    raise ShapefileException(\"Failed to write elevation value for record %s. Expected floats.\" % self.shpNum)\r\n        # Write a single M value\r\n        # Note: missing m values are autoset to NODATA.\r\n        if s.shapeType in (11,21):\r\n            # update the global m box\r\n            self.__mbox(s)\r\n            # then write value\r\n            if hasattr(s, \"m\"):\r\n                # if m values are stored in attribute\r\n                try:\r\n                    if not s.m or s.m[0] is None:\r\n                        s.m = (NODATA,) \r\n                    f.write(pack(\"<1d\", s.m[0]))\r\n                except error:\r\n                    raise ShapefileException(\"Failed to write measure value for record %s. Expected floats.\" % self.shpNum)\r\n            else:\r\n                # if m values are stored as 3rd/4th dimension\r\n                # 0-index position of m value is 3 if z type (x,y,z,m), or 2 if m type (x,y,m)\r\n                try:\r\n                    mpos = 3 if s.shapeType == 11 else 2\r\n                    if len(s.points[0]) < mpos+1:\r\n                        s.points[0].append(NODATA)\r\n                    elif s.points[0][mpos] is None:\r\n                        s.points[0][mpos] = NODATA\r\n                    f.write(pack(\"<1d\", s.points[0][mpos]))\r\n                except error:\r\n                    raise ShapefileException(\"Failed to write measure value for record %s. Expected floats.\" % self.shpNum)\r\n        # Finalize record length as 16-bit words\r\n        finish = f.tell()\r\n        length = (finish - start) // 2\r\n        # start - 4 bytes is the content length field\r\n        f.seek(start-4)\r\n        f.write(pack(\">i\", length))\r\n        f.seek(finish)\r\n        return offset,length\r\n\r\n    def __shxRecord(self, offset, length):\r\n         \"\"\"Writes the shx records.\"\"\"\r\n         f = self.__getFileObj(self.shx)\r\n         f.write(pack(\">i\", offset // 2))\r\n         f.write(pack(\">i\", length))\r\n\r\n    def record(self, *recordList, **recordDict):\r\n        \"\"\"Creates a dbf attribute record. You can submit either a sequence of\r\n        field values or keyword arguments of field names and values. Before\r\n        adding records you must add fields for the record values using the\r\n        fields() method. If the record values exceed the number of fields the\r\n        extra ones won't be added. In the case of using keyword arguments to specify\r\n        field/value pairs only fields matching the already registered fields\r\n        will be added.\"\"\"\r\n        # Balance if already not balanced\r\n        if self.autoBalance and self.recNum > self.shpNum:\r\n            self.balance()\r\n            \r\n        record = []\r\n        fieldCount = len(self.fields)\r\n        # Compensate for deletion flag\r\n        if self.fields[0][0].startswith(\"Deletion\"): fieldCount -= 1\r\n        if recordList:\r\n            record = [recordList[i] for i in range(fieldCount)]\r\n        elif recordDict:\r\n            for field in self.fields:\r\n                if field[0] in recordDict:\r\n                    val = recordDict[field[0]]\r\n                    if val is None:\r\n                        record.append(\"\")\r\n                    else:\r\n                        record.append(val)\r\n        else:\r\n            # Blank fields for empty record\r\n            record = [\"\" for i in range(fieldCount)]\r\n        self.__dbfRecord(record)\r\n\r\n    def __dbfRecord(self, record):\r\n        \"\"\"Writes the dbf records.\"\"\"\r\n        f = self.__getFileObj(self.dbf)\r\n        if self.recNum == 0:\r\n            # first records, so all fields should be set\r\n            # allowing us to write the dbf header\r\n            # cannot change the fields after this point\r\n            self.__dbfHeader()\r\n        # begin\r\n        self.recNum += 1\r\n        if not self.fields[0][0].startswith(\"Deletion\"):\r\n            f.write(b' ') # deletion flag\r\n        for (fieldName, fieldType, size, deci), value in zip(self.fields, record):\r\n            fieldType = fieldType.upper()\r\n            size = int(size)\r\n            if fieldType in (\"N\",\"F\"):\r\n                # numeric or float: number stored as a string, right justified, and padded with blanks to the width of the field.\r\n                if value in MISSING:\r\n                    value = b\"*\"*size # QGIS NULL\r\n                elif not deci:\r\n                    # force to int\r\n                    try:\r\n                        # first try to force directly to int.\r\n                        # forcing a large int to float and back to int\r\n                        # will lose information and result in wrong nr.\r\n                        value = int(value) \r\n                    except ValueError:\r\n                        # forcing directly to int failed, so was probably a float.\r\n                        value = int(float(value))\r\n                    value = format(value, \"d\")[:size].rjust(size) # caps the size if exceeds the field size\r\n                else:\r\n                    value = float(value)\r\n                    value = format(value, \".%sf\"%deci)[:size].rjust(size) # caps the size if exceeds the field size\r\n            elif fieldType == \"D\":\r\n                # date: 8 bytes - date stored as a string in the format YYYYMMDD.\r\n                if isinstance(value, date):\r\n                    value = '{:04d}{:02d}{:02d}'.format(value.year, value.month, value.day)\r\n                elif isinstance(value, list) and len(value) == 3:\r\n                    value = '{:04d}{:02d}{:02d}'.format(*value)\r\n                elif value in MISSING:\r\n                    value = b'0' * 8 # QGIS NULL for date type\r\n                elif is_string(value) and len(value) == 8:\r\n                    pass # value is already a date string\r\n                else:\r\n                    raise ShapefileException(\"Date values must be either a datetime.date object, a list, a YYYYMMDD string, or a missing value.\")\r\n            elif fieldType == 'L':\r\n                # logical: 1 byte - initialized to 0x20 (space) otherwise T or F.\r\n                if value in MISSING:\r\n                    value = b' ' # missing is set to space\r\n                elif value in [True,1]:\r\n                    value = b'T'\r\n                elif value in [False,0]:\r\n                    value = b'F'\r\n                else:\r\n                    value = b' ' # unknown is set to space\r\n            else:\r\n                # anything else is forced to string, truncated to the length of the field\r\n                value = b(value, self.encoding, self.encodingErrors)[:size].ljust(size)\r\n            if not isinstance(value, bytes):\r\n                # just in case some of the numeric format() and date strftime() results are still in unicode (Python 3 only)\r\n                value = b(value, 'ascii', self.encodingErrors) # should be default ascii encoding\r\n            if len(value) != size:\r\n                raise ShapefileException(\r\n                    \"Shapefile Writer unable to pack incorrect sized value\"\r\n                    \" (size %d) into field '%s' (size %d).\" % (len(value), fieldName, size))\r\n            f.write(value)\r\n\r\n    def balance(self):\r\n        \"\"\"Adds corresponding empty attributes or null geometry records depending\r\n        on which type of record was created to make sure all three files\r\n        are in synch.\"\"\"\r\n        while self.recNum > self.shpNum:\r\n            self.null()\r\n        while self.recNum < self.shpNum:\r\n            self.record()\r\n\r\n\r\n    def null(self):\r\n        \"\"\"Creates a null shape.\"\"\"\r\n        self.shape(Shape(NULL))\r\n\r\n\r\n    def point(self, x, y):\r\n        \"\"\"Creates a POINT shape.\"\"\"\r\n        shapeType = POINT\r\n        pointShape = Shape(shapeType)\r\n        pointShape.points.append([x, y])\r\n        self.shape(pointShape)\r\n\r\n    def pointm(self, x, y, m=None):\r\n        \"\"\"Creates a POINTM shape.\r\n        If the m (measure) value is not set, it defaults to NoData.\"\"\"\r\n        shapeType = POINTM\r\n        pointShape = Shape(shapeType)\r\n        pointShape.points.append([x, y, m])\r\n        self.shape(pointShape)\r\n\r\n    def pointz(self, x, y, z=0, m=None):\r\n        \"\"\"Creates a POINTZ shape.\r\n        If the z (elevation) value is not set, it defaults to 0.\r\n        If the m (measure) value is not set, it defaults to NoData.\"\"\"\r\n        shapeType = POINTZ\r\n        pointShape = Shape(shapeType)\r\n        pointShape.points.append([x, y, z, m])\r\n        self.shape(pointShape)\r\n        \r\n\r\n    def multipoint(self, points):\r\n        \"\"\"Creates a MULTIPOINT shape.\r\n        Points is a list of xy values.\"\"\"\r\n        shapeType = MULTIPOINT\r\n        points = [points] # nest the points inside a list to be compatible with the generic shapeparts method\r\n        self._shapeparts(parts=points, shapeType=shapeType)\r\n\r\n    def multipointm(self, points):\r\n        \"\"\"Creates a MULTIPOINTM shape.\r\n        Points is a list of xym values.\r\n        If the m (measure) value is not included, it defaults to None (NoData).\"\"\"\r\n        shapeType = MULTIPOINTM\r\n        points = [points] # nest the points inside a list to be compatible with the generic shapeparts method\r\n        self._shapeparts(parts=points, shapeType=shapeType)\r\n\r\n    def multipointz(self, points):\r\n        \"\"\"Creates a MULTIPOINTZ shape.\r\n        Points is a list of xyzm values.\r\n        If the z (elevation) value is not included, it defaults to 0.\r\n        If the m (measure) value is not included, it defaults to None (NoData).\"\"\"\r\n        shapeType = MULTIPOINTZ\r\n        points = [points] # nest the points inside a list to be compatible with the generic shapeparts method\r\n        self._shapeparts(parts=points, shapeType=shapeType)\r\n\r\n\r\n    def line(self, lines):\r\n        \"\"\"Creates a POLYLINE shape.\r\n        Lines is a collection of lines, each made up of a list of xy values.\"\"\"\r\n        shapeType = POLYLINE\r\n        self._shapeparts(parts=lines, shapeType=shapeType)\r\n\r\n    def linem(self, lines):\r\n        \"\"\"Creates a POLYLINEM shape.\r\n        Lines is a collection of lines, each made up of a list of xym values.\r\n        If the m (measure) value is not included, it defaults to None (NoData).\"\"\"\r\n        shapeType = POLYLINEM\r\n        self._shapeparts(parts=lines, shapeType=shapeType)\r\n\r\n    def linez(self, lines):\r\n        \"\"\"Creates a POLYLINEZ shape.\r\n        Lines is a collection of lines, each made up of a list of xyzm values.\r\n        If the z (elevation) value is not included, it defaults to 0.\r\n        If the m (measure) value is not included, it defaults to None (NoData).\"\"\"\r\n        shapeType = POLYLINEZ\r\n        self._shapeparts(parts=lines, shapeType=shapeType)\r\n\r\n\r\n    def poly(self, polys):\r\n        \"\"\"Creates a POLYGON shape.\r\n        Polys is a collection of polygons, each made up of a list of xy values.\r\n        Note that for ordinary polygons the coordinates must run in a clockwise direction.\r\n        If some of the polygons are holes, these must run in a counterclockwise direction.\"\"\"\r\n        shapeType = POLYGON\r\n        self._shapeparts(parts=polys, shapeType=shapeType)\r\n\r\n    def polym(self, polys):\r\n        \"\"\"Creates a POLYGONM shape.\r\n        Polys is a collection of polygons, each made up of a list of xym values.\r\n        Note that for ordinary polygons the coordinates must run in a clockwise direction.\r\n        If some of the polygons are holes, these must run in a counterclockwise direction.\r\n        If the m (measure) value is not included, it defaults to None (NoData).\"\"\"\r\n        shapeType = POLYGONM\r\n        self._shapeparts(parts=polys, shapeType=shapeType)\r\n\r\n    def polyz(self, polys):\r\n        \"\"\"Creates a POLYGONZ shape.\r\n        Polys is a collection of polygons, each made up of a list of xyzm values.\r\n        Note that for ordinary polygons the coordinates must run in a clockwise direction.\r\n        If some of the polygons are holes, these must run in a counterclockwise direction.\r\n        If the z (elevation) value is not included, it defaults to 0.\r\n        If the m (measure) value is not included, it defaults to None (NoData).\"\"\"\r\n        shapeType = POLYGONZ\r\n        self._shapeparts(parts=polys, shapeType=shapeType)\r\n\r\n\r\n    def multipatch(self, parts, partTypes):\r\n        \"\"\"Creates a MULTIPATCH shape.\r\n        Parts is a collection of 3D surface patches, each made up of a list of xyzm values.\r\n        PartTypes is a list of types that define each of the surface patches.\r\n        The types can be any of the following module constants: TRIANGLE_STRIP,\r\n        TRIANGLE_FAN, OUTER_RING, INNER_RING, FIRST_RING, or RING.\r\n        If the z (elavation) value is not included, it defaults to 0.\r\n        If the m (measure) value is not included, it defaults to None (NoData).\"\"\"\r\n        shapeType = MULTIPATCH\r\n        polyShape = Shape(shapeType)\r\n        polyShape.parts = []\r\n        polyShape.points = []\r\n        for part in parts:\r\n            # set part index position\r\n            polyShape.parts.append(len(polyShape.points))\r\n            # add points\r\n            for point in part:\r\n                # Ensure point is list\r\n                if not isinstance(point, list):\r\n                    point = list(point)\r\n                polyShape.points.append(point)\r\n        polyShape.partTypes = partTypes\r\n        # write the shape\r\n        self.shape(polyShape)\r\n\r\n\r\n    def _shapeparts(self, parts, shapeType):\r\n        \"\"\"Internal method for adding a shape that has multiple collections of points (parts):\r\n        lines, polygons, and multipoint shapes.\r\n        \"\"\"\r\n        polyShape = Shape(shapeType)\r\n        polyShape.parts = []\r\n        polyShape.points = []\r\n        for part in parts:\r\n            # set part index position\r\n            polyShape.parts.append(len(polyShape.points))\r\n            # add points\r\n            for point in part:\r\n                # Ensure point is list\r\n                if not isinstance(point, list):\r\n                    point = list(point)\r\n                polyShape.points.append(point)\r\n        # write the shape\r\n        self.shape(polyShape)\r\n\r\n    def field(self, name, fieldType=\"C\", size=\"50\", decimal=0):\r\n        \"\"\"Adds a dbf field descriptor to the shapefile.\"\"\"\r\n        if fieldType == \"D\":\r\n            size = \"8\"\r\n            decimal = 0\r\n        elif fieldType == \"L\":\r\n            size = \"1\"\r\n            decimal = 0\r\n        if len(self.fields) >= 2046:\r\n            raise ShapefileException(\r\n                \"Shapefile Writer reached maximum number of fields: 2046.\")\r\n        self.fields.append((name, fieldType, size, decimal))\r\n\r\n##    def saveShp(self, target):\r\n##        \"\"\"Save an shp file.\"\"\"\r\n##        if not hasattr(target, \"write\"):\r\n##            target = os.path.splitext(target)[0] + '.shp'\r\n##        self.shp = self.__getFileObj(target)\r\n##        self.__shapefileHeader(self.shp, headerType='shp')\r\n##        self.shp.seek(100)\r\n##        self._shp.seek(0)\r\n##        chunk = True\r\n##        while chunk:\r\n##            chunk = self._shp.read(self.bufsize)\r\n##            self.shp.write(chunk)\r\n##\r\n##    def saveShx(self, target):\r\n##        \"\"\"Save an shx file.\"\"\"\r\n##        if not hasattr(target, \"write\"):\r\n##            target = os.path.splitext(target)[0] + '.shx'\r\n##        self.shx = self.__getFileObj(target)\r\n##        self.__shapefileHeader(self.shx, headerType='shx')\r\n##        self.shx.seek(100)\r\n##        self._shx.seek(0)\r\n##        chunk = True\r\n##        while chunk:\r\n##            chunk = self._shx.read(self.bufsize)\r\n##            self.shx.write(chunk)\r\n##\r\n##    def saveDbf(self, target):\r\n##        \"\"\"Save a dbf file.\"\"\"\r\n##        if not hasattr(target, \"write\"):\r\n##            target = os.path.splitext(target)[0] + '.dbf'\r\n##        self.dbf = self.__getFileObj(target)\r\n##        self.__dbfHeader() # writes to .dbf\r\n##        self._dbf.seek(0)\r\n##        chunk = True\r\n##        while chunk:\r\n##            chunk = self._dbf.read(self.bufsize)\r\n##            self.dbf.write(chunk)\r\n\r\n##    def save(self, target=None, shp=None, shx=None, dbf=None):\r\n##        \"\"\"Save the shapefile data to three files or\r\n##        three file-like objects. SHP and DBF files can also\r\n##        be written exclusively using saveShp, saveShx, and saveDbf respectively.\r\n##        If target is specified but not shp, shx, or dbf then the target path and\r\n##        file name are used.  If no options or specified, a unique base file name\r\n##        is generated to save the files and the base file name is returned as a \r\n##        string. \r\n##        \"\"\"\r\n##        # Balance if already not balanced\r\n##        if shp and dbf:\r\n##            if self.autoBalance:\r\n##                self.balance()\r\n##            if self.recNum != self.shpNum:\r\n##                raise ShapefileException(\"When saving both the dbf and shp file, \"\r\n##                                         \"the number of records (%s) must correspond \"\r\n##                                         \"with the number of shapes (%s)\" % (self.recNum, self.shpNum))\r\n##        # Save\r\n##        if shp:\r\n##            self.saveShp(shp)\r\n##        if shx:\r\n##            self.saveShx(shx)\r\n##        if dbf:\r\n##            self.saveDbf(dbf)\r\n##        # Create a unique file name if one is not defined\r\n##        if not shp and not shx and not dbf:\r\n##            generated = False\r\n##            if not target:\r\n##                temp = tempfile.NamedTemporaryFile(prefix=\"shapefile_\",dir=os.getcwd())\r\n##                target = temp.name\r\n##                generated = True         \r\n##            self.saveShp(target)\r\n##            self.shp.close()\r\n##            self.saveShx(target)\r\n##            self.shx.close()\r\n##            self.saveDbf(target)\r\n##            self.dbf.close()\r\n##            if generated:\r\n##                return target\r\n\r\n# Begin Testing\r\ndef test(**kwargs):\r\n    import doctest\r\n    doctest.NORMALIZE_WHITESPACE = 1\r\n    verbosity = kwargs.get('verbose', 0)\r\n    if verbosity == 0:\r\n        print('Running doctests...')\r\n\r\n    # ignore py2-3 unicode differences\r\n    import re\r\n    class Py23DocChecker(doctest.OutputChecker):\r\n        def check_output(self, want, got, optionflags):\r\n            if sys.version_info[0] == 2:\r\n                got = re.sub(\"u'(.*?)'\", \"'\\\\1'\", got)\r\n                got = re.sub('u\"(.*?)\"', '\"\\\\1\"', got)\r\n            res = doctest.OutputChecker.check_output(self, want, got, optionflags)\r\n            return res\r\n        def summarize(self):\r\n            doctest.OutputChecker.summarize(True)\r\n\r\n    # run tests\r\n    runner = doctest.DocTestRunner(checker=Py23DocChecker(), verbose=verbosity)\r\n    with open(\"README.md\",\"rb\") as fobj:\r\n        test = doctest.DocTestParser().get_doctest(string=fobj.read().decode(\"utf8\").replace('\\r\\n','\\n'), globs={}, name=\"README\", filename=\"README.md\", lineno=0)\r\n    failure_count, test_count = runner.run(test)\r\n\r\n    # print results\r\n    if verbosity:\r\n        runner.summarize(True)\r\n    else:\r\n        if failure_count == 0:\r\n            print('All test passed successfully')\r\n        elif failure_count > 0:\r\n            runner.summarize(verbosity)\r\n\r\n    return failure_count\r\n    \r\nif __name__ == \"__main__\":\r\n    \"\"\"\r\n    Doctests are contained in the file 'README.md', and are tested using the built-in\r\n    testing libraries. \r\n    \"\"\"\r\n    failure_count = test()\r\n    sys.exit(failure_count)\r\n"
  },
  {
    "path": "core/lib/shapefile123.py",
    "content": "\"\"\"\nshapefile.py\nProvides read and write support for ESRI Shapefiles.\nauthor: jlawhead<at>geospatialpython.com\ndate: 2015/06/22\nversion: 1.2.3\nCompatible with Python versions 2.4-3.x\nversion changelog: Reader.iterShapeRecords() bugfix for Python 3\n\"\"\"\n\n__version__ = \"1.2.3\"\n\nfrom struct import pack, unpack, calcsize, error\nimport os\nimport sys\nimport time\nimport array\nimport tempfile\nimport itertools\n\n#\n# Constants for shape types\nNULL = 0\nPOINT = 1\nPOLYLINE = 3\nPOLYGON = 5\nMULTIPOINT = 8\nPOINTZ = 11\nPOLYLINEZ = 13\nPOLYGONZ = 15\nMULTIPOINTZ = 18\nPOINTM = 21\nPOLYLINEM = 23\nPOLYGONM = 25\nMULTIPOINTM = 28\nMULTIPATCH = 31\n\nPYTHON3 = sys.version_info[0] == 3\n\nif PYTHON3:\n    xrange = range\n    izip = zip\nelse:\n    from itertools import izip\n\ndef b(v):\n    if PYTHON3:\n        if isinstance(v, str):\n            # For python 3 encode str to bytes.\n            return v.encode('utf-8')\n        elif isinstance(v, bytes):\n            # Already bytes.\n            return v\n        else:\n            # Error.\n            raise Exception('Unknown input type')\n    else:\n        # For python 2 assume str passed in and return str.\n        return v\n\ndef u(v):\n    if PYTHON3:\n        # try/catch added 2014/05/07\n        # returned error on dbf of shapefile\n        # from www.naturalearthdata.com named\n        # \"ne_110m_admin_0_countries\".\n        # Just returning v as is seemed to fix\n        # the problem.  This function could\n        # be condensed further.\n        try:\n          if isinstance(v, bytes):\n              # For python 3 decode bytes to str.\n              return v.decode('utf-8')\n          elif isinstance(v, str):\n              # Already str.\n              return v\n          else:\n              # Error.\n              raise Exception('Unknown input type')\n        except: return v\n    else:\n        # For python 2 assume str passed in and return str.\n        return v\n\ndef is_string(v):\n    if PYTHON3:\n        return isinstance(v, str)\n    else:\n        return isinstance(v, basestring)\n\nclass _Array(array.array):\n    \"\"\"Converts python tuples to lits of the appropritate type.\n    Used to unpack different shapefile header parts.\"\"\"\n    def __repr__(self):\n        return str(self.tolist())\n\ndef signed_area(coords):\n    \"\"\"Return the signed area enclosed by a ring using the linear time\n    algorithm at http://www.cgafaq.info/wiki/Polygon_Area. A value >= 0\n    indicates a counter-clockwise oriented ring.\n    \"\"\"\n    xs, ys = map(list, zip(*coords))\n    xs.append(xs[1])\n    ys.append(ys[1])\n    return sum(xs[i]*(ys[i+1]-ys[i-1]) for i in range(1, len(coords)))/2.0\n\nclass _Shape:\n    def __init__(self, shapeType=None):\n        \"\"\"Stores the geometry of the different shape types\n        specified in the Shapefile spec. Shape types are\n        usually point, polyline, or polygons. Every shape type\n        except the \"Null\" type contains points at some level for\n        example verticies in a polygon. If a shape type has\n        multiple shapes containing points within a single\n        geometry record then those shapes are called parts. Parts\n        are designated by their starting index in geometry record's\n        list of shapes.\"\"\"\n        self.shapeType = shapeType\n        self.points = []\n\n    @property\n    def __geo_interface__(self):\n        if self.shapeType in [POINT, POINTM, POINTZ]:\n            return {\n            'type': 'Point',\n            'coordinates': tuple(self.points[0])\n            }\n        elif self.shapeType in [MULTIPOINT, MULTIPOINTM, MULTIPOINTZ]:\n            return {\n            'type': 'MultiPoint',\n            'coordinates': tuple([tuple(p) for p in self.points])\n            }\n        elif self.shapeType in [POLYLINE, POLYLINEM, POLYLINEZ]:\n            if len(self.parts) == 1:\n                return {\n                'type': 'LineString',\n                'coordinates': tuple([tuple(p) for p in self.points])\n                }\n            else:\n                ps = None\n                coordinates = []\n                for part in self.parts:\n                    if ps == None:\n                        ps = part\n                        continue\n                    else:\n                        coordinates.append(tuple([tuple(p) for p in self.points[ps:part]]))\n                        ps = part\n                else:\n                    coordinates.append(tuple([tuple(p) for p in self.points[part:]]))\n                return {\n                'type': 'MultiLineString',\n                'coordinates': tuple(coordinates)\n                }\n        elif self.shapeType in [POLYGON, POLYGONM, POLYGONZ]:\n            if len(self.parts) == 1:\n                return {\n                'type': 'Polygon',\n                'coordinates': (tuple([tuple(p) for p in self.points]),)\n                }\n            else:\n                ps = None\n                coordinates = []\n                for part in self.parts:\n                    if ps == None:\n                        ps = part\n                        continue\n                    else:\n                        coordinates.append(tuple([tuple(p) for p in self.points[ps:part]]))\n                        ps = part\n                else:\n                    coordinates.append(tuple([tuple(p) for p in self.points[part:]]))\n                polys = []\n                poly = [coordinates[0]]\n                for coord in coordinates[1:]:\n                    if signed_area(coord) < 0:\n                        polys.append(poly)\n                        poly = [coord]\n                    else:\n                        poly.append(coord)\n                polys.append(poly)\n                if len(polys) == 1:\n                    return {\n                    'type': 'Polygon',\n                    'coordinates': tuple(polys[0])\n                    }\n                elif len(polys) > 1:\n                    return {\n                    'type': 'MultiPolygon',\n                    'coordinates': polys\n                    }\n\nclass _ShapeRecord:\n    \"\"\"A shape object of any type.\"\"\"\n    def __init__(self, shape=None, record=None):\n        self.shape = shape\n        self.record = record\n\nclass ShapefileException(Exception):\n    \"\"\"An exception to handle shapefile specific problems.\"\"\"\n    pass\n\nclass Reader:\n    \"\"\"Reads the three files of a shapefile as a unit or\n    separately.  If one of the three files (.shp, .shx,\n    .dbf) is missing no exception is thrown until you try\n    to call a method that depends on that particular file.\n    The .shx index file is used if available for efficiency\n    but is not required to read the geometry from the .shp\n    file. The \"shapefile\" argument in the constructor is the\n    name of the file you want to open.\n\n    You can instantiate a Reader without specifying a shapefile\n    and then specify one later with the load() method.\n\n    Only the shapefile headers are read upon loading. Content\n    within each file is only accessed when required and as\n    efficiently as possible. Shapefiles are usually not large\n    but they can be.\n    \"\"\"\n    def __init__(self, *args, **kwargs):\n        self.shp = None\n        self.shx = None\n        self.dbf = None\n        self.shapeName = \"Not specified\"\n        self._offsets = []\n        self.shpLength = None\n        self.numRecords = None\n        self.fields = []\n        self.__dbfHdrLength = 0\n        # See if a shapefile name was passed as an argument\n        if len(args) > 0:\n            if is_string(args[0]):\n                self.load(args[0])\n                return\n        if \"shp\" in kwargs.keys():\n            if hasattr(kwargs[\"shp\"], \"read\"):\n                self.shp = kwargs[\"shp\"]\n                if hasattr(self.shp, \"seek\"):\n                    self.shp.seek(0)\n            if \"shx\" in kwargs.keys():\n                if hasattr(kwargs[\"shx\"], \"read\"):\n                    self.shx = kwargs[\"shx\"]\n                    if hasattr(self.shx, \"seek\"):\n                        self.shx.seek(0)\n        if \"dbf\" in kwargs.keys():\n            if hasattr(kwargs[\"dbf\"], \"read\"):\n                self.dbf = kwargs[\"dbf\"]\n                if hasattr(self.dbf, \"seek\"):\n                    self.dbf.seek(0)\n        if self.shp or self.dbf:        \n            self.load()\n        else:\n            raise ShapefileException(\"Shapefile Reader requires a shapefile or file-like object.\")\n\n    def load(self, shapefile=None):\n        \"\"\"Opens a shapefile from a filename or file-like\n        object. Normally this method would be called by the\n        constructor with the file object or file name as an\n        argument.\"\"\"\n        if shapefile:\n            (shapeName, ext) = os.path.splitext(shapefile)\n            self.shapeName = shapeName\n            try:\n                self.shp = open(\"%s.shp\" % shapeName, \"rb\")\n            except IOError:\n                raise ShapefileException(\"Unable to open %s.shp\" % shapeName)\n            try:\n                self.shx = open(\"%s.shx\" % shapeName, \"rb\")\n            except IOError:\n                raise ShapefileException(\"Unable to open %s.shx\" % shapeName)\n            try:\n                self.dbf = open(\"%s.dbf\" % shapeName, \"rb\")\n            except IOError:\n                raise ShapefileException(\"Unable to open %s.dbf\" % shapeName)\n        if self.shp:\n            self.__shpHeader()\n        if self.dbf:\n            self.__dbfHeader()\n\n    def __getFileObj(self, f):\n        \"\"\"Checks to see if the requested shapefile file object is\n        available. If not a ShapefileException is raised.\"\"\"\n        if not f:\n            raise ShapefileException(\"Shapefile Reader requires a shapefile or file-like object.\")\n        if self.shp and self.shpLength is None:\n            self.load()\n        if self.dbf and len(self.fields) == 0:\n            self.load()\n        return f\n\n    def __restrictIndex(self, i):\n        \"\"\"Provides list-like handling of a record index with a clearer\n        error message if the index is out of bounds.\"\"\"\n        if self.numRecords:\n            rmax = self.numRecords - 1\n            if abs(i) > rmax:\n                raise IndexError(\"Shape or Record index out of range.\")\n            if i < 0: i = range(self.numRecords)[i]\n        return i\n\n    def __shpHeader(self):\n        \"\"\"Reads the header information from a .shp or .shx file.\"\"\"\n        if not self.shp:\n            raise ShapefileException(\"Shapefile Reader requires a shapefile or file-like object. (no shp file found\")\n        shp = self.shp\n        # File length (16-bit word * 2 = bytes)\n        shp.seek(24)\n        self.shpLength = unpack(\">i\", shp.read(4))[0] * 2\n        # Shape type\n        shp.seek(32)\n        self.shapeType= unpack(\"<i\", shp.read(4))[0]\n        # The shapefile's bounding box (lower left, upper right)\n        self.bbox = _Array('d', unpack(\"<4d\", shp.read(32)))\n        # Elevation\n        self.elevation = _Array('d', unpack(\"<2d\", shp.read(16)))\n        # Measure\n        self.measure = _Array('d', unpack(\"<2d\", shp.read(16)))\n\n    def __shape(self):\n        \"\"\"Returns the header info and geometry for a single shape.\"\"\"\n        f = self.__getFileObj(self.shp)\n        record = _Shape()\n        nParts = nPoints = zmin = zmax = mmin = mmax = None\n        (recNum, recLength) = unpack(\">2i\", f.read(8))\n        # Determine the start of the next record\n        next = f.tell() + (2 * recLength)\n        shapeType = unpack(\"<i\", f.read(4))[0]\n        record.shapeType = shapeType\n        # For Null shapes create an empty points list for consistency\n        if shapeType == 0:\n            record.points = []\n        # All shape types capable of having a bounding box\n        elif shapeType in (3,5,8,13,15,18,23,25,28,31):\n            record.bbox = _Array('d', unpack(\"<4d\", f.read(32)))\n        # Shape types with parts\n        if shapeType in (3,5,13,15,23,25,31):\n            nParts = unpack(\"<i\", f.read(4))[0]\n        # Shape types with points\n        if shapeType in (3,5,8,13,15,23,25,31):\n            nPoints = unpack(\"<i\", f.read(4))[0]\n        # Read parts\n        if nParts:\n            record.parts = _Array('i', unpack(\"<%si\" % nParts, f.read(nParts * 4)))\n        # Read part types for Multipatch - 31\n        if shapeType == 31:\n            record.partTypes = _Array('i', unpack(\"<%si\" % nParts, f.read(nParts * 4)))\n        # Read points - produces a list of [x,y] values\n        if nPoints:\n            record.points = [_Array('d', unpack(\"<2d\", f.read(16))) for p in range(nPoints)]\n        # Read z extremes and values\n        if shapeType in (13,15,18,31):\n            (zmin, zmax) = unpack(\"<2d\", f.read(16))\n            record.z = _Array('d', unpack(\"<%sd\" % nPoints, f.read(nPoints * 8)))\n        # Read m extremes and values if header m values do not equal 0.0\n        if shapeType in (13,15,18,23,25,28,31) and not 0.0 in self.measure:\n            (mmin, mmax) = unpack(\"<2d\", f.read(16))\n            # Measure values less than -10e38 are nodata values according to the spec\n            record.m = []\n            for m in _Array('d', unpack(\"<%sd\" % nPoints, f.read(nPoints * 8))):\n                if m > -10e38:\n                    record.m.append(m)\n                else:\n                    record.m.append(None)\n        # Read a single point\n        if shapeType in (1,11,21):\n            record.points = [_Array('d', unpack(\"<2d\", f.read(16)))]\n        # Read a single Z value\n        if shapeType == 11:\n            record.z = unpack(\"<d\", f.read(8))\n        # Read a single M value\n        if shapeType in (11,21):\n            record.m = unpack(\"<d\", f.read(8))\n        # Seek to the end of this record as defined by the record header because\n        # the shapefile spec doesn't require the actual content to meet the header\n        # definition.  Probably allowed for lazy feature deletion. \n        f.seek(next)\n        return record\n\n    def __shapeIndex(self, i=None):\n        \"\"\"Returns the offset in a .shp file for a shape based on information\n        in the .shx index file.\"\"\"\n        shx = self.shx\n        if not shx:\n            return None\n        if not self._offsets:\n            # File length (16-bit word * 2 = bytes) - header length\n            shx.seek(24)\n            shxRecordLength = (unpack(\">i\", shx.read(4))[0] * 2) - 100\n            numRecords = shxRecordLength // 8\n            # Jump to the first record.\n            shx.seek(100)\n            for r in range(numRecords):\n                # Offsets are 16-bit words just like the file length\n                self._offsets.append(unpack(\">i\", shx.read(4))[0] * 2)\n                shx.seek(shx.tell() + 4)\n        if not i == None:\n            return self._offsets[i]\n\n    def shape(self, i=0):\n        \"\"\"Returns a shape object for a shape in the the geometry\n        record file.\"\"\"\n        shp = self.__getFileObj(self.shp)\n        i = self.__restrictIndex(i)\n        offset = self.__shapeIndex(i)\n        if not offset:\n            # Shx index not available so iterate the full list.\n            for j,k in enumerate(self.iterShapes()):\n                if j == i:\n                    return k\n        shp.seek(offset)\n        return self.__shape()\n\n    def shapes(self):\n        \"\"\"Returns all shapes in a shapefile.\"\"\"\n        shp = self.__getFileObj(self.shp)\n        # Found shapefiles which report incorrect\n        # shp file length in the header. Can't trust\n        # that so we seek to the end of the file\n        # and figure it out.\n        shp.seek(0,2)\n        self.shpLength = shp.tell()\n        shp.seek(100)\n        shapes = []\n        while shp.tell() < self.shpLength:\n            shapes.append(self.__shape())\n        return shapes\n\n    def iterShapes(self):\n        \"\"\"Serves up shapes in a shapefile as an iterator. Useful\n        for handling large shapefiles.\"\"\"\n        shp = self.__getFileObj(self.shp)\n        shp.seek(0,2)\n        self.shpLength = shp.tell()\n        shp.seek(100)\n        while shp.tell() < self.shpLength:\n            yield self.__shape()    \n\n    def __dbfHeaderLength(self):\n        \"\"\"Retrieves the header length of a dbf file header.\"\"\"\n        if not self.__dbfHdrLength:\n            if not self.dbf:\n                raise ShapefileException(\"Shapefile Reader requires a shapefile or file-like object. (no dbf file found)\")\n            dbf = self.dbf\n            (self.numRecords, self.__dbfHdrLength) = \\\n                    unpack(\"<xxxxLH22x\", dbf.read(32))\n        return self.__dbfHdrLength\n\n    def __dbfHeader(self):\n        \"\"\"Reads a dbf header. Xbase-related code borrows heavily from ActiveState Python Cookbook Recipe 362715 by Raymond Hettinger\"\"\"\n        if not self.dbf:\n            raise ShapefileException(\"Shapefile Reader requires a shapefile or file-like object. (no dbf file found)\")\n        dbf = self.dbf\n        headerLength = self.__dbfHeaderLength()\n        numFields = (headerLength - 33) // 32\n        for field in range(numFields):\n            fieldDesc = list(unpack(\"<11sc4xBB14x\", dbf.read(32)))\n            name = 0\n            idx = 0\n            if b(\"\\x00\") in fieldDesc[name]:\n                idx = fieldDesc[name].index(b(\"\\x00\"))\n            else:\n                idx = len(fieldDesc[name]) - 1\n            fieldDesc[name] = fieldDesc[name][:idx]\n            fieldDesc[name] = u(fieldDesc[name])\n            fieldDesc[name] = fieldDesc[name].lstrip()\n            fieldDesc[1] = u(fieldDesc[1])\n            self.fields.append(fieldDesc)\n        terminator = dbf.read(1)\n        if terminator != b(\"\\r\"):\n            raise ShapefileException(\"Shapefile dbf header lacks expected terminator. (likely corrupt?)\")\n        self.fields.insert(0, ('DeletionFlag', 'C', 1, 0))\n\n    def __recordFmt(self):\n        \"\"\"Calculates the size of a .shp geometry record.\"\"\"\n        if not self.numRecords:\n            self.__dbfHeader()\n        fmt = ''.join(['%ds' % fieldinfo[2] for fieldinfo in self.fields])\n        fmtSize = calcsize(fmt)\n        return (fmt, fmtSize)\n\n    def __record(self):\n        \"\"\"Reads and returns a dbf record row as a list of values.\"\"\"\n        f = self.__getFileObj(self.dbf)\n        recFmt = self.__recordFmt()\n        recordContents = unpack(recFmt[0], f.read(recFmt[1]))\n        if recordContents[0] != b(' '):\n            # deleted record\n            return None\n        record = []\n        for (name, typ, size, deci), value in zip(self.fields,\n                                                                                                recordContents):\n            if name == 'DeletionFlag':\n                continue\n            elif not value.strip():\n                record.append(value)\n                continue\n            elif typ == \"N\":\n                value = value.replace(b('\\0'), b('')).strip()\n                value = value.replace(b('*'), b(''))  # QGIS NULL is all '*' chars\n                if value == b(''):\n                    value = None\n                elif deci:\n                    value = float(value)\n                else:\n                    value = int(value)\n            elif typ == b('D'):\n                if value.count(b('0')) == len(value):  # QGIS NULL is all '0' chars\n                    value = None\n                else:\n                    try:\n                        y, m, d = int(value[:4]), int(value[4:6]), int(value[6:8])\n                        value = [y, m, d]\n                    except:\n                        value = value.strip()\n            elif typ == b('L'):\n                value = (value in b('YyTt') and b('T')) or \\\n                                        (value in b('NnFf') and b('F')) or b('?')\n            else:\n                value = u(value)\n                value = value.strip()\n            record.append(value)\n        return record\n\n    def record(self, i=0):\n        \"\"\"Returns a specific dbf record based on the supplied index.\"\"\"\n        f = self.__getFileObj(self.dbf)\n        if not self.numRecords:\n            self.__dbfHeader()\n        i = self.__restrictIndex(i)\n        recSize = self.__recordFmt()[1]\n        f.seek(0)\n        f.seek(self.__dbfHeaderLength() + (i * recSize))\n        return self.__record()\n\n    def records(self):\n        \"\"\"Returns all records in a dbf file.\"\"\"\n        if not self.numRecords:\n            self.__dbfHeader()\n        records = []\n        f = self.__getFileObj(self.dbf)\n        f.seek(self.__dbfHeaderLength())\n        for i in range(self.numRecords):\n            r = self.__record()\n            if r:\n                records.append(r)\n        return records\n\n    def iterRecords(self):\n        \"\"\"Serves up records in a dbf file as an iterator.\n        Useful for large shapefiles or dbf files.\"\"\"\n        if not self.numRecords:\n            self.__dbfHeader()\n        f = self.__getFileObj(self.dbf)\n        f.seek(self.__dbfHeaderLength())\n        for i in xrange(self.numRecords):\n            r = self.__record()\n            if r:\n                yield r\n\n    def shapeRecord(self, i=0):\n        \"\"\"Returns a combination geometry and attribute record for the\n        supplied record index.\"\"\"\n        i = self.__restrictIndex(i)\n        return _ShapeRecord(shape=self.shape(i), record=self.record(i))\n\n    def shapeRecords(self):\n        \"\"\"Returns a list of combination geometry/attribute records for\n        all records in a shapefile.\"\"\"\n        shapeRecords = []\n        return [_ShapeRecord(shape=rec[0], record=rec[1]) \\\n                                for rec in zip(self.shapes(), self.records())]\n\n    def iterShapeRecords(self):\n        \"\"\"Returns a generator of combination geometry/attribute records for\n        all records in a shapefile.\"\"\"\n        for shape, record in izip(self.iterShapes(), self.iterRecords()):\n            yield _ShapeRecord(shape=shape, record=record)\n\n\nclass Writer:\n    \"\"\"Provides write support for ESRI Shapefiles.\"\"\"\n    def __init__(self, shapeType=None):\n        self._shapes = []\n        self.fields = []\n        self.records = []\n        self.shapeType = shapeType\n        self.shp = None\n        self.shx = None\n        self.dbf = None\n        # Geometry record offsets and lengths for writing shx file.\n        self._offsets = []\n        self._lengths = []\n        # Use deletion flags in dbf? Default is false (0).\n        self.deletionFlag = 0\n\n    def __getFileObj(self, f):\n        \"\"\"Safety handler to verify file-like objects\"\"\"\n        if not f:\n            raise ShapefileException(\"No file-like object available.\")\n        elif hasattr(f, \"write\"):\n            return f\n        else:\n            pth = os.path.split(f)[0]\n            if pth and not os.path.exists(pth):\n                os.makedirs(pth)\n            return open(f, \"wb\")\n\n    def __shpFileLength(self):\n        \"\"\"Calculates the file length of the shp file.\"\"\"\n        # Start with header length\n        size = 100\n        # Calculate size of all shapes\n        for s in self._shapes:\n            # Add in record header and shape type fields\n            size += 12\n            # nParts and nPoints do not apply to all shapes\n            #if self.shapeType not in (0,1):\n            #       nParts = len(s.parts)\n            #       nPoints = len(s.points)\n            if hasattr(s,'parts'):\n                nParts = len(s.parts)\n            if hasattr(s,'points'):\n                nPoints = len(s.points)\n            # All shape types capable of having a bounding box\n            if self.shapeType in (3,5,8,13,15,18,23,25,28,31):\n                size += 32\n            # Shape types with parts\n            if self.shapeType in (3,5,13,15,23,25,31):\n                # Parts count\n                size += 4\n                # Parts index array\n                size += nParts * 4\n            # Shape types with points\n            if self.shapeType in (3,5,8,13,15,23,25,31):\n                # Points count\n                size += 4\n                # Points array\n                size += 16 * nPoints\n            # Calc size of part types for Multipatch (31)\n            if self.shapeType == 31:\n                size += nParts * 4\n            # Calc z extremes and values\n            if self.shapeType in (13,15,18,31):\n                # z extremes\n                size += 16\n                # z array\n                size += 8 * nPoints\n            # Calc m extremes and values\n            if self.shapeType in (23,25,31):\n                # m extremes\n                size += 16\n                # m array\n                size += 8 * nPoints\n            # Calc a single point\n            if self.shapeType in (1,11,21):\n                size += 16\n            # Calc a single Z value\n            if self.shapeType == 11:\n                size += 8\n            # Calc a single M value\n            if self.shapeType in (11,21):\n                size += 8\n        # Calculate size as 16-bit words\n        size //= 2\n        return size\n\n    def __bbox(self, shapes, shapeTypes=[]):\n        x = []\n        y = []\n        for s in shapes:\n            shapeType = self.shapeType\n            if shapeTypes:\n                shapeType = shapeTypes[shapes.index(s)]\n            px, py = list(zip(*s.points))[:2]\n            x.extend(px)\n            y.extend(py)\n        return [min(x), min(y), max(x), max(y)]\n\n    def __zbox(self, shapes, shapeTypes=[]):\n        z = []\n        for s in shapes:\n            try:\n                for p in s.points:\n                    z.append(p[2])\n            except IndexError:\n                pass\n        if not z: z.append(0)\n        return [min(z), max(z)]\n\n    def __mbox(self, shapes, shapeTypes=[]):\n        m = [0]\n        for s in shapes:\n            try:\n                for p in s.points:\n                    m.append(p[3])\n            except IndexError:\n                pass\n        return [min(m), max(m)]\n\n    def bbox(self):\n        \"\"\"Returns the current bounding box for the shapefile which is\n        the lower-left and upper-right corners. It does not contain the\n        elevation or measure extremes.\"\"\"\n        return self.__bbox(self._shapes)\n\n    def zbox(self):\n        \"\"\"Returns the current z extremes for the shapefile.\"\"\"\n        return self.__zbox(self._shapes)\n\n    def mbox(self):\n        \"\"\"Returns the current m extremes for the shapefile.\"\"\"\n        return self.__mbox(self._shapes)\n\n    def __shapefileHeader(self, fileObj, headerType='shp'):\n        \"\"\"Writes the specified header type to the specified file-like object.\n        Several of the shapefile formats are so similar that a single generic\n        method to read or write them is warranted.\"\"\"\n        f = self.__getFileObj(fileObj)\n        f.seek(0)\n        # File code, Unused bytes\n        f.write(pack(\">6i\", 9994,0,0,0,0,0))\n        # File length (Bytes / 2 = 16-bit words)\n        if headerType == 'shp':\n            f.write(pack(\">i\", self.__shpFileLength()))\n        elif headerType == 'shx':\n            f.write(pack('>i', ((100 + (len(self._shapes) * 8)) // 2)))\n        # Version, Shape type\n        f.write(pack(\"<2i\", 1000, self.shapeType))\n        # The shapefile's bounding box (lower left, upper right)\n        if self.shapeType != 0:\n            try:\n                f.write(pack(\"<4d\", *self.bbox()))\n            except error:\n                raise ShapefileException(\"Failed to write shapefile bounding box. Floats required.\")\n        else:\n            f.write(pack(\"<4d\", 0,0,0,0))\n        # Elevation\n        z = self.zbox()\n        # Measure\n        m = self.mbox()\n        try:\n            f.write(pack(\"<4d\", z[0], z[1], m[0], m[1]))\n        except error:\n            raise ShapefileException(\"Failed to write shapefile elevation and measure values. Floats required.\")\n\n    def __dbfHeader(self):\n        \"\"\"Writes the dbf header and field descriptors.\"\"\"\n        f = self.__getFileObj(self.dbf)\n        f.seek(0)\n        version = 3\n        year, month, day = time.localtime()[:3]\n        year -= 1900\n        # Remove deletion flag placeholder from fields\n        for field in self.fields:\n            if field[0].startswith(\"Deletion\"):\n                self.fields.remove(field)\n        numRecs = len(self.records)\n        numFields = len(self.fields)\n        headerLength = numFields * 32 + 33\n        recordLength = sum([int(field[2]) for field in self.fields]) + 1\n        header = pack('<BBBBLHH20x', version, year, month, day, numRecs,\n                headerLength, recordLength)\n        f.write(header)\n        # Field descriptors\n        for field in self.fields:\n            name, fieldType, size, decimal = field\n            name = b(name)\n            name = name.replace(b(' '), b('_'))\n            name = name.ljust(11).replace(b(' '), b('\\x00'))\n            fieldType = b(fieldType)\n            size = int(size)\n            fld = pack('<11sc4xBB14x', name, fieldType, size, decimal)\n            f.write(fld)\n        # Terminator\n        f.write(b('\\r'))\n\n    def __shpRecords(self):\n        \"\"\"Write the shp records\"\"\"\n        f = self.__getFileObj(self.shp)\n        f.seek(100)\n        recNum = 1\n        for s in self._shapes:\n            self._offsets.append(f.tell())\n            # Record number, Content length place holder\n            f.write(pack(\">2i\", recNum, 0))\n            recNum += 1\n            start = f.tell()\n            # Shape Type\n            if self.shapeType != 31:\n                s.shapeType = self.shapeType\n            f.write(pack(\"<i\", s.shapeType))\n            # All shape types capable of having a bounding box\n            if s.shapeType in (3,5,8,13,15,18,23,25,28,31):\n                try:\n                    f.write(pack(\"<4d\", *self.__bbox([s])))\n                except error:\n                    raise ShapefileException(\"Falied to write bounding box for record %s. Expected floats.\" % recNum)\n            # Shape types with parts\n            if s.shapeType in (3,5,13,15,23,25,31):\n                # Number of parts\n                f.write(pack(\"<i\", len(s.parts)))\n            # Shape types with multiple points per record\n            if s.shapeType in (3,5,8,13,15,23,25,31):\n                # Number of points\n                f.write(pack(\"<i\", len(s.points)))\n            # Write part indexes\n            if s.shapeType in (3,5,13,15,23,25,31):\n                for p in s.parts:\n                    f.write(pack(\"<i\", p))\n            # Part types for Multipatch (31)\n            if s.shapeType == 31:\n                for pt in s.partTypes:\n                    f.write(pack(\"<i\", pt))\n            # Write points for multiple-point records\n            if s.shapeType in (3,5,8,13,15,23,25,31):\n                try:\n                    [f.write(pack(\"<2d\", *p[:2])) for p in s.points]\n                except error:\n                    raise ShapefileException(\"Failed to write points for record %s. Expected floats.\" % recNum)\n            # Write z extremes and values\n            if s.shapeType in (13,15,18,31):\n                try:\n                    f.write(pack(\"<2d\", *self.__zbox([s])))\n                except error:\n                    raise ShapefileException(\"Failed to write elevation extremes for record %s. Expected floats.\" % recNum)\n                try:\n                    if hasattr(s,\"z\"):\n                        f.write(pack(\"<%sd\" % len(s.z), *s.z))\n                    else:\n                        [f.write(pack(\"<d\", p[2])) for p in s.points]  \n                except error:\n                    raise ShapefileException(\"Failed to write elevation values for record %s. Expected floats.\" % recNum)\n            # Write m extremes and values\n            if s.shapeType in (13,15,18,23,25,28,31):\n                try:\n                    if hasattr(s,\"m\"):\n                        f.write(pack(\"<%sd\" % len(s.m), *s.m))\n                    else:\n                        f.write(pack(\"<2d\", *self.__mbox([s])))\n                except error:\n                    raise ShapefileException(\"Failed to write measure extremes for record %s. Expected floats\" % recNum)\n                try:\n                    [f.write(pack(\"<d\", p[3])) for p in s.points]\n                except error:\n                    raise ShapefileException(\"Failed to write measure values for record %s. Expected floats\" % recNum)\n            # Write a single point\n            if s.shapeType in (1,11,21):\n                try:\n                    f.write(pack(\"<2d\", s.points[0][0], s.points[0][1]))\n                except error:\n                    raise ShapefileException(\"Failed to write point for record %s. Expected floats.\" % recNum)\n            # Write a single Z value\n            if s.shapeType == 11:\n                if hasattr(s, \"z\"):\n                    try:\n                        if not s.z:\n                            s.z = (0,)    \n                        f.write(pack(\"<d\", s.z[0]))\n                    except error:\n                        raise ShapefileException(\"Failed to write elevation value for record %s. Expected floats.\" % recNum)\n                else:\n                    try:\n                        if len(s.points[0])<3:\n                            s.points[0].append(0)\n                        f.write(pack(\"<d\", s.points[0][2]))\n                    except error:\n                        raise ShapefileException(\"Failed to write elevation value for record %s. Expected floats.\" % recNum)\n            # Write a single M value\n            if s.shapeType in (11,21):\n                if hasattr(s, \"m\"):\n                    try:\n                        if not s.m:\n                            s.m = (0,) \n                        f.write(pack(\"<1d\", s.m[0]))\n                    except error:\n                        raise ShapefileException(\"Failed to write measure value for record %s. Expected floats.\" % recNum)    \n                else:                                \n                    try:\n                        if len(s.points[0])<4:\n                            s.points[0].append(0)\n                        f.write(pack(\"<1d\", s.points[0][3]))\n                    except error:\n                        raise ShapefileException(\"Failed to write measure value for record %s. Expected floats.\" % recNum)\n            # Finalize record length as 16-bit words\n            finish = f.tell()\n            length = (finish - start) // 2\n            self._lengths.append(length)\n            # start - 4 bytes is the content length field\n            f.seek(start-4)\n            f.write(pack(\">i\", length))\n            f.seek(finish)\n\n    def __shxRecords(self):\n        \"\"\"Writes the shx records.\"\"\"\n        f = self.__getFileObj(self.shx)\n        f.seek(100)\n        for i in range(len(self._shapes)):\n            f.write(pack(\">i\", self._offsets[i] // 2))\n            f.write(pack(\">i\", self._lengths[i]))\n\n    def __dbfRecords(self):\n        \"\"\"Writes the dbf records.\"\"\"\n        f = self.__getFileObj(self.dbf)\n        for record in self.records:\n            if not self.fields[0][0].startswith(\"Deletion\"):\n                f.write(b(' ')) # deletion flag\n            for (fieldName, fieldType, size, dec), value in zip(self.fields, record):\n                fieldType = fieldType.upper()\n                size = int(size)\n                if fieldType.upper() == \"N\":\n                    value = str(value).rjust(size)\n                elif fieldType == 'L':\n                    value = str(value)[0].upper()\n                else:\n                    value = str(value)[:size].ljust(size)\n                if len(value) != size:\n                    raise ShapefileException(\n                        \"Shapefile Writer unable to pack incorrect sized value\"\n                        \" (size %d) into field '%s' (size %d).\" % (len(value), fieldName, size))\n                value = b(value)\n                f.write(value)\n\n    def null(self):\n        \"\"\"Creates a null shape.\"\"\"\n        self._shapes.append(_Shape(NULL))\n\n    def point(self, x, y, z=0, m=0):\n        \"\"\"Creates a point shape.\"\"\"\n        pointShape = _Shape(self.shapeType)\n        pointShape.points.append([x, y, z, m])\n        self._shapes.append(pointShape)\n\n    def line(self, parts=[], shapeType=POLYLINE):\n        \"\"\"Creates a line shape. This method is just a convienience method\n        which wraps 'poly()'.\n        \"\"\"\n        self.poly(parts, shapeType, [])\n\n    def poly(self, parts=[], shapeType=POLYGON, partTypes=[]):\n        \"\"\"Creates a shape that has multiple collections of points (parts)\n        including lines, polygons, and even multipoint shapes. If no shape type\n        is specified it defaults to 'polygon'. If no part types are specified\n        (which they normally won't be) then all parts default to the shape type.\n        \"\"\"\n        polyShape = _Shape(shapeType)\n        polyShape.parts = []\n        polyShape.points = []\n        # Make sure polygons are closed\n        if shapeType in (5,15,25,31):\n            for part in parts:\n                    if part[0] != part[-1]:\n                        part.append(part[0])\n        for part in parts:\n            polyShape.parts.append(len(polyShape.points))\n            for point in part:\n                # Ensure point is list\n                if not isinstance(point, list):\n                    point = list(point)\n                # Make sure point has z and m values\n                while len(point) < 4:\n                    point.append(0)\n                polyShape.points.append(point)\n        if polyShape.shapeType == 31:\n            if not partTypes:\n                for part in parts:\n                    partTypes.append(polyShape.shapeType)\n            polyShape.partTypes = partTypes\n        self._shapes.append(polyShape)\n\n    def field(self, name, fieldType=\"C\", size=\"50\", decimal=0):\n        \"\"\"Adds a dbf field descriptor to the shapefile.\"\"\"\n        self.fields.append((name, fieldType, size, decimal))\n\n    def record(self, *recordList, **recordDict):\n        \"\"\"Creates a dbf attribute record. You can submit either a sequence of\n        field values or keyword arguments of field names and values. Before\n        adding records you must add fields for the record values using the\n        fields() method. If the record values exceed the number of fields the\n        extra ones won't be added. In the case of using keyword arguments to specify\n        field/value pairs only fields matching the already registered fields\n        will be added.\"\"\"\n        record = []\n        fieldCount = len(self.fields)\n        # Compensate for deletion flag\n        if self.fields[0][0].startswith(\"Deletion\"): fieldCount -= 1\n        if recordList:\n            [record.append(recordList[i]) for i in range(fieldCount)]\n        elif recordDict:\n            for field in self.fields:\n                if field[0] in recordDict:\n                    val = recordDict[field[0]]\n                    if val is None:\n                        record.append(\"\")\n                    else:\n                        record.append(val)\n        if record:\n            self.records.append(record)\n\n    def shape(self, i):\n        return self._shapes[i]\n\n    def shapes(self):\n        \"\"\"Return the current list of shapes.\"\"\"\n        return self._shapes\n\n    def saveShp(self, target):\n        \"\"\"Save an shp file.\"\"\"\n        if not hasattr(target, \"write\"):\n            target = os.path.splitext(target)[0] + '.shp'\n        if not self.shapeType:\n            self.shapeType = self._shapes[0].shapeType\n        self.shp = self.__getFileObj(target)\n        self.__shapefileHeader(self.shp, headerType='shp')\n        self.__shpRecords()\n\n    def saveShx(self, target):\n        \"\"\"Save an shx file.\"\"\"\n        if not hasattr(target, \"write\"):\n            target = os.path.splitext(target)[0] + '.shx'\n        if not self.shapeType:\n            self.shapeType = self._shapes[0].shapeType\n        self.shx = self.__getFileObj(target)\n        self.__shapefileHeader(self.shx, headerType='shx')\n        self.__shxRecords()\n\n    def saveDbf(self, target):\n        \"\"\"Save a dbf file.\"\"\"\n        if not hasattr(target, \"write\"):\n            target = os.path.splitext(target)[0] + '.dbf'\n        self.dbf = self.__getFileObj(target)\n        self.__dbfHeader()\n        self.__dbfRecords()\n\n    def save(self, target=None, shp=None, shx=None, dbf=None):\n        \"\"\"Save the shapefile data to three files or\n        three file-like objects. SHP and DBF files can also\n        be written exclusively using saveShp, saveShx, and saveDbf respectively.\n        If target is specified but not shp,shx, or dbf then the target path and\n        file name are used.  If no options or specified, a unique base file name\n        is generated to save the files and the base file name is returned as a \n        string. \n        \"\"\"\n        # Create a unique file name if one is not defined\n        if shp:\n            self.saveShp(shp)\n        if shx:\n            self.saveShx(shx)\n        if dbf:\n            self.saveDbf(dbf)\n        elif not shp and not shx and not dbf:\n            generated = False\n            if not target:\n                temp = tempfile.NamedTemporaryFile(prefix=\"shapefile_\",dir=os.getcwd())\n                target = temp.name\n                generated = True         \n            self.saveShp(target)\n            self.shp.close()\n            self.saveShx(target)\n            self.shx.close()\n            self.saveDbf(target)\n            self.dbf.close()\n            if generated:\n                return target\nclass Editor(Writer):\n    def __init__(self, shapefile=None, shapeType=POINT, autoBalance=1):\n        self.autoBalance = autoBalance\n        if not shapefile:\n            Writer.__init__(self, shapeType)\n        elif is_string(shapefile):\n            base = os.path.splitext(shapefile)[0]\n            if os.path.isfile(\"%s.shp\" % base):\n                r = Reader(base)\n                Writer.__init__(self, r.shapeType)\n                self._shapes = r.shapes()\n                self.fields = r.fields\n                self.records = r.records()\n\n    def select(self, expr):\n        \"\"\"Select one or more shapes (to be implemented)\"\"\"\n        # TODO: Implement expressions to select shapes.\n        pass\n\n    def delete(self, shape=None, part=None, point=None):\n        \"\"\"Deletes the specified part of any shape by specifying a shape\n        number, part number, or point number.\"\"\"\n        # shape, part, point\n        if shape and part and point:\n            del self._shapes[shape][part][point]\n        # shape, part\n        elif shape and part and not point:\n            del self._shapes[shape][part]\n        # shape\n        elif shape and not part and not point:\n            del self._shapes[shape]\n        # point\n        elif not shape and not part and point:\n            for s in self._shapes:\n                if s.shapeType == 1:\n                    del self._shapes[point]\n                else:\n                    for part in s.parts:\n                        del s[part][point]\n        # part, point\n        elif not shape and part and point:\n            for s in self._shapes:\n                del s[part][point]\n        # part\n        elif not shape and part and not point:\n            for s in self._shapes:\n                del s[part]\n\n    def point(self, x=None, y=None, z=None, m=None, shape=None, part=None, point=None, addr=None):\n        \"\"\"Creates/updates a point shape. The arguments allows\n        you to update a specific point by shape, part, point of any\n        shape type.\"\"\"\n        # shape, part, point\n        if shape and part and point:\n            try: self._shapes[shape]\n            except IndexError: self._shapes.append([])\n            try: self._shapes[shape][part]\n            except IndexError: self._shapes[shape].append([])\n            try: self._shapes[shape][part][point]\n            except IndexError: self._shapes[shape][part].append([])\n            p = self._shapes[shape][part][point]\n            if x: p[0] = x\n            if y: p[1] = y\n            if z: p[2] = z\n            if m: p[3] = m\n            self._shapes[shape][part][point] = p\n        # shape, part\n        elif shape and part and not point:\n            try: self._shapes[shape]\n            except IndexError: self._shapes.append([])\n            try: self._shapes[shape][part]\n            except IndexError: self._shapes[shape].append([])\n            points = self._shapes[shape][part]\n            for i in range(len(points)):\n                p = points[i]\n                if x: p[0] = x\n                if y: p[1] = y\n                if z: p[2] = z\n                if m: p[3] = m\n                self._shapes[shape][part][i] = p\n        # shape\n        elif shape and not part and not point:\n            try: self._shapes[shape]\n            except IndexError: self._shapes.append([])\n\n        # point\n        # part\n        if addr:\n            shape, part, point = addr\n            self._shapes[shape][part][point] = [x, y, z, m]\n        else:\n            Writer.point(self, x, y, z, m)\n        if self.autoBalance:\n            self.balance()\n\n    def validate(self):\n        \"\"\"An optional method to try and validate the shapefile\n        as much as possible before writing it (not implemented).\"\"\"\n        #TODO: Implement validation method\n        pass\n\n    def balance(self):\n        \"\"\"Adds a corresponding empty attribute or null geometry record depending\n        on which type of record was created to make sure all three files\n        are in synch.\"\"\"\n        if len(self.records) > len(self._shapes):\n            self.null()\n        elif len(self.records) < len(self._shapes):\n            self.record()\n\n    def __fieldNorm(self, fieldName):\n        \"\"\"Normalizes a dbf field name to fit within the spec and the\n        expectations of certain ESRI software.\"\"\"\n        if len(fieldName) > 11: fieldName = fieldName[:11]\n        fieldName = fieldName.upper()\n        fieldName.replace(' ', '_')\n\n# Begin Testing\ndef test():\n    import doctest\n    doctest.NORMALIZE_WHITESPACE = 1\n    doctest.testfile(\"README.txt\", verbose=1)\n\nif __name__ == \"__main__\":\n    \"\"\"\n    Doctests are contained in the file 'README.txt'. This library was originally developed\n    using Python 2.3. Python 2.4 and above have some excellent improvements in the built-in\n    testing libraries but for now unit testing is done using what's available in\n    2.3.\n    \"\"\"\n    test()\n\n"
  },
  {
    "path": "core/maths/__init__.py",
    "content": "from .interpo import scale, linearInterpo\n'''\nfrom .maths.kmeans1D import kmeans1d, getBreaks\nfrom . import akima\nfrom fillnodata import replace_nans\n'''\n"
  },
  {
    "path": "core/maths/akima.py",
    "content": "# -*- coding: utf-8 -*-\r\n# akima.py\r\n\r\n# Copyright (c) 2007-2015, Christoph Gohlke\r\n# Copyright (c) 2007-2015, The Regents of the University of California\r\n# Produced at the Laboratory for Fluorescence Dynamics\r\n# All rights reserved.\r\n#\r\n# Redistribution and use in source and binary forms, with or without\r\n# modification, are permitted provided that the following conditions are met:\r\n#\r\n# * Redistributions of source code must retain the above copyright\r\n#   notice, this list of conditions and the following disclaimer.\r\n# * Redistributions in binary form must reproduce the above copyright\r\n#   notice, this list of conditions and the following disclaimer in the\r\n#   documentation and/or other materials provided with the distribution.\r\n# * Neither the name of the copyright holders nor the names of any\r\n#   contributors may be used to endorse or promote products derived\r\n#   from this software without specific prior written permission.\r\n#\r\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\r\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\r\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\r\n# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\r\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\r\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\r\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\r\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\r\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\r\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\r\n# POSSIBILITY OF SUCH DAMAGE.\r\n\r\n\"\"\"Interpolation of data points in a plane based on Akima's method.\r\n\r\nAkima's interpolation method uses a continuously differentiable sub-spline\r\nbuilt from piecewise cubic polynomials. The resultant curve passes through\r\nthe given data points and will appear smooth and natural.\r\n\r\n:Author:\r\n  `Christoph Gohlke <http://www.lfd.uci.edu/~gohlke/>`_\r\n\r\n:Organization:\r\n  Laboratory for Fluorescence Dynamics, University of California, Irvine\r\n\r\n:Version: 2015.01.29\r\n\r\nRequirements\r\n------------\r\n* `CPython 2.7 or 3.4 <http://www.python.org>`_\r\n* `Numpy 1.8 <http://www.numpy.org>`_\r\n* `Akima.c 2015.01.29 <http://www.lfd.uci.edu/~gohlke/>`_  (optional speedup)\r\n* `Matplotlib 1.4 <http://www.matplotlib.org>`_  (optional for plotting)\r\n\r\nNotes\r\n-----\r\nConsider using `scipy.interpolate.Akima1DInterpolator\r\n<http://docs.scipy.org/doc/scipy/reference/interpolate.html>`_.\r\n\r\nReferences\r\n----------\r\n(1) A new method of interpolation and smooth curve fitting based\r\n    on local procedures. Hiroshi Akima, J. ACM, October 1970, 17(4), 589-602.\r\n\r\nExamples\r\n--------\r\n>>> def example():\r\n...     '''Plot interpolated Gaussian noise.'''\r\n...     x = numpy.sort(numpy.random.random(10) * 100)\r\n...     y = numpy.random.normal(0.0, 0.1, size=len(x))\r\n...     x2 = numpy.arange(x[0], x[-1], 0.05)\r\n...     y2 = interpolate(x, y, x2)\r\n...     from matplotlib import pyplot\r\n...     pyplot.title(\"Akima interpolation of Gaussian noise\")\r\n...     pyplot.plot(x2, y2, \"b-\")\r\n...     pyplot.plot(x, y, \"ro\")\r\n...     pyplot.show()\r\n>>> example()\r\n\r\n\"\"\"\r\n\r\nimport numpy\r\n\r\n__version__ = '2015.01.29'\r\n__docformat__ = 'restructuredtext en'\r\n__all__ = 'interpolate',\r\n\r\n\r\ndef interpolate(x, y, x_new, axis=-1, out=None):\r\n    \"\"\"Return interpolated data using Akima's method.\r\n\r\n    This Python implementation is inspired by the Matlab(r) code by\r\n    N. Shamsundar. It lacks certain capabilities of the C implementation\r\n    such as the output array argument and interpolation along an axis of a\r\n    multidimensional data array.\r\n\r\n    Parameters\r\n    ----------\r\n    x : array like\r\n        1D array of monotonically increasing real values.\r\n    y : array like\r\n        N-D array of real values. y's length along the interpolation\r\n        axis must be equal to the length of x.\r\n    x_new : array like\r\n        New independent variables.\r\n    axis : int\r\n        Specifies axis of y along which to interpolate. Interpolation\r\n        defaults to last axis of y.\r\n    out : array\r\n        Optional array to receive results. Dimension at axis must equal\r\n        length of x.\r\n\r\n    Examples\r\n    --------\r\n    >>> interpolate([0, 1, 2], [0, 0, 1], [0.5, 1.5])\r\n    array([-0.125,  0.375])\r\n    >>> x = numpy.sort(numpy.random.random(10) * 10)\r\n    >>> y = numpy.random.normal(0.0, 0.1, size=len(x))\r\n    >>> z = interpolate(x, y, x)\r\n    >>> numpy.allclose(y, z)\r\n    True\r\n    >>> x = x[:10]\r\n    >>> y = numpy.reshape(y, (10, -1))\r\n    >>> z = numpy.reshape(y, (10, -1))\r\n    >>> interpolate(x, y, x, axis=0, out=z)\r\n    >>> numpy.allclose(y, z)\r\n    True\r\n\r\n    \"\"\"\r\n    x = numpy.array(x, dtype=numpy.float64, copy=True)\r\n    y = numpy.array(y, dtype=numpy.float64, copy=True)\r\n    xi = numpy.array(x_new, dtype=numpy.float64, copy=True)\r\n\r\n    if axis != -1 or out is not None or y.ndim != 1:\r\n        raise NotImplementedError(\"implemented in C extension module\")\r\n\r\n    if x.ndim != 1 or xi.ndim != 1:\r\n        raise ValueError(\"x-arrays must be one dimensional\")\r\n\r\n    n = len(x)\r\n    if n < 2:\r\n        raise ValueError(\"array too small\")\r\n    if n != y.shape[axis]:\r\n        raise ValueError(\"size of x-array must match data shape\")\r\n\r\n    dx = numpy.diff(x)\r\n    if any(dx <= 0.0):\r\n        raise ValueError(\"x-axis not valid\")\r\n\r\n    if any(xi < x[0]) or any(xi > x[-1]):\r\n        raise ValueError(\"interpolation x-axis out of bounds\")\r\n\r\n    m = numpy.diff(y) / dx\r\n    mm = 2.0 * m[0] - m[1]\r\n    mmm = 2.0 * mm - m[0]\r\n    mp = 2.0 * m[n - 2] - m[n - 3]\r\n    mpp = 2.0 * mp - m[n - 2]\r\n\r\n    m1 = numpy.concatenate(([mmm], [mm], m, [mp], [mpp]))\r\n\r\n    dm = numpy.abs(numpy.diff(m1))\r\n    f1 = dm[2:n + 2]\r\n    f2 = dm[0:n]\r\n    f12 = f1 + f2\r\n\r\n    ids = numpy.nonzero(f12 > 1e-9 * numpy.max(f12))[0]\r\n    b = m1[1:n + 1]\r\n\r\n    b[ids] = (f1[ids] * m1[ids + 1] + f2[ids] * m1[ids + 2]) / f12[ids]\r\n    c = (3.0 * m - 2.0 * b[0:n - 1] - b[1:n]) / dx\r\n    d = (b[0:n - 1] + b[1:n] - 2.0 * m) / dx ** 2\r\n\r\n    bins = numpy.digitize(xi, x)\r\n    bins = numpy.minimum(bins, n - 1) - 1\r\n    bb = bins[0:len(xi)]\r\n    wj = xi - x[bb]\r\n\r\n    return ((wj * d[bb] + c[bb]) * wj + b[bb]) * wj + y[bb]\r\n\r\n"
  },
  {
    "path": "core/maths/fillnodata.py",
    "content": "# -*- coding:utf-8 -*-\r\n\r\n# This file is part of BlenderGIS\r\n\r\n#  ***** GPL LICENSE BLOCK *****\r\n#\r\n#  This program is free software: you can redistribute it and/or modify\r\n#  it under the terms of the GNU General Public License as published by\r\n#  the Free Software Foundation, either version 3 of the License, or\r\n#  (at your option) any later version.\r\n#\r\n#  This program is distributed in the hope that it will be useful,\r\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\r\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\r\n#  GNU General Public License for more details.\r\n#\r\n#  You should have received a copy of the GNU General Public License\r\n#  along with this program.  If not, see <http://www.gnu.org/licenses/>.\r\n#  All rights reserved.\r\n#  ***** GPL LICENSE BLOCK *****\r\n\r\n\r\n\r\n\r\n########################################\r\n# Inpainting function\r\n# http://astrolitterbox.blogspot.fr/2012/03/healing-holes-in-arrays-in-python.html\r\n# https://github.com/gasagna/openpiv-python/blob/master/openpiv/src/lib.pyx\r\n\r\n\r\nimport numpy as np\r\n\r\nDTYPEf = np.float32\r\n#DTYPEi = np.int32\r\n\r\n\r\ndef replace_nans(array, max_iter, tolerance, kernel_size=1, method='localmean'):\r\n\t\"\"\"\r\n\tReplace NaN elements in an array using an iterative image inpainting algorithm.\r\n\tThe algorithm is the following:\r\n\t1) For each element in the input array, replace it by a weighted average\r\n\tof the neighbouring elements which are not NaN themselves. The weights depends\r\n\tof the method type. If ``method=localmean`` weight are equal to 1/( (2*kernel_size+1)**2 -1 )\r\n\t2) Several iterations are needed if there are adjacent NaN elements.\r\n\tIf this is the case, information is \"spread\" from the edges of the missing\r\n\tregions iteratively, until the variation is below a certain threshold.\r\n\r\n\tParameters\r\n\t----------\r\n\tarray : 2d np.ndarray\r\n\tan array containing NaN elements that have to be replaced\r\n\r\n\tmax_iter : int\r\n\tthe number of iterations\r\n\r\n\tkernel_size : int\r\n\tthe size of the kernel, default is 1\r\n\r\n\tmethod : str\r\n\tthe method used to replace invalid values. Valid options are 'localmean', 'idw'.\r\n\r\n\tReturns\r\n\t-------\r\n\tfilled : 2d np.ndarray\r\n\ta copy of the input array, where NaN elements have been replaced.\r\n\t\"\"\"\r\n\r\n\tfilled = np.empty( [array.shape[0], array.shape[1]], dtype=DTYPEf)\r\n\tkernel = np.empty( (2*kernel_size+1, 2*kernel_size+1), dtype=DTYPEf )\r\n\r\n\t# indices where array is NaN\r\n\tinans, jnans = np.nonzero( np.isnan(array) )\r\n\r\n\t# number of NaN elements\r\n\tn_nans = len(inans)\r\n\r\n\t# arrays which contain replaced values to check for convergence\r\n\treplaced_new = np.zeros( n_nans, dtype=DTYPEf)\r\n\treplaced_old = np.zeros( n_nans, dtype=DTYPEf)\r\n\r\n\t# depending on kernel type, fill kernel array\r\n\tif method == 'localmean':\r\n\t\t# weight are equal to 1/( (2*kernel_size+1)**2 -1 )\r\n\t\tfor i in range(2*kernel_size+1):\r\n\t\t\tfor j in range(2*kernel_size+1):\r\n\t\t\t\tkernel[i,j] = 1\r\n\t\t#print(kernel, 'kernel')\r\n\telif method == 'idw':\r\n\t\tkernel = np.array([[0, 0.5, 0.5, 0.5,0],\r\n\t\t\t\t  [0.5,0.75,0.75,0.75,0.5],\r\n\t\t\t\t  [0.5,0.75,1,0.75,0.5],\r\n\t\t\t\t  [0.5,0.75,0.75,0.5,1],\r\n\t\t\t\t  [0, 0.5, 0.5 ,0.5 ,0]])\r\n\t\t#print(kernel, 'kernel')\r\n\telse:\r\n\t\traise ValueError(\"method not valid. Should be one of 'localmean', 'idw'.\")\r\n\r\n\t# fill new array with input elements\r\n\tfor i in range(array.shape[0]):\r\n\t\tfor j in range(array.shape[1]):\r\n\t\t\tfilled[i,j] = array[i,j]\r\n\r\n\t# make several passes\r\n\t# until we reach convergence\r\n\tfor it in range(max_iter):\r\n\t\t#print('Fill NaN iteration', it)\r\n\t\t# for each NaN element\r\n\t\tfor k in range(n_nans):\r\n\t\t\ti = inans[k]\r\n\t\t\tj = jnans[k]\r\n\r\n\t\t\t# initialize to zero\r\n\t\t\tfilled[i,j] = 0.0\r\n\t\t\tn = 0\r\n\r\n\t\t\t# loop over the kernel\r\n\t\t\tfor I in range(2*kernel_size+1):\r\n\t\t\t\tfor J in range(2*kernel_size+1):\r\n\r\n\t\t\t\t\t# if we are not out of the boundaries\r\n\t\t\t\t\tif i+I-kernel_size < array.shape[0] and i+I-kernel_size >= 0:\r\n\t\t\t\t\t\tif j+J-kernel_size < array.shape[1] and j+J-kernel_size >= 0:\r\n\r\n\t\t\t\t\t\t\t# if the neighbour element is not NaN itself.\r\n\t\t\t\t\t\t\tif filled[i+I-kernel_size, j+J-kernel_size] == filled[i+I-kernel_size, j+J-kernel_size] :\r\n\r\n\t\t\t\t\t\t\t\t# do not sum itself\r\n\t\t\t\t\t\t\t\tif I-kernel_size != 0 and J-kernel_size != 0:\r\n\r\n\t\t\t\t\t\t\t\t\t# convolve kernel with original array\r\n\t\t\t\t\t\t\t\t\tfilled[i,j] = filled[i,j] + filled[i+I-kernel_size, j+J-kernel_size]*kernel[I, J]\r\n\t\t\t\t\t\t\t\t\tn = n + 1*kernel[I,J]\r\n\t\t\t# divide value by effective number of added elements\r\n\t\t\tif n != 0:\r\n\t\t\t\tfilled[i,j] = filled[i,j] / n\r\n\t\t\t\treplaced_new[k] = filled[i,j]\r\n\t\t\telse:\r\n\t\t\t\tfilled[i,j] = np.nan\r\n\r\n\t\t# check if mean square difference between values of replaced\r\n\t\t# elements is below a certain tolerance\r\n\t\t#print('tolerance', np.mean( (replaced_new-replaced_old)**2 ))\r\n\t\tif np.mean( (replaced_new-replaced_old)**2 ) < tolerance:\r\n\t\t\tbreak\r\n\t\telse:\r\n\t\t\tfor l in range(n_nans):\r\n\t\t\t\treplaced_old[l] = replaced_new[l]\r\n\r\n\treturn filled\r\n\r\n\r\ndef sincinterp(image, x,  y, kernel_size=3 ):\r\n\t\"\"\"\r\n\tRe-sample an image at intermediate positions between pixels.\r\n\tThis function uses a cardinal interpolation formula which limits\r\n\tthe loss of information in the resampling process. It uses a limited\r\n\tnumber of neighbouring pixels.\r\n\r\n\tThe new image :math:`im^+` at fractional locations :math:`x` and :math:`y` is computed as:\r\n\t.. math::\r\n\tim^+(x,y) = \\sum_{i=-\\mathtt{kernel\\_size}}^{i=\\mathtt{kernel\\_size}} \\sum_{j=-\\mathtt{kernel\\_size}}^{j=\\mathtt{kernel\\_size}} \\mathtt{image}(i,j) sin[\\pi(i-\\mathtt{x})] sin[\\pi(j-\\mathtt{y})] / \\pi(i-\\mathtt{x}) / \\pi(j-\\mathtt{y})\r\n\r\n\tParameters\r\n\t----------\r\n\timage : np.ndarray, dtype np.int32\r\n\tthe image array.\r\n\r\n\tx : two dimensions np.ndarray of floats\r\n\tan array containing fractional pixel row\r\n\tpositions at which to interpolate the image\r\n\r\n\ty : two dimensions np.ndarray of floats\r\n\tan array containing fractional pixel column\r\n\tpositions at which to interpolate the image\r\n\r\n\tkernel_size : int\r\n\tinterpolation is performed over a ``(2*kernel_size+1)*(2*kernel_size+1)``\r\n\tsubmatrix in the neighbourhood of each interpolation point.\r\n\r\n\tReturns\r\n\t-------\r\n\tim : np.ndarray, dtype np.float64\r\n\tthe interpolated value of ``image`` at the points specified by ``x`` and ``y``\r\n\t\"\"\"\r\n\r\n\t# the output array\r\n\tr = np.zeros( [x.shape[0], x.shape[1]], dtype=DTYPEf)\r\n\r\n\t# fast pi\r\n\tpi = 3.1419\r\n\r\n\t# for each point of the output array\r\n\tfor I in range(x.shape[0]):\r\n\t\tfor J in range(x.shape[1]):\r\n\r\n\t\t\t#loop over all neighbouring grid points\r\n\t\t\tfor i in range( int(x[I,J])-kernel_size, int(x[I,J])+kernel_size+1 ):\r\n\t\t\t\tfor j in range( int(y[I,J])-kernel_size, int(y[I,J])+kernel_size+1 ):\r\n\t\t\t\t\t# check that we are in the boundaries\r\n\t\t\t\t\tif i >= 0 and i <= image.shape[0] and j >= 0 and j <= image.shape[1]:\r\n\t\t\t\t\t\tif (i-x[I,J]) == 0.0 and (j-y[I,J]) == 0.0:\r\n\t\t\t\t\t\t\tr[I,J] = r[I,J] + image[i,j]\r\n\t\t\t\t\t\telif (i-x[I,J]) == 0.0:\r\n\t\t\t\t\t\t\tr[I,J] = r[I,J] + image[i,j] * np.sin( pi*(j-y[I,J]) )/( pi*(j-y[I,J]) )\r\n\t\t\t\t\t\telif (j-y[I,J]) == 0.0:\r\n\t\t\t\t\t\t\tr[I,J] = r[I,J] + image[i,j] * np.sin( pi*(i-x[I,J]) )/( pi*(i-x[I,J]) )\r\n\t\t\t\t\t\telse:\r\n\t\t\t\t\t\t\tr[I,J] = r[I,J] + image[i,j] * np.sin( pi*(i-x[I,J]) )*np.sin( pi*(j-y[I,J]) )/( pi*pi*(i-x[I,J])*(j-y[I,J]))\r\n\treturn r\r\n"
  },
  {
    "path": "core/maths/interpo.py",
    "content": "\n\n#Scale/normalize function : linear stretch from lowest value to highest value\n#########################################\ndef scale(inVal, inMin, inMax, outMin, outMax):\n\treturn (inVal - inMin) * (outMax - outMin) / (inMax - inMin) + outMin\n\n\n\ndef linearInterpo(x1, x2, y1, y2, x):\n\t#Linear interpolation = y1 + slope * tx\n\tdx = x2 - x1\n\tdy = y2-y1\n\tslope = dy/dx\n\ttx = x - x1 #position from x1 (target x)\n\treturn y1 + slope * tx\n"
  },
  {
    "path": "core/maths/kmeans1D.py",
    "content": "\"\"\"\nkmeans1D.py\nAuthor : domlysz@gmail.com\nDate : february 2016\nLicense : GPL\n\nThis file is part of BlenderGIS.\nThis is a kmeans implementation optimized for 1D data.\n\nOriginal kmeans code :\nhttps://gist.github.com/iandanforth/5862470\n\n1D optimizations are inspired from this talking :\nhttp://stats.stackexchange.com/questions/40454/determine-different-clusters-of-1d-data-from-database\n\nOptimizations consists to :\n-sort the data and initialize clusters with a quantile classification\n-compute distance in 1D instead of euclidean\n-optimize only the borders of the clusters instead of test each cluster values\n\nClustering results are similar to Jenks natural break and ckmeans algorithms.\nThere are Python implementations of these alg. based on javascript code from simple-statistics library :\n* Jenks : https://gist.github.com/llimllib/4974446 (https://gist.github.com/tmcw/4977508)\n* Ckmeans : https://github.com/llimllib/ckmeans (https://github.com/simple-statistics/simple-statistics/blob/master/src/ckmeans.js)\n\nBut both are terribly slow because there is a lot of exponential-time looping. These algorithms makes this somewhat inevitable.\nIn contrast, this script works in a reasonable time, but keep in mind it's not Jenks. We just use cluster's centroids (mean) as\nreference to distribute the values while Jenks try to minimize within-class variance, and maximizes between group variance.\n\"\"\"\n\nfrom ..utils.timing import perf_clock\n\n\ndef kmeans1d(data, k, cutoff=False, maxIter=False):\n\t'''\n\tCompute natural breaks of a one dimensionnal list through an optimized kmeans algorithm\n\tInputs:\n\t* data = input list, must be sorted beforehand\n\t* k = number of expected classes\n\t* cutoff (optional) = stop algorithm when centroids shift are under this value\n\t* maxIter (optional) = stop algorithm when iteration count reach this value\n\tOutput:\n\t* A list of k clusters. A cluster is represented by a tuple containing first and last index of the cluster's values.\n\tUse these index on the input data list to retreive the effectives values containing in a cluster.\n\t'''\n\n\tdef getClusterValues(cluster):\n\t\ti, j = cluster\n\t\treturn data[i:j+1]\n\n\tdef getClusterCentroid(cluster):\n\t\tvalues = getClusterValues(cluster)\n\t\treturn sum(values) / len(values)\n\n\tn = len(data)\n\tif k >= n:\n\t\traise ValueError('Too many expected classes')\n\tif k == 1:\n\t\treturn [ [0, n-1] ]\n\n\t# Step 1: Create k clusters with quantile classification\n\t#  quantile = number of value per clusters\n\tq = int(n // k) #with floor, last cluster will be bigger the others, with ceil it will be smaller\n\tif q == 1:\n\t\traise ValueError('Too many expected classes')\n\t#  define a cluster with its first and last index\n\tclusters = [ [i, i+q-1] for i in range(0, q*k, q)]\n\t#  adjust the last index of the last cluster to the effective number of value\n\tclusters[-1][1] = n-1\n\n\t# Get centroids before first iter\n\tcentroids = [getClusterCentroid(c) for c in clusters]\n\n\t# Loop through the dataset until the clusters stabilize\n\tloopCounter = 0\n\tchangeOccured = True\n\n\twhile changeOccured:\n\t\tloopCounter += 1\n\n\t\t# Will be set to true if at least one border has been adjusted\n\t\tchangeOccured = False\n\n\t\t# Step 2 : for each border...\n\t\tfor i in range(k-1):\n\t\t\tc1 = clusters[i] #current cluster\n\t\t\tc2 = clusters[i+1] #next cluster\n\n\t\t\t#tag if this border has been adjusted or not\n\t\t\tadjusted = False\n\n\t\t\t# Test the distance between the right border of the current cluster and the neightbors centroids\n\t\t\t# Move the values if it's closer to the next cluster's centroid.\n\t\t\t# Then, test the new right border or stop if no more move is needed.\n\t\t\twhile True:\n\t\t\t\tif c1[0] == c1[1]:\n\t\t\t\t\t# only one value remaining in the current cluster\n\t\t\t\t\t# stop executing any more move to avoid having an empty cluster\n\t\t\t\t\tbreak\n\t\t\t\tbreakValue = data[c1[1]]\n\t\t\t\tdst1 = abs(breakValue - centroids[i])\n\t\t\t\tdst2 = abs(breakValue - centroids[i+1])\n\t\t\t\tif dst1 > dst2:\n\t\t\t\t\t# Adjust border : move last value of the current cluster to the next cluster\n\t\t\t\t\tc1[1] -= 1 #decrease right border index of current cluster\n\t\t\t\t\tc2[0] -= 1 #decrease left border index of the next cluster\n\t\t\t\t\tadjusted = True\n\t\t\t\telse:\n\t\t\t\t\tbreak\n\n\t\t\t# Test left border of next cluster only if we don't have adjusted the right border of current cluster\n\t\t\tif not adjusted:\n\t\t\t\t# Test the distance between the left border of the next cluster and the neightbors centroids\n\t\t\t\t# Move the values if it's closer to the current cluster's centroid.\n\t\t\t\t# Then, test the new left border or stop if no more move is needed.\n\t\t\t\twhile True:\n\t\t\t\t\tif c2[0] == c2[1]:\n\t\t\t\t\t\t# only one value remaining in the next cluster\n\t\t\t\t\t\t# stop executing any more move to avoid having an empty cluster\n\t\t\t\t\t\tbreak\n\t\t\t\t\tbreakValue = data[c2[0]]\n\t\t\t\t\tdst1 = abs(breakValue - centroids[i])\n\t\t\t\t\tdst2 = abs(breakValue - centroids[i+1])\n\t\t\t\t\tif dst2 > dst1:\n\t\t\t\t\t\t# Adjust border : move first value of the next cluster to the current cluster\n\t\t\t\t\t\tc2[0] += 1 #increase left border index of the next cluster\n\t\t\t\t\t\tc1[1] += 1 #increase right border index of current cluster\n\t\t\t\t\t\tadjusted = True\n\t\t\t\t\telse:\n\t\t\t\t\t\tbreak\n\n\t\t\t# Loop again if some borders were adjusted\n\t\t\t# or stop looping if no more move are possible\n\t\t\tif adjusted:\n\t\t\t\tchangeOccured = True\n\n\t\t# Update centroids and compute the bigger shift\n\t\tnewCentroids = [getClusterCentroid(c) for c in clusters]\n\t\tbiggest_shift = max([abs(newCentroids[i] - centroids[i]) for i in range(k)])\n\t\tcentroids = newCentroids\n\n\t\t# Force stopping the main loop ...\n\t\t# > if the centroids have stopped moving much (in the case we set a cutoff value)\n\t\t# > or if we reach max iteration value (in the case we set a maxIter value)\n\t\tif (cutoff and biggest_shift < cutoff) or (maxIter and loopCounter == maxIter):\n\t\t\tbreak\n\n\t#print(\"Converged after %s iterations\" % loopCounter)\n\treturn clusters\n\n\n#-----------------\n#Helpers to get values from clusters's indices list returning by kmeans1d function\n\ndef getClustersValues(data, clusters):\n\treturn [data[i:j+1] for i, j in clusters]\n\ndef getBreaks(data, clusters, includeBounds=False):\n\tif includeBounds:\n\t\treturn [data[0]] + [data[j] for i, j in clusters]\n\telse:\n\t\treturn [data[j] for i, j in clusters[:-1]]\n\n\n\nif __name__ == '__main__':\n\timport random, time\n\n\t#make data with a gap between 1000 and 2000\n\tdata = [random.uniform(0, 1000) for i in range(10000)]\n\tdata.extend([random.uniform(2000, 4000) for i in range(10000)])\n\tdata.sort()\n\n\tk = 4\n\n\tprint('---------------')\n\tprint('%i values, %i classes' %(len(data),k))\n\tt1 = perf_clock()\n\tclusters = kmeans1d(data, k)\n\tt2 = perf_clock()\n\tprint('Completed in %f seconds' %(t2-t1))\n\n\tprint('Breaks :')\n\tprint(getBreaks(data, clusters))\n\n\tprint('Clusters details (nb values, min, max) :')\n\tfor clusterValues in getClustersValues(data, clusters):\n\t\tprint( len(clusterValues), clusterValues[0], clusterValues[-1] )\n"
  },
  {
    "path": "core/proj/__init__.py",
    "content": "from .srs import SRS\nfrom .reproj import Reproj, reprojPt, reprojPts, reprojBbox, reprojImg\nfrom .srv import EPSGIO, TWCC, MapTilerCoordinates\nfrom .ellps import dd2meters, meters2dd, Ellps, GRS80\n"
  },
  {
    "path": "core/proj/ellps.py",
    "content": "import math\n\n\nclass Ellps():\n\t\"\"\"ellipsoid\"\"\"\n\tdef __init__(self, a, b):\n\t\tself.a =  a#equatorial radius in meters\n\t\tself.b =  b#polar radius in meters\n\t\tself.f = (self.a-self.b)/self.a#inverse flat\n\t\tself.perimeter = (2*math.pi*self.a)#perimeter at equator\n\nGRS80 = Ellps(6378137, 6356752.314245)\n\ndef dd2meters(dst):\n\t\"\"\"\n\tBasic function to approximaly convert a short distance in decimal degrees to meters\n\tOnly true at equator and along horizontal axis\n\t\"\"\"\n\tk = GRS80.perimeter/360\n\treturn dst * k\n\ndef meters2dd(dst):\n\tk = GRS80.perimeter/360\n\treturn dst / k\n"
  },
  {
    "path": "core/proj/reproj.py",
    "content": "# -*- coding:utf-8 -*-\n\n#  ***** GPL LICENSE BLOCK *****\n#\n#  This program is free software: you can redistribute it and/or modify\n#  it under the terms of the GNU General Public License as published by\n#  the Free Software Foundation, either version 3 of the License, or\n#  (at your option) any later version.\n#\n#  This program is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#  GNU General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public License\n#  along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#  All rights reserved.\n#  ***** GPL LICENSE BLOCK *****\n\n\nimport math\n\nfrom .srs import SRS\nfrom .utm import UTM, UTM_EPSG_CODES\nfrom .ellps import GRS80\nfrom .srv import MapTilerCoordinates\n\nfrom ..errors import ReprojError\nfrom ..utils import BBOX\nfrom ..checkdeps import HAS_GDAL, HAS_PYPROJ\nfrom .. import settings\n\nif HAS_GDAL:\n\tfrom osgeo import osr, gdal\n\nif HAS_PYPROJ:\n\timport pyproj\n\n\n######################################\n# Build in functions\n\ndef webMercToLonLat(x, y):\n\tk = GRS80.perimeter/360\n\tlon = x / k\n\tlat = y / k\n\tlat = 180 / math.pi * (2 * math.atan( math.exp( lat * math.pi / 180.0)) - math.pi / 2.0)\n\treturn lon, lat\n\ndef lonLatToWebMerc(lon, lat):\n\tk = GRS80.perimeter/360\n\tx = lon * k\n\tlat = math.log( math.tan((90 + lat) * math.pi / 360.0 )) / (math.pi / 180.0)\n\ty = lat * k\n\treturn x, y\n\n\n######################################\n# Raster reproj using GDAL\n\ndef reprojImg(crs1, crs2, ds1, out_ul=None, out_size=None, out_res=None, sqPx=False, resamplAlg='BL', path=None, geoTiffOptions={'TFW':'YES', 'TILED':'YES', 'BIGTIFF':'YES', 'COMPRESS':'JPEG', 'JPEG_QUALITY':80, 'PHOTOMETRIC':'YCBCR'}):\n\t'''\n\tUse GDAL Python binding to reproject an image\n\tcrs1, crs2 >> epsg code\n\tds1 >> input GDAL dataset object\n\tout_ul >> [tuple] output raster top left coords (same as input if None)\n\tout_size >> |tuple], output raster size (same as input is None)\n\tout_res >> [number], output raster resolution (same as input if None) (resx = resy)\n\tsqPx >> [boolean] force square pixel resolution when resoltion is automatically computed\n\tpath >> a geotiff file path to store the result into (optional)\n\tgeoTiffOptions >> GDAL create option for tiff format (optional)\n\treturn ds2 >> output GDAL dataset object. If path is None, the dataset will be stored in memory however into a geotiff file on disk\n\t'''\n\n\tif not HAS_GDAL:\n\t\traise NotImplementedError\n\n\tgeoTrans = ds1.GetGeoTransform()\n\tif geoTrans is not None:\n\t\txmin, resx, rotx, ymax, roty, resy = geoTrans\n\t\t#Note that instead of worldfile, topleft geotag is at corner not pixel center\n\telse:\n\t\traise IOError(\"Reprojection fails: input raster is not georeferenced\")\n\n\n\timg_w, img_h = ds1.RasterXSize, ds1.RasterYSize\n\tnbBands = ds1.RasterCount\n\tdtype = gdal.GetDataTypeName(ds1.GetRasterBand(1).DataType)\n\n\tif rotx == roty == 0:\n\t\txmax = xmin + img_w * resx\n\t\tymin = ymax + img_h * resy\n\t\tbbox = BBOX(xmin, ymin, xmax, ymax)\n\telse:\n\t\traise IOError(\"Raster must be rectified (no rotation parameters)\")\n\t\t#TODO reuse the GeoRef class to extract bbox even if there are rotation parameters\n\n\t#Assign input CRS to input datasource\n\tprj1 = SRS(crs1).getOgrSpatialRef()\n\twkt1 = prj1.ExportToWkt()\n\tds1.SetProjection(wkt1)\n\n\t#Build destination dataset\n\t# ds2 will be a template empty raster to reproject the data into\n\t# we can directly set its size, res and top left coord as expected\n\t# reproject funtion will match the template (clip and resampling)\n\n\tif out_ul is not None:\n\t\txmin, ymax = out_ul\n\telse:\n\t\txmin, ymax = reprojPt(crs1, crs2, xmin, ymax)\n\n\t#submit resolution and size\n\tif out_res is not None and out_size is not None:\n\t\tresx, resy = out_res, -out_res\n\t\timg_w, img_h = out_size\n\n\t#submit resolution and auto compute the best image size\n\tif out_res is not None and out_size is None:\n\t\tresx, resy = out_res, -out_res\n\t\t#reprojected image size depend on final bbox and expected resolution\n\t\txmin, ymin, xmax, ymax = reprojBbox(crs1, crs2, bbox)\n\t\timg_w = int( (xmax - xmin) / resx )\n\t\timg_h = int( (ymax - ymin) / resy )\n\n\t#submit image size and ...\n\tif out_res is None and out_size is not None:\n\t\timg_w, img_h = out_size\n\t\t#...let's res as source value ? (image will be croped)\n\n\t#Keep original image px size and compute resolution to approximately preserve geosize\n\tif out_res is None and out_size is None:\n\t\t#find the res that match source diagolal size\n\t\txmin, ymin, xmax, ymax = reprojBbox(crs1, crs2, bbox)\n\t\t'''\n\t\tdst_diag = math.sqrt( (xmax - xmin)**2 + (ymax - ymin)**2)\n\t\tpx_diag = math.sqrt(img_w**2 + img_h**2)\n\t\tres = dst_diag / px_diag\n\t\t'''\n\t\tresx = (xmax-xmin) / img_w\n\t\tresy = -(ymax-ymin) / img_h\n\t\tif sqPx:\n\t\t\tresx = max(resx, abs(resy))\n\t\t\tresy = -resx\n\n\tif path is None:\n\t\tds2 = gdal.GetDriverByName('MEM').Create('', img_w, img_h, nbBands, gdal.GetDataTypeByName(dtype))\n\telse:\n\t\tgdal.SetConfigOption('GDAL_TIFF_INTERNAL_MASK', 'YES')\n\t\toptions = [str(k) + '=' + str(v) for k, v in geoTiffOptions.items()]\n\t\tds2 = gdal.GetDriverByName('GTiff').Create(path, img_w, img_h, nbBands, gdal.GetDataTypeByName(dtype), options)\n\t\tif geoTiffOptions.get('COMPRESS', None) == 'JPEG':\n\t\t\tds2.CreateMaskBand(gdal.GMF_PER_DATASET)\n\t\t\tds2.GetRasterBand(1).GetMaskBand().Fill(255) #WARNING, it seems gdal.ReprojectImage does not honor internal mask !\n\tgeoTrans = (xmin, resx, 0, ymax, 0, resy)\n\tds2.SetGeoTransform(geoTrans)\n\tprj2 = SRS(crs2).getOgrSpatialRef()\n\twkt2 = prj2.ExportToWkt()\n\tds2.SetProjection(wkt2)\n\n\t#Perform the projection/resampling\n\t# Resample algo\n\tif resamplAlg == 'NN' : alg = gdal.GRA_NearestNeighbour\n\telif resamplAlg == 'BL' : alg = gdal.GRA_Bilinear\n\telif resamplAlg == 'CB' : alg = gdal.GRA_Cubic\n\telif resamplAlg == 'CBS' : alg = gdal.GRA_CubicSpline\n\telif resamplAlg == 'LCZ' : alg = gdal.GRA_Lanczos\n\t# Memory limit (0 = no limit)\n\tmemLimit = 0\n\t# Error in pixels (0 will use the exact transformer)\n\tthreshold = 0.25\n\t# Warp options (http://www.gdal.org/structGDALWarpOptions.html)\n\topt = ['NUM_THREADS=ALL_CPUS, SAMPLE_GRID=YES']\n\t#option parameters available since gdal 2.1\n\ta, b, c = gdal.__version__.split('.', 2)\n\tif (int(a) == 2 and int(b) >=1) or int(a) > 2:\n\t\tgdal.ReprojectImage(ds1, ds2, wkt1, wkt2, alg, memLimit, threshold, options=opt)\n\telse:\n\t\tgdal.ReprojectImage(ds1, ds2, wkt1, wkt2, alg, memLimit, threshold)\n\n\t#ds1 = None\n\n\treturn ds2\n\n\n\nclass Reproj():\n\n\tdef __init__(self, crs1, crs2):\n\n\t\t#init CRS class\n\t\ttry:\n\t\t\tcrs1, crs2 = SRS(crs1), SRS(crs2)\n\t\texcept Exception as e:\n\t\t\traise ReprojError(str(e))\n\n\t\tif crs1 == crs2:\n\t\t\tself.iproj = 'NO_REPROJ'\n\t\t\treturn\n\n\t\t#Get proj engine from module settings\n\t\tself.iproj = settings.proj_engine\n\t\tif self.iproj not in ['AUTO', 'GDAL', 'PYPROJ', 'BUILTIN', 'EPSGIO']:\n\t\t\traise ReprojError('Wrong engine name')\n\n\t\tif self.iproj == 'AUTO':\n\t\t\t# Init proj4 interface for this instance\n\t\t\tif HAS_GDAL:\n\t\t\t\tself.iproj = 'GDAL'\n\t\t\telif HAS_PYPROJ:\n\t\t\t\t self.iproj = 'PYPROJ'\n\t\t\telif ((crs1.isWM or crs1.isUTM) and crs2.isWGS84) or (crs1.isWGS84 and (crs2.isWM or crs2.isUTM)):\n\t\t\t\tself.iproj = 'BUILTIN'\n\t\t\telse:\n\t\t\t\t#this is the slower solution, not suitable for reproject lot of points\n\t\t\t\tself.iproj = 'EPSGIO'\n\t\telse:\n\t\t\tif (self.iproj == 'GDAL' and not HAS_GDAL) or (self.iproj == 'PYPROJ' and not HAS_PYPROJ):\n\t\t\t\traise ReprojError('Missing reproj engine')\n\t\t\tif self.iproj == 'BUILTIN':\n\t\t\t\tif not ( ((crs1.isWM or crs1.isUTM) and crs2.isWGS84) or (crs1.isWGS84 and (crs2.isWM or crs2.isUTM)) ):\n\t\t\t\t\traise ReprojError('Too limited built in reprojection capabilities')\n\n\t\tif self.iproj == 'GDAL':\n\t\t\tself.crs1 = crs1.getOgrSpatialRef()\n\t\t\tself.crs2 = crs2.getOgrSpatialRef()\n\t\t\tself.osrTransfo = osr.CoordinateTransformation(self.crs1, self.crs2)\n\n\t\telif self.iproj == 'PYPROJ':\n\t\t\tself.crs1 = crs1.getPyProj()\n\t\t\tself.crs2 = crs2.getPyProj()\n\n\t\telif self.iproj == 'EPSGIO':\n\t\t\tself.mapTilerCoords = MapTilerCoordinates()\n\t\t\tif crs1.isEPSG and crs2.isEPSG:\n\t\t\t\tself.crs1, self.crs2 = crs1.code, crs2.code\n\t\t\telse:\n\t\t\t\traise ReprojError('EPSG.io support only EPSG code')\n\n\t\telif self.iproj == 'BUILTIN':\n\t\t\tif ((crs1.isWM or crs1.isUTM) and crs2.isWGS84) or (crs1.isWGS84 and (crs2.isWM or crs2.isUTM)):\n\t\t\t\t#just store codes\n\t\t\t\tself.crs1, self.crs2 = crs1.code, crs2.code\n\t\t\telse:\n\t\t\t\traise ReprojError('Not implemented transformation')\n\t\t\t#init UTM class\n\t\t\tif crs1.isUTM:\n\t\t\t\tself.utm = UTM.init_from_epsg(crs1)\n\t\t\telif crs2.isUTM:\n\t\t\t\tself.utm = UTM.init_from_epsg(crs2)\n\n\n\tdef pts(self, pts):\n\t\tif len(pts) == 0:\n\t\t\treturn []\n\n\t\tif len(pts[0]) != 2:\n\t\t\traise ReprojError('Points must be [ (x,y) ]')\n\n\t\tif self.iproj == 'NO_REPROJ':\n\t\t\treturn pts\n\n\t\tif self.iproj == 'GDAL':\n\t\t\t#Since PROJ 6, the order of coordinates for geographic crs is latitude first, longitude second.\n\t\t\tif hasattr(osr, 'GetPROJVersionMajor'):\n\t\t\t\tprojVersion = osr.GetPROJVersionMajor()\n\t\t\telse:\n\t\t\t\tprojVersion = 4\n\t\t\tif projVersion >= 6 and self.crs1.IsGeographic():\n\t\t\t\tpts = [ (pt[1], pt[0]) for pt in pts]\n\t\t\tif self.crs2.IsGeographic():\n\t\t\t\tys, xs, _zs = zip(*self.osrTransfo.TransformPoints(pts))\n\t\t\telse:\n\t\t\t\txs, ys, _zs = zip(*self.osrTransfo.TransformPoints(pts))\n\t\t\treturn list(zip(xs, ys))\n\n\t\telif self.iproj == 'PYPROJ':\n\t\t\tif self.crs1.crs.is_geographic:\n\t\t\t\tys, xs = zip(*pts)\n\t\t\telse:\n\t\t\t\txs, ys = zip(*pts)\n\t\t\ttransformer = pyproj.Transformer.from_proj(self.crs1, self.crs2)\n\t\t\tif self.crs2.crs.is_geographic:\n\t\t\t\tys, xs = transformer.transform(xs, ys)\n\t\t\telse:\n\t\t\t\txs, ys = transformer.transform(xs, ys)\n\t\t\treturn list(zip(xs, ys))\n\n\t\telif self.iproj == 'EPSGIO':\n\t\t\treturn self.mapTilerCoords.reprojPts(self.crs1, self.crs2, pts)\n\n\t\telif self.iproj == 'BUILTIN':\n\t\t\t#Web Mercator\n\t\t\tif self.crs1 == 4326 and self.crs2 == 3857:\n\t\t\t\treturn [lonLatToWebMerc(*pt) for pt in pts]\n\t\t\telif self.crs1 == 3857 and self.crs2 == 4326:\n\t\t\t\treturn [webMercToLonLat(*pt) for pt in pts]\n\t\t\t#UTM\n\t\t\tif self.crs1 == 4326 and self.crs2 in UTM_EPSG_CODES:\n\t\t\t\treturn [self.utm.lonlat_to_utm(*pt) for pt in pts]\n\t\t\telif self.crs1 in UTM_EPSG_CODES and self.crs2 == 4326:\n\t\t\t\treturn [self.utm.utm_to_lonlat(*pt) for pt in pts]\n\n\tdef pt(self, x, y):\n\t\tif x is None or y is None:\n\t\t\traise ReprojError('Cannot reproj None coordinates')\n\t\treturn self.pts([(x,y)])[0]\n\n\n\tdef bbox(self, bbox):\n\t\t'''io type = BBOX() class'''\n\t\tif not isinstance(bbox, BBOX):\n\t\t\tbbox = BBOX(*bbox) #list must be ordered from bottom left upper right\n\t\tcorners = self.pts(bbox.corners)\n\t\t_xmin = min( pt[0] for pt in corners )\n\t\t_xmax = max( pt[0] for pt in corners )\n\t\t_ymin = min( pt[1] for pt in corners )\n\t\t_ymax = max( pt[1] for pt in corners )\n\t\tif bbox.hasZ:\n\t\t\treturn BBOX(_xmin, _ymin, bbox.zmin, _xmax, _ymax, bbox.zmax)\n\t\telse:\n\t\t\treturn BBOX(_xmin, _ymin, _xmax, _ymax)\n\n\n\ndef reprojPt(crs1, crs2, x, y):\n\t\"\"\"\n\tReproject x1,y1 coords from crs1 to crs2\n\tcrs can be an EPSG code (interger or string) or a proj4 string\n\tWARN : do not use this function in a loop because Reproj() init is slow\n\t\"\"\"\n\trprj = Reproj(crs1, crs2)\n\treturn rprj.pt(x, y)\n\n\ndef reprojPts(crs1, crs2, pts):\n\t\"\"\"\n\tReproject [pts] from crs1 to crs2\n\tcrs can be an EPSG code (integer or srid string) or a proj4 string\n\tpts must be [(x,y)]\n\tWARN : do not use this function in a loop because Reproj() init is slow\n\t\"\"\"\n\trprj = Reproj(crs1, crs2)\n\treturn rprj.pts(pts)\n\ndef reprojBbox(crs1, crs2, bbox):\n\trprj = Reproj(crs1, crs2)\n\treturn rprj.bbox(bbox)\n"
  },
  {
    "path": "core/proj/srs.py",
    "content": "# -*- coding:utf-8 -*-\n\n#  ***** GPL LICENSE BLOCK *****\n#\n#  This program is free software: you can redistribute it and/or modify\n#  it under the terms of the GNU General Public License as published by\n#  the Free Software Foundation, either version 3 of the License, or\n#  (at your option) any later version.\n#\n#  This program is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#  GNU General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public License\n#  along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#  All rights reserved.\n#  ***** GPL LICENSE BLOCK *****\nimport logging\nlog = logging.getLogger(__name__)\n\nfrom .utm import UTM, UTM_EPSG_CODES\nfrom .srv import EPSGIO\n\nfrom ..checkdeps import HAS_GDAL, HAS_PYPROJ\n\nif HAS_GDAL:\n\tfrom osgeo import osr, gdal\n\nif HAS_PYPROJ:\n\timport pyproj\n\nclass SRS():\n\n\t'''\n\tA simple class to handle Spatial Ref System inputs\n\t'''\n\n\t@classmethod\n\tdef validate(cls, crs):\n\t\ttry:\n\t\t\tcls(crs)\n\t\t\treturn True\n\t\texcept Exception as e:\n\t\t\tlog.error('Cannot initialize crs', exc_info=True)\n\t\t\treturn False\n\n\tdef __init__(self, crs):\n\t\t'''\n\t\tValid crs input can be :\n\t\t> an epsg code (integer or string)\n\t\t> a SRID string (AUTH:CODE)\n\t\t> a proj4 string\n\t\t'''\n\n\t\t#force cast to string\n\t\tcrs = str(crs)\n\n\t\t#case 1 : crs is just a code\n\t\tif crs.isdigit():\n\t\t\tself.auth = 'EPSG' #assume authority is EPSG\n\t\t\tself.code = int(crs)\n\t\t\tself.proj4 = '+init=epsg:'+str(self.code)\n\t\t\t#note : 'epsg' must be lower case to be compatible with gdal osr\n\n\t\t#case 2 crs is in the form AUTH:CODE\n\t\telif ':' in crs:\n\t\t\tself.auth, self.code = crs.split(':')\n\t\t\tif self.code.isdigit(): #what about non integer code ??? (IGNF:LAMB93)\n\t\t\t\tself.code = int(self.code)\n\t\t\t\tif self.auth.startswith('+init='):\n\t\t\t\t\t_, self.auth = self.auth.split('=')\n\t\t\t\tself.auth = self.auth.upper()\n\t\t\t\tself.proj4 = '+init=' + self.auth.lower() + ':' + str(self.code)\n\t\t\telse:\n\t\t\t\traise ValueError('Invalid CRS : '+crs)\n\n\t\t#case 3 : crs is proj4 string\n\t\telif all([param.startswith('+') for param in crs.split(' ') if param]):\n\t\t\tself.auth = None\n\t\t\tself.code = None\n\t\t\tself.proj4 = crs\n\n\t\telse:\n\t\t\traise ValueError('Invalid CRS : '+crs)\n\n\t@classmethod\n\tdef fromGDAL(cls, ds):\n\t\tif not HAS_GDAL:\n\t\t\traise ImportError('GDAL not available')\n\t\twkt = ds.GetProjection()\n\t\tif not wkt: #empty string\n\t\t\traise ImportError('This raster has no projection')\n\t\tcrs = osr.SpatialReference()\n\t\tcrs.ImportFromWkt(wkt)\n\t\treturn cls(crs.ExportToProj4())\n\n\t@property\n\tdef SRID(self):\n\t\tif self.isSRID:\n\t\t\treturn self.auth + ':' + str(self.code)\n\t\telse:\n\t\t\treturn None\n\n\t@property\n\tdef hasCode(self):\n\t\treturn self.code is not None\n\n\t@property\n\tdef hasAuth(self):\n\t\treturn self.auth is not None\n\n\t@property\n\tdef isSRID(self):\n\t\treturn self.hasAuth and self.hasCode\n\n\t@property\n\tdef isEPSG(self):\n\t\treturn self.auth == 'EPSG' and self.code is not None\n\n\t@property\n\tdef isWM(self):\n\t\treturn self.auth == 'EPSG' and self.code == 3857\n\n\t@property\n\tdef isWGS84(self):\n\t\treturn self.auth == 'EPSG' and self.code == 4326\n\n\t@property\n\tdef isUTM(self):\n\t\treturn self.auth == 'EPSG' and self.code in UTM_EPSG_CODES\n\n\tdef __str__(self):\n\t\t'''Return the best string representation for this crs'''\n\t\tif self.isSRID:\n\t\t\treturn self.SRID\n\t\telse:\n\t\t\treturn self.proj4\n\n\tdef __eq__(self, srs2):\n\t\treturn self.__str__() == srs2.__str__()\n\n\tdef getOgrSpatialRef(self):\n\t\t'''Build gdal osr spatial ref object'''\n\t\tif not HAS_GDAL:\n\t\t\traise ImportError('GDAL not available')\n\n\t\tprj = osr.SpatialReference()\n\n\t\tif self.isEPSG:\n\t\t\tr = prj.ImportFromEPSG(self.code)\n\t\telse:\n\t\t\tr = prj.ImportFromProj4(self.proj4)\n\n\t\t#ImportFromEPSG and ImportFromProj4 do not raise any exception\n\t\t#but return zero if the projection is valid\n\t\tif r > 0:\n\t\t\traise ValueError('Cannot initialize osr : ' + self.proj4)\n\n\t\treturn prj\n\n\n\tdef getPyProj(self):\n\t\t'''Build pyproj object'''\n\t\tif not HAS_PYPROJ:\n\t\t\traise ImportError('PYPROJ not available')\n\t\tif self.isSRID:\n\t\t\treturn pyproj.Proj(self.SRID)\n\t\telse:\n\t\t\ttry:\n\t\t\t\treturn pyproj.Proj(self.proj4)\n\t\t\texcept Exception as e:\n\t\t\t\traise ValueError('Cannot initialize pyproj object for projection {}. Error : {}'.format(self.proj4, e))\n\n\n\tdef loadProj4(self):\n\t\t'''Return a Python dict of proj4 parameters'''\n\t\tdc = {}\n\t\tif self.proj4 is None:\n\t\t\treturn dc\n\t\tfor param in self.proj4.split(' '):\n\t\t\tif param.count('=') == 1:\n\t\t\t\tk, v = param.split('=')\n\t\t\t\ttry:\n\t\t\t\t\tv = float(v)\n\t\t\t\texcept ValueError:\n\t\t\t\t\tpass\n\t\t\t\tdc[k] = v\n\t\t\telse:\n\t\t\t\tpass\n\t\treturn dc\n\n\t@property\n\tdef isGeo(self):\n\t\tif self.code == 4326:\n\t\t\treturn True\n\t\telif HAS_GDAL:\n\t\t\tprj = self.getOgrSpatialRef()\n\t\t\tisGeo = prj.IsGeographic()\n\t\t\treturn isGeo == 1\n\t\telif HAS_PYPROJ:\n\t\t\tprj = self.getPyProj()\n\t\t\treturn prj.crs.is_geographic\n\t\telse:\n\t\t\treturn None\n\n\tdef getWKT(self):\n\t\tif HAS_GDAL:\n\t\t\tprj = self.getOgrSpatialRef()\n\t\t\treturn prj.ExportToWkt()\n\t\telif self.isEPSG:\n\t\t\treturn EPSGIO.getEsriWkt(self.code)\n\t\telse:\n\t\t\traise NotImplementedError\n"
  },
  {
    "path": "core/proj/srv.py",
    "content": "# -*- coding:utf-8 -*-\n\n#  ***** GPL LICENSE BLOCK *****\n#\n#  This program is free software: you can redistribute it and/or modify\n#  it under the terms of the GNU General Public License as published by\n#  the Free Software Foundation, either version 3 of the License, or\n#  (at your option) any later version.\n#\n#  This program is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#  GNU General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public License\n#  along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#  All rights reserved.\n#  ***** GPL LICENSE BLOCK *****\nimport logging\nlog = logging.getLogger(__name__)\n\n\nfrom urllib.request import Request, urlopen\nfrom urllib.error import URLError, HTTPError\nimport json\n\nfrom .. import settings\nfrom ..errors import ApiKeyError\n\nUSER_AGENT = settings.user_agent\n\nDEFAULT_TIMEOUT = 2\nREPROJ_TIMEOUT = 60\n\n######################################\n# MapTiler Coordinates API (formerly EPSG.io)\n# Migration guide: https://docs.maptiler.com/cloud/api/coordinates/\n\nclass MapTilerCoordinates():\n\n\tdef __init__(self, apiKey=None):\n\t\tif apiKey is None:\n\t\t\tif settings.maptiler_api_key:\n\t\t\t\tself.apiKey = settings.maptiler_api_key\n\t\t\telse:\n\t\t\t\traise ApiKeyError\n\t\t\t\tlog.error('Missing MapTilerCoordinates API key')\n\t\telse:\n\t\t\tself.apiKey = apiKey\n\n\t\t\"\"\"Test connection to MapTiler API server\"\"\"\n\t\turl = \"https://api.maptiler.com\"\n\t\ttry:\n\t\t\trq = Request(url, headers={'User-Agent': USER_AGENT})\n\t\t\turlopen(rq, timeout=DEFAULT_TIMEOUT)\n\t\texcept URLError as e:\n\t\t\tlog.error('Cannot ping {} web service, {}'.format(url, e.reason))\n\t\t\traise e\n\t\texcept HTTPError as e:\n\t\t\tlog.error('Cannot ping {} web service, http error {}'.format(url, e.code))\n\t\t\traise e\n\t\texcept:\n\t\t\traise\n\n\tdef reprojPt(self, epsg1, epsg2, x1, y1):\n\t\t\"\"\"Reproject a single point using MapTiler Coordinates API\"\"\"\n\n\t\turl = f\"https://api.maptiler.com/coordinates/transform/{x1},{y1}.json?s_srs={epsg1}&t_srs={epsg2}&key={self.apiKey}\"\n\n\t\tlog.debug(url)\n\n\t\ttry:\n\t\t\trq = Request(url, headers={'User-Agent': USER_AGENT})\n\t\t\tresponse = urlopen(rq, timeout=REPROJ_TIMEOUT).read().decode('utf8')\n\t\texcept (URLError, HTTPError) as err:\n\t\t\tlog.error('Http request fails url:{}, code:{}, error:{}'.format(url, err.code, err.reason))\n\t\t\traise\n\n\t\tobj = json.loads(response)['results'][0]\n\n\t\treturn (float(obj['x']), float(obj['y']))\n\n\n\tdef reprojPts(self, epsg1, epsg2, points):\n\t\t\"\"\"Reproject multiple points using MapTiler Coordinates API\"\"\"\n\n\t\tif len(points) == 1:\n\t\t\tx, y = points[0]\n\t\t\treturn [self.reprojPt(epsg1, epsg2, x, y)]\n\n\t\turlTemplate = \"https://api.maptiler.com/coordinates/transform/{{POINTS}}.json?s_srs={CRS1}&t_srs={CRS2}&key={KEY}\".format(\n\t\t\tCRS1=epsg1,\n\t\t\tCRS2=epsg2,\n\t\t\tKEY=self.apiKey\n\t\t)\n\n\t\tprecision = 4\n\t\tdata = [','.join([str(round(v, precision)) for v in p]) for p in points]\n\t\t\n\t\t# MapTiler API supports up to 50 points per request in batch mode\n\t\tbatch_size = 50\n\t\tbatches = [data[i:i + batch_size] for i in range(0, len(data), batch_size)]\n\t\t\n\t\tresult = []\n\t\tfor batch in batches:\n\t\t\tpart = ';'.join(batch)\n\t\t\turl = urlTemplate.replace(\"{POINTS}\", part)\n\t\t\tlog.debug(url)\n\n\t\t\ttry:\n\t\t\t\trq = Request(url, headers={'User-Agent': USER_AGENT})\n\t\t\t\tresponse = urlopen(rq, timeout=REPROJ_TIMEOUT).read().decode('utf8')\n\t\t\texcept (URLError, HTTPError) as err:\n\t\t\t\tlog.error('Http request fails url:{}, code:{}, error:{}'.format(url, err.code, err.reason))\n\t\t\t\traise\n\n\t\t\tobj = json.loads(response)['results']\n\t\t\t\n\t\t\tresult.extend([(float(p['x']), float(p['y'])) for p in obj])\n\n\t\treturn result\n\n\tdef search(self, query):\n\t\t\"\"\"Search coordinate systems using MapTiler Coordinates API\"\"\"\n\n\t\tquery = str(query).replace(' ', '+')\n\t\t# New endpoint with API key\n\t\turl = f\"https://api.maptiler.com/coordinates/search/{query}.json?exports=true&key={self.apiKey}\"\n\t\t\n\t\tlog.debug('Search crs : {}'.format(url))\n\t\trq = Request(url, headers={'User-Agent': USER_AGENT})\n\t\tresponse = urlopen(rq, timeout=DEFAULT_TIMEOUT).read().decode('utf8')\n\t\tobj = json.loads(response)\n\t\t\n\t\tlog.debug('Search results : {}'.format([(r['id']['code'], r['name']) for r in obj['results']]))\n\t\treturn obj['results']\n\n\tdef getEsriWkt(self, epsg):\n\t\t\"\"\"Get ESRI WKT for a specific EPSG code using MapTiler Coordinates API\"\"\"\n\t\tobj = self.search(epsg)\n\t\ttry:\n\t\t\treturn obj[0]['exports']['wkt']\n\t\texcept:\n\t\t\tlog.error('Could not find ESRI WKT for EPSG:{}'.format(epsg))\n\t\t\treturn None\n\n\n# For backward compatibility, you can keep the EPSGIO class as an alias to MapTilerCoordinates\nclass EPSGIO(MapTilerCoordinates):\n\tpass\n\n\n######################################\n# World Coordinate Converter\n# https://github.com/ClemRz/TWCC\n\nclass TWCC():\n\n\t@staticmethod\n\tdef reprojPt(epsg1, epsg2, x1, y1):\n\n\t\turl = f\"http://twcc.fr/en/ws/?fmt=json&x={x1}&y={y1}&in=EPSG:{epsg1}&out=EPSG:{epsg2}\"\n\n\t\trq = Request(url, headers={'User-Agent': USER_AGENT})\n\t\tresponse = urlopen(rq, timeout=REPROJ_TIMEOUT).read().decode('utf8')\n\t\tobj = json.loads(response)\n\n\t\treturn (float(obj['point']['x']), float(obj['point']['y']))\n\n\n######################################\n# http://spatialreference.org/ref/epsg/2154/esriwkt/\n\n# class SpatialRefOrg():\n\n\n######################################\n# http://prj2epsg.org/search\n"
  },
  {
    "path": "core/proj/utm.py",
    "content": "\n\n#Original code from https://github.com/Turbo87/utm\n#>simplified version that only handle utm zones (and not latitude bands from MGRS grid)\n#>reverse coord order : latlon --> lonlat\n#>add support for UTM EPSG codes\n\n# more infos : http://geokov.com/education/utm.aspx\n# formulas : https://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system\n\nimport math\n\n\nK0 = 0.9996\n\nE = 0.00669438\nE2 = E * E\nE3 = E2 * E\nE_P2 = E / (1.0 - E)\n\nSQRT_E = math.sqrt(1 - E)\n_E = (1 - SQRT_E) / (1 + SQRT_E)\n_E2 = _E * _E\n_E3 = _E2 * _E\n_E4 = _E3 * _E\n_E5 = _E4 * _E\n\nM1 = (1 - E / 4 - 3 * E2 / 64 - 5 * E3 / 256)\nM2 = (3 * E / 8 + 3 * E2 / 32 + 45 * E3 / 1024)\nM3 = (15 * E2 / 256 + 45 * E3 / 1024)\nM4 = (35 * E3 / 3072)\n\nP2 = (3. / 2 * _E - 27. / 32 * _E3 + 269. / 512 * _E5)\nP3 = (21. / 16 * _E2 - 55. / 32 * _E4)\nP4 = (151. / 96 * _E3 - 417. / 128 * _E5)\nP5 = (1097. / 512 * _E4)\n\nR = 6378137\n\n\nclass OutOfRangeError(ValueError):\n\tpass\n\n\ndef longitude_to_zone_number(longitude):\n\treturn int((longitude + 180) / 6) + 1\n\ndef latitude_to_northern(latitude):\n\treturn latitude >= 0\n\ndef lonlat_to_zone_northern(lon, lat):\n\tzone = longitude_to_zone_number(lon)\n\tnorth = latitude_to_northern(lat)\n\treturn zone, north\n\ndef zone_number_to_central_longitude(zone_number):\n\treturn (zone_number - 1) * 6 - 180 + 3\n\n\n# Each UTM zone on WGS84 datum has a dedicated EPSG code : 326xx for north hemisphere and 327xx for south\n# where xx is the zone number from 1 to 60\n\n#UTM_EPSG_CODES = ['326' + str(i).zfill(2) for i in range(1,61)] + ['327' + str(i).zfill(2) for i in range(1,61)]\nUTM_EPSG_CODES = [32600 + i for i in range(1,61)] + [32700 + i for i in range(1,61)]\n\ndef _code_from_epsg(epsg):\n\t'''Return & validate EPSG code str from user input'''\n\tepsg = str(epsg)\n\tif epsg.isdigit():\n\t\tcode = epsg\n\telif ':' in epsg:\n\t\tauth, code = epsg.split(':')\n\telse:\n\t\traise ValueError('Invalid UTM EPSG code')\n\tif code in map(str, UTM_EPSG_CODES):\n\t\treturn code\n\telse:\n\t\traise ValueError('Invalid UTM EPSG code')\n\ndef epsg_to_zone_northern(epsg):\n\tcode = _code_from_epsg(epsg)\n\tzone = int(code[-2:])\n\tif code[2] == '6':\n\t\tnorthern = True\n\telse:\n\t\tnorthern = False\n\treturn zone, northern\n\ndef lonlat_to_epsg(longitude, latitude):\n\tzone = longitude_to_zone_number(longitude)\n\tif latitude_to_northern(latitude):\n\t\treturn 'EPSG:326' + str(zone).zfill(2)\n\telse:\n\t\treturn 'EPSG:327' + str(zone).zfill(2)\n\ndef zone_northern_to_epsg(zone, northern):\n\tif northern:\n\t\treturn 'EPSG:326' + str(zone).zfill(2)\n\telse:\n\t\treturn 'EPSG:327' + str(zone).zfill(2)\n\n\n######\n\nclass UTM():\n\n\tdef __init__(self, zone, north):\n\t\t'''\n\t\tzone : UTM zone number \n\t\tnorth : True if north hemesphere, False if south\n\t\t'''\n\t\tif not 1 <= zone <= 60:\n\t\t\traise OutOfRangeError('zone number out of range (must be between 1 and 60)')\n\t\tself.zone_number = zone\n\t\tself.northern = north\n\n\t@classmethod\n\tdef init_from_epsg(cls, epsg):\n\t\tzone, north = epsg_to_zone_northern(epsg)\n\t\treturn cls(zone, north)\n\n\t@classmethod\n\tdef init_from_lonlat(cls, lon, lat):\n\t\tzone, north = lonlat_to_zone_northern(lon, lat)\n\t\treturn cls(zone, north)\n\n\n\tdef utm_to_lonlat(self, easting, northing):\n\n\t\tif not 100000 <= easting < 1000000:\n\t\t\traise OutOfRangeError('easting out of range (must be between 100.000 m and 999.999 m)')\n\t\tif not 0 <= northing <= 10000000:\n\t\t\traise OutOfRangeError('northing out of range (must be between 0 m and 10.000.000 m)')\n\n\t\tx = easting - 500000\n\t\ty = northing\n\n\t\tif not self.northern:\n\t\t\ty -= 10000000\n\n\t\tm = y / K0\n\t\tmu = m / (R * M1)\n\n\t\tp_rad = (mu +\n\t\t\t\t P2 * math.sin(2 * mu) +\n\t\t\t\t P3 * math.sin(4 * mu) +\n\t\t\t\t P4 * math.sin(6 * mu) +\n\t\t\t\t P5 * math.sin(8 * mu))\n\n\t\tp_sin = math.sin(p_rad)\n\t\tp_sin2 = p_sin * p_sin\n\n\t\tp_cos = math.cos(p_rad)\n\n\t\tp_tan = p_sin / p_cos\n\t\tp_tan2 = p_tan * p_tan\n\t\tp_tan4 = p_tan2 * p_tan2\n\n\t\tep_sin = 1 - E * p_sin2\n\t\tep_sin_sqrt = math.sqrt(1 - E * p_sin2)\n\n\t\tn = R / ep_sin_sqrt\n\t\tr = (1 - E) / ep_sin\n\n\t\tc = _E * p_cos**2\n\t\tc2 = c * c\n\n\t\td = x / (n * K0)\n\t\td2 = d * d\n\t\td3 = d2 * d\n\t\td4 = d3 * d\n\t\td5 = d4 * d\n\t\td6 = d5 * d\n\n\t\tlatitude = (p_rad - (p_tan / r) *\n\t\t\t\t\t(d2 / 2 -\n\t\t\t\t\t d4 / 24 * (5 + 3 * p_tan2 + 10 * c - 4 * c2 - 9 * E_P2)) +\n\t\t\t\t\t d6 / 720 * (61 + 90 * p_tan2 + 298 * c + 45 * p_tan4 - 252 * E_P2 - 3 * c2))\n\n\t\tlongitude = (d -\n\t\t\t\t\t d3 / 6 * (1 + 2 * p_tan2 + c) +\n\t\t\t\t\t d5 / 120 * (5 - 2 * c + 28 * p_tan2 - 3 * c2 + 8 * E_P2 + 24 * p_tan4)) / p_cos\n\n\t\treturn (math.degrees(longitude) + zone_number_to_central_longitude(self.zone_number),\n\t\t\t\tmath.degrees(latitude))\n\n\n\tdef lonlat_to_utm(self, longitude, latitude):\n\t\tif not -80.0 <= latitude <= 84.0:\n\t\t\traise OutOfRangeError('latitude out of range (must be between 80 deg S and 84 deg N)')\n\t\tif not -180.0 <= longitude <= 180.0:\n\t\t\traise OutOfRangeError('longitude out of range (must be between 180 deg W and 180 deg E)')\n\n\t\tlat_rad = math.radians(latitude)\n\t\tlat_sin = math.sin(lat_rad)\n\t\tlat_cos = math.cos(lat_rad)\n\n\t\tlat_tan = lat_sin / lat_cos\n\t\tlat_tan2 = lat_tan * lat_tan\n\t\tlat_tan4 = lat_tan2 * lat_tan2\n\n\t\tlon_rad = math.radians(longitude)\n\t\tcentral_lon = zone_number_to_central_longitude(self.zone_number)\n\t\tcentral_lon_rad = math.radians(central_lon)\n\n\t\tn = R / math.sqrt(1 - E * lat_sin**2)\n\t\tc = E_P2 * lat_cos**2\n\n\t\ta = lat_cos * (lon_rad - central_lon_rad)\n\t\ta2 = a * a\n\t\ta3 = a2 * a\n\t\ta4 = a3 * a\n\t\ta5 = a4 * a\n\t\ta6 = a5 * a\n\n\t\tm = R * (M1 * lat_rad -\n\t\t\t\t M2 * math.sin(2 * lat_rad) +\n\t\t\t\t M3 * math.sin(4 * lat_rad) -\n\t\t\t\t M4 * math.sin(6 * lat_rad))\n\n\t\teasting = K0 * n * (a +\n\t\t\t\t\t\t\ta3 / 6 * (1 - lat_tan2 + c) +\n\t\t\t\t\t\t\ta5 / 120 * (5 - 18 * lat_tan2 + lat_tan4 + 72 * c - 58 * E_P2)) + 500000\n\n\t\tnorthing = K0 * (m + n * lat_tan * (a2 / 2 +\n\t\t\t\t\t\t\t\t\t\t\ta4 / 24 * (5 - lat_tan2 + 9 * c + 4 * c**2) +\n\t\t\t\t\t\t\t\t\t\t\ta6 / 720 * (61 - 58 * lat_tan2 + lat_tan4 + 600 * c - 330 * E_P2)))\n\n\t\tif not self.northern:\n\t\t\tnorthing += 10000000\n\n\t\treturn easting, northing\n\n\n\n\n"
  },
  {
    "path": "core/settings.json",
    "content": "{\n\t\"proj_engine\": \"AUTO\",\n\t\"img_engine\": \"AUTO\",\n\t\"user_agent\": \"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0\"\n}\n"
  },
  {
    "path": "core/settings.py",
    "content": "# -*- coding:utf-8 -*-\nimport os\nimport json\n\nfrom .checkdeps import HAS_GDAL, HAS_PYPROJ, HAS_IMGIO, HAS_PIL\n\ndef getAvailableProjEngines():\n\tengines = ['AUTO', 'BUILTIN']\n\t#if EPSGIO.ping():\n\tengines.append('EPSGIO')\n\tif HAS_GDAL:\n\t\tengines.append('GDAL')\n\tif HAS_PYPROJ:\n\t\tengines.append('PYPROJ')\n\treturn engines\n\ndef getAvailableImgEngines():\n\tengines = ['AUTO']\n\tif HAS_GDAL:\n\t\tengines.append('GDAL')\n\tif HAS_IMGIO:\n\t\tengines.append('IMGIO')\n\tif HAS_PIL:\n\t\tengines.append('PIL')\n\treturn engines\n\n\nclass Settings():\n\n\tdef __init__(self, **kwargs):\n\t\tself._proj_engine = kwargs['proj_engine']\n\t\tself._img_engine = kwargs['img_engine']\n\t\tself.user_agent = kwargs['user_agent']\n\t\tif 'maptiler_api_key' in kwargs:\n\t\t\tself.maptiler_api_key = kwargs['maptiler_api_key']\n\t\telse:\n\t\t\tself.maptiler_api_key = None\n\n\t@property\n\tdef proj_engine(self):\n\t\treturn self._proj_engine\n\n\t@proj_engine.setter\n\tdef proj_engine(self, engine):\n\t\tif engine not in getAvailableProjEngines():\n\t\t\traise IOError\n\t\telse:\n\t\t\tself._proj_engine = engine\n\n\t@property\n\tdef img_engine(self):\n\t\treturn self._img_engine\n\n\t@img_engine.setter\n\tdef img_engine(self, engine):\n\t\tif engine not in getAvailableImgEngines():\n\t\t\traise IOError\n\t\telse:\n\t\t\tself._img_engine = engine\n\n\ncfgFile = os.path.join(os.path.dirname(__file__), \"settings.json\")\n\nwith open(cfgFile, 'r') as cfg:\n\t\tprefs = json.load(cfg)\n\nsettings = Settings(**prefs)\n"
  },
  {
    "path": "core/utils/__init__.py",
    "content": "from .xy import XY\nfrom .bbox import BBOX\nfrom .gradient import Color, Stop, Gradient\nfrom .timing import perf_clock\n"
  },
  {
    "path": "core/utils/bbox.py",
    "content": "# -*- coding:utf-8 -*-\n\n# This file is part of BlenderGIS\n\n#  ***** GPL LICENSE BLOCK *****\n#\n#  This program is free software: you can redistribute it and/or modify\n#  it under the terms of the GNU General Public License as published by\n#  the Free Software Foundation, either version 3 of the License, or\n#  (at your option) any later version.\n#\n#  This program is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#  GNU General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public License\n#  along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#  All rights reserved.\n#  ***** GPL LICENSE BLOCK *****\n\nfrom . import XY\nimport logging\nlog = logging.getLogger(__name__)\n\nclass BBOX(dict):\n\t'''A class to represent a bounding box'''\n\n\tdef __init__(self, *args, **kwargs):\n\t\t'''\n\t\tThree ways for init a BBOX class:\n\t\t- from a list of values ordered from bottom left to upper right\n\t\t\t>> BBOX(xmin, ymin, xmax, ymax) or BBOX(xmin, ymin, zmin, xmax, ymax, zmax)\n\t\t- from a tuple contained a list of values ordered from bottom left to upper right\n\t\t\t>> BBOX( (xmin, ymin, xmax, ymax) ) or BBOX( (xmin, ymin, zmin, xmax, ymax, zmax) )\n\t\t- from keyword arguments with no particular order\n\t\t\t>> BBOX(xmin=, ymin=, xmax=, ymax=) or BBOX(xmin=, ymin=, zmin=, xmax=, ymax=, zmax=)\n\t\t'''\n\t\tif args:\n\t\t\tif len(args) == 1: #maybee we pass directly a tuple\n\t\t\t\targs = args[0]\n\t\t\tif len(args) == 4:\n\t\t\t\tself.xmin, self.ymin, self.xmax, self.ymax = args\n\t\t\telif len(args) == 6:\n\t\t\t\tself.xmin, self.ymin, self.zmin, self.xmax, self.ymax, self.zmax = args\n\t\t\telse:\n\t\t\t\traise ValueError('BBOX() initialization expects 4 or 6 arguments, got %g' % len(args))\n\t\telif kwargs:\n\t\t\tif not all( [kw in kwargs for kw in ['xmin', 'ymin', 'xmax', 'ymax']] ):\n\t\t\t\traise ValueError('invalid keyword arguments')\n\t\t\tself.xmin, self.xmax = kwargs['xmin'], kwargs['xmax']\n\t\t\tself.ymin, self.ymax = kwargs['ymin'], kwargs['ymax']\n\t\t\tif 'zmin' in kwargs and 'zmax' in kwargs:\n\t\t\t\tself.zmin, self.zmax = kwargs['zmin'], kwargs['zmax']\n\n\tdef __str__(self):\n\t\tif self.hasZ:\n\t\t\treturn 'xmin:%g, ymin:%g, zmin:%g, xmax:%g, ymax:%g, zmax:%g' % tuple(self)\n\t\telse:\n\t\t\treturn 'xmin:%g, ymin:%g, xmax:%g, ymax:%g' % tuple(self)\n\n\tdef __getitem__(self, attr):\n\t\t'''access attributes like a dictionnary'''\n\t\treturn getattr(self, attr)\n\n\tdef __setitem__(self, key, value):\n\t\t'''set attributes like a dictionnary'''\n\t\tsetattr(self, key, value)\n\n\tdef __iter__(self):\n\t\t'''iterate overs values in bottom left to upper right order\n\t\tallows support of unpacking and conversion to tuple or list'''\n\t\tif self.hasZ:\n\t\t\treturn iter([self.xmin, self.ymin, self.zmin, self.xmax, self.ymax, self.ymax])\n\t\telse:\n\t\t\treturn iter([self.xmin, self.ymin, self.xmax, self.ymax])\n\n\tdef keys(self):\n\t\t'''override dict keys() method'''\n\t\treturn self.__dict__.keys()\n\n\tdef items(self):\n\t\t'''override dict keys() method'''\n\t\treturn self.__dict__.items()\n\n\tdef values(self):\n\t\t'''override dict keys() method'''\n\t\treturn self.__dict__.values()\n\n\t@classmethod\n\tdef fromXYZ(cls, lst):\n\t\t'''Create a BBOX from a flat list of values ordered following XYZ axis\n\t\t--> (xmin, xmax, ymin, ymax) or (xmin, xmax, ymin, ymax, zmin, zmax)'''\n\t\tif len(lst) == 4:\n\t\t\txmin, xmax, ymin, ymax = lst\n\t\t\treturn cls(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax)\n\t\telif len(lst) == 6:\n\t\t\txmin, xmax, ymin, ymax, zmin, zmax = lst\n\t\t\treturn cls(xmin=xmin, ymin=ymin, zmin=zmin, xmax=xmax, ymax=ymax, zmax=zmax)\n\n\tdef toXYZ(self):\n\t\t'''Export to simple tuple of values ordered following XYZ axis'''\n\t\tif self.hasZ:\n\t\t\treturn (self.xmin, self.xmax, self.ymin, self.ymax, self.zmin, self.zmax)\n\t\telse:\n\t\t\treturn (self.xmin, self.xmax, self.ymin, self.ymax)\n\n\t@classmethod\n\tdef fromLatlon(cls, lst):\n\t\t'''Create a 2D BBOX from a list of values ordered as latlon format (latmin, lonmin, latmax, lonmax) <--> (min, xmin, ymax, xmax)'''\n\t\tymin, xmin, ymax, xmax = lst\n\t\treturn cls(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax)\n\n\tdef toLatlon(self):\n\t\t'''Export to simple tuple of values ordered as latlon format in 2D'''\n\t\treturn (self.ymin, self.xmin, self.ymax, self.xmax)\n\n\t@property\n\tdef hasZ(self):\n\t\t'''Check if this bbox is in 3D'''\n\t\tif hasattr(self, 'zmin') and hasattr(self, 'zmax'):\n\t\t\treturn True\n\t\telse:\n\t\t\treturn False\n\n\tdef to2D(self):\n\t\t'''Cast 3d bbox to 2d >> discard zmin and zmax values'''\n\t\treturn BBOX(self.xmin, self.ymin, self.xmax, self.ymax)\n\n\tdef toGeo(self, geoscn):\n\t\t'''Convert the BBOX into Spatial Ref System space defined in Scene'''\n\t\tif geoscn.isBroken or not geoscn.isGeoref:\n\t\t\tlog.warning('Cannot convert bbox, invalid georef')\n\t\t\treturn None\n\t\txmax = geoscn.crsx + (self.xmax * geoscn.scale)\n\t\tymax = geoscn.crsy + (self.ymax * geoscn.scale)\n\t\txmin = geoscn.crsx + (self.xmin * geoscn.scale)\n\t\tymin = geoscn.crsy + (self.ymin * geoscn.scale)\n\t\tif self.hasZ:\n\t\t\treturn BBOX(xmin, ymin, self.zmin, xmax, ymax, self.zmax)\n\t\telse:\n\t\t\treturn BBOX(xmin, ymin, xmax, ymax)\n\n\tdef __eq__(self, bb):\n\t\t'''Test if 2 bbox are equals'''\n\t\tif self.xmin == bb.xmin and self.xmax == bb.xmax and self.ymin == bb.ymin and self.ymax == bb.ymax:\n\t\t\tif self.hasZ and bb.hasZ:\n\t\t\t\t\tif self.zmin == bb.zmin and self.zmax == bb.zmax:\n\t\t\t\t\t\treturn True\n\t\t\telse:\n\t\t\t\treturn True\n\n\tdef overlap(self, bb):\n\t\t'''Test if 2 bbox objects have intersection areas (in 2D only)'''\n\t\tdef test_overlap(a_min, a_max, b_min, b_max):\n\t\t\treturn not ((a_min > b_max) or (b_min > a_max))\n\t\treturn test_overlap(self.xmin, self.xmax, bb.xmin, bb.xmax) and test_overlap(self.ymin, self.ymax, bb.ymin, bb.ymax)\n\n\tdef isWithin(self, bb):\n\t\t'''Test if this bbox is within another bbox'''\n\t\tif bb.xmin <= self.xmin and bb.xmax >= self.xmax and bb.ymin <= self.ymin and bb.ymax >= self.ymax:\n\t\t\treturn True\n\t\telse:\n\t\t\treturn False\n\n\tdef contains(self, bb):\n\t\t'''Test if this bbox contains another bbox'''\n\t\tif bb.xmin > self.xmin and bb.xmax < self.xmax and bb.ymin > self.ymin and bb.ymax < self.ymax:\n\t\t\treturn True\n\t\telse:\n\t\t\treturn False\n\n\tdef __add__(self, bb):\n\t\t'''Use '+' operator to perform the union of 2 bbox'''\n\t\txmax = max(self.xmax, bb.xmax)\n\t\txmin = min(self.xmin, bb.xmin)\n\t\tymax = max(self.ymax, bb.ymax)\n\t\tymin = min(self.ymin, bb.ymin)\n\t\tif self.hasZ and bb.hasZ:\n\t\t\tzmax = max(self.zmax, bb.zmax)\n\t\t\tzmin = min(self.zmin, bb.zmin)\n\t\t\treturn BBOX(xmin, ymin, zmin, xmax, ymax, zmax)\n\t\telse:\n\t\t\treturn BBOX(xmin, ymin, xmax, ymax)\n\n\tdef shift(self, dx, dy):\n\t\t'''translate the bbox in 2D'''\n\t\tself.xmin += dx\n\t\tself.xmax += dx\n\t\tself.ymin += dy\n\t\tself.ymax += dy\n\n\t@property\n\tdef center(self):\n\t\tx = (self.xmin + self.xmax) / 2\n\t\ty = (self.ymin + self.ymax) / 2\n\t\tif self.hasZ:\n\t\t\tz = (self.zmin + self.zmax) / 2\n\t\t\treturn XY(x,y,z)\n\t\telse:\n\t\t\treturn XY(x,y)\n\n\t@property\n\tdef dimensions(self):\n\t\tdx = self.xmax - self.xmin\n\t\tdy = self.ymax - self.ymin\n\t\tif self.hasZ:\n\t\t\tdz = self.zmax - self.zmin\n\t\t\treturn XY(dx,dy,dz)\n\t\telse:\n\t\t\treturn XY(dx,dy)\n\n\t################\n\t## 2D properties\n\n\t@property\n\tdef corners(self):\n\t\t'''Get the list of corners coords, starting from upperleft and ordered clockwise'''\n\t\treturn [ self.ul, self.ur, self.br, self.bl ]\n\n\t@property\n\tdef ul(self):\n\t\t'''upper left corner'''\n\t\treturn XY(self.xmin, self.ymax)\n\t@property\n\tdef ur(self):\n\t\t'''upper right corner'''\n\t\treturn XY(self.xmax, self.ymax)\n\t@property\n\tdef bl(self):\n\t\t'''bottom left corner'''\n\t\treturn XY(self.xmin, self.ymin)\n\t@property\n\tdef br(self):\n\t\t'''bottom right corner'''\n\t\treturn XY(self.xmax, self.ymin)\n"
  },
  {
    "path": "core/utils/gradient.py",
    "content": "# -*- coding:utf-8 -*-\nimport logging\nlog = logging.getLogger(__name__)\nimport os\nimport colorsys\nfrom xml.dom.minidom import parse, parseString\nfrom xml.etree import ElementTree as etree\nfrom ..maths.interpo import scale, linearInterpo\nfrom ..maths import akima\n\n\nclass Color(object):\n\n\tdef __init__(self, values=None, space='RGBA'):\n\t\t#color data is stored as rgba vector (values range from 0 to 1)\n\t\tself.data = None\n\t\t#\n\t\tif type(values) == dict:\n\t\t\t#find correct space\n\t\t\tif all(key in 'RGBA' for key in values.keys()):\n\t\t\t\tspace = 'RGBA'\n\t\t\telif all(key in 'rgba' for key in values.keys()):\n\t\t\t\tspace = 'rgba'\n\t\t\telif all(key in 'HSVA' for key in values.keys()):\n\t\t\t\tspace = 'HSVA'\n\t\t\telif all(key in 'hsva' for key in values.keys()):\n\t\t\t\tspace = 'hsva'\n\t\t\telse:\n\t\t\t\tspace = None\n\t\t#\n\t\tif values is not None and space is not None:\n\t\t\tif type(values) not in (tuple, list, dict) or space not in ('RGB', 'RGBA', 'rgb', 'rgba', 'HSV', 'HSVA', 'hsv', 'hsva'):\n\t\t\t\traise ValueError(\"Wrong parameters\")\n\t\t\t#\n\t\t\tif space in ['RGB', 'RGBA']:\n\t\t\t\tif type(values) == dict:\n\t\t\t\t\tself.from_RGB(**values)\n\t\t\t\telif type(values) in [tuple, list]:\n\t\t\t\t\tself.from_RGB(*values)\n\t\t\telif space in ['rgb', 'rgba']:\n\t\t\t\tif type(values) == dict:\n\t\t\t\t\tself.from_rgb(**values)\n\t\t\t\telif type(values) in [tuple, list]:\n\t\t\t\t\tself.from_rgb(*values)\n\t\t\t#\n\t\t\tif space in ['HSV', 'HSVA']:\n\t\t\t\tif type(values) == dict:\n\t\t\t\t\tself.from_HSV(**values)\n\t\t\t\telif type(values) in [tuple, list]:\n\t\t\t\t\tself.from_HSV(*values)\n\t\t\telif space in ['hsv', 'hsva']:\n\t\t\t\tif type(values) == dict:\n\t\t\t\t\tself.from_hsv(**values)\n\t\t\t\telif type(values) in [tuple, list]:\n\t\t\t\t\tself.from_hsv(*values)\n\n\tdef __str__(self):\n\t\tif self.data is not None:\n\t\t\tstrRGB = 'RGB ' + str(self.RGB)\n\t\t\tstrHSV = 'HSV ' + str(self.HSV)\n\t\t\tstrAlpha = 'Alpha ' + str(self.alpha)\n\t\t\treturn strRGB + ' - ' + strHSV + ' - ' + strAlpha\n\t\telse:\n\t\t\treturn \"No color defined\"\n\n\tdef __eq__(self, other):\n\t\treturn self.data == other.data\n\n\t#All properties will be computed from rgba vector data\n\t@property\n\tdef alpha(self):\n\t\tif self.data is not None:\n\t\t\treturn self.rgba[-1]#range from 0 to 1\n\t\telse:\n\t\t\treturn None\n\t@property\n\tdef hex(self):\n\t\tif self.data is not None:\n\t\t\treturn \"#\"+\"\".join([\"0{0:x}\".format(v) if v < 16 else \"{0:x}\".format(v) for v in self.RGB])\n\t\telse:\n\t\t\treturn None\n\t## props with alpha\n\t@property\n\tdef RGBA(self): #values range from 0 to 255\n\t\tif self.data is not None:\n\t\t\treturn tuple([int(v*255) for v in self.rgba])\n\t\telse:\n\t\t\treturn None\n\t@property\n\tdef rgba(self): #values range from 0 to 1\n\t\tif self.data is not None:\n\t\t\treturn tuple(self.data)\n\t\telse:\n\t\t\treturn None\n\t@property\n\tdef HSVA(self): #H ranges from 0° to 360°. Other values range from 0 to 100%\n\t\tif self.data is not None:\n\t\t\th, s, v, a = self.hsva\n\t\t\treturn tuple([h*360, s*100, v*100, a*100])\n\t\telse:\n\t\t\treturn None\n\t@property\n\tdef hsva(self): #values range from 0 to 1\n\t\tif self.data is not None:\n\t\t\treturn self.hsv + tuple([self.alpha])\n\t\telse:\n\t\t\treturn None\n\t## props without alpha\n\t@property\n\tdef RGB(self):\n\t\tif self.data is not None:\n\t\t\treturn tuple(self.RGBA[:-1])\n\t\telse:\n\t\t\treturn None\n\t@property\n\tdef rgb(self):\n\t\tif self.data is not None:\n\t\t\treturn tuple(self.rgba[:-1])\n\t\telse:\n\t\t\treturn None\n\t@property\n\tdef HSV(self):\n\t\tif self.data is not None:\n\t\t\th, s, v = self.hsv\n\t\t\treturn tuple([h*360, s*100, v*100])\n\t\telse:\n\t\t\treturn None\n\t@property\n\tdef hsv(self):\n\t\tif self.data is not None:\n\t\t\treturn colorsys.rgb_to_hsv(*self.rgb)\n\t\telse:\n\t\t\treturn None\n\n\t#another way to get color value (dictionary output possible)\n\tdef getColor(self, space='RGB', asDict=False):\n\t\tif space == 'RGB':\n\t\t\tif asDict:\n\t\t\t\treturn {key:self.RGB[i] for i, key in enumerate(space)}\n\t\t\telse:\n\t\t\t\treturn self.RGB\n\t\telif space == 'RGBA':\n\t\t\tif asDict:\n\t\t\t\treturn {key:self.RGBA[i] for i, key in enumerate(space)}\n\t\t\telse:\n\t\t\t\treturn self.RGBA\n\t\telif space == 'rgba':\n\t\t\tif asDict:\n\t\t\t\treturn {key:self.rgba[i] for i, key in enumerate(space)}\n\t\t\telse:\n\t\t\t\treturn self.rgba\n\t\telif space == 'rgb':\n\t\t\tif asDict:\n\t\t\t\treturn {key:self.rgb[i] for i, key in enumerate(space)}\n\t\t\telse:\n\t\t\t\treturn self.rgb\n\t\tif space == 'HSV':\n\t\t\tif asDict:\n\t\t\t\treturn {key:self.HSV[i] for i, key in enumerate(space)}\n\t\t\telse:\n\t\t\t\treturn self.HSV\n\t\telif space == 'HSVA':\n\t\t\tif asDict:\n\t\t\t\treturn {key:self.HSVA[i] for i, key in enumerate(space)}\n\t\t\telse:\n\t\t\t\treturn self.HSVA\n\t\telif space == 'hsva':\n\t\t\tif asDict:\n\t\t\t\treturn {key:self.hsva[i] for i, key in enumerate(space)}\n\t\t\telse:\n\t\t\t\treturn self.hsva\n\t\telif space == 'hsv':\n\t\t\tif asDict:\n\t\t\t\treturn {key:self.hsv[i] for i, key in enumerate(space)}\n\t\t\telse:\n\t\t\t\treturn self.hsv\n\n\t#You can create Color object in many ways:\n\t# Color.from_rgb(0, 1, 1, 1) - passing arguments\n\t# Color.from_rgb(r=0, g=1, b=1, a=1) - passing keywords arguments\n\t# Color.from_rgb(*[0, 1, 1, 1]) - unpacking a list or a tuple (or a generic iterable)\n\t# Color.from_rgb(**{'r':0, 'g':1, 'b':1, 'a':1}) - unpacking a dictionary\n\n\tdef from_RGB(self, R, G, B, A=255):\n\t\tif all(0<=v<=255 for v in (R, G, B, A)):\n\t\t\tself.data = [ v/255 for v in (R, G, B, A) ]\n\t\telse:\n\t\t\traise ValueError(\"RGB values must range from 0 to 255\")\n\n\tdef from_rgb(self, r, g, b, a=1):\n\t\tif all(0<=v<=1 for v in (r, g, b, a)):\n\t\t\tself.data = [r, g, b, a]\n\t\telse:\n\t\t\traise ValueError(\"rgb values must range from 0 to 1\")\n\n\tdef from_HSV(self, H, S, V, A=1):\n\t\tif 0<=H<=360 and 0<=S<=100 and 0<=V<=100:\n\t\t\tself.data = list(colorsys.hsv_to_rgb(H/360, S/100, V/100))\n\t\t\tself.data.append(A)\n\t\telse:\n\t\t\traise ValueError(\"Hue must range from 0 to 360°. S and V must range from 0 to 100%\")\n\n\tdef from_hsv(self, h, s, v, a=1):\n\t\tif all(0<=v<=1 for v in (h, s, v, a)):\n\t\t\tself.data = list(colorsys.hsv_to_rgb(h, s, v))\n\t\t\tself.data.append(a)\n\t\telse:\n\t\t\traise ValueError(\"hsv values must range from 0 to 1\")\n\n\tdef from_hex(self, hex):\n\t\tR,G,B = [int(hex[i:i+2], 16) for i in range(1,6,2)]\n\t\tself.data = [ v/255 for v in (R, G, B, 255) ]\n\n\n\nclass Stop():\n\tdef __init__(self, position, color):\n\t\tself.position = position\n\t\tself.color = color\n\n\tdef __lt__(self, other):\n\t\treturn self.position < other.position\n\n\nclass Gradient():\n\n\tdef __init__(self, svg=False, permissive=False):\n\t\tself.stops = []\n\t\t#permissive rules allows duplicate position and same color for two followings stops\n\t\t#this option is useful when define discrete ramp\n\t\tself.permissive = permissive\n\t\tif svg:\n\t\t\tself.__readSVG(svg)\n\n\tdef __str__(self):\n\t\treturn str(self.asList())\n\n\tdef __readSVG(self, svg):\n\t\ttry:\n\t\t\tdomData = parse(svg)\n\t\texcept Exception as e:\n\t\t\tlog.error(\"Cannot parse svg file : {}\".format(e))\n\t\t\treturn False\n\t\tlinearGradients = domData.getElementsByTagName('linearGradient')\n\t\tnbGradients = len(linearGradients)\n\t\tif nbGradients == 0:\n\t\t\tlog.error(\"No gradient in this SVG\")\n\t\t\treturn False\n\t\telif nbGradients > 1:\n\t\t\tlog.error('Only the first gradient will be imported')\n\t\tlinearGradient = linearGradients[0]\n\t\tstops = linearGradient.getElementsByTagName('stop')\n\t\tif len(stops) <= 1:\n\t\t\tlog.error('No enough stops')\n\t\t\treturn False\n\t\t#begin import\n\t\tfor stop in stops:\n\t\t\tpositionStr = stop.getAttribute('offset') # \"33.5%\"\n\t\t\tposition = float(positionStr[:-1])/100\n\t\t\tcolorStr = stop.getAttribute('stop-color') # \"rgb(51, 188, 207)\"\n\t\t\talpha = float(stop.getAttribute('stop-opacity')) #value between 0-1\n\t\t\tif ', ' in colorStr:\n\t\t\t\trgba = colorStr[4:-1].split(', ')\n\t\t\telse:\n\t\t\t\trgba = colorStr[4:-1].split(',')\n\t\t\trgba = [int(c)/255 for c in rgba]\n\t\t\trgba.append(alpha)\n\t\t\tcolor = Color()\n\t\t\tcolor.from_rgb(*rgba)\n\t\t\tself.addStop(position, color, reorder=False)\n\t\t#finish\n\t\tself.sortStops()\n\t\tdomData.unlink()\n\t\treturn True\n\n\n\t@property\n\tdef positions(self):\n\t\treturn [stop.position for stop in self.stops]\n\t@property\n\tdef colors(self):\n\t\treturn [stop.color for stop in self.stops]\n\n\n\tdef asList(self, space='RGBA'):\n\t\treturn [(round(stop.position,2), stop.color.getColor(space)) for stop in self.stops]\n\n\tdef asDict(self, space='RGBA'):\n\t\treturn {round(stop.position,2):stop.color.getColor(space, asDict=True) for stop in self.stops}\n\n\n\tdef addStop(self, position, color, reorder=True):\n\t\tif not self.permissive: #permissive option allows discrete color ramp definition\n\t\t\t#avoid same color in two following stops\n\t\t\tif len(self.colors) >= 1:\n\t\t\t\tif color == self.colors[-1]:\n\t\t\t\t\treturn False\n\t\t\t#avoid duplicate position\n\t\t\tif position in self.positions:\n\t\t\t\treturn False\n\t\t#check if position is between 0-1\n\t\tif not 0<=position<=1:\n\t\t\treturn False\n\t\t#check color\n\t\tif type(color) != Color:\n\t\t\treturn False\n\t\tstop = Stop(position, color)\n\t\tself.stops.append(stop)\n\t\tif reorder:\n\t\t\tself.sortStops()\n\t\treturn True\n\n\tdef addStops(self, positions, colors):\n\t\tif len(positions) != len(colors):\n\t\t\treturn False\n\t\tfor i, pos in enumerate(positions):\n\t\t\tself.addStop(pos, colors[i], reorder=False)\n\t\tself.sortStops()\n\t\treturn True\n\n\tdef sortStops(self):\n\t\tself.stops.sort() #sort(key=attrgetter('position'))\n\n\tdef rmColor(self, color):\n\t\tif type(color) != Color:\n\t\t\treturn False\n\t\ttry:\n\t\t\tidx = self.colors.index(color)\n\t\texcept ValueError as e :\n\t\t\tlog.error('Cannot remove color from this gradient : {}'.format(e))\n\t\t\treturn False\n\t\telse:\n\t\t\tself.stops.pop(idx)\n\t\t\treturn True\n\n\tdef rmPosition(self, pos):\n\t\ttry:\n\t\t\tidx = self.positions.index(pos)\n\t\texcept ValueError as e:\n\t\t\tlog.error('Cannot remove position from this gradient : {}'.format(e))\n\t\t\treturn False\n\t\telse:\n\t\t\tself.stops.pop(idx)\n\t\t\treturn True\n\n\tdef rescale(self, toMin, toMax):\n\t\tfromMin = min(self.positions)\n\t\tfromMax = max(self.positions)\n\t\tfor stop in self.stops:\n\t\t\tstop.position = scale(stop.position, fromMin, fromMax, toMin, toMax)\n\n\tdef evaluate(self, pos, colorSpace = 'RGB', method='LINEAR'):\n\t\t#check interpo method\n\t\tif method not in ['DISCRETE', 'NEAREST', 'LINEAR', 'SPLINE']:\n\t\t\tmethod = 'LINEAR'\n\t\t#check color space\n\t\tif colorSpace in ['RGB', 'RGBA', 'rgb', 'rgba']:\n\t\t\tcolorSpace = 'rgba' #we will work with normalized values\n\t\telif colorSpace in ['HSV', 'HSVA', 'hsv', 'hsva']:\n\t\t\tcolorSpace = 'hsva'\n\t\telse:\n\t\t\tcolorSpace = 'rgba' #default\n\t\t#check position\n\t\tself.sortStops()\n\t\tpositions = self.positions\n\t\t#if pos already exist return it's color\n\t\tif pos in positions:\n\t\t\tidx = positions.index(pos)\n\t\t\treturn self.stops[idx].color\n\t\t#if pos is first or last stop, return corresponding color (no extrapolation)\n\t\tif pos < positions[0]:\n\t\t\treturn self.stops[0].color\n\t\telif pos > positions[-1]:\n\t\t\treturn self.stops[-1].color\n\t\t#find previous and next stops\n\t\tfor i, p in enumerate(positions):\n\t\t\tif p<pos<positions[i+1]:\n\t\t\t\tprevStop = self.stops[i]\n\t\t\t\tnextStop = self.stops[i+1]\n\t\t\t\tbreak\n\n\t\tif method == 'DISCRETE':\n\t\t\treturn prevStop.color\n\n\t\telif method == 'NEAREST':\n\t\t\tif (pos - prevStop.position) < (nextStop.position - pos):\n\t\t\t\treturn prevStop.color\n\t\t\telse:\n\t\t\t\treturn nextStop.color\n\n\t\telif method == 'LINEAR':\n\t\t\tx1, x2 = prevStop.position, nextStop.position\n\t\t\tinterpolateValues = []\n\t\t\tfor i in range(4): #4 channels (rgba or hsva)\n\t\t\t\ty1, y2 = prevStop.color.getColor(colorSpace)[i], nextStop.color.getColor(colorSpace)[i]\n\t\t\t\tdy = y2-y1\n\t\t\t\tif colorSpace == 'hsva' and i == 0 and abs(dy) > 0.5: # hue values with delta > 180°\n\t\t\t\t\t# Hue is cyclic\n\t\t\t\t\t# > interpolation must be done through the shortest path (clockwise or counterclockwise)\n\t\t\t\t\t# > to interpolate CCW, add 180° to all hue values, then compute modulo 360° on interpolate result\n\t\t\t\t\ty1, y2 = [hue+0.5 if hue<0 else hue-0.5 for hue in (y1, y2)]\n\t\t\t\t\ty = linearInterpo(x1, x2, y1, y2, pos) % 1\n\t\t\t\telse:\n\t\t\t\t\ty = linearInterpo(x1, x2, y1, y2, pos)\n\t\t\t\tinterpolateValues.append(round(y,2))\n\t\t\treturn Color(interpolateValues, colorSpace)\n\n\t\telif method == 'SPLINE':\n\t\t\txData = self.positions\n\t\t\tif len(xData) < 3: #spline interpo needs at least 3 pts, otherwise compute a linear interpolation\n\t\t\t\treturn self.evaluate(pos, colorSpace, method='LINEAR')\n\t\t\tinterpolateValues = []\n\t\t\tfor i in range(4): #4 channels (rgba or hsva)\n\t\t\t\tyData = [color.getColor(colorSpace)[i] for color in self.colors]\n\t\t\t\tdy = (nextStop.color.getColor(colorSpace)[i] - prevStop.color.getColor(colorSpace)[i])\n\t\t\t\tif colorSpace == 'hsva' and i == 0 and abs(dy) > 0.5: # hue values with delta > 180°\n\t\t\t\t\t# Hue is cyclic\n\t\t\t\t\t# > interpolation must be done through the shortest path (clockwise or counterclockwise)\n\t\t\t\t\t# > to interpolate CCW, add 180° to all hue values, then compute modulo 360° on interpolate result\n\t\t\t\t\tyData = [hue+0.5 if hue<0 else hue-0.5 for hue in yData]\n\t\t\t\t\ty = akima.interpolate(xData, yData, [pos])[0] % 1\n\t\t\t\telse:\n\t\t\t\t\ty = akima.interpolate(xData, yData, [pos])[0]\n\t\t\t\t#Constrain result between 0-1\n\t\t\t\ty = 1 if y>1 else 0 if y<0 else y\n\t\t\t\t#append\n\t\t\t\tinterpolateValues.append(round(y,2))\n\t\t\treturn Color(interpolateValues, colorSpace)\n\n\n\tdef getRangeColor(self, n, interpoSpace='RGB', interpoMethod='LINEAR'):\n\t\t'''return a new gradient'''\n\t\tramp = Gradient(permissive=True)#permissive needed because discrete interpo can return same color for 2 or more following stops\n\t\toffset = 1/(n-1)\n\t\tposition = 0\n\t\tfor i in range(n):\n\t\t\tcolor = self.evaluate(position, interpoSpace, interpoMethod)\n\t\t\tramp.addStop(position, color, reorder=False)\n\t\t\tposition += offset\n\t\treturn ramp\n\n\n\tdef exportSVG(self, svgPath, discrete=False):\n\t\tname = os.path.splitext(os.path.basename(svgPath))[0]\n\t\tname = name.replace(\" \", \"_\")\n\t\t# create an SVG XML element (see the SVG specification for attribute details)\n\t\tsvg = etree.Element('svg', width='300', height='45', version='1.1', xmlns='http://www.w3.org/2000/svg', viewBox='0 0 300 45')\n\t\tgradient = etree.Element('linearGradient', id=name, gradientUnits='objectBoundingBox', spreadMethod='pad', x1='0%', x2='100%', y1='0%', y2='0%')\n\n\t\t#make discrete svg ramp\n\t\tif discrete:\n\t\t\tstops = []\n\t\t\tfor i, stop in enumerate(self.stops):\n\t\t\t\tif i>0:\n\t\t\t\t\tstops.append( Stop(stop.position, self.stops[i-1].color) )\n\t\t\t\tstops.append( Stop(stop.position, stop.color) )\n\t\telse:\n\t\t\tstops = self.stops\n\n\t\tfor stop in stops:\n\t\t\tp = stop.position * 100\n\t\t\tp = str(round(p,2)) + '%'\n\t\t\tr,g,b = stop.color.RGB\n\t\t\tc = \"rgb(%d,%d,%d)\" % (r, g, b)\n\t\t\ta = str(stop.color.alpha)\n\t\t\tetree.SubElement(gradient, 'stop', {'offset':p, 'stop-color':c, 'stop-opacity':a}) #use dict because hyphens in tags\n\t\tsvg.append(gradient)\n\t\trect = etree.Element('rect', {'fill':\"url(#%s)\" % (name), 'x':'4', 'y':'4', 'width':'292', 'height':'37', 'stroke':'black', 'stroke_width':'1'})\n\t\tsvg.append(rect)\n\t\t# get string\n\t\txmlstr = etree.tostring(svg, encoding='utf8', method='xml').decode('utf-8')\n\t\t# etree doesn't have pretty xml function, so use minidom tu get a pretty xml ...\n\t\treparsed = parseString(xmlstr)\n\t\txmlstr = reparsed.toprettyxml()\n\t\t# write to file\n\t\tf = open(svgPath,\"w\")\n\t\tf.write(xmlstr)\n\t\tf.close()\n\n\t\treturn\n"
  },
  {
    "path": "core/utils/timing.py",
    "content": "import time\n\ndef perf_clock():\n    if hasattr(time, 'clock'):\n        return time.clock()\n    elif hasattr(time, 'perf_counter'):\n        return time.perf_counter()\n    else:\n        raise Exception(\"Python time lib doesn't contain a suitable clock function\")"
  },
  {
    "path": "core/utils/xy.py",
    "content": "# -*- coding:utf-8 -*-\n\n# This file is part of BlenderGIS\n\n#  ***** GPL LICENSE BLOCK *****\n#\n#  This program is free software: you can redistribute it and/or modify\n#  it under the terms of the GNU General Public License as published by\n#  the Free Software Foundation, either version 3 of the License, or\n#  (at your option) any later version.\n#\n#  This program is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#  GNU General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public License\n#  along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#  All rights reserved.\n#  ***** GPL LICENSE BLOCK *****\n\n\nclass XY(object):\n\t'''A class to represent 2-tuple value'''\n\tdef __init__(self, x, y, z=None):\n\t\t'''\n\t\tYou can use the constructor in many ways:\n\t\tXY(0, 1) - passing two arguments\n\t\tXY(x=0, y=1) - passing keywords arguments\n\t\tXY(**{'x': 0, 'y': 1}) - unpacking a dictionary\n\t\tXY(*[0, 1]) - unpacking a list or a tuple (or a generic iterable)\n\t\t'''\n\t\tif z is None:\n\t\t\tself.data=[x, y]\n\t\telse:\n\t\t\tself.data=[x, y, z]\n\tdef __str__(self):\n\t\tif self.z is not None:\n\t\t\treturn \"(%s, %s, %s)\"%(self.x, self.y, self.z)\n\t\telse:\n\t\t\treturn \"(%s, %s)\"%(self.x,self.y)\n\tdef __repr__(self):\n\t\treturn self.__str__()\n\tdef __getitem__(self,item):\n\t\treturn self.data[item]\n\tdef __setitem__(self, idx, value):\n\t\tself.data[idx] = value\n\tdef __iter__(self):\n\t\treturn iter(self.data)\n\tdef __len__(self):\n\t\treturn len(self.data)\n\t@property\n\tdef x(self):\n\t\treturn self.data[0]\n\t@property\n\tdef y(self):\n\t\treturn self.data[1]\n\t@property\n\tdef z(self):\n\t\ttry:\n\t\t\treturn self.data[2]\n\t\texcept IndexError:\n\t\t\treturn None\n\t@property\n\tdef xy(self):\n\t\treturn self.data[:2]\n\t@property\n\tdef xyz(self):\n\t\treturn self.data\n"
  },
  {
    "path": "geoscene.py",
    "content": "# -*- coding:utf-8 -*-\n\n#  ***** GPL LICENSE BLOCK *****\n#\n#  This program is free software: you can redistribute it and/or modify\n#  it under the terms of the GNU General Public License as published by\n#  the Free Software Foundation, either version 3 of the License, or\n#  (at your option) any later version.\n#\n#  This program is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#  GNU General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public License\n#  along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#  All rights reserved.\n#  ***** GPL LICENSE BLOCK *****\n\nimport logging\nlog = logging.getLogger(__name__)\n\nimport bpy\nfrom bpy.props import (StringProperty, IntProperty, FloatProperty, BoolProperty,\nEnumProperty, FloatVectorProperty, PointerProperty)\nfrom bpy.types import Operator, Panel, PropertyGroup\n\nfrom .prefs import PredefCRS\nfrom .core.proj.reproj import reprojPt\nfrom .core.proj.srs import SRS\n\nfrom .operators.utils import mouseTo3d\n\nPKG = __package__\n\n'''\nPolicy :\nThis module manages in priority the CRS coordinates of the scene's origin and\nupdates the corresponding longitude/latitude only if it can to do the math.\n\nA scene is considered correctly georeferenced when at least a valid CRS is defined\nand the coordinates of scene's origin in this CRS space is set. A geoscene will be\nbroken if the origin is set but not the CRS or if the origin is only set as longitude/latitude.\n\nChanging the CRS will raise an error if updating existing origin coordinate is not possible.\n\nBoth methods setOriginGeo() and setOriginPrj() try a projection task to maintain\ncoordinates synchronized. Failing reprojection does not abort the exec, but will\ntrigger deletion of unsynch coordinates. Synchronization can be disable for\nsetOriginPrj() method only.\n\nExcept setOriginGeo() method, dealing directly with longitude/latitude\nautomatically trigger a reprojection task which will raise an error if failing.\n\nSequences of methods :\nmoveOriginPrj() | updOriginPrj() > setOriginPrj() > [reprojPt()]\nmoveOriginGeo() > updOriginGeo() > reprojPt() > updOriginPrj() > setOriginPrj()\n\nStandalone properties (lon, lat, crsx et crsy) can be edited independently without any extra checks.\n'''\n\nclass SK():\n\t\"\"\"Alias to Scene Keys used to store georef infos\"\"\"\n\t# latitude and longitude of scene origin in decimal degrees\n\tLAT = \"latitude\"\n\tLON = \"longitude\"\n\t#Spatial Reference System Identifier\n\t# can be directly an EPSG code or formated following the template \"AUTH:4326\"\n\t# or a proj4 string definition of Coordinate Reference System (CRS)\n\tCRS = \"SRID\"\n\t# Coordinates of scene origin in CRS space\n\tCRSX = \"crs x\"\n\tCRSY = \"crs y\"\n\t# General scale denominator of the map (1:x)\n\tSCALE = \"scale\"\n\t# Current zoom level in the Tile Matrix Set\n\tZOOM = \"zoom\"\n\n\n\nclass GeoScene():\n\n\tdef __init__(self, scn=None):\n\t\tif scn is None:\n\t\t\tself.scn = bpy.context.scene\n\t\telse:\n\t\t\tself.scn = scn\n\t\tself.SK = SK()\n\n\t@property\n\tdef _rna_ui(self):\n\t\t# get or init the dictionary containing IDprops settings\n\t\trna_ui = self.scn.get('_RNA_UI', None)\n\t\tif rna_ui is None:\n\t\t\tself.scn['_RNA_UI'] = {}\n\t\t\trna_ui = self.scn['_RNA_UI']\n\t\treturn rna_ui\n\n\tdef view3dToProj(self, dx, dy):\n\t\t'''Convert view3d coords to crs coords'''\n\t\tif self.hasOriginPrj:\n\t\t\tx = self.crsx + (dx * self.scale)\n\t\t\ty = self.crsy + (dy * self.scale)\n\t\t\treturn x, y\n\t\telse:\n\t\t\traise Exception(\"Scene origin coordinate is unset\")\n\n\tdef projToView3d(self, dx, dy):\n\t\t'''Convert view3d coords to crs coords'''\n\t\tif self.hasOriginPrj:\n\t\t\tx = (dx * self.scale) - self.crsx\n\t\t\ty = (dy * self.scale) - self.crsy\n\t\t\treturn x, y\n\t\telse:\n\t\t\traise Exception(\"Scene origin coordinate is unset\")\n\n\t@property\n\tdef hasCRS(self):\n\t\treturn SK.CRS in self.scn\n\n\t@property\n\tdef hasValidCRS(self):\n\t\tif not self.hasCRS:\n\t\t\treturn False\n\t\treturn SRS.validate(self.crs)\n\n\t@property\n\tdef isGeoref(self):\n\t\t'''A scene is georef if at least a valid CRS is defined and\n\t\tthe coordinates of scene's origin in this CRS space is set'''\n\t\treturn self.hasValidCRS and self.hasOriginPrj\n\n\t@property\n\tdef isFullyGeoref(self):\n\t\treturn self.hasValidCRS and self.hasOriginPrj and self.hasOriginGeo\n\n\t@property\n\tdef isPartiallyGeoref(self):\n\t\treturn self.hasCRS or self.hasOriginPrj or self.hasOriginGeo\n\n\t@property\n\tdef isBroken(self):\n\t\t\"\"\"partial georef infos make the geoscene unusuable and broken\"\"\"\n\t\treturn (self.hasCRS and not self.hasValidCRS) \\\n\t\tor (not self.hasCRS and (self.hasOriginPrj or self.hasOriginGeo)) \\\n\t\tor (self.hasCRS and self.hasOriginGeo and not self.hasOriginPrj)\n\n\t@property\n\tdef hasOriginGeo(self):\n\t\treturn SK.LAT in self.scn and SK.LON in self.scn\n\n\t@property\n\tdef hasOriginPrj(self):\n\t\treturn SK.CRSX in self.scn and SK.CRSY in self.scn\n\n\tdef setOriginGeo(self, lon, lat):\n\t\tself.lon, self.lat = lon, lat\n\t\ttry:\n\t\t\tself.crsx, self.crsy = reprojPt(4326, self.crs, lon, lat)\n\t\texcept Exception as e:\n\t\t\tif self.hasOriginPrj:\n\t\t\t\tself.delOriginPrj()\n\t\t\t\tlog.warning('Origin proj has been deleted because the property could not be updated', exc_info=True)\n\n\tdef setOriginPrj(self, x, y, synch=True):\n\t\tself.crsx, self.crsy = x, y\n\t\tif synch:\n\t\t\ttry:\n\t\t\t\tself.lon, self.lat = reprojPt(self.crs, 4326, x, y)\n\t\t\texcept Exception as e:\n\t\t\t\tif self.hasOriginGeo:\n\t\t\t\t\tself.delOriginGeo()\n\t\t\t\t\tlog.warning('Origin geo has been deleted because the property could not be updated', exc_info=True)\n\t\telif self.hasOriginGeo:\n\t\t\tself.delOriginGeo()\n\t\t\tlog.warning('Origin geo has been deleted because coordinate synchronization is disable')\n\n\tdef updOriginPrj(self, x, y, updObjLoc=True, synch=True):\n\t\t'''Update/move scene origin passing absolute coordinates'''\n\t\tif not self.hasOriginPrj:\n\t\t\traise Exception(\"Cannot update an unset origin.\")\n\t\tdx = x - self.crsx\n\t\tdy = y - self.crsy\n\t\tself.setOriginPrj(x, y, synch)\n\t\tif updObjLoc:\n\t\t\tself._moveObjLoc(dx, dy)\n\n\n\tdef updOriginGeo(self, lon, lat, updObjLoc=True):\n\t\tif not self.isGeoref:\n\t\t\traise Exception(\"Cannot update geo origin of an ungeoref scene.\")\n\t\tx, y = reprojPt(4326, self.crs, lon, lat)\n\t\tself.updOriginPrj(x, y, updObjLoc)\n\n\n\tdef moveOriginGeo(self, dx, dy, updObjLoc=True):\n\t\tif not self.hasOriginGeo:\n\t\t\traise Exception(\"Cannot move an unset origin.\")\n\t\tx = self.lon + dx\n\t\ty = self.lat + dy\n\t\tself.updOriginGeo(x, y, updObjLoc=updObjLoc)\n\n\tdef moveOriginPrj(self, dx, dy, useScale=True, updObjLoc=True, synch=True):\n\t\t'''Move scene origin passing relative deltas'''\n\t\tif not self.hasOriginPrj:\n\t\t\traise Exception(\"Cannot move an unset origin.\")\n\n\t\tif useScale:\n\t\t\tself.setOriginPrj(self.crsx + dx * self.scale, self.crsy + dy * self.scale, synch)\n\t\telse:\n\t\t\tself.setOriginPrj(self.crsx + dx, self.crsy + dy, synch)\n\n\t\tif updObjLoc:\n\t\t\tself._moveObjLoc(dx, dy)\n\n\n\tdef _moveObjLoc(self, dx, dy):\n\t\ttopParents = [obj for obj in self.scn.objects if not obj.parent]\n\t\tfor obj in topParents:\n\t\t\tobj.location.x -= dx\n\t\t\tobj.location.y -= dy\n\n\n\tdef getOriginGeo(self):\n\t\treturn self.lon, self.lat\n\n\tdef getOriginPrj(self):\n\t\treturn self.crsx, self.crsy\n\n\tdef delOriginGeo(self):\n\t\tdel self.lat\n\t\tdel self.lon\n\n\tdef delOriginPrj(self):\n\t\tdel self.crsx\n\t\tdel self.crsy\n\n\tdef delOrigin(self):\n\t\tself.delOriginGeo()\n\t\tself.delOriginPrj()\n\n\t@property\n\tdef crs(self):\n\t\treturn self.scn.get(SK.CRS, None) #always string\n\t@crs.setter\n\tdef crs(self, v):\n\t\t#Make sure input value is a valid crs string representation\n\t\tcrs = SRS(v) #will raise an error if the crs is not valid\n\t\t#Reproj existing origin. New CRS will not be set if updating existing origin is not possible\n\t\t# try first to reproj from origin geo because self.crs can be empty or broken\n\t\tif self.hasOriginGeo:\n\t\t\tif crs.isWGS84:\n\t\t\t\t#if destination crs is wgs84, just assign lonlat to originprj\n\t\t\t\tself.crsx, self.crsy = self.lon, self.lat\n\t\t\tself.crsx, self.crsy = reprojPt(4326, str(crs), self.lon, self.lat)\n\t\telif self.hasOriginPrj and self.hasCRS:\n\t\t\tif self.hasValidCRS:\n\t\t\t\t# will raise an error is current crs is empty or invalid\n\t\t\t\tself.crsx, self.crsy = reprojPt(self.crs, str(crs), self.crsx, self.crsy)\n\t\t\telse:\n\t\t\t\traise Exception(\"Scene origin coordinates cannot be updated because current CRS is invalid.\")\n\t\t#Set ID prop\n\t\tif SK.CRS not in self.scn:\n\t\t\tself._rna_ui[SK.CRS] = {\"description\": \"Map Coordinate Reference System\", \"default\": ''}\n\t\tself.scn[SK.CRS] = str(crs)\n\t@crs.deleter\n\tdef crs(self):\n\t\tif SK.CRS in self.scn:\n\t\t\tdel self.scn[SK.CRS]\n\n\n\t@property\n\tdef lat(self):\n\t\treturn self.scn.get(SK.LAT, None)\n\t@lat.setter\n\tdef lat(self, v):\n\t\tif SK.LAT not in self.scn:\n\t\t\tself._rna_ui[SK.LAT] = {\"description\": \"Scene origin latitude\", \"default\": 0.0, \"min\":-90.0, \"max\":90.0}\n\t\tif -90 <= v <= 90:\n\t\t\tself.scn[SK.LAT] = v\n\t\telse:\n\t\t\traise ValueError('Wrong latitude value '+str(v))\n\t@lat.deleter\n\tdef lat(self):\n\t\tif SK.LAT in self.scn:\n\t\t\tdel self.scn[SK.LAT]\n\n\t@property\n\tdef lon(self):\n\t\treturn self.scn.get(SK.LON, None)\n\t@lon.setter\n\tdef lon(self, v):\n\t\tif SK.LON not in self.scn:\n\t\t\tself._rna_ui[SK.LON] = {\"description\": \"Scene origin longitude\", \"default\": 0.0, \"min\":-180.0, \"max\":180.0}\n\t\tif -180 <= v <= 180:\n\t\t\tself.scn[SK.LON] = v\n\t\telse:\n\t\t\traise ValueError('Wrong longitude value '+str(v))\n\t@lon.deleter\n\tdef lon(self):\n\t\tif SK.LON in self.scn:\n\t\t\tdel self.scn[SK.LON]\n\n\t@property\n\tdef crsx(self):\n\t\treturn self.scn.get(SK.CRSX, None)\n\t@crsx.setter\n\tdef crsx(self, v):\n\t\tif SK.CRSX not in self.scn:\n\t\t\tself._rna_ui[SK.CRSX] = {\"description\": \"Scene x origin in CRS space\", \"default\": 0.0}\n\t\tif isinstance(v, (int, float)):\n\t\t\tself.scn[SK.CRSX] = v\n\t\telse:\n\t\t\traise ValueError('Wrong x origin value '+str(v))\n\t@crsx.deleter\n\tdef crsx(self):\n\t\tif SK.CRSX in self.scn:\n\t\t\tdel self.scn[SK.CRSX]\n\n\t@property\n\tdef crsy(self):\n\t\treturn self.scn.get(SK.CRSY, None)\n\t@crsy.setter\n\tdef crsy(self, v):\n\t\tif SK.CRSY not in self.scn:\n\t\t\tself._rna_ui[SK.CRSY] = {\"description\": \"Scene y origin in CRS space\", \"default\": 0.0}\n\t\tif isinstance(v, (int, float)):\n\t\t\tself.scn[SK.CRSY] = v\n\t\telse:\n\t\t\traise ValueError('Wrong y origin value '+str(v))\n\t@crsy.deleter\n\tdef crsy(self):\n\t\tif SK.CRSY in self.scn:\n\t\t\tdel self.scn[SK.CRSY]\n\n\t@property\n\tdef scale(self):\n\t\treturn self.scn.get(SK.SCALE, 1)\n\t@scale.setter\n\tdef scale(self, v):\n\t\tif SK.SCALE not in self.scn:\n\t\t\tself._rna_ui[SK.SCALE] = {\"description\": \"Map scale denominator\", \"default\": 1, \"min\": 1}\n\t\tself.scn[SK.SCALE] = v\n\t@scale.deleter\n\tdef scale(self):\n\t\tif SK.SCALE in self.scn:\n\t\t\tdel self.scn[SK.SCALE]\n\n\t@property\n\tdef zoom(self):\n\t\treturn self.scn.get(SK.ZOOM, None)\n\t@zoom.setter\n\tdef zoom(self, v):\n\t\tif SK.ZOOM not in self.scn:\n\t\t\tself._rna_ui[SK.ZOOM] = {\"description\": \"Basemap zoom level\", \"default\": 1, \"min\": 0, \"max\":25}\n\t\tself.scn[SK.ZOOM] = v\n\t@zoom.deleter\n\tdef zoom(self):\n\t\tif SK.ZOOM in self.scn:\n\t\t\tdel self.scn[SK.ZOOM]\n\n\t@property\n\tdef hasScale(self):\n\t\t#return self.scale is not None\n\t\treturn SK.SCALE in self.scn\n\n\t@property\n\tdef hasZoom(self):\n\t\treturn self.zoom is not None\n\n\n################  OPERATORS ######################\nfrom bpy_extras.view3d_utils import region_2d_to_location_3d, region_2d_to_vector_3d\n\nclass GEOSCENE_OT_coords_viewer(Operator):\n\tbl_idname = \"geoscene.coords_viewer\"\n\tbl_description = ''\n\tbl_label = \"\"\n\tbl_options = {'INTERNAL', 'UNDO'}\n\n\tcoords: FloatVectorProperty(subtype='XYZ')\n\n\t@classmethod\n\tdef poll(cls, context):\n\t\treturn bpy.context.mode == 'OBJECT' and context.area.type == 'VIEW_3D'\n\n\tdef invoke(self, context, event):\n\t\tself.geoscn = GeoScene(context.scene)\n\t\tif not self.geoscn.isGeoref or self.geoscn.isBroken:\n\t\t\t\tself.report({'ERROR'}, \"Scene is not correctly georeferencing\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t#Add modal handler and init a timer\n\t\tcontext.window_manager.modal_handler_add(self)\n\t\tself.timer = context.window_manager.event_timer_add(0.05, window=context.window)\n\t\tcontext.window.cursor_set('CROSSHAIR')\n\t\treturn {'RUNNING_MODAL'}\n\n\tdef modal(self, context, event):\n\t\tif event.type == 'MOUSEMOVE':\n\t\t\tloc = mouseTo3d(context, event.mouse_region_x, event.mouse_region_y)\n\t\t\tx, y = self.geoscn.view3dToProj(loc.x, loc.y)\n\t\t\tcontext.area.header_text_set(\"x {:.3f}, y {:.3f}, z {:.3f}\".format(x, y, loc.z))\n\t\tif event.type == 'ESC' and event.value == 'PRESS':\n\t\t\tcontext.window.cursor_set('DEFAULT')\n\t\t\tcontext.area.header_text_set(None)\n\t\t\treturn {'CANCELLED'}\n\t\treturn {'RUNNING_MODAL'}\n\n\nclass GEOSCENE_OT_set_crs(Operator):\n\t'''\n\tuse the enum of predefinites crs defined in addon prefs\n\tto select and switch scene crs definition\n\t'''\n\n\tbl_idname = \"geoscene.set_crs\"\n\tbl_description = 'Switch scene crs'\n\tbl_label = \"Switch to\"\n\tbl_options = {'INTERNAL', 'UNDO'}\n\n\t\"\"\"\n\t#to avoid conflict, make a distinct predef crs enum\n\t#instead of reuse the one defined in addon pref\n\n\tdef listPredefCRS(self, context):\n\t\treturn PredefCRS.getEnumItems()\n\n\tcrsEnum = EnumProperty(\n\t\tname = \"Predefinate CRS\",\n\t\tdescription = \"Choose predefinite Coordinate Reference System\",\n\t\titems = listPredefCRS\n\t\t)\n\t\"\"\"\n\n\tdef draw(self,context):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\tlayout = self.layout\n\t\trow = layout.row(align=True)\n\t\t#row.prop(self, \"crsEnum\", text='')\n\t\trow.prop(prefs, \"predefCrs\", text='')\n\t\t#row.operator(\"geoscene.show_pref\", text='', icon='PREFERENCES')\n\t\trow.operator(\"bgis.add_predef_crs\", text='', icon='ADD')\n\n\tdef invoke(self, context, event):\n\t\treturn context.window_manager.invoke_props_dialog(self, width=200)\n\n\tdef execute(self, context):\n\t\tgeoscn = GeoScene(context.scene)\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\ttry:\n\t\t\tgeoscn.crs = prefs.predefCrs\n\t\texcept Exception as err:\n\t\t\tlog.error('Cannot update crs', exc_info=True)\n\t\t\tself.report({'ERROR'}, 'Cannot update crs. Check logs form more info')\n\t\t\treturn {'CANCELLED'}\n\t\t#\n\t\tcontext.area.tag_redraw()\n\t\treturn {'FINISHED'}\n\nclass GEOSCENE_OT_init_org(Operator):\n\n\tbl_idname = \"geoscene.init_org\"\n\tbl_description = 'Init scene origin custom props at location 0,0'\n\tbl_label = \"Init origin\"\n\tbl_options = {'INTERNAL', 'UNDO'}\n\n\tlonlat: BoolProperty(\n\t\tname = \"As lonlat\",\n\t\tdescription = \"Set origin coordinate as longitude and latitude\"\n\t\t)\n\n\tx: FloatProperty()\n\ty: FloatProperty()\n\n\tdef invoke(self, context, event):\n\t\treturn context.window_manager.invoke_props_dialog(self, width=200)\n\n\tdef execute(self, context):\n\t\tgeoscn = GeoScene(context.scene)\n\t\tif geoscn.hasOriginGeo or geoscn.hasOriginPrj:\n\t\t\tlog.warning('Cannot init scene origin because it already exist')\n\t\t\treturn {'CANCELLED'}\n\t\telse:\n\t\t\t#geoscn.lon, geoscn.lat = 0, 0\n\t\t\t#geoscn.crsx, geoscn.crsy = 0, 0\n\t\t\tif self.lonlat:\n\t\t\t\tgeoscn.setOriginGeo(self.x, self.y)\n\t\t\telse:\n\t\t\t\tgeoscn.setOriginPrj(self.x, self.y)\n\t\treturn {'FINISHED'}\n\nclass GEOSCENE_OT_edit_org_geo(Operator):\n\n\tbl_idname = \"geoscene.edit_org_geo\"\n\tbl_description = 'Edit scene origin longitude/latitude'\n\tbl_label = \"Edit origin geo\"\n\tbl_options = {'INTERNAL', 'UNDO'}\n\n\tlon: FloatProperty()\n\tlat: FloatProperty()\n\n\tdef invoke(self, context, event):\n\t\tgeoscn = GeoScene(context.scene)\n\t\tif geoscn.isBroken:\n\t\t\tself.report({'ERROR'}, \"Scene georef is broken\")\n\t\t\treturn {'CANCELLED'}\n\t\tself.lon, self.lat = geoscn.getOriginGeo()\n\t\treturn context.window_manager.invoke_props_dialog(self)\n\n\tdef execute(self, context):\n\t\tgeoscn = GeoScene(context.scene)\n\t\tif geoscn.hasOriginGeo:\n\t\t\tgeoscn.updOriginGeo(self.lon, self.lat)\n\t\telse:\n\t\t\tgeoscn.setOriginGeo(self.lon, self.lat)\n\t\treturn {'FINISHED'}\n\nclass GEOSCENE_OT_edit_org_prj(Operator):\n\n\tbl_idname = \"geoscene.edit_org_prj\"\n\tbl_description = 'Edit scene origin in projected system'\n\tbl_label = \"Edit origin proj\"\n\tbl_options = {'INTERNAL', 'UNDO'}\n\n\tx: FloatProperty()\n\ty: FloatProperty()\n\n\tdef invoke(self, context, event):\n\t\tgeoscn = GeoScene(context.scene)\n\t\tif geoscn.isBroken:\n\t\t\tself.report({'ERROR'}, \"Scene georef is broken\")\n\t\t\treturn {'CANCELLED'}\n\t\tself.x, self.y = geoscn.getOriginPrj()\n\t\treturn context.window_manager.invoke_props_dialog(self)\n\n\tdef execute(self, context):\n\t\tgeoscn = GeoScene(context.scene)\n\t\tif geoscn.hasOriginPrj:\n\t\t\tgeoscn.updOriginPrj(self.x, self.y)\n\t\telse:\n\t\t\tgeoscn.setOriginPrj(self.x, self.y)\n\t\treturn {'FINISHED'}\n\nclass GEOSCENE_OT_link_org_geo(Operator):\n\n\tbl_idname = \"geoscene.link_org_geo\"\n\tbl_description = 'Link scene origin lat long'\n\tbl_label = \"Link geo\"\n\tbl_options = {'INTERNAL', 'UNDO'}\n\n\tdef execute(self, context):\n\t\tgeoscn = GeoScene(context.scene)\n\t\tif geoscn.hasOriginPrj and geoscn.hasCRS:\n\t\t\ttry:\n\t\t\t\tgeoscn.lon, geoscn.lat = reprojPt(geoscn.crs, 4326, geoscn.crsx, geoscn.crsy)\n\t\t\texcept Exception as err:\n\t\t\t\tlog.error('Cannot compute lat/lon coordinates', exc_info=True)\n\t\t\t\tself.report({'ERROR'}, 'Cannot compute lat/lon. Check logs for more infos.')\n\t\t\t\treturn {'CANCELLED'}\n\t\telse:\n\t\t\tself.report({'ERROR'}, 'No enough infos')\n\t\t\treturn {'CANCELLED'}\n\t\treturn {'FINISHED'}\n\n\nclass GEOSCENE_OT_link_org_prj(Operator):\n\n\tbl_idname = \"geoscene.link_org_prj\"\n\tbl_description = 'Link scene origin in crs space'\n\tbl_label = \"Link prj\"\n\tbl_options = {'INTERNAL', 'UNDO'}\n\n\tdef execute(self, context):\n\t\tgeoscn = GeoScene(context.scene)\n\t\tif geoscn.hasOriginGeo and geoscn.hasCRS:\n\t\t\ttry:\n\t\t\t\tgeoscn.crsx, geoscn.crsy = reprojPt(4326, geoscn.crs, geoscn.lon, geoscn.lat)\n\t\t\texcept Exception as err:\n\t\t\t\tlog.error('Cannot compute crs coordinates', exc_info=True)\n\t\t\t\tself.report({'ERROR'}, 'Cannot compute crs coordinates. Check logs for more infos.')\n\t\t\t\treturn {'CANCELLED'}\n\t\telse:\n\t\t\tself.report({'ERROR'}, 'No enough infos')\n\t\t\treturn {'CANCELLED'}\n\t\treturn {'FINISHED'}\n\n\nclass GEOSCENE_OT_clear_org(Operator):\n\n\tbl_idname = \"geoscene.clear_org\"\n\tbl_description = 'Clear scene origin coordinates'\n\tbl_label = \"Clear origin\"\n\tbl_options = {'INTERNAL', 'UNDO'}\n\n\tdef execute(self, context):\n\t\tgeoscn = GeoScene(context.scene)\n\t\tgeoscn.delOrigin()\n\t\treturn {'FINISHED'}\n\nclass GEOSCENE_OT_clear_georef(Operator):\n\n\tbl_idname = \"geoscene.clear_georef\"\n\tbl_description = 'Clear all georef infos'\n\tbl_label = \"Clear georef\"\n\tbl_options = {'INTERNAL', 'UNDO'}\n\n\tdef execute(self, context):\n\t\tgeoscn = GeoScene(context.scene)\n\t\tgeoscn.delOrigin()\n\t\tdel geoscn.crs\n\t\treturn {'FINISHED'}\n\n\n################  PROPS GETTERS SETTERS ######################\n\ndef getLon(self):\n\tgeoscn = GeoScene()\n\treturn geoscn.lon\n\ndef getLat(self):\n\tgeoscn = GeoScene()\n\treturn geoscn.lat\n\ndef setLon(self, lon):\n\tgeoscn = GeoScene()\n\tprefs = bpy.context.preferences.addons[PKG].preferences\n\tif geoscn.hasOriginGeo:\n\t\tgeoscn.updOriginGeo(lon, geoscn.lat, updObjLoc=prefs.lockObj)\n\telse:\n\t\tgeoscn.setOriginGeo(lon, geoscn.lat)\n\ndef setLat(self, lat):\n\tgeoscn = GeoScene()\n\tprefs = bpy.context.preferences.addons[PKG].preferences\n\tif geoscn.hasOriginGeo:\n\t\tgeoscn.updOriginGeo(geoscn.lon, lat, updObjLoc=prefs.lockObj)\n\telse:\n\t\tgeoscn.setOriginGeo(geoscn.lon, lat)\n\ndef getCrsx(self):\n\tgeoscn = GeoScene()\n\treturn geoscn.crsx\n\ndef getCrsy(self):\n\tgeoscn = GeoScene()\n\treturn geoscn.crsy\n\ndef setCrsx(self, x):\n\tgeoscn = GeoScene()\n\tprefs = bpy.context.preferences.addons[PKG].preferences\n\tif geoscn.hasOriginPrj:\n\t\tgeoscn.updOriginPrj(x, geoscn.crsy, updObjLoc=prefs.lockObj)\n\telse:\n\t\tgeoscn.setOriginPrj(x, geoscn.crsy)\n\ndef setCrsy(self, y):\n\tgeoscn = GeoScene()\n\tprefs = bpy.context.preferences.addons[PKG].preferences\n\tif geoscn.hasOriginPrj:\n\t\tgeoscn.updOriginPrj(geoscn.crsx, y, updObjLoc=prefs.lockObj)\n\telse:\n\t\tgeoscn.setOriginPrj(geoscn.crsx, y)\n\n################  PANEL ######################\n\nclass GEOSCENE_PT_georef(Panel):\n\tbl_category = \"View\"#\"GIS\"\n\tbl_label = \"Geoscene\"\n\tbl_space_type = \"VIEW_3D\"\n\tbl_context = \"objectmode\"\n\tbl_region_type = \"UI\"\n\n\n\tdef draw(self, context):\n\t\tlayout = self.layout\n\t\tscn = context.scene\n\t\tgeoscn = GeoScene(scn)\n\n\t\t#layout.operator(\"bgis.pref_show\", icon='PREFERENCES')\n\n\t\tgeorefManagerLayout(self, context)\n\n\t\tlayout.operator(\"geoscene.coords_viewer\", icon='WORLD', text='Geo-coordinates')\n\n#hidden props used as display options in georef manager panel\nclass GLOBAL_PROPS(PropertyGroup):\n\tdisplayOriginGeo: BoolProperty(\n\t\tname='Geo', description='Display longitude and latitude of scene origin')\n\tdisplayOriginPrj: BoolProperty(\n\t\tname='Proj', description='Display coordinates of scene origin in CRS space')\n\tlon: FloatProperty(get=getLon, set=setLon)\n\tlat: FloatProperty(get=getLat, set=setLat)\n\tcrsx: FloatProperty(get=getCrsx, set=setCrsx)\n\tcrsy: FloatProperty(get=getCrsy, set=setCrsy)\n\ndef georefManagerLayout(self, context):\n\t'''Use this method to extend a panel with georef managment tools'''\n\tlayout = self.layout\n\tscn = context.scene\n\twm = bpy.context.window_manager\n\tgeoscn = GeoScene(scn)\n\n\tprefs = context.preferences.addons[PKG].preferences\n\n\tif geoscn.isBroken:\n\t\tlayout.alert = True\n\n\trow = layout.row(align=True)\n\trow.label(text='Scene georeferencing :')\n\tif geoscn.hasCRS:\n\t\trow.operator(\"geoscene.clear_georef\", text='', icon='CANCEL')\n\n\t#CRS\n\trow = layout.row(align=True)\n\t#row.alignment = 'LEFT'\n\t#row.label(icon='EMPTY_DATA')\n\tsplit = row.split(factor=0.25)\n\tif geoscn.hasCRS:\n\t\tsplit.label(icon='PROP_ON', text='CRS:')\n\telif not geoscn.hasCRS and (geoscn.hasOriginGeo or geoscn.hasOriginPrj):\n\t\tsplit.label(icon='ERROR', text='CRS:')\n\telse:\n\t\tsplit.label(icon='PROP_OFF', text='CRS:')\n\n\tif geoscn.hasCRS:\n\t\t##col = split.column(align=True)\n\t\t##col.enabled = False\n\t\t##col.prop(scn, '[\"'+SK.CRS+'\"]', text='')\n\t\tcrs = scn[SK.CRS]\n\t\tname = PredefCRS.getName(crs)\n\t\tif name is not None:\n\t\t\tsplit.label(text=name)\n\t\telse:\n\t\t\tsplit.label(text=crs)\n\telse:\n\t\tsplit.label(text=\"Not set\")\n\n\trow.operator(\"geoscene.set_crs\", text='', icon='PREFERENCES')\n\n\t#Origin\n\trow = layout.row(align=True)\n\t#row.alignment = 'LEFT'\n\t#row.label(icon='PIVOT_CURSOR')\n\tsplit = row.split(factor=0.25, align=True)\n\tif not geoscn.hasOriginGeo and not geoscn.hasOriginPrj:\n\t\tsplit.label(icon='PROP_OFF', text=\"Origin:\")\n\telif not geoscn.hasOriginGeo and geoscn.hasOriginPrj:\n\t\tsplit.label(icon='PROP_CON', text=\"Origin:\")\n\telif geoscn.hasOriginGeo and geoscn.hasOriginPrj:\n\t\tsplit.label(icon='PROP_ON', text=\"Origin:\")\n\telif geoscn.hasOriginGeo and not geoscn.hasOriginPrj:\n\t\tsplit.label(icon='ERROR', text=\"Origin:\")\n\n\tcol = split.column(align=True)\n\tif not geoscn.hasOriginGeo:\n\t\tcol.enabled = False\n\tcol.prop(wm.geoscnProps, 'displayOriginGeo', toggle=True)\n\n\tcol = split.column(align=True)\n\tif not geoscn.hasOriginPrj:\n\t\tcol.enabled = False\n\tcol.prop(wm.geoscnProps, 'displayOriginPrj', toggle=True)\n\n\tif geoscn.hasOriginGeo or geoscn.hasOriginPrj:\n\t\tif geoscn.hasCRS and not geoscn.hasOriginPrj:\n\t\t\trow.operator(\"geoscene.link_org_prj\", text=\"\", icon='CONSTRAINT')\n\t\tif geoscn.hasCRS and not geoscn.hasOriginGeo:\n\t\t\trow.operator(\"geoscene.link_org_geo\", text=\"\", icon='CONSTRAINT')\n\t\trow.operator(\"geoscene.clear_org\", text=\"\", icon='REMOVE')\n\n\tif not geoscn.hasOriginGeo and not geoscn.hasOriginPrj:\n\t\trow.operator(\"geoscene.init_org\", text=\"\", icon='ADD')\n\n\tif geoscn.hasOriginGeo and wm.geoscnProps.displayOriginGeo:\n\t\trow = layout.row()\n\t\trow.prop(wm.geoscnProps, 'lon', text='Lon')\n\t\trow.prop(wm.geoscnProps, 'lat', text='Lat')\n\t\t'''\n\t\trow.enabled = False\n\t\trow.prop(scn, '[\"'+SK.LON+'\"]', text='Lon')\n\t\trow.prop(scn, '[\"'+SK.LAT+'\"]', text='Lat')\n\t\t'''\n\n\tif  geoscn.hasOriginPrj and wm.geoscnProps.displayOriginPrj:\n\t\trow = layout.row()\n\t\trow.prop(wm.geoscnProps, 'crsx', text='X')\n\t\trow.prop(wm.geoscnProps, 'crsy', text='Y')\n\t\t'''\n\t\trow.enabled = False\n\t\trow.prop(scn, '[\"'+SK.CRSX+'\"]', text='X')\n\t\trow.prop(scn, '[\"'+SK.CRSY+'\"]', text='Y')\n\t\t'''\n\n\tif geoscn.hasScale:\n\t\trow = layout.row()\n\t\trow.label(text='Map scale:')\n\t\tcol = row.column()\n\t\tcol.enabled = False\n\t\tcol.prop(scn, '[\"'+SK.SCALE+'\"]', text='')\n\n\t#if geoscn.hasZoom:\n\t#\tlayout.prop(scn, '[\"'+SK.ZOOM+'\"]', text='Zoom level', slider=True)\n\n\n###########################\n\nclasses = [\n\tGEOSCENE_OT_coords_viewer,\n\tGEOSCENE_OT_set_crs,\n\tGEOSCENE_OT_init_org,\n\tGEOSCENE_OT_edit_org_geo,\n\tGEOSCENE_OT_edit_org_prj,\n\tGEOSCENE_OT_link_org_geo,\n\tGEOSCENE_OT_link_org_prj,\n\tGEOSCENE_OT_clear_org,\n\tGEOSCENE_OT_clear_georef,\n\tGEOSCENE_PT_georef,\n\tGLOBAL_PROPS\n]\n\n\ndef register():\n\tfor cls in classes:\n\t\ttry:\n\t\t\tbpy.utils.register_class(cls)\n\t\texcept ValueError as e:\n\t\t\tlog.warning('{} is already registered, now unregister and retry... '.format(cls))\n\t\t\tbpy.utils.unregister_class(cls)\n\t\t\tbpy.utils.register_class(cls)\n\tbpy.types.WindowManager.geoscnProps = PointerProperty(type=GLOBAL_PROPS)\n\ndef unregister():\n\tdel bpy.types.WindowManager.geoscnProps\n\tfor cls in classes:\n\t\tbpy.utils.unregister_class(cls)\n"
  },
  {
    "path": "issue_template.md",
    "content": "<!--\nPlease respect the issue template describe here :\nhttps://github.com/domlysz/BlenderGIS/issues/289\n\nIssues that does not respect the template will be automatically closed.\n\n**Mac users warning :** currently the addon does not work on Mac with Blender 2.80 to 2.82. Please do not report the issue here. It's already solved with Blender 2.83.\n -->\n\n# **Blender and OS versions**\n\n<!-- Tag here which version of Blender you are using and what's your operating system.-->\n\n# **Describe the bug**\n\n<!-- A clear and concise description of what the bug is. -->\n\n# **How to Reproduce**\n\n<!-- Steps, to reproduce the behavior. Screencasts or screenshots welcome -->\n\n# **Error message**\n\n<!--If the addon crash, report here the complete error message reported in the logs-->\n"
  },
  {
    "path": "operators/__init__.py",
    "content": "__all__ = [\"add_camera_exif\", \"add_camera_georef\", \"io_export_shp\", \"io_get_srtm\", \"io_import_georaster\", \"io_import_osm\", \"io_import_shp\", \"io_import_asc\", \"mesh_delaunay_voronoi\", \"nodes_terrain_analysis_builder\", \"nodes_terrain_analysis_reclassify\", \"view3d_mapviewer\", \"object_drop\", \"mesh_earth_sphere\"]\n"
  },
  {
    "path": "operators/add_camera_exif.py",
    "content": "# -*- coding:utf-8 -*-\n\n#  ***** GPL LICENSE BLOCK *****\n#\n#  This program is free software: you can redistribute it and/or modify\n#  it under the terms of the GNU General Public License as published by\n#  the Free Software Foundation, either version 3 of the License, or\n#  (at your option) any later version.\n#\n#  This program is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#  GNU General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public License\n#  along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#  All rights reserved.\n#  ***** GPL LICENSE BLOCK *****\n\nimport os\nfrom math import pi\n\nimport logging\nlog = logging.getLogger(__name__)\n\nimport bpy\nfrom bpy.props import StringProperty, CollectionProperty, EnumProperty\nfrom bpy.types import Panel, Operator, OperatorFileListElement\n\n#bgis\nfrom ..geoscene import GeoScene\n\n#core\nfrom ..core.proj import reprojPt\nfrom ..core.georaster import getImgFormat\n\n#deps\nfrom ..core.lib import Tyf\n\n\ndef newEmpty(scene, name, location):\n    \"\"\"Create a new empty\"\"\"\n    target = bpy.data.objects.new(name, None)\n    target.empty_display_size = 40\n    target.empty_display_type = 'PLAIN_AXES'\n    target.location = location\n    scene.collection.objects.link(target)\n    return target\n\ndef newCamera(scene, name, location, focalLength):\n    \"\"\"Create a new camera\"\"\"\n    cam = bpy.data.cameras.new(name)\n    cam.sensor_width = 35\n    cam.lens = focalLength\n    cam.display_size = 40\n    cam_obj = bpy.data.objects.new(name,cam)\n    cam_obj.location = location\n    cam_obj.rotation_euler[0] = pi/2\n    cam_obj.rotation_euler[2] = pi\n    scene.collection.objects.link(cam_obj)\n    return cam, cam_obj\n\ndef newTargetCamera(scene, name, location, focalLength):\n    \"\"\"Create a new camera.target\"\"\"\n    cam, cam_obj = newCamera(scene, name, location, focalLength)\n    x, y, z = location[:]\n    target = newEmpty(scene, name+\".target\", (x, y - 50, z))\n    constraint = cam_obj.constraints.new(type='TRACK_TO')\n    constraint.track_axis = 'TRACK_NEGATIVE_Z'\n    constraint.up_axis = 'UP_Y'\n    constraint.target = target\n    return cam, cam_obj\n\n\n\nclass CAMERA_OT_geophotos_add(Operator):\n    bl_idname = \"camera.geophotos\"\n    bl_description  = \"Create cameras from geotagged photos\"\n    bl_label = \"Exif cam\"\n    bl_options = {\"REGISTER\"}\n\n    files: CollectionProperty(\n            name=\"File Path\",\n            type=OperatorFileListElement,\n            )\n\n    directory: StringProperty(\n            subtype='DIR_PATH',\n            )\n\n    filter_glob: StringProperty(\n        default=\"*.jpg;*.jpeg;*.tif;*.tiff\",\n        options={'HIDDEN'},\n        )\n\n    filename_ext = \"\"\n\n    exifMode: EnumProperty(\n        attr=\"exif_mode\",\n        name=\"Action\",\n        description=\"Choose an action\",\n        items=[('TARGET_CAMERA','Target Camera','Create a camera with target helper'),('CAMERA','Camera','Create a camera'),('EMPTY','Empty','Create an empty helper'),('CURSOR','Cursor','Move cursor')],\n        default=\"TARGET_CAMERA\"\n        )\n\n\n    def invoke(self, context, event):\n        scn = context.scene\n        geoscn = GeoScene(scn)\n        if not geoscn.isGeoref:\n            self.report({'ERROR'},\"The scene must be georeferenced.\")\n            return {'CANCELLED'}\n        #File browser\n        context.window_manager.fileselect_add(self)\n        return {'RUNNING_MODAL'}\n\n    def execute(self, context):\n        scn = context.scene\n        geoscn = GeoScene(scn)\n        directory = self.directory\n        for file_elem in self.files:\n            filepath = os.path.join(directory, file_elem.name)\n\n            if not os.path.isfile(filepath):\n                self.report({'ERROR'},\"Invalid file\")\n                return {'CANCELLED'}\n\n            imgFormat = getImgFormat(filepath)\n            if imgFormat not in ['JPEG', 'TIFF']:\n                self.report({'ERROR'},\"Invalid format \" + str(imgFormat))\n                return {'CANCELLED'}\n\n            try:\n                exif = Tyf.open(filepath)\n            except Exception as e:\n                log.error(\"Unable to open file\", exc_info=True)\n                self.report({'ERROR'},\"Unable to open file. Checks logs for more infos.\")\n                return {'CANCELLED'}\n\n            #tags = {t.key:exif[t.key] for t in exif.exif.tags() if t.key != 'Unknown' }\n            #print(tags)\n\n            #Warning : Tyf object does not totally behave like a python dictionnary\n            #testing if a tags exists with the syntax \"if k in exif\" does not works\n            #using the get method does not work either. For example : alt = exif.get(\"GPSAltitude\", 0)\n            #that's why we proceed with \"try except KeyError\" blocks instead of conditional block or get() method\n\n            try: #if not any([k in exif for k in ('GPSLatitude', 'GPSLatitudeRef', 'GPSLongitude', 'GPSLongitudeRef')]):\n                lat = exif[\"GPSLatitude\"] * exif[\"GPSLatitudeRef\"]\n                lon = exif[\"GPSLongitude\"] * exif[\"GPSLongitudeRef\"]\n            except KeyError:\n                self.report({'ERROR'},\"Can't find GPS longitude or latitude.\")\n                return {'CANCELLED'}\n\n            #alt = exif.get(\"GPSAltitude\", 0)\n            try:\n                alt = exif[\"GPSAltitude\"]\n            except KeyError:\n                alt = 0\n\n            try:\n                x, y = reprojPt(4326, geoscn.crs, lon, lat)\n            except Exception as e:\n                log.error(\"Reprojection fails\", exc_info=True)\n                self.report({'ERROR'},\"Reprojection error. Check logs for more infos.\")\n                return {'CANCELLED'}\n\n            try:\n                focalLength = exif[\"FocalLengthIn35mmFilm\"]\n            except KeyError:\n                focalLength = 35\n\n            location = (x-geoscn.crsx, y-geoscn.crsy, alt)\n            name = bpy.path.display_name_from_filepath(filepath)\n            if self.exifMode == \"TARGET_CAMERA\":\n                cam, cam_obj = newTargetCamera(scn, name, location, focalLength)\n            elif self.exifMode == \"CAMERA\":\n                cam, cam_obj = newCamera(scn, name, location, focalLength)\n            elif self.exifMode == \"EMPTY\":\n                newEmpty(scn, name, location)\n            else:\n                scn.cursor.location = location\n\n\n            if self.exifMode in [\"TARGET_CAMERA\",\"CAMERA\"]:\n                cam['background']  = filepath\n\n                '''\n                try:\n                    cam['imageWidth']  = exif[\"PixelXDimension\"] #for jpg, in tif tag is named imageWidth...\n                    cam['imageHeight'] = exif[\"PixelYDimension\"]\n                except KeyError:\n                    pass\n                '''\n\n                img = bpy.data.images.load(filepath)\n                w, h = img.size\n                cam['imageWidth']  = w #exif[\"PixelXDimension\"] #for jpg, in tif file the tag is named imageWidth...\n                cam['imageHeight'] = h\n\n                try:\n                    cam['orientation'] = exif[\"Orientation\"]\n                except KeyError:\n                    cam['orientation'] = 1 #no rotation\n\n                #Set camera rotation (NOT TESTED)\n                if cam['orientation'] == 8: #90° CCW\n                    cam_obj.rotation_euler[1] -= pi/2\n                if cam['orientation'] == 6: #90° CW\n                    cam_obj.rotation_euler[1] += pi/2\n                if cam['orientation'] == 3: #180°\n                    cam_obj.rotation_euler[1] += pi\n\n                if scn.camera is None:\n                    bpy.ops.camera.geophotos_setactive('EXEC_DEFAULT', camLst=cam_obj.name)\n\n        return {'FINISHED'}\n\n\nclass CAMERA_OT_geophotos_setactive(Operator):\n    bl_idname = \"camera.geophotos_setactive\"\n    bl_description  = \"Switch active geophoto camera\"\n    bl_label = \"Switch geophoto camera\"\n    bl_options = {\"REGISTER\"}\n\n    def listGeoCam(self, context):\n        scn = context.scene\n        #put each object in a tuple (key, label, tooltip)\n        return [(obj.name, obj.name, obj.name) for obj in scn.objects if obj.type == 'CAMERA' and 'background' in obj.data]\n\n    camLst: EnumProperty(name='Camera', description='Select camera', items=listGeoCam)\n\n    def draw(self, context):\n        layout = self.layout\n        layout.prop(self, 'camLst')#, text='')\n\n    def invoke(self, context, event):\n        if len(self.camLst) == 0:\n            self.report({'ERROR'},\"No valid camera\")\n            return {'CANCELLED'}\n        return context.window_manager.invoke_props_dialog(self)#, width=200)\n\n    def execute(self, context):\n        if context.space_data.type != 'VIEW_3D':\n            self.report({'ERROR'},\"Wrong context\")\n            return {'CANCELLED'}\n\n        scn = context.scene\n        view3d = context.space_data\n\n        #Get cam\n        cam_obj = scn.objects[self.camLst]\n        cam_obj.select_set(True)\n        context.view_layer.objects.active = cam_obj\n        cam = cam_obj.data\n        scn.camera = cam_obj\n\n        #Set render size\n        scn.render.resolution_x = cam['imageWidth']\n        scn.render.resolution_y = cam['imageHeight']\n        scn.render.resolution_percentage = 100\n\n        #Get or load bpy image\n        filepath = cam['background']\n        try:\n            img = [img for img in bpy.data.images if img.filepath == filepath][0]\n        except IndexError:\n            img = bpy.data.images.load(filepath)\n\n        #Activate view3d background\n        cam.show_background_images = True\n\n        #Hide all existing camera background\n        for bkg in cam.background_images:\n            bkg.show_background_image = False\n\n        #Get or load background image\n        bkgs = [bkg for bkg in cam.background_images if bkg.image is not None]\n        try:\n            bkg = [bkg for bkg in bkgs if bkg.image.filepath == filepath][0]\n        except IndexError:\n            bkg = cam.background_images.new()\n            bkg.image = img\n\n        #Set some props\n        bkg.show_background_image = True\n        bkg.alpha = 1\n\n        return {'FINISHED'}\n\nclasses = [\n\tCAMERA_OT_geophotos_add,\n\tCAMERA_OT_geophotos_setactive\n]\n\ndef register():\n\tfor cls in classes:\n\t\ttry:\n\t\t\tbpy.utils.register_class(cls)\n\t\texcept ValueError as e:\n\t\t\tlog.warning('{} is already registered, now unregister and retry... '.format(cls))\n\t\t\tbpy.utils.unregister_class(cls)\n\t\t\tbpy.utils.register_class(cls)\n\ndef unregister():\n\tfor cls in classes:\n\t\tbpy.utils.unregister_class(cls)\n"
  },
  {
    "path": "operators/add_camera_georef.py",
    "content": "# -*- coding:utf-8 -*-\n\n#  ***** GPL LICENSE BLOCK *****\n#\n#  This program is free software: you can redistribute it and/or modify\n#  it under the terms of the GNU General Public License as published by\n#  the Free Software Foundation, either version 3 of the License, or\n#  (at your option) any later version.\n#\n#  This program is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#  GNU General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public License\n#  along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#  All rights reserved.\n#  ***** GPL LICENSE BLOCK *****\nimport logging\nlog = logging.getLogger(__name__)\n\nimport bpy\nfrom mathutils import Vector\nfrom bpy.props import StringProperty, BoolProperty, EnumProperty, FloatProperty\n\nfrom .utils import getBBOX\nfrom ..geoscene import GeoScene\n\n\n\nclass CAMERA_OT_add_georender_cam(bpy.types.Operator):\n\t'''\n\tAdd a new georef camera or update an existing one\n\tA georef camera is a top view orthographic camera that can be used to render a map\n\tThe camera is setting to encompass the selected object, the output spatial resolution (meters/pixel) can be set by the user\n\tA worldfile is writen in BLender text editor, it can be used to georef the output render\n\t'''\n\tbl_idname = \"camera.georender\"\n\tbl_label = \"Georef cam\"\n\tbl_description = \"Create or update a camera to render a georeferencing map\"\n\tbl_options = {\"REGISTER\", \"UNDO\"}\n\n\n\tname: StringProperty(name = \"Camera name\", default=\"Georef cam\", description=\"\")\n\ttarget_res: FloatProperty(name = \"Pixel size\", default=5, description=\"Pixel size in map units/pixel\", min=0.00001)\n\tzLocOffset: FloatProperty(name = \"Z loc. off.\", default=50, description=\"Camera z location offet, defined as percentage of z dimension of the target mesh\", min=0)\n\n\tredo = 0\n\tbbox = None #global var used to avoid recomputing the bbox at each redo\n\n\tdef check(self, context):\n\t\treturn True\n\n\n\tdef draw(self, context):\n\t\tlayout = self.layout\n\t\tlayout.prop(self, 'name')\n\t\tlayout.prop(self, 'target_res')\n\t\tlayout.prop(self, 'zLocOffset')\n\n\t@classmethod\n\tdef poll(cls, context):\n\t\treturn context.mode == 'OBJECT'\n\n\tdef execute(self, context):#every times operator redo options are modified\n\n\t\t#Operator redo count\n\t\tself.redo += 1\n\n\t\t#Check georef\n\t\tscn = context.scene\n\t\tgeoscn = GeoScene(scn)\n\t\tif not geoscn.isGeoref:\n\t\t\tself.report({'ERROR'}, \"Scene isn't georef\")\n\t\t\treturn {'CANCELLED'}\n\n\t\t#Validate selection\n\t\tobjs = bpy.context.selected_objects\n\t\tif (not objs or len(objs) > 2) or \\\n\t\t(len(objs) == 1 and not objs[0].type == 'MESH') or \\\n\t\t(len(objs) == 2 and not set( (objs[0].type, objs[1].type )) == set( ('MESH','CAMERA') ) ):\n\t\t\tself.report({'ERROR'}, \"Pre-selection is incorrect\")\n\t\t\treturn {'CANCELLED'}\n\n\t\t#Flag new camera creation\n\t\tif len(objs) == 2:\n\t\t\tnewCam = False\n\t\telse:\n\t\t\tnewCam = True\n\n\t\t#Get georef data\n\t\tdx, dy = geoscn.getOriginPrj()\n\n\t\t#Allocate obj\n\t\tfor obj in objs:\n\t\t\tif obj.type == 'MESH':\n\t\t\t\tgeorefObj = obj\n\t\t\telif obj.type == 'CAMERA':\n\t\t\t\tcamObj = obj\n\t\t\t\tcam = camObj.data\n\n\t\t#do not recompute bbox at operator redo because zdim is miss-evaluated\n\t\t#when redoing the op on an obj that have a displace modifier on it\n\t\t#TODO find a less hacky fix\n\t\tif self.bbox is None:\n\t\t\tbbox = getBBOX.fromObj(georefObj, applyTransform = True)\n\t\t\tself.bbox = bbox\n\t\telse:\n\t\t\tbbox = self.bbox\n\n\t\tlocx, locy, locz = bbox.center\n\t\tdimx, dimy, dimz = bbox.dimensions\n\t\tif dimz == 0:\n\t\t\tdimz = 1\n\t\t#dimx, dimy, dimz = georefObj.dimensions #dimensions property apply object transformations (scale and rot.)\n\n\t\t#Set active cam\n\t\tif newCam:\n\t\t\tcam = bpy.data.cameras.new(name=self.name)\n\t\t\tcam['mapRes'] = self.target_res #custom prop\n\t\t\tcamObj = bpy.data.objects.new(name=self.name, object_data=cam)\n\t\t\tscn.collection.objects.link(camObj)\n\t\t\tscn.camera = camObj\n\t\telif self.redo == 1: #first exec, get initial camera res\n\t\t\tscn.camera = camObj\n\t\t\ttry:\n\t\t\t\tself.target_res = cam['mapRes']\n\t\t\texcept KeyError:\n\t\t\t\tself.report({'ERROR'}, \"This camera has not map resolution property\")\n\t\t\t\treturn {'CANCELLED'}\n\t\telse: #following exec, set camera res in redo panel\n\t\t\ttry:\n\t\t\t\tcam['mapRes'] = self.target_res\n\t\t\texcept KeyError:\n\t\t\t\tself.report({'ERROR'}, \"This camera has not map resolution property\")\n\t\t\t\treturn {'CANCELLED'}\n\n\t\t#Set camera data\n\t\tcam.type = 'ORTHO'\n\t\tcam.ortho_scale = max((dimx, dimy)) #ratio = max((dimx, dimy)) / min((dimx, dimy))\n\n\t\t#General offset used to set cam z loc and clip end distance\n\t\t#needed to avoid clipping/black hole effects\n\t\toffset = dimz * self.zLocOffset/100\n\n\t\t#Set camera location\n\t\tcamLocZ = bbox['zmin'] + dimz + offset\n\t\tcamObj.location = (locx, locy, camLocZ)\n\n\t\t#Set camera clipping\n\t\tcam.clip_start = 0\n\t\tcam.clip_end = dimz + offset*2\n\t\tcam.show_limits = True\n\n\t\tif not newCam:\n\t\t\tif self.redo == 1:#first exec, get initial camera name\n\t\t\t\tself.name = camObj.name\n\t\t\telse:#following exec, set camera name in redo panel\n\t\t\t\tcamObj.name = self.name\n\t\t\t\tcamObj.data.name = self.name\n\n\t\t#Update selection\n\t\tbpy.ops.object.select_all(action='DESELECT')\n\t\tcamObj.select_set(True)\n\t\tcontext.view_layer.objects.active = camObj\n\n\t\t#setup scene\n\t\tscn.camera = camObj\n\t\tscn.render.resolution_x = int(dimx / self.target_res)\n\t\tscn.render.resolution_y = int(dimy / self.target_res)\n\t\tscn.render.resolution_percentage = 100\n\n\t\t#Write wf\n\t\tres = self.target_res#dimx / scene.render.resolution_x\n\t\trot = 0\n\t\tx = bbox['xmin'] + dx\n\t\ty = bbox['ymax'] + dy\n\t\twf_data = '\\n'.join(map(str, [res, rot, rot, -res, x+res/2, y-res/2]))\n\t\twf_name = camObj.name + '.wld'\n\t\tif wf_name in bpy.data.texts:\n\t\t\twfText = bpy.data.texts[wf_name]\n\t\t\twfText.clear()\n\t\telse:\n\t\t\twfText = bpy.data.texts.new(name=wf_name)\n\t\twfText.write(wf_data)\n\n\t\t#Purge old wf text\n\t\tfor wfText in bpy.data.texts:\n\t\t\tname, ext = wfText.name[:-4], wfText.name[-4:]\n\t\t\tif ext == '.wld' and name not in bpy.data.objects:\n\t\t\t\tbpy.data.texts.remove(wfText)\n\n\t\treturn {'FINISHED'}\n\n\ndef register():\n\ttry:\n\t\tbpy.utils.register_class(CAMERA_OT_add_georender_cam)\n\texcept ValueError as e:\n\t\tlog.warning('{} is already registered, now unregister and retry... '.format(CAMERA_OT_add_georender_cam))\n\t\tunregister()\n\t\tbpy.utils.register_class(CAMERA_OT_add_georender_cam)\n\n\ndef unregister():\n\tbpy.utils.unregister_class(CAMERA_OT_add_georender_cam)\n"
  },
  {
    "path": "operators/io_export_shp.py",
    "content": "# -*- coding:utf-8 -*-\nimport os\nimport bpy\nimport bmesh\nimport mathutils\n\nimport logging\nlog = logging.getLogger(__name__)\n\nfrom ..core.lib.shapefile import Writer as shpWriter\nfrom ..core.lib.shapefile import POINTZ, POLYLINEZ, POLYGONZ, MULTIPOINTZ\n\nfrom bpy_extras.io_utils import ExportHelper #helper class defines filename and invoke() function which calls the file selector\nfrom bpy.props import StringProperty, BoolProperty, EnumProperty, IntProperty\nfrom bpy.types import Operator\n\nfrom ..geoscene import GeoScene\n\nfrom ..core.proj import SRS\n\nclass EXPORTGIS_OT_shapefile(Operator, ExportHelper):\n\t\"\"\"Export from ESRI shapefile file format (.shp)\"\"\"\n\tbl_idname = \"exportgis.shapefile\" # important since its how bpy.ops.import.shapefile is constructed (allows calling operator from python console or another script)\n\t#bl_idname rules: must contain one '.' (dot) charactere, no capital letters, no reserved words (like 'import')\n\tbl_description = 'export to ESRI shapefile file format (.shp)'\n\tbl_label = \"Export SHP\"\n\tbl_options = {\"UNDO\"}\n\n\n\t# ExportHelper class properties\n\tfilename_ext = \".shp\"\n\tfilter_glob: StringProperty(\n\t\t\tdefault = \"*.shp\",\n\t\t\toptions = {'HIDDEN'},\n\t\t\t)\n\n\texportType: EnumProperty(\n\t\t\tname = \"Feature type\",\n\t\t\tdescription = \"Select feature type\",\n\t\t\titems = [\n\t\t\t\t('POINTZ', 'Point', \"\"),\n\t\t\t\t('POLYLINEZ', 'Line', \"\"),\n\t\t\t\t('POLYGONZ', 'Polygon', \"\")\n\t\t\t])\n\n\tobjectsSource: EnumProperty(\n\t\t\tname = \"Objects\",\n\t\t\tdescription = \"Objects to export\",\n\t\t\titems = [\n\t\t\t\t('COLLEC', 'Collection', \"Export a collection of objects\"),\n\t\t\t\t('SELECTED', 'Selected objects', \"Export the current selection\")\n\t\t\t],\n\t\t\tdefault = 'SELECTED'\n\t\t\t)\n\n\tdef listCollections(self, context):\n\t\treturn [(c.name, c.name, \"Collection\") for c in bpy.data.collections]\n\n\tselectedColl: EnumProperty(\n\t\tname = \"Collection\",\n\t\tdescription = \"Select the collection to export\",\n\t\titems = listCollections)\n\n\tmode: EnumProperty(\n\t\t\tname = \"Mode\",\n\t\t\tdescription = \"Select the export strategy\",\n\t\t\titems = [\n\t\t\t\t('OBJ2FEAT', 'Objects to features', \"Create one multipart feature per object\"),\n\t\t\t\t('MESH2FEAT', 'Mesh to features', \"Decompose mesh primitives to separate features\")\n\t\t\t],\n\t\t\tdefault = 'OBJ2FEAT'\n\t\t\t)\n\n\n\t@classmethod\n\tdef poll(cls, context):\n\t\treturn context.mode == 'OBJECT'\n\n\tdef draw(self, context):\n\t\t#Function used by blender to draw the panel.\n\t\tlayout = self.layout\n\t\tlayout.prop(self, 'objectsSource')\n\t\tif self.objectsSource == 'COLLEC':\n\t\t\tlayout.prop(self, 'selectedColl')\n\t\tlayout.prop(self, 'mode')\n\t\tlayout.prop(self, 'exportType')\n\n\tdef execute(self, context):\n\t\tfilePath = self.filepath\n\t\tfolder = os.path.dirname(filePath)\n\t\tscn = context.scene\n\t\tgeoscn = GeoScene(scn)\n\n\t\tif geoscn.isGeoref:\n\t\t\tdx, dy = geoscn.getOriginPrj()\n\t\t\tcrs = SRS(geoscn.crs)\n\t\t\ttry:\n\t\t\t\twkt = crs.getWKT()\n\t\t\texcept Exception as e:\n\t\t\t\tlog.warning('Cannot convert crs to wkt', exc_info=True)\n\t\t\t\twkt = None\n\t\telif geoscn.isBroken:\n\t\t\tself.report({'ERROR'}, \"Scene georef is broken, please fix it beforehand\")\n\t\t\treturn {'CANCELLED'}\n\t\telse:\n\t\t\tdx, dy = (0, 0)\n\t\t\twkt = None\n\n\t\tif self.objectsSource == 'SELECTED':\n\t\t\tobjects = [obj for obj in bpy.context.selected_objects if obj.type == 'MESH']\n\t\telif self.objectsSource == 'COLLEC':\n\t\t\tobjects = bpy.data.collections[self.selectedColl].all_objects\n\t\t\tobjects = [obj for obj in objects if obj.type == 'MESH']\n\n\t\tif not objects:\n\t\t\tself.report({'ERROR'}, \"Selection is empty or does not contain any mesh\")\n\t\t\treturn {'CANCELLED'}\n\n\n\t\toutShp = shpWriter(filePath)\n\t\tif self.exportType == 'POLYGONZ':\n\t\t\toutShp.shapeType = POLYGONZ #15\n\t\tif self.exportType == 'POLYLINEZ':\n\t\t\toutShp.shapeType = POLYLINEZ #13\n\t\tif self.exportType == 'POINTZ' and self.mode == 'MESH2FEAT':\n\t\t\toutShp.shapeType = POINTZ\n\t\tif self.exportType == 'POINTZ' and self.mode == 'OBJ2FEAT':\n\t\t\toutShp.shapeType = MULTIPOINTZ\n\n\t\t#create fields (all needed fields sould be created before adding any new record)\n\t\t#TODO more robust evaluation, and check for boolean and date types\n\t\tcLen = 255 #string fields default length\n\t\tnLen = 20 #numeric fields default length\n\t\tdLen = 5 #numeric fields default decimal precision\n\t\tmaxFieldNameLen = 8 #shp capabilities limit field name length to 8 characters\n\t\toutShp.field('objId','N', nLen) #export id\n\t\tfor obj in objects:\n\t\t\tfor k, v in obj.items():\n\t\t\t\tk = k[0:maxFieldNameLen]\n\t\t\t\t#evaluate the field type with the first value\n\t\t\t\tif k not in [f[0] for f in outShp.fields]:\n\t\t\t\t\tif isinstance(v, float) or isinstance(v, int):\n\t\t\t\t\t\tfieldType = 'N'\n\t\t\t\t\telif isinstance(v, str):\n\t\t\t\t\t\tif v.lstrip(\"-+\").isdigit():\n\t\t\t\t\t\t\tv = int(v)\n\t\t\t\t\t\t\tfieldType = 'N'\n\t\t\t\t\t\telse:\n\t\t\t\t\t\t\ttry:\n\t\t\t\t\t\t\t\tv = float(v)\n\t\t\t\t\t\t\texcept ValueError:\n\t\t\t\t\t\t\t\tfieldType = 'C'\n\t\t\t\t\t\t\telse:\n\t\t\t\t\t\t\t\tfieldType = 'N'\n\t\t\t\t\telse:\n\t\t\t\t\t\tcontinue\n\n\t\t\t\t\tif fieldType == 'C':\n\t\t\t\t\t\toutShp.field(k, fieldType, cLen)\n\t\t\t\t\telif fieldType == 'N':\n\t\t\t\t\t\tif isinstance(v, int):\n\t\t\t\t\t\t\toutShp.field(k, fieldType, nLen, 0)\n\t\t\t\t\t\telse:\n\t\t\t\t\t\t\toutShp.field(k, fieldType, nLen, dLen)\n\n\n\t\tfor i, obj in enumerate(objects):\n\n\t\t\tloc = obj.location\n\t\t\tbm = bmesh.new()\n\t\t\tbm.from_object(obj, context.evaluated_depsgraph_get())\n\t\t\t#bmesh.from_object 'deform=True' arg allows to consider modifier deformation ->> deprecated since Blender 3.0\n\t\t\tbm.transform(obj.matrix_world)\n\n\t\t\tnFeat = 1\n\n\t\t\tif self.exportType == 'POINTZ':\n\t\t\t\tif len(bm.verts) == 0:\n\t\t\t\t\tcontinue\n\n\t\t\t\t#Extract coords & adjust values against georef deltas\n\t\t\t\tpts = [[v.co.x+dx, v.co.y+dy, v.co.z] for v in bm.verts]\n\n\n\t\t\t\tif self.mode == 'MESH2FEAT':\n\t\t\t\t\tfor j, pt in enumerate(pts):\n\t\t\t\t\t\toutShp.pointz(*pt)\n\t\t\t\t\tnFeat = len(pts)\n\t\t\t\telif self.mode == 'OBJ2FEAT':\n\t\t\t\t\toutShp.multipointz(pts)\n\n\n\t\t\tif self.exportType == 'POLYLINEZ':\n\n\t\t\t\tif len(bm.edges) == 0:\n\t\t\t\t\tcontinue\n\n\t\t\t\tlines = []\n\t\t\t\tfor edge in bm.edges:\n\t\t\t\t\t#Extract coords & adjust values against georef deltas\n\t\t\t\t\tline = [(vert.co.x+dx, vert.co.y+dy, vert.co.z) for vert in edge.verts]\n\t\t\t\t\tlines.append(line)\n\n\t\t\t\tif self.mode == 'MESH2FEAT':\n\t\t\t\t\tfor j, line in enumerate(lines):\n\t\t\t\t\t\toutShp.linez([line])\n\t\t\t\t\tnFeat = len(lines)\n\t\t\t\telif self.mode == 'OBJ2FEAT':\n\t\t\t\t\toutShp.linez(lines)\n\n\n\t\t\tif self.exportType == 'POLYGONZ':\n\n\t\t\t\tif len(bm.faces) == 0:\n\t\t\t\t\tcontinue\n\n\t\t\t\t#build geom\n\t\t\t\tpolygons = []\n\t\t\t\tfor face in bm.faces:\n\t\t\t\t\t#Extract coords & adjust values against georef deltas\n\t\t\t\t\tpoly = [(vert.co.x+dx, vert.co.y+dy, vert.co.z) for vert in face.verts]\n\t\t\t\t\tpoly.append(poly[0])#close poly\n\t\t\t\t\t#In Blender face is up if points are in anticlockwise order\n\t\t\t\t\t#for shapefiles, face's up with clockwise order\n\t\t\t\t\tpoly.reverse()\n\t\t\t\t\tpolygons.append(poly)\n\n\t\t\t\tif self.mode == 'MESH2FEAT':\n\t\t\t\t\tfor j, polygon in enumerate(polygons):\n\t\t\t\t\t\toutShp.polyz([polygon])\n\t\t\t\t\tnFeat = len(polygons)\n\t\t\t\telif self.mode == 'OBJ2FEAT':\n\t\t\t\t\toutShp.polyz(polygons)\n\n\n\t\t\t#Writing attributes Data\n\t\t\tattributes = {'objId':i}\n\t\t\tfor k, v in obj.items():\n\t\t\t\tk = k[0:maxFieldNameLen]\n\t\t\t\tif not any([f[0] == k for f in outShp.fields]):\n\t\t\t\t\tcontinue\n\t\t\t\tfType = next( (f[1] for f in outShp.fields if f[0] == k) )\n\t\t\t\tif fType in ('N', 'F'):\n\t\t\t\t\ttry:\n\t\t\t\t\t\tv = float(v)\n\t\t\t\t\texcept ValueError:\n\t\t\t\t\t\tlog.info('Cannot cast value {} to float for appending field {}, NULL value will be inserted instead'.format(v, k))\n\t\t\t\t\t\tv = None\n\t\t\t\tattributes[k] = v\n\t\t\t#assign None to orphans shp fields (if the key does not exists in the custom props of this object)\n\t\t\tattributes.update({f[0]:None for f in outShp.fields if f[0] not in attributes.keys()})\n\t\t\t#Write\n\t\t\tfor n in range(nFeat):\n\t\t\t\toutShp.record(**attributes)\n\n\n\t\toutShp.close()\n\n\t\tif wkt is not None:\n\t\t\tprjPath = os.path.splitext(filePath)[0] + '.prj'\n\t\t\tprj = open(prjPath, \"w\")\n\t\t\tprj.write(wkt)\n\t\t\tprj.close()\n\n\t\tself.report({'INFO'}, \"Export complete\")\n\n\t\treturn {'FINISHED'}\n\n\ndef register():\n\ttry:\n\t\tbpy.utils.register_class(EXPORTGIS_OT_shapefile)\n\texcept ValueError as e:\n\t\tlog.warning('{} is already registered, now unregister and retry... '.format(EXPORTGIS_OT_shapefile))\n\t\tunregister()\n\t\tbpy.utils.register_class(EXPORTGIS_OT_shapefile)\n\ndef unregister():\n\tbpy.utils.unregister_class(EXPORTGIS_OT_shapefile)\n"
  },
  {
    "path": "operators/io_get_dem.py",
    "content": "import os\nimport time\n\nimport logging\nlog = logging.getLogger(__name__)\n\nfrom urllib.request import Request, urlopen\nfrom urllib.error import URLError, HTTPError\n\nimport bpy\nimport bmesh\nfrom bpy.types import Operator, Panel, AddonPreferences\nfrom bpy.props import StringProperty, IntProperty, FloatProperty, BoolProperty, EnumProperty, FloatVectorProperty\n\nfrom ..geoscene import GeoScene\nfrom .utils import adjust3Dview, getBBOX, isTopView\nfrom ..core.proj import SRS, reprojBbox\n\nfrom ..core import settings\nUSER_AGENT = settings.user_agent\n\nPKG, SUBPKG = __package__.split('.', maxsplit=1)\n\nTIMEOUT = 120\n\nclass IMPORTGIS_OT_dem_query(Operator):\n\t\"\"\"Import elevation data from a web service\"\"\"\n\n\tbl_idname = \"importgis.dem_query\"\n\tbl_description = 'Query for elevation data from a web service'\n\tbl_label = \"Get elevation (SRTM)\"\n\tbl_options = {\"UNDO\"}\n\n\tdef invoke(self, context, event):\n\n\t\t#check georef\n\t\tgeoscn = GeoScene(context.scene)\n\t\tif not geoscn.isGeoref:\n\t\t\t\tself.report({'ERROR'}, \"Scene is not georef\")\n\t\t\t\treturn {'CANCELLED'}\n\t\tif geoscn.isBroken:\n\t\t\t\tself.report({'ERROR'}, \"Scene georef is broken, please fix it beforehand\")\n\t\t\t\treturn {'CANCELLED'}\n\n\t\t#return self.execute(context)\n\t\treturn context.window_manager.invoke_props_dialog(self)#, width=350)\n\n\tdef draw(self,context):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\tlayout = self.layout\n\t\trow = layout.row(align=True)\n\t\trow.prop(prefs, \"demServer\", text='Server')\n\t\tif 'opentopography' in prefs.demServer:\n\t\t\trow = layout.row(align=True)\n\t\t\trow.prop(prefs, \"opentopography_api_key\", text='Api Key')\n\n\t@classmethod\n\tdef poll(cls, context):\n\t\treturn context.mode == 'OBJECT'\n\n\tdef execute(self, context):\n\n\t\tprefs = bpy.context.preferences.addons[PKG].preferences\n\t\tscn = context.scene\n\t\tgeoscn = GeoScene(scn)\n\t\tcrs = SRS(geoscn.crs)\n\n\t\t#Validate selection\n\t\tobjs = bpy.context.selected_objects\n\t\taObj = context.active_object\n\t\tif len(objs) == 1 and aObj.type == 'MESH':\n\t\t\tonMesh = True\n\t\t\tbbox = getBBOX.fromObj(aObj).toGeo(geoscn)\n\t\telif isTopView(context):\n\t\t\tonMesh = False\n\t\t\tbbox = getBBOX.fromTopView(context).toGeo(geoscn)\n\t\telse:\n\t\t\tself.report({'ERROR'}, \"Please define the query extent in orthographic top view or by selecting a reference object\")\n\t\t\treturn {'CANCELLED'}\n\n\t\tif bbox.dimensions.x > 1000000 or bbox.dimensions.y > 1000000:\n\t\t\tself.report({'ERROR'}, \"Too large extent\")\n\t\t\treturn {'CANCELLED'}\n\n\t\tbbox = reprojBbox(geoscn.crs, 4326, bbox)\n\n\t\tif 'SRTM' in prefs.demServer:\n\t\t\tif bbox.ymin > 60:\n\t\t\t\tself.report({'ERROR'}, \"SRTM is not available beyond 60 degrees north\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\tif bbox.ymax < -56:\n\t\t\t\tself.report({'ERROR'}, \"SRTM is not available below 56 degrees south\")\n\t\t\t\treturn {'CANCELLED'}\n\n\t\tif 'opentopography' in prefs.demServer:\n\t\t\tif not prefs.opentopography_api_key:\n\t\t\t\tself.report({'ERROR'}, \"Please register to opentopography.org and request for an API key\")\n\t\t\t\treturn {'CANCELLED'}\n\n\t\t#Set cursor representation to 'loading' icon\n\t\tw = context.window\n\t\tw.cursor_set('WAIT')\n\n\t\t#url template\n\t\t#http://opentopo.sdsc.edu/otr/getdem?demtype=SRTMGL3&west=-120.168457&south=36.738884&east=-118.465576&north=38.091337&outputFormat=GTiff\n\t\te = 0.002 #opentopo service does not always respect the entire bbox, so request for a little more\n\t\txmin, xmax = bbox.xmin - e, bbox.xmax + e\n\t\tymin, ymax = bbox.ymin - e, bbox.ymax + e\n\n\t\turl = prefs.demServer.format(W=xmin, E=xmax, S=ymin, N=ymax, API_KEY=prefs.opentopography_api_key)\n\t\tlog.debug(url)\n\n\t\t# Download the file from url and save it locally\n\t\t# opentopo return a geotiff object in wgs84\n\t\tif bpy.data.is_saved:\n\t\t\tfilePath = os.path.join(os.path.dirname(bpy.data.filepath), 'srtm.tif')\n\t\telse:\n\t\t\tfilePath = os.path.join(bpy.app.tempdir, 'srtm.tif')\n\n\t\t#we can directly init NpImg from blob but if gdal is not used as image engine then georef will not be extracted\n\t\t#Alternatively, we can save on disk, open with GeoRaster class (will use tyf if gdal not available)\n\t\trq = Request(url, headers={'User-Agent': USER_AGENT})\n\t\ttry:\n\t\t\twith urlopen(rq, timeout=TIMEOUT) as response, open(filePath, 'wb') as outFile:\n\t\t\t\tdata = response.read() # a `bytes` object\n\t\t\t\toutFile.write(data) #\n\t\texcept (URLError, HTTPError) as err:\n\t\t\tlog.error('Http request fails url:{}, code:{}, error:{}'.format(url, getattr(err, 'code', None), err.reason))\n\t\t\tself.report({'ERROR'}, \"Cannot reach OpenTopography web service, check logs for more infos\")\n\t\t\treturn {'CANCELLED'}\n\t\texcept TimeoutError:\n\t\t\tlog.error('Http request does not respond. url:{}, code:{}, error:{}'.format(url, getattr(err, 'code', None), err.reason))\n\t\t\tinfo = \"Cannot reach SRTM web service provider, server can be down or overloaded. Please retry later\"\n\t\t\tlog.info(info)\n\t\t\tself.report({'ERROR'}, info)\n\t\t\treturn {'CANCELLED'}\n\n\t\tif not onMesh:\n\t\t\tbpy.ops.importgis.georaster(\n\t\t\t'EXEC_DEFAULT',\n\t\t\tfilepath = filePath,\n\t\t\treprojection = True,\n\t\t\trastCRS = 'EPSG:4326',\n\t\t\timportMode = 'DEM',\n\t\t\tsubdivision = 'subsurf',\n\t\t\tdemInterpolation = True)\n\t\telse:\n\t\t\tbpy.ops.importgis.georaster(\n\t\t\t'EXEC_DEFAULT',\n\t\t\tfilepath = filePath,\n\t\t\treprojection = True,\n\t\t\trastCRS = 'EPSG:4326',\n\t\t\timportMode = 'DEM',\n\t\t\tsubdivision = 'subsurf',\n\t\t\tdemInterpolation = True,\n\t\t\tdemOnMesh = True,\n\t\t\tobjectsLst = [str(i) for i, obj in enumerate(scn.collection.all_objects) if obj.name == bpy.context.active_object.name][0],\n\t\t\tclip = False,\n\t\t\tfillNodata = False)\n\n\t\tbbox = getBBOX.fromScn(scn)\n\t\tadjust3Dview(context, bbox, zoomToSelect=False)\n\n\t\treturn {'FINISHED'}\n\n\ndef register():\n\ttry:\n\t\tbpy.utils.register_class(IMPORTGIS_OT_dem_query)\n\texcept ValueError as e:\n\t\tlog.warning('{} is already registered, now unregister and retry... '.format(IMPORTGIS_OT_srtm_query))\n\t\tunregister()\n\t\tbpy.utils.register_class(IMPORTGIS_OT_dem_query)\n\ndef unregister():\n\tbpy.utils.unregister_class(IMPORTGIS_OT_dem_query)\n"
  },
  {
    "path": "operators/io_import_asc.py",
    "content": "# Derived from https://github.com/hrbaer/Blender-ASCII-Grid-Import\n\nimport re\nimport os\nimport string\nimport bpy\nimport math\nimport string\n\nimport logging\nlog = logging.getLogger(__name__)\n\nfrom bpy_extras.io_utils import ImportHelper #helper class defines filename and invoke() function which calls the file selector\nfrom bpy.props import StringProperty, BoolProperty, EnumProperty, IntProperty\nfrom bpy.types import Operator\n\nfrom ..core.proj import Reproj\nfrom ..core.utils import XY\nfrom ..geoscene import GeoScene, georefManagerLayout\nfrom ..prefs import PredefCRS\n\nfrom .utils import bpyGeoRaster as GeoRaster\nfrom .utils import placeObj, adjust3Dview, showTextures, addTexture, getBBOX\nfrom .utils import rasterExtentToMesh, geoRastUVmap, setDisplacer\n\nPKG, SUBPKG = __package__.split('.', maxsplit=1)\n\n\nclass IMPORTGIS_OT_ascii_grid(Operator, ImportHelper):\n    \"\"\"Import ESRI ASCII grid file\"\"\"\n    bl_idname = \"importgis.asc_file\"  # important since its how bpy.ops.importgis.asc is constructed (allows calling operator from python console or another script)\n    #bl_idname rules: must contain one '.' (dot) charactere, no capital letters, no reserved words (like 'import')\n    bl_description = 'Import ESRI ASCII grid with world file'\n    bl_label = \"Import ASCII Grid\"\n    bl_options = {\"UNDO\"}\n\n    # ImportHelper class properties\n    filter_glob: StringProperty(\n        default=\"*.asc;*.grd\",\n        options={'HIDDEN'},\n    )\n\n    # Raster CRS definition\n    def listPredefCRS(self, context):\n        return PredefCRS.getEnumItems()\n    fileCRS: EnumProperty(\n        name = \"CRS\",\n        description = \"Choose a Coordinate Reference System\",\n        items = listPredefCRS,\n    )\n\n    # List of operator properties, the attributes will be assigned\n    # to the class instance from the operator settings before calling.\n    importMode: EnumProperty(\n        name = \"Mode\",\n        description = \"Select import mode\",\n        items = [\n            ('MESH', 'Mesh', \"Create triangulated regular network mesh\"),\n            ('CLOUD', 'Point cloud', \"Create vertex point cloud\"),\n        ],\n    )\n\n    # Step makes point clouds with billions of points possible to read on consumer hardware\n    step: IntProperty(\n        name = \"Step\",\n        description = \"Only read every Nth point for massive point clouds\",\n        default = 1,\n        min = 1\n    )\n\n    # Let the user decide whether to use the faster newline method\n    # Alternatively, use self.total_newlines(filename) to see whether total >= nrows and automatically decide (at the cost of time spent counting lines)\n    newlines: BoolProperty(\n        name = \"Newline-delimited rows\",\n        description = \"Use this method if the file contains newline separated rows for faster import\",\n        default = True,\n    )\n\n    def draw(self, context):\n        #Function used by blender to draw the panel.\n        layout = self.layout\n        layout.prop(self, 'importMode')\n        layout.prop(self, 'step')\n        layout.prop(self, 'newlines')\n\n        row = layout.row(align=True)\n        split = row.split(factor=0.35, align=True)\n        split.label(text='CRS:')\n        split.prop(self, \"fileCRS\", text='')\n        row.operator(\"bgis.add_predef_crs\", text='', icon='ADD')\n        scn = bpy.context.scene\n        geoscn = GeoScene(scn)\n        if geoscn.isPartiallyGeoref:\n            georefManagerLayout(self, context)\n\n\n    def total_lines(self, filename):\n        \"\"\"\n        Count newlines in file.\n        512MB file ~3 seconds.\n        \"\"\"\n        with open(filename) as f:\n            lines = 0\n            for _ in f:\n                lines += 1\n            return lines\n\n    def read_row_newlines(self, f, ncols):\n        \"\"\"\n        Read a row by columns separated by newline.\n        \"\"\"\n        return f.readline().split()\n\n    def read_row_whitespace(self, f, ncols):\n        \"\"\"\n        Read a row by columns separated by whitespace (including newlines).\n        6x slower than readlines() method but faster than any other method I can come up with. See commit 4d337c4 for alternatives.\n        \"\"\"\n        # choose a buffer that requires the least reads, but not too much memory (32MB max)\n        # cols * 6 allows us 5 chars plus space, approximating values such as '12345', '-1234', '12.34', '-12.3'\n        buf_size = min(1024 * 32, ncols * 6)\n        row = []\n        read_f = f.read\n        while True:\n            chunk = read_f(buf_size)\n\n            # assuming we read a complete chunk, remove end of string up to last whitespace to avoid partial values\n            # if the chunk is smaller than our buffer size, then we've read to the end of file and\n            #   can skip truncating the chunk since we know the last value will be complete\n            if len(chunk) == buf_size:\n                for i in range(len(chunk) - 1, -1, -1):\n                    if chunk[i].isspace():\n                        f.seek(f.tell() - (len(chunk) - i))\n                        chunk = chunk[:i]\n                        break\n\n            # either read was EOF or chunk was all whitespace\n            if not chunk:\n                return row  # eof without reaching ncols?\n\n            # find each value separated by any whitespace char\n            for m in re.finditer('([^\\s]+)', chunk):\n                row.append(m.group(0))\n                if len(row) == ncols:\n                    # completed a row within this chunk, rewind the position to start at the beginning of the next row\n                    f.seek(f.tell() - (len(chunk) - m.end()))\n                    return row\n\n    @classmethod\n    def poll(cls, context):\n        return context.mode == 'OBJECT'\n\n    def execute(self, context):\n        prefs = context.preferences.addons[PKG].preferences\n        bpy.ops.object.select_all(action='DESELECT')\n        #Get scene and some georef data\n        scn = bpy.context.scene\n        geoscn = GeoScene(scn)\n        if geoscn.isBroken:\n            self.report({'ERROR'}, \"Scene georef is broken, please fix it beforehand\")\n            return {'CANCELLED'}\n        if geoscn.isGeoref:\n            dx, dy = geoscn.getOriginPrj()\n        scale = geoscn.scale #TODO\n        if not geoscn.hasCRS:\n            try:\n                geoscn.crs = self.fileCRS\n            except Exception as e:\n                log.error(\"Cannot set scene crs\", exc_info=True)\n                self.report({'ERROR'}, \"Cannot set scene crs, check logs for more infos\")\n                return {'CANCELLED'}\n\n        #build reprojector objects\n        if geoscn.crs != self.fileCRS:\n            rprj = True\n            rprjToRaster = Reproj(geoscn.crs, self.fileCRS)\n            rprjToScene = Reproj(self.fileCRS, geoscn.crs)\n        else:\n            rprj = False\n            rprjToRaster = None\n            rprjToScene = None\n\n        #Path\n        filename = self.filepath\n        name = os.path.splitext(os.path.basename(filename))[0]\n        log.info('Importing {}...'.format(filename))\n\n        f = open(filename, 'r')\n        meta_re = re.compile('^([^\\s]+)\\s+([^\\s]+)$')  # 'abc  123'\n        meta = {}\n        for i in range(6):\n            line = f.readline()\n            m = meta_re.match(line)\n            if m:\n                meta[m.group(1).lower()] = m.group(2)\n        log.debug(meta)\n\n        # step allows reduction during import, only taking every Nth point\n        step = self.step\n        nrows = int(meta['nrows'])\n        ncols = int(meta['ncols'])\n        cellsize = float(meta['cellsize'])\n        nodata = float(meta['nodata_value'])\n\n        # options are lower left cell corner, or lower left cell centre\n        reprojection = {}\n        offset = XY(0, 0)\n        if 'xllcorner' in meta:\n            llcorner = XY(float(meta['xllcorner']), float(meta['yllcorner']))\n            reprojection['from'] = llcorner\n        elif 'xllcenter' in meta:\n            centre = XY(float(meta['xllcenter']), float(meta['yllcenter']))\n            offset = XY(-cellsize / 2, -cellsize / 2)\n            reprojection['from'] = centre\n\n        # now set the correct offset for the mesh\n        if rprj:\n            reprojection['to'] = XY(*rprjToScene.pt(*reprojection['from']))\n            log.debug('{name} reprojected from {from} to {to}'.format(**reprojection, name=name))\n        else:\n            reprojection['to'] = reprojection['from']\n\n        if not geoscn.isGeoref:\n            # use the centre of the imported grid as scene origin (calculate only if grid file specified llcorner)\n            centre = (reprojection['from'].x + offset.x + ((ncols / 2) * cellsize),\n                      reprojection['from'].y + offset.y + ((nrows / 2) * cellsize))\n            if rprj:\n                centre = rprjToScene.pt(*centre)\n            geoscn.setOriginPrj(*centre)\n            dx, dy = geoscn.getOriginPrj()\n\n        index = 0\n        vertices = []\n        faces = []\n\n        # determine row read method\n        read = self.read_row_whitespace\n        if self.newlines:\n            read = self.read_row_newlines\n\n        for y in range(nrows - 1, -1, -step):\n            # spec doesn't require newline separated rows so make it handle a single line of all values\n            coldata = read(f, ncols)\n            if len(coldata) != ncols:\n                log.error('Incorrect number of columns for row {row}. Expected {expected}, got {actual}.'.format(row=nrows-y, expected=ncols, actual=len(coldata)))\n                self.report({'ERROR'}, 'Incorrect number of columns for row, check logs for more infos')\n                return {'CANCELLED'}\n\n            for i in range(step - 1):\n                _ = read(f, ncols)\n\n            for x in range(0, ncols, step):\n                # TODO: exclude nodata values (implications for face generation)\n                if not (self.importMode == 'CLOUD' and coldata[x] == nodata):\n                    pt = (x * cellsize + offset.x, y * cellsize + offset.y)\n                    if rprj:\n                        # reproject world-space source coordinate, then transform back to target local-space\n                        pt = rprjToScene.pt(pt[0] + reprojection['from'].x, pt[1] + reprojection['from'].y)\n                        pt = (pt[0] - reprojection['to'].x, pt[1] - reprojection['to'].y)\n                    try:\n                        vertices.append(pt + (float(coldata[x]),))\n                    except ValueError as e:\n                        log.error('Value \"{val}\" in row {row}, column {col} could not be converted to a float.'.format(val=coldata[x], row=nrows-y, col=x))\n                        self.report({'ERROR'}, 'Cannot convert value to float')\n                        return {'CANCELLED'}\n\n        if self.importMode == 'MESH':\n            step_ncols = math.ceil(ncols / step)\n            for r in range(0, math.ceil(nrows / step) - 1):\n                for c in range(0, step_ncols - 1):\n                    v1 = index\n                    v2 = v1 + step_ncols\n                    v3 = v2 + 1\n                    v4 = v1 + 1\n                    faces.append((v1, v2, v3, v4))\n                    index += 1\n                index += 1\n\n        # Create mesh\n        me = bpy.data.meshes.new(name)\n        ob = bpy.data.objects.new(name, me)\n        ob.location = (reprojection['to'].x - dx, reprojection['to'].y - dy, 0)\n\n        # Link object to scene and make active\n        scn = bpy.context.scene\n        scn.collection.objects.link(ob)\n        bpy.context.view_layer.objects.active = ob\n        ob.select_set(True)\n\n        me.from_pydata(vertices, [], faces)\n        me.update()\n        f.close()\n\n        if prefs.adjust3Dview:\n            bb = getBBOX.fromObj(ob)\n            adjust3Dview(context, bb)\n\n        return {'FINISHED'}\n\ndef register():\n\ttry:\n\t\tbpy.utils.register_class(IMPORTGIS_OT_ascii_grid)\n\texcept ValueError as e:\n\t\tlog.warning('{} is already registered, now unregister and retry... '.format(IMPORTGIS_OT_ascii_grid))\n\t\tunregister()\n\t\tbpy.utils.register_class(IMPORTGIS_OT_ascii_grid)\n\ndef unregister():\n\tbpy.utils.unregister_class(IMPORTGIS_OT_ascii_grid)\n"
  },
  {
    "path": "operators/io_import_georaster.py",
    "content": "# -*- coding:utf-8 -*-\n\n# This file is part of BlenderGIS\n\n#  ***** GPL LICENSE BLOCK *****\n#\n#  This program is free software: you can redistribute it and/or modify\n#  it under the terms of the GNU General Public License as published by\n#  the Free Software Foundation, either version 3 of the License, or\n#  (at your option) any later version.\n#\n#  This program is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#  GNU General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public License\n#  along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#  All rights reserved.\n#  ***** GPL LICENSE BLOCK *****\n\nimport bpy\nimport bmesh\nimport os\nimport math\nfrom mathutils import Vector\nimport numpy as np#Ship with Blender since 2.70\n\nimport logging\nlog = logging.getLogger(__name__)\n\nfrom ..geoscene import GeoScene, georefManagerLayout\nfrom ..prefs import PredefCRS\n\nfrom ..core.georaster import GeoRaster\nfrom .utils import bpyGeoRaster, exportAsMesh\nfrom .utils import placeObj, adjust3Dview, showTextures, addTexture, getBBOX\nfrom .utils import rasterExtentToMesh, geoRastUVmap, setDisplacer\n\nfrom ..core import HAS_GDAL\nif HAS_GDAL:\n\tfrom osgeo import gdal\n\nfrom ..core import XY as xy\nfrom ..core.errors import OverlapError\nfrom ..core.proj import Reproj\n\nfrom bpy_extras.io_utils import ImportHelper #helper class defines filename and invoke() function which calls the file selector\nfrom bpy.props import StringProperty, BoolProperty, EnumProperty, IntProperty\nfrom bpy.types import Operator\n\nPKG, SUBPKG = __package__.split('.', maxsplit=1)\n\nclass IMPORTGIS_OT_georaster(Operator, ImportHelper):\n\t\"\"\"Import georeferenced raster (need world file)\"\"\"\n\tbl_idname = \"importgis.georaster\"  # important since its how bpy.ops.importgis.georaster is constructed (allows calling operator from python console or another script)\n\t#bl_idname rules: must contain one '.' (dot) charactere, no capital letters, no reserved words (like 'import')\n\tbl_description = 'Import raster georeferenced with world file'\n\tbl_label = \"Import georaster\"\n\tbl_options = {\"UNDO\"}\n\n\tdef listObjects(self, context):\n\t\t#Function used to update the objects list (obj_list) used by the dropdown box.\n\t\tobjs = [] #list containing tuples of each object\n\t\tfor index, object in enumerate(bpy.context.scene.objects): #iterate over all objects\n\t\t\tif object.type == 'MESH':\n\t\t\t\tobjs.append((str(index), object.name, \"Object named \" +object.name)) #put each object in a tuple (key, label, tooltip) and add this to the objects list\n\t\treturn objs\n\n\t# ImportHelper class properties\n\tfilter_glob: StringProperty(\n\t\t\tdefault=\"*.tif;*.jpg;*.jpeg;*.png;*.bmp\",\n\t\t\toptions={'HIDDEN'},\n\t\t\t)\n\n\t# Raster CRS definition\n\tdef listPredefCRS(self, context):\n\t\treturn PredefCRS.getEnumItems()\n\trastCRS: EnumProperty(\n\t\tname = \"Raster CRS\",\n\t\tdescription = \"Choose a Coordinate Reference System\",\n\t\titems = listPredefCRS,\n\t\t)\n\treprojection: BoolProperty(\n\t\t\tname=\"Specifiy raster CRS\",\n\t\t\tdescription=\"Specifiy raster CRS if it's different from scene CRS\",\n\t\t\tdefault=False )\n\n\t# List of operator properties, the attributes will be assigned\n\t# to the class instance from the operator settings before calling.\n\timportMode: EnumProperty(\n\t\t\tname=\"Mode\",\n\t\t\tdescription=\"Select import mode\",\n\t\t\titems=[ ('PLANE', 'Basemap on new plane', \"Place raster texture on new plane mesh\"),\n\t\t\t('BKG', 'Basemap as background', \"Place raster as background image\"),\n\t\t\t('MESH', 'Basemap on mesh', \"UV map raster on an existing mesh\"),\n\t\t\t('DEM', 'DEM as displacement texture', \"Use DEM raster as height texture to wrap a base mesh\"),\n\t\t\t('DEM_RAW', 'DEM raw data build [slow]', \"Import a DEM as pixels points cloud with building faces. Do not use with huge dataset.\")]\n\t\t\t)\n\t#\n\tobjectsLst: EnumProperty(attr=\"obj_list\", name=\"Objects\", description=\"Choose object to edit\", items=listObjects)\n\t#\n\t#Subdivise (as DEM option)\n\tdef listSubdivisionModes(self, context):\n\t\titems = [ ('subsurf', 'Subsurf', \"Add a subsurf modifier\"), ('none', 'None', \"No subdivision\")]\n\t\tif not self.demOnMesh:\n\t\t\t#mesh subdivision method can not be applyed on an existing mesh\n\t\t\t#this option makes sense only when the mesh is created from scratch\n\t\t\titems.append(('mesh', 'Mesh', \"Create vertices at each pixels\"))\n\t\treturn items\n\n\tsubdivision: EnumProperty(\n\t\t\tname=\"Subdivision\",\n\t\t\tdescription=\"How to subdivise the plane (dispacer needs vertex to work with)\",\n\t\t\titems=listSubdivisionModes\n\t\t\t)\n\t#\n\tdemOnMesh: BoolProperty(\n\t\t\tname=\"Apply on existing mesh\",\n\t\t\tdescription=\"Use DEM as displacer for an existing mesh\",\n\t\t\tdefault=False\n\t\t\t)\n\t#\n\tclip: BoolProperty(\n\t\t\tname=\"Clip to working extent\",\n\t\t\tdescription=\"Use the reference bounding box to clip the DEM\",\n\t\t\tdefault=False\n\t\t\t)\n\t#\n\tdemInterpolation: BoolProperty(\n\t\t\tname=\"Smooth relief\",\n\t\t\tdescription=\"Use texture interpolation to smooth the resulting terrain\",\n\t\t\tdefault=True\n\t\t\t)\n\t#\n\tfillNodata: BoolProperty(\n\t\t\tname=\"Fill nodata values\",\n\t\t\tdescription=\"Interpolate existing nodata values to get an usuable displacement texture\",\n\t\t\tdefault=False\n\t\t\t)\n\t#\n\tstep: IntProperty(name = \"Step\", default=1, description=\"Pixel step\", min=1)\n\n\tbuildFaces: BoolProperty(name=\"Build faces\", default=True, description='Build quad faces connecting pixel point cloud')\n\n\tdef draw(self, context):\n\t\t#Function used by blender to draw the panel.\n\t\tlayout = self.layout\n\t\tlayout.prop(self, 'importMode')\n\t\tscn = bpy.context.scene\n\t\tgeoscn = GeoScene(scn)\n\t\t#\n\t\tif self.importMode == 'PLANE':\n\t\t\tpass\n\t\t#\n\t\tif self.importMode == 'BKG':\n\t\t\tpass\n\t\t#\n\t\tif self.importMode == 'MESH':\n\t\t\tif geoscn.isGeoref and len(self.objectsLst) > 0:\n\t\t\t\tlayout.prop(self, 'objectsLst')\n\t\t\telse:\n\t\t\t\tlayout.label(text=\"There isn't georef mesh to UVmap on\")\n\t\t#\n\t\tif self.importMode == 'DEM':\n\t\t\tlayout.prop(self, 'demOnMesh')\n\t\t\tif self.demOnMesh:\n\t\t\t\tif geoscn.isGeoref and len(self.objectsLst) > 0:\n\t\t\t\t\tlayout.prop(self, 'objectsLst')\n\t\t\t\t\tlayout.prop(self, 'clip')\n\t\t\t\telse:\n\t\t\t\t\tlayout.label(text=\"There isn't georef mesh to apply on\")\n\t\t\tlayout.prop(self, 'subdivision')\n\t\t\tlayout.prop(self, 'demInterpolation')\n\t\t\tif self.subdivision == 'mesh':\n\t\t\t\tlayout.prop(self, 'step')\n\t\t\tlayout.prop(self, 'fillNodata')\n\t\t#\n\t\tif self.importMode == 'DEM_RAW':\n\t\t\tlayout.prop(self, 'buildFaces')\n\t\t\tlayout.prop(self, 'step')\n\t\t\tlayout.prop(self, 'clip')\n\t\t\tif self.clip:\n\t\t\t\tif geoscn.isGeoref and len(self.objectsLst) > 0:\n\t\t\t\t\tlayout.prop(self, 'objectsLst')\n\t\t\t\telse:\n\t\t\t\t\tlayout.label(text=\"There isn't georef mesh to refer\")\n\t\t#\n\t\tif geoscn.isPartiallyGeoref:\n\t\t\tlayout.prop(self, 'reprojection')\n\t\t\tif self.reprojection:\n\t\t\t\tself.crsInputLayout(context)\n\t\t\t#\n\t\t\tgeorefManagerLayout(self, context)\n\t\telse:\n\t\t\tself.crsInputLayout(context)\n\n\tdef crsInputLayout(self, context):\n\t\tlayout = self.layout\n\t\trow = layout.row(align=True)\n\t\tsplit = row.split(factor=0.35, align=True)\n\t\tsplit.label(text='CRS:')\n\t\tsplit.prop(self, \"rastCRS\", text='')\n\t\trow.operator(\"bgis.add_predef_crs\", text='', icon='ADD')\n\n\t@classmethod\n\tdef poll(cls, context):\n\t\treturn context.mode == 'OBJECT'\n\n\tdef execute(self, context):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\n\t\tbpy.ops.object.select_all(action='DESELECT')\n\t\t#Get scene and some georef data\n\t\tscn = bpy.context.scene\n\t\tgeoscn = GeoScene(scn)\n\t\tif geoscn.isBroken:\n\t\t\tself.report({'ERROR'}, \"Scene georef is broken, please fix it beforehand\")\n\t\t\treturn {'CANCELLED'}\n\n\t\tscale = geoscn.scale #TODO\n\n\t\tif geoscn.isGeoref:\n\t\t\tdx, dy = geoscn.getOriginPrj()\n\t\t\tif self.reprojection:\n\t\t\t\trastCRS = self.rastCRS\n\t\t\telse:\n\t\t\t\trastCRS = geoscn.crs\n\t\telse: #if not geoscn.hasCRS\n\t\t\trastCRS = self.rastCRS\n\t\t\ttry:\n\t\t\t\tgeoscn.crs = rastCRS\n\t\t\texcept Exception as e:\n\t\t\t\tlog.error(\"Cannot set scene crs\", exc_info=True)\n\t\t\t\tself.report({'ERROR'}, \"Cannot set scene crs, check logs for more infos\")\n\t\t\t\treturn {'CANCELLED'}\n\n\t\t#Raster reprojection throught UV mapping\n\t\t#build reprojector objects\n\t\tif geoscn.crs != rastCRS:\n\t\t\trprj = True\n\t\t\trprjToRaster = Reproj(geoscn.crs, rastCRS)\n\t\t\trprjToScene = Reproj(rastCRS, geoscn.crs)\n\t\telse:\n\t\t\trprj = False\n\t\t\trprjToRaster = None\n\t\t\trprjToScene = None\n\n\t\t#Path\n\t\tfilePath = self.filepath\n\t\tname = os.path.basename(filePath)[:-4]\n\n\t\t######################################\n\t\tif self.importMode == 'PLANE':#on plane\n\t\t\t#Load raster\n\t\t\ttry:\n\t\t\t\trast = bpyGeoRaster(filePath)\n\t\t\texcept IOError as e:\n\t\t\t\tlog.error(\"Unable to open raster\", exc_info=True)\n\t\t\t\tself.report({'ERROR'}, \"Unable to open raster, check logs for more infos\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\t#Get or set georef dx, dy\n\t\t\tif not geoscn.isGeoref:\n\t\t\t\tdx, dy = rast.center.x, rast.center.y\n\t\t\t\tif rprj:\n\t\t\t\t\tdx, dy = rprjToScene.pt(dx, dy)\n\t\t\t\tgeoscn.setOriginPrj(dx, dy)\n\t\t\t#create a new mesh from raster extent\n\t\t\tmesh = rasterExtentToMesh(name, rast, dx, dy, reproj=rprjToScene)\n\t\t\t#place obj\n\t\t\tobj = placeObj(mesh, name)\n\t\t\t#UV mapping\n\t\t\tuvTxtLayer = mesh.uv_layers.new(name='rastUVmap')# Add UV map texture layer\n\t\t\tgeoRastUVmap(obj, uvTxtLayer, rast, dx, dy, reproj=rprjToRaster)\n\t\t\t# Create material\n\t\t\tmat = bpy.data.materials.new('rastMat')\n\t\t\t# Add material to current object\n\t\t\tobj.data.materials.append(mat)\n\t\t\t# Add texture to material\n\t\t\taddTexture(mat, rast.bpyImg, uvTxtLayer, name='rastText')\n\n\t\t######################################\n\t\tif self.importMode == 'BKG':#background\n\t\t\tif rprj:\n\t\t\t\t#TODO, do gdal true reproj\n\t\t\t\tself.report({'ERROR'}, \"Raster reprojection is not possible in background mode\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\t#Load raster\n\t\t\ttry:\n\t\t\t\trast = bpyGeoRaster(filePath)\n\t\t\texcept IOError as e:\n\t\t\t\tlog.error(\"Unable to open raster\", exc_info=True)\n\t\t\t\tself.report({'ERROR'}, \"Unable to open raster, check logs for more infos\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\t#Check pixel size and rotation\n\t\t\tif rast.rotation.xy != [0,0]:\n\t\t\t\tself.report({'ERROR'}, \"Cannot apply a rotation in background image mode\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\tif abs(round(rast.pxSize.x, 3)) != abs(round(rast.pxSize.y, 3)):\n\t\t\t\tself.report({'ERROR'}, \"Background image needs equal pixel size in map units in both x ans y axis\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\t#\n\t\t\ttrueSizeX = rast.geoSize.x\n\t\t\ttrueSizeY = rast.geoSize.y\n\t\t\tratio = rast.size.x / rast.size.y\n\t\t\tif geoscn.isGeoref:\n\t\t\t\toffx, offy = rast.center.x - dx, rast.center.y - dy\n\t\t\telse:\n\t\t\t\tdx, dy = rast.center.x, rast.center.y\n\t\t\t\tgeoscn.setOriginPrj(dx, dy)\n\t\t\t\toffx, offy = 0, 0\n\n\t\t\tbkg = bpy.data.objects.new(self.name, None) #None will create an empty\n\t\t\tbkg.empty_display_type = 'IMAGE'\n\t\t\tbkg.empty_image_depth = 'BACK'\n\t\t\tbkg.data = rast.bpyImg\n\t\t\tscn.collection.objects.link(bkg)\n\n\t\t\tbkg.empty_display_size = 1 #a size of 1 means image width=1bu\n\t\t\tbkg.scale = (trueSizeX, trueSizeY*ratio, 1)\n\t\t\tbkg.location = (offx, offy, 0)\n\n\t\t\tbpy.context.view_layer.objects.active = bkg\n\t\t\tbkg.select_set(True)\n\n\t\t\tif prefs.adjust3Dview:\n\t\t\t\tadjust3Dview(context, rast.bbox)\n\n\t\t######################################\n\t\tif self.importMode == 'MESH':\n\t\t\tif not geoscn.isGeoref or len(self.objectsLst) == 0:\n\t\t\t\tself.report({'ERROR'}, \"There isn't georef mesh to apply on\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\t# Get choosen object\n\t\t\tobj = scn.objects[int(self.objectsLst)]\n\t\t\t# Select and active this obj\n\t\t\tobj.select_set(True)\n\t\t\tcontext.view_layer.objects.active = obj\n\t\t\t# Compute projeted bbox (in geographic coordinates system)\n\t\t\tsubBox = getBBOX.fromObj(obj).toGeo(geoscn)\n\t\t\tif rprj:\n\t\t\t\tsubBox = rprjToRaster.bbox(subBox)\n\t\t\t#Load raster\n\t\t\ttry:\n\t\t\t\trast = bpyGeoRaster(filePath, subBoxGeo=subBox)\n\t\t\texcept IOError as e:\n\t\t\t\tlog.error(\"Unable to open raster\", exc_info=True)\n\t\t\t\tself.report({'ERROR'}, \"Unable to open raster, check logs for more infos\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\texcept OverlapError:\n\t\t\t\tself.report({'ERROR'}, \"Non overlap data\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\t# Add UV map texture layer\n\t\t\tmesh = obj.data\n\t\t\tuvTxtLayer = mesh.uv_layers.new(name='rastUVmap')\n\t\t\tuvTxtLayer.active = True\n\t\t\t# UV mapping\n\t\t\tgeoRastUVmap(obj, uvTxtLayer, rast, dx, dy, reproj=rprjToRaster)\n\t\t\t# Add material and texture\n\t\t\tmat = bpy.data.materials.new('rastMat')\n\t\t\tobj.data.materials.append(mat)\n\t\t\taddTexture(mat, rast.bpyImg, uvTxtLayer, name='rastText')\n\n\t\t######################################\n\t\tif self.importMode == 'DEM':\n\n\t\t\t# Get reference plane\n\t\t\tif self.demOnMesh:\n\t\t\t\tif not geoscn.isGeoref or len(self.objectsLst) == 0:\n\t\t\t\t\tself.report({'ERROR'}, \"There isn't georef mesh to apply on\")\n\t\t\t\t\treturn {'CANCELLED'}\n\t\t\t\t# Get choosen object\n\t\t\t\tobj = scn.objects[int(self.objectsLst)]\n\t\t\t\tmesh = obj.data\n\t\t\t\t# Select and active this obj\n\t\t\t\tobj.select_set(True)\n\t\t\t\tcontext.view_layer.objects.active = obj\n\t\t\t\t# Compute projeted bbox (in geographic coordinates system)\n\t\t\t\tsubBox = getBBOX.fromObj(obj).toGeo(geoscn)\n\t\t\t\tif rprj:\n\t\t\t\t\tsubBox = rprjToRaster.bbox(subBox)\n\t\t\telse:\n\t\t\t\tsubBox = None\n\n\t\t\t# Load raster\n\t\t\ttry:\n\t\t\t\tgrid = bpyGeoRaster(filePath, subBoxGeo=subBox, clip=self.clip, fillNodata=self.fillNodata, useGDAL=HAS_GDAL, raw=True)\n\t\t\texcept IOError as e:\n\t\t\t\tlog.error(\"Unable to open raster\", exc_info=True)\n\t\t\t\tself.report({'ERROR'}, \"Unable to open raster, check logs for more infos\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\texcept OverlapError:\n\t\t\t\tself.report({'ERROR'}, \"Non overlap data\")\n\t\t\t\treturn {'CANCELLED'}\n\n\t\t\t# If needed, create a new plane object from raster extent\n\t\t\tif not self.demOnMesh:\n\t\t\t\tif not geoscn.isGeoref:\n\t\t\t\t\tdx, dy = grid.center.x, grid.center.y\n\t\t\t\t\tif rprj:\n\t\t\t\t\t\tdx, dy = rprjToScene.pt(dx, dy)\n\t\t\t\t\tgeoscn.setOriginPrj(dx, dy)\n\t\t\t\tif self.subdivision == 'mesh':#Mesh cut\n\t\t\t\t\tmesh = exportAsMesh(grid, dx, dy, self.step, reproj=rprjToScene, flat=True)\n\t\t\t\telse:\n\t\t\t\t\tmesh = rasterExtentToMesh(name, grid, dx, dy, pxLoc='CENTER', reproj=rprjToScene) #use pixel center to avoid displacement glitch\n\t\t\t\tobj = placeObj(mesh, name)\n\t\t\t\tsubBox = getBBOX.fromObj(obj).toGeo(geoscn)\n\n\t\t\t# Add UV map texture layer\n\t\t\tpreviousUVmapIdx = mesh.uv_layers.active_index\n\t\t\tuvTxtLayer = mesh.uv_layers.new(name='demUVmap')\n\t\t\t#UV mapping\n\t\t\tgeoRastUVmap(obj, uvTxtLayer, grid, dx, dy, reproj=rprjToRaster)\n\t\t\t#Restore previous uv map\n\t\t\tif previousUVmapIdx != -1:\n\t\t\t\tmesh.uv_layers.active_index = previousUVmapIdx\n\t\t\t#Make subdivision\n\t\t\tif self.subdivision == 'subsurf':#Add subsurf modifier\n\t\t\t\tif not 'SUBSURF' in [mod.type for mod in obj.modifiers]:\n\t\t\t\t\tsubsurf = obj.modifiers.new('DEM', type='SUBSURF')\n\t\t\t\t\tsubsurf.subdivision_type = 'SIMPLE'\n\t\t\t\t\tsubsurf.levels = 6\n\t\t\t\t\tsubsurf.render_levels = 6\n\t\t\t#Set displacer\n\t\t\tdsp = setDisplacer(obj, grid, uvTxtLayer, interpolation=self.demInterpolation)\n\n\t\t######################################\n\t\tif self.importMode == 'DEM_RAW':\n\n\t\t\t# Get reference plane\n\t\t\tsubBox = None\n\t\t\tif self.clip:\n\t\t\t\tif not geoscn.isGeoref or len(self.objectsLst) == 0:\n\t\t\t\t\tself.report({'ERROR'}, \"No working extent\")\n\t\t\t\t\treturn {'CANCELLED'}\n\t\t\t\t# Get choosen object\n\t\t\t\tobj = scn.objects[int(self.objectsLst)]\n\t\t\t\tsubBox = getBBOX.fromObj(obj).toGeo(geoscn)\n\t\t\t\tif rprj:\n\t\t\t\t\tsubBox = rprjToRaster.bbox(subBox)\n\n\t\t\t# Load raster\n\t\t\ttry:\n\t\t\t\tgrid = GeoRaster(filePath, subBoxGeo=subBox, useGDAL=HAS_GDAL)\n\t\t\texcept IOError as e:\n\t\t\t\tlog.error(\"Unable to open raster\", exc_info=True)\n\t\t\t\tself.report({'ERROR'}, \"Unable to open raster, check logs for more infos\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\texcept OverlapError:\n\t\t\t\tself.report({'ERROR'}, \"Non overlap data\")\n\t\t\t\treturn {'CANCELLED'}\n\n\t\t\tif not geoscn.isGeoref:\n\t\t\t\tdx, dy = grid.center.x, grid.center.y\n\t\t\t\tif rprj:\n\t\t\t\t\tdx, dy = rprjToScene.pt(dx, dy)\n\t\t\t\tgeoscn.setOriginPrj(dx, dy)\n\t\t\tmesh = exportAsMesh(grid, dx, dy, self.step, reproj=rprjToScene, subset=self.clip, flat=False, buildFaces=self.buildFaces)\n\t\t\tobj = placeObj(mesh, name)\n\t\t\t#grid.unload()\n\n\t\t######################################\n\n\t\t#Flag if a new object as been created...\n\t\tif self.importMode == 'PLANE' or (self.importMode == 'DEM' and not self.demOnMesh) or self.importMode == 'DEM_RAW':\n\t\t\tnewObjCreated = True\n\t\telse:\n\t\t\tnewObjCreated = False\n\n\t\t#...if so, maybee we need to adjust 3d view settings to it\n\t\tif newObjCreated and prefs.adjust3Dview:\n\t\t\tbb = getBBOX.fromObj(obj)\n\t\t\tadjust3Dview(context, bb)\n\n\t\t#Force view mode with textures\n\t\tif prefs.forceTexturedSolid:\n\t\t\tshowTextures(context)\n\n\n\t\treturn {'FINISHED'}\n\n\ndef register():\n\ttry:\n\t\tbpy.utils.register_class(IMPORTGIS_OT_georaster)\n\texcept ValueError as e:\n\t\tlog.warning('{} is already registered, now unregister and retry... '.format(IMPORTGIS_OT_georaster))\n\t\tunregister()\n\t\tbpy.utils.register_class(IMPORTGIS_OT_georaster)\n\ndef unregister():\n\tbpy.utils.unregister_class(IMPORTGIS_OT_georaster)\n"
  },
  {
    "path": "operators/io_import_osm.py",
    "content": "import os\nimport time\nimport json\nimport random\n\nimport logging\nlog = logging.getLogger(__name__)\n\nimport bpy\nimport bmesh\nfrom bpy.types import Operator, Panel, AddonPreferences\nfrom bpy.props import StringProperty, IntProperty, FloatProperty, BoolProperty, EnumProperty, FloatVectorProperty\n\nfrom .lib.osm import overpy\n\nfrom ..geoscene import GeoScene\nfrom .utils import adjust3Dview, getBBOX, DropToGround, isTopView\n\nfrom ..core.proj import Reproj, reprojBbox, reprojPt, utm\nfrom ..core.utils import perf_clock\n\nfrom ..core import settings\nUSER_AGENT = settings.user_agent\n\nPKG, SUBPKG = __package__.split('.', maxsplit=1)\n\n#WARNING: There is a known bug with using an enum property with a callback, Python must keep a reference to the strings returned\n#https://developer.blender.org/T48873\n#https://developer.blender.org/T38489\ndef getTags():\n\tprefs = bpy.context.preferences.addons[PKG].preferences\n\ttags = json.loads(prefs.osmTagsJson)\n\treturn tags\n\n#Global variable that will be seed by getTags() at each operator invoke\n#then callback of dynamic enum will use this global variable\nOSMTAGS = []\n\n\n\nclosedWaysArePolygons = ['aeroway', 'amenity', 'boundary', 'building', 'craft', 'geological', 'historic', 'landuse', 'leisure', 'military', 'natural', 'office', 'place', 'shop' , 'sport', 'tourism']\nclosedWaysAreExtruded = ['building']\n\n\ndef queryBuilder(bbox, tags=['building', 'highway'], types=['node', 'way', 'relation'], format='json'):\n\n\t\t'''\n\t\tQL template syntax :\n\t\t[out:json][bbox:ymin,xmin,ymax,xmax];(node[tag1];node[tag2];((way[tag1];way[tag2];);>;);relation;);out;\n\t\t'''\n\n\t\t#s,w,n,e <--> ymin,xmin,ymax,xmax\n\t\tbboxStr = ','.join(map(str, bbox.toLatlon()))\n\n\t\tif not types:\n\t\t\t#if no type filter is defined then just select all kind of type\n\t\t\ttypes = ['node', 'way', 'relation']\n\n\t\thead = \"[out:\"+format+\"][bbox:\"+bboxStr+\"];\"\n\n\t\tunion = '('\n\t\t#all tagged nodes\n\t\tif 'node' in types:\n\t\t\tif tags:\n\t\t\t\tunion += ';'.join( ['node['+tag+']' for tag in tags] ) + ';'\n\t\t\telse:\n\t\t\t\tunion += 'node;'\n\t\t#all tagged ways with all their nodes (recurse down)\n\t\tif 'way' in types:\n\t\t\tunion += '(('\n\t\t\tif tags:\n\t\t\t\tunion += ';'.join( ['way['+tag+']' for tag in tags] ) + ';);'\n\t\t\telse:\n\t\t\t\tunion += 'way;);'\n\t\t\tunion += '>;);'\n\t\t#all relations (no filter tag applied)\n\t\tif 'relation' in types or 'rel' in types:\n\t\t\tunion += 'relation;'\n\t\tunion += ')'\n\n\t\toutput = ';out;'\n\t\tqry = head + union + output\n\n\t\treturn qry\n\n\n\n\n\n########################\ndef joinBmesh(src_bm, dest_bm):\n\t'''\n\tHack to join a bmesh to another\n\tTODO: replace this function by bmesh.ops.duplicate when 'dest' argument will be implemented\n\t'''\n\tbuff = bpy.data.meshes.new(\".temp\")\n\tsrc_bm.to_mesh(buff)\n\tdest_bm.from_mesh(buff)\n\tbpy.data.meshes.remove(buff)\n\n\n\n\n\nclass OSM_IMPORT():\n\t\"\"\"Import from Open Street Map\"\"\"\n\n\tdef enumTags(self, context):\n\t\titems = []\n\t\t##prefs = context.preferences.addons[PKG].preferences\n\t\t##osmTags = json.loads(prefs.osmTagsJson)\n\t\t#we need to use a global variable as workaround to enum callback bug (T48873, T38489)\n\t\tfor tag in OSMTAGS:\n\t\t\t#put each item in a tuple (key, label, tooltip)\n\t\t\titems.append( (tag, tag, tag) )\n\t\treturn items\n\n\tfilterTags: EnumProperty(\n\t\t\tname = \"Tags\",\n\t\t\tdescription = \"Select tags to include\",\n\t\t\titems = enumTags,\n\t\t\toptions = {\"ENUM_FLAG\"})\n\n\tfeatureType: EnumProperty(\n\t\t\tname = \"Type\",\n\t\t\tdescription = \"Select types to include\",\n\t\t\titems = [\n\t\t\t\t('node', 'Nodes', 'Request all nodes'),\n\t\t\t\t('way', 'Ways', 'Request all ways'),\n\t\t\t\t('relation', 'Relations', 'Request all relations')\n\t\t\t],\n\t\t\tdefault = {'way'},\n\t\t\toptions = {\"ENUM_FLAG\"}\n\t\t\t)\n\n\t# Elevation object\n\tdef listObjects(self, context):\n\t\tobjs = []\n\t\tfor index, object in enumerate(bpy.context.scene.objects):\n\t\t\tif object.type == 'MESH':\n\t\t\t\t#put each object in a tuple (key, label, tooltip) and add this to the objects list\n\t\t\t\tobjs.append((str(index), object.name, \"Object named \" + object.name))\n\t\treturn objs\n\n\tobjElevLst: EnumProperty(\n\t\tname=\"Elev. object\",\n\t\tdescription=\"Choose the mesh from which extract z elevation\",\n\t\titems=listObjects )\n\n\tuseElevObj: BoolProperty(\n\t\t\tname=\"Elevation from object\",\n\t\t\tdescription=\"Get z elevation value from an existing ground mesh\",\n\t\t\tdefault=False )\n\n\tseparate: BoolProperty(name='Separate objects', description='Warning : can be very slow with lot of features', default=False)\n\n\tbuildingsExtrusion: BoolProperty(name='Buildings extrusion', description='', default=True)\n\tdefaultHeight: FloatProperty(name='Default Height', description='Set the height value using for extrude building when the tag is missing', default=20)\n\tlevelHeight: FloatProperty(name='Level height', description='Set a height for a building level, using for compute extrude height based on number of levels', default=3)\n\trandomHeightThreshold: IntProperty(name='Random height threshold', description='Threshold value for randomize default height', default=0)\n\n\tdef draw(self, context):\n\t\tlayout = self.layout\n\t\trow = layout.row()\n\t\trow.prop(self, \"featureType\", expand=True)\n\t\trow = layout.row()\n\t\tcol = row.column()\n\t\tcol.prop(self, \"filterTags\", expand=True)\n\t\tlayout.prop(self, 'useElevObj')\n\t\tif self.useElevObj:\n\t\t\tlayout.prop(self, 'objElevLst')\n\t\tlayout.prop(self, 'buildingsExtrusion')\n\t\tif self.buildingsExtrusion:\n\t\t\tlayout.prop(self, 'defaultHeight')\n\t\t\tlayout.prop(self, 'randomHeightThreshold')\n\t\t\tlayout.prop(self, 'levelHeight')\n\t\tlayout.prop(self, 'separate')\n\n\n\tdef build(self, context, result, dstCRS):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\tscn = context.scene\n\t\tgeoscn = GeoScene(scn)\n\t\tscale = geoscn.scale #TODO\n\n\t\t#Init reprojector class\n\t\ttry:\n\t\t\trprj = Reproj(4326, dstCRS)\n\t\texcept Exception as e:\n\t\t\tlog.error('Unable to reproject data', exc_info=True)\n\t\t\tself.report({'ERROR'}, \"Unable to reproject data ckeck logs for more infos\")\n\t\t\treturn {'FINISHED'}\n\n\t\tif self.useElevObj:\n\t\t\tif not self.objElevLst:\n\t\t\t\tlog.error('There is no elevation object in the scene to get elevation from')\n\t\t\t\tself.report({'ERROR'}, \"There is no elevation object in the scene to get elevation from\")\n\t\t\t\treturn {'FINISHED'}\n\t\t\televObj = scn.objects[int(self.objElevLst)]\n\t\t\trayCaster = DropToGround(scn, elevObj)\n\n\t\tbmeshes = {}\n\t\tvgroupsObj = {}\n\n\t\t#######\n\t\tdef seed(id, tags, pts):\n\t\t\t'''\n\t\t\tSub funtion :\n\t\t\t\t1. create a bmesh from [pts]\n\t\t\t\t2. seed a global bmesh or create a new object\n\t\t\t'''\n\t\t\tif len(pts) > 1:\n\t\t\t\tif pts[0] == pts[-1] and any(tag in closedWaysArePolygons for tag in tags):\n\t\t\t\t\ttype = 'Areas'\n\t\t\t\t\tclosed = True\n\t\t\t\t\tpts.pop() #exclude last duplicate node\n\t\t\t\telse:\n\t\t\t\t\ttype = 'Ways'\n\t\t\t\t\tclosed = False\n\t\t\telse:\n\t\t\t\ttype = 'Nodes'\n\t\t\t\tclosed = False\n\n\t\t\t#reproj and shift coords\n\t\t\tpts = rprj.pts(pts)\n\t\t\tdx, dy = geoscn.crsx, geoscn.crsy\n\n\t\t\tif self.useElevObj:\n\t\t\t\t#pts = [rayCaster.rayCast(v[0]-dx, v[1]-dy).loc for v in pts]\n\t\t\t\tpts = [rayCaster.rayCast(v[0]-dx, v[1]-dy) for v in pts]\n\t\t\t\thits = [pt.hit for pt in pts]\n\t\t\t\tif not all(hits) and any(hits):\n\t\t\t\t\tzs = [p.loc.z for p in pts if p.hit]\n\t\t\t\t\tmeanZ = sum(zs) / len(zs)\n\t\t\t\t\tfor v in pts:\n\t\t\t\t\t\tif not v.hit:\n\t\t\t\t\t\t\tv.loc.z = meanZ\n\t\t\t\tpts = [pt.loc for pt in pts]\n\t\t\telse:\n\t\t\t\tpts = [ (v[0]-dx, v[1]-dy, 0) for v in pts]\n\n\t\t\t#Create a new bmesh\n\t\t\t#>using an intermediate bmesh object allows some extra operation like extrusion\n\t\t\tbm = bmesh.new()\n\n\t\t\tif len(pts) == 1:\n\t\t\t\tverts = [bm.verts.new(pt) for pt in pts]\n\n\t\t\telif closed: #faces\n\t\t\t\tverts = [bm.verts.new(pt) for pt in pts]\n\t\t\t\tface = bm.faces.new(verts)\n\t\t\t\t#ensure face is up (anticlockwise order)\n\t\t\t\t#because in OSM there is no particular order for closed ways\n\t\t\t\tface.normal_update()\n\t\t\t\tif face.normal.z < 0:\n\t\t\t\t\tface.normal_flip()\n\n\t\t\t\tif self.buildingsExtrusion and any(tag in closedWaysAreExtruded for tag in tags):\n\t\t\t\t\toffset = None\n\t\t\t\t\tif \"height\" in tags:\n\t\t\t\t\t\t\thtag = tags[\"height\"]\n\t\t\t\t\t\t\thtag.replace(',', '.')\n\t\t\t\t\t\t\ttry:\n\t\t\t\t\t\t\t\toffset = int(htag)\n\t\t\t\t\t\t\texcept:\n\t\t\t\t\t\t\t\ttry:\n\t\t\t\t\t\t\t\t\toffset = float(htag)\n\t\t\t\t\t\t\t\texcept:\n\t\t\t\t\t\t\t\t\tfor i, c in enumerate(htag):\n\t\t\t\t\t\t\t\t\t\tif not c.isdigit():\n\t\t\t\t\t\t\t\t\t\t\ttry:\n\t\t\t\t\t\t\t\t\t\t\t\toffset, unit = float(htag[:i]), htag[i:].strip()\n\t\t\t\t\t\t\t\t\t\t\t\t#todo : parse unit  25, 25m, 25 ft, etc.\n\t\t\t\t\t\t\t\t\t\t\texcept:\n\t\t\t\t\t\t\t\t\t\t\t\toffset = None\n\t\t\t\t\telif \"building:levels\" in tags:\n\t\t\t\t\t\ttry:\n\t\t\t\t\t\t\toffset = int(tags[\"building:levels\"]) * self.levelHeight\n\t\t\t\t\t\texcept ValueError as e:\n\t\t\t\t\t\t\toffset = None\n\n\t\t\t\t\tif offset is None:\n\t\t\t\t\t\tminH = self.defaultHeight - self.randomHeightThreshold\n\t\t\t\t\t\tif minH < 0 :\n\t\t\t\t\t\t\tminH = 0\n\t\t\t\t\t\tmaxH = self.defaultHeight + self.randomHeightThreshold\n\t\t\t\t\t\toffset = random.randint(int(minH), int(maxH))\n\n\t\t\t\t\t#Extrude\n\t\t\t\t\t\"\"\"\n\t\t\t\t\tif self.extrusionAxis == 'NORMAL':\n\t\t\t\t\t\tnormal = face.normal\n\t\t\t\t\t\tvect = normal * offset\n\t\t\t\t\telif self.extrusionAxis == 'Z':\n\t\t\t\t\t\"\"\"\n\t\t\t\t\tvect = (0, 0, offset)\n\t\t\t\t\tfaces = bmesh.ops.extrude_discrete_faces(bm, faces=[face]) #return {'faces': [BMFace]}\n\t\t\t\t\tverts = faces['faces'][0].verts\n\t\t\t\t\tif self.useElevObj:\n\t\t\t\t\t\t#Making flat roof\n\t\t\t\t\t\tz = max([v.co.z for v in verts]) + offset #get max z coord\n\t\t\t\t\t\tfor v in verts:\n\t\t\t\t\t\t\tv.co.z = z\n\t\t\t\t\telse:\n\t\t\t\t\t\tbmesh.ops.translate(bm, verts=verts, vec=vect)\n\n\n\t\t\telif len(pts) > 1: #edge\n\t\t\t\tverts = [bm.verts.new(pt) for pt in pts]\n\t\t\t\tfor i in range(len(pts)-1):\n\t\t\t\t\tedge = bm.edges.new( [verts[i], verts[i+1] ])\n\n\n\t\t\tif self.separate:\n\n\t\t\t\tname = tags.get('name', str(id))\n\n\t\t\t\tmesh = bpy.data.meshes.new(name)\n\t\t\t\tbm.to_mesh(mesh)\n\t\t\t\tmesh.update()\n\t\t\t\tmesh.validate()\n\n\t\t\t\tobj = bpy.data.objects.new(name, mesh)\n\n\t\t\t\t#Assign tags to custom props\n\t\t\t\tobj['id'] = str(id) #cast to str to avoid overflow error \"Python int too large to convert to C int\"\n\t\t\t\tfor key in tags.keys():\n\t\t\t\t\tobj[key] = tags[key]\n\n\t\t\t\t#Put object in right collection\n\t\t\t\tif self.filterTags:\n\t\t\t\t\ttagsList = self.filterTags\n\t\t\t\telse:\n\t\t\t\t\ttagsList = OSMTAGS\n\t\t\t\tif any(tag in tagsList for tag in tags):\n\t\t\t\t\tfor k in tagsList:\n\t\t\t\t\t\tif k in tags:\n\t\t\t\t\t\t\ttry:\n\t\t\t\t\t\t\t\ttagCollec = layer.children[k]\n\t\t\t\t\t\t\texcept KeyError:\n\t\t\t\t\t\t\t\ttagCollec = bpy.data.collections.new(k)\n\t\t\t\t\t\t\t\tlayer.children.link(tagCollec)\n\t\t\t\t\t\t\ttagCollec.objects.link(obj)\n\t\t\t\t\t\t\tbreak\n\t\t\t\telse:\n\t\t\t\t\tlayer.objects.link(obj)\n\n\t\t\t\tobj.select_set(True)\n\n\n\t\t\telse:\n\t\t\t\t#Grouping\n\n\t\t\t\tbm.verts.index_update()\n\t\t\t\t#bm.edges.index_update()\n\t\t\t\t#bm.faces.index_update()\n\n\t\t\t\tif self.filterTags:\n\n\t\t\t\t\t#group by tags (there could be some duplicates)\n\t\t\t\t\tfor k in self.filterTags:\n\n\t\t\t\t\t\tif k in extags: #\n\t\t\t\t\t\t\tobjName = type + ':' + k\n\t\t\t\t\t\t\tkbm = bmeshes.setdefault(objName, bmesh.new())\n\t\t\t\t\t\t\toffset = len(kbm.verts)\n\t\t\t\t\t\t\tjoinBmesh(bm, kbm)\n\n\t\t\t\telse:\n\t\t\t\t\t#group all into one unique mesh\n\t\t\t\t\tobjName = type\n\t\t\t\t\t_bm = bmeshes.setdefault(objName, bmesh.new())\n\t\t\t\t\toffset = len(_bm.verts)\n\t\t\t\t\tjoinBmesh(bm, _bm)\n\n\n\t\t\t\t#vertex group\n\t\t\t\tname = tags.get('name', None)\n\t\t\t\tvidx = [v.index + offset for v in bm.verts]\n\t\t\t\tvgroups = vgroupsObj.setdefault(objName, {})\n\n\t\t\t\tfor tag in extags:\n\t\t\t\t\t#if tag in osmTags:#filter\n\t\t\t\t\tif not tag.startswith('name'):\n\t\t\t\t\t\tvgroup = vgroups.setdefault('Tag:'+tag, [])\n\t\t\t\t\t\tvgroup.extend(vidx)\n\n\t\t\t\tif name is not None:\n\t\t\t\t\t#vgroup['Name:'+name] = [vidx]\n\t\t\t\t\tvgroup = vgroups.setdefault('Name:'+name, [])\n\t\t\t\t\tvgroup.extend(vidx)\n\n\t\t\t\tif 'relation' in self.featureType:\n\t\t\t\t\tfor rel in result.relations:\n\t\t\t\t\t\tname = rel.tags.get('name', str(rel.id))\n\t\t\t\t\t\tfor member in rel.members:\n\t\t\t\t\t\t\t#todo: remove duplicate members\n\t\t\t\t\t\t\tif id == member.ref:\n\t\t\t\t\t\t\t\tvgroup = vgroups.setdefault('Relation:'+name, [])\n\t\t\t\t\t\t\t\tvgroup.extend(vidx)\n\n\n\n\t\t\tbm.free()\n\n\n\t\t######\n\n\t\tif self.separate:\n\t\t\tlayer = bpy.data.collections.new('OSM')\n\t\t\tcontext.scene.collection.children.link(layer)\n\n\t\t#Build mesh\n\t\twaysNodesId = [node.id for way in result.ways for node in way.nodes]\n\n\t\tif 'node' in self.featureType:\n\n\t\t\tfor node in result.nodes:\n\n\t\t\t\t#extended tags list\n\t\t\t\textags = list(node.tags.keys()) + [k + '=' + v for k, v in node.tags.items()]\n\n\t\t\t\tif node.id in waysNodesId:\n\t\t\t\t\tcontinue\n\n\t\t\t\tif self.filterTags and not any(tag in self.filterTags for tag in extags):\n\t\t\t\t\tcontinue\n\n\t\t\t\tpt = (float(node.lon), float(node.lat))\n\t\t\t\tseed(node.id, node.tags, [pt])\n\n\n\t\tif 'way' in self.featureType:\n\n\t\t\tfor way in result.ways:\n\n\t\t\t\textags = list(way.tags.keys()) + [k + '=' + v for k, v in way.tags.items()]\n\n\t\t\t\tif self.filterTags and not any(tag in self.filterTags for tag in extags):\n\t\t\t\t\tcontinue\n\n\t\t\t\tpts = [(float(node.lon), float(node.lat)) for node in way.nodes]\n\t\t\t\tseed(way.id, way.tags, pts)\n\n\n\n\t\tif not self.separate:\n\n\t\t\tfor name, bm in bmeshes.items():\n\t\t\t\tif prefs.mergeDoubles:\n\t\t\t\t\tbmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)\n\t\t\t\tmesh = bpy.data.meshes.new(name)\n\t\t\t\tbm.to_mesh(mesh)\n\t\t\t\tbm.free()\n\n\t\t\t\tmesh.update()#calc_edges=True)\n\t\t\t\tmesh.validate()\n\t\t\t\tobj = bpy.data.objects.new(name, mesh)\n\t\t\t\tscn.collection.objects.link(obj)\n\t\t\t\tobj.select_set(True)\n\n\t\t\t\tvgroups = vgroupsObj.get(name, None)\n\t\t\t\tif vgroups is not None:\n\t\t\t\t\t#for vgroupName, vgroupIdx in vgroups.items():\n\t\t\t\t\tfor vgroupName in sorted(vgroups.keys()):\n\t\t\t\t\t\tvgroupIdx = vgroups[vgroupName]\n\t\t\t\t\t\tg = obj.vertex_groups.new(name=vgroupName)\n\t\t\t\t\t\tg.add(vgroupIdx, weight=1, type='ADD')\n\n\n\t\telif 'relation' in self.featureType:\n\n\t\t\trelations = bpy.data.collections.new('Relations')\n\t\t\tbpy.data.collections['OSM'].children.link(relations)\n\t\t\timportedObjects = bpy.data.collections['OSM'].objects\n\n\t\t\tfor rel in result.relations:\n\n\t\t\t\tname = rel.tags.get('name', str(rel.id))\n\t\t\t\ttry:\n\t\t\t\t\trelation = relations.children[name] #or bpy.data.collections[name]\n\t\t\t\texcept KeyError:\n\t\t\t\t\trelation = bpy.data.collections.new(name)\n\t\t\t\t\trelations.children.link(relation)\n\n\t\t\t\tfor member in rel.members:\n\n\t\t\t\t\t#todo: remove duplicate members\n\n\t\t\t\t\tfor obj in importedObjects:\n\t\t\t\t\t\t#id = int(obj.get('id', -1))\n\t\t\t\t\t\ttry:\n\t\t\t\t\t\t\tid = int(obj['id'])\n\t\t\t\t\t\texcept:\n\t\t\t\t\t\t\tid = None\n\t\t\t\t\t\tif id == member.ref:\n\t\t\t\t\t\t\ttry:\n\t\t\t\t\t\t\t\trelation.objects.link(obj)\n\t\t\t\t\t\t\texcept Exception as e:\n\t\t\t\t\t\t\t\tlog.error('Object {} already in group {}'.format(obj.name, name), exc_info=True)\n\n\t\t\t\t#cleanup\n\t\t\t\tif not relation.objects:\n\t\t\t\t\tbpy.data.collections.remove(relation)\n\n\n\n\n\n#######################\n\nclass IMPORTGIS_OT_osm_file(Operator, OSM_IMPORT):\n\n\tbl_idname = \"importgis.osm_file\"\n\tbl_description = 'Select and import osm xml file'\n\tbl_label = \"Import OSM\"\n\tbl_options = {\"UNDO\"}\n\n\t# Import dialog properties\n\tfilepath: StringProperty(\n\t\tname=\"File Path\",\n\t\tdescription=\"Filepath used for importing the file\",\n\t\tmaxlen=1024,\n\t\tsubtype='FILE_PATH' )\n\n\tfilename_ext = \".osm\"\n\n\tfilter_glob: StringProperty(\n\t\t\tdefault = \"*.osm\",\n\t\t\toptions = {'HIDDEN'} )\n\n\tdef invoke(self, context, event):\n\t\t#workaround to enum callback bug (T48873, T38489)\n\t\tglobal OSMTAGS\n\t\tOSMTAGS = getTags()\n\t\t#open file browser\n\t\tcontext.window_manager.fileselect_add(self)\n\t\treturn {'RUNNING_MODAL'}\n\n\tdef execute(self, context):\n\n\t\tscn = context.scene\n\n\t\tif not os.path.exists(self.filepath):\n\t\t\tself.report({'ERROR'}, \"Invalid file\")\n\t\t\treturn{'CANCELLED'}\n\n\t\ttry:\n\t\t\tbpy.ops.object.mode_set(mode='OBJECT')\n\t\texcept:\n\t\t\tpass\n\t\tbpy.ops.object.select_all(action='DESELECT')\n\n\t\t#Set cursor representation to 'loading' icon\n\t\tw = context.window\n\t\tw.cursor_set('WAIT')\n\n\t\t#Spatial ref system\n\t\tgeoscn = GeoScene(scn)\n\t\tif geoscn.isBroken:\n\t\t\tself.report({'ERROR'}, \"Scene georef is broken, please fix it beforehand\")\n\t\t\treturn {'CANCELLED'}\n\n\t\t#Parse file\n\t\tt0 = perf_clock()\n\t\tapi = overpy.Overpass()\n\t\t#with open(self.filepath, \"r\", encoding\"utf-8\") as f:\n\t\t#\tresult = api.parse_xml(f.read()) #WARNING read() load all the file into memory\n\t\tresult = api.parse_xml(self.filepath)\n\t\tt = perf_clock() - t0\n\t\tlog.info('File parsed in {} seconds'.format(round(t, 2)))\n\n\t\t#Get bbox\n\t\tbounds = result.bounds\n\t\tlon = (bounds[\"minlon\"] + bounds[\"maxlon\"])/2\n\t\tlat = (bounds[\"minlat\"] + bounds[\"maxlat\"])/2\n\t\t#Set CRS\n\t\tif not geoscn.hasCRS:\n\t\t\ttry:\n\t\t\t\tgeoscn.crs = utm.lonlat_to_epsg(lon, lat)\n\t\t\texcept Exception as e:\n\t\t\t\tlog.error(\"Cannot set UTM CRS\", exc_info=True)\n\t\t\t\tself.report({'ERROR'}, \"Cannot set UTM CRS, ckeck logs for more infos\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t#Set scene origin georef\n\t\tif not geoscn.hasOriginPrj:\n\t\t\tx, y = reprojPt(4326, geoscn.crs, lon, lat)\n\t\t\tgeoscn.setOriginPrj(x, y)\n\n\t\t#Build meshes\n\t\tt0 = perf_clock()\n\t\tself.build(context, result, geoscn.crs)\n\t\tt = perf_clock() - t0\n\t\tlog.info('Mesh build in {} seconds'.format(round(t, 2)))\n\n\t\tbbox = getBBOX.fromScn(scn)\n\t\tadjust3Dview(context, bbox)\n\n\t\treturn{'FINISHED'}\n\n\n\n\n########################\n\nclass IMPORTGIS_OT_osm_query(Operator, OSM_IMPORT):\n\t\"\"\"Import from Open Street Map\"\"\"\n\n\tbl_idname = \"importgis.osm_query\"\n\tbl_description = 'Query for Open Street Map data covering the current view3d area'\n\tbl_label = \"Get OSM\"\n\tbl_options = {\"UNDO\"}\n\n\t#special function to auto redraw an operator popup called through invoke_props_dialog\n\tdef check(self, context):\n\t\treturn True\n\n\n\t@classmethod\n\tdef poll(cls, context):\n\t\treturn context.mode == 'OBJECT'\n\n\n\tdef invoke(self, context, event):\n\t\t#workaround to enum callback bug (T48873, T38489)\n\t\tglobal OSMTAGS\n\t\tOSMTAGS = getTags()\n\n\t\treturn context.window_manager.invoke_props_dialog(self)\n\n\n\tdef execute(self, context):\n\n\t\tprefs = bpy.context.preferences.addons[PKG].preferences\n\t\tscn = context.scene\n\t\tgeoscn = GeoScene(scn)\n\t\tobjs = context.selected_objects\n\t\taObj = context.active_object\n\n\t\tif not geoscn.isGeoref:\n\t\t\t\tself.report({'ERROR'}, \"Scene is not georef\")\n\t\t\t\treturn {'CANCELLED'}\n\t\telif geoscn.isBroken:\n\t\t\t\tself.report({'ERROR'}, \"Scene georef is broken, please fix it beforehand\")\n\t\t\t\treturn {'CANCELLED'}\n\n\t\tif len(objs) == 1 and aObj.type == 'MESH':\n\t\t\tbbox = getBBOX.fromObj(aObj).toGeo(geoscn)\n\t\telif isTopView(context):\n\t\t\tbbox = getBBOX.fromTopView(context).toGeo(geoscn)\n\t\telse:\n\t\t\tself.report({'ERROR'}, \"Please define the query extent in orthographic top view or by selecting a reference object\")\n\t\t\treturn {'CANCELLED'}\n\n\t\tif bbox.dimensions.x > 20000 or bbox.dimensions.y > 20000:\n\t\t\tself.report({'ERROR'}, \"Too large extent\")\n\t\t\treturn {'CANCELLED'}\n\n\t\t#Get view3d bbox in lonlat\n\t\tbbox = reprojBbox(geoscn.crs, 4326, bbox)\n\n\t\t#Set cursor representation to 'loading' icon\n\t\tw = context.window\n\t\tw.cursor_set('WAIT')\n\n\t\t#Download from overpass api\n\t\tlog.debug('Requests overpass server : {}'.format(prefs.overpassServer))\n\t\tapi = overpy.Overpass(overpass_server=prefs.overpassServer, user_agent=USER_AGENT)\n\t\tquery = queryBuilder(bbox, tags=list(self.filterTags), types=list(self.featureType), format='xml')\n\t\tlog.debug('Overpass query : {}'.format(query)) # can fails with non utf8 chars\n\n\t\ttry:\n\t\t\tresult = api.query(query)\n\t\texcept Exception as e:\n\t\t\tlog.error(\"Overpass query failed\", exc_info=True)\n\t\t\tself.report({'ERROR'}, \"Overpass query failed, ckeck logs for more infos.\")\n\t\t\treturn {'CANCELLED'}\n\t\telse:\n\t\t\tlog.info('Overpass query successful')\n\n\t\tself.build(context, result, geoscn.crs)\n\n\t\tbbox = getBBOX.fromScn(scn)\n\t\tadjust3Dview(context, bbox, zoomToSelect=False)\n\n\t\treturn {'FINISHED'}\n\nclasses = [\n\tIMPORTGIS_OT_osm_file,\n\tIMPORTGIS_OT_osm_query\n]\n\ndef register():\n\tfor cls in classes:\n\t\ttry:\n\t\t\tbpy.utils.register_class(cls)\n\t\texcept ValueError as e:\n\t\t\tlog.warning('{} is already registered, now unregister and retry... '.format(cls))\n\t\t\tbpy.utils.unregister_class(cls)\n\t\t\tbpy.utils.register_class(cls)\n\ndef unregister():\n\tfor cls in classes:\n\t\tbpy.utils.unregister_class(cls)\n"
  },
  {
    "path": "operators/io_import_shp.py",
    "content": "# -*- coding:utf-8 -*-\nimport os, sys, time\nimport bpy\nfrom bpy.props import StringProperty, BoolProperty, EnumProperty, IntProperty\nfrom bpy.types import Operator\nimport bmesh\nimport math\nfrom mathutils import Vector\n\nimport logging\nlog = logging.getLogger(__name__)\n\nfrom ..core.lib.shapefile import Reader as shpReader\n\nfrom ..geoscene import GeoScene, georefManagerLayout\nfrom ..prefs import PredefCRS\nfrom ..core import BBOX\nfrom ..core.proj import Reproj\nfrom ..core.utils import perf_clock\n\nfrom .utils import adjust3Dview, getBBOX, DropToGround\n\nPKG, SUBPKG = __package__.split('.', maxsplit=1)\n\nfeatureType={\n0:'Null',\n1:'Point',\n3:'PolyLine',\n5:'Polygon',\n8:'MultiPoint',\n11:'PointZ',\n13:'PolyLineZ',\n15:'PolygonZ',\n18:'MultiPointZ',\n21:'PointM',\n23:'PolyLineM',\n25:'PolygonM',\n28:'MultiPointM',\n31:'MultiPatch'\n}\n\n\n\"\"\"\ndbf fields type:\n\tC is ASCII characters\n\tN is a double precision integer limited to around 18 characters in length\n\tD is for dates in the YYYYMMDD format, with no spaces or hyphens between the sections\n\tF is for floating point numbers with the same length limits as N\n\tL is for logical data which is stored in the shapefile's attribute table as a short integer as a 1 (true) or a 0 (false).\n\tThe values it can receive are 1, 0, y, n, Y, N, T, F or the python builtins True and False\n\"\"\"\n\n\nclass IMPORTGIS_OT_shapefile_file_dialog(Operator):\n\t\"\"\"Select shp file, loads the fields and start importgis.shapefile_props_dialog operator\"\"\"\n\n\tbl_idname = \"importgis.shapefile_file_dialog\"\n\tbl_description = 'Import ESRI shapefile (.shp)'\n\tbl_label = \"Import SHP\"\n\tbl_options = {'INTERNAL'}\n\n\t# Import dialog properties\n\tfilepath: StringProperty(\n\t\tname=\"File Path\",\n\t\tdescription=\"Filepath used for importing the file\",\n\t\tmaxlen=1024,\n\t\tsubtype='FILE_PATH' )\n\n\tfilename_ext = \".shp\"\n\n\tfilter_glob: StringProperty(\n\t\t\tdefault = \"*.shp\",\n\t\t\toptions = {'HIDDEN'} )\n\n\tdef invoke(self, context, event):\n\t\tcontext.window_manager.fileselect_add(self)\n\t\treturn {'RUNNING_MODAL'}\n\n\tdef draw(self, context):\n\t\tlayout = self.layout\n\t\tlayout.label(text=\"Options will be available\")\n\t\tlayout.label(text=\"after selecting a file\")\n\n\tdef execute(self, context):\n\t\tif os.path.exists(self.filepath):\n\t\t\tbpy.ops.importgis.shapefile_props_dialog('INVOKE_DEFAULT', filepath=self.filepath)\n\t\telse:\n\t\t\tself.report({'ERROR'}, \"Invalid filepath\")\n\t\treturn{'FINISHED'}\n\n\n\nclass IMPORTGIS_OT_shapefile_props_dialog(Operator):\n\t\"\"\"Shapefile importer properties dialog\"\"\"\n\n\tbl_idname = \"importgis.shapefile_props_dialog\"\n\tbl_description = 'Import ESRI shapefile (.shp)'\n\tbl_label = \"Import SHP\"\n\tbl_options = {\"INTERNAL\"}\n\n\tfilepath: StringProperty()\n\n\t#special function to auto redraw an operator popup called through invoke_props_dialog\n\tdef check(self, context):\n\t\treturn True\n\n\tdef listFields(self, context):\n\t\tfieldsItems = []\n\t\ttry:\n\t\t\tshp = shpReader(self.filepath)\n\t\texcept Exception as e:\n\t\t\tlog.warning(\"Unable to read shapefile fields\", exc_info=True)\n\t\t\treturn fieldsItems\n\t\tfields = [field for field in shp.fields if field[0] != 'DeletionFlag'] #ignore default DeletionFlag field\n\t\tfor i, field in enumerate(fields):\n\t\t\t#put each item in a tuple (key, label, tooltip)\n\t\t\tfieldsItems.append( (field[0], field[0], '') )\n\t\treturn fieldsItems\n\n\t# Shapefile CRS definition\n\tdef listPredefCRS(self, context):\n\t\treturn PredefCRS.getEnumItems()\n\n\tdef listObjects(self, context):\n\t\tobjs = []\n\t\tfor index, object in enumerate(bpy.context.scene.objects):\n\t\t\tif object.type == 'MESH':\n\t\t\t\t#put each object in a tuple (key, label, tooltip) and add this to the objects list\n\t\t\t\tobjs.append((object.name, object.name, \"Object named \" + object.name))\n\t\treturn objs\n\n\treprojection: BoolProperty(\n\t\t\tname=\"Specifiy shapefile CRS\",\n\t\t\tdescription=\"Specifiy shapefile CRS if it's different from scene CRS\",\n\t\t\tdefault=False )\n\n\tshpCRS: EnumProperty(\n\t\tname = \"Shapefile CRS\",\n\t\tdescription = \"Choose a Coordinate Reference System\",\n\t\titems = listPredefCRS)\n\n\t# Elevation source\n\tvertsElevSource: EnumProperty(\n\t\t\tname=\"Elevation source\",\n\t\t\tdescription=\"Select the source of vertices z value\",\n\t\t\titems=[\n\t\t\t('NONE', 'None', \"Flat geometry\"),\n\t\t\t('GEOM', 'Geometry', \"Use z value from shape geometry if exists\"),\n\t\t\t('FIELD', 'Field', \"Extract z elevation value from an attribute field\"),\n\t\t\t('OBJ', 'Object', \"Get z elevation value from an existing ground mesh\")\n\t\t\t],\n\t\t\tdefault='GEOM')\n\n\t# Elevation object\n\tobjElevLst: EnumProperty(\n\t\tname=\"Elev. object\",\n\t\tdescription=\"Choose the mesh from which extract z elevation\",\n\t\titems=listObjects )\n\n\t# Elevation field\n\t'''\n\tuseFieldElev: BoolProperty(\n\t\t\tname=\"Elevation from field\",\n\t\t\tdescription=\"Extract z elevation value from an attribute field\",\n\t\t\tdefault=False )\n\t'''\n\tfieldElevName: EnumProperty(\n\t\tname = \"Elev. field\",\n\t\tdescription = \"Choose field\",\n\t\titems = listFields )\n\n\t#Extrusion field\n\tuseFieldExtrude: BoolProperty(\n\t\t\tname=\"Extrusion from field\",\n\t\t\tdescription=\"Extract z extrusion value from an attribute field\",\n\t\t\tdefault=False )\n\n\tfieldExtrudeName: EnumProperty(\n\t\tname = \"Field\",\n\t\tdescription = \"Choose field\",\n\t\titems = listFields )\n\n\t#Extrusion axis\n\textrusionAxis: EnumProperty(\n\t\t\tname=\"Extrude along\",\n\t\t\tdescription=\"Select extrusion axis\",\n\t\t\titems=[ ('Z', 'z axis', \"Extrude along Z axis\"),\n\t\t\t('NORMAL', 'Normal', \"Extrude along normal\")] )\n\n\t#Create separate objects\n\tseparateObjects: BoolProperty(\n\t\t\tname=\"Separate objects\",\n\t\t\tdescription=\"Warning : can be very slow with lot of features\",\n\t\t\tdefault=False )\n\n\t#Name objects from field\n\tuseFieldName: BoolProperty(\n\t\t\tname=\"Object name from field\",\n\t\t\tdescription=\"Extract name for created objects from an attribute field\",\n\t\t\tdefault=False )\n\tfieldObjName: EnumProperty(\n\t\tname = \"Field\",\n\t\tdescription = \"Choose field\",\n\t\titems = listFields )\n\n\n\tdef draw(self, context):\n\t\t#Function used by blender to draw the panel.\n\t\tscn = context.scene\n\t\tlayout = self.layout\n\n\t\t#\n\t\tlayout.prop(self, 'vertsElevSource')\n\t\t#\n\t\t#layout.prop(self, 'useFieldElev')\n\t\tif self.vertsElevSource == 'FIELD':\n\t\t\tlayout.prop(self, 'fieldElevName')\n\t\telif self.vertsElevSource == 'OBJ':\n\t\t\tlayout.prop(self, 'objElevLst')\n\t\t#\n\t\tlayout.prop(self, 'useFieldExtrude')\n\t\tif self.useFieldExtrude:\n\t\t\tlayout.prop(self, 'fieldExtrudeName')\n\t\t\tlayout.prop(self, 'extrusionAxis')\n\t\t#\n\t\tlayout.prop(self, 'separateObjects')\n\t\tif self.separateObjects:\n\t\t\tlayout.prop(self, 'useFieldName')\n\t\telse:\n\t\t\tself.useFieldName = False\n\t\tif self.separateObjects and self.useFieldName:\n\t\t\tlayout.prop(self, 'fieldObjName')\n\t\t#\n\t\tgeoscn = GeoScene()\n\t\t#geoscnPrefs = context.preferences.addons['geoscene'].preferences\n\t\tif geoscn.isPartiallyGeoref:\n\t\t\tlayout.prop(self, 'reprojection')\n\t\t\tif self.reprojection:\n\t\t\t\tself.shpCRSInputLayout(context)\n\t\t\t#\n\t\t\tgeorefManagerLayout(self, context)\n\t\telse:\n\t\t\tself.shpCRSInputLayout(context)\n\n\n\tdef shpCRSInputLayout(self, context):\n\t\tlayout = self.layout\n\t\trow = layout.row(align=True)\n\t\t#row.prop(self, \"shpCRS\", text='CRS')\n\t\tsplit = row.split(factor=0.35, align=True)\n\t\tsplit.label(text='CRS:')\n\t\tsplit.prop(self, \"shpCRS\", text='')\n\t\trow.operator(\"bgis.add_predef_crs\", text='', icon='ADD')\n\n\n\tdef invoke(self, context, event):\n\t\treturn context.window_manager.invoke_props_dialog(self)\n\n\tdef execute(self, context):\n\n\t\t#elevField = self.fieldElevName if self.useFieldElev else \"\"\n\t\televField = self.fieldElevName if self.vertsElevSource == 'FIELD' else \"\"\n\t\textrudField = self.fieldExtrudeName if self.useFieldExtrude else \"\"\n\t\tnameField = self.fieldObjName if self.useFieldName else \"\"\n\t\tif self.vertsElevSource == 'OBJ':\n\t\t\tif not self.objElevLst:\n\t\t\t\tself.report({'ERROR'}, \"No elevation object\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\telse:\n\t\t\t\tobjElevName = self.objElevLst\n\t\telse:\n\t\t\tobjElevName = '' #will not be used\n\n\t\tgeoscn = GeoScene()\n\t\tif geoscn.isBroken:\n\t\t\tself.report({'ERROR'}, \"Scene georef is broken, please fix it beforehand\")\n\t\t\treturn {'CANCELLED'}\n\n\t\tif geoscn.isGeoref:\n\t\t\tif self.reprojection:\n\t\t\t\tshpCRS = self.shpCRS\n\t\t\telse:\n\t\t\t\tshpCRS = geoscn.crs\n\t\telse:\n\t\t\tshpCRS = self.shpCRS\n\n\t\ttry:\n\t\t\tbpy.ops.importgis.shapefile('INVOKE_DEFAULT', filepath=self.filepath, shpCRS=shpCRS, elevSource=self.vertsElevSource,\n\t\t\t\tfieldElevName=elevField, objElevName=objElevName, fieldExtrudeName=extrudField, fieldObjName=nameField,\n\t\t\t\textrusionAxis=self.extrusionAxis, separateObjects=self.separateObjects)\n\t\texcept Exception as e:\n\t\t\tlog.error('Shapefile import fails', exc_info=True)\n\t\t\tself.report({'ERROR'}, 'Shapefile import fails, check logs.')\n\t\t\treturn {'CANCELLED'}\n\n\t\treturn{'FINISHED'}\n\n\nclass IMPORTGIS_OT_shapefile(Operator):\n\t\"\"\"Import from ESRI shapefile file format (.shp)\"\"\"\n\n\tbl_idname = \"importgis.shapefile\" # important since its how bpy.ops.import.shapefile is constructed (allows calling operator from python console or another script)\n\t#bl_idname rules: must contain one '.' (dot) charactere, no capital letters, no reserved words (like 'import')\n\tbl_description = 'Import ESRI shapefile (.shp)'\n\tbl_label = \"Import SHP\"\n\tbl_options = {\"UNDO\"}\n\n\tfilepath: StringProperty()\n\n\tshpCRS: StringProperty(name = \"Shapefile CRS\", description = \"Coordinate Reference System\")\n\n\televSource: StringProperty(name = \"Elevation source\", description = \"Elevation source\", default='GEOM') # [NONE, GEOM, OBJ, FIELD]\n\tobjElevName: StringProperty(name = \"Elevation object name\", description = \"\")\n\n\tfieldElevName: StringProperty(name = \"Elevation field\", description = \"Field name\")\n\tfieldExtrudeName: StringProperty(name = \"Extrusion field\", description = \"Field name\")\n\tfieldObjName: StringProperty(name = \"Objects names field\", description = \"Field name\")\n\n\t#Extrusion axis\n\textrusionAxis: EnumProperty(\n\t\t\tname=\"Extrude along\",\n\t\t\tdescription=\"Select extrusion axis\",\n\t\t\titems=[ ('Z', 'z axis', \"Extrude along Z axis\"),\n\t\t\t('NORMAL', 'Normal', \"Extrude along normal\")]\n\t\t\t)\n\t#Create separate objects\n\tseparateObjects: BoolProperty(\n\t\t\tname=\"Separate objects\",\n\t\t\tdescription=\"Import to separate objects instead one large object\",\n\t\t\tdefault=False\n\t\t\t)\n\n\t@classmethod\n\tdef poll(cls, context):\n\t\treturn context.mode == 'OBJECT'\n\n\tdef __del__(self):\n\t\tbpy.context.window.cursor_set('DEFAULT')\n\n\tdef execute(self, context):\n\n\t\tprefs = bpy.context.preferences.addons[PKG].preferences\n\n\t\t#Set cursor representation to 'loading' icon\n\t\tw = context.window\n\t\tw.cursor_set('WAIT')\n\t\tt0 = perf_clock()\n\n\t\tbpy.ops.object.select_all(action='DESELECT')\n\n\t\t#Path\n\t\tshpName = os.path.basename(self.filepath)[:-4]\n\n\t\t#Get shp reader\n\t\tlog.info(\"Read shapefile...\")\n\t\ttry:\n\t\t\tshp = shpReader(self.filepath)\n\t\texcept Exception as e:\n\t\t\tlog.error(\"Unable to read shapefile\", exc_info=True)\n\t\t\tself.report({'ERROR'}, \"Unable to read shapefile, check logs\")\n\t\t\treturn {'CANCELLED'}\n\n\t\t#Check shape type\n\t\tshpType = featureType[shp.shapeType]\n\t\tlog.info('Feature type : ' + shpType)\n\t\tif shpType not in ['Point','PolyLine','Polygon','PointZ','PolyLineZ','PolygonZ']:\n\t\t\tself.report({'ERROR'}, \"Cannot process multipoint, multipointZ, pointM, polylineM, polygonM and multipatch feature type\")\n\t\t\treturn {'CANCELLED'}\n\n\t\tif self.elevSource != 'FIELD':\n\t\t\tself.fieldElevName = ''\n\n\t\tif self.elevSource == 'OBJ':\n\t\t\tscn = bpy.context.scene\n\t\t\televObj = scn.objects[self.objElevName]\n\t\t\trayCaster = DropToGround(scn, elevObj)\n\n\t\t#Get fields\n\t\tfields = [field for field in shp.fields if field[0] != 'DeletionFlag'] #ignore default DeletionFlag field\n\t\tfieldsNames = [field[0] for field in fields]\n\t\tlog.debug(\"DBF fields : \"+str(fieldsNames))\n\n\t\tif self.separateObjects or self.fieldElevName or self.fieldObjName or self.fieldExtrudeName:\n\t\t\tself.useDbf = True\n\t\telse:\n\t\t\tself.useDbf = False\n\n\t\tif self.fieldObjName and self.separateObjects:\n\t\t\ttry:\n\t\t\t\tnameFieldIdx = fieldsNames.index(self.fieldObjName)\n\t\t\texcept Exception as e:\n\t\t\t\tlog.error('Unable to find name field', exc_info=True)\n\t\t\t\tself.report({'ERROR'}, \"Unable to find name field\")\n\t\t\t\treturn {'CANCELLED'}\n\n\t\tif self.fieldElevName:\n\t\t\ttry:\n\t\t\t\tzFieldIdx = fieldsNames.index(self.fieldElevName)\n\t\t\texcept Exception as e:\n\t\t\t\tlog.error('Unable to find elevation field', exc_info=True)\n\t\t\t\tself.report({'ERROR'}, \"Unable to find elevation field\")\n\t\t\t\treturn {'CANCELLED'}\n\n\t\t\tif fields[zFieldIdx][1] not in ['N', 'F', 'L'] :\n\t\t\t\tself.report({'ERROR'}, \"Elevation field do not contains numeric values\")\n\t\t\t\treturn {'CANCELLED'}\n\n\t\tif self.fieldExtrudeName:\n\t\t\ttry:\n\t\t\t\textrudeFieldIdx = fieldsNames.index(self.fieldExtrudeName)\n\t\t\texcept ValueError:\n\t\t\t\tlog.error('Unable to find extrusion field', exc_info=True)\n\t\t\t\tself.report({'ERROR'}, \"Unable to find extrusion field\")\n\t\t\t\treturn {'CANCELLED'}\n\n\t\t\tif fields[extrudeFieldIdx][1] not in ['N', 'F', 'L'] :\n\t\t\t\tself.report({'ERROR'}, \"Extrusion field do not contains numeric values\")\n\t\t\t\treturn {'CANCELLED'}\n\n\t\t#Get shp and scene georef infos\n\t\tshpCRS = self.shpCRS\n\t\tgeoscn = GeoScene()\n\t\tif geoscn.isBroken:\n\t\t\tself.report({'ERROR'}, \"Scene georef is broken, please fix it beforehand\")\n\t\t\treturn {'CANCELLED'}\n\n\t\tscale = geoscn.scale #TODO\n\n\t\tif not geoscn.hasCRS: #if not geoscn.isGeoref:\n\t\t\ttry:\n\t\t\t\tgeoscn.crs = shpCRS\n\t\t\texcept Exception as e:\n\t\t\t\tlog.error(\"Cannot set scene crs\", exc_info=True)\n\t\t\t\tself.report({'ERROR'}, \"Cannot set scene crs, check logs for more infos\")\n\t\t\t\treturn {'CANCELLED'}\n\n\t\t#Init reprojector class\n\t\tif geoscn.crs != shpCRS:\n\t\t\tlog.info(\"Data will be reprojected from {} to {}\".format(shpCRS, geoscn.crs))\n\t\t\ttry:\n\t\t\t\trprj = Reproj(shpCRS, geoscn.crs)\n\t\t\texcept Exception as e:\n\t\t\t\tlog.error('Reprojection fails', exc_info=True)\n\t\t\t\tself.report({'ERROR'}, \"Unable to reproject data, check logs for more infos.\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\tif rprj.iproj == 'EPSGIO':\n\t\t\t\tif shp.numRecords > 100:\n\t\t\t\t\tself.report({'ERROR'}, \"Reprojection through online epsg.io engine is limited to 100 features. \\nPlease install GDAL or pyproj module.\")\n\t\t\t\t\treturn {'CANCELLED'}\n\n\t\t#Get bbox\n\t\tbbox = BBOX(shp.bbox)\n\t\tif geoscn.crs != shpCRS:\n\t\t\tbbox = rprj.bbox(bbox)\n\n\t\t#Get or set georef dx, dy\n\t\tif not geoscn.isGeoref:\n\t\t\tdx, dy = bbox.center\n\t\t\tgeoscn.setOriginPrj(dx, dy)\n\t\telse:\n\t\t\tdx, dy = geoscn.getOriginPrj()\n\n\t\t#Get reader iterator (using iterator avoids loading all data in memory)\n\t\t#warn, shp with zero field will return an empty shapeRecords() iterator\n\t\t#to prevent this issue, iter only on shapes if there is no field required\n\t\tif self.useDbf:\n\t\t\t#Note: using shapeRecord solve the issue where number of shapes does not match number of table records\n\t\t\t#because it iter only on features with geom and record\n\t\t\tshpIter = shp.iterShapeRecords()\n\t\telse:\n\t\t\tshpIter = shp.iterShapes()\n\t\tnbFeats = shp.numRecords\n\n\t\t#Create an empty BMesh\n\t\tbm = bmesh.new()\n\t\t#Extrusion is exponentially slow with large bmesh\n\t\t#it's fastest to extrude a small bmesh and then join it to a final large bmesh\n\t\tif not self.separateObjects and self.fieldExtrudeName:\n\t\t\tfinalBm = bmesh.new()\n\n\t\tprogress = -1\n\n\t\tif self.separateObjects:\n\t\t\tlayer = bpy.data.collections.new(shpName)\n\t\t\tcontext.scene.collection.children.link(layer)\n\n\t\t#Main iteration over features\n\t\tfor i, feat in enumerate(shpIter):\n\n\t\t\tif self.useDbf:\n\t\t\t\tshape = feat.shape\n\t\t\t\trecord = feat.record\n\t\t\telse:\n\t\t\t\tshape = feat\n\n\t\t\t#Progress infos\n\t\t\tpourcent = round(((i+1)*100)/nbFeats)\n\t\t\tif pourcent in list(range(0, 110, 10)) and pourcent != progress:\n\t\t\t\tprogress = pourcent\n\t\t\t\tif pourcent == 100:\n\t\t\t\t\tprint(str(pourcent)+'%')\n\t\t\t\telse:\n\t\t\t\t\tprint(str(pourcent), end=\"%, \")\n\t\t\t\tsys.stdout.flush() #we need to flush or it won't print anything until after the loop has finished\n\n\t\t\t#Deal with multipart features\n\t\t\t#If the shape record has multiple parts, the 'parts' attribute will contains the index of\n\t\t\t#the first point of each part. If there is only one part then a list containing 0 is returned\n\t\t\tif (shpType == 'PointZ' or shpType == 'Point'): #point layer has no attribute 'parts'\n\t\t\t\tpartsIdx = [0]\n\t\t\telse:\n\t\t\t\ttry: #prevent \"_shape object has no attribute parts\" error\n\t\t\t\t\tpartsIdx = shape.parts\n\t\t\t\texcept Exception as e:\n\t\t\t\t\tlog.warning('Cannot access \"parts\" attribute for feature {} : {}'.format(i, e))\n\t\t\t\t\tpartsIdx = [0]\n\t\t\tnbParts = len(partsIdx)\n\n\t\t\t#Get list of shape's points\n\t\t\tpts = shape.points\n\t\t\tnbPts = len(pts)\n\n\t\t\t#Skip null geom\n\t\t\tif nbPts == 0:\n\t\t\t\tcontinue #go to next iteration of the loop\n\n\t\t\t#Reproj geom\n\t\t\tif geoscn.crs != shpCRS:\n\t\t\t\tpts = rprj.pts(pts)\n\n\t\t\t#Get extrusion offset\n\t\t\tif self.fieldExtrudeName:\n\t\t\t\ttry:\n\t\t\t\t\toffset = float(record[extrudeFieldIdx])\n\t\t\t\texcept Exception as e:\n\t\t\t\t\tlog.warning('Cannot extract extrusion value for feature {} : {}'.format(i, e))\n\t\t\t\t\toffset = 0 #null values will be set to zero\n\n\t\t\t#Iter over parts\n\t\t\tfor j in range(nbParts):\n\n\t\t\t\t# EXTRACT 3D GEOM\n\n\t\t\t\tgeom = [] #will contains a list of 3d points\n\n\t\t\t\t#Find first and last part index\n\t\t\t\tidx1 = partsIdx[j]\n\t\t\t\tif j+1 == nbParts:\n\t\t\t\t\tidx2 = nbPts\n\t\t\t\telse:\n\t\t\t\t\tidx2 = partsIdx[j+1]\n\n\t\t\t\t#Build 3d geom\n\t\t\t\tfor k, pt in enumerate(pts[idx1:idx2]):\n\n\t\t\t\t\tif self.elevSource == 'OBJ':\n\t\t\t\t\t\trcHit = rayCaster.rayCast(x=pt[0]-dx, y=pt[1]-dy)\n\t\t\t\t\t\tz = rcHit.loc.z #will be automatically set to zero if not rcHit.hit\n\n\t\t\t\t\telif self.elevSource == 'FIELD':\n\t\t\t\t\t\ttry:\n\t\t\t\t\t\t\tz = float(record[zFieldIdx])\n\t\t\t\t\t\texcept Exception as e:\n\t\t\t\t\t\t\tlog.warning('Cannot extract elevation value for feature {} : {}'.format(i, e))\n\t\t\t\t\t\t\tz = 0 #null values will be set to zero\n\n\t\t\t\t\telif shpType[-1] == 'Z' and self.elevSource == 'GEOM':\n\t\t\t\t\t\tz = shape.z[idx1:idx2][k]\n\n\t\t\t\t\telse:\n\t\t\t\t\t\tz = 0\n\n\t\t\t\t\tgeom.append((pt[0], pt[1], z))\n\n\t\t\t\t#Shift coords\n\t\t\t\tgeom = [(pt[0]-dx, pt[1]-dy, pt[2]) for pt in geom]\n\n\n\t\t\t\t# BUILD BMESH\n\n\t\t\t\t# POINTS\n\t\t\t\tif (shpType == 'PointZ' or shpType == 'Point'):\n\t\t\t\t\tvert = [bm.verts.new(pt) for pt in geom]\n\t\t\t\t\t#Extrusion\n\t\t\t\t\tif self.fieldExtrudeName and offset > 0:\n\t\t\t\t\t\tvect = (0, 0, offset) #along Z\n\t\t\t\t\t\tresult = bmesh.ops.extrude_vert_indiv(bm, verts=vert)\n\t\t\t\t\t\tverts = result['verts']\n\t\t\t\t\t\tbmesh.ops.translate(bm, verts=verts, vec=vect)\n\n\t\t\t\t# LINES\n\t\t\t\tif (shpType == 'PolyLine' or shpType == 'PolyLineZ'):\n\t\t\t\t\tverts = [bm.verts.new(pt) for pt in geom]\n\t\t\t\t\tedges = []\n\t\t\t\t\tfor i in range(len(geom)-1):\n\t\t\t\t\t\tedge = bm.edges.new( [verts[i], verts[i+1] ])\n\t\t\t\t\t\tedges.append(edge)\n\t\t\t\t\t#Extrusion\n\t\t\t\t\tif self.fieldExtrudeName and offset > 0:\n\t\t\t\t\t\tvect = (0, 0, offset) # along Z\n\t\t\t\t\t\tresult = bmesh.ops.extrude_edge_only(bm, edges=edges)\n\t\t\t\t\t\tverts = [elem for elem in result['geom'] if isinstance(elem, bmesh.types.BMVert)]\n\t\t\t\t\t\tbmesh.ops.translate(bm, verts=verts, vec=vect)\n\n\t\t\t\t# NGONS\n\t\t\t\tif (shpType == 'Polygon' or shpType == 'PolygonZ'):\n\t\t\t\t\t#According to the shapefile spec, polygons points are clockwise and polygon holes are counterclockwise\n\t\t\t\t\t#in Blender face is up if points are in anticlockwise order\n\t\t\t\t\tgeom.reverse() #face up\n\t\t\t\t\tgeom.pop() #exlude last point because it's the same as first pt\n\t\t\t\t\tif len(geom) >= 3: #needs 3 points to get a valid face\n\t\t\t\t\t\tverts = [bm.verts.new(pt) for pt in geom]\n\t\t\t\t\t\tface = bm.faces.new(verts)\n\t\t\t\t\t\t#update normal to avoid null vector\n\t\t\t\t\t\tface.normal_update()\n\t\t\t\t\t\tif face.normal.z < 0: #this is a polygon hole, bmesh cannot handle polygon hole\n\t\t\t\t\t\t\tpass #TODO\n\t\t\t\t\t\t#Extrusion\n\t\t\t\t\t\tif self.fieldExtrudeName and offset > 0:\n\t\t\t\t\t\t\t#build translate vector\n\t\t\t\t\t\t\tif self.extrusionAxis == 'NORMAL':\n\t\t\t\t\t\t\t\tnormal = face.normal\n\t\t\t\t\t\t\t\tvect = normal * offset\n\t\t\t\t\t\t\telif self.extrusionAxis == 'Z':\n\t\t\t\t\t\t\t\tvect = (0, 0, offset)\n\t\t\t\t\t\t\tfaces = bmesh.ops.extrude_discrete_faces(bm, faces=[face]) #return {'faces': [BMFace]}\n\t\t\t\t\t\t\tverts = faces['faces'][0].verts\n\t\t\t\t\t\t\tif self.elevSource == 'OBJ':\n\t\t\t\t\t\t\t\t# Making flat roof (TODO add an user input parameter to setup this behaviour)\n\t\t\t\t\t\t\t\tz = max([v.co.z for v in verts]) + offset #get max z coord\n\t\t\t\t\t\t\t\tfor v in verts:\n\t\t\t\t\t\t\t\t\tv.co.z = z\n\t\t\t\t\t\t\telse:\n\t\t\t\t\t\t\t\t##result = bmesh.ops.extrude_face_region(bm, geom=[face]) #return dict {\"geom\":[BMVert, BMEdge, BMFace]}\n\t\t\t\t\t\t\t\t##verts = [elem for elem in result['geom'] if isinstance(elem, bmesh.types.BMVert)] #geom type filter\n\t\t\t\t\t\t\t\tbmesh.ops.translate(bm, verts=verts, vec=vect)\n\n\n\t\t\tif self.separateObjects:\n\n\t\t\t\tif self.fieldObjName:\n\t\t\t\t\ttry:\n\t\t\t\t\t\tname = record[nameFieldIdx]\n\t\t\t\t\texcept Exception as e:\n\t\t\t\t\t\tlog.warning('Cannot extract name value for feature {} : {}'.format(i, e))\n\t\t\t\t\t\tname = ''\n\t\t\t\t\t# null values will return a bytes object containing a blank string of length equal to fields length definition\n\t\t\t\t\tif isinstance(name, bytes):\n\t\t\t\t\t\tname = ''\n\t\t\t\t\telse:\n\t\t\t\t\t\tname = str(name)\n\t\t\t\telse:\n\t\t\t\t\tname = shpName\n\n\t\t\t\t#Calc bmesh bbox\n\t\t\t\t_bbox = getBBOX.fromBmesh(bm)\n\n\t\t\t\t#Calc bmesh geometry origin and translate coords according to it\n\t\t\t\t#then object location will be set to initial bmesh origin\n\t\t\t\t#its a work around to bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY')\n\t\t\t\tox, oy, oz = _bbox.center\n\t\t\t\toz = _bbox.zmin\n\t\t\t\tbmesh.ops.translate(bm, verts=bm.verts, vec=(-ox, -oy, -oz))\n\n\t\t\t\t#Create new mesh from bmesh\n\t\t\t\tmesh = bpy.data.meshes.new(name)\n\t\t\t\tbm.to_mesh(mesh)\n\t\t\t\tbm.clear()\n\n\t\t\t\t#Validate new mesh\n\t\t\t\tmesh.validate(verbose=False)\n\n\t\t\t\t#Place obj\n\t\t\t\tobj = bpy.data.objects.new(name, mesh)\n\t\t\t\tlayer.objects.link(obj)\n\t\t\t\tcontext.view_layer.objects.active = obj\n\t\t\t\tobj.select_set(True)\n\t\t\t\tobj.location = (ox, oy, oz)\n\n\t\t\t\t# bpy operators can be very cumbersome when scene contains lot of objects\n\t\t\t\t# because it cause implicit scene updates calls\n\t\t\t\t# so we must avoid using operators when created many objects with the 'separate objects' option)\n\t\t\t\t##bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY')\n\n\t\t\t\t#write attributes data\n\t\t\t\tfor i, field in enumerate(shp.fields):\n\t\t\t\t\tfieldName, fieldType, fieldLength, fieldDecLength = field\n\t\t\t\t\tif fieldName != 'DeletionFlag':\n\t\t\t\t\t\tif fieldType in ('N', 'F'):\n\t\t\t\t\t\t\tv = record[i-1]\n\t\t\t\t\t\t\tif v is not None:\n\t\t\t\t\t\t\t\t#cast to float to avoid overflow error when affecting custom property\n\t\t\t\t\t\t\t\tobj[fieldName] = float(record[i-1])\n\t\t\t\t\t\telse:\n\t\t\t\t\t\t\tobj[fieldName] = record[i-1]\n\n\t\t\telif self.fieldExtrudeName:\n\t\t\t\t#Join to final bmesh (use from_mesh method hack)\n\t\t\t\tbuff = bpy.data.meshes.new(\".temp\")\n\t\t\t\tbm.to_mesh(buff)\n\t\t\t\tfinalBm.from_mesh(buff)\n\t\t\t\tbpy.data.meshes.remove(buff)\n\t\t\t\tbm.clear()\n\n\t\t#Write back the whole mesh\n\t\tif not self.separateObjects:\n\n\t\t\tmesh = bpy.data.meshes.new(shpName)\n\n\t\t\tif self.fieldExtrudeName:\n\t\t\t\tbm.free()\n\t\t\t\tbm = finalBm\n\n\t\t\tif prefs.mergeDoubles:\n\t\t\t\tbmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001)\n\t\t\tbm.to_mesh(mesh)\n\n\t\t\t#Finish\n\t\t\t#mesh.update(calc_edges=True)\n\t\t\tmesh.validate(verbose=False) #return true if the mesh has been corrected\n\t\t\tobj = bpy.data.objects.new(shpName, mesh)\n\t\t\tcontext.scene.collection.objects.link(obj)\n\t\t\tcontext.view_layer.objects.active = obj\n\t\t\tobj.select_set(True)\n\t\t\tbpy.ops.object.origin_set(type='ORIGIN_GEOMETRY')\n\n\t\t#free the bmesh\n\t\tbm.free()\n\n\t\tt = perf_clock() - t0\n\t\tlog.info('Build in %f seconds' % t)\n\n\t\t#Adjust grid size\n\t\tif prefs.adjust3Dview:\n\t\t\tbbox.shift(-dx, -dy) #convert shapefile bbox in 3d view space\n\t\t\tadjust3Dview(context, bbox)\n\n\n\t\treturn {'FINISHED'}\n\nclasses = [\n\tIMPORTGIS_OT_shapefile_file_dialog,\n\tIMPORTGIS_OT_shapefile_props_dialog,\n\tIMPORTGIS_OT_shapefile\n]\n\ndef register():\n\tfor cls in classes:\n\t\ttry:\n\t\t\tbpy.utils.register_class(cls)\n\t\texcept ValueError as e:\n\t\t\tlog.warning('{} is already registered, now unregister and retry... '.format(cls))\n\t\t\tbpy.utils.unregister_class(cls)\n\t\t\tbpy.utils.register_class(cls)\n\ndef unregister():\n\tfor cls in classes:\n\t\tbpy.utils.unregister_class(cls)\n"
  },
  {
    "path": "operators/lib/osm/nominatim.py",
    "content": "import os, ssl\n\nimport logging\nlog = logging.getLogger(__name__)\n\nimport json\n\nfrom urllib.request import urlopen\nfrom urllib.request import Request\nfrom urllib.parse import quote_plus\n\nTIMEOUT = 2\n\ndef nominatimQuery(\n    query,\n    base_url = 'https://nominatim.openstreetmap.org/',\n    referer = None,\n    user_agent = None,\n    format = 'json',\n    limit = 10):\n\n    url = base_url + 'search?'\n    url += 'format=' + format\n    url += '&q=' + quote_plus(query)\n    url += '&limit=' + str(limit)\n\n    log.debug('Nominatim search request : {}'.format(url))\n\n    req = Request(url)\n    if referer:\n        req.add_header('Referer', referer)\n    if user_agent:\n        req.add_header('User-Agent', user_agent)\n\n    response = urlopen(req, timeout=TIMEOUT)\n\n    r = json.loads(response.read().decode('utf-8'))\n\n    return r\n"
  },
  {
    "path": "operators/lib/osm/overpy/__about__.py",
    "content": "__all__ = [\n    \"__author__\",\n    \"__copyright__\",\n    \"__email__\",\n    \"__license__\",\n    \"__summary__\",\n    \"__title__\",\n    \"__uri__\",\n    \"__version__\",\n]\n\n__title__ = \"overpy\"\n__summary__ = \"Python Wrapper to access the OpenStreepMap Overpass API\"\n__uri__ = \"https://github.com/DinoTools/python-overpy\"\n\n__version__ = \"0.3.1\"\n\n__author__ = \"PhiBo (DinoTools)\"\n__email__ = \"\"\n\n__license__ = \"MIT\"\n__copyright__ = \"Copyright 2014-2015 %s\" % __author__\n"
  },
  {
    "path": "operators/lib/osm/overpy/__init__.py",
    "content": "from collections import OrderedDict\nfrom decimal import Decimal\nimport re\nimport sys\nimport os\n\nfrom . import exception\nfrom .__about__ import (\n    __author__, __copyright__, __email__, __license__, __summary__, __title__,\n    __uri__, __version__\n)\n\nimport xml.etree.ElementTree as ET\nimport json\n\nPY2 = sys.version_info[0] == 2\nPY3 = sys.version_info[0] == 3\n\nif PY2:\n    from StringIO import StringIO\n    from urllib2 import urlopen\n    from urllib2 import HTTPError\nelif PY3:\n    from io import StringIO\n    from urllib.request import urlopen, Request\n    from urllib.error import HTTPError\n\nTIMEOUT = 120\n\ndef is_valid_type(element, cls):\n    \"\"\"\n    Test if an element is of a given type.\n\n    :param Element() element: The element instance to test\n    :param Element cls: The element class to test\n    :return: False or True\n    :rtype: Boolean\n    \"\"\"\n    return isinstance(element, cls) and element.id is not None\n\n\nclass Overpass(object):\n\n    \"\"\"\n    Class to access the Overpass API\n    \"\"\"\n    default_read_chunk_size = 4096\n\n    def __init__(self, overpass_server=\"http://overpass-api.de/api/interpreter\", read_chunk_size=None, referer=None, user_agent=None):\n        \"\"\"\n        :param read_chunk_size: Max size of each chunk read from the server response\n        :type read_chunk_size: Integer\n        \"\"\"\n        self.referer = referer\n        self.user_agent = user_agent\n        self.url = overpass_server\n        self._regex_extract_error_msg = re.compile(b\"\\<p\\>(?P<msg>\\<strong\\s.*?)\\</p\\>\")\n        self._regex_remove_tag = re.compile(b\"<[^>]*?>\")\n        if read_chunk_size is None:\n            read_chunk_size = self.default_read_chunk_size\n        self.read_chunk_size = read_chunk_size\n\n    def query(self, query):\n        \"\"\"\n        Query the Overpass API\n\n        :param String|Bytes query: The query string in Overpass QL\n        :return: The parsed result\n        :rtype: overpy.Result\n        \"\"\"\n        if not isinstance(query, bytes):\n            query = query.encode(\"utf-8\")\n\n        req = Request(self.url)\n        if self.referer:\n            req.add_header('Referer', self.referer)\n        if self.user_agent:\n            req.add_header('User-Agent', self.user_agent)\n\n        try:\n            f = urlopen(req, query, timeout=TIMEOUT)\n        except HTTPError as e:\n            f = e\n\n        response = f.read(self.read_chunk_size)\n        while True:\n            data = f.read(self.read_chunk_size)\n            if len(data) == 0:\n                break\n            response = response + data\n        f.close()\n\n        if f.code == 200:\n            if PY2:\n                http_info = f.info()\n                content_type = http_info.getheader(\"content-type\")\n            else:\n                content_type = f.getheader(\"Content-Type\")\n\n            if content_type == \"application/json\":\n                return self.parse_json(response)\n\n            if content_type == \"application/osm3s+xml\":\n                return self.parse_xml(response)\n\n            raise exception.OverpassUnknownContentType(content_type)\n\n        if f.code == 400:\n            msgs = []\n            for msg in self._regex_extract_error_msg.finditer(response):\n                tmp = self._regex_remove_tag.sub(b\"\", msg.group(\"msg\"))\n                try:\n                    tmp = tmp.decode(\"utf-8\")\n                except UnicodeDecodeError:\n                    tmp = repr(tmp)\n                msgs.append(tmp)\n\n            raise exception.OverpassBadRequest(\n                query,\n                msgs=msgs\n            )\n\n        if f.code == 429:\n            raise exception.OverpassTooManyRequests\n\n        if f.code == 504:\n            raise exception.OverpassGatewayTimeout\n\n        raise exception.OverpassUnknownHTTPStatusCode(f.code)\n\n    def parse_json(self, data, encoding=\"utf-8\"):\n        \"\"\"\n        Parse raw response from Overpass service.\n\n        :param data: Raw JSON Data\n        :type data: String or Bytes\n        :param encoding: Encoding to decode byte string\n        :type encoding: String\n        :return: Result object\n        :rtype: overpy.Result\n        \"\"\"\n        if isinstance(data, bytes):\n            data = data.decode(encoding)\n        data = json.loads(data, parse_float=Decimal)\n        return Result.from_json(data, api=self)\n\n    def parse_xml(self, data, encoding=\"utf-8\"):\n        \"\"\"\n\n        :param data: Raw XML Data\n        :type data: String or Bytes\n        :param encoding: Encoding to decode byte string\n        :type encoding: String\n        :return: Result object\n        :rtype: overpy.Result\n        \"\"\"\n\n        try:\n            isFile = os.path.exists(data)\n        except:\n            isFile = False\n        if not isFile:\n\n            if isinstance(data, bytes):\n                data = data.decode(encoding)\n            if PY2 and not isinstance(data, str):\n                # Python 2.x: Convert unicode strings\n                data = data.encode(encoding)\n\n        return Result.from_xml(data, api=self)\n\n\nclass Result(object):\n\n    \"\"\"\n    Class to handle the result.\n    \"\"\"\n\n    def __init__(self, elements=None, api=None):\n        \"\"\"\n\n        :param List elements:\n        :param api:\n        :type api: overpy.Overpass\n        \"\"\"\n        if elements is None:\n            elements = []\n        self._nodes = OrderedDict((element.id, element) for element in elements if is_valid_type(element, Node))\n        self._ways = OrderedDict((element.id, element) for element in elements if is_valid_type(element, Way))\n        self._relations = OrderedDict((element.id, element)\n                                      for element in elements if is_valid_type(element, Relation))\n        self._class_collection_map = {Node: self._nodes, Way: self._ways, Relation: self._relations}\n        self.api = api\n        self._bounds = {}\n\n    def expand(self, other):\n        \"\"\"\n        Add all elements from an other result to the list of elements of this result object.\n\n        It is used by the auto resolve feature.\n\n        :param other: Expand the result with the elements from this result.\n        :type other: overpy.Result\n        :raises ValueError: If provided parameter is not instance of :class:`overpy.Result`\n        \"\"\"\n        if not isinstance(other, Result):\n            raise ValueError(\"Provided argument has to be instance of overpy:Result()\")\n\n        other_collection_map = {Node: other.nodes, Way: other.ways, Relation: other.relations}\n        for element_type, own_collection in self._class_collection_map.items():\n            for element in other_collection_map[element_type]:\n                if is_valid_type(element, element_type) and element.id not in own_collection:\n                    own_collection[element.id] = element\n\n    def append(self, element):\n        \"\"\"\n        Append a new element to the result.\n\n        :param element: The element to append\n        :type element: overpy.Element\n        \"\"\"\n        if is_valid_type(element, Element):\n            self._class_collection_map[element.__class__].setdefault(element.id, element)\n\n    def get_elements(self, filter_cls, elem_id=None):\n        \"\"\"\n        Get a list of elements from the result and filter the element type by a class.\n\n        :param filter_cls:\n        :param elem_id: ID of the object\n        :type elem_id: Integer\n        :return: List of available elements\n        :rtype: List\n        \"\"\"\n        result = []\n        if elem_id is not None:\n            try:\n                result = [self._class_collection_map[filter_cls][elem_id]]\n            except KeyError:\n                result = []\n        else:\n            for e in self._class_collection_map[filter_cls].values():\n                result.append(e)\n        return result\n\n    def get_ids(self, filter_cls):\n        \"\"\"\n\n        :param filter_cls:\n        :return:\n        \"\"\"\n        return list(self._class_collection_map[filter_cls].keys())\n\n    def get_node_ids(self):\n        return self.get_ids(filter_cls=Node)\n\n    def get_way_ids(self):\n        return self.get_ids(filter_cls=Way)\n\n    def get_relation_ids(self):\n        return self.get_ids(filter_cls=Relation)\n\n    @classmethod\n    def from_json(cls, data, api=None):\n        \"\"\"\n        Create a new instance and load data from json object.\n\n        :param data: JSON data returned by the Overpass API\n        :type data: Dict\n        :param api:\n        :type api: overpy.Overpass\n        :return: New instance of Result object\n        :rtype: overpy.Result\n        \"\"\"\n        result = cls(api=api)\n        for elem_cls in [Node, Way, Relation]:\n            for element in data.get(\"elements\", []):\n                e_type = element.get(\"type\")\n                if hasattr(e_type, \"lower\") and e_type.lower() == elem_cls._type_value:\n                    result.append(elem_cls.from_json(element, result=result))\n\n        return result\n\n    @classmethod\n    def from_xml(cls, data, api=None, iterparse=False):\n        \"\"\"\n        Create a new instance and load data from xml object.\n\n        :param data: Root element\n        :type data: xml.etree.ElementTree.Element\n        :param api:\n        :type api: Overpass\n        :return: New instance of Result object\n        :rtype: Result\n        \"\"\"\n        result = cls(api=api)\n\n        try:\n            isFile = os.path.exists(data)\n        except:\n            isFile = False\n\n        if not iterparse:\n            #Method 1 : full parsing at once\n            if isFile:\n                with open(data, 'r', encoding='utf-8') as f:\n                    data = f.read() #all file in memory\n            root = ET.fromstring(data)\n            for elem_cls in [Node, Way, Relation]:\n                for child in root:\n                    if child.tag.lower() == elem_cls._type_value:\n                        result.append(elem_cls.from_xml(child, result=result))\n        else:\n            #Method 2 : iter parsing (memory friendly)\n            #WARNING Issue #198\n            if not isFile:\n                data = StringIO(data)\n            root = ET.iterparse(data, events=(\"start\", \"end\"))\n            elem_clss = {'node':Node, 'way':Way, 'relation':Relation}\n            for event, child in root:\n                if event == 'start':\n                    if child.tag.lower() == 'bounds':\n                        result._bounds = {k:float(v) for k, v in child.attrib.items()}\n                    if child.tag.lower() in elem_clss:\n                        elem_cls = elem_clss[child.tag.lower()]\n                        result.append(elem_cls.from_xml(child, result=result))\n                elif event == 'end':\n                    child.clear()\n\n        return result\n\n    def get_node(self, node_id, resolve_missing=False):\n        \"\"\"\n        Get a node by its ID.\n\n        :param node_id: The node ID\n        :type node_id: Integer\n        :param resolve_missing: Query the Overpass API if the node is missing in the result set.\n        :return: The node\n        :rtype: overpy.Node\n        :raises overpy.exception.DataIncomplete: At least one referenced node is not available in the result cache.\n        :raises overpy.exception.DataIncomplete: If resolve_missing is True and at least one node can't be resolved.\n        \"\"\"\n        nodes = self.get_nodes(node_id=node_id)\n        if len(nodes) == 0:\n            if not resolve_missing:\n                raise exception.DataIncomplete(\"Resolve missing nodes is disabled\")\n\n            query = (\"\\n\"\n                     \"[out:json];\\n\"\n                     \"node({node_id});\\n\"\n                     \"out body;\\n\"\n                     )\n            query = query.format(\n                node_id=node_id\n            )\n            tmp_result = self.api.query(query)\n            self.expand(tmp_result)\n\n            nodes = self.get_nodes(node_id=node_id)\n\n        if len(nodes) == 0:\n            raise exception.DataIncomplete(\"Unable to resolve all nodes\")\n\n        return nodes[0]\n\n    def get_nodes(self, node_id=None, **kwargs):\n        \"\"\"\n        Alias for get_elements() but filter the result by Node()\n\n        :param node_id: The Id of the node\n        :type node_id: Integer\n        :return: List of elements\n        \"\"\"\n        return self.get_elements(Node, elem_id=node_id, **kwargs)\n\n    def get_relation(self, rel_id, resolve_missing=False):\n        \"\"\"\n        Get a relation by its ID.\n\n        :param rel_id: The relation ID\n        :type rel_id: Integer\n        :param resolve_missing: Query the Overpass API if the relation is missing in the result set.\n        :return: The relation\n        :rtype: overpy.Relation\n        :raises overpy.exception.DataIncomplete: The requested relation is not available in the result cache.\n        :raises overpy.exception.DataIncomplete: If resolve_missing is True and the relation can't be resolved.\n        \"\"\"\n        relations = self.get_relations(rel_id=rel_id)\n        if len(relations) == 0:\n            if resolve_missing is False:\n                raise exception.DataIncomplete(\"Resolve missing relations is disabled\")\n\n            query = (\"\\n\"\n                     \"[out:json];\\n\"\n                     \"relation({relation_id});\\n\"\n                     \"out body;\\n\"\n                     )\n            query = query.format(\n                relation_id=rel_id\n            )\n            tmp_result = self.api.query(query)\n            self.expand(tmp_result)\n\n            relations = self.get_relations(rel_id=rel_id)\n\n        if len(relations) == 0:\n            raise exception.DataIncomplete(\"Unable to resolve requested reference\")\n\n        return relations[0]\n\n    def get_relations(self, rel_id=None, **kwargs):\n        \"\"\"\n        Alias for get_elements() but filter the result by Relation\n\n        :param rel_id: Id of the relation\n        :type rel_id: Integer\n        :return: List of elements\n        \"\"\"\n        return self.get_elements(Relation, elem_id=rel_id, **kwargs)\n\n    def get_way(self, way_id, resolve_missing=False):\n        \"\"\"\n        Get a way by its ID.\n\n        :param way_id: The way ID\n        :type way_id: Integer\n        :param resolve_missing: Query the Overpass API if the way is missing in the result set.\n        :return: The way\n        :rtype: overpy.Way\n        :raises overpy.exception.DataIncomplete: The requested way is not available in the result cache.\n        :raises overpy.exception.DataIncomplete: If resolve_missing is True and the way can't be resolved.\n        \"\"\"\n        ways = self.get_ways(way_id=way_id)\n        if len(ways) == 0:\n            if resolve_missing is False:\n                raise exception.DataIncomplete(\"Resolve missing way is disabled\")\n\n            query = (\"\\n\"\n                     \"[out:json];\\n\"\n                     \"way({way_id});\\n\"\n                     \"out body;\\n\"\n                     )\n            query = query.format(\n                way_id=way_id\n            )\n            tmp_result = self.api.query(query)\n            self.expand(tmp_result)\n\n            ways = self.get_ways(way_id=way_id)\n\n        if len(ways) == 0:\n            raise exception.DataIncomplete(\"Unable to resolve requested way\")\n\n        return ways[0]\n\n    def get_ways(self, way_id=None, **kwargs):\n        \"\"\"\n        Alias for get_elements() but filter the result by Way\n\n        :param way_id: The Id of the way\n        :type way_id: Integer\n        :return: List of elements\n        \"\"\"\n        return self.get_elements(Way, elem_id=way_id, **kwargs)\n\n    def get_bounds(self):\n        if not self._bounds:\n            lons, lats = zip(*[(e.lon, e.lat) for e in self._nodes.values()])\n            self._bounds['minlon'] = float(min(lons))\n            self._bounds['maxlon'] = float(max(lons))\n            self._bounds['minlat'] = float(min(lats))\n            self._bounds['maxlat'] = float(max(lats))\n        return self._bounds\n\n    node_ids = property(get_node_ids)\n    nodes = property(get_nodes)\n    relation_ids = property(get_relation_ids)\n    relations = property(get_relations)\n    way_ids = property(get_way_ids)\n    ways = property(get_ways)\n    bounds = property(get_bounds)\n\nclass Element(object):\n\n    \"\"\"\n    Base element\n    \"\"\"\n\n    def __init__(self, attributes=None, result=None, tags=None):\n        \"\"\"\n        :param attributes: Additional attributes\n        :type attributes: Dict\n        :param result: The result object this element belongs to\n        :param tags: List of tags\n        :type tags: Dict\n        \"\"\"\n\n        self._result = result\n        self.attributes = attributes\n        self.id = None\n        self.tags = tags\n\n\nclass Node(Element):\n\n    \"\"\"\n    Class to represent an element of type node\n    \"\"\"\n\n    _type_value = \"node\"\n\n    def __init__(self, node_id=None, lat=None, lon=None, **kwargs):\n        \"\"\"\n        :param lat: Latitude\n        :type lat: Decimal or Float\n        :param lon: Longitude\n        :type long: Decimal or Float\n        :param node_id: Id of the node element\n        :type node_id: Integer\n        :param kwargs: Additional arguments are passed directly to the parent class\n        \"\"\"\n\n        Element.__init__(self, **kwargs)\n        self.id = node_id\n        self.lat = lat\n        self.lon = lon\n\n    def __repr__(self):\n        return \"<overpy.Node id={} lat={} lon={}>\".format(self.id, self.lat, self.lon)\n\n    @classmethod\n    def from_json(cls, data, result=None):\n        \"\"\"\n        Create new Node element from JSON data\n\n        :param data: Element data from JSON\n        :type data: Dict\n        :param result: The result this element belongs to\n        :type result: overpy.Result\n        :return: New instance of Node\n        :rtype: overpy.Node\n        :raises overpy.exception.ElementDataWrongType: If type value of the passed JSON data does not match.\n        \"\"\"\n        if data.get(\"type\") != cls._type_value:\n            raise exception.ElementDataWrongType(\n                type_expected=cls._type_value,\n                type_provided=data.get(\"type\")\n            )\n\n        tags = data.get(\"tags\", {})\n\n        node_id = data.get(\"id\")\n        lat = data.get(\"lat\")\n        lon = data.get(\"lon\")\n\n        attributes = {}\n        ignore = [\"type\", \"id\", \"lat\", \"lon\", \"tags\"]\n        for n, v in data.items():\n            if n in ignore:\n                continue\n            attributes[n] = v\n\n        return cls(node_id=node_id, lat=lat, lon=lon, tags=tags, attributes=attributes, result=result)\n\n    @classmethod\n    def from_xml(cls, child, result=None):\n        \"\"\"\n        Create new way element from XML data\n\n        :param child: XML node to be parsed\n        :type child: xml.etree.ElementTree.Element\n        :param result: The result this node belongs to\n        :type result: overpy.Result\n        :return: New Way oject\n        :rtype: overpy.Node\n        :raises overpy.exception.ElementDataWrongType: If name of the xml child node doesn't match\n        :raises ValueError: If a tag doesn't have a name\n        \"\"\"\n        if child.tag.lower() != cls._type_value:\n            raise exception.ElementDataWrongType(\n                type_expected=cls._type_value,\n                type_provided=child.tag.lower()\n            )\n\n        tags = {}\n\n        for sub_child in child:\n            if sub_child.tag.lower() == \"tag\":\n                name = sub_child.attrib.get(\"k\")\n                if name is None:\n                    raise ValueError(\"Tag without name/key.\")\n                value = sub_child.attrib.get(\"v\")\n                tags[name] = value\n\n        node_id = child.attrib.get(\"id\")\n        if node_id is not None:\n            node_id = int(node_id)\n        lat = child.attrib.get(\"lat\")\n        if lat is not None:\n            lat = Decimal(lat)\n        lon = child.attrib.get(\"lon\")\n        if lon is not None:\n            lon = Decimal(lon)\n\n        attributes = {}\n        ignore = [\"id\", \"lat\", \"lon\"]\n        for n, v in child.attrib.items():\n            if n in ignore:\n                continue\n            attributes[n] = v\n\n        return cls(node_id=node_id, lat=lat, lon=lon, tags=tags, attributes=attributes, result=result)\n\n\nclass Way(Element):\n\n    \"\"\"\n    Class to represent an element of type way\n    \"\"\"\n\n    _type_value = \"way\"\n\n    def __init__(self, way_id=None, node_ids=None, **kwargs):\n        \"\"\"\n        :param node_ids: List of node IDs\n        :type node_ids: List or Tuple\n        :param way_id: Id of the way element\n        :type way_id: Integer\n        :param kwargs: Additional arguments are passed directly to the parent class\n\n        \"\"\"\n\n        Element.__init__(self, **kwargs)\n        #: The id of the way\n        self.id = way_id\n\n        #: List of Ids of the associated nodes\n        self._node_ids = node_ids\n\n    def __repr__(self):\n        return \"<overpy.Way id={} nodes={}>\".format(self.id, self._node_ids)\n\n    @property\n    def nodes(self):\n        \"\"\"\n        List of nodes associated with the way.\n        \"\"\"\n        return self.get_nodes()\n\n    def get_nodes(self, resolve_missing=False):\n        \"\"\"\n        Get the nodes defining the geometry of the way\n\n        :param resolve_missing: Try to resolve missing nodes.\n        :type resolve_missing: Boolean\n        :return: List of nodes\n        :rtype: List of overpy.Node\n        :raises overpy.exception.DataIncomplete: At least one referenced node is not available in the result cache.\n        :raises overpy.exception.DataIncomplete: If resolve_missing is True and at least one node can't be resolved.\n        \"\"\"\n        result = []\n        resolved = False\n\n        for node_id in self._node_ids:\n            try:\n                node = self._result.get_node(node_id)\n            except exception.DataIncomplete:\n                node = None\n\n            if node is not None:\n                result.append(node)\n                continue\n\n            if not resolve_missing:\n                raise exception.DataIncomplete(\"Resolve missing nodes is disabled\")\n\n            # We tried to resolve the data but some nodes are still missing\n            if resolved:\n                raise exception.DataIncomplete(\"Unable to resolve all nodes\")\n\n            query = (\"\\n\"\n                     \"[out:json];\\n\"\n                     \"way({way_id});\\n\"\n                     \"node(w);\\n\"\n                     \"out body;\\n\"\n                     )\n            query = query.format(\n                way_id=self.id\n            )\n            tmp_result = self._result.api.query(query)\n            self._result.expand(tmp_result)\n            resolved = True\n\n            try:\n                node = self._result.get_node(node_id)\n            except exception.DataIncomplete:\n                node = None\n\n            if node is None:\n                raise exception.DataIncomplete(\"Unable to resolve all nodes\")\n\n            result.append(node)\n\n        return result\n\n    @classmethod\n    def from_json(cls, data, result=None):\n        \"\"\"\n        Create new Way element from JSON data\n\n        :param data: Element data from JSON\n        :type data: Dict\n        :param result: The result this element belongs to\n        :type result: overpy.Result\n        :return: New instance of Way\n        :rtype: overpy.Way\n        :raises overpy.exception.ElementDataWrongType: If type value of the passed JSON data does not match.\n        \"\"\"\n        if data.get(\"type\") != cls._type_value:\n            raise exception.ElementDataWrongType(\n                type_expected=cls._type_value,\n                type_provided=data.get(\"type\")\n            )\n\n        tags = data.get(\"tags\", {})\n\n        way_id = data.get(\"id\")\n        node_ids = data.get(\"nodes\")\n\n        attributes = {}\n        ignore = [\"id\", \"nodes\", \"tags\", \"type\"]\n        for n, v in data.items():\n            if n in ignore:\n                continue\n            attributes[n] = v\n\n        return cls(way_id=way_id, attributes=attributes, node_ids=node_ids, tags=tags, result=result)\n\n    @classmethod\n    def from_xml(cls, child, result=None):\n        \"\"\"\n        Create new way element from XML data\n\n        :param child: XML node to be parsed\n        :type child: xml.etree.ElementTree.Element\n        :param result: The result this node belongs to\n        :type result: overpy.Result\n        :return: New Way oject\n        :rtype: overpy.Way\n        :raises overpy.exception.ElementDataWrongType: If name of the xml child node doesn't match\n        :raises ValueError: If the ref attribute of the xml node is not provided\n        :raises ValueError: If a tag doesn't have a name\n        \"\"\"\n        if child.tag.lower() != cls._type_value:\n            raise exception.ElementDataWrongType(\n                type_expected=cls._type_value,\n                type_provided=child.tag.lower()\n            )\n\n        tags = {}\n        node_ids = []\n\n        for sub_child in child:\n            if sub_child.tag.lower() == \"tag\":\n                name = sub_child.attrib.get(\"k\")\n                if name is None:\n                    raise ValueError(\"Tag without name/key.\")\n                value = sub_child.attrib.get(\"v\")\n                tags[name] = value\n            if sub_child.tag.lower() == \"nd\":\n                ref_id = sub_child.attrib.get(\"ref\")\n                if ref_id is None:\n                    raise ValueError(\"Unable to find required ref value.\")\n                ref_id = int(ref_id)\n                node_ids.append(ref_id)\n\n        way_id = child.attrib.get(\"id\")\n        if way_id is not None:\n            way_id = int(way_id)\n\n        attributes = {}\n        ignore = [\"id\"]\n        for n, v in child.attrib.items():\n            if n in ignore:\n                continue\n            attributes[n] = v\n\n        return cls(way_id=way_id, attributes=attributes, node_ids=node_ids, tags=tags, result=result)\n\n\nclass Relation(Element):\n\n    \"\"\"\n    Class to represent an element of type relation\n    \"\"\"\n\n    _type_value = \"relation\"\n\n    def __init__(self, rel_id=None, members=None, **kwargs):\n        \"\"\"\n        :param members:\n        :param rel_id: Id of the relation element\n        :type rel_id: Integer\n        :param kwargs:\n        :return:\n        \"\"\"\n\n        Element.__init__(self, **kwargs)\n        self.id = rel_id\n        self.members = members\n\n    def __repr__(self):\n        return \"<overpy.Relation id={}>\".format(self.id)\n\n    @classmethod\n    def from_json(cls, data, result=None):\n        \"\"\"\n        Create new Relation element from JSON data\n\n        :param data: Element data from JSON\n        :type data: Dict\n        :param result: The result this element belongs to\n        :type result: overpy.Result\n        :return: New instance of Relation\n        :rtype: overpy.Relation\n        :raises overpy.exception.ElementDataWrongType: If type value of the passed JSON data does not match.\n        \"\"\"\n        if data.get(\"type\") != cls._type_value:\n            raise exception.ElementDataWrongType(\n                type_expected=cls._type_value,\n                type_provided=data.get(\"type\")\n            )\n\n        tags = data.get(\"tags\", {})\n\n        rel_id = data.get(\"id\")\n\n        members = []\n\n        supported_members = [RelationNode, RelationWay, RelationRelation]\n        for member in data.get(\"members\", []):\n            type_value = member.get(\"type\")\n            for member_cls in supported_members:\n                if member_cls._type_value == type_value:\n                    members.append(\n                        member_cls.from_json(\n                            member,\n                            result=result\n                        )\n                    )\n\n        attributes = {}\n        ignore = [\"id\", \"members\", \"tags\", \"type\"]\n        for n, v in data.items():\n            if n in ignore:\n                continue\n            attributes[n] = v\n\n        return cls(rel_id=rel_id, attributes=attributes, members=members, tags=tags, result=result)\n\n    @classmethod\n    def from_xml(cls, child, result=None):\n        \"\"\"\n        Create new way element from XML data\n\n        :param child: XML node to be parsed\n        :type child: xml.etree.ElementTree.Element\n        :param result: The result this node belongs to\n        :type result: overpy.Result\n        :return: New Way oject\n        :rtype: overpy.Relation\n        :raises overpy.exception.ElementDataWrongType: If name of the xml child node doesn't match\n        :raises ValueError: If a tag doesn't have a name\n        \"\"\"\n        if child.tag.lower() != cls._type_value:\n            raise exception.ElementDataWrongType(\n                type_expected=cls._type_value,\n                type_provided=child.tag.lower()\n            )\n\n        tags = {}\n        members = []\n\n        supported_members = [RelationNode, RelationWay, RelationRelation]\n        for sub_child in child:\n            if sub_child.tag.lower() == \"tag\":\n                name = sub_child.attrib.get(\"k\")\n                if name is None:\n                    raise ValueError(\"Tag without name/key.\")\n                value = sub_child.attrib.get(\"v\")\n                tags[name] = value\n            if sub_child.tag.lower() == \"member\":\n                type_value = sub_child.attrib.get(\"type\")\n                for member_cls in supported_members:\n                    if member_cls._type_value == type_value:\n                        members.append(\n                            member_cls.from_xml(\n                                sub_child,\n                                result=result\n                            )\n                        )\n\n        rel_id = child.attrib.get(\"id\")\n        if rel_id is not None:\n            rel_id = int(rel_id)\n\n        attributes = {}\n        ignore = [\"id\"]\n        for n, v in child.attrib.items():\n            if n in ignore:\n                continue\n            attributes[n] = v\n\n        return cls(rel_id=rel_id, attributes=attributes, members=members, tags=tags, result=result)\n\n\nclass RelationMember(object):\n\n    \"\"\"\n    Base class to represent a member of a relation.\n    \"\"\"\n\n    def __init__(self, ref=None, role=None, result=None):\n        \"\"\"\n        :param ref: Reference Id\n        :type ref: Integer\n        :param role: The role of the relation member\n        :type role: String\n        :param result:\n        \"\"\"\n        self.ref = ref\n        self._result = result\n        self.role = role\n\n    @classmethod\n    def from_json(cls, data, result=None):\n        \"\"\"\n        Create new RelationMember element from JSON data\n\n        :param child: Element data from JSON\n        :type child: Dict\n        :param result: The result this element belongs to\n        :type result: overpy.Result\n        :return: New instance of RelationMember\n        :rtype: overpy.RelationMember\n        :raises overpy.exception.ElementDataWrongType: If type value of the passed JSON data does not match.\n        \"\"\"\n        if data.get(\"type\") != cls._type_value:\n            raise exception.ElementDataWrongType(\n                type_expected=cls._type_value,\n                type_provided=data.get(\"type\")\n            )\n\n        ref = data.get(\"ref\")\n        role = data.get(\"role\")\n        return cls(ref=ref, role=role, result=result)\n\n    @classmethod\n    def from_xml(cls, child, result=None):\n        \"\"\"\n        Create new RelationMember from XML data\n\n        :param child: XML node to be parsed\n        :type child: xml.etree.ElementTree.Element\n        :param result: The result this element belongs to\n        :type result: overpy.Result\n        :return: New relation member oject\n        :rtype: overpy.RelationMember\n        :raises overpy.exception.ElementDataWrongType: If name of the xml child node doesn't match\n        \"\"\"\n        if child.attrib.get(\"type\") != cls._type_value:\n            raise exception.ElementDataWrongType(\n                type_expected=cls._type_value,\n                type_provided=child.tag.lower()\n            )\n\n        ref = child.attrib.get(\"ref\")\n        if ref is not None:\n            ref = int(ref)\n        role = child.attrib.get(\"role\")\n        return cls(ref=ref, role=role, result=result)\n\n\nclass RelationNode(RelationMember):\n    _type_value = \"node\"\n\n    def resolve(self, resolve_missing=False):\n        return self._result.get_node(self.ref, resolve_missing=resolve_missing)\n\n    def __repr__(self):\n        return \"<overpy.RelationNode ref={} role={}>\".format(self.ref, self.role)\n\n\nclass RelationWay(RelationMember):\n    _type_value = \"way\"\n\n    def resolve(self, resolve_missing=False):\n        return self._result.get_way(self.ref, resolve_missing=resolve_missing)\n\n    def __repr__(self):\n        return \"<overpy.RelationWay ref={} role={}>\".format(self.ref, self.role)\n\n\nclass RelationRelation(RelationMember):\n    _type_value = \"relation\"\n\n    def resolve(self, resolve_missing=False):\n        return self._result.get_relation(self.ref, resolve_missing=resolve_missing)\n\n    def __repr__(self):\n        return \"<overpy.RelationRelation ref={} role={}>\".format(self.ref, self.role)\n"
  },
  {
    "path": "operators/lib/osm/overpy/exception.py",
    "content": "class OverPyException(BaseException):\n    \"\"\"OverPy base exception\"\"\"\n    pass\n\n\nclass DataIncomplete(OverPyException):\n    \"\"\"\n    Raised if the requested data isn't available in the result.\n    Try to improve the query or to resolve the missing data.\n    \"\"\"\n    def __init__(self, *args, **kwargs):\n        OverPyException.__init__(\n            self,\n            \"Data incomplete try to improve the query to resolve the missing data\",\n            *args,\n            **kwargs\n        )\n\n\nclass ElementDataWrongType(OverPyException):\n    \"\"\"\n    Raised if the provided element does not match the expected type.\n\n    :param type_expected: The expected element type\n    :type type_expected: String\n    :param type_provided: The provided element type\n    :type type_provided: String|None\n    \"\"\"\n    def __init__(self, type_expected, type_provided=None):\n        self.type_expected = type_expected\n        self.type_provided = type_provided\n\n    def __str__(self):\n        return \"Type expected '%s' but '%s' provided\" % (\n            self.type_expected,\n            str(self.type_provided)\n        )\n\n\nclass OverpassBadRequest(OverPyException):\n    \"\"\"\n    Raised if the Overpass API service returns a syntax error.\n\n    :param query: The encoded query how it was send to the server\n    :type query: Bytes\n    :param msgs: List of error messages\n    :type msgs: List\n    \"\"\"\n    def __init__(self, query, msgs=None):\n        self.query = query\n        if msgs is None:\n            msgs = []\n        self.msgs = msgs\n\n    def __str__(self):\n        tmp_msgs = []\n        for tmp_msg in self.msgs:\n            if not isinstance(tmp_msg, str):\n                tmp_msg = str(tmp_msg)\n            tmp_msgs.append(tmp_msg)\n\n        return \"\\n\".join(tmp_msgs)\n\n\nclass OverpassGatewayTimeout(OverPyException):\n    \"\"\"\n    Raised if load of the Overpass API service is too high and it can't handle the request.\n    \"\"\"\n    def __init__(self):\n        OverPyException.__init__(self, \"Server load too high\")\n\n\nclass OverpassTooManyRequests(OverPyException):\n    \"\"\"\n    Raised if the Overpass API service returns a 429 status code.\n    \"\"\"\n    def __init__(self):\n        OverPyException.__init__(self, \"Too many requests\")\n\n\nclass OverpassUnknownContentType(OverPyException):\n    \"\"\"\n    Raised if the reported content type isn't handled by OverPy.\n\n    :param content_type: The reported content type\n    :type content_type: None or String\n    \"\"\"\n    def __init__(self, content_type):\n        self.content_type = content_type\n\n    def __str__(self):\n        if self.content_type is None:\n            return \"No content type returned\"\n        return \"Unknown content type: %s\" % self.content_type\n\n\nclass OverpassUnknownHTTPStatusCode(OverPyException):\n    \"\"\"\n    Raised if the returned HTTP status code isn't handled by OverPy.\n\n    :param code: The HTTP status code\n    :type code: Integer\n    \"\"\"\n    def __init__(self, code):\n        self.code = code\n\n    def __str__(self):\n        return \"Unknown/Unhandled status code: %d\" % self.code"
  },
  {
    "path": "operators/lib/osm/overpy/helper.py",
    "content": "__author__ = 'mjob'\n\nimport overpy\n\n\ndef get_street(street, areacode, api=None):\n    \"\"\"\n    Retrieve streets in a given bounding area\n\n    :param overpy.Overpass api: First street of intersection\n    :param String street: Name of street\n    :param String areacode: The OSM id of the bounding area\n    :return: Parsed result\n    :raises overpy.exception.OverPyException: If something bad happens.\n    \"\"\"\n    if api is None:\n        api = overpy.Overpass()\n\n    query = \"\"\"\n        area(%s)->.location;\n        (\n            way[highway][name=\"%s\"](area.location);\n            - (\n                way[highway=service](area.location);\n                way[highway=track](area.location);\n            );\n        );\n        out body;\n        >;\n        out skel qt;\n    \"\"\"\n\n    data = api.query(query % (areacode, street))\n\n    return data\n\n\ndef get_intersection(street1, street2, areacode, api=None):\n    \"\"\"\n    Retrieve intersection of two streets in a given bounding area\n\n    :param overpy.Overpass api: First street of intersection\n    :param String street1: Name of first street of intersection\n    :param String street2: Name of second street of intersection\n    :param String areacode: The OSM id of the bounding area\n    :return: List of intersections\n    :raises overpy.exception.OverPyException: If something bad happens.\n    \"\"\"\n    if api is None:\n        api = overpy.Overpass()\n\n    query = \"\"\"\n        area(%s)->.location;\n        (\n            way[highway][name=\"%s\"](area.location); node(w)->.n1;\n            way[highway][name=\"%s\"](area.location); node(w)->.n2;\n        );\n        node.n1.n2;\n        out meta;\n    \"\"\"\n\n    data = api.query(query % (areacode, street1, street2))\n\n    return data.get_nodes()\n"
  },
  {
    "path": "operators/mesh_delaunay_voronoi.py",
    "content": "# -*- coding:utf-8 -*-\n#import DelaunayVoronoi\nimport bpy\nimport time\nfrom .utils import computeVoronoiDiagram, computeDelaunayTriangulation\nfrom ..core.utils import perf_clock\n\ntry:\n\tfrom mathutils.geometry import delaunay_2d_cdt\nexcept ImportError:\n\tNATIVE = False\nelse:\n\tNATIVE = True\n\nimport logging\nlog = logging.getLogger(__name__)\n\nclass Point:\n\tdef __init__(self, x, y, z):\n\t\tself.x, self.y, self.z = x, y, z\n\ndef unique(L):\n\t\"\"\"Return a list of unhashable elements in s, but without duplicates.\n\t[[1, 2], [2, 3], [1, 2]] >>> [[1, 2], [2, 3]]\"\"\"\n\t#For unhashable objects, you can sort the sequence and then scan from the end of the list, deleting duplicates as you go\n\tnDupli=0\n\tnZcolinear=0\n\tL.sort()#sort() brings the equal elements together; then duplicates are easy to weed out in a single pass.\n\tlast = L[-1]\n\tfor i in range(len(L)-2, -1, -1):\n\t\tif last[:2] == L[i][:2]:#XY coordinates compararison\n\t\t\tif last[2] == L[i][2]:#Z coordinates compararison\n\t\t\t\tnDupli+=1#duplicates vertices\n\t\t\telse:#Z colinear\n\t\t\t\tnZcolinear+=1\n\t\t\tdel L[i]\n\t\telse:\n\t\t\tlast = L[i]\n\treturn (nDupli, nZcolinear)#list data type is mutable, input list will automatically update and doesn't need to be returned\n\ndef checkEqual(lst):\n\treturn lst[1:] == lst[:-1]\n\n\nclass OBJECT_OT_tesselation_delaunay(bpy.types.Operator):\n\tbl_idname = \"tesselation.delaunay\" #name used to refer to this operator (button)\n\tbl_label = \"Triangulation\" #operator's label\n\tbl_description = \"Terrain points cloud Delaunay triangulation in 2.5D\" #tooltip\n\tbl_options = {\"UNDO\"}\n\n\tdef execute(self, context):\n\t\tw = context.window\n\t\tw.cursor_set('WAIT')\n\t\tt0 = perf_clock()\n\t\t#Get selected obj\n\t\tobjs = context.selected_objects\n\t\tif len(objs) == 0 or len(objs) > 1:\n\t\t\tself.report({'INFO'}, \"Selection is empty or too much object selected\")\n\t\t\treturn {'CANCELLED'}\n\t\tobj = objs[0]\n\t\tif obj.type != 'MESH':\n\t\t\tself.report({'INFO'}, \"Selection isn't a mesh\")\n\t\t\treturn {'CANCELLED'}\n\t\t#Get points coodinates\n\t\t#bpy.ops.object.transform_apply(rotation=True, scale=True)\n\t\tr = obj.rotation_euler\n\t\ts = obj.scale\n\t\tmesh = obj.data\n\n\t\tif NATIVE:\n\t\t\t'''\n\t\t\tUse native Delaunay triangulation function : delaunay_2d_cdt(verts, edges, faces, output_type, epsilon) >> [verts, edges, faces, orig_verts, orig_edges, orig_faces]\n\t\t\tThe three returned orig lists give, for each of verts, edges, and faces, the list of input element indices corresponding to the positionally same output element. For edges, the orig indices start with the input edges and then continue with the edges implied by each of the faces (n of them for an n-gon).\n\t\t\tOutput type :\n\t\t\t# 0 => triangles with convex hull.\n\t\t\t# 1 => triangles inside constraints.\n\t\t\t# 2 => the input constraints, intersected.\n\t\t\t# 3 => like 2 but with extra edges to make valid BMesh faces.\n\t\t\t'''\n\t\t\tlog.info(\"Triangulate {} points...\".format(len(mesh.vertices)))\n\t\t\tverts, edges, faces, overts, oedges, ofaces  = delaunay_2d_cdt([v.co.to_2d() for v in mesh.vertices], [], [], 0, 0.1)\n\t\t\tverts = [ (v.x, v.y, mesh.vertices[overts[i][0]].co.z) for i, v in enumerate(verts)] #retrieve z values\n\t\t\tlog.info(\"Getting {} triangles\".format(len(faces)))\n\t\t\tlog.info(\"Create mesh...\")\n\t\t\ttinMesh = bpy.data.meshes.new(\"TIN\")\n\t\t\ttinMesh.from_pydata(verts, edges, faces)\n\t\t\ttinMesh.update()\n\t\telse:\n\t\t\tvertsPts = [vertex.co for vertex in mesh.vertices]\n\t\t\t#Remove duplicate\n\t\t\tverts = [[vert.x, vert.y, vert.z] for vert in vertsPts]\n\t\t\tnDupli, nZcolinear = unique(verts)\n\t\t\tnVerts = len(verts)\n\t\t\tlog.info(\"{} duplicates points ignored\".format(nDupli))\n\t\t\tlog.info(\"{} z colinear points excluded\".format(nZcolinear))\n\t\t\tif nVerts < 3:\n\t\t\t\tself.report({'ERROR'}, \"Not enough points\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\t#Check colinear\n\t\t\txValues = [pt[0] for pt in verts]\n\t\t\tyValues = [pt[1] for pt in verts]\n\t\t\tif checkEqual(xValues) or checkEqual(yValues):\n\t\t\t\tself.report({'ERROR'}, \"Points are colinear\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\t#Triangulate\n\t\t\tlog.info(\"Triangulate {} points...\".format(nVerts))\n\t\t\tvertsPts = [Point(vert[0], vert[1], vert[2]) for vert in verts]\n\t\t\tfaces = computeDelaunayTriangulation(vertsPts)\n\t\t\tfaces = [tuple(reversed(tri)) for tri in faces]#reverse point order --> if all triangles are specified anticlockwise then all faces up\n\t\t\tlog.info(\"Getting {} triangles\".format(len(faces)))\n\t\t\t#Create new mesh structure\n\t\t\tlog.info(\"Create mesh...\")\n\t\t\ttinMesh = bpy.data.meshes.new(\"TIN\") #create a new mesh\n\t\t\ttinMesh.from_pydata(verts, [], faces) #Fill the mesh with triangles\n\t\t\ttinMesh.update(calc_edges=True) #Update mesh with new data\n\n\t\t#Create an object with that mesh\n\t\ttinObj = bpy.data.objects.new(\"TIN\", tinMesh)\n\t\t#Place object\n\t\ttinObj.location = obj.location.copy()\n\t\ttinObj.rotation_euler = r\n\t\ttinObj.scale = s\n\t\t#Update scene\n\t\tcontext.scene.collection.objects.link(tinObj) #Link object to scene\n\t\tcontext.view_layer.objects.active = tinObj\n\t\ttinObj.select_set(True)\n\t\tobj.select_set(False)\n\t\t#Report\n\t\tt = round(perf_clock() - t0, 2)\n\t\tmsg = \"{} triangles created in {} seconds\".format(len(faces), t)\n\t\tself.report({'INFO'}, msg)\n\t\t#log.info(msg) #duplicate log\n\t\treturn {'FINISHED'}\n\nclass OBJECT_OT_tesselation_voronoi(bpy.types.Operator):\n\tbl_idname = \"tesselation.voronoi\" #name used to refer to this operator (button)\n\tbl_label = \"Diagram\" #operator's label\n\tbl_description = \"Points cloud Voronoi diagram in 2D\" #tooltip\n\tbl_options = {\"REGISTER\",\"UNDO\"}#need register to draw operator options/redo panel (F6)\n\t#options\n\tmeshType: bpy.props.EnumProperty(\n\t\titems = [(\"Edges\", \"Edges\", \"\"), (\"Faces\", \"Faces\", \"\")],#(Key, Label, Description)\n\t\tname = \"Mesh type\",\n\t\tdescription = \"\"\n\t\t)\n\n\t\"\"\"\n\tdef draw(self, context):\n\t\"\"\"\n\n\tdef execute(self, context):\n\t\tw = context.window\n\t\tw.cursor_set('WAIT')\n\t\tt0 = perf_clock()\n\t\t#Get selected obj\n\t\tobjs = context.selected_objects\n\t\tif len(objs) == 0 or len(objs) > 1:\n\t\t\tself.report({'INFO'}, \"Selection is empty or too much object selected\")\n\t\t\treturn {'CANCELLED'}\n\t\tobj = objs[0]\n\t\tif obj.type != 'MESH':\n\t\t\tself.report({'INFO'}, \"Selection isn't a mesh\")\n\t\t\treturn {'CANCELLED'}\n\t\t#Get points coodinates\n\t\tr = obj.rotation_euler\n\t\ts = obj.scale\n\t\tmesh = obj.data\n\t\tvertsPts = [vertex.co for vertex in mesh.vertices]\n\t\t#Remove duplicate\n\t\tverts = [[vert.x, vert.y, vert.z] for vert in vertsPts]\n\t\tnDupli, nZcolinear = unique(verts)\n\t\tnVerts = len(verts)\n\t\tlog.info(\"{} duplicates points ignored\".format(nDupli))\n\t\tlog.info(\"{} z colinear points excluded\".format(nZcolinear))\n\t\tif nVerts < 3:\n\t\t\tself.report({'ERROR'}, \"Not enough points\")\n\t\t\treturn {'CANCELLED'}\n\t\t#Check colinear\n\t\txValues = [pt[0] for pt in verts]\n\t\tyValues = [pt[1] for pt in verts]\n\t\tif checkEqual(xValues) or checkEqual(yValues):\n\t\t\tself.report({'ERROR'}, \"Points are colinear\")\n\t\t\treturn {'CANCELLED'}\n\t\t#Create diagram\n\t\tlog.info(\"Tesselation... ({} points)\".format(nVerts))\n\t\txbuff, ybuff = 5, 5 # %\n\t\tzPosition = 0\n\t\tvertsPts = [Point(vert[0], vert[1], vert[2]) for vert in verts]\n\t\tif self.meshType == \"Edges\":\n\t\t\tpts, edgesIdx = computeVoronoiDiagram(vertsPts, xbuff, ybuff, polygonsOutput=False, formatOutput=True)\n\t\telse:\n\t\t\tpts, polyIdx = computeVoronoiDiagram(vertsPts, xbuff, ybuff, polygonsOutput=True, formatOutput=True, closePoly=False)\n\t\t#\n\t\tpts = [[pt[0], pt[1], zPosition] for pt in pts]\n\t\t#Create new mesh structure\n\t\tlog.info(\"Create mesh...\")\n\t\tvoronoiDiagram = bpy.data.meshes.new(\"VoronoiDiagram\") #create a new mesh\n\t\tif self.meshType == \"Edges\":\n\t\t\tvoronoiDiagram.from_pydata(pts, edgesIdx, []) #Fill the mesh with triangles\n\t\telse:\n\t\t\tvoronoiDiagram.from_pydata(pts, [], list(polyIdx.values())) #Fill the mesh with triangles\n\t\tvoronoiDiagram.update(calc_edges=True) #Update mesh with new data\n\t\t#create an object with that mesh\n\t\tvoronoiObj = bpy.data.objects.new(\"VoronoiDiagram\", voronoiDiagram)\n\t\t#place object\n\t\tvoronoiObj.location = obj.location.copy()\n\t\tvoronoiObj.rotation_euler = r\n\t\tvoronoiObj.scale = s\n\t\t#update scene\n\t\tcontext.scene.collection.objects.link(voronoiObj) #Link object to scene\n\t\tcontext.view_layer.objects.active = voronoiObj\n\t\tvoronoiObj.select_set(True)\n\t\tobj.select_set(False)\n\t\t#Report\n\t\tt = round(perf_clock() - t0, 2)\n\t\tif self.meshType == \"Edges\":\n\t\t\tself.report({'INFO'}, \"{} edges created in {} seconds\".format(len(edgesIdx), t))\n\t\telse:\n\t\t\tself.report({'INFO'}, \"{} polygons created in {} seconds\".format(len(polyIdx), t))\n\t\treturn {'FINISHED'}\n\nclasses = [\n\tOBJECT_OT_tesselation_delaunay,\n\tOBJECT_OT_tesselation_voronoi\n]\n\ndef register():\n\tfor cls in classes:\n\t\ttry:\n\t\t\tbpy.utils.register_class(cls)\n\t\texcept ValueError as e:\n\t\t\tlog.warning('{} is already registered, now unregister and retry... '.format(cls))\n\t\t\tbpy.utils.unregister_class(cls)\n\t\t\tbpy.utils.register_class(cls)\n\ndef unregister():\n\tfor cls in classes:\n\t\tbpy.utils.unregister_class(cls)\n"
  },
  {
    "path": "operators/mesh_earth_sphere.py",
    "content": "import bpy\nfrom bpy.types import Operator\nfrom bpy.props import IntProperty\n\nfrom math import cos, sin, radians, sqrt\nfrom mathutils import Vector\n\nimport logging\nlog = logging.getLogger(__name__)\n\n\ndef lonlat2xyz(R, lon, lat):\n\tlon, lat = radians(lon), radians(lat)\n\tx = R * cos(lat) * cos(lon)\n\ty = R * cos(lat) * sin(lon)\n\tz = R *sin(lat)\n\treturn Vector((x, y, z))\n\n\nclass OBJECT_OT_earth_sphere(Operator):\n\tbl_idname = \"earth.sphere\"\n\tbl_label = \"lonlat to sphere\"\n\tbl_description = \"Transform longitude/latitude data to a sphere like earth globe\"\n\tbl_options = {\"REGISTER\", \"UNDO\"}\n\n\tradius: IntProperty(name = \"Radius\", default=100, description=\"Sphere radius\", min=1)\n\n\tdef execute(self, context):\n\t\tscn = bpy.context.scene\n\t\tobjs = bpy.context.selected_objects\n\n\t\tif not objs:\n\t\t\tself.report({'INFO'}, \"No selected object\")\n\t\t\treturn {'CANCELLED'}\n\n\t\tfor obj in objs:\n\t\t\tif obj.type != 'MESH':\n\t\t\t\tlog.warning(\"Object {} is not a mesh\".format(obj.name))\n\t\t\t\tcontinue\n\n\t\t\tw, h, thick = obj.dimensions\n\t\t\tif w > 360:\n\t\t\t\tlog.warning(\"Longitude of object {} exceed 360°\".format(obj.name))\n\t\t\t\tcontinue\n\t\t\tif h > 180:\n\t\t\t\tlog.warning(\"Latitude of object {} exceed 180°\".format(obj.name))\n\t\t\t\tcontinue\n\n\t\t\tmesh = obj.data\n\t\t\tm = obj.matrix_world\n\t\t\tfor vertex in mesh.vertices:\n\t\t\t\tco = m @ vertex.co\n\t\t\t\tlon, lat = co.x, co.y\n\t\t\t\tvertex.co = m.inverted() @ lonlat2xyz(self.radius, lon, lat)\n\n\t\treturn {'FINISHED'}\n\nEARTH_RADIUS = 6378137 #meters\ndef getZDelta(d):\n\t'''delta value for adjusting z across earth curvature\n\thttp://webhelp.infovista.com/Planet/62/Subsystems/Raster/Content/help/analysis/viewshedanalysis.html'''\n\treturn sqrt(EARTH_RADIUS**2 + d**2) - EARTH_RADIUS\n\n\nclass OBJECT_OT_earth_curvature(Operator):\n\tbl_idname = \"earth.curvature\"\n\tbl_label = \"Earth curvature correction\"\n\tbl_description = \"Apply earth curvature correction for viewsheed analysis\"\n\tbl_options = {\"REGISTER\", \"UNDO\"}\n\n\tdef execute(self, context):\n\t\tscn = bpy.context.scene\n\t\tobj = bpy.context.view_layer.objects.active\n\n\t\tif not obj:\n\t\t\tself.report({'INFO'}, \"No active object\")\n\t\t\treturn {'CANCELLED'}\n\n\t\tif obj.type != 'MESH':\n\t\t\tself.report({'INFO'}, \"Selection isn't a mesh\")\n\t\t\treturn {'CANCELLED'}\n\n\t\tmesh = obj.data\n\t\tviewpt = scn.cursor.location\n\n\t\tfor vertex in mesh.vertices:\n\t\t\td = (viewpt.xy - vertex.co.xy).length\n\t\t\tvertex.co.z = vertex.co.z - getZDelta(d)\n\n\t\treturn {'FINISHED'}\n\n\nclasses = [\n\tOBJECT_OT_earth_sphere,\n\tOBJECT_OT_earth_curvature\n]\n\ndef register():\n\tfor cls in classes:\n\t\ttry:\n\t\t\tbpy.utils.register_class(cls)\n\t\texcept ValueError as e:\n\t\t\tlog.warning('{} is already registered, now unregister and retry... '.format(cls))\n\t\t\tbpy.utils.unregister_class(cls)\n\t\t\tbpy.utils.register_class(cls)\n\ndef unregister():\n\tfor cls in classes:\n\t\tbpy.utils.unregister_class(cls)\n"
  },
  {
    "path": "operators/nodes_terrain_analysis_builder.py",
    "content": "# -*- coding:utf-8 -*-\nimport math\nimport bpy\nfrom bpy.types import Panel, Operator\n\nimport logging\nlog = logging.getLogger(__name__)\n\nfrom .utils import getBBOX\n\nfrom ..core.maths.interpo import scale\n\n\nclass TERRAIN_ANALYSIS_OT_build_nodes(Operator):\n\t'''Create material node thee to analysis height, slope and aspect'''\n\tbl_idname = \"analysis.nodes\"\n\tbl_description  = \"Create height, slope and aspect material nodes setup for Cycles\"\n\tbl_label = \"Terrain analysis\"\n\n\tdef execute(self, context):\n\t\tscn = context.scene\n\t\tscn.render.engine = 'CYCLES' #force Cycles render\n\t\tobj = context.view_layer.objects.active\n\t\tif obj is None:\n\t\t\tself.report({'ERROR'}, \"No active object\")\n\t\t\treturn {'CANCELLED'}\n\t\t#######################\n\t\t#HEIGHT\n\t\t#######################\n\t\t# Create material\n\t\theightMatName = 'Height_' + obj.name\n\t\tif heightMatName not in [m.name for m in bpy.data.materials]:\n\t\t\theightMat = bpy.data.materials.new(heightMatName)\n\t\telse:#edit existing height material\n\t\t\theightMat = bpy.data.materials[heightMatName]\n\t\theightMat.use_nodes = True\n\t\theightMat.use_fake_user = True\n\t\tnode_tree = heightMat.node_tree\n\t\tnode_tree.nodes.clear()\n\t\t# create geometry node (world coordinates)\n\t\tgeomNode = node_tree.nodes.new('ShaderNodeNewGeometry')\n\t\tgeomNode.location = (-600, 200)\n\t\t# create separate xyz node\n\t\txyzSplitNode = node_tree.nodes.new('ShaderNodeSeparateXYZ')\n\t\txyzSplitNode.location = (-400, 200)\n\t\t#\n\t\t#Normalize node group\n\t\tgroupsTree = bpy.data.node_groups\n\t\t'''\n\t\t#make a purge (for testing)\n\t\tfor nodeTree in groupsTree:\n\t\t\tname = nodeTree.name\n\t\t\ttry:\n\t\t\t\tgroupsTree.remove(nodeTree)\n\t\t\t\tprint(name+' has been deleted')\n\t\t\texcept:\n\t\t\t\tprint('cannot delete '+name)\n\t\t'''\n\t\tif 'Normalize' in [nodeTree.name for nodeTree in groupsTree]:\n\t\t   #groupsTree.remove(groupsTree['Normalize'])\n\t\t\tscaleNodesGroupTree = groupsTree['Normalize']\n\t\t\tscaleNodesGroupTree.nodes.clear()\n\t\t\tscaleNodesGroupTree.inputs.clear()\n\t\t\tscaleNodesGroupTree.outputs.clear()\n\t\telse:\n\t\t\tscaleNodesGroupTree = groupsTree.new('Normalize', 'ShaderNodeTree') # = bpy.types.node_tree\n\t\tscaleNodesGroupName = scaleNodesGroupTree.name #Normalize.001 if normalize already exists\n\t\t#  group inputs\n\t\tscaleInputsNode = scaleNodesGroupTree.nodes.new('NodeGroupInput')\n\t\tscaleInputsNode.location = (-350,0)\n\t\tscaleNodesGroupTree.inputs.new('NodeSocketFloat','val')\n\t\tscaleNodesGroupTree.inputs.new('NodeSocketFloat','min')\n\t\tscaleNodesGroupTree.inputs.new('NodeSocketFloat','max')\n\t\t#  group outputs\n\t\tscaleOutputsNode = scaleNodesGroupTree.nodes.new('NodeGroupOutput')\n\t\tscaleOutputsNode.location = (300,0)\n\t\tscaleNodesGroupTree.outputs.new('NodeSocketFloat','val')\n\t\t#  create 3 math nodes in a group\n\t\tsubtractNode1 = scaleNodesGroupTree.nodes.new('ShaderNodeMath')\n\t\tsubtractNode1.operation = 'SUBTRACT'\n\t\tsubtractNode1.location = (-100,100)\n\t\tsubtractNode2 = scaleNodesGroupTree.nodes.new('ShaderNodeMath')\n\t\tsubtractNode2.operation = 'SUBTRACT'\n\t\tsubtractNode2.location = (-100,-100)\n\t\tdivideNode = scaleNodesGroupTree.nodes.new('ShaderNodeMath')\n\t\tdivideNode.operation = 'DIVIDE'\n\t\tdivideNode.location = (100,0)\n\t\t#  link nodes\n\t\tscaleNodesGroupTree.links.new(scaleInputsNode.outputs['val'], subtractNode1.inputs[0])\n\t\tscaleNodesGroupTree.links.new(scaleInputsNode.outputs['min'], subtractNode1.inputs[1])\n\t\tscaleNodesGroupTree.links.new(scaleInputsNode.outputs['min'], subtractNode2.inputs[1])\n\t\tscaleNodesGroupTree.links.new(scaleInputsNode.outputs['max'], subtractNode2.inputs[0])\n\t\tscaleNodesGroupTree.links.new(subtractNode1.outputs[0], divideNode.inputs[0])\n\t\tscaleNodesGroupTree.links.new(subtractNode2.outputs[0], divideNode.inputs[1])\n\t\tscaleNodesGroupTree.links.new(divideNode.outputs[0], scaleOutputsNode.inputs['val'])\n\t\t#  finally add the group to main node_tree\n\t\tscaleNodeGroup = node_tree.nodes.new('ShaderNodeGroup')\n\t\tscaleNodeGroup.node_tree = bpy.data.node_groups[scaleNodesGroupName]#['Normalize']\n\t\tscaleNodeGroup.location = (-200, 200)\n\t\t#\n\t\t# create z bbox value nodes\n\t\tbbox = getBBOX.fromObj(obj)\n\t\tzmin = node_tree.nodes.new('ShaderNodeValue')\n\t\tzmin.label = 'zmin ' + obj.name\n\t\tzmin.outputs[0].default_value = bbox['zmin']\n\t\tzmin.location = (-400,0)\n\t\tzmax = node_tree.nodes.new('ShaderNodeValue')\n\t\tzmax.label = 'zmax ' + obj.name\n\t\tzmax.outputs[0].default_value = bbox['zmax']\n\t\tzmax.location = (-400,-100)\n\t\t# create color ramp node\n\t\tcolorRampNode = node_tree.nodes.new('ShaderNodeValToRGB')\n\t\tcolorRampNode.location = (0, 200)\n\t\tcr = colorRampNode.color_ramp\n\t\tcr.elements[0].color = (0,1,0,1)\n\t\tcr.elements[1].color = (1,0,0,1)\n\t\t# Create BSDF diffuse node\n\t\tdiffuseNode = node_tree.nodes.new('ShaderNodeBsdfDiffuse')\n\t\tdiffuseNode.location = (300, 200)\n\t\t# Create output node\n\t\toutputNode = node_tree.nodes.new('ShaderNodeOutputMaterial')\n\t\toutputNode.location = (500, 200)\n\t\t# Connect the nodes\n\t\tnode_tree.links.new(geomNode.outputs['Position'] , xyzSplitNode.inputs['Vector'])\n\t\tnode_tree.links.new(xyzSplitNode.outputs['Z'] , scaleNodeGroup.inputs['val'])\n\t\tnode_tree.links.new(zmin.outputs[0] , scaleNodeGroup.inputs['min'])\n\t\tnode_tree.links.new(zmax.outputs[0] , scaleNodeGroup.inputs['max'])\n\t\tnode_tree.links.new(scaleNodeGroup.outputs['val'] , colorRampNode.inputs['Fac'])\n\t\tnode_tree.links.new(colorRampNode.outputs['Color'] , diffuseNode.inputs['Color'])\n\t\tnode_tree.links.new(diffuseNode.outputs['BSDF'] , outputNode.inputs['Surface'])\n\t\t# Deselect nodes\n\t\tfor node in node_tree.nodes:\n\t\t\tnode.select = False\n\t\t#select color ramp\n\t\tcolorRampNode.select = True\n\t\tnode_tree.nodes.active = colorRampNode\n\n\t\t#######################\n\t\t#SLOPE\n\t\t#######################\n\t\t# Create material\n\t\tslopeMatName = 'Slope'\n\t\tif slopeMatName not in [m.name for m in bpy.data.materials]:\n\t\t\tslopeMat = bpy.data.materials.new(slopeMatName)\n\t\telse:\n\t\t\tslopeMat = bpy.data.materials[slopeMatName]\n\t\tslopeMat.use_nodes = True\n\t\tslopeMat.use_fake_user = True\n\t\tnode_tree = slopeMat.node_tree\n\t\tnode_tree.nodes.clear()\n\t\t'''\n\t\t# create texture coordinate node (local coordinates)\n\t\ttexCoordNode = node_tree.nodes.new('ShaderNodeTexCoord')\n\t\ttexCoordNode.location = (-600, 0)\n\t\t'''\n\t\t# create geometry node (world coordinates)\n\t\tgeomNode = node_tree.nodes.new('ShaderNodeNewGeometry')\n\t\tgeomNode.location = (-600, 0)\n\t\t# create separate xyz node\n\t\txyzSplitNode = node_tree.nodes.new('ShaderNodeSeparateXYZ')\n\t\txyzSplitNode.location = (-400, 0)\n\t\t#  create arc-cos node\n\t\tarcCosNode = node_tree.nodes.new('ShaderNodeMath')\n\t\tarcCosNode.operation = 'ARCCOSINE'\n\t\tarcCosNode.location = (-200,0)\n\t\t#  create math node to convert radians to degrees\n\t\trad2dg = node_tree.nodes.new('ShaderNodeMath')\n\t\trad2dg.operation = 'MULTIPLY'\n\t\trad2dg.location = (0,0)\n\t\trad2dg.label = \"Radians to degrees\"\n\t\trad2dg.inputs[1].default_value = 180/math.pi\n\t\t#  create math node to normalize value\n\t\tnormalize = node_tree.nodes.new('ShaderNodeMath')\n\t\tnormalize.operation = 'DIVIDE'\n\t\tnormalize.location = (200,0)\n\t\tnormalize.label = \"Normalize\"\n\t\tnormalize.inputs[1].default_value = 100\n\t\t# create color ramp node\n\t\tcolorRampNode = node_tree.nodes.new('ShaderNodeValToRGB')\n\t\tcolorRampNode.location = (400, 0)\n\t\tcr = colorRampNode.color_ramp\n\t\tcr.elements[0].color = (0,1,0,1)\n\t\tcr.elements[1].position = 0.5\n\t\tcr.elements[1].color = (1,0,0,1)\n\t\t# Create BSDF diffuse node\n\t\tdiffuseNode = node_tree.nodes.new('ShaderNodeBsdfDiffuse')\n\t\tdiffuseNode.location = (800, 0)\n\t\t# Create output node\n\t\toutputNode = node_tree.nodes.new('ShaderNodeOutputMaterial')\n\t\toutputNode.location = (1000, 0)\n\t\t# Connect the nodes\n\t\t#node_tree.links.new(texCoordNode.outputs['Normal'] , xyzSplitNode.inputs['Vector'])\n\t\tnode_tree.links.new(geomNode.outputs['True Normal'] , xyzSplitNode.inputs['Vector'])\n\t\tnode_tree.links.new(xyzSplitNode.outputs['Z'] , arcCosNode.inputs[0])\n\t\tnode_tree.links.new(arcCosNode.outputs[0] , rad2dg.inputs[0])\n\t\tnode_tree.links.new(rad2dg.outputs[0] , normalize.inputs[0])\n\t\tnode_tree.links.new(normalize.outputs[0] , colorRampNode.inputs['Fac'])\n\t\tnode_tree.links.new(colorRampNode.outputs['Color'] , diffuseNode.inputs['Color'])\n\t\tnode_tree.links.new(diffuseNode.outputs['BSDF'] , outputNode.inputs['Surface'])\n\t\t# Deselect nodes\n\t\tfor node in node_tree.nodes:\n\t\t\tnode.select = False\n\t\t#select color ramp\n\t\tcolorRampNode.select = True\n\t\tnode_tree.nodes.active = colorRampNode\n\n\t\t#######################\n\t\t#ASPECT\n\t\t#######################\n\t\t# Create material\n\t\taspectMatName = 'Aspect'\n\t\tif aspectMatName not in [m.name for m in bpy.data.materials]:\n\t\t\taspectMat = bpy.data.materials.new(aspectMatName)\n\t\telse:\n\t\t\taspectMat = bpy.data.materials[aspectMatName]\n\t\taspectMat.use_nodes = True\n\t\taspectMat.use_fake_user = True\n\t\tnode_tree = aspectMat.node_tree\n\t\tnode_tree.nodes.clear()\n\t\t# create geometry node (world coordinates)\n\t\tgeomNode = node_tree.nodes.new('ShaderNodeNewGeometry')\n\t\tgeomNode.location = (-600, 200)\n\t\t# create separate xyz node\n\t\txyzSplitNode = node_tree.nodes.new('ShaderNodeSeparateXYZ')\n\t\txyzSplitNode.location = (-400, 200)\n\t\tnode_tree.links.new(geomNode.outputs['True Normal'] , xyzSplitNode.inputs['Vector'])\n\t\t#  create maths nodes to compute aspect angle = atan(x/y)\n\t\txyDiv = node_tree.nodes.new('ShaderNodeMath')\n\t\txyDiv.operation = 'DIVIDE'\n\t\txyDiv.location = (-200,0)\n\t\tnode_tree.links.new(xyzSplitNode.outputs['X'] , xyDiv.inputs[0])\n\t\tnode_tree.links.new(xyzSplitNode.outputs['Y'] , xyDiv.inputs[1])\n\t\tatanNode = node_tree.nodes.new('ShaderNodeMath')\n\t\tatanNode.operation = 'ARCTANGENT'\n\t\tatanNode.label = 'Aspect radians'\n\t\tatanNode.location = (0,0)\n\t\tnode_tree.links.new(xyDiv.outputs[0] , atanNode.inputs[0])\n\t\t#  create math node to convert radians to degrees\n\t\trad2dg = node_tree.nodes.new('ShaderNodeMath')\n\t\trad2dg.operation = 'MULTIPLY'\n\t\trad2dg.location = (200,0)\n\t\trad2dg.label = \"Aspect degrees\"\n\t\trad2dg.inputs[1].default_value = 180/math.pi\n\t\tnode_tree.links.new(atanNode.outputs[0] , rad2dg.inputs[0])\n\t\t# maths nodes --> if y < 0 then aspect = aspect + 180\n\t\tyNegMask = node_tree.nodes.new('ShaderNodeMath')\n\t\tyNegMask.operation = 'LESS_THAN'\n\t\tyNegMask.location = (0,200)\n\t\tyNegMask.label = \"y negative ?\"\n\t\tyNegMask.inputs[1].default_value = 0\n\t\tnode_tree.links.new(xyzSplitNode.outputs['Y'] , yNegMask.inputs[0])\n\t\tyNegMutiply = node_tree.nodes.new('ShaderNodeMath')\n\t\tyNegMutiply.operation = 'MULTIPLY'\n\t\tyNegMutiply.location = (200,200)\n\t\tnode_tree.links.new(yNegMask.outputs[0] , yNegMutiply.inputs[0])\n\t\tyNegMutiply.inputs[1].default_value = 180\n\t\tyNegAdd = node_tree.nodes.new('ShaderNodeMath')\n\t\tyNegAdd.operation = 'ADD'\n\t\tyNegAdd.location = (400,200)\n\t\tnode_tree.links.new(yNegMutiply.outputs[0] , yNegAdd.inputs[0])\n\t\tnode_tree.links.new(rad2dg.outputs[0] , yNegAdd.inputs[1])\n\t\t# if y > 0 & x < 0 then aspect = aspect + 360\n\t\txNegMask = node_tree.nodes.new('ShaderNodeMath')\n\t\txNegMask.operation = 'LESS_THAN'\n\t\txNegMask.location = (0,600)\n\t\txNegMask.label = \"x negative ?\"\n\t\txNegMask.inputs[1].default_value = 0\n\t\tnode_tree.links.new(xyzSplitNode.outputs['X'] , xNegMask.inputs[0])\n\t\tyPosMask = node_tree.nodes.new('ShaderNodeMath')\n\t\tyPosMask.operation = 'GREATER_THAN'\n\t\tyPosMask.location = (0,400)\n\t\tyPosMask.label = \"y positive ?\"\n\t\tyPosMask.inputs[1].default_value = 0\n\t\tnode_tree.links.new(xyzSplitNode.outputs['Y'] , yPosMask.inputs[0])\n\t\tmask = node_tree.nodes.new('ShaderNodeMath')\n\t\tmask.operation = 'MULTIPLY'\n\t\tmask.location = (200,500)\n\t\tnode_tree.links.new(xNegMask.outputs[0] , mask.inputs[0])\n\t\tnode_tree.links.new(yPosMask.outputs[0] , mask.inputs[1])\n\t\tmaskMultiply = node_tree.nodes.new('ShaderNodeMath')\n\t\tmaskMultiply.operation = 'MULTIPLY'\n\t\tmaskMultiply.location = (400,500)\n\t\tnode_tree.links.new(mask.outputs[0] , maskMultiply.inputs[0])\n\t\tmaskMultiply.inputs[1].default_value = 360\n\t\tmaskAdd = node_tree.nodes.new('ShaderNodeMath')\n\t\tmaskAdd.operation = 'ADD'\n\t\tmaskAdd.location = (600,300)\n\t\tnode_tree.links.new(maskMultiply.outputs[0] , maskAdd.inputs[0])\n\t\tnode_tree.links.new(yNegAdd.outputs[0] , maskAdd.inputs[1])\n\t\t#  create math node to normalize value\n\t\tnormalize = node_tree.nodes.new('ShaderNodeMath')\n\t\tnormalize.operation = 'DIVIDE'\n\t\tnormalize.location = (800,300)\n\t\tnormalize.label = \"Normalize\"\n\t\tnormalize.inputs[1].default_value = 360\n\t\tnode_tree.links.new(maskAdd.outputs[0] , normalize.inputs[0])\n\t\t# create color ramp node\n\t\tcolorRampNode = node_tree.nodes.new('ShaderNodeValToRGB')\n\t\tcolorRampNode.location = (1000, 300)\n\t\tcr = colorRampNode.color_ramp\n\t\tstops = cr.elements\n\t\tcr.elements[0].color = (1,0,0,1)#first stop = red\n\t\tstops.remove(stops[1])#remove last stop\n\t\t#orange, yellow, green, cyan, blue1, blue2, pink, red\n\t\tcolors = [(1,0.5,0,1), (1,1,0,1), (0,1,0,1), (0,1,1,1), (0,0.5,1,1), (0,0,1,1), (1,0,1,1), (1,0,0,1)]\n\t\tfor i, angle in enumerate([22.5, 67.5, 112.5, 157.5, 202.5, 247.5, 292.5, 337.5]):\n\t\t\tpos = scale(angle, 0, 360, 0, 1)\n\t\t\tstop = stops.new(pos)\n\t\t\tstop.color = colors[i]\n\t\tcr.interpolation = 'CONSTANT'\n\t\tnode_tree.links.new(normalize.outputs[0] , colorRampNode.inputs['Fac'])\n\t\t# Create BSDF diffuse node\n\t\tdiffuseNode = node_tree.nodes.new('ShaderNodeBsdfDiffuse')\n\t\tdiffuseNode.location = (1300, 300)\n\t\tnode_tree.links.new(colorRampNode.outputs['Color'] , diffuseNode.inputs['Color'])\n\t\t# Flat color diffuse\n\t\tdiffuseFlat = node_tree.nodes.new('ShaderNodeBsdfDiffuse')\n\t\tdiffuseFlat.location = (1300, 0)\n\t\tdiffuseFlat.inputs[0].default_value = (1,1,1,1)\n\t\t# flat test\n\t\tflatMask = node_tree.nodes.new('ShaderNodeMath')\n\t\tflatMask.operation = 'LESS_THAN'\n\t\tflatMask.location = (800,-100)\n\t\tflatMask.label = \"is flat?\"\n\t\tflatMask.inputs[1].default_value = 0.999\n\t\tnode_tree.links.new(xyzSplitNode.outputs['Z'] , flatMask.inputs[0])\n\t\t# Mix shader\n\t\tmixNode = node_tree.nodes.new('ShaderNodeMixShader')\n\t\tmixNode.location = (1500, 200)\n\t\tnode_tree.links.new(diffuseNode.outputs['BSDF'] , mixNode.inputs[2])\n\t\tnode_tree.links.new(diffuseFlat.outputs['BSDF'] , mixNode.inputs[1])\n\t\tnode_tree.links.new(flatMask.outputs[0] , mixNode.inputs['Fac'])\n\t\t# Create output node\n\t\toutputNode = node_tree.nodes.new('ShaderNodeOutputMaterial')\n\t\toutputNode.location = (1700, 200)\n\t\tnode_tree.links.new(mixNode.outputs[0] , outputNode.inputs['Surface'])\n\t\t# Deselect nodes\n\t\tfor node in node_tree.nodes:\n\t\t\tnode.select = False\n\t\t#select color ramp\n\t\tcolorRampNode.select = True\n\t\tnode_tree.nodes.active = colorRampNode\n\n\n\t\t#######################\n\n\t\t# Add material to current object\n\t\t'''\n\t\tif heightMat.name not in [m.name for m in obj.data.materials]:\n\t\t\t#add slot & move ui list index\n\t\telse:#this name already exist, just move ui list index to select it\n\t\t\tobj.active_material_index = obj.material_slots.find(heightMat.name)\n\t\t'''\n\t\t#add slot\n\t\tobj.data.materials.append(heightMat)\n\t\t#move ui list index\n\t\tobj.active_material_index = len(obj.material_slots)-1\n\t\t#Assignmaterial to faces\n\t\tfor faces in obj.data.polygons:\n\t\t\tfaces.material_index = obj.active_material_index\n\n\t\treturn {'FINISHED'}\n\ndef register():\n\ttry:\n\t\tbpy.utils.register_class(TERRAIN_ANALYSIS_OT_build_nodes)\n\texcept ValueError as e:\n\t\tlog.warning('{} is already registered, now unregister and retry... '.format(TERRAIN_ANALYSIS_OT_build_nodes))\n\t\tunregister()\n\t\tbpy.utils.register_class(TERRAIN_ANALYSIS_OT_build_nodes)\n\ndef unregister():\n\tbpy.utils.unregister_class(TERRAIN_ANALYSIS_OT_build_nodes)\n"
  },
  {
    "path": "operators/nodes_terrain_analysis_reclassify.py",
    "content": "# -*- coding:utf-8 -*-\n\nimport os\nimport math\n\nimport bpy\nfrom mathutils import Vector\n\nfrom bpy.props import StringProperty, IntProperty, FloatProperty, BoolProperty, EnumProperty, CollectionProperty, FloatVectorProperty\nfrom bpy.types import PropertyGroup, UIList, Panel, Operator\nfrom bpy.app.handlers import persistent\n\nimport logging\nlog = logging.getLogger(__name__)\n\nfrom .utils import getBBOX\n\nfrom ..core.utils.gradient import Color, Stop, Gradient\n\nfrom ..core.maths.interpo import scale\nfrom ..core.maths.kmeans1D import kmeans1d, getBreaks\n#from ..core.maths.jenks_caspall import jenksCaspall\n\n#Folder containing SVG gradients\nsvgGradientFolder = os.path.dirname(os.path.realpath(__file__)) + os.sep + \"rsrc\" + os.sep + \"gradients\" + os.sep\n\n\n#Global var\n########################################\n#These variables are the bounds values of the topographic property represented\n#this is an altitude (zmin & zmax) in meters if material represents a height map\n#or a slope in degrees if material represents a slope map\n#bounds values are used to scale user input (altitude or slope) between 0 and 1\n#then scale values are used to setup color ramp node\ninMin = 0\ninMax = 0\n# other global for handler check\nscn = None\nobj = None\nmat = None\nnode = None\n\n\n#Set up a propertyGroup and populate a CollectionProperty\n#########################################\nclass RECLASS_PG_color(PropertyGroup):\n\n\t#Define update function for FloatProperty\n\tdef updStop(item, context):\n\t\t#first arg is the container of the prop to update, here a customItem\n\t\tif context.space_data is not None:\n\t\t\tif context.space_data.type == 'NODE_EDITOR':\n\t\t\t\tv = item.val\n\t\t\t\ti = item.idx\n\t\t\t\tnode = context.active_node\n\t\t\t\tcr = node.color_ramp\n\t\t\t\tstops = cr.elements\n\t\t\t\tnewPos = scale(v, inMin, inMax, 0, 1)\n\t\t\t\t#limit move between previous and next stops\n\t\t\t\tif i+1 == len(stops):#this is the last stop\n\t\t\t\t\tnextPos = 1\n\t\t\t\telse:\n\t\t\t\t\tnextPos = stops[i+1].position\n\t\t\t\tif i == 0:#this is the first stop\n\t\t\t\t\tprevPos = 0\n\t\t\t\telse:\n\t\t\t\t\tprevPos = stops[i-1].position\n\t\t\t\t#\n\t\t\t\tif newPos > nextPos:\n\t\t\t\t\tstops[i].position = nextPos\n\t\t\t\t\titem.val = scale(nextPos, 0, 1, inMin, inMax)\n\t\t\t\telif newPos < prevPos:\n\t\t\t\t\tstops[i].position = prevPos\n\t\t\t\t\titem.val = scale(prevPos, 0, 1, inMin, inMax)\n\t\t\t\telse:\n\t\t\t\t\tstops[i].position = newPos\n\n\t#Define update function for color property\n\tdef updColor(item, context):\n\t\tif context.space_data is not None:\n\t\t\tif context.space_data.type == 'NODE_EDITOR':\n\t\t\t\tcolor = item.color\n\t\t\t\ti = item.idx\n\t\t\t\tnode = context.active_node\n\t\t\t\tcr = node.color_ramp\n\t\t\t\tstops = cr.elements\n\t\t\t\tstops[i].color = color\n\n\t#Properties in the group\n\tidx: IntProperty()\n\tval: FloatProperty(update=updStop)\n\tcolor: FloatVectorProperty(subtype='COLOR', min=0, max=1, update=updColor, size=4)\n\n\n#POPULATE\n#Make function to populate collection\ndef populateList(colorRampNode):\n\tsetBounds()\n\tif colorRampNode is not None:\n\t\tif colorRampNode.bl_idname == 'ShaderNodeValToRGB':\n\t\t\tbpy.context.scene.uiListCollec.clear()\n\t\t\tcr = colorRampNode.color_ramp\n\t\t\tfor i, stop in enumerate(cr.elements):\n\t\t\t\tv = scale(stop.position, 0, 1, inMin, inMax, )\n\t\t\t\titem = bpy.context.scene.uiListCollec.add()\n\t\t\t\titem.idx = i #warn. : assign idx before val because idx is used in property update function\n\t\t\t\titem.val = v #warn. : causes exec. of property update function\n\t\t\t\titem.color = stop.color\n\n\n\n#Set others properties in scene and their update functions\n#########################################\ndef updateAnalysisMode(scn, context):\n\tif context.space_data.type == 'NODE_EDITOR':\n\t\t#refresh\n\t\tnode = context.active_node\n\t\tpopulateList(node)\n\n\n\n\ndef setBounds():\n\tscn = bpy.context.scene\n\tmode = scn.analysisMode\n\tglobal inMin\n\tglobal inMax\n\tglobal obj\n\tif mode == 'HEIGHT':\n\t\tobj = bpy.context.view_layer.objects.active\n\t\tbbox = getBBOX.fromObj(obj)\n\t\tinMin = bbox['zmin']\n\t\tinMax = bbox['zmax']\n\telif mode == 'SLOPE':\n\t\t#slope of a terrain won't exceed vertical plane (90°)\n\t\t#so for easiest calculation we consider slope between 0 and 100°\n\t\tinMin = 0\n\t\tinMax = 100\n\telif mode == 'ASPECT':\n\t\tinMin = 0\n\t\tinMax = 360\n\n\n#Handler to refresh ui list when user\n# > select another obj\n# > change active material\n# > move, delete or add stop on the node\n# > select another color ramp node\n#########################################\n@persistent\ndef scene_update(scn):\n\t'keep colorramp node and reclass panel in synch'\n\tglobal obj\n\tglobal mat\n\tglobal node\n\t#print(node.bl_idname)\n\tactiveObj = bpy.context.view_layer.objects.active\n\tif activeObj is not None:\n\t\tactiveMat = activeObj.active_material\n\t\tif activeMat is not None and activeMat.use_nodes:\n\t\t\tactiveNode = activeMat.node_tree.nodes.active\n\t\t\t#check color ramp node edits\n\t\t\t#>issue : activeMat.is_updated function is no more available in 2.8, use depsgraph instead\n\t\t\t'''\n\t\t\tdepsgraph = bpy.context.evaluated_depsgraph_get() #cause recursion depth error\n\t\t\tif depsgraph.id_type_updated('MATERIAL'):\n\t\t\t\tpopulateList(activeNode)\n\t\t\t'''\n\t\t\t#check selected obj\n\t\t\tif obj != activeObj:\n\t\t\t\tobj = activeObj\n\t\t\t\tpopulateList(activeNode)\n\t\t\t#check active material\n\t\t\tif mat != activeMat:\n\t\t\t\tmat = activeMat\n\t\t\t\tpopulateList(activeNode)\n\t\t\t#check selected node\n\t\t\tif node != activeNode:\n\t\t\t\tnode = activeNode\n\t\t\t\tpopulateList(activeNode)\n\n#Set up ui list\n#########################################\nclass RECLASS_UL_stops(UIList):\n\n\tdef getAspectLabels(self):\n\t\tvals = [round(item.val,2) for item in bpy.context.scene.uiListCollec]\n\t\tif vals == [0, 45, 135, 225, 315]:\n\t\t\treturn ['N', 'E', 'S', 'W', 'N']\n\t\telif vals == [0, 22.5, 67.5, 112.5, 157.5, 202.5, 247.5, 292.5, 337.5]:\n\t\t\treturn ['N', 'N-E', 'E', 'S-E', 'S', 'S-W', 'W', 'N-W', 'N']\n\t\telif vals == [0, 30, 90, 150, 210, 270, 330]:\n\t\t\treturn ['N', 'N-E', 'S-E', 'S', 'S-W', 'N-W', 'N']\n\t\telif vals == [0, 60, 120, 180, 240, 300, 360]:\n\t\t\treturn ['N-E', 'E', 'S-E', 'S-W', 'W', 'N-W', 'N-E']\n\t\telif vals == [0, 90, 270]:\n\t\t\treturn ['N', 'S', 'N']\n\t\telif vals == [0, 180]:\n\t\t\treturn ['E', 'W']\n\t\telse:\n\t\t\treturn False\n\n\tdef draw_item(self, context, layout, data, item, icon, active_data, active_propname, index):\n\t\t'''\n\t\tcalled for each item of the collection visible in the list\n\t\tmust handle the three layout types 'DEFAULT', 'COMPACT' and 'GRID'\n\t\tdata is the object containing the collection (in our case, the scene)\n\t\titem is the current drawn item of the collection (in our case a propertyGroup \"customItem\")\n\t\tindex is index of the current item in the collection (optional)\n\t\t'''\n\t\tscn = bpy.context.scene\n\t\tmode = scn.analysisMode\n\t\tself.use_filter_show = False\n\t\tif self.layout_type in {'DEFAULT', 'COMPACT'}:\n\t\t\tif mode == 'ASPECT':\n\t\t\t\taspectLabels = self.getAspectLabels()\n\t\t\t\tsplit = layout.split(factor=0.2)\n\t\t\t\tif aspectLabels:\n\t\t\t\t\tsplit.label(text=aspectLabels[item.idx])\n\t\t\t\telse:\n\t\t\t\t\tsplit.label(text=str(item.idx+1))\n\t\t\t\tsplit = split.split(factor=0.4)\n\t\t\t\tsplit.prop(item, \"color\", text=\"\")\n\t\t\t\tsplit.prop(item, \"val\", text=\"\")\n\t\t\telse:\n\t\t\t\tsplit = layout.split(factor=0.2)\n\t\t\t\t#split.label(text=str(index))\n\t\t\t\tsplit.label(text=str(item.idx+1))\n\t\t\t\tsplit = split.split(factor=0.4)\n\t\t\t\tsplit.prop(item, \"color\", text=\"\")\n\t\t\t\tsplit.prop(item, \"val\", text=\"\")\n\t\telif self.layout_type in {'GRID'}:\n\t\t\tlayout.alignment = 'CENTER'\n\n\n#Make a Panel\n#########################################\nclass RECLASS_PT_reclassify(Panel):\n\t\"\"\"Creates a panel in the properties of node editor\"\"\"\n\tbl_label = \"Reclassify\"\n\tbl_space_type = 'NODE_EDITOR'\n\tbl_region_type = 'UI'\n\tbl_category = \"Item\"\n\n\tdef draw(self, context):\n\t\tnode = context.active_node\n\t\tif node is not None:\n\t\t\tif node.bl_idname == 'ShaderNodeValToRGB':\n\t\t\t\tlayout = self.layout\n\t\t\t\tscn = context.scene\n\t\t\t\tlayout.prop(scn, \"analysisMode\")\n\t\t\t\trow = layout.row()\n\t\t\t\t#Draw ui list with template_list function\n\t\t\t\trow.template_list(\"RECLASS_UL_stops\", \"\", scn, \"uiListCollec\", scn, \"uiListIndex\", rows=10)\n\t\t\t\t#Draw side tools\n\t\t\t\tcol = row.column(align=True)\n\t\t\t\tcol.operator(\"reclass.list_add\", text=\"\", icon='ADD')\n\t\t\t\tcol.operator(\"reclass.list_rm\", text=\"\", icon='REMOVE')\n\t\t\t\tcol.operator(\"reclass.list_clear\", text=\"\", icon='FILE_PARENT')\n\t\t\t\tcol.separator()\n\t\t\t\tcol.operator(\"reclass.list_refresh\", text=\"\", icon='FILE_REFRESH')\n\t\t\t\tcol.separator()\n\t\t\t\tcol.operator(\"reclass.switch_interpolation\", text=\"\", icon='SMOOTHCURVE')\n\t\t\t\tcol.operator(\"reclass.flip\", text=\"\", icon='ARROW_LEFTRIGHT')\n\t\t\t\tcol.operator(\"reclass.quick_gradient\", text=\"\", icon=\"COLOR\")\n\t\t\t\tcol.operator(\"reclass.svg_gradient\", text=\"\", icon=\"GROUP_VCOL\")\n\t\t\t\tcol.operator(\"reclass.export_svg\", text=\"\", icon=\"FORWARD\")\n\t\t\t\tcol.separator()\n\t\t\t\tcol.operator(\"reclass.auto\", text=\"\", icon='FULLSCREEN_ENTER')\n\t\t\t\t##col.separator()\n\t\t\t\t##col.operator(\"reclass.settings\", text=\"\", icon='PREFERENCES')\n\t\t\t\t#Draw infos\n\t\t\t\t#row = layout.row()\n\t\t\t\t#row.label(text=scn.collection.objects.active.name)\n\t\t\t\trow = layout.row()\n\t\t\t\trow.label(text=\"min = \" + str(round(inMin,2)))\n\t\t\t\trow.label(text=\"max = \" + str(round(inMax,2)))\n\t\t\t\trow = layout.row()\n\t\t\t\trow.label(text=\"delta = \" + str(round(inMax-inMin,2)))\n\n\n#Make Operators to manage ui list\n#########################################\n\nclass RECLASS_OT_switch_interpolation(Operator):\n\t'''Switch color interpolation (continuous / discrete)'''\n\tbl_idname = \"reclass.switch_interpolation\"\n\tbl_label = \"Switch color interpolation (continuous or discrete)\"\n\n\tdef execute(self, context):\n\t\tnode = context.active_node\n\t\tcr = node.color_ramp\n\t\tcr.color_mode = 'RGB'\n\t\tif cr.interpolation != 'CONSTANT':\n\t\t\tcr.interpolation = 'CONSTANT'\n\t\telse:\n\t\t\tcr.interpolation = 'LINEAR'\n\t\treturn {'FINISHED'}\n\nclass RECLASS_OT_flip(Operator):\n\t'''Flip color ramp'''\n\tbl_idname = \"reclass.flip\"\n\tbl_label = \"Flip color ramp\"\n\n\tdef execute(self, context):\n\t\tnode = context.active_node\n\t\tcr = node.color_ramp\n\t\tstops = cr.elements\n\t\t#buid reversed color ramp\n\t\trevStops = []\n\t\tfor i, stop in reversed(list(enumerate(stops))):\n\t\t\trevPos = 1-stop.position\n\t\t\tcolor = tuple(stop.color)\n\t\t\trevStops.append((revPos, color))\n\t\t#assign new position and color\n\t\tfor i, stop in enumerate(stops):\n\t\t\t#stop.position = newStops[i][0]\n\t\t\tstop.color = revStops[i][1]\n\t\t#refresh\n\t\tpopulateList(node)\n\t\treturn {'FINISHED'}\n\nclass RECLASS_OT_refresh(Operator):\n\t\"\"\"Refresh list to match node setting\"\"\"\n\tbl_idname = \"reclass.list_refresh\"\n\tbl_label = \"Populate list\"\n\n\tdef execute(self, context):\n\t\tnode = context.active_node\n\t\tpopulateList(node)\n\t\treturn {'FINISHED'}\n\n\nclass RECLASS_OT_clear(Operator):\n\t\"\"\"Clear color ramp\"\"\"\n\tbl_idname = \"reclass.list_clear\"\n\tbl_label = \"Clear list\"\n\n\tdef execute(self, context):\n\t\t#bpy.context.scene.uiListCollec.clear()\n\t\tnode = context.active_node\n\t\tcr = node.color_ramp\n\t\tstops = cr.elements\n\t\t#remove stops from color ramp\n\t\tfor stop in reversed(stops):\n\t\t\tif len(stops) > 1:#cannot remove last element\n\t\t\t\tstops.remove(stop)\n\t\t\telse:\n\t\t\t\tstop.position = 0\n\t\t#refresh ui list\n\t\tpopulateList(node)\n\t\treturn{'FINISHED'}\n\n\nclass RECLASS_OT_add(Operator):\n\t\"\"\"Add stop\"\"\"\n\tbl_idname = \"reclass.list_add\"\n\tbl_label = \"Add stop\"\n\n\tdef execute(self, context):\n\t\tlst = bpy.context.scene.uiListCollec\n\t\tcurrentIdx = bpy.context.scene.uiListIndex\n\t\tif currentIdx > len(lst)-1:\n\t\t\t#return {'CANCELLED'}\n\t\t\tcurrentIdx = 0 #move ui selection to first idx\n\t\t#lst.add()\n\t\t#\n\t\tnode = context.active_node\n\t\tcr = node.color_ramp\n\t\tstops = cr.elements\n\t\tif len(stops) >=32:\n\t\t\tself.report({'ERROR'}, \"Ramp is limited to 32 colors\")\n\t\t\treturn {'CANCELLED'}\n\t\tcurrentPos = stops[currentIdx].position\n\t\tif currentIdx == len(stops)-1:#last stop\n\t\t\tnextPos = 1.0\n\t\telse:\n\t\t\tnextPos = stops[currentIdx+1].position\n\t\tnewPos = currentPos + ((nextPos-currentPos)/2)\n\t\tstops.new(newPos)\n\t\t#Refresh list\n\t\tpopulateList(node)\n\t\t#Move selection in ui list\n\t\tbpy.context.scene.uiListIndex = currentIdx+1\n\t\treturn {'FINISHED'}\n\n\nclass RECLASS_OT_rm(Operator):\n\t\"\"\"Remove stop\"\"\"\n\tbl_idname = \"reclass.list_rm\"\n\tbl_label = \"Remove Stop\"\n\n\tdef execute(self, context):\n\t\tcurrentIdx = bpy.context.scene.uiListIndex\n\t\tlst = bpy.context.scene.uiListCollec\n\t\tif currentIdx > len(lst)-1:\n\t\t\treturn {'CANCELLED'}\n\t\t#lst.remove(currentIdx)\n\t\t#\n\t\tnode = context.active_node\n\t\tcr = node.color_ramp\n\t\tstops = cr.elements\n\t\tif len(stops) > 1: #cannot remove last element\n\t\t\tstops.remove(stops[currentIdx])\n\t\t#Refresh list\n\t\tpopulateList(node)\n\t\t#Move selecton in ui list if last element has been removed\n\t\tif currentIdx > len(lst)-1:\n\t\t\tbpy.context.scene.uiListIndex = currentIdx-1\n\t\treturn {'FINISHED'}\n\n\n\n#Make Operators to auto reclassify\n#########################################\n\ndef clearRamp(stops, startColor=(0,0,0,1), endColor=(1,1,1,1), startPos=0, endPos=1):\n\t#clear actual color ramp\n\tfor stop in reversed(stops):\n\t\tif len(stops) > 1:#cannot remove last element\n\t\t\tstops.remove(stop)\n\t\telse:#move last element to first position\n\t\t\tfirst = stop\n\t\t\tfirst.position = startPos\n\t\t\tfirst.color = startColor\n\t#Add last stop\n\tlast = stops.new(endPos)\n\tlast.color = endColor\n\treturn (first, last)\n\ndef getValues():\n\t'''Return mesh data values (z, slope or az) for classification'''\n\tscn = bpy.context.scene\n\tobj = bpy.context.view_layer.objects.active\n\t#make a temp mesh with modifiers apply\n\tmesh = obj.to_mesh()\n\tmesh.transform(obj.matrix_world)\n\t#\n\tmode = scn.analysisMode\n\tif mode == 'HEIGHT':\n\t\tvalues = [vertex.co.z for vertex in mesh.vertices]\n\telif mode == 'SLOPE':\n\t\tz = Vector((0,0,1))\n\t\tm = obj.matrix_world\n\t\tvalues =  [math.degrees(z.angle(m * face.normal)) for face in mesh.polygons]\n\telif mode == 'ASPECT':\n\t\ty = Vector((0,1,0))\n\t\tm = obj.matrix_world\n\t\t#values =  [math.degrees(y.angle(m * face.normal)) for face in mesh.polygons]\n\t\tvalues = []\n\t\tfor face in mesh.polygons:\n\t\t\tnormal = face.normal.copy()\n\t\t\tnormal.z = 0 #project vector into XY plane\n\t\t\ttry:\n\t\t\t\ta = math.degrees(y.angle(m * normal))\n\t\t\texcept ValueError:\n\t\t\t\tpass#zero length vector as no angle\n\t\t\telse:\n\t\t\t\t#returned angle is between 0° (north) to 180° (south)\n\t\t\t\t#we must correct it to get angle between 0 to 360°\n\t\t\t\tif normal.x <0:\n\t\t\t\t\ta = 360 - a\n\t\t\t\tvalues.append(a)\n\tvalues.sort()\n\t#remove temp mesh\n\tobj.to_mesh_clear()\n\n\treturn values\n\n\nclass RECLASS_OT_auto(Operator):\n\t'''Auto reclass by equal interval or fixed classe number'''\n\tbl_idname = \"reclass.auto\"\n\tbl_label = \"Reclass by equal interval or fixed classe number\"\n\n\tautoReclassMode: EnumProperty(\n\t\t\tname=\"Mode\",\n\t\t\tdescription=\"Select auto reclassify mode\",\n\t\t\titems=[\n\t\t\t('CLASSES_NB', 'Fixed classes number', \"Define the expected number of classes\"),\n\t\t\t('EQUAL_STEP', 'Equal interval value', \"Define step value between classes\"),\n\t\t\t('TARGET_STEP', 'Target interval value', \"Define target step value that stops will match\"),\n\t\t\t('QUANTILE', 'Quantile', 'Assigns the same number of data values to each class.'),\n\t\t\t('1DKMEANS', 'Natural breaks', 'kmeans clustering optimized for one dimensional data'),\n\t\t\t('ASPECT', 'Aspect reclassification', \"Value define the number of azimuth\")]\n\t\t\t)\n\tcolor1: FloatVectorProperty(name=\"Start color\", subtype='COLOR', min=0, max=1, size=4)\n\tcolor2: FloatVectorProperty(name=\"End color\", subtype='COLOR', min=0, max=1, size=4)\n\tvalue: IntProperty(name=\"Value\", default=4)\n\n\tdef invoke(self, context, event):\n\t\t#Set color to actual ramp\n\t\tnode = context.active_node\n\t\tcr = node.color_ramp\n\t\tstops = cr.elements\n\t\tself.color1 = stops[0].color\n\t\tself.color2 = stops[len(stops)-1].color\n\t\t#Show dialog with operator properties\n\t\twm = context.window_manager\n\t\treturn wm.invoke_props_dialog(self)\n\n\tdef execute(self, context):\n\t\tnode = context.active_node\n\t\tcr = node.color_ramp\n\t\t#switch to linear so new stops will have correctly evaluate color\n\t\tcr.color_mode = 'RGB'\n\t\tcr.interpolation = 'LINEAR'\n\t\tstops = cr.elements\n\t\t#Get colors\n\t\tstartColor = self.color1\n\t\tendColor = self.color2\n\n\t\tif self.autoReclassMode == 'TARGET_STEP':\n\t\t\tinterval = self.value\n\t\t\tdelta = inMax-inMin\n\t\t\tnbClasses = math.ceil(delta/interval)\n\t\t\tif nbClasses >= 32:\n\t\t\t\tself.report({'ERROR'}, \"Ramp is limited to 32 colors\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\tclearRamp(stops, startColor, endColor)\n\t\t\tnextStop = inMin + interval - (inMin % interval)\n\t\t\twhile nextStop < inMax:\n\t\t\t\tposition = scale(nextStop, inMin, inMax, 0, 1)\n\t\t\t\tstop = stops.new(position)\n\t\t\t\tnextStop += interval\n\n\t\tif self.autoReclassMode == 'EQUAL_STEP':\n\t\t\tinterval = self.value\n\t\t\tdelta = inMax-inMin\n\t\t\tnbClasses = math.ceil(delta/interval)\n\t\t\tif nbClasses >= 32:\n\t\t\t\tself.report({'ERROR'}, \"Ramp is limited to 32 colors\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\tclearRamp(stops, startColor, endColor)\n\t\t\tval = inMin\n\t\t\tfor i in range(nbClasses-1):\n\t\t\t\tval += interval\n\t\t\t\tposition = scale(val, inMin, inMax, 0, 1)\n\t\t\t\tstop = stops.new(position)\n\n\t\tif self.autoReclassMode == 'CLASSES_NB':\n\t\t\tnbClasses = self.value\n\t\t\tif nbClasses >= 32:\n\t\t\t\tself.report({'ERROR'}, \"Ramp is limited to 32 colors\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\tdelta = inMax-inMin\n\t\t\tif nbClasses >= delta:\n\t\t\t\tself.report({'ERROR'}, \"Too many classes\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\tclearRamp(stops, startColor, endColor)\n\t\t\tinterval = delta/nbClasses\n\t\t\tval = inMin\n\t\t\tfor i in range(nbClasses-1):\n\t\t\t\tval += interval\n\t\t\t\tposition = scale(val, inMin, inMax, 0, 1)\n\t\t\t\tstop = stops.new(position)\n\n\t\tif self.autoReclassMode == 'ASPECT':\n\t\t\tbpy.context.scene.analysisMode = 'ASPECT'\n\t\t\tdelta = inMax-inMin #360°\n\t\t\tinterval = 360 / self.value\n\t\t\tnbClasses = self.value #math.ceil(delta/interval)\n\t\t\tif nbClasses >= 32:\n\t\t\t\tself.report({'ERROR'}, \"Ramp is limited to 32 colors\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\tfirst, last = clearRamp(stops, startColor, endColor)\n\t\t\toffset = interval/2\n\t\t\tintervalNorm = scale(interval, inMin, inMax, 0, 1)\n\t\t\toffsetNorm = scale(offset, inMin, inMax, 0, 1)\n\t\t\t#move actual last stop to before last position\n\t\t\tlast.position -= intervalNorm + offsetNorm\n\t\t\t#add intermediates stops\n\t\t\tval = 0\n\t\t\tfor i in range(nbClasses-2):\n\t\t\t\tif i == 0:\n\t\t\t\t\tval += offset\n\t\t\t\telse:\n\t\t\t\t\tval += interval\n\t\t\t\tposition = scale(val, inMin, inMax, 0, 1)\n\t\t\t\tstop = stops.new(position)\n\t\t\t#add last\n\t\t\tstop = stops.new(1-offsetNorm)\n\t\t\tstop.color = first.color\n\t\t\tcr.interpolation = 'CONSTANT'\n\n\t\tif self.autoReclassMode == 'QUANTILE':\n\t\t\tnbClasses = self.value\n\t\t\tvalues = getValues()\n\t\t\tif nbClasses >= 32:\n\t\t\t\tself.report({'ERROR'}, \"Ramp is limited to 32 colors\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\tif nbClasses >= len(values):\n\t\t\t\tself.report({'ERROR'}, \"Too many classes\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\tclearRamp(stops, startColor, endColor)\n\t\t\tn = len(values)\n\t\t\tq = int(n/nbClasses) #number of value per quantile\n\t\t\tcumulative_q = q\n\t\t\tpreviousVal = scale(0, 0, 1, inMin, inMax)\n\t\t\tfor i in range(nbClasses-1):\n\t\t\t\tval = values[cumulative_q]\n\t\t\t\tif val != previousVal:\n\t\t\t\t\tposition = scale(val, inMin, inMax, 0, 1)\n\t\t\t\t\tstop = stops.new(position)\n\t\t\t\t\tpreviousVal = val\n\t\t\t\tcumulative_q += q\n\n\t\tif self.autoReclassMode == '1DKMEANS':\n\t\t\tnbClasses = self.value\n\t\t\tvalues = getValues()\n\t\t\tif nbClasses >= 32:\n\t\t\t\tself.report({'ERROR'}, \"Ramp is limited to 32 colors\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\tif nbClasses >= len(values):\n\t\t\t\tself.report({'ERROR'}, \"Too many classes\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\tclearRamp(stops, startColor, endColor)\n\t\t\t#compute clusters\n\t\t\t#clusters = jenksCaspall(values, nbClasses, 4)\n\t\t\t#for val in clusters.breaks:\n\t\t\tclusters = kmeans1d(values, nbClasses)\n\t\t\tfor val in getBreaks(values, clusters):\n\t\t\t\tposition = scale(val, inMin, inMax, 0, 1)\n\t\t\t\tstop = stops.new(position)\n\n\t\t#refresh\n\t\tpopulateList(node)\n\t\treturn {'FINISHED'}\n\n\n#Operators to change color ramp\n#########################################\n\ncolorSpaces = [('RGB', 'RGB', \"RGB color space\"),\n\t\t('HSV', 'HSV', \"HSV color space\")]\n\ninterpoMethods = [('LINEAR', 'Linear', \"Linear interpolation\"),\n\t\t('SPLINE', 'Spline', \"Spline interpolation (Akima's method)\"),\n\t\t('DISCRETE', 'Discrete', \"No interpolation (return previous color)\"),\n\t\t('NEAREST', 'Nearest', \"No interpolation (return nearest color)\") ]\n\n\n#QUICK GRADIENT\nclass RECLASS_PG_color_preview(PropertyGroup):\n\tcolor: FloatVectorProperty(subtype='COLOR', min=0, max=1, size=4)\n\n\n\nclass RECLASS_OT_quick_gradient(Operator):\n\t'''Quick colors gradient edit'''\n\tbl_idname = \"reclass.quick_gradient\"\n\tbl_label = \"Quick colors gradient edit\"\n\n\tcolorSpace: EnumProperty(\n\t\t\tname=\"Space\",\n\t\t\tdescription=\"Select interpolation color space\",\n\t\t\titems = colorSpaces)\n\n\tmethod: EnumProperty(\n\t\t\tname=\"Method\",\n\t\t\tdescription=\"Select interpolation method\",\n\t\t\titems = interpoMethods)\n\n\t#special function to redraw an operator popup called through invoke_props_dialog\n\tdef check(self, context):\n\t\treturn True\n\n\tdef initPreview(self, context):\n\t\tcontext.scene.colorRampPreview.clear()\n\t\tnode = context.active_node\n\t\tcr = node.color_ramp\n\t\tstops = cr.elements\n\t\tif self.fitGradient:\n\t\t\tminPos, maxPos = stops[0].position, stops[-1].position\n\t\t\tdelta = maxPos-minPos\n\t\telse:\n\t\t\tdelta = 1\n\t\toffset = delta/(self.nbColors-1)\n\t\tposition = 0\n\t\tfor i in range(self.nbColors):\n\t\t\titem = bpy.context.scene.colorRampPreview.add()\n\t\t\titem.color = cr.evaluate(position)\n\t\t\tposition += offset\n\t\treturn\n\n\tdef updatePreview(self, context):\n\t\t#Add or remove colors from preview when change nb colors\n\t\tcolorItems = bpy.context.scene.colorRampPreview\n\t\tnb = len(colorItems)\n\t\tif nb == self.nbColors:\n\t\t\treturn\n\t\tdelta = abs(self.nbColors - nb)\n\t\tfor i in range(delta):\n\t\t\tif self.nbColors > nb:\n\t\t\t\titem = colorItems.add()\n\t\t\t\titem.color = colorItems[-2].color\n\t\t\telse:\n\t\t\t\tcolorItems.remove(nb-1)\n\n\tfitGradient: BoolProperty(update=initPreview)\n\n\tnbColors: IntProperty(\n\t\t\tname=\"Number of colors\",\n\t\t\tdescription=\"Set the number of colors needed to define the quick quadient\",\n\t\t\tmin=2, default=4, update=updatePreview)\n\n\tdef invoke(self, context, event):\n\t\t#initialize colors preview\n\t\tself.initPreview(context)\n\t\t#Show dialog with operator properties\n\t\twm = context.window_manager\n\t\treturn wm.invoke_props_dialog(self, width=200, height=200)\n\n\tdef draw(self, context):\n\t\tlayout = self.layout\n\t\tlayout.prop(self, \"colorSpace\", text='Space')\n\t\tlayout.prop(self, \"method\", text='Method')\n\t\tlayout.prop(self, \"fitGradient\", text=\"Fit gradient to min/max positions\")\n\t\tlayout.prop(self, \"nbColors\", text='Number of colors')\n\t\trow = layout.row(align=True)\n\t\tcolorItems = context.scene.colorRampPreview\n\t\tfor i in range(self.nbColors):\n\t\t\tcolorItem = colorItems[i]\n\t\t\trow.prop(colorItem, 'color', text='')\n\n\tdef execute(self, context):\n\t\t#build gradient\n\t\tcolorList = context.scene.colorRampPreview\n\t\tcolorRamp = Gradient()\n\t\tnbColors = len(colorList)\n\t\toffset = 1/(nbColors-1)\n\t\tposition = 0\n\t\tfor i, item in enumerate(colorList):\n\t\t\tcolor = Color(list(item.color), 'rgb')\n\t\t\tcolorRamp.addStop(round(position,4), color)\n\t\t\tposition += offset\n\t\t#get color ramp node\n\t\tnode = context.active_node\n\t\tcr = node.color_ramp\n\t\tstops = cr.elements\n\t\t#rescale\n\t\tif self.fitGradient:\n\t\t\tminPos, maxPos = stops[0].position, stops[-1].position\n\t\t\tcolorRamp.rescale(minPos, maxPos)\n\t\t#update colors\n\t\tfor stop in stops:\n\t\t\tstop.color = colorRamp.evaluate(stop.position, self.colorSpace, self.method).rgba\n\t\t#\n\t\tif self.colorSpace == 'HSV':\n\t\t\tcr.color_mode = 'HSV'\n\t\telse:\n\t\t\tcr.color_mode = 'RGB'\n\t\t#refresh\n\t\tpopulateList(node)\n\t\treturn {'FINISHED'}\n\n#SVG COLOR RAMP\ndef filesList(inFolder, ext):\n\tif not os.path.exists(inFolder):\n\t\t#os.makedirs(inFolder)\n\t\treturn []\n\tlst = os.listdir(inFolder)\n\textLst=[elem for elem in lst if os.path.splitext(elem)[1]==ext]\n\textLst.sort()\n\treturn extLst\n\nsvgFiles = filesList(svgGradientFolder, '.svg')\n\ncolorPreviewRange = 20\n\nclass RECLASS_OT_svg_gradient(Operator):\n\t'''Define colors gradient with presets'''\n\tbl_idname = \"reclass.svg_gradient\"\n\tbl_label = \"Define colors gradient with presets\"\n\n\tdef listSVG(self, context):\n\t\t#Function used to update the gradient list used by the dropdown box.\n\t\tsvgs = [] #list containing tuples of each object\n\t\tfor index, svg in enumerate(svgFiles): #iterate over all objects\n\t\t\tsvgs.append((str(index), os.path.splitext(svg)[0], svgGradientFolder + svg)) #tuple (key, label, tooltip)\n\t\treturn svgs\n\n\tdef updatePreview(self, context):\n\t\tif len(self.colorPresets) == 0:\n\t\t\treturn\n\t\t#build gradient\n\t\tenumIdx = int(self.colorPresets)\n\t\tpath = svgGradientFolder + svgFiles[enumIdx]\n\t\tcolorRamp = Gradient(path)\n\t\t#make preview\n\t\tnbColors = colorPreviewRange\n\t\tinterpoGradient = colorRamp.getRangeColor(nbColors, self.colorSpace, self.method)\n\t\tfor i, stop in enumerate(interpoGradient.stops):\n\t\t\titem = bpy.context.scene.colorRampPreview[i]\n\t\t\titem.color = stop.color.rgba\n\t\treturn\n\n\tcolorPresets: EnumProperty(\n\t\t\tname=\"preset\",\n\t\t\tdescription=\"Select a color ramp preset\",\n\t\t\titems=listSVG,\n\t\t\tupdate=updatePreview\n\t\t\t)\n\n\tcolorSpace: EnumProperty(\n\t\t\tname=\"Space\",\n\t\t\tdescription=\"Select interpolation color space\",\n\t\t\titems = colorSpaces,\n\t\t\tupdate = updatePreview\n\t\t\t)\n\n\tmethod: EnumProperty(\n\t\t\tname=\"Method\",\n\t\t\tdescription=\"Select interpolation method\",\n\t\t\titems = interpoMethods,\n\t\t\tupdate = updatePreview\n\t\t\t)\n\n\tfitGradient: BoolProperty()\n\n\tdef invoke(self, context, event):\n\t\t#clear collection\n\t\tcontext.scene.colorRampPreview.clear()\n\t\t#feed collection\n\t\tfor i in range(colorPreviewRange):\n\t\t\tbpy.context.scene.colorRampPreview.add()\n\t\t#update colors preview\n\t\tself.updatePreview(context)\n\t\t#Show dialog with operator properties\n\t\twm = context.window_manager\n\t\treturn wm.invoke_props_dialog(self, width=200, height=200)\n\n\tdef draw(self, context):#layout for invoke props modal dialog\n\t\t#operator.draw() is different from panel.draw()\n\t\t#because it's only called once (when the pop-up is created)\n\t\tlayout = self.layout\n\t\tlayout.prop(self, \"colorSpace\")\n\t\tlayout.prop(self, \"method\")\n\t\tlayout.prop(self, \"colorPresets\", text='')\n\t\trow = layout.row(align=True)\n\t\trow.enabled = False\n\t\tfor item in context.scene.colorRampPreview:\n\t\t\trow.prop(item, 'color', text='')\n\t\trow = layout.row()\n\t\trow.prop(self, \"fitGradient\", text=\"Fit gradient to min/max positions\")\n\n\tdef execute(self, context):\n\t\tif len(self.colorPresets) == 0:\n\t\t\treturn {'CANCELLED'}\n\t\t#build gradient\n\t\tenumIdx = int(self.colorPresets)\n\t\tpath = svgGradientFolder + svgFiles[enumIdx]\n\t\tcolorRamp = Gradient(path)\n\t\t#get color ramp node\n\t\tnode = context.active_node\n\t\tcr = node.color_ramp\n\t\tstops = cr.elements\n\t\t#rescale\n\t\tif self.fitGradient:\n\t\t\tminPos, maxPos = stops[0].position, stops[-1].position\n\t\t\tcolorRamp.rescale(minPos, maxPos)\n\t\t#update colors\n\t\tfor stop in stops:\n\t\t\tstop.color = colorRamp.evaluate(stop.position, self.colorSpace, self.method).rgba\n\t\t#\n\t\tif self.colorSpace == 'HSV':\n\t\t\tcr.color_mode = 'HSV'\n\t\telse:\n\t\t\tcr.color_mode = 'RGB'\n\t\t#refresh\n\t\tpopulateList(node)\n\t\treturn {'FINISHED'}\n\n\nclass RECLASS_OT_export_svg(Operator):\n\t'''Export current gradient to SVG file'''\n\tbl_idname = \"reclass.export_svg\"\n\tbl_label = \"Export current gradient to SVG file\"\n\n\tname: StringProperty(description=\"Put name of SVG file\")\n\tn: IntProperty(default=5, description=\"Select expected number of interpolate colors\")\n\n\tgradientType: EnumProperty(\n\t\t\tname=\"Build method\",\n\t\t\tdescription=\"Select methods to build gradient\",\n\t\t\titems = [('SELF_STOPS', 'Use actual stops', \"\"),\n\t\t\t('INTERPOLATE', 'Interpolate n colors', \"\")]\n\t\t\t)\n\n\tmakeDiscrete: BoolProperty(name=\"Make discrete\", description=\"Build discrete svg gradient\")\n\n\tcolorSpace: EnumProperty(\n\t\t\tname=\"Color space\",\n\t\t\tdescription=\"Select interpolation color space\",\n\t\t\titems = colorSpaces)\n\n\tmethod: EnumProperty(\n\t\t\tname=\"Interp. method\",\n\t\t\tdescription=\"Select interpolation method\",\n\t\t\titems = interpoMethods)\n\n\t#special function to redraw an operator popup called through invoke_props_dialog\n\tdef check(self, context):\n\t\treturn True\n\n\tdef invoke(self, context, event):\n\t\t#Show dialog with operator properties\n\t\twm = context.window_manager\n\t\treturn wm.invoke_props_dialog(self, width=250, height=200)\n\n\tdef draw(self, context):\n\t\tlayout = self.layout\n\t\tlayout.prop(self, \"name\", text='Name')\n\t\tlayout.prop(self, \"gradientType\")\n\t\tlayout.prop(self, \"makeDiscrete\")\n\t\tif self.gradientType == \"INTERPOLATE\":\n\t\t\tlayout.separator()\n\t\t\tlayout.label(text='Interpolation options')\n\t\t\tlayout.prop(self, \"colorSpace\", text='Color space')\n\t\t\tlayout.prop(self, \"method\", text='Method')\n\t\t\tlayout.prop(self, \"n\", text=\"Number of colors\")\n\n\tdef execute(self, context):\n\t\t#Get node color ramp\n\t\tnode = context.active_node\n\t\tcr = node.color_ramp\n\t\tstops = cr.elements\n\t\t#Build gradient class\n\t\tcolorRamp = Gradient()\n\t\tfor stop in stops:\n\t\t\tcolor = Color(list(stop.color), 'rgba')\n\t\t\tcolorRamp.addStop(stop.position, color)\n\t\t#write svg\n\t\tsvgPath = svgGradientFolder + self.name + '.svg'\n\t\tif self.gradientType == \"INTERPOLATE\":\n\t\t\tinterpoGradient = colorRamp.getRangeColor(self.n, self.colorSpace, self.method)\n\t\t\tinterpoGradient.exportSVG(svgPath, self.makeDiscrete)\n\t\telif self.gradientType == \"SELF_STOPS\":\n\t\t\tcolorRamp.exportSVG(svgPath, self.makeDiscrete)\n\t\t#update svg files list\n\t\tglobal svgFiles\n\t\tsvgFiles = filesList(svgGradientFolder , '.svg')\n\t\treturn {'FINISHED'}\n\n\nclasses = [\n\tRECLASS_PG_color,\n\tRECLASS_PG_color_preview,\n\tRECLASS_UL_stops,\n\tRECLASS_PT_reclassify,\n\tRECLASS_OT_switch_interpolation,\n\tRECLASS_OT_flip,\n\tRECLASS_OT_refresh,\n\tRECLASS_OT_clear,\n\tRECLASS_OT_add,\n\tRECLASS_OT_rm,\n\tRECLASS_OT_auto,\n\tRECLASS_OT_quick_gradient,\n\tRECLASS_OT_svg_gradient,\n\tRECLASS_OT_export_svg\n]\n\ndef register():\n\tfor cls in classes:\n\t\ttry:\n\t\t\tbpy.utils.register_class(cls)\n\t\texcept ValueError as e:\n\t\t\tlog.warning('{} is already registered, now unregister and retry... '.format(cls))\n\t\t\tbpy.utils.unregister_class(cls)\n\t\t\tbpy.utils.register_class(cls)\n\t#Create uilist collections\n\tbpy.types.Scene.uiListCollec = CollectionProperty(type=RECLASS_PG_color)\n\tbpy.types.Scene.uiListIndex = IntProperty() #used to store the index of the selected item in the uilist\n\tbpy.types.Scene.colorRampPreview = CollectionProperty(type=RECLASS_PG_color_preview)\n\t#Add handlers\n\tbpy.app.handlers.depsgraph_update_post.append(scene_update)\n\t#\n\tbpy.types.Scene.analysisMode = EnumProperty(\n\t\tname = \"Mode\",\n\t\tdescription = \"Choose the type of analysis this material do\",\n\t\titems = [('HEIGHT', 'Height', \"Height analysis\"),\n\t\t('SLOPE', 'Slope', \"Slope analysis\"),\n\t\t('ASPECT', 'Aspect', \"Aspect analysis\")],\n\t\tupdate = updateAnalysisMode\n\t\t)\n\ndef unregister():\n\tdel bpy.types.Scene.analysisMode\n\t#Clear uilist\n\tdel bpy.types.Scene.uiListCollec\n\tdel bpy.types.Scene.uiListIndex\n\tdel bpy.types.Scene.colorRampPreview\n\t#Clear handlers\n\tbpy.app.handlers.depsgraph_update_post.clear()\n\t#Unregister\n\tfor cls in classes:\n\t\tbpy.utils.unregister_class(cls)\n"
  },
  {
    "path": "operators/object_drop.py",
    "content": "# ##### BEGIN GPL LICENSE BLOCK #####\n#\n#  This program is free software; you can redistribute it and/or\n#  modify it under the terms of the GNU General Public License\n#  as published by the Free Software Foundation; either version 2\n#  of the License, or (at your option) any later version.\n#\n#  This program is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#  GNU General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public License\n#  along with this program; if not, write to the Free Software Foundation,\n#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.\n#\n# ##### END GPL LICENSE BLOCK #####\n\n# Original Drop to Ground addon code from Unnikrishnan(kodemax), Florian Meyer(testscreenings)\n\nimport logging\nlog = logging.getLogger(__name__)\n\nimport bpy\nimport bmesh\n\nfrom .utils import DropToGround, getBBOX\n\nfrom mathutils import Vector, Matrix\nfrom bpy.types import Operator\nfrom bpy.props import BoolProperty, EnumProperty\n\n\ndef get_align_matrix(location, normal):\n    up = Vector((0, 0, 1))\n    angle = normal.angle(up)\n    axis = up.cross(normal)\n    mat_rot = Matrix.Rotation(angle, 4, axis)\n    mat_loc = Matrix.Translation(location)\n    mat_align = mat_rot @ mat_loc\n    return mat_align\n\ndef get_lowest_world_co(ob, mat_parent=None):\n    bme = bmesh.new()\n    bme.from_mesh(ob.data)\n    mat_to_world = ob.matrix_world.copy()\n    if mat_parent:\n        mat_to_world = mat_parent @ mat_to_world\n    lowest = None\n    for v in bme.verts:\n        if not lowest:\n            lowest = v\n        if (mat_to_world @ v.co).z < (mat_to_world @ lowest.co).z:\n            lowest = v\n    lowest_co = mat_to_world @ lowest.co\n    bme.free()\n\n    return lowest_co\n\n\nclass OBJECT_OT_drop_to_ground(Operator):\n    bl_idname = \"object.drop\"\n    bl_label = \"Drop to Ground\"\n    bl_description = (\"Drop selected objects on the Active object\")\n    bl_options = {\"REGISTER\", \"UNDO\"} #register needed to draw operator options/redo panel\n\n    align: BoolProperty(\n            name=\"Align to ground\",\n            description=\"Aligns the objects' rotation to the ground\",\n            default=False)\n\n    axisAlign: EnumProperty(\n            items = [(\"N\", \"Normal\", \"Ground normal\"), (\"X\", \"X\", \"Ground X normal\"), (\"Y\", \"Y\", \"Ground Y normal\"), (\"Z\", \"Z\", \"Ground Z normal\")],\n            name=\"Align axis\",\n            description=\"\")\n\n    useOrigin: BoolProperty(\n            name=\"Use Origins\",\n            description=\"Drop to objects' origins\\n\"\n                        \"Use this option for dropping all types of Objects\",\n            default=False)\n\n    #this method will disable the button if the conditions are not respected\n    @classmethod\n    def poll(cls, context):\n        act_obj = context.active_object\n        return (context.mode == 'OBJECT'\n                and len(context.selected_objects) >= 2\n                and act_obj\n                and act_obj.type in {'MESH', 'FONT', 'META', 'CURVE', 'SURFACE'}\n                )\n\n    def draw(self, context):\n        layout = self.layout\n        layout.prop(self, 'align')\n        if self.align:\n            layout.prop(self, 'axisAlign')\n        layout.prop(self, 'useOrigin')\n\n\n    def execute(self, context):\n\n        bpy.context.view_layer.update() #needed to make raycast function redoable (evaluate objects)\n        ground = context.active_object\n        obs = context.selected_objects\n        if ground in obs:\n            obs.remove(ground)\n        scn = context.scene\n        rayCaster = DropToGround(scn, ground)\n\n        for ob in obs:\n            if self.useOrigin:\n                minLoc = ob.location\n            else:\n                minLoc = get_lowest_world_co(ob)\n                #minLoc = min([(ob.matrix_world * v.co).z for v in ob.data.vertices])\n                #getBBOX.fromObj(ob).zmin #what xy coords ???\n\n            if not minLoc:\n                msg = \"Object {} is of type {} works only with Use Center option \" \\\n                          \"checked\".format(ob.name, ob.type)\n                log.info(msg)\n\n            x, y = minLoc.x, minLoc.y\n            hit = rayCaster.rayCast(x, y)\n\n            if not hit.hit:\n                log.info(ob.name + \" did not hit the Active Object\")\n                continue\n\n            # simple drop down\n            down = hit.loc - minLoc\n            ob.location += down\n            #ob.location = hit.loc\n\n            # drop with align to hit normal\n            if self.align:\n                vect = ob.location - hit.loc\n                # rotate object to align with face normal\n                normal = get_align_matrix(hit.loc, hit.normal)\n                rot = normal.to_euler()\n                if self.axisAlign == \"X\":\n                    rot.y = 0\n                    rot.z = 0\n                elif self.axisAlign == \"Y\":\n                    rot.x = 0\n                    rot.z = 0\n                elif self.axisAlign == \"Z\":\n                    rot.x = 0\n                    rot.y = 0\n                matrix = ob.matrix_world.copy().to_3x3()\n                matrix.rotate(rot)\n                matrix = matrix.to_4x4()\n                ob.matrix_world = matrix\n                # move_object to hit_location\n                ob.location = hit.loc\n                # move object above surface again\n                vect.rotate(rot)\n                ob.location += vect\n\n\n        return {'FINISHED'}\n\ndef register():\n\ttry:\n\t\tbpy.utils.register_class(OBJECT_OT_drop_to_ground)\n\texcept ValueError as e:\n\t\tlog.warning('{} is already registered, now unregister and retry... '.format(OBJECT_OT_drop_to_ground))\n\t\tunregister()\n\t\tbpy.utils.register_class(OBJECT_OT_drop_to_ground)\n\n\ndef unregister():\n\tbpy.utils.unregister_class(OBJECT_OT_drop_to_ground)\n"
  },
  {
    "path": "operators/utils/__init__.py",
    "content": "from .bgis_utils import placeObj, adjust3Dview, showTextures, addTexture, getBBOX, DropToGround, mouseTo3d, isTopView\nfrom .georaster_utils import rasterExtentToMesh, geoRastUVmap, setDisplacer, bpyGeoRaster, exportAsMesh\nfrom .delaunay_voronoi import computeVoronoiDiagram, computeDelaunayTriangulation\n"
  },
  {
    "path": "operators/utils/bgis_utils.py",
    "content": "\nimport bpy\nfrom mathutils import Vector, Matrix\nfrom mathutils.bvhtree import BVHTree\nfrom bpy_extras.view3d_utils import region_2d_to_location_3d, region_2d_to_vector_3d\n\nfrom ...core import BBOX\n\ndef isTopView(context):\n\tif context.area.type == 'VIEW_3D':\n\t\treg3d = context.region_data\n\telse:\n\t\treturn False\n\treturn reg3d.view_perspective == 'ORTHO' and tuple(reg3d.view_matrix.to_euler()) == (0,0,0)\n\ndef mouseTo3d(context, x, y):\n\t'''Convert event.mouse_region to world coordinates'''\n\tif context.area.type != 'VIEW_3D':\n\t\traise Exception('Wrong context')\n\tcoords = (x, y)\n\treg = context.region\n\treg3d = context.region_data\n\tvec = region_2d_to_vector_3d(reg, reg3d, coords)\n\tloc = region_2d_to_location_3d(reg, reg3d, coords, vec) #WARNING, this function return indeterminate value when view3d clip distance is too large\n\treturn loc\n\n\nclass DropToGround():\n\t'''A class to perform raycasting accross z axis'''\n\n\tdef __init__(self, scn, ground, method='OBJ'):\n\t\tself.method = method # 'BVH' or 'OBJ'\n\t\tself.scn = scn\n\t\tself.ground = ground\n\t\tself.bbox = getBBOX.fromObj(ground, applyTransform=True)\n\t\tself.mw = self.ground.matrix_world\n\t\tself.mwi = self.mw.inverted()\n\t\tif self.method == 'BVH':\n\t\t\tself.bvh = BVHTree.FromObject(self.ground, bpy.context.evaluated_depsgraph_get(), deform=True)\n\n\tdef rayCast(self, x, y):\n\t\t#Hit vector\n\t\toffset = 100\n\t\torgWldSpace = Vector((x, y, self.bbox.zmax + offset))\n\t\torgObjSpace = self.mwi @ orgWldSpace\n\t\tdirection = Vector((0,0,-1)) #down\n\t\t#build ray cast hit namespace object\n\t\tclass RayCastHit(): pass\n\t\trcHit = RayCastHit()\n\t\t#raycast\n\t\tif self.method == 'OBJ':\n\t\t\trcHit.hit, rcHit.loc, rcHit.normal, rcHit.faceIdx = self.ground.ray_cast(orgObjSpace, direction)\n\t\telif self.method == 'BVH':\n\t\t\trcHit.loc, rcHit.normal, rcHit.faceIdx, rcHit.dst = self.bvh.ray_cast(orgObjSpace, direction)\n\t\t\tif not rcHit.loc:\n\t\t\t\trcHit.hit = False\n\t\t\telse:\n\t\t\t\trcHit.hit = True\n\t\t#adjust values\n\t\tif not rcHit.hit:\n\t\t\t#return same original 2d point with z=0\n\t\t\trcHit.loc = Vector((orgWldSpace.x, orgWldSpace.y, 0)) #elseZero\n\t\telse:\n\t\t\trcHit.hit = True\n\n\t\trcHit.loc = self.mw @ rcHit.loc\n\t\treturn rcHit\n\ndef placeObj(mesh, objName):\n\t'''Build and add a new object from a given mesh'''\n\tbpy.ops.object.select_all(action='DESELECT')\n\t#create an object with that mesh\n\tobj = bpy.data.objects.new(objName, mesh)\n\t# Link object to scene\n\tbpy.context.scene.collection.objects.link(obj)\n\tbpy.context.view_layer.objects.active = obj\n\tobj.select_set(True)\n\t#bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY')\n\treturn obj\n\n\ndef adjust3Dview(context, bbox, zoomToSelect=True):\n\t'''adjust all 3d views clip distance to match the submited bbox'''\n\tdst = round(max(bbox.dimensions))\n\tk = 5 #increase factor\n\tdst = dst * k\n\t# set each 3d view\n\tareas = context.screen.areas\n\tfor area in areas:\n\t\tif area.type == 'VIEW_3D':\n\t\t\tspace = area.spaces.active\n\t\t\tif dst < 100:\n\t\t\t\tspace.clip_start = 1\n\t\t\telif dst < 1000:\n\t\t\t\tspace.clip_start = 10\n\t\t\telse:\n\t\t\t\tspace.clip_start = 100\n\t\t\t#Adjust clip end distance if the new obj is largest than actual setting\n\t\t\tif space.clip_end < dst:\n\t\t\t\tif dst > 10000000:\n\t\t\t\t\tdst = 10000000 #too large clip distance broke the 3d view\n\t\t\t\tspace.clip_end = dst\n\t\t\tif zoomToSelect:\n\t\t\t\toverrideContext = context.copy()\n\t\t\t\toverrideContext['area'] = area\n\t\t\t\toverrideContext['region'] = area.regions[-1]\n\t\t\t\tif bpy.app.version[0] > 3:\n\t\t\t\t\twith context.temp_override(**overrideContext):\n\t\t\t\t\t\tbpy.ops.view3d.view_selected()\n\t\t\t\telse:\n\t\t\t\t\tbpy.ops.view3d.view_selected(overrideContext)\n\n\ndef showTextures(context):\n\t'''Force view mode with textures'''\n\tscn = context.scene\n\tfor area in context.screen.areas:\n\t\tif area.type == 'VIEW_3D':\n\t\t\tspace = area.spaces.active\n\t\t\tif space.shading.type == 'SOLID':\n\t\t\t\tspace.shading.color_type = 'TEXTURE'\n\n\ndef addTexture(mat, img, uvLay, name='texture'):\n\t'''Set a new image texture to a given material and following a given uv map'''\n\tengine = bpy.context.scene.render.engine\n\tmat.use_nodes = True\n\tnode_tree = mat.node_tree\n\tnode_tree.nodes.clear()\n\t# create uv map node\n\tuvMapNode = node_tree.nodes.new('ShaderNodeUVMap')\n\tuvMapNode.uv_map = uvLay.name\n\tuvMapNode.location = (-800, 200)\n\t# create image texture node\n\ttextureNode = node_tree.nodes.new('ShaderNodeTexImage')\n\ttextureNode.image = img\n\ttextureNode.extension = 'CLIP'\n\ttextureNode.show_texture = True\n\ttextureNode.location = (-400, 200)\n\t# Create BSDF diffuse node\n\tdiffuseNode = node_tree.nodes.new('ShaderNodeBsdfPrincipled')#ShaderNodeBsdfDiffuse\n\tdiffuseNode.location = (0, 200)\n\t# Create output node\n\toutputNode = node_tree.nodes.new('ShaderNodeOutputMaterial')\n\toutputNode.location = (400, 200)\n\t# Connect the nodes\n\tnode_tree.links.new(uvMapNode.outputs['UV'] , textureNode.inputs['Vector'])\n\tnode_tree.links.new(textureNode.outputs['Color'] , diffuseNode.inputs['Base Color'])#diffuseNode.inputs['Color'])\n\tnode_tree.links.new(diffuseNode.outputs['BSDF'] , outputNode.inputs['Surface'])\n\n\nclass getBBOX():\n\n\t'''Utilities to build BBOX object from various Blender context'''\n\n\t@staticmethod\n\tdef fromObj(obj, applyTransform = True):\n\t\t'''Create a 3D BBOX from Blender object'''\n\t\tif applyTransform:\n\t\t\tboundPts = [obj.matrix_world @ Vector(corner) for corner in obj.bound_box]\n\t\telse:\n\t\t\tboundPts = obj.bound_box\n\t\txmin = min([pt[0] for pt in boundPts])\n\t\txmax = max([pt[0] for pt in boundPts])\n\t\tymin = min([pt[1] for pt in boundPts])\n\t\tymax = max([pt[1] for pt in boundPts])\n\t\tzmin = min([pt[2] for pt in boundPts])\n\t\tzmax = max([pt[2] for pt in boundPts])\n\t\treturn BBOX(xmin=xmin, ymin=ymin, zmin=zmin, xmax=xmax, ymax=ymax, zmax=zmax)\n\n\t@classmethod\n\tdef fromScn(cls, scn):\n\t\t'''Create a 3D BBOX from Blender Scene\n\t\tunion of bounding box of all objects containing in the scene'''\n\t\t#objs = scn.collection.objects\n\t\tobjs = [obj for obj in scn.collection.all_objects if obj.empty_display_type != 'IMAGE']\n\t\tif len(objs) == 0:\n\t\t\tscnBbox = BBOX(0,0,0,0,0,0)\n\t\telse:\n\t\t\tscnBbox = cls.fromObj(objs[0])\n\t\tfor obj in objs:\n\t\t\tbbox = cls.fromObj(obj)\n\t\t\tscnBbox += bbox\n\t\treturn scnBbox\n\n\t@staticmethod\n\tdef fromBmesh(bm):\n\t\t'''Create a 3D bounding box from a bmesh object'''\n\t\txmin = min([pt.co.x for pt in bm.verts])\n\t\txmax = max([pt.co.x for pt in bm.verts])\n\t\tymin = min([pt.co.y for pt in bm.verts])\n\t\tymax = max([pt.co.y for pt in bm.verts])\n\t\tzmin = min([pt.co.z for pt in bm.verts])\n\t\tzmax = max([pt.co.z for pt in bm.verts])\n\t\t#\n\t\treturn BBOX(xmin=xmin, ymin=ymin, zmin=zmin, xmax=xmax, ymax=ymax, zmax=zmax)\n\n\t@staticmethod\n\tdef fromTopView(context):\n\t\t'''Create a 2D BBOX from Blender 3dview if the view is top left ortho else return None'''\n\t\tscn = context.scene\n\t\tarea = context.area\n\t\tif area.type != 'VIEW_3D':\n\t\t\treturn None\n\t\treg = context.region\n\t\treg3d = context.region_data\n\t\tif reg3d.view_perspective != 'ORTHO' or tuple(reg3d.view_matrix.to_euler()) != (0,0,0):\n\t\t\tprint(\"View3d must be in top ortho\")\n\t\t\treturn None\n\t\t#\n\t\tloc = mouseTo3d(context, area.width, area.height)\n\t\txmax, ymax = loc.x, loc.y\n\t\t#\n\t\tloc = mouseTo3d(context, 0, 0)\n\t\txmin, ymin = loc.x, loc.y\n\t\t#\n\t\treturn BBOX(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax)\n"
  },
  {
    "path": "operators/utils/delaunay_voronoi.py",
    "content": "# -*- coding: utf-8 -*-\n\n#############################################################################\n#\n# Voronoi diagram calculator/ Delaunay triangulator\n#\n# - Voronoi Diagram Sweepline algorithm and C code by Steven Fortune, 1987, http://ect.bell-labs.com/who/sjf/\n# - Python translation to file voronoi.py by Bill Simons, 2005, http://www.oxfish.com/\n# - Additional changes for QGIS by Carson Farmer added November 2010\n# - 2012 Ported to Python 3 and additional clip functions by domlysz at gmail.com\n#\n# Calculate Delaunay triangulation or the Voronoi polygons for a set of\n# 2D input points.\n#\n# Derived from code bearing the following notice:\n#\n#  The author of this software is Steven Fortune.  Copyright (c) 1994 by AT&T\n#  Bell Laboratories.\n#  Permission to use, copy, modify, and distribute this software for any\n#  purpose without fee is hereby granted, provided that this entire notice\n#  is included in all copies of any software which is or includes a copy\n#  or modification of this software and in all copies of the supporting\n#  documentation for such software.\n#  THIS SOFTWARE IS BEING PROVIDED \"AS IS\", WITHOUT ANY EXPRESS OR IMPLIED\n#  WARRANTY.  IN PARTICULAR, NEITHER THE AUTHORS NOR AT&T MAKE ANY\n#  REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY\n#  OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE.\n#\n# Comments were incorporated from Shane O'Sullivan's translation of the\n# original code into C++ (http://mapviewer.skynet.ie/voronoi.html)\n#\n# Steve Fortune's homepage: http://netlib.bell-labs.com/cm/cs/who/sjf/index.html\n#\n#\n#\n# For programmatic use two functions are available:\n#\n#\tcomputeVoronoiDiagram(points, xBuff, yBuff, polygonsOutput=False, formatOutput=False) :\n#\tTakes :\n#\t\t- a list of point objects (which must have x and y fields).\n#\t\t- x and y buffer values which are the expansion percentages of the bounding box rectangle including all input points.\n#\t\tReturns :\n#\t\t- With default options : \n#\t\t  A list of 2-tuples, representing the two points of each Voronoi diagram edge.\n#\t\t  Each point contains 2-tuples which are the x,y coordinates of point.\n#\t\t  if formatOutput is True, returns : \n#\t\t\t\t- a list of 2-tuples, which are the x,y coordinates of the Voronoi diagram vertices.\n#\t\t\t\t- and a list of 2-tuples (v1, v2) representing edges of the Voronoi diagram.\n#\t\t\t\t  v1 and v2 are the indices of the vertices at the end of the edge.\n#\t\t- If polygonsOutput option is True, returns :\n#\t\t  A dictionary of polygons, keys are the indices of the input points,\n#\t\t  values contains n-tuples representing the n points of each Voronoi diagram polygon.\n#\t\t  Each point contains 2-tuples which are the x,y coordinates of point.\n#\t\t  if formatOutput is True, returns : \n#\t\t\t\t- A list of 2-tuples, which are the x,y coordinates of the Voronoi diagram vertices.\n#\t\t\t\t- and a dictionary of input points indices. Values contains n-tuples representing the n points of each Voronoi diagram polygon.\n#\t\t\t\t  Each tuple contains the vertex indices of the polygon vertices.\n#\n#\tcomputeDelaunayTriangulation(points):\n#\t\tTakes a list of point objects (which must have x and y fields).\n#\t\tReturns a list of 3-tuples: the indices of the points that form a Delaunay triangle.\n#\n#############################################################################\nimport math\nimport sys\nimport getopt\nTOLERANCE = 1e-9\nBIG_FLOAT = 1e38\n\nif sys.version > '3':\n\tPY3 = True\nelse:\n\tPY3 = False\n\n#------------------------------------------------------------------\nclass Context(object):\n\tdef __init__(self):\n\t\tself.doPrint = 0\n\t\tself.debug\t= 0\n\t\tself.extent=()#tuple (xmin, xmax, ymin, ymax)\n\t\tself.triangulate = False\n\t\tself.vertices  = [] # list of vertex 2-tuples: (x,y)\n\t\tself.lines\t = [] # equation of line 3-tuple (a b c), for the equation of the line a*x+b*y = c\n\t\tself.edges\t = [] # edge 3-tuple: (line index, vertex 1 index, vertex 2 index)\tif either vertex index is -1, the edge extends to infinity\n\t\tself.triangles = [] # 3-tuple of vertex indices\n\t\tself.polygons  = {} # a dict of site:[edges] pairs\n\n########Clip functions########\n\tdef getClipEdges(self):\n\t\txmin, xmax, ymin, ymax = self.extent\n\t\tclipEdges=[]\n\t\tfor edge in self.edges:\n\t\t\tequation=self.lines[edge[0]]#line equation\n\t\t\tif edge[1]!=-1 and edge[2]!=-1:#finite line\n\t\t\t\tx1, y1=self.vertices[edge[1]][0], self.vertices[edge[1]][1]\n\t\t\t\tx2, y2=self.vertices[edge[2]][0], self.vertices[edge[2]][1]\n\t\t\t\tpt1, pt2 = (x1,y1), (x2,y2)\n\t\t\t\tinExtentP1, inExtentP2 = self.inExtent(x1,y1), self.inExtent(x2,y2)\n\t\t\t\tif inExtentP1 and inExtentP2:\n\t\t\t\t\tclipEdges.append((pt1, pt2))\n\t\t\t\telif inExtentP1 and not inExtentP2:\n\t\t\t\t\tpt2=self.clipLine(x1, y1, equation, leftDir=False)\n\t\t\t\t\tclipEdges.append((pt1, pt2))\n\t\t\t\telif not inExtentP1 and inExtentP2:\n\t\t\t\t\tpt1=self.clipLine(x2, y2, equation, leftDir=True)\n\t\t\t\t\tclipEdges.append((pt1, pt2))\n\t\t\telse:#infinite line\n\t\t\t\tif edge[1]!=-1:\n\t\t\t\t\tx1, y1 = self.vertices[edge[1]][0], self.vertices[edge[1]][1]\n\t\t\t\t\tleftDir=False\n\t\t\t\telse:\n\t\t\t\t\tx1, y1 = self.vertices[edge[2]][0], self.vertices[edge[2]][1]\n\t\t\t\t\tleftDir=True\n\t\t\t\tif self.inExtent(x1,y1):\n\t\t\t\t\tpt1=(x1,y1)\n\t\t\t\t\tpt2=self.clipLine(x1, y1, equation, leftDir)\n\t\t\t\t\tclipEdges.append((pt1, pt2))\n\t\treturn clipEdges\n\n\n\tdef getClipPolygons(self, closePoly):\n\t\txmin, xmax, ymin, ymax = self.extent\n\t\tpoly={}\n\t\tfor inPtsIdx, edges in self.polygons.items():\n\t\t\tclipEdges=[]\n\t\t\tfor edge in edges:\n\t\t\t\tequation=self.lines[edge[0]]#line equation\n\t\t\t\tif edge[1]!=-1 and edge[2]!=-1:#finite line\n\t\t\t\t\tx1, y1=self.vertices[edge[1]][0], self.vertices[edge[1]][1]\n\t\t\t\t\tx2, y2=self.vertices[edge[2]][0], self.vertices[edge[2]][1]\n\t\t\t\t\tpt1, pt2 = (x1,y1), (x2,y2)\n\t\t\t\t\tinExtentP1, inExtentP2 = self.inExtent(x1,y1), self.inExtent(x2,y2)\n\t\t\t\t\tif inExtentP1 and inExtentP2:\n\t\t\t\t\t\tclipEdges.append((pt1, pt2))\n\t\t\t\t\telif inExtentP1 and not inExtentP2:\n\t\t\t\t\t\tpt2=self.clipLine(x1, y1, equation, leftDir=False)\n\t\t\t\t\t\tclipEdges.append((pt1, pt2))\n\t\t\t\t\telif not inExtentP1 and inExtentP2:\n\t\t\t\t\t\tpt1=self.clipLine(x2, y2, equation, leftDir=True)\n\t\t\t\t\t\tclipEdges.append((pt1, pt2))\n\t\t\t\telse:#infinite line\n\t\t\t\t\tif edge[1]!=-1:\n\t\t\t\t\t\tx1, y1 = self.vertices[edge[1]][0], self.vertices[edge[1]][1]\n\t\t\t\t\t\tleftDir=False\n\t\t\t\t\telse:\n\t\t\t\t\t\tx1, y1 = self.vertices[edge[2]][0], self.vertices[edge[2]][1]\n\t\t\t\t\t\tleftDir=True\n\t\t\t\t\tif self.inExtent(x1,y1):\n\t\t\t\t\t\tpt1=(x1,y1)\n\t\t\t\t\t\tpt2=self.clipLine(x1, y1, equation, leftDir)\n\t\t\t\t\t\tclipEdges.append((pt1, pt2))\n\t\t\t#create polygon definition from edges and check if polygon is completely closed\n\t\t\tpolyPts, complete=self.orderPts(clipEdges)\n\t\t\tif not complete:\n\t\t\t\tstartPt=polyPts[0]\n\t\t\t\tendPt=polyPts[-1]\n\t\t\t\tif startPt[0]==endPt[0] or startPt[1]==endPt[1]: #if start & end points are collinear then they are along an extent border\n\t\t\t\t\tpolyPts.append(polyPts[0])#simple close\n\t\t\t\telse:#close at extent corner\n\t\t\t\t\tif (startPt[0]==xmin and endPt[1]==ymax) or (endPt[0]==xmin and startPt[1]==ymax): #upper left\n\t\t\t\t\t\tpolyPts.append((xmin, ymax))#corner point\n\t\t\t\t\t\tpolyPts.append(polyPts[0])#close polygon\n\t\t\t\t\tif (startPt[0]==xmax and endPt[1]==ymax) or (endPt[0]==xmax and startPt[1]==ymax): #upper right\n\t\t\t\t\t\tpolyPts.append((xmax, ymax))\n\t\t\t\t\t\tpolyPts.append(polyPts[0])\n\t\t\t\t\tif (startPt[0]==xmax and endPt[1]==ymin) or (endPt[0]==xmax and startPt[1]==ymin): #bottom right\n\t\t\t\t\t\tpolyPts.append((xmax, ymin))\n\t\t\t\t\t\tpolyPts.append(polyPts[0])\n\t\t\t\t\tif (startPt[0]==xmin and endPt[1]==ymin) or (endPt[0]==xmin and startPt[1]==ymin): #bottom left\n\t\t\t\t\t\tpolyPts.append((xmin, ymin))\n\t\t\t\t\t\tpolyPts.append(polyPts[0])\n\t\t\tif not closePoly:#unclose polygon\n\t\t\t\tpolyPts=polyPts[:-1]\n\t\t\tpoly[inPtsIdx]=polyPts\n\t\treturn poly\n\n\tdef clipLine(self, x1, y1, equation, leftDir):\n\t\txmin, xmax, ymin, ymax = self.extent\n\t\ta,b,c=equation\n\t\tif b==0:#vertical line\n\t\t\tif leftDir:#left is bottom of vertical line\n\t\t\t\treturn (x1,ymax)\n\t\t\telse:\n\t\t\t\treturn (x1,ymin)\n\t\telif a==0:#horizontal line\n\t\t\tif leftDir:\n\t\t\t\treturn (xmin,y1)\n\t\t\telse:\n\t\t\t\treturn (xmax,y1)\n\t\telse:\n\t\t\ty2_at_xmin=(c-a*xmin)/b\n\t\t\ty2_at_xmax=(c-a*xmax)/b\n\t\t\tx2_at_ymin=(c-b*ymin)/a\n\t\t\tx2_at_ymax=(c-b*ymax)/a\n\t\t\tintersectPts=[]\n\t\t\tif ymin<=y2_at_xmin<=ymax:#valid intersect point\n\t\t\t\tintersectPts.append((xmin, y2_at_xmin))\n\t\t\tif ymin<=y2_at_xmax<=ymax:\n\t\t\t\tintersectPts.append((xmax, y2_at_xmax))\n\t\t\tif xmin<=x2_at_ymin<=xmax:\n\t\t\t\tintersectPts.append((x2_at_ymin, ymin))\n\t\t\tif xmin<=x2_at_ymax<=xmax:\n\t\t\t\tintersectPts.append((x2_at_ymax, ymax))\n\t\t\t#delete duplicate (happens if intersect point is at extent corner)\n\t\t\tintersectPts=set(intersectPts)\n\t\t\t#choose target intersect point\n\t\t\tif leftDir:\n\t\t\t\tpt=min(intersectPts)#smaller x value\n\t\t\telse:\n\t\t\t\tpt=max(intersectPts)\n\t\t\treturn pt\n\n\tdef inExtent(self, x, y):\n\t\txmin, xmax, ymin, ymax = self.extent\n\t\treturn x>=xmin and x<=xmax and y>=ymin and y<=ymax\n\n\tdef orderPts(self, edges):\n\t\tpoly=[]#returned polygon points list [pt1, pt2, pt3, pt4 ....]\n\t\tpts=[]\n\t\t#get points list\n\t\tfor edge in edges:\n\t\t\tpts.extend([pt for pt in edge])\n\t\t#try to get start & end point\n\t\ttry:\n\t\t\tstartPt, endPt = [pt for pt in pts if pts.count(pt)<2]#start and end point aren't duplicate\n\t\texcept:#all points are duplicate --> polygon is complete --> append some or other edge points\n\t\t\tcomplete=True\n\t\t\tfirstIdx=0\n\t\t\tpoly.append(edges[0][0])\n\t\t\tpoly.append(edges[0][1])\n\t\telse:#incomplete --> append the first edge points\n\t\t\tcomplete=False\n\t\t\t#search first edge\n\t\t\tfor i, edge in enumerate(edges):\n\t\t\t\tif startPt in edge:#find\n\t\t\t\t\tfirstIdx=i\n\t\t\t\t\tbreak\n\t\t\tpoly.append(edges[firstIdx][0])\n\t\t\tpoly.append(edges[firstIdx][1])\n\t\t\tif poly[0]!=startPt: poly.reverse()\n\t\t#append next points in list\n\t\tdel edges[firstIdx]\n\t\twhile edges:#all points will be treated when edges list will be empty\n\t\t\tcurrentPt = poly[-1]#last item\n\t\t\tfor i, edge in enumerate(edges):\n\t\t\t\tif currentPt==edge[0]:\n\t\t\t\t\tpoly.append(edge[1])\n\t\t\t\t\tbreak\n\t\t\t\telif currentPt==edge[1]:\n\t\t\t\t\tpoly.append(edge[0])\n\t\t\t\t\tbreak\n\t\t\tdel edges[i]\n\t\treturn poly, complete\n\n\tdef setClipBuffer(self, xpourcent, ypourcent):\n\t\txmin, xmax, ymin, ymax = self.extent\n\t\twitdh=xmax-xmin\n\t\theight=ymax-ymin\n\t\txmin=xmin-witdh*xpourcent/100\n\t\txmax=xmax+witdh*xpourcent/100\n\t\tymin=ymin-height*ypourcent/100\n\t\tymax=ymax+height*ypourcent/100\n\t\tself.extent=xmin, xmax, ymin, ymax\n\n########End clip functions########\n\n\tdef outSite(self,s):\n\t\tif(self.debug):\n\t\t\tprint(\"site (%d) at %f %f\" % (s.sitenum, s.x, s.y))\n\t\telif(self.triangulate):\n\t\t\tpass\n\t\telif(self.doPrint):\n\t\t\tprint(\"s %f %f\" % (s.x, s.y))\n\n\tdef outVertex(self,s):\n\t\tself.vertices.append((s.x,s.y))\n\t\tif(self.debug):\n\t\t\tprint(\"vertex(%d) at %f %f\" % (s.sitenum, s.x, s.y))\n\t\telif(self.triangulate):\n\t\t\tpass\n\t\telif(self.doPrint):\n\t\t\tprint(\"v %f %f\" % (s.x,s.y))\n\n\tdef outTriple(self,s1,s2,s3):\n\t\tself.triangles.append((s1.sitenum, s2.sitenum, s3.sitenum))\n\t\tif(self.debug):\n\t\t\tprint(\"circle through left=%d right=%d bottom=%d\" % (s1.sitenum, s2.sitenum, s3.sitenum))\n\t\telif(self.triangulate and self.doPrint):\n\t\t\tprint(\"%d %d %d\" % (s1.sitenum, s2.sitenum, s3.sitenum))\n\n\tdef outBisector(self,edge):\n\t\tself.lines.append((edge.a, edge.b, edge.c))\n\t\tif(self.debug):\n\t\t\tprint(\"line(%d) %gx+%gy=%g, bisecting %d %d\" % (edge.edgenum, edge.a, edge.b, edge.c, edge.reg[0].sitenum, edge.reg[1].sitenum))\n\t\telif(self.doPrint):\n\t\t\tprint(\"l %f %f %f\" % (edge.a, edge.b, edge.c))\n\n\tdef outEdge(self,edge):\n\t\tsitenumL = -1\n\t\tif edge.ep[Edge.LE] is not None:\n\t\t\tsitenumL = edge.ep[Edge.LE].sitenum\n\t\tsitenumR = -1\n\t\tif edge.ep[Edge.RE] is not None:\n\t\t\tsitenumR = edge.ep[Edge.RE].sitenum\n\n\t\t#polygons dict add by CF\n\t\tif edge.reg[0].sitenum not in self.polygons:\n\t\t\tself.polygons[edge.reg[0].sitenum] = []\n\t\tif edge.reg[1].sitenum not in self.polygons:\n\t\t\tself.polygons[edge.reg[1].sitenum] = []\n\t\tself.polygons[edge.reg[0].sitenum].append((edge.edgenum,sitenumL,sitenumR))\n\t\tself.polygons[edge.reg[1].sitenum].append((edge.edgenum,sitenumL,sitenumR))\n\n\t\tself.edges.append((edge.edgenum,sitenumL,sitenumR))\n\n\t\tif(not self.triangulate):\n\t\t\tif(self.doPrint):\n\t\t\t\tprint(\"e %d\" % edge.edgenum)\n\t\t\t\tprint(\" %d \" % sitenumL)\n\t\t\t\tprint(\"%d\" % sitenumR)\n\n#------------------------------------------------------------------\ndef voronoi(siteList,context):\n\tcontext.extent=siteList.extent\n\tedgeList  = EdgeList(siteList.xmin,siteList.xmax,len(siteList))\n\tpriorityQ = PriorityQueue(siteList.ymin,siteList.ymax,len(siteList))\n\tsiteIter = siteList.iterator()\n\n\tbottomsite = siteIter.next()\n\tcontext.outSite(bottomsite)\n\tnewsite = siteIter.next()\n\tminpt = Site(-BIG_FLOAT,-BIG_FLOAT)\n\twhile True:\n\t\tif not priorityQ.isEmpty():\n\t\t\tminpt = priorityQ.getMinPt()\n\n\t\tif (newsite and (priorityQ.isEmpty() or newsite<minpt)):\n\t\t\t# newsite is smallest -  this is a site event\n\t\t\tcontext.outSite(newsite)\n\n\t\t\t# get first Halfedge to the LEFT and RIGHT of the new site\n\t\t\tlbnd = edgeList.leftbnd(newsite)\n\t\t\trbnd = lbnd.right\n\n\t\t\t# if this halfedge has no edge, bot = bottom site (whatever that is)\n\t\t\t# create a new edge that bisects\n\t\t\tbot  = lbnd.rightreg(bottomsite)\n\t\t\tedge = Edge.bisect(bot,newsite)\n\t\t\tcontext.outBisector(edge)\n\n\t\t\t# create a new Halfedge, setting its pm field to 0 and insert\n\t\t\t# this new bisector edge between the left and right vectors in\n\t\t\t# a linked list\n\t\t\tbisector = Halfedge(edge,Edge.LE)\n\t\t\tedgeList.insert(lbnd,bisector)\n\n\t\t\t# if the new bisector intersects with the left edge, remove\n\t\t\t# the left edge's vertex, and put in the new one\n\t\t\tp = lbnd.intersect(bisector)\n\t\t\tif p is not None:\n\t\t\t\tpriorityQ.delete(lbnd)\n\t\t\t\tpriorityQ.insert(lbnd,p,newsite.distance(p))\n\n\t\t\t# create a new Halfedge, setting its pm field to 1\n\t\t\t# insert the new Halfedge to the right of the original bisector\n\t\t\tlbnd = bisector\n\t\t\tbisector = Halfedge(edge,Edge.RE)\n\t\t\tedgeList.insert(lbnd,bisector)\n\n\t\t\t# if this new bisector intersects with the right Halfedge\n\t\t\tp = bisector.intersect(rbnd)\n\t\t\tif p is not None:\n\t\t\t\t# push the Halfedge into the ordered linked list of vertices\n\t\t\t\tpriorityQ.insert(bisector,p,newsite.distance(p))\n\n\t\t\tnewsite = siteIter.next()\n\n\t\telif not priorityQ.isEmpty():\n\t\t\t# intersection is smallest - this is a vector (circle) event\n\n\t\t\t# pop the Halfedge with the lowest vector off the ordered list of\n\t\t\t# vectors.  Get the Halfedge to the left and right of the above HE\n\t\t\t# and also the Halfedge to the right of the right HE\n\t\t\tlbnd  = priorityQ.popMinHalfedge()\n\t\t\tllbnd = lbnd.left\n\t\t\trbnd  = lbnd.right\n\t\t\trrbnd = rbnd.right\n\n\t\t\t# get the Site to the left of the left HE and to the right of\n\t\t\t# the right HE which it bisects\n\t\t\tbot = lbnd.leftreg(bottomsite)\n\t\t\ttop = rbnd.rightreg(bottomsite)\n\n\t\t\t# output the triple of sites, stating that a circle goes through them\n\t\t\tmid = lbnd.rightreg(bottomsite)\n\t\t\tcontext.outTriple(bot,top,mid)\n\n\t\t\t# get the vertex that caused this event and set the vertex number\n\t\t\t# couldn't do this earlier since we didn't know when it would be processed\n\t\t\tv = lbnd.vertex\n\t\t\tsiteList.setSiteNumber(v)\n\t\t\tcontext.outVertex(v)\n\n\t\t\t# set the endpoint of the left and right Halfedge to be this vector\n\t\t\tif lbnd.edge.setEndpoint(lbnd.pm,v):\n\t\t\t\tcontext.outEdge(lbnd.edge)\n\n\t\t\tif rbnd.edge.setEndpoint(rbnd.pm,v):\n\t\t\t\tcontext.outEdge(rbnd.edge)\n\n\n\t\t\t# delete the lowest HE, remove all vertex events to do with the\n\t\t\t# right HE and delete the right HE\n\t\t\tedgeList.delete(lbnd)\n\t\t\tpriorityQ.delete(rbnd)\n\t\t\tedgeList.delete(rbnd)\n\n\n\t\t\t# if the site to the left of the event is higher than the Site\n\t\t\t# to the right of it, then swap them and set 'pm' to RIGHT\n\t\t\tpm = Edge.LE\n\t\t\tif bot.y > top.y:\n\t\t\t\tbot,top = top,bot\n\t\t\t\tpm = Edge.RE\n\n\t\t\t# Create an Edge (or line) that is between the two Sites.  This\n\t\t\t# creates the formula of the line, and assigns a line number to it\n\t\t\tedge = Edge.bisect(bot, top)\n\t\t\tcontext.outBisector(edge)\n\n\t\t\t# create a HE from the edge\n\t\t\tbisector = Halfedge(edge, pm)\n\n\t\t\t# insert the new bisector to the right of the left HE\n\t\t\t# set one endpoint to the new edge to be the vector point 'v'\n\t\t\t# If the site to the left of this bisector is higher than the right\n\t\t\t# Site, then this endpoint is put in position 0; otherwise in pos 1\n\t\t\tedgeList.insert(llbnd, bisector)\n\t\t\tif edge.setEndpoint(Edge.RE - pm, v):\n\t\t\t\tcontext.outEdge(edge)\n\n\t\t\t# if left HE and the new bisector don't intersect, then delete\n\t\t\t# the left HE, and reinsert it\n\t\t\tp = llbnd.intersect(bisector)\n\t\t\tif p is not None:\n\t\t\t\tpriorityQ.delete(llbnd);\n\t\t\t\tpriorityQ.insert(llbnd, p, bot.distance(p))\n\n\t\t\t# if right HE and the new bisector don't intersect, then reinsert it\n\t\t\tp = bisector.intersect(rrbnd)\n\t\t\tif p is not None:\n\t\t\t\tpriorityQ.insert(bisector, p, bot.distance(p))\n\t\telse:\n\t\t\tbreak\n\n\the = edgeList.leftend.right\n\twhile he is not edgeList.rightend:\n\t\tcontext.outEdge(he.edge)\n\t\the = he.right\n\tEdge.EDGE_NUM = 0#CF\n\n#------------------------------------------------------------------\ndef isEqual(a,b,relativeError=TOLERANCE):\n\t# is nearly equal to within the allowed relative error\n\tnorm = max(abs(a),abs(b))\n\treturn (norm < relativeError) or (abs(a - b) < (relativeError * norm))\n\n#------------------------------------------------------------------\nclass Site(object):\n\tdef __init__(self,x=0.0,y=0.0,sitenum=0):\n\t\tself.x = x\n\t\tself.y = y\n\t\tself.sitenum = sitenum\n\n\tdef dump(self):\n\t\tprint(\"Site #%d (%g, %g)\" % (self.sitenum,self.x,self.y))\n\n\tdef __lt__(self,other):\n\t\tif self.y < other.y:\n\t\t\treturn True\n\t\telif self.y > other.y:\n\t\t\treturn False\n\t\telif self.x < other.x:\n\t\t\treturn True\n\t\telif self.x > other.x:\n\t\t\treturn False\n\t\telse:\n\t\t\treturn False\n\n\tdef __eq__(self,other):\n\t\tif self.y == other.y and self.x == other.x:\n\t\t\treturn True\n\n\tdef distance(self,other):\n\t\tdx = self.x - other.x\n\t\tdy = self.y - other.y\n\t\treturn math.sqrt(dx*dx + dy*dy)\n\n#------------------------------------------------------------------\nclass Edge(object):\n\tLE = 0#left end indice --> edge.ep[Edge.LE]\n\tRE = 1#right end indice\n\tEDGE_NUM = 0\n\tDELETED = {}\t# marker value\n\n\tdef __init__(self):\n\t\tself.a = 0.0#equation of the line a*x+b*y = c\n\t\tself.b = 0.0\n\t\tself.c = 0.0\n\t\tself.ep  = [None,None]#end point (2 tuples of site)\n\t\tself.reg = [None,None]\n\t\tself.edgenum = 0\n\n\tdef dump(self):\n\t\tprint(\"(#%d a=%g, b=%g, c=%g)\" % (self.edgenum,self.a,self.b,self.c))\n\t\tprint(\"ep\",self.ep)\n\t\tprint(\"reg\",self.reg)\n\n\tdef setEndpoint(self, lrFlag, site):\n\t\tself.ep[lrFlag] = site\n\t\tif self.ep[Edge.RE - lrFlag] is None:\n\t\t\treturn False\n\t\treturn True\n\n\t@staticmethod\n\tdef bisect(s1,s2):\n\t\tnewedge = Edge()\n\t\tnewedge.reg[0] = s1 # store the sites that this edge is bisecting\n\t\tnewedge.reg[1] = s2\n\n\t\t# to begin with, there are no endpoints on the bisector - it goes to infinity\n\t\t# ep[0] and ep[1] are None\n\n\t\t# get the difference in x dist between the sites\n\t\tdx = float(s2.x - s1.x)\n\t\tdy = float(s2.y - s1.y)\n\t\tadx = abs(dx)  # make sure that the difference in positive\n\t\tady = abs(dy)\n\n\t\t# get the slope of the line\n\t\tnewedge.c = float(s1.x * dx + s1.y * dy + (dx*dx + dy*dy)*0.5)\n\t\tif adx > ady :\n\t\t\t# set formula of line, with x fixed to 1\n\t\t\tnewedge.a = 1.0\n\t\t\tnewedge.b = dy/dx\n\t\t\tnewedge.c /= dx\n\t\telse:\n\t\t\t# set formula of line, with y fixed to 1\n\t\t\tnewedge.b = 1.0\n\t\t\tnewedge.a = dx/dy\n\t\t\tnewedge.c /= dy\n\n\t\tnewedge.edgenum = Edge.EDGE_NUM\n\t\tEdge.EDGE_NUM += 1\n\t\treturn newedge\n\n\n#------------------------------------------------------------------\nclass Halfedge(object):\n\tdef __init__(self,edge=None,pm=Edge.LE):\n\t\tself.left  = None\t# left Halfedge in the edge list\n\t\tself.right = None\t# right Halfedge in the edge list\n\t\tself.qnext = None\t# priority queue linked list pointer\n\t\tself.edge  = edge\t# edge list Edge\n\t\tself.pm = pm\n\t\tself.vertex = None  # Site()\n\t\tself.ystar  = BIG_FLOAT\n\n\tdef dump(self):\n\t\tprint(\"Halfedge--------------------------\")\n\t\tprint(\"left: \",\tself.left)\n\t\tprint(\"right: \",\tself.right)\n\t\tprint(\"edge: \",\tself.edge)\n\t\tprint(\"pm: \",\t  self.pm)\n\t\tprint(\"vertex: \"),\n\t\tif self.vertex: self.vertex.dump()\n\t\telse: print(\"None\")\n\t\tprint(\"ystar: \", self.ystar)\n\n\tdef __lt__(self,other):\n\t\tif self.ystar < other.ystar:\n\t\t\treturn True\n\t\telif self.ystar > other.ystar:\n\t\t\treturn False\n\t\telif self.vertex.x < other.vertex.x:\n\t\t\treturn True\n\t\telif self.vertex.x > other.vertex.x:\n\t\t\treturn False\n\t\telse:\n\t\t\treturn False\n\n\tdef __eq__(self,other):\n\t\tif self.ystar == other.ystar and self.vertex.x == other.vertex.x:\n\t\t\treturn True\n\n\tdef leftreg(self,default):\n\t\tif not self.edge:\n\t\t\treturn default\n\t\telif self.pm == Edge.LE:\n\t\t\treturn self.edge.reg[Edge.LE]\n\t\telse:\n\t\t\treturn self.edge.reg[Edge.RE]\n\n\tdef rightreg(self,default):\n\t\tif not self.edge:\n\t\t\treturn default\n\t\telif self.pm == Edge.LE:\n\t\t\treturn self.edge.reg[Edge.RE]\n\t\telse:\n\t\t\treturn self.edge.reg[Edge.LE]\n\n\n\t# returns True if p is to right of halfedge self\n\tdef isPointRightOf(self,pt):\n\t\te = self.edge\n\t\ttopsite = e.reg[1]\n\t\tright_of_site = pt.x > topsite.x\n\n\t\tif(right_of_site and self.pm == Edge.LE):\n\t\t\treturn True\n\n\t\tif(not right_of_site and self.pm == Edge.RE):\n\t\t\treturn False\n\n\t\tif(e.a == 1.0):\n\t\t\tdyp = pt.y - topsite.y\n\t\t\tdxp = pt.x - topsite.x\n\t\t\tfast = 0;\n\t\t\tif ((not right_of_site and e.b < 0.0) or (right_of_site and e.b >= 0.0)):\n\t\t\t\tabove = dyp >= e.b * dxp\n\t\t\t\tfast = above\n\t\t\telse:\n\t\t\t\tabove = pt.x + pt.y * e.b > e.c\n\t\t\t\tif(e.b < 0.0):\n\t\t\t\t\tabove = not above\n\t\t\t\tif (not above):\n\t\t\t\t\tfast = 1\n\t\t\tif (not fast):\n\t\t\t\tdxs = topsite.x - (e.reg[0]).x\n\t\t\t\tabove = e.b * (dxp*dxp - dyp*dyp) < dxs*dyp*(1.0+2.0*dxp/dxs + e.b*e.b)\n\t\t\t\tif(e.b < 0.0):\n\t\t\t\t\tabove = not above\n\t\telse:  # e.b == 1.0\n\t\t\tyl = e.c - e.a * pt.x\n\t\t\tt1 = pt.y - yl\n\t\t\tt2 = pt.x - topsite.x\n\t\t\tt3 = yl - topsite.y\n\t\t\tabove = t1*t1 > t2*t2 + t3*t3\n\n\t\tif(self.pm==Edge.LE):\n\t\t\treturn above\n\t\telse:\n\t\t\treturn not above\n\n\t#--------------------------\n\t# create a new site where the Halfedges el1 and el2 intersect\n\tdef intersect(self,other):\n\t\te1 = self.edge\n\t\te2 = other.edge\n\t\tif (e1 is None) or (e2 is None):\n\t\t\treturn None\n\n\t\t# if the two edges bisect the same parent return None\n\t\tif e1.reg[1] is e2.reg[1]:\n\t\t\treturn None\n\n\t\td = e1.a * e2.b - e1.b * e2.a\n\t\tif isEqual(d,0.0):\n\t\t\treturn None\n\n\t\txint = (e1.c*e2.b - e2.c*e1.b) / d\n\t\tyint = (e2.c*e1.a - e1.c*e2.a) / d\n\t\tif e1.reg[1]< e2.reg[1]:\n\t\t\the = self\n\t\t\te = e1\n\t\telse:\n\t\t\the = other\n\t\t\te = e2\n\n\t\trightOfSite = xint >= e.reg[1].x\n\t\tif((rightOfSite and he.pm == Edge.LE) or\n\t\t\t(not rightOfSite and he.pm == Edge.RE)):\n\t\t\treturn None\n\n\t\t# create a new site at the point of intersection - this is a new\n\t\t# vector event waiting to happen\n\t\treturn Site(xint,yint)\n\n\n\n#------------------------------------------------------------------\nclass EdgeList(object):\n\tdef __init__(self,xmin,xmax,nsites):\n\t\tif xmin > xmax: xmin,xmax = xmax,xmin\n\t\tself.hashsize = int(2*math.sqrt(nsites+4))\n\n\t\tself.xmin\t= xmin\n\t\tself.deltax = float(xmax - xmin)\n\t\tself.hash\t= [None]*self.hashsize\n\n\t\tself.leftend  = Halfedge()\n\t\tself.rightend = Halfedge()\n\t\tself.leftend.right = self.rightend\n\t\tself.rightend.left = self.leftend\n\t\tself.hash[0]  = self.leftend\n\t\tself.hash[-1] = self.rightend\n\n\tdef insert(self,left,he):\n\t\the.left  = left\n\t\the.right = left.right\n\t\tleft.right.left = he\n\t\tleft.right = he\n\n\tdef delete(self,he):\n\t\the.left.right = he.right\n\t\the.right.left = he.left\n\t\the.edge = Edge.DELETED\n\n\t# Get entry from hash table, pruning any deleted nodes\n\tdef gethash(self,b):\n\t\tif(b < 0 or b >= self.hashsize):\n\t\t\treturn None\n\t\the = self.hash[b]\n\t\tif he is None or he.edge is not Edge.DELETED:\n\t\t\treturn he\n\n\t\t#  Hash table points to deleted half edge.  Patch as necessary.\n\t\tself.hash[b] = None\n\t\treturn None\n\n\tdef leftbnd(self,pt):\n\t\t# Use hash table to get close to desired halfedge\n\t\tbucket = int(((pt.x - self.xmin)/self.deltax * self.hashsize))\n\n\t\tif(bucket < 0):\n\t\t\tbucket =0;\n\n\t\tif(bucket >=self.hashsize):\n\t\t\tbucket = self.hashsize-1\n\n\t\the = self.gethash(bucket)\n\t\tif(he is None):\n\t\t\ti = 1\n\t\t\twhile True:\n\t\t\t\the = self.gethash(bucket-i)\n\t\t\t\tif (he is not None): break;\n\t\t\t\the = self.gethash(bucket+i)\n\t\t\t\tif (he is not None): break;\n\t\t\t\ti += 1\n\n\t\t# Now search linear list of halfedges for the corect one\n\t\tif (he is self.leftend) or (he is not self.rightend and he.isPointRightOf(pt)):\n\t\t\the = he.right\n\t\t\twhile he is not self.rightend and he.isPointRightOf(pt):\n\t\t\t\the = he.right\n\t\t\the = he.left;\n\t\telse:\n\t\t\the = he.left\n\t\t\twhile (he is not self.leftend and not he.isPointRightOf(pt)):\n\t\t\t\the = he.left\n\n\t\t# Update hash table and reference counts\n\t\tif(bucket > 0 and bucket < self.hashsize-1):\n\t\t\tself.hash[bucket] = he\n\t\treturn he\n\n\n#------------------------------------------------------------------\nclass PriorityQueue(object):\n\tdef __init__(self,ymin,ymax,nsites):\n\t\tself.ymin = ymin\n\t\tself.deltay = ymax - ymin\n\t\tself.hashsize = int(4 * math.sqrt(nsites))\n\t\tself.count = 0\n\t\tself.minidx = 0\n\t\tself.hash = []\n\t\tfor i in range(self.hashsize):\n\t\t\tself.hash.append(Halfedge())\n\n\tdef __len__(self):\n\t\treturn self.count\n\n\tdef isEmpty(self):\n\t\treturn self.count == 0\n\n\tdef insert(self,he,site,offset):\n\t\the.vertex = site\n\t\the.ystar  = site.y + offset\n\t\tlast = self.hash[self.getBucket(he)]\n\t\tnext = last.qnext\n\t\twhile((next is not None) and he > next):\n\t\t\tlast = next\n\t\t\tnext = last.qnext\n\t\the.qnext = last.qnext\n\t\tlast.qnext = he\n\t\tself.count += 1\n\n\tdef delete(self,he):\n\t\tif (he.vertex is not None):\n\t\t\tlast = self.hash[self.getBucket(he)]\n\t\t\twhile last.qnext is not he:\n\t\t\t\tlast = last.qnext\n\t\t\tlast.qnext = he.qnext\n\t\t\tself.count -= 1\n\t\t\the.vertex = None\n\n\tdef getBucket(self,he):\n\t\tbucket = int(((he.ystar - self.ymin) / self.deltay) * self.hashsize)\n\t\tif bucket < 0: bucket = 0\n\t\tif bucket >= self.hashsize: bucket = self.hashsize-1\n\t\tif bucket < self.minidx:  self.minidx = bucket\n\t\treturn bucket\n\n\tdef getMinPt(self):\n\t\twhile(self.hash[self.minidx].qnext is None):\n\t\t\tself.minidx += 1\n\t\the = self.hash[self.minidx].qnext\n\t\tx = he.vertex.x\n\t\ty = he.ystar\n\t\treturn Site(x,y)\n\n\tdef popMinHalfedge(self):\n\t\tcurr = self.hash[self.minidx].qnext\n\t\tself.hash[self.minidx].qnext = curr.qnext\n\t\tself.count -= 1\n\t\treturn curr\n\n\n#------------------------------------------------------------------\nclass SiteList(object):\n\tdef __init__(self,pointList):\n\t\tself.__sites = []\n\t\tself.__sitenum = 0\n\n\t\tself.__xmin = min([pt.x for pt in pointList])\n\t\tself.__ymin = min([pt.y for pt in pointList])\n\t\tself.__xmax = max([pt.x for pt in pointList])\n\t\tself.__ymax = max([pt.y for pt in pointList])\n\t\tself.__extent=(self.__xmin, self.__xmax, self.__ymin, self.__ymax)\n\n\t\tfor i,pt in enumerate(pointList):\n\t\t\tself.__sites.append(Site(pt.x,pt.y,i))\n\t\tself.__sites.sort()\n\n\tdef setSiteNumber(self,site):\n\t\tsite.sitenum = self.__sitenum\n\t\tself.__sitenum += 1\n\n\tclass Iterator(object):\n\t\tdef __init__(this,lst):  this.generator = (s for s in lst)\n\t\tdef __iter__(this):\t  return this\n\t\tdef next(this):\n\t\t\ttry:\n\t\t\t\tif PY3:\n\t\t\t\t\treturn this.generator.__next__()\n\t\t\t\telse:\n\t\t\t\t\treturn this.generator.next()\n\t\t\texcept StopIteration:\n\t\t\t\treturn None\n\n\tdef iterator(self):\n\t\treturn SiteList.Iterator(self.__sites)\n\n\tdef __iter__(self):\n\t\treturn SiteList.Iterator(self.__sites)\n\n\tdef __len__(self):\n\t\treturn len(self.__sites)\n\n\tdef _getxmin(self): return self.__xmin\n\tdef _getymin(self): return self.__ymin\n\tdef _getxmax(self): return self.__xmax\n\tdef _getymax(self): return self.__ymax\n\tdef _getextent(self): return self.__extent\n\txmin = property(_getxmin)\n\tymin = property(_getymin)\n\txmax = property(_getxmax)\n\tymax = property(_getymax)\n\textent = property(_getextent)\n\n\n#------------------------------------------------------------------\ndef computeVoronoiDiagram(points, xBuff=0, yBuff=0, polygonsOutput=False, formatOutput=False, closePoly=True):\n\t\"\"\"\n\tTakes :\n\t\t- a list of point objects (which must have x and y fields).\n\t\t- x and y buffer values which are the expansion percentages of the bounding box rectangle including all input points.\n\t\tReturns :\n\t\t- With default options : \n\t\t  A list of 2-tuples, representing the two points of each Voronoi diagram edge.\n\t\t  Each point contains 2-tuples which are the x,y coordinates of point.\n\t\t  if formatOutput is True, returns : \n\t\t\t\t- a list of 2-tuples, which are the x,y coordinates of the Voronoi diagram vertices.\n\t\t\t\t- and a list of 2-tuples (v1, v2) representing edges of the Voronoi diagram.\n\t\t\t\t  v1 and v2 are the indices of the vertices at the end of the edge.\n\t\t- If polygonsOutput option is True, returns :\n\t\t  A dictionary of polygons, keys are the indices of the input points,\n\t\t  values contains n-tuples representing the n points of each Voronoi diagram polygon.\n\t\t  Each point contains 2-tuples which are the x,y coordinates of point.\n\t\t  if formatOutput is True, returns : \n\t\t\t\t- A list of 2-tuples, which are the x,y coordinates of the Voronoi diagram vertices.\n\t\t\t\t- and a dictionary of input points indices. Values contains n-tuples representing the n points of each Voronoi diagram polygon.\n\t\t\t\t  Each tuple contains the vertex indices of the polygon vertices.\n\t\t- if closePoly is True then, in the list of points of a polygon, last point will be the same of first point\n\t\"\"\"\n\tsiteList = SiteList(points)\n\tcontext  = Context()\n\tvoronoi(siteList,context)\n\tcontext.setClipBuffer(xBuff, yBuff)\n\tif not polygonsOutput:\n\t\tclipEdges=context.getClipEdges()\n\t\tif formatOutput:\n\t\t\tvertices, edgesIdx = formatEdgesOutput(clipEdges)\n\t\t\treturn vertices, edgesIdx\n\t\telse:\n\t\t\treturn clipEdges\n\telse:\n\t\tclipPolygons=context.getClipPolygons(closePoly)\n\t\tif formatOutput:\n\t\t\tvertices, polyIdx = formatPolygonsOutput(clipPolygons)\n\t\t\treturn vertices, polyIdx\n\t\telse:\n\t\t\treturn clipPolygons\n\ndef formatEdgesOutput(edges):\n\t#get list of points\n\tpts=[]\n\tfor edge in edges:\n\t\tpts.extend(edge)\n\t#get unique values\n\tpts=set(pts)#unique values (tuples are hashable)\n\t#get dict {values:index}\n\tvaluesIdxDict = dict(zip(pts,range(len(pts))))\n\t#get edges index reference\n\tedgesIdx=[]\n\tfor edge in edges:\n\t\tedgesIdx.append([valuesIdxDict[pt] for pt in edge])\n\treturn list(pts), edgesIdx\n\ndef formatPolygonsOutput(polygons):\n\t#get list of points\n\tpts=[]\n\tfor poly in polygons.values():\n\t\tpts.extend(poly)\n\t#get unique values\n\tpts=set(pts)#unique values (tuples are hashable)\n\t#get dict {values:index}\n\tvaluesIdxDict = dict(zip(pts,range(len(pts))))\n\t#get polygons index reference\n\tpolygonsIdx={}\n\tfor inPtsIdx, poly in polygons.items():\n\t\tpolygonsIdx[inPtsIdx]=[valuesIdxDict[pt] for pt in poly]\n\treturn list(pts), polygonsIdx\n\n#------------------------------------------------------------------\ndef computeDelaunayTriangulation(points):\n\t\"\"\" Takes a list of point objects (which must have x and y fields).\n\t\tReturns a list of 3-tuples: the indices of the points that form a\n\t\tDelaunay triangle.\n\t\"\"\"\n\tsiteList = SiteList(points)\n\tcontext  = Context()\n\tcontext.triangulate = True\n\tvoronoi(siteList,context)\n\treturn context.triangles\n\n#-----------------------------------------------------------------------------\n#if __name__==\"__main__\":\n"
  },
  {
    "path": "operators/utils/georaster_utils.py",
    "content": "# -*- coding:utf-8 -*-\n\n# This file is part of BlenderGIS\n\n#  ***** GPL LICENSE BLOCK *****\n#\n#  This program is free software: you can redistribute it and/or modify\n#  it under the terms of the GNU General Public License as published by\n#  the Free Software Foundation, either version 3 of the License, or\n#  (at your option) any later version.\n#\n#  This program is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#  GNU General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public License\n#  along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#  All rights reserved.\n#  ***** GPL LICENSE BLOCK *****\n\nimport os\nimport numpy as np\nimport bpy, bmesh\nimport math\n\nimport logging\nlog = logging.getLogger(__name__)\n\nfrom ...core.georaster import GeoRaster\n\n\ndef _exportAsMesh(georaster, dx=0, dy=0, step=1, buildFaces=True, flat=False, subset=False, reproj=None):\n\t'''Numpy test'''\n\n\tif subset and georaster.subBoxGeo is None:\n\t\tsubset = False\n\n\tif not subset:\n\t\tgeoref = georaster.georef\n\telse:\n\t\tgeoref = georaster.getSubBoxGeoRef()\n\n\tx0, y0 = georef.origin #pxcenter\n\tpxSizeX, pxSizeY = georef.pxSize.x, georef.pxSize.y\n\tw, h = georef.rSize.x, georef.rSize.y\n\n\t#adjust against step\n\tw, h = math.ceil(w/step), math.ceil(h/step)\n\tpxSizeX, pxSizeY = pxSizeX * step, pxSizeY * step\n\n\tx = np.array([(x0 + (pxSizeX * i)) - dx for i in range(0, w)])\n\ty = np.array([(y0 + (pxSizeY * i)) - dy for i in range(0, h)])\n\txx, yy = np.meshgrid(x, y)\n\t#TODO reproj\n\n\tif flat:\n\t\tzz = np.zeros((h, w))\n\telse:\n\t\tzz = georaster.readAsNpArray(subset=subset).data[::step,::step] #TODO raise error if multiband\n\n\tverts = np.column_stack((xx.ravel(), yy.ravel(), zz.ravel()))\n\tif buildFaces:\n\t\tfaces = [(x+y*w, x+y*w+1, x+y*w+1+w, x+y*w+w) for x in range(0, w-1) for y in range(0, h-1)]\n\telse:\n\t\tfaces = []\n\tmesh = bpy.data.meshes.new(\"DEM\")\n\tmesh.from_pydata(verts, [], faces)\n\tmesh.update()\n\treturn mesh\n\n\ndef exportAsMesh(georaster, dx=0, dy=0, step=1, buildFaces=True, subset=False, reproj=None, flat=False):\n\tif subset and georaster.subBoxGeo is None:\n\t\tsubset = False\n\n\tif not subset:\n\t\tgeoref = georaster.georef\n\telse:\n\t\tgeoref = georaster.getSubBoxGeoRef()\n\n\tif not flat:\n\t\timg = georaster.readAsNpArray(subset=subset)\n\t\t#TODO raise error if multiband\n\t\tdata = img.data\n\n\tx0, y0 = georef.origin #pxcenter\n\tpxSizeX, pxSizeY = georef.pxSize.x, georef.pxSize.y\n\tw, h = georef.rSize.x, georef.rSize.y\n\n\t#Build the mesh (Note : avoid using bmesh because it's very slow with large mesh, use from_pydata instead)\n\tverts = []\n\tfaces = []\n\tnodata = []\n\tidxMap = {}\n\tfor py in range(0, h, step):\n\t\tfor px in range(0, w, step):\n\t\t\tx = x0 + (pxSizeX * px)\n\t\t\ty = y0 + (pxSizeY * py)\n\n\t\t\tif reproj is not None:\n\t\t\t\tx, y = reproj.pt(x, y)\n\n\t\t\t#shift\n\t\t\tx -= dx\n\t\t\ty -= dy\n\n\t\t\tif flat:\n\t\t\t\tz = 0\n\t\t\telse:\n\t\t\t\tz = data[py, px]\n\n\t\t\t#vertex index\n\t\t\tv1 = px + py * w #bottom right\n\n\t\t\t#Filter nodata\n\t\t\tif z == georaster.noData:\n\t\t\t\tnodata.append(v1)\n\t\t\telse:\n\t\t\t\tverts.append((x, y, z))\n\t\t\t\tidxMap[v1] = len(verts) - 1\n\n\t\t\t\t#build face from bottomright to topright (using only points already created)\n\t\t\t\tif buildFaces and px > 0 and py > 0: #filter first row and column\n\t\t\t\t\tv2 = v1 - step #bottom left\n\t\t\t\t\tv3 = v2 - w * step #topleft\n\t\t\t\t\tv4 = v3 + step #topright\n\t\t\t\t\tf = [v4, v3, v2, v1] #anticlockwise --> face up\n\t\t\t\t\tif not any(v in f for v in nodata): #TODO too slow ?\n\t\t\t\t\t\tf = [idxMap[v] for v in f]\n\t\t\t\t\t\tfaces.append(f)\n\n\tmesh = bpy.data.meshes.new(\"DEM\")\n\tmesh.from_pydata(verts, [], faces)\n\tmesh.update()\n\n\treturn mesh\n\n\ndef rasterExtentToMesh(name, rast, dx, dy, pxLoc='CORNER', reproj=None, subdivise=False):\n\t'''Build a new mesh that represent a georaster extent'''\n\t#create mesh\n\tbm = bmesh.new()\n\tif pxLoc == 'CORNER':\n\t\tpts = [(pt[0], pt[1]) for pt in rast.corners]#shift coords\n\telif pxLoc == 'CENTER':\n\t\tpts = [(pt[0], pt[1]) for pt in rast.cornersCenter]\n\t#Reprojection\n\tif reproj is not None:\n\t\tpts = reproj.pts(pts)\n\t#build shifted flat 3d vertices\n\tpts = [bm.verts.new((pt[0]-dx, pt[1]-dy, 0)) for pt in pts]#upper left to botton left (clockwise)\n\tpts.reverse()#bottom left to upper left (anticlockwise --> face up)\n\tbm.faces.new(pts)\n\t#Create mesh from bmesh\n\tmesh = bpy.data.meshes.new(name)\n\tbm.to_mesh(mesh)\n\tbm.free()\n\treturn mesh\n\ndef geoRastUVmap(obj, uvLayer, rast, dx, dy, reproj=None):\n\t'''uv map a georaster texture on a given mesh'''\n\tmesh = obj.data\n\t#Assign uv coords\n\tloc = obj.location\n\tfor pg in mesh.polygons:\n\t\tfor i in pg.loop_indices:\n\t\t\tvertIdx = mesh.loops[i].vertex_index\n\t\t\tpt = list(mesh.vertices[vertIdx].co)\n\t\t\t#adjust coords against object location and shift values to retrieve original point coords\n\t\t\tpt = (pt[0] + loc.x + dx, pt[1] + loc.y + dy)\n\t\t\tif reproj is not None:\n\t\t\t\tpt = reproj.pt(*pt)\n\t\t\t#Compute UV coords --> pourcent from image origin (bottom left)\n\t\t\tdx_px, dy_px = rast.pxFromGeo(pt[0], pt[1], reverseY=True, round2Floor=False)\n\t\t\tu = dx_px / rast.size[0]\n\t\t\tv = dy_px / rast.size[1]\n\t\t\t#Assign coords\n\t\t\t#uvLoop = uvLoopLayer.data[i]\n\t\t\t#uvLoop.uv = [u,v]\n\t\t\tuvLayer.data[i].uv = [u,v]\n\ndef setDisplacer(obj, rast, uvTxtLayer, mid=0, interpolation=False):\n\t#Config displacer\n\tdisplacer = obj.modifiers.new('DEM', type='DISPLACE')\n\tdemTex = bpy.data.textures.new('demText', type = 'IMAGE')\n\tdemTex.image = rast.bpyImg\n\tdemTex.use_interpolation = interpolation\n\tdemTex.extension = 'CLIP'\n\tdemTex.use_clamp = False #Needed to get negative displacement with float32 texture\n\tdisplacer.texture = demTex\n\tdisplacer.texture_coords = 'UV'\n\tdisplacer.uv_layer = uvTxtLayer.name\n\tdisplacer.mid_level = mid #Texture values below this value will result in negative displacement\n\t#Setting the displacement strength :\n\t#displacement = (texture value - Midlevel) * Strength\n\t#>> Strength = displacement / texture value (because mid=0)\n\t#If DEM non scaled then\n\t#\t*displacement = alt max - alt min = delta Z\n\t#\t*texture value = delta Z / (2^depth-1)\n\t#\t\t(because in Blender, pixel values are normalized between 0.0 and 1.0)\n\t#>> Strength = delta Z / (delta Z / (2^depth-1))\n\t#>> Strength = 2^depth-1\n\tif rast.depth < 32:\n\t\t#8 or 16 bits unsigned values (signed int16 must be converted to float to be usuable)\n\t\tdisplacer.strength = 2**rast.depth-1\n\telse:\n\t\t#32 bits values\n\t\t#with float raster, blender give directly raw float values(non normalied)\n\t\t#so a texture value of 100 simply give a displacement of 100\n\t\tdisplacer.strength = 1\n\tbpy.ops.object.shade_smooth()\n\treturn displacer\n\n\n#########################################\n\nclass bpyGeoRaster(GeoRaster):\n\n\tdef __init__(self, path, subBoxGeo=None, useGDAL=False, clip=False, fillNodata=False, raw=False):\n\n\t\t#First init parent class\n\t\tGeoRaster.__init__(self, path, subBoxGeo=subBoxGeo, useGDAL=useGDAL)\n\n\t\t#Before open the raster into blender we need to assert that the file can be correctly loaded and exploited\n\t\t#- it must be in a file format supported by Blender (jpeg, tiff, png, bmp, or jpeg2000) and not a GIS specific format\n\t\t#- it must not be coded in int16 because this datatype cannot be correctly handle as displacement texture (issue with negatives values)\n\t\t#- it must not be too large or it will overflow Blender memory\n\t\t#- it must does not contain nodata values because nodata is coded with a large value that will cause huge unwanted displacement\n\t\tif self.format not in ['GTiff', 'TIFF', 'BMP', 'PNG', 'JPEG', 'JPEG2000'] \\\n\t\tor (clip and self.subBoxGeo is not None) \\\n\t\tor fillNodata \\\n\t\tor self.ddtype == 'int16':\n\n\t\t\t#Open the raster as numpy array (read only a subset if we want to clip it)\n\t\t\tif clip:\n\t\t\t\timg = self.readAsNpArray(subset=True)\n\t\t\telse:\n\t\t\t\timg = self.readAsNpArray()\n\n\t\t\t#always cast to float because it's the more convenient datatype for displace texture\n\t\t\t#(will not be normalized from 0.0 to 1.0 in Blender)\n\t\t\timg.cast2float()\n\n\t\t\t#replace nodata with interpolated values\n\t\t\tif fillNodata:\n\t\t\t\timg.fillNodata()\n\n\t\t\t#save to a new tiff file on disk\n\t\t\tfilepath = os.path.splitext(self.path)[0] + '_bgis.tif'\n\t\t\timg.save(filepath)\n\n\t\t\t#reinit the parent class\n\t\t\tGeoRaster.__init__(self, filepath, useGDAL=useGDAL)\n\n\t\tself.raw = raw #flag non color raster like DEM\n\n\t\t#Open the file into Blender\n\t\tself._load()\n\n\n\tdef _load(self, pack=False):\n\t\t'''Load the georaster in Blender'''\n\t\ttry:\n\t\t\tself.bpyImg = bpy.data.images.load(self.path)\n\t\texcept Exception as e:\n\t\t\tlog.error(\"Unable to open raster\", exc_info=True)\n\t\t\traise IOError(\"Unable to open raster\") #it will not print traceback (instead of a bare raise)\n\t\tif pack:\n\t\t\t#WARN : packed image can only be stored as png and this format does not support float32 datatype\n\t\t\tself.bpyImg.pack()\n\t\tif self.raw:\n\t\t\tself.bpyImg.colorspace_settings.is_data = True\n\n\tdef unload(self):\n\t\tself.bpyImg.user_clear()\n\t\tbpy.data.images.remove(self.bpyImg)\n\t\tself.bpyImg = None\n\n\t@property\n\tdef isLoaded(self):\n\t\t'''Flag if the image has been loaded in Blender'''\n\t\tif self.bpyImg is not None:\n\t\t\treturn True\n\t\telse:\n\t\t\treturn False\n\t@property\n\tdef isPacked(self):\n\t\t'''Flag if the image has been packed in Blender'''\n\t\tif self.bpyImg is not None:\n\t\t\tif len(self.bpyImg.packed_files) == 0:\n\t\t\t\treturn False\n\t\t\telse:\n\t\t\t\treturn True\n\t\telse:\n\t\t\treturn False\n\n\t###############################################\n\t# Old methods that use bpy.image.pixels and numpy, keeped here as history\n\t# depreciated because bpy is too slow and we need to process the image before load it in Blender\n\t###############################################\n\n\tdef toBitDepth(self, a):\n\t\t\"\"\"\n\t\tConvert Blender pixel intensity value (from 0.0 to 1.0)\n\t\tin true pixel value in initial image bit depth range\n\t\t\"\"\"\n\t\treturn a * (2**self.depth - 1)\n\n\tdef fromBitDepth(self, a):\n\t\t\"\"\"\n\t\tConvert true pixel value in initial image bit depth range\n\t\tto Blender pixel intensity value (from 0.0 to 1.0)\n\t\t\"\"\"\n\t\treturn a / (2**self.depth - 1)\n\n\tdef getPixelsArray(self, bandIdx=None, subset=False):\n\t\t'''\n\t\tUse bpy to extract pixels values as numpy array\n\t\tIn numpy fist dimension of a 2D matrix represents rows (y) and second dimension represents cols (x)\n\t\tso to get pixel value at a specified location be careful not confusing axes: data[row, column]\n\t\tIt's possible to swap axes if you prefere accessing values with [x,y] indices instead of [y,x]: data.swapaxes(0,1)\n\t\tArray origin is top left\n\t\t'''\n\t\tif not self.isLoaded:\n\t\t\traise IOError(\"Can read only image opened in Blender\")\n\t\tif self.ddtype is None:\n\t\t\traise IOError(\"Undefined data type\")\n\t\tif subset and self.subBoxGeo is None:\n\t\t\treturn None\n\t\tnbBands = self.bpyImg.channels #Blender will return 4 channels even with a one band tiff\n\t\t# Make a first Numpy array in one dimension\n\t\ta = np.array(self.bpyImg.pixels[:])#[r,g,b,a,r,g,b,a,r,g,b,a, ... ] counting from bottom to up and left to right\n\t\t# Regroup rgba values\n\t\ta = a.reshape(len(a)/nbBands, nbBands)#[[r,g,b,a],[r,g,b,a],[r,g,b,a],[r,g,b,a]...]\n\t\t# Build 2 dimensional array (In numpy first dimension represents rows (y) and second dimension represents cols (x))\n\t\ta = a.reshape(self.size.y, self.size.x, nbBands)# [ [[rgba], [rgba]...], [lines2], [lines3]...]\n\t\t# Change origin to top left\n\t\ta = np.flipud(a)\n\t\t# Swap axes to access pixels with [x,y] indices instead of [y,x]\n\t\t##a = a.swapaxes(0,1)\n\t\t# Extract the requested band\n\t\tif bandIdx is not None:\n\t\t\ta = a[:,:,bandIdx]\n\t\t# In blender, non float raster pixels values are normalized from 0.0 to 1.0\n\t\tif not self.isFloat:\n\t\t\t# Multiply by 2**depth - 1 to get raw values\n\t\t\ta = self.toBitDepth(a)\n\t\t\t# Round the result to nearest int and cast to orginal data type\n\t\t\t# when cast signed 16 bits dataset, the negatives values are correctly interpreted by numpy\n\t\t\ta = np.rint(a).astype(self.ddtype)\n\t\t\t# Get the negatives values from signed int16 raster\n\t\t\t# This part is no longer needed because previous numpy's cast already did the job\n\t\t\t'''\n\t\t\tif self.ddtype == 'int16':\n\t\t\t\t#16 bits allows coding values from 0 to 65535 (with 65535 == 2**depth / 2 - 1 )\n\t\t\t\t#positives value are coded from 0 to 32767 (from 0.0 to 0.5 in Blender)\n\t\t\t\t#negatives values are coded in reverse order from 65535 to 32768 (1.0 to 0.5 in Blender)\n\t\t\t\t#corresponding to a range from -1 to -32768\n\t\t\t\ta = np.where(a > 32767, -(65536-a), a)\n\t\t\t'''\n\t\tif not subset:\n\t\t\treturn a\n\t\telse:\n\t\t\t# Get overlay extent (in pixels)\n\t\t\tsubBoxPx = self.subBoxPx\n\t\t\t# Get subset data (min and max pixel number are both include)\n\t\t\ta = a[subBoxPx.ymin:subBoxPx.ymax+1, subBoxPx.xmin:subBoxPx.xmax+1] #topleft to bottomright\n\t\t\treturn a\n\n\n\tdef flattenPixelsArray(self, px):\n\t\t'''\n\t\tFlatten a 3d array of pixels to match the shape of bpy.pixels\n\t\t[ [[rgba], [rgba]...], [lines2], [lines3]...] >> [r,g,b,a,r,g,b,a,r,g,b,a, ... ]\n\t\tIf the submited array contains only one band, then the band will be duplicate\n\t\tand an alpha band will be added to get all rgba values.\n\t\t'''\n\t\tshape = px.shape\n\t\tif len(shape) == 2:\n\t\t\tpx = np.expand_dims(px, axis=2)\n\t\t\tpx = np.repeat(px, 3, axis=2)\n\t\t\talpha = np.ones(shape)\n\t\t\talpha = np.expand_dims(alpha, axis=2)\n\t\t\tpx = np.append(px, alpha, axis=2)\n\t\t#px = px.swapaxes(0,1)\n\t\tpx = np.flipud(px)\n\t\tpx = px.flatten()\n\t\treturn px\n"
  },
  {
    "path": "operators/view3d_mapviewer.py",
    "content": "# -*- coding:utf-8 -*-\n\n#  ***** GPL LICENSE BLOCK *****\n#\n#  This program is free software: you can redistribute it and/or modify\n#  it under the terms of the GNU General Public License as published by\n#  the Free Software Foundation, either version 3 of the License, or\n#  (at your option) any later version.\n#\n#  This program is distributed in the hope that it will be useful,\n#  but WITHOUT ANY WARRANTY; without even the implied warranty of\n#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n#  GNU General Public License for more details.\n#\n#  You should have received a copy of the GNU General Public License\n#  along with this program.  If not, see <http://www.gnu.org/licenses/>.\n#  All rights reserved.\n#  ***** GPL LICENSE BLOCK *****\n\n#built-in imports\nimport math\nimport os\nimport threading\nimport logging\nlog = logging.getLogger(__name__)\n\n#bpy imports\nimport bpy\nfrom mathutils import Vector\nfrom bpy.types import Operator, Panel, AddonPreferences\nfrom bpy.props import StringProperty, IntProperty, FloatProperty, BoolProperty, EnumProperty, FloatVectorProperty\nimport addon_utils\nimport gpu\nfrom gpu_extras.batch import batch_for_shader\n\n#core imports\nfrom ..core import HAS_GDAL, HAS_PIL, HAS_IMGIO\nfrom ..core.proj import reprojPt, reprojBbox, dd2meters, meters2dd\nfrom ..core.basemaps import GRIDS, SOURCES, MapService\n\nfrom ..core import settings\nUSER_AGENT = settings.user_agent\n\n#bgis imports\nfrom ..geoscene import GeoScene, SK, georefManagerLayout\nfrom ..prefs import PredefCRS\n\n#utilities\nfrom .utils import getBBOX, mouseTo3d\nfrom .utils import placeObj, adjust3Dview, showTextures, rasterExtentToMesh, geoRastUVmap, addTexture #for export to mesh tool\n\n#OSM Nominatim API module\n#https://github.com/damianbraun/nominatim\nfrom .lib.osm.nominatim import nominatimQuery\n\nPKG, SUBPKG = __package__.split('.', maxsplit=1) #blendergis.basemaps\n\n####################\n\nclass BaseMap(GeoScene):\n\n\t\"\"\"Handle a map as background image in Blender\"\"\"\n\n\tdef __init__(self, context, srckey, laykey, grdkey=None):\n\n\t\t#Get context\n\t\tself.context = context\n\t\tself.scn = context.scene\n\t\tGeoScene.__init__(self, self.scn)\n\t\tself.area = context.area\n\t\tself.area3d = [r for r in self.area.regions if r.type == 'WINDOW'][0]\n\t\tself.view3d = self.area.spaces.active\n\t\tself.reg3d = self.view3d.region_3d\n\n\t\t#Get cache destination folder in addon preferences\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\tcacheFolder = prefs.cacheFolder\n\n\t\tself.synchOrj = prefs.synchOrj\n\n\t\t#Get resampling algo preference and set the constant\n\t\tMapService.RESAMP_ALG = prefs.resamplAlg\n\n\t\t#Init MapService class\n\t\tself.srv = MapService(srckey, cacheFolder)\n\t\tself.name = srckey + '_' + laykey + '_' + grdkey\n\n\t\t#Set destination tile matrix\n\t\tif grdkey is None:\n\t\t\tgrdkey = self.srv.srcGridKey\n\t\tif grdkey == self.srv.srcGridKey:\n\t\t\tself.tm = self.srv.srcTms\n\t\telse:\n\t\t\t#Define destination grid in map service\n\t\t\tself.srv.setDstGrid(grdkey)\n\t\t\tself.tm = self.srv.dstTms\n\n\t\t#Init some geoscene props if needed\n\t\tif not self.hasCRS:\n\t\t\tself.crs = self.tm.CRS\n\t\tif not self.hasOriginPrj:\n\t\t\tself.setOriginPrj(0, 0, self.synchOrj)\n\t\tif not self.hasScale:\n\t\t\tself.scale = 1\n\t\tif not self.hasZoom:\n\t\t\tself.zoom = 0\n\n\t\tself.lockedZoom = None\n\n\t\t#Set path to tiles mosaic used as background image in Blender\n\t\t#We need a format that support transparency so jpg is exclude\n\t\t#Writing to tif is generally faster than writing to png\n\t\tif bpy.data.is_saved:\n\t\t\tfolder = os.path.dirname(bpy.data.filepath) + os.sep\n\t\t\t##folder = bpy.path.abspath(\"//\"))\n\t\telse:\n\t\t\t##folder = bpy.context.preferences.filepaths.temporary_directory\n\t\t\t#Blender crease a sub-directory within the temp directory, for each session, which is cleared on exit\n\t\t\tfolder = bpy.app.tempdir\n\t\tself.imgPath = folder + self.name + \".tif\"\n\n\t\t#Get layer def obj\n\t\tself.layer = self.srv.layers[laykey]\n\n\t\t#map keys\n\t\tself.srckey = srckey\n\t\tself.laykey = laykey\n\t\tself.grdkey = grdkey\n\n\t\t#Thread attributes\n\t\tself.thread = None\n\t\t#Background image attributes\n\t\tself.img = None #bpy image\n\t\tself.bkg = None #empty image obj\n\t\tself.viewDstZ = None #view 3d z distance\n\t\t#Store previous request\n\t\t#TODO\n\n\n\tdef get(self):\n\t\t'''Launch run() function in a new thread'''\n\t\tself.stop()\n\t\tself.srv.start()\n\t\tself.thread = threading.Thread(target=self.run)\n\t\tself.thread.start()\n\n\tdef stop(self):\n\t\t'''Stop actual thread'''\n\t\tif self.srv.running:\n\t\t\tself.srv.stop()\n\t\t\tself.thread.join()\n\n\tdef run(self):\n\t\t\"\"\"thread method\"\"\"\n\t\tself.mosaic = self.request()\n\t\tif self.srv.running and self.mosaic is not None:\n\t\t\t#save image\n\t\t\tself.mosaic.save(self.imgPath)\n\t\tif self.srv.running:\n\t\t\t#Place background image\n\t\t\tself.place()\n\t\tself.srv.stop()\n\n\tdef moveOrigin(self, dx, dy, useScale=True, updObjLoc=True):\n\t\t'''Move scene origin and update props'''\n\t\tself.moveOriginPrj(dx, dy, useScale, updObjLoc, self.synchOrj) #geoscene function\n\n\tdef request(self):\n\t\t'''Request map service to build a mosaic of required tiles to cover view3d area'''\n\t\t#Get area dimension\n\t\tw, h = self.area.width, self.area.height\n\t\t#w, h = self.area3d.width, self.area3d.height #WARN return [1,1] !!!!????\n\n\t\t#Get area bbox coords in destination tile matrix crs (map origin is bottom lelf)\n\n\t\t#Method 1 : Get bbox coords in scene crs and then reproject the bbox if needed\n\t\tz = self.lockedZoom if self.lockedZoom is not None else self.zoom\n\t\tres = self.tm.getRes(z)\n\t\tif self.crs == 'EPSG:4326':\n\t\t\tres = meters2dd(res)\n\t\tdx, dy, dz = self.reg3d.view_location\n\t\tox = self.crsx + (dx * self.scale)\n\t\toy = self.crsy + (dy * self.scale)\n\t\txmin = ox - w/2 * res * self.scale\n\t\tymax = oy + h/2 * res * self.scale\n\t\txmax = ox + w/2 * res * self.scale\n\t\tymin = oy - h/2 * res * self.scale\n\t\tbbox = (xmin, ymin, xmax, ymax)\n\t\t#reproj bbox to destination grid crs if scene crs is different\n\t\tif self.crs != self.tm.CRS:\n\t\t\tbbox = reprojBbox(self.crs, self.tm.CRS, bbox)\n\n\t\t'''\n\t\t#Method 2\n\t\tbbox = getBBOX.fromTopView(self.context) #ERROR context is None ????\n\t\tbbox = bbox.toGeo(geoscn=self)\n\t\tif self.crs != self.tm.CRS:\n\t\t\tbbox = reprojBbox(self.crs, self.tm.CRS, bbox)\n\t\t'''\n\n\t\tlog.debug('Bounding box request : {}'.format(bbox))\n\n\t\t#Stop thread if the request is same as previous\n\t\t#TODO\n\n\t\tif self.srv.srcGridKey == self.grdkey:\n\t\t\ttoDstGrid = False\n\t\telse:\n\t\t\ttoDstGrid = True\n\n\t\tmosaic = self.srv.getImage(self.laykey, bbox, self.zoom, toDstGrid=toDstGrid, outCRS=self.crs)\n\n\t\treturn mosaic\n\n\n\tdef place(self):\n\t\t'''Set map as background image'''\n\n\t\t#Get or load bpy image\n\t\ttry:\n\t\t\tself.img = [img for img in bpy.data.images if img.filepath == self.imgPath and len(img.packed_files) == 0][0]\n\t\texcept IndexError:\n\t\t\tself.img = bpy.data.images.load(self.imgPath)\n\n\t\t#Get or load background image\n\t\tempties = [obj for obj in self.scn.objects if obj.type == 'EMPTY']\n\t\tbkgs = [obj for obj in empties if obj.empty_display_type == 'IMAGE']\n\t\tfor bkg in bkgs:\n\t\t\tbkg.hide_viewport = True\n\t\ttry:\n\t\t\tself.bkg = [bkg for bkg in bkgs if bkg.data.filepath == self.imgPath and len(bkg.data.packed_files) == 0][0]\n\t\texcept IndexError:\n\t\t\tself.bkg = bpy.data.objects.new(self.name, None) #None will create an empty\n\t\t\tself.bkg.empty_display_type = 'IMAGE'\n\t\t\tself.bkg.empty_image_depth = 'BACK'\n\t\t\tself.bkg.data = self.img\n\t\t\tself.scn.collection.objects.link(self.bkg)\n\t\telse:\n\t\t\tself.bkg.hide_viewport = False\n\n\t\t#Get some image props\n\t\timg_ox, img_oy = self.mosaic.center\n\t\timg_w, img_h = self.mosaic.size\n\t\tres = self.mosaic.pxSize.x\n\t\t#res = self.tm.getRes(self.zoom)\n\n\t\t#Set background size\n\t\tsizex = img_w * res / self.scale\n\t\tsizey = img_h * res / self.scale\n\t\tsize = max([sizex, sizey])\n\t\t#self.bkg.empty_display_size = sizex #limited to 1000\n\t\tself.bkg.empty_display_size = 1 #a size of 1 means image width=1bu\n\t\tself.bkg.scale = (size, size, 1)\n\n\t\t#Set background offset (image origin does not match scene origin)\n\t\tdx = (self.crsx - img_ox) / self.scale\n\t\tdy = (self.crsy - img_oy) / self.scale\n\t\t#self.bkg.empty_image_offset = [-0.5, -0.5] #in image unit space\n\t\tself.bkg.location = (-dx, -dy, 0)\n\t\t#ratio = img_w / img_h\n\t\t#self.bkg.offset_y = -dy * ratio #https://developer.blender.org/T48034\n\n\t\t#Get 3d area's number of pixels and resulting size at the requested zoom level resolution\n\t\t#dst =  max( [self.area3d.width, self.area3d.height] ) #WARN return [1,1] !!!!????\n\t\tdst =  max( [self.area.width, self.area.height] )\n\t\tz = self.lockedZoom if self.lockedZoom is not None else self.zoom\n\t\tres = self.tm.getRes(z)\n\t\tdst = dst * res / self.scale\n\n\t\t#Compute 3dview FOV and needed z distance to see the maximum extent that\n\t\t#can be draw at full res (area 3d needs enough pixels otherwise the image will appears downgraded)\n\t\t#WARN seems these formulas does not works properly in Blender2.8\n\t\tview3D_aperture = 36 #Blender constant (see source code)\n\t\tview3D_zoom = 2 #Blender constant (see source code)\n\t\tfov = 2 * math.atan(view3D_aperture / (self.view3d.lens*2) ) #fov equation\n\t\tfov = math.atan(math.tan(fov/2) * view3D_zoom) * 2 #zoom correction (see source code)\n\t\tzdst = (dst/2) / math.tan(fov/2) #trigo\n\t\tzdst = math.floor(zdst) #make sure no downgrade\n\t\tself.reg3d.view_distance = zdst\n\t\tself.viewDstZ = zdst\n\n\t\t#Update image drawing\n\t\tself.bkg.data.reload()\n\n\n\n\n####################################\ndef drawInfosText(self, context):\n\t#Get contexts\n\tscn = context.scene\n\tarea = context.area\n\tarea3d = [reg for reg in area.regions if reg.type == 'WINDOW'][0]\n\tview3d = area.spaces.active\n\treg3d = view3d.region_3d\n\t#Get map props stored in scene\n\tgeoscn = GeoScene(scn)\n\tzoom = geoscn.zoom\n\tscale = geoscn.scale\n\t#\n\ttxt = \"Map view : \"\n\ttxt += \"Zoom \" + str(zoom)\n\tif self.map.lockedZoom is not None:\n\t\ttxt += \" (Locked)\"\n\ttxt += \" - Scale 1:\" + str(int(scale))\n\t'''\n\t# view3d distance\n\tdst = reg3d.view_distance\n\tif dst > 1000:\n\t\tdst /= 1000\n\t\tunit = 'km'\n\telse:\n\t\tunit = 'm'\n\ttxt += ' 3D View distance ' + str(int(dst)) + ' ' + unit\n\t'''\n\t# cursor crs coords\n\ttxt += ' ' + str((int(self.posx), int(self.posy)))\n\t# progress\n\ttxt += ' ' + self.progress\n\tcontext.area.header_text_set(txt)\n\n\ndef drawZoomBox(self, context):\n\tif self.zoomBoxMode and not self.zoomBoxDrag:\n\t\t# before selection starts draw infinite cross\n\t\tpx, py = self.zb_xmax, self.zb_ymax\n\t\tp1 = (0, py, 0)\n\t\tp2 = (context.area.width, py, 0)\n\t\tp3 = (px, 0, 0)\n\t\tp4 = (px, context.area.height, 0)\n\t\tcoords = [p1, p2, p3, p4]\n\t\tshader = gpu.shader.from_builtin('UNIFORM_COLOR')\n\t\tbatch = batch_for_shader(shader, 'LINES', {\"pos\": coords})\n\t\tshader.bind()\n\t\tshader.uniform_float(\"color\", (0, 0, 0, 1))\n\t\tbatch.draw(shader)\n\n\telif self.zoomBoxMode and self.zoomBoxDrag:\n\t\tp1 = (self.zb_xmin, self.zb_ymin, 0)\n\t\tp2 = (self.zb_xmin, self.zb_ymax, 0)\n\t\tp3 = (self.zb_xmax, self.zb_ymax, 0)\n\t\tp4 = (self.zb_xmax, self.zb_ymin, 0)\n\t\tcoords = [p1, p2, p2, p3, p3, p4, p4, p1]\n\t\tshader = gpu.shader.from_builtin('UNIFORM_COLOR')\n\t\tbatch = batch_for_shader(shader, 'LINES', {\"pos\": coords})\n\t\tshader.bind()\n\t\tshader.uniform_float(\"color\", (0, 0, 0, 1))\n\t\tbatch.draw(shader)\n\n###############\n\nclass VIEW3D_OT_map_start(Operator):\n\n\tbl_idname = \"view3d.map_start\"\n\tbl_description = 'Toggle 2d map navigation'\n\tbl_label = \"Basemap\"\n\tbl_options = {'REGISTER'}\n\n\t#special function to auto redraw an operator popup called through invoke_props_dialog\n\tdef check(self, context):\n\t\treturn True\n\n\tdef listSources(self, context):\n\t\tsrcItems = []\n\t\tfor srckey, src in SOURCES.items():\n\t\t\t#put each item in a tuple (key, label, tooltip)\n\t\t\tsrcItems.append( (srckey, src['name'], src['description']) )\n\t\treturn srcItems\n\n\tdef listGrids(self, context):\n\t\tgrdItems = []\n\t\tsrc = SOURCES[self.src]\n\t\tfor gridkey, grd in GRIDS.items():\n\t\t\t#put each item in a tuple (key, label, tooltip)\n\t\t\tif gridkey == src['grid']:\n\t\t\t\t#insert at first position\n\t\t\t\tgrdItems.insert(0, (gridkey, grd['name']+' (source)', grd['description']) )\n\t\t\telse:\n\t\t\t\tgrdItems.append( (gridkey, grd['name'], grd['description']) )\n\t\treturn grdItems\n\n\tdef listLayers(self, context):\n\t\tlayItems = []\n\t\tsrc = SOURCES[self.src]\n\t\tfor laykey, lay in src['layers'].items():\n\t\t\t#put each item in a tuple (key, label, tooltip)\n\t\t\tlayItems.append( (laykey, lay['name'], lay['description']) )\n\t\treturn layItems\n\n\n\tsrc: EnumProperty(\n\t\t\t\tname = \"Map\",\n\t\t\t\tdescription = \"Choose map service source\",\n\t\t\t\titems = listSources\n\t\t\t\t)\n\n\tgrd: EnumProperty(\n\t\t\t\tname = \"Grid\",\n\t\t\t\tdescription = \"Choose cache tiles matrix\",\n\t\t\t\titems = listGrids\n\t\t\t\t)\n\n\tlay: EnumProperty(\n\t\t\t\tname = \"Layer\",\n\t\t\t\tdescription = \"Choose layer\",\n\t\t\t\titems = listLayers\n\t\t\t\t)\n\n\n\tdialog: StringProperty(default='MAP') # 'MAP', 'SEARCH', 'OPTIONS'\n\n\tquery: StringProperty(name=\"Go to\")\n\n\tzoom: IntProperty(name='Zoom level', min=0, max=25)\n\n\trecenter: BoolProperty(name='Center to existing objects')\n\n\tdef draw(self, context):\n\t\taddonPrefs = context.preferences.addons[PKG].preferences\n\t\tscn = context.scene\n\t\tlayout = self.layout\n\n\t\tif self.dialog == 'SEARCH':\n\t\t\t\tlayout.prop(self, 'query')\n\t\t\t\tlayout.prop(self, 'zoom', slider=True)\n\n\t\telif self.dialog == 'OPTIONS':\n\t\t\t#viewPrefs = context.preferences.view\n\t\t\t#layout.prop(viewPrefs, \"use_zoom_to_mouse\")\n\t\t\tlayout.prop(addonPrefs, \"zoomToMouse\")\n\t\t\tlayout.prop(addonPrefs, \"lockObj\")\n\t\t\tlayout.prop(addonPrefs, \"lockOrigin\")\n\t\t\tlayout.prop(addonPrefs, \"synchOrj\")\n\n\t\telif self.dialog == 'MAP':\n\t\t\tlayout.prop(self, 'src', text='Source')\n\t\t\tlayout.prop(self, 'lay', text='Layer')\n\t\t\tcol = layout.column()\n\t\t\tif not HAS_GDAL:\n\t\t\t\tcol.enabled = False\n\t\t\t\tcol.label(text='(No raster reprojection support)')\n\t\t\tcol.prop(self, 'grd', text='Tile matrix set')\n\n\t\t\t#srcCRS = GRIDS[SOURCES[self.src]['grid']]['CRS']\n\t\t\tgrdCRS = GRIDS[self.grd]['CRS']\n\t\t\trow = layout.row()\n\t\t\t#row.alignment = 'RIGHT'\n\t\t\tdesc = PredefCRS.getName(grdCRS)\n\t\t\tif desc is not None:\n\t\t\t\trow.label(text='CRS: ' + desc)\n\t\t\telse:\n\t\t\t\trow.label(text='CRS: ' + grdCRS)\n\n\t\t\trow = layout.row()\n\t\t\trow.prop(self, 'recenter')\n\n\t\t\tgeoscn = GeoScene(scn)\n\t\t\tif geoscn.isPartiallyGeoref:\n\t\t\t\t#layout.separator()\n\t\t\t\tgeorefManagerLayout(self, context)\n\n\t\t\t#row = layout.row()\n\t\t\t#row.label(text='Map scale:')\n\t\t\t#row.prop(scn, '[\"'+SK.SCALE+'\"]', text='')\n\n\n\tdef invoke(self, context, event):\n\n\t\tif not HAS_PIL and not HAS_GDAL and not HAS_IMGIO:\n\t\t\tself.report({'ERROR'}, \"No imaging library available. ImageIO module was not correctly installed.\")\n\t\t\treturn {'CANCELLED'}\n\n\t\tif not context.area.type == 'VIEW_3D':\n\t\t\tself.report({'WARNING'}, \"View3D not found, cannot run operator\")\n\t\t\treturn {'CANCELLED'}\n\n\t\t#Update zoom\n\t\tgeoscn = GeoScene(context.scene)\n\t\tif geoscn.hasZoom:\n\t\t\tself.zoom = geoscn.zoom\n\n\t\t#Display dialog\n\t\treturn context.window_manager.invoke_props_dialog(self)\n\n\tdef execute(self, context):\n\t\tscn = context.scene\n\t\tgeoscn = GeoScene(scn)\n\t\tprefs = context.preferences.addons[PKG].preferences\n\n\t\t#check cache folder\n\t\tfolder = prefs.cacheFolder\n\t\tif folder == \"\" or not os.path.exists(folder):\n\t\t\tself.report({'ERROR'}, \"Please define a valid cache folder path in addon's preferences\")\n\t\t\treturn {'CANCELLED'}\n\t\tif not os.access(folder, os.X_OK | os.W_OK):\n\t\t\tself.report({'ERROR'}, \"The selected cache folder has no write access\")\n\t\t\treturn {'CANCELLED'}\n\n\t\tif self.dialog == 'MAP':\n\t\t\tgrdCRS = GRIDS[self.grd]['CRS']\n\t\t\tif geoscn.isBroken:\n\t\t\t\tself.report({'ERROR'}, \"Scene georef is broken, please fix it beforehand\")\n\t\t\t\treturn {'CANCELLED'}\n\t\t\t#set scene crs as grid crs\n\t\t\t#if not geoscn.hasCRS:\n\t\t\t\t#geoscn.crs = grdCRS\n\t\t\t#Check if raster reproj is needed\n\t\t\tif geoscn.hasCRS and geoscn.crs != grdCRS and not HAS_GDAL:\n\t\t\t\tself.report({'ERROR'}, \"Please install gdal to enable raster reprojection support\")\n\t\t\t\treturn {'CANCELLED'}\n\n\t\t#Move scene origin to the researched place\n\t\tif self.dialog == 'SEARCH':\n\t\t\tr = bpy.ops.view3d.map_search('EXEC_DEFAULT', query=self.query)\n\t\t\tif r == {'CANCELLED'}:\n\t\t\t\tself.report({'INFO'}, \"No location found\")\n\t\t\telse:\n\t\t\t\tgeoscn.zoom = self.zoom\n\n\n\t\t#Start map viewer operator\n\t\tself.dialog = 'MAP' #reinit dialog type\n\t\tbpy.ops.view3d.map_viewer('INVOKE_DEFAULT', srckey=self.src, laykey=self.lay, grdkey=self.grd, recenter=self.recenter)\n\n\t\treturn {'FINISHED'}\n\n\n\n\n\n###############\n\n\nclass VIEW3D_OT_map_viewer(Operator):\n\n\tbl_idname = \"view3d.map_viewer\"\n\tbl_description = 'Toggle 2d map navigation'\n\tbl_label = \"Map viewer\"\n\tbl_options = {'INTERNAL'}\n\n\tsrckey: StringProperty()\n\n\tgrdkey: StringProperty()\n\n\tlaykey: StringProperty()\n\n\trecenter: BoolProperty()\n\n\t@classmethod\n\tdef poll(cls, context):\n\t\treturn context.area.type == 'VIEW_3D'\n\n\n\tdef __del__(self):\n\t\tif getattr(self, 'restart', False):\n\t\t\tbpy.ops.view3d.map_start('INVOKE_DEFAULT', src=self.srckey, lay=self.laykey, grd=self.grdkey, dialog=self.dialog)\n\n\n\tdef invoke(self, context, event):\n\n\t\tself.restart = False\n\t\tself.dialog = 'MAP' # dialog name for MAP_START >> string in  ['MAP', 'SEARCH', 'OPTIONS']\n\n\t\tself.moveFactor = 0.1\n\n\t\tself.prefs = context.preferences.addons[PKG].preferences\n\t\t#Option to adjust or not objects location when panning\n\t\tself.updObjLoc = self.prefs.lockObj #if georef is locked then we need to adjust object location after each pan\n\n\t\t#Add draw callback to view space\n\t\targs = (self, context)\n\t\tself._drawTextHandler = bpy.types.SpaceView3D.draw_handler_add(drawInfosText, args, 'WINDOW', 'POST_PIXEL')\n\t\tself._drawZoomBoxHandler = bpy.types.SpaceView3D.draw_handler_add(drawZoomBox, args, 'WINDOW', 'POST_PIXEL')\n\n\t\t#Add modal handler and init a timer\n\t\tcontext.window_manager.modal_handler_add(self)\n\t\tself.timer = context.window_manager.event_timer_add(0.04, window=context.window)\n\n\t\t#Switch to top view ortho (center to origin)\n\t\tview3d = context.area.spaces.active\n\t\tbpy.ops.view3d.view_axis(type='TOP')\n\t\tview3d.region_3d.view_perspective = 'ORTHO'\n\t\tcontext.scene.cursor.location = (0, 0, 0)\n\t\tif not self.prefs.lockOrigin:\n\t\t\t#bpy.ops.view3d.view_center_cursor()\n\t\t\tview3d.region_3d.view_location = (0, 0, 0)\n\n\t\t#Init some properties\n\t\t# tag if map is currently drag\n\t\tself.inMove = False\n\t\t# mouse crs coordinates reported in draw callback\n\t\tself.posx, self.posy = 0, 0\n\t\t# thread progress infos reported in draw callback\n\t\tself.progress = ''\n\t\t# Zoom box\n\t\tself.zoomBoxMode = False\n\t\tself.zoomBoxDrag = False\n\t\tself.zb_xmin, self.zb_xmax = 0, 0\n\t\tself.zb_ymin, self.zb_ymax = 0, 0\n\n\t\t#Get map\n\t\tself.map = BaseMap(context, self.srckey, self.laykey, self.grdkey)\n\n\t\tif self.recenter and len(context.scene.objects) > 0:\n\t\t\tscnBbox = getBBOX.fromScn(context.scene).to2D()\n\t\t\tw, h = scnBbox.dimensions\n\t\t\tpx_diag = math.sqrt(context.area.width**2 + context.area.height**2)\n\t\t\tdst_diag = math.sqrt( w**2 + h**2 )\n\t\t\ttargetRes = dst_diag / px_diag\n\t\t\tz = self.map.tm.getNearestZoom(targetRes, rule='lower')\n\t\t\tresFactor = self.map.tm.getFromToResFac(self.map.zoom, z)\n\t\t\tcontext.region_data.view_distance *= resFactor\n\t\t\tx, y = scnBbox.center\n\t\t\tif self.prefs.lockOrigin:\n\t\t\t\tcontext.region_data.view_location = (x, y, 0)\n\t\t\telse:\n\t\t\t\tself.map.moveOrigin(x, y)\n\t\t\tself.map.zoom = z\n\n\t\tself.map.get()\n\n\t\treturn {'RUNNING_MODAL'}\n\n\n\tdef modal(self, context, event):\n\n\t\tcontext.area.tag_redraw()\n\t\tscn = bpy.context.scene\n\n\t\tif event.type == 'TIMER':\n\t\t\t#report thread progression\n\t\t\tself.progress = self.map.srv.report\n\t\t\treturn {'PASS_THROUGH'}\n\n\n\t\tif event.type in ['WHEELUPMOUSE', 'NUMPAD_PLUS']:\n\n\t\t\tif event.value == 'PRESS':\n\n\t\t\t\tif event.alt:\n\t\t\t\t\t# map scale up\n\t\t\t\t\tself.map.scale *= 10\n\t\t\t\t\tself.map.place()\n\t\t\t\t\t#Scale existing objects\n\t\t\t\t\tfor obj in scn.objects:\n\t\t\t\t\t\tobj.location /= 10\n\t\t\t\t\t\tobj.scale /= 10\n\n\t\t\t\telif event.ctrl:\n\t\t\t\t\t# view3d zoom up\n\t\t\t\t\tdst = context.region_data.view_distance\n\t\t\t\t\tcontext.region_data.view_distance -= dst * self.moveFactor\n\t\t\t\t\tif self.prefs.zoomToMouse:\n\t\t\t\t\t\tmouseLoc = mouseTo3d(context, event.mouse_region_x, event.mouse_region_y)\n\t\t\t\t\t\tviewLoc = context.region_data.view_location\n\t\t\t\t\t\tdeltaVect = (mouseLoc - viewLoc) * self.moveFactor\n\t\t\t\t\t\tviewLoc += deltaVect\n\t\t\t\telse:\n\t\t\t\t\t# map zoom up\n\t\t\t\t\tif self.map.zoom < self.map.layer.zmax and self.map.zoom < self.map.tm.nbLevels-1:\n\t\t\t\t\t\tself.map.zoom += 1\n\t\t\t\t\t\tif self.map.lockedZoom is None:\n\t\t\t\t\t\t\tresFactor = self.map.tm.getNextResFac(self.map.zoom)\n\t\t\t\t\t\t\tif not self.prefs.zoomToMouse:\n\t\t\t\t\t\t\t\tcontext.region_data.view_distance *= resFactor\n\t\t\t\t\t\t\telse:\n\t\t\t\t\t\t\t\t#Progressibly zoom to cursor\n\t\t\t\t\t\t\t\tdst = context.region_data.view_distance\n\t\t\t\t\t\t\t\tdst2 = dst * resFactor\n\t\t\t\t\t\t\t\tcontext.region_data.view_distance = dst2\n\t\t\t\t\t\t\t\tmouseLoc = mouseTo3d(context, event.mouse_region_x, event.mouse_region_y)\n\t\t\t\t\t\t\t\tviewLoc = context.region_data.view_location\n\t\t\t\t\t\t\t\tmoveFactor = (dst - dst2) / dst\n\t\t\t\t\t\t\t\tdeltaVect = (mouseLoc - viewLoc) * moveFactor\n\t\t\t\t\t\t\t\tif self.prefs.lockOrigin:\n\t\t\t\t\t\t\t\t\tviewLoc += deltaVect\n\t\t\t\t\t\t\t\telse:\n\t\t\t\t\t\t\t\t\tdx, dy, dz = deltaVect\n\t\t\t\t\t\t\t\t\tif not self.prefs.lockObj and self.map.bkg is not None:\n\t\t\t\t\t\t\t\t\t\tself.map.bkg.location  -= deltaVect\n\t\t\t\t\t\t\t\t\tself.map.moveOrigin(dx, dy, updObjLoc=self.updObjLoc)\n\t\t\t\t\t\tself.map.get()\n\n\n\t\tif event.type in ['WHEELDOWNMOUSE', 'NUMPAD_MINUS']:\n\n\t\t\tif event.value == 'PRESS':\n\n\t\t\t\tif event.alt:\n\t\t\t\t\t#map scale down\n\t\t\t\t\ts = self.map.scale / 10\n\t\t\t\t\tif s < 1: s = 1\n\t\t\t\t\tself.map.scale = s\n\t\t\t\t\tself.map.place()\n\t\t\t\t\t#Scale existing objects\n\t\t\t\t\tfor obj in scn.objects:\n\t\t\t\t\t\tobj.location *= 10\n\t\t\t\t\t\tobj.scale *= 10\n\n\t\t\t\telif event.ctrl:\n\t\t\t\t\t#view3d zoom down\n\t\t\t\t\tdst = context.region_data.view_distance\n\t\t\t\t\tcontext.region_data.view_distance += dst * self.moveFactor\n\t\t\t\t\tif self.prefs.zoomToMouse:\n\t\t\t\t\t\tmouseLoc = mouseTo3d(context, event.mouse_region_x, event.mouse_region_y)\n\t\t\t\t\t\tviewLoc = context.region_data.view_location\n\t\t\t\t\t\tdeltaVect = (mouseLoc - viewLoc) * self.moveFactor\n\t\t\t\t\t\tviewLoc -= deltaVect\n\t\t\t\telse:\n\t\t\t\t\t#map zoom down\n\t\t\t\t\tif self.map.zoom > self.map.layer.zmin and self.map.zoom > 0:\n\t\t\t\t\t\tself.map.zoom -= 1\n\t\t\t\t\t\tif self.map.lockedZoom is None:\n\t\t\t\t\t\t\tresFactor = self.map.tm.getPrevResFac(self.map.zoom)\n\t\t\t\t\t\t\tif not self.prefs.zoomToMouse:\n\t\t\t\t\t\t\t\tcontext.region_data.view_distance *= resFactor\n\t\t\t\t\t\t\telse:\n\t\t\t\t\t\t\t\t#Progressibly zoom to cursor\n\t\t\t\t\t\t\t\tdst = context.region_data.view_distance\n\t\t\t\t\t\t\t\tdst2 = dst * resFactor\n\t\t\t\t\t\t\t\tcontext.region_data.view_distance = dst2\n\t\t\t\t\t\t\t\tmouseLoc = mouseTo3d(context, event.mouse_region_x, event.mouse_region_y)\n\t\t\t\t\t\t\t\tviewLoc = context.region_data.view_location\n\t\t\t\t\t\t\t\tmoveFactor = (dst - dst2) / dst\n\t\t\t\t\t\t\t\tdeltaVect = (mouseLoc - viewLoc) * moveFactor\n\t\t\t\t\t\t\t\tif self.prefs.lockOrigin:\n\t\t\t\t\t\t\t\t\tviewLoc += deltaVect\n\t\t\t\t\t\t\t\telse:\n\t\t\t\t\t\t\t\t\tdx, dy, dz = deltaVect\n\t\t\t\t\t\t\t\t\tif not self.prefs.lockObj and self.map.bkg is not None:\n\t\t\t\t\t\t\t\t\t\tself.map.bkg.location  -= deltaVect\n\t\t\t\t\t\t\t\t\tself.map.moveOrigin(dx, dy, updObjLoc=self.updObjLoc)\n\t\t\t\t\t\tself.map.get()\n\n\n\n\t\tif event.type == 'MOUSEMOVE':\n\n\t\t\t#Report mouse location coords in projeted crs\n\t\t\tloc = mouseTo3d(context, event.mouse_region_x, event.mouse_region_y)\n\t\t\tself.posx, self.posy = self.map.view3dToProj(loc.x, loc.y)\n\n\t\t\tif self.zoomBoxMode:\n\t\t\t\tself.zb_xmax, self.zb_ymax = event.mouse_region_x, event.mouse_region_y\n\n\t\t\t#Drag background image (edit its offset values)\n\t\t\tif self.inMove:\n\t\t\t\tloc1 = mouseTo3d(context, self.x1, self.y1)\n\t\t\t\tloc2 = mouseTo3d(context, event.mouse_region_x, event.mouse_region_y)\n\t\t\t\tdlt = loc1 - loc2\n\t\t\t\tif event.ctrl or self.prefs.lockOrigin:\n\t\t\t\t\tcontext.region_data.view_location = self.viewLoc1 + dlt\n\t\t\t\telse:\n\t\t\t\t\t#Move background image\n\t\t\t\t\tif self.map.bkg is not None:\n\t\t\t\t\t\tself.map.bkg.location[0] = self.offx1 - dlt.x\n\t\t\t\t\t\tself.map.bkg.location[1] = self.offy1 - dlt.y\n\t\t\t\t\t#Move existing objects (only top level parent)\n\t\t\t\t\tif self.updObjLoc:\n\t\t\t\t\t\ttopParents = [obj for obj in scn.objects if not obj.parent]\n\t\t\t\t\t\tfor i, obj in enumerate(topParents):\n\t\t\t\t\t\t\tif obj == self.map.bkg: #the background empty used as basemap\n\t\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t\tloc1 = self.objsLoc1[i]\n\t\t\t\t\t\t\tobj.location.x = loc1.x - dlt.x\n\t\t\t\t\t\t\tobj.location.y = loc1.y - dlt.y\n\n\n\t\tif event.type in {'LEFTMOUSE', 'MIDDLEMOUSE'}:\n\n\t\t\tif event.value == 'PRESS' and not self.zoomBoxMode:\n\t\t\t\t#Get click mouse position and background image offset (if exist)\n\t\t\t\tself.x1, self.y1 = event.mouse_region_x, event.mouse_region_y\n\t\t\t\tself.viewLoc1 = context.region_data.view_location.copy()\n\t\t\t\tif not event.ctrl:\n\t\t\t\t\t#Stop thread now, because we don't know when the mouse click will be released\n\t\t\t\t\tself.map.stop()\n\t\t\t\t\tif not self.prefs.lockOrigin:\n\t\t\t\t\t\tif self.map.bkg is not None:\n\t\t\t\t\t\t\tself.offx1 = self.map.bkg.location[0]\n\t\t\t\t\t\t\tself.offy1 = self.map.bkg.location[1]\n\t\t\t\t\t\t#Store current location of each objects (only top level parent)\n\t\t\t\t\t\tself.objsLoc1 = [obj.location.copy() for obj in scn.objects if not obj.parent]\n\t\t\t\t#Tag that map is currently draging\n\t\t\t\tself.inMove = True\n\n\t\t\tif event.value == 'RELEASE' and not self.zoomBoxMode:\n\t\t\t\tself.inMove = False\n\t\t\t\tif not event.ctrl:\n\t\t\t\t\tif not self.prefs.lockOrigin:\n\t\t\t\t\t\t#Compute final shift\n\t\t\t\t\t\tloc1 = mouseTo3d(context, self.x1, self.y1)\n\t\t\t\t\t\tloc2 = mouseTo3d(context, event.mouse_region_x, event.mouse_region_y)\n\t\t\t\t\t\tdlt = loc1 - loc2\n\t\t\t\t\t\t#Update map (do not update objects location because it was updated while mouse move)\n\t\t\t\t\t\tself.map.moveOrigin(dlt.x, dlt.y, updObjLoc=False)\n\t\t\t\t\tself.map.get()\n\n\n\t\t\tif event.value == 'PRESS' and self.zoomBoxMode:\n\t\t\t\tself.zoomBoxDrag = True\n\t\t\t\tself.zb_xmin, self.zb_ymin = event.mouse_region_x, event.mouse_region_y\n\n\t\t\tif event.value == 'RELEASE' and self.zoomBoxMode:\n\t\t\t\t#Get final zoom box\n\t\t\t\txmax = max(event.mouse_region_x, self.zb_xmin)\n\t\t\t\tymax = max(event.mouse_region_y, self.zb_ymin)\n\t\t\t\txmin = min(event.mouse_region_x, self.zb_xmin)\n\t\t\t\tymin = min(event.mouse_region_y, self.zb_ymin)\n\t\t\t\t#Exit zoom box mode\n\t\t\t\tself.zoomBoxDrag = False\n\t\t\t\tself.zoomBoxMode = False\n\t\t\t\tcontext.window.cursor_set('DEFAULT')\n\t\t\t\t#Compute the move to box origin\n\t\t\t\tw = xmax - xmin\n\t\t\t\th = ymax - ymin\n\t\t\t\tcx = xmin + w/2\n\t\t\t\tcy = ymin + h/2\n\t\t\t\tloc = mouseTo3d(context, cx, cy)\n\t\t\t\t#Compute target resolution\n\t\t\t\tpx_diag = math.sqrt(context.area.width**2 + context.area.height**2)\n\t\t\t\tmapRes = self.map.tm.getRes(self.map.zoom)\n\t\t\t\tdst_diag = math.sqrt( (w*mapRes)**2 + (h*mapRes)**2)\n\t\t\t\ttargetRes = dst_diag / px_diag\n\t\t\t\tz = self.map.tm.getNearestZoom(targetRes, rule='lower')\n\t\t\t\tresFactor = self.map.tm.getFromToResFac(self.map.zoom, z)\n\t\t\t\t#Preview\n\t\t\t\tcontext.region_data.view_distance *= resFactor\n\t\t\t\tif self.prefs.lockOrigin:\n\t\t\t\t\tcontext.region_data.view_location = loc\n\t\t\t\telse:\n\t\t\t\t\tself.map.moveOrigin(loc.x, loc.y, updObjLoc=self.updObjLoc)\n\t\t\t\tself.map.zoom = z\n\t\t\t\tself.map.get()\n\n\n\t\tif event.type in ['LEFT_CTRL', 'RIGHT_CTRL']:\n\n\t\t\tif event.value == 'PRESS':\n\t\t\t\tself._viewDstZ = context.region_data.view_distance\n\t\t\t\tself._viewLoc = context.region_data.view_location.copy()\n\n\t\t\tif event.value == 'RELEASE':\n\t\t\t\t#restore view 3d distance and location\n\t\t\t\tcontext.region_data.view_distance = self._viewDstZ\n\t\t\t\tcontext.region_data.view_location = self._viewLoc\n\n\n\t\t#NUMPAD MOVES (3D VIEW or MAP)\n\t\tif event.value == 'PRESS' and event.type in ['NUMPAD_2', 'NUMPAD_4', 'NUMPAD_6', 'NUMPAD_8']:\n\t\t\tdelta = self.map.bkg.scale.x * self.moveFactor\n\t\t\tif event.type == 'NUMPAD_4':\n\t\t\t\tif event.ctrl or self.prefs.lockOrigin:\n\t\t\t\t\tcontext.region_data.view_location += Vector( (-delta, 0, 0) )\n\t\t\t\telse:\n\t\t\t\t\tself.map.moveOrigin(-delta, 0, updObjLoc=self.updObjLoc)\n\t\t\tif event.type == 'NUMPAD_6':\n\t\t\t\tif event.ctrl or self.prefs.lockOrigin:\n\t\t\t\t\tcontext.region_data.view_location += Vector( (delta, 0, 0) )\n\t\t\t\telse:\n\t\t\t\t\tself.map.moveOrigin(delta, 0, updObjLoc=self.updObjLoc)\n\t\t\tif event.type == 'NUMPAD_2':\n\t\t\t\tif event.ctrl or self.prefs.lockOrigin:\n\t\t\t\t\tcontext.region_data.view_location += Vector( (0, -delta, 0) )\n\t\t\t\telse:\n\t\t\t\t\tself.map.moveOrigin(0, -delta, updObjLoc=self.updObjLoc)\n\t\t\tif event.type == 'NUMPAD_8':\n\t\t\t\tif event.ctrl or self.prefs.lockOrigin:\n\t\t\t\t\tcontext.region_data.view_location += Vector( (0, delta, 0) )\n\t\t\t\telse:\n\t\t\t\t\tself.map.moveOrigin(0, delta, updObjLoc=self.updObjLoc)\n\t\t\tif not event.ctrl:\n\t\t\t\tself.map.get()\n\n\t\t#SWITCH LAYER\n\t\tif event.type == 'SPACE':\n\t\t\tself.map.stop()\n\t\t\tbpy.types.SpaceView3D.draw_handler_remove(self._drawTextHandler, 'WINDOW')\n\t\t\tbpy.types.SpaceView3D.draw_handler_remove(self._drawZoomBoxHandler, 'WINDOW')\n\t\t\tcontext.area.header_text_set(None)\n\t\t\tself.restart = True\n\t\t\treturn {'FINISHED'}\n\n\t\t#GO TO\n\t\tif event.type == 'G':\n\t\t\tself.map.stop()\n\t\t\tbpy.types.SpaceView3D.draw_handler_remove(self._drawTextHandler, 'WINDOW')\n\t\t\tbpy.types.SpaceView3D.draw_handler_remove(self._drawZoomBoxHandler, 'WINDOW')\n\t\t\tcontext.area.header_text_set(None)\n\t\t\tself.restart = True\n\t\t\tself.dialog = 'SEARCH'\n\t\t\treturn {'FINISHED'}\n\n\t\t#OPTIONS\n\t\tif event.type == 'O':\n\t\t\tself.map.stop()\n\t\t\tbpy.types.SpaceView3D.draw_handler_remove(self._drawTextHandler, 'WINDOW')\n\t\t\tbpy.types.SpaceView3D.draw_handler_remove(self._drawZoomBoxHandler, 'WINDOW')\n\t\t\tcontext.area.header_text_set(None)\n\t\t\tself.restart = True\n\t\t\tself.dialog = 'OPTIONS'\n\t\t\treturn {'FINISHED'}\n\n\t\t#Lock/unlock 3d view zoom distance\n\t\tif event.type == 'L' and event.value == 'PRESS':\n\t\t\tif self.map.lockedZoom is None:\n\t\t\t\tself.map.lockedZoom = self.map.zoom\n\t\t\telse:\n\t\t\t\tself.map.lockedZoom = None\n\t\t\t\tself.map.get()\n\n\n\t\t#ZOOM BOX\n\t\tif event.type == 'B' and event.value == 'PRESS':\n\t\t\tself.map.stop()\n\t\t\tself.zoomBoxMode = True\n\t\t\tself.zb_xmax, self.zb_ymax = event.mouse_region_x, event.mouse_region_y\n\t\t\tcontext.window.cursor_set('CROSSHAIR')\n\n\t\t#EXPORT\n\t\tif event.type == 'E' and event.value == 'PRESS':\n\t\t\t#\n\t\t\tif not self.map.srv.running and self.map.mosaic is not None:\n\t\t\t\tself.map.stop()\n\t\t\t\tself.map.bkg.hide_viewport = True\n\n\t\t\t\tbpy.types.SpaceView3D.draw_handler_remove(self._drawTextHandler, 'WINDOW')\n\t\t\t\tbpy.types.SpaceView3D.draw_handler_remove(self._drawZoomBoxHandler, 'WINDOW')\n\t\t\t\tcontext.area.header_text_set(None)\n\n\t\t\t\t#Copy image to new datablock\n\t\t\t\tbpyImg = bpy.data.images.load(self.map.imgPath) #(self.map.img.filepath)\n\t\t\t\tname = 'EXPORT_' + self.map.srckey + '_' + self.map.laykey + '_' + self.map.grdkey\n\t\t\t\tbpyImg.name = name\n\t\t\t\tbpyImg.pack()\n\n\t\t\t\t#Add new attribute to GeoRaster (used by geoRastUVmap function)\n\t\t\t\trast = self.map.mosaic\n\t\t\t\tsetattr(rast, 'bpyImg', bpyImg)\n\n\t\t\t\t#Create Mesh\n\t\t\t\tdx, dy = self.map.getOriginPrj()\n\t\t\t\tmesh = rasterExtentToMesh(name, rast, dx, dy, pxLoc='CORNER')\n\n\t\t\t\t#Create object\n\t\t\t\tobj = placeObj(mesh, name)\n\n\t\t\t\t#UV mapping\n\t\t\t\tuvTxtLayer = mesh.uv_layers.new(name='rastUVmap')# Add UV map texture layer\n\t\t\t\tgeoRastUVmap(obj, uvTxtLayer, rast, dx, dy)\n\n\t\t\t\t#Create material\n\t\t\t\tmat = bpy.data.materials.new('rastMat')\n\t\t\t\tobj.data.materials.append(mat)\n\t\t\t\taddTexture(mat, bpyImg, uvTxtLayer)\n\n\t\t\t\t#Adjust 3d view and display textures\n\t\t\t\tif self.prefs.adjust3Dview:\n\t\t\t\t\tadjust3Dview(context, getBBOX.fromObj(obj))\n\t\t\t\tif self.prefs.forceTexturedSolid:\n\t\t\t\t\tshowTextures(context)\n\n\t\t\t\treturn {'FINISHED'}\n\n\t\t#EXIT\n\t\tif event.type == 'ESC' and event.value == 'PRESS':\n\t\t\tif self.zoomBoxMode:\n\t\t\t\tself.zoomBoxDrag = False\n\t\t\t\tself.zoomBoxMode = False\n\t\t\t\tcontext.window.cursor_set('DEFAULT')\n\t\t\telse:\n\t\t\t\tself.map.stop()\n\t\t\t\tbpy.types.SpaceView3D.draw_handler_remove(self._drawTextHandler, 'WINDOW')\n\t\t\t\tbpy.types.SpaceView3D.draw_handler_remove(self._drawZoomBoxHandler, 'WINDOW')\n\t\t\t\tcontext.area.header_text_set(None)\n\t\t\t\treturn {'CANCELLED'}\n\n\n\n\t\treturn {'RUNNING_MODAL'}\n\n\n\n####################################\n\nclass VIEW3D_OT_map_search(bpy.types.Operator):\n\n\tbl_idname = \"view3d.map_search\"\n\tbl_description = 'Search for a place and move scene origin to it'\n\tbl_label = \"Map search\"\n\tbl_options = {'INTERNAL'}\n\n\tquery: StringProperty(name=\"Go to\")\n\n\tdef invoke(self, context, event):\n\t\tgeoscn = GeoScene(context.scene)\n\t\tif geoscn.isBroken:\n\t\t\tself.report({'ERROR'}, \"Scene georef is broken\")\n\t\t\treturn {'CANCELLED'}\n\t\treturn context.window_manager.invoke_props_dialog(self)\n\n\tdef execute(self, context):\n\t\tgeoscn = GeoScene(context.scene)\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\ttry:\n\t\t\tresults = nominatimQuery(self.query, referer='bgis', user_agent=USER_AGENT)\n\t\texcept Exception as e:\n\t\t\tlog.error('Failed Nominatim query', exc_info=True)\n\t\t\treturn {'CANCELLED'}\n\t\tif len(results) == 0:\n\t\t\treturn {'CANCELLED'}\n\t\telse:\n\t\t\tlog.debug('Nominatim search results : {}'.format([r['display_name'] for r in results]))\n\t\t\tresult = results[0]\n\t\t\tlat, lon = float(result['lat']), float(result['lon'])\n\t\t\tif geoscn.isGeoref:\n\t\t\t\tgeoscn.updOriginGeo(lon, lat, updObjLoc=prefs.lockObj)\n\t\t\telse:\n\t\t\t\tgeoscn.setOriginGeo(lon, lat)\n\t\treturn {'FINISHED'}\n\n\n\nclasses = [\n\tVIEW3D_OT_map_start,\n\tVIEW3D_OT_map_viewer,\n\tVIEW3D_OT_map_search\n]\n\ndef register():\n\tfor cls in classes:\n\t\ttry:\n\t\t\tbpy.utils.register_class(cls)\n\t\texcept ValueError as e:\n\t\t\t#log.error('Cannot register {}'.format(cls), exc_info=True)\n\t\t\tlog.warning('{} is already registered, now unregister and retry... '.format(cls))\n\t\t\tbpy.utils.unregister_class(cls)\n\t\t\tbpy.utils.register_class(cls)\n\ndef unregister():\n\tfor cls in classes:\n\t\tbpy.utils.unregister_class(cls)\n"
  },
  {
    "path": "prefs.py",
    "content": "import json\nimport logging\nlog = logging.getLogger(__name__)\nimport sys, os\n\nimport bpy\nfrom bpy.props import StringProperty, IntProperty, FloatProperty, BoolProperty, EnumProperty, FloatVectorProperty\nfrom bpy.types import Operator, Panel, AddonPreferences\nimport addon_utils\n\nfrom . import bl_info\nfrom .core.proj.reproj import MapTilerCoordinates\nfrom .core.proj.srs import SRS\nfrom .core.checkdeps import HAS_GDAL, HAS_PYPROJ, HAS_PIL, HAS_IMGIO\nfrom .core import settings\n\nPKG = __package__\n\ndef getAppData():\n\thome = os.path.expanduser('~')\n\tloc = os.path.join(home, '.bgis')\n\tif not os.path.exists(loc):\n\t\tos.mkdir(loc)\n\treturn loc\n\nAPP_DATA = getAppData()\n\n'''\nDefault Enum properties contents (list of tuple (value, label, tootip))\nTheses properties are automatically filled from a serialized json string stored in a StringProperty\nThis is workaround to have an editable EnumProperty (ie the user can add, remove or edit an entry)\nbecause the Blender Python API does not provides built in functions for these tasks.\nTo edit the content of these enum, we just need to write new operators which will simply update the json string\nAs the json backend is stored in addon preferences, the property will be saved and restored for the next blender session\n'''\n\n\nDEFAULT_CRS = [\n\t('EPSG:3857', 'Web Mercator', 'Worldwide projection, high distortions, not suitable for precision modelling'),\n\t('EPSG:4326', 'WGS84 latlon', 'Longitude and latitude in degrees, DO NOT USE AS SCENE CRS (this system is defined only for reprojection tasks')\n]\n\n\nDEFAULT_DEM_SERVER = [\n\t(\"https://portal.opentopography.org/API/globaldem?demtype=SRTMGL1&west={W}&east={E}&south={S}&north={N}&outputFormat=GTiff&API_Key={API_KEY}\", 'OpenTopography SRTM 30m', 'OpenTopography.org web service for SRTM 30m global DEM'),\n\t(\"https://portal.opentopography.org/API/globaldem?demtype=SRTMGL3&west={W}&east={E}&south={S}&north={N}&outputFormat=GTiff&API_Key={API_KEY}\", 'OpenTopography SRTM 90m', 'OpenTopography.org web service for SRTM 90m global DEM'),\n\t(\"http://www.gmrt.org/services/GridServer?west={W}&east={E}&south={S}&north={N}&layer=topo&format=geotiff&resolution=high\", 'Marine-geo.org GMRT', 'Marine-geo.org web service for GMRT global DEM (terrestrial (ASTER) and bathymetry)')\n]\n\nDEFAULT_OVERPASS_SERVER =  [\n\t(\"https://lz4.overpass-api.de/api/interpreter\", 'overpass-api.de', 'Main Overpass API instance'),\n\t(\"http://overpass.openstreetmap.fr/api/interpreter\", 'overpass.openstreetmap.fr', 'French Overpass API instance'),\n\t(\"https://overpass.kumi.systems/api/interpreter\", 'overpass.kumi.systems', 'Kumi Systems Overpass Instance')\n]\n\n#default filter tags for OSM import\nDEFAULT_OSM_TAGS = [\n\t'building',\n\t'highway',\n\t'landuse',\n\t'leisure',\n\t'natural',\n\t'railway',\n\t'waterway'\n]\n\n\n\nclass BGIS_OT_pref_show(Operator):\n\n\tbl_idname = \"bgis.pref_show\"\n\tbl_description = 'Display BlenderGIS addons preferences'\n\tbl_label = \"Preferences\"\n\tbl_options = {'INTERNAL'}\n\n\tdef execute(self, context):\n\t\taddon_utils.modules_refresh()\n\t\tcontext.preferences.active_section = 'ADDONS'\n\t\tbpy.data.window_managers[\"WinMan\"].addon_search = bl_info['name']\n\t\t#bpy.ops.wm.addon_expand(module=PKG)\n\t\tmod = addon_utils.addons_fake_modules.get(PKG)\n\t\tmod.bl_info['show_expanded'] = True\n\t\tbpy.ops.screen.userpref_show('INVOKE_DEFAULT')\n\t\treturn {'FINISHED'}\n\n\n\nclass BGIS_PREFS(AddonPreferences):\n\n\tbl_idname = PKG\n\n\t################\n\t#Predefined Spatial Ref. Systems\n\n\tdef listPredefCRS(self, context):\n\t\treturn [tuple(elem) for elem in json.loads(self.predefCrsJson)]\n\n\t#store crs preset as json string into addon preferences\n\tpredefCrsJson: StringProperty(default=json.dumps(DEFAULT_CRS))\n\n\tpredefCrs: EnumProperty(\n\t\tname = \"Predefinate CRS\",\n\t\tdescription = \"Choose predefinite Coordinate Reference System\",\n\t\t#default = 1, #possible only since Blender 2.90\n\t\titems = listPredefCRS\n\t\t)\n\n\t################\n\t#proj engine\n\n\tdef getProjEngineItems(self, context):\n\t\titems = [ ('AUTO', 'Auto detect', 'Auto select the best library for reprojection tasks') ]\n\t\tif HAS_GDAL:\n\t\t\titems.append( ('GDAL', 'GDAL', 'Force GDAL as reprojection engine') )\n\t\tif HAS_PYPROJ:\n\t\t\titems.append( ('PYPROJ', 'pyProj', 'Force pyProj as reprojection engine') )\n\t\t#if EPSGIO.ping(): #too slow\n\t\t#\titems.append( ('EPSGIO', 'epsg.io', '') )\n\t\titems.append( ('EPSGIO', 'epsg.io / MapTilerCoords', 'Force epsg.io as reprojection engine') )\n\t\titems.append( ('BUILTIN', 'Built in', 'Force reprojection through built in Python functions') )\n\t\treturn items\n\n\tdef updateProjEngine(self, context):\n\t\tsettings.proj_engine = self.projEngine\n\n\tprojEngine: EnumProperty(\n\t\tname = \"Projection engine\",\n\t\tdescription = \"Select projection engine\",\n\t\titems = getProjEngineItems,\n\t\tupdate = updateProjEngine\n\t\t)\n\n\t################\n\t#img engine\n\n\tdef getImgEngineItems(self, context):\n\t\titems = [ ('AUTO', 'Auto detect', 'Auto select the best imaging library') ]\n\t\tif HAS_GDAL:\n\t\t\titems.append( ('GDAL', 'GDAL', 'Force GDAL as image processing engine') )\n\t\tif HAS_IMGIO:\n\t\t\titems.append( ('IMGIO', 'ImageIO', 'Force ImageIO as image processing  engine') )\n\t\tif HAS_PIL:\n\t\t\titems.append( ('PIL', 'PIL', 'Force PIL as image processing  engine') )\n\t\treturn items\n\n\tdef updateImgEngine(self, context):\n\t\tsettings.img_engine = self.imgEngine\n\n\timgEngine: EnumProperty(\n\t\tname = \"Image processing engine\",\n\t\tdescription = \"Select image processing engine\",\n\t\titems = getImgEngineItems,\n\t\tupdate = updateImgEngine\n\t\t)\n\n\t################\n\t#OSM\n\n\tosmTagsJson: StringProperty(default=json.dumps(DEFAULT_OSM_TAGS)) #just a serialized list of tags\n\n\tdef listOsmTags(self, context):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\ttags = json.loads(prefs.osmTagsJson)\n\t\t#put each item in a tuple (key, label, tooltip)\n\t\treturn [ (tag, tag, tag) for tag in tags]\n\n\tosmTags: EnumProperty(\n\t\tname = \"OSM tags\",\n\t\tdescription = \"List of registered OSM tags\",\n\t\titems = listOsmTags\n\t\t)\n\n\t################\n\t#Basemaps\n\n\tdef getCacheFolder5x(self, v, isSet):\n\t\treturn bpy.path.abspath(v)\n\n\tdef getCacheFolder(self):\n\t\treturn bpy.path.abspath(self.get(\"cacheFolder\", ''))\n\n\tdef setCacheFolder5x(self, newVal, currentVal, isSet):\n\t\tif os.access(newVal, os.X_OK | os.W_OK):\n\t\t\treturn newVal\n\t\telse:\n\t\t\tlog.error(\"The selected cache folder has no write access\")\n\n\tdef setCacheFolder(self, value):\n\t\tif os.access(value, os.X_OK | os.W_OK):\n\t\t\tself[\"cacheFolder\"] = value\n\t\telse:\n\t\t\tself[\"cacheFolder\"] = \"The selected folder has no write access\"\n\n\tif bpy.app.version[0] >= 5 :\n\t\tcacheFolder: StringProperty(\n\t\t\tname = \"Cache folder\",\n\t\t\tdefault = APP_DATA, #Does not works !?\n\t\t\tdescription = \"Define a folder where to store Geopackage SQlite db\",\n\t\t\tsubtype = 'DIR_PATH',\n\t\t\tget_transform = getCacheFolder5x,\n\t\t\tset_transform = setCacheFolder5x\n\t\t\t)\n\telse:\n\t\tcacheFolder: StringProperty(\n\t\t\tname = \"Cache folder\",\n\t\t\tdefault = APP_DATA, #Does not works !?\n\t\t\tdescription = \"Define a folder where to store Geopackage SQlite db\",\n\t\t\tsubtype = 'DIR_PATH',\n\t\t\tget = getCacheFolder,\n\t\t\tset = setCacheFolder\n\t\t\t)\n\n\n\tsynchOrj: BoolProperty(\n\t\tname=\"Synch. lat/long\",\n\t\tdescription='Keep geo origin synchronized with crs origin. Can be slow with remote reprojection services',\n\t\tdefault=True)\n\n\tzoomToMouse: BoolProperty(name=\"Zoom to mouse\", description='Zoom towards the mouse pointer position', default=True)\n\n\tlockOrigin: BoolProperty(name=\"Lock origin\", description='Do not move scene origin when panning map', default=False)\n\tlockObj: BoolProperty(name=\"Lock objects\", description='Retain objects geolocation when moving map origin', default=True)\n\n\tresamplAlg: EnumProperty(\n\t\tname = \"Resampling method\",\n\t\tdescription = \"Choose GDAL's resampling method used for reprojection\",\n\t\titems = [ ('NN', 'Nearest Neighboor', ''), ('BL', 'Bilinear', ''), ('CB', 'Cubic', ''), ('CBS', 'Cubic Spline', ''), ('LCZ', 'Lanczos', '') ]\n\t\t)\n\n\t################\n\t#Network\n\n\tdef listOverpassServer(self, context):\n\t\treturn [tuple(entry) for entry in json.loads(self.overpassServerJson)]\n\n\t#store crs preset as json string into addon preferences\n\toverpassServerJson: StringProperty(default=json.dumps(DEFAULT_OVERPASS_SERVER))\n\n\toverpassServer: EnumProperty(\n\t\tname = \"Overpass server\",\n\t\tdescription = \"Select an overpass server\",\n\t\t#default = 0,\n\t\titems = listOverpassServer\n\t\t)\n\n\tdef listDemServer(self, context):\n\t\treturn [tuple(entry) for entry in json.loads(self.demServerJson)]\n\n\t#store crs preset as json string into addon preferences\n\tdemServerJson: StringProperty(default=json.dumps(DEFAULT_DEM_SERVER))\n\n\tdemServer: EnumProperty(\n\t\tname = \"Elevation server\",\n\t\tdescription = \"Select a server that provides Digital Elevation Model datasource\",\n\t\t#default = 0,\n\t\titems = listDemServer\n\t\t)\n\n\topentopography_api_key: StringProperty(\n\t\tname = \"\",\n\t\tdescription=\"you need to register and request a key from opentopography website\"\n\t)\n\n\tdef updateMapTilerApiKey(self, context):\n\t\tsettings.maptiler_api_key = self.maptiler_api_key\n\n\tmaptiler_api_key: StringProperty(\n\t\tname = \"\",\n\t\tdescription = \"API key for MapTiler Coordinates API (required for EPSG.io migration)\",\n\t\tupdate = updateMapTilerApiKey\n\t)\n\n\t################\n\t#IO options\n\tmergeDoubles: BoolProperty(\n\t\tname = \"Merge duplicate vertices\",\n\t\tdescription = 'Merge shared vertices between features when importing vector data',\n\t\tdefault = False)\n\tadjust3Dview: BoolProperty(\n\t\tname = \"Adjust 3D view\",\n\t\tdescription = \"Update 3d view grid size and clip distances according to the new imported object's size\",\n\t\tdefault = True)\n\tforceTexturedSolid: BoolProperty(\n\t\tname = \"Force textured solid shading\",\n\t\tdescription = \"Update shading mode to display raster's texture\",\n\t\tdefault = True)\n\n\t################\n\t#System\n\tdef updateLogLevel(self, context):\n\t\tlogger = logging.getLogger(PKG)\n\t\tlogger.setLevel(logging.getLevelName(self.logLevel))\n\n\tlogLevel: EnumProperty(\n\t\tname = \"Logging level\",\n\t\tdescription = \"Select the logging level\",\n\t\titems = [('DEBUG', 'Debug', ''), ('INFO', 'Info', ''), ('WARNING', 'Warning', ''), ('ERROR', 'Error', ''), ('CRITICAL', 'Critical', '')],\n\t\tupdate = updateLogLevel,\n\t\tdefault = 'DEBUG'\n\t\t)\n\n\t################\n\tdef draw(self, context):\n\t\tlayout = self.layout\n\n\t\t#SRS\n\t\tbox = layout.box()\n\t\tbox.label(text='Spatial Reference Systems')\n\t\trow = box.row().split(factor=0.5)\n\t\trow.prop(self, \"predefCrs\", text='')\n\t\trow.operator(\"bgis.add_predef_crs\", icon='ADD')\n\t\trow.operator(\"bgis.edit_predef_crs\", icon='PREFERENCES')\n\t\trow.operator(\"bgis.rmv_predef_crs\", icon='REMOVE')\n\t\trow.operator(\"bgis.reset_predef_crs\", icon='PLAY_REVERSE')\n\n\t\t#Basemaps\n\t\tbox = layout.box()\n\t\tbox.label(text='Basemaps')\n\t\tbox.prop(self, \"cacheFolder\")\n\t\trow = box.row()\n\t\trow.prop(self, \"zoomToMouse\")\n\t\trow.prop(self, \"lockObj\")\n\t\trow.prop(self, \"lockOrigin\")\n\t\trow.prop(self, \"synchOrj\")\n\t\trow = box.row()\n\t\trow.prop(self, \"resamplAlg\")\n\n\t\t#IO\n\t\tbox = layout.box()\n\t\tbox.label(text='Import/Export')\n\t\trow = box.row().split(factor=0.5)\n\t\tsplit = row.split(factor=0.9, align=True)\n\t\tsplit.prop(self, \"osmTags\")\n\t\tsplit.operator(\"wm.url_open\", icon='INFO').url = \"http://wiki.openstreetmap.org/wiki/Map_Features\"\n\t\trow.operator(\"bgis.add_osm_tag\", icon='ADD')\n\t\trow.operator(\"bgis.edit_osm_tag\", icon='PREFERENCES')\n\t\trow.operator(\"bgis.rmv_osm_tag\", icon='REMOVE')\n\t\trow.operator(\"bgis.reset_osm_tags\", icon='PLAY_REVERSE')\n\t\trow = box.row()\n\t\trow.prop(self, \"mergeDoubles\")\n\t\trow.prop(self, \"adjust3Dview\")\n\t\trow.prop(self, \"forceTexturedSolid\")\n\n\t\t#Network\n\t\tbox = layout.box()\n\t\tbox.label(text='Remote datasource')\n\t\trow = box.row().split(factor=0.5)\n\t\trow.prop(self, \"overpassServer\")\n\t\trow.operator(\"bgis.add_overpass_server\", icon='ADD')\n\t\trow.operator(\"bgis.edit_overpass_server\", icon='PREFERENCES')\n\t\trow.operator(\"bgis.rmv_overpass_server\", icon='REMOVE')\n\t\trow.operator(\"bgis.reset_overpass_server\", icon='PLAY_REVERSE')\n\t\trow = box.row().split(factor=0.5)\n\t\trow.prop(self, \"demServer\")\n\t\trow.operator(\"bgis.add_dem_server\", icon='ADD')\n\t\trow.operator(\"bgis.edit_dem_server\", icon='PREFERENCES')\n\t\trow.operator(\"bgis.rmv_dem_server\", icon='REMOVE')\n\t\trow.operator(\"bgis.reset_dem_server\", icon='PLAY_REVERSE')\n\n\t\trow = box.row().split(factor=0.2)\n\t\trow.label(text=\"Opentopography Api Key\")\n\t\trow.prop(self, \"opentopography_api_key\")\n\n\t\trow = box.row().split(factor=0.2)\n\t\trow.label(text=\"MapTiler API Key\")\n\t\trow.prop(self, \"maptiler_api_key\")\n\n\t\t#System\n\t\tbox = layout.box()\n\t\tbox.label(text='System')\n\t\tbox.prop(self, \"projEngine\")\n\t\tbox.prop(self, \"imgEngine\")\n\t\tbox.prop(self, \"logLevel\")\n\n#######################\n\nclass PredefCRS():\n\n\t'''\n\tCollection of utility methods (callable at class level) to deal with predefined CRS dictionary\n\tCan be used by others operators that need to fill their own crs enum\n\t'''\n\n\t@staticmethod\n\tdef getData():\n\t\t'''Load the json string'''\n\t\tprefs = bpy.context.preferences.addons[PKG].preferences\n\t\treturn json.loads(prefs.predefCrsJson)\n\n\t@classmethod\n\tdef getName(cls, key):\n\t\t'''Return the convenient name of a given srid or None if this crs does not exist in the list'''\n\t\tdata = cls.getData()\n\t\ttry:\n\t\t\treturn [entry[1] for entry in data if entry[0] == key][0]\n\t\texcept IndexError:\n\t\t\treturn None\n\n\t@classmethod\n\tdef getEnumItems(cls):\n\t\t'''Return a list of predefined crs usable to fill a bpy EnumProperty'''\n\t\treturn [tuple(entry) for entry in cls.getData()]\n\n\n#################\n# Collection of operators to manage predefined CRS\n\nclass BGIS_OT_add_predef_crs(Operator):\n\tbl_idname = \"bgis.add_predef_crs\"\n\tbl_description = 'Add predefinate CRS'\n\tbl_label = \"Add\"\n\tbl_options = {'INTERNAL'}\n\n\tcrs: StringProperty(name = \"Definition\",  description = \"Specify EPSG code or Proj4 string definition for this CRS\")\n\tname: StringProperty(name = \"Description\", description = \"Choose a convenient name for this CRS\")\n\tdesc: StringProperty(name = \"Description\", description = \"Add a description or comment about this CRS\")\n\n\tdef check(self, context):\n\t\treturn True\n\n\tdef search(self, context):\n\n\t\tapiKey = settings.maptiler_api_key\n\n\t\tif not apiKey:\n\t\t\t#self.report({'ERROR'}, \"MapTiler API key is required. Please set it in the preferences.\") #report is not available outsite of the execute function\n\t\t\tlog.error(\"No Maptiler API key\")\n\t\t\treturn\n\n\t\tmtc = MapTilerCoordinates(apiKey=apiKey)\n\t\tresults = mtc.search(self.query)\n\t\tself.results = json.dumps(results)\n\t\tif results:\n\t\t\tself.crs = 'EPSG:' + str(results[0]['id']['code'])\n\t\t\tself.name = results[0]['name']\n\n\tdef updEnum(self, context):\n\t\tcrsItems = []\n\t\tif self.results != '':\n\t\t\tfor result in json.loads(self.results):\n\t\t\t\tsrid = 'EPSG:' + str(result['id']['code'])\n\t\t\t\tcrsItems.append( (str(result['id']['code']), result['name'], srid) )\n\t\treturn crsItems\n\n\tdef fill(self, context):\n\t\tif self.results != '':\n\t\t\tcrs = [crs for crs in json.loads(self.results) if str(crs['id']['code']) == self.crsEnum][0]\n\t\t\tself.crs = 'EPSG:' + str(crs['id']['code'])\n\t\t\tself.desc = crs['name']\n\n\tquery: StringProperty(name='Query', description='Hit enter to process the search', update=search)\n\n\tresults: StringProperty()\n\n\tcrsEnum: EnumProperty(name='Results', description='Select the desired CRS', items=updEnum, update=fill)\n\n\tsearch: BoolProperty(name='Search', description='Search for coordinate system into EPSG database', default=False)\n\n\tsave: BoolProperty(name='Save to addon preferences',  description='Save Blender user settings after the addition', default=False)\n\n\tdef invoke(self, context, event):\n\t\treturn context.window_manager.invoke_props_dialog(self)#, width=300)\n\n\tdef draw(self, context):\n\t\tlayout = self.layout\n\t\tlayout.prop(self, 'search')\n\t\tif self.search:\n\t\t\tprefs = context.preferences.addons[PKG].preferences\n\t\t\tif not prefs.maptiler_api_key:\n\t\t\t\tlayout.label(text=\"Searching require a MapTiler API key\", icon_value=3)\n\t\t\t\tlayout.prop(prefs, \"maptiler_api_key\", text='API Key')\n\t\t\telse:\n\t\t\t\tlayout.prop(self, 'query')\n\t\t\t\tlayout.prop(self, 'crsEnum')\n\t\t\tlayout.separator()\n\t\tlayout.prop(self, 'crs')\n\t\tlayout.prop(self, 'name')\n\t\tlayout.prop(self, 'desc')\n\t\t#layout.prop(self, 'save') #sincce Blender2.8 prefs are autosaved\n\n\tdef execute(self, context):\n\t\tif not SRS.validate(self.crs):\n\t\t\tself.report({'ERROR'}, 'Invalid CRS')\n\t\tif self.crs.isdigit():\n\t\t\tself.crs = 'EPSG:' + self.crs\n\t\t#append the new crs def to json string\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\tdata = json.loads(prefs.predefCrsJson)\n\t\tdata.append((self.crs, self.name, self.desc))\n\t\tprefs.predefCrsJson = json.dumps(data)\n\t\t#change enum index to new added crs and redraw\n\t\t#prefs.predefCrs = self.crs\n\t\tcontext.area.tag_redraw()\n\t\t#end\n\t\tif self.save:\n\t\t\tbpy.ops.wm.save_userpref()\n\t\treturn {'FINISHED'}\n\nclass BGIS_OT_rmv_predef_crs(Operator):\n\n\tbl_idname = \"bgis.rmv_predef_crs\"\n\tbl_description = 'Remove predefinate CRS'\n\tbl_label = \"Remove\"\n\tbl_options = {'INTERNAL'}\n\n\tdef execute(self, context):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\tkey = prefs.predefCrs\n\t\tif key != '':\n\t\t\tdata = json.loads(prefs.predefCrsJson)\n\t\t\tdata = [e for e in data if e[0] != key]\n\t\t\tprefs.predefCrsJson = json.dumps(data)\n\t\tcontext.area.tag_redraw()\n\t\treturn {'FINISHED'}\n\nclass BGIS_OT_reset_predef_crs(Operator):\n\n\tbl_idname = \"bgis.reset_predef_crs\"\n\tbl_description = 'Reset predefinate CRS'\n\tbl_label = \"Reset\"\n\tbl_options = {'INTERNAL'}\n\n\tdef execute(self, context):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\tprefs.predefCrsJson = json.dumps(DEFAULT_CRS)\n\t\tcontext.area.tag_redraw()\n\t\treturn {'FINISHED'}\n\nclass BGIS_OT_edit_predef_crs(Operator):\n\n\tbl_idname = \"bgis.edit_predef_crs\"\n\tbl_description = 'Edit predefinate CRS'\n\tbl_label = \"Edit\"\n\tbl_options = {'INTERNAL'}\n\n\tcrs: StringProperty(name = \"EPSG code or Proj4 string\",  description = \"Specify EPSG code or Proj4 string definition for this CRS\")\n\tname: StringProperty(name = \"Description\", description = \"Choose a convenient name for this CRS\")\n\tdesc: StringProperty(name = \"Name\", description = \"Add a description or comment about this CRS\")\n\n\tdef invoke(self, context, event):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\tkey = prefs.predefCrs\n\t\tif key == '':\n\t\t\treturn {'CANCELLED'}\n\t\tdata = json.loads(prefs.predefCrsJson)\n\t\tentry = [entry for entry in data if entry[0] == key][0]\n\t\tself.crs, self.name, self.desc = entry\n\t\treturn context.window_manager.invoke_props_dialog(self)\n\n\tdef execute(self, context):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\tkey = prefs.predefCrs\n\t\tdata = json.loads(prefs.predefCrsJson)\n\n\t\tif SRS.validate(self.crs):\n\t\t\tdata = [entry for entry in data if entry[0] != key] #deleting\n\t\t\tdata.append((self.crs, self.name, self.desc))\n\t\t\tprefs.predefCrsJson = json.dumps(data)\n\t\t\tcontext.area.tag_redraw()\n\t\telse:\n\t\t\tself.report({'ERROR'}, 'Invalid CRS')\n\n\t\treturn {'FINISHED'}\n\n\n#################\n# Collection of operators to manage predefinates OSM Tags\n\nclass BGIS_OT_add_osm_tag(Operator):\n\tbl_idname = \"bgis.add_osm_tag\"\n\tbl_description = 'Add new predefinate OSM filter tag'\n\tbl_label = \"Add\"\n\tbl_options = {'INTERNAL'}\n\n\ttag: StringProperty(name = \"Tag\",  description = \"Specify the tag (examples : 'building', 'landuse=forest' ...)\")\n\n\tdef invoke(self, context, event):\n\t\treturn context.window_manager.invoke_props_dialog(self)#, width=300)\n\n\tdef execute(self, context):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\ttags = json.loads(prefs.osmTagsJson)\n\t\ttags.append(self.tag)\n\t\tprefs.osmTagsJson = json.dumps(tags)\n\t\tprefs.osmTags = self.tag #update current idx\n\t\tcontext.area.tag_redraw()\n\t\treturn {'FINISHED'}\n\nclass BGIS_OT_rmv_osm_tag(Operator):\n\n\tbl_idname = \"bgis.rmv_osm_tag\"\n\tbl_description = 'Remove predefinate OSM filter tag'\n\tbl_label = \"Remove\"\n\tbl_options = {'INTERNAL'}\n\n\tdef execute(self, context):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\ttag = prefs.osmTags\n\t\tif tag != '':\n\t\t\ttags = json.loads(prefs.osmTagsJson)\n\t\t\tdel tags[tags.index(tag)]\n\t\t\tprefs.osmTagsJson = json.dumps(tags)\n\t\tcontext.area.tag_redraw()\n\t\treturn {'FINISHED'}\n\nclass BGIS_OT_reset_osm_tags(Operator):\n\n\tbl_idname = \"bgis.reset_osm_tags\"\n\tbl_description = 'Reset predefinate OSM filter tag'\n\tbl_label = \"Reset\"\n\tbl_options = {'INTERNAL'}\n\n\tdef execute(self, context):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\tprefs.osmTagsJson = json.dumps(DEFAULT_OSM_TAGS)\n\t\tcontext.area.tag_redraw()\n\t\treturn {'FINISHED'}\n\nclass BGIS_OT_edit_osm_tag(Operator):\n\n\tbl_idname = \"bgis.edit_osm_tag\"\n\tbl_description = 'Edit predefinate OSM filter tag'\n\tbl_label = \"Edit\"\n\tbl_options = {'INTERNAL'}\n\n\ttag: StringProperty(name = \"Tag\",  description = \"Specify the tag (examples : 'building', 'landuse=forest' ...)\")\n\n\tdef invoke(self, context, event):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\tself.tag = prefs.osmTags\n\t\tif self.tag == '':\n\t\t\treturn {'CANCELLED'}\n\t\treturn context.window_manager.invoke_props_dialog(self)\n\n\tdef execute(self, context):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\ttag = prefs.osmTags\n\t\ttags = json.loads(prefs.osmTagsJson)\n\t\tdel tags[tags.index(tag)]\n\t\ttags.append(self.tag)\n\t\tprefs.osmTagsJson = json.dumps(tags)\n\t\tprefs.osmTags = self.tag #update current idx\n\t\tcontext.area.tag_redraw()\n\t\treturn {'FINISHED'}\n\n#################\n# Collection of operators to manage DEM server urls\n\nclass BGIS_OT_add_dem_server(Operator):\n\tbl_idname = \"bgis.add_dem_server\"\n\tbl_description = 'Add new topography web service'\n\tbl_label = \"Add\"\n\tbl_options = {'INTERNAL'}\n\n\turl: StringProperty(name = \"Url template\",  description = \"Define url template string. Bounding box varaibles are {W}, {E}, {S} and {N}\")\n\tname: StringProperty(name = \"Description\", description = \"Choose a convenient name for this server\")\n\tdesc: StringProperty(name = \"Description\", description = \"Add a description or comment about this remote datasource\")\n\n\tdef invoke(self, context, event):\n\t\treturn context.window_manager.invoke_props_dialog(self)#, width=300)\n\n\tdef execute(self, context):\n\t\ttemplates = ['{W}', '{E}', '{S}', '{N}']\n\t\tif all([t in self.url for t in templates]):\n\t\t\tprefs = context.preferences.addons[PKG].preferences\n\t\t\tdata = json.loads(prefs.demServerJson)\n\t\t\tdata.append( (self.url, self.name, self.desc) )\n\t\t\tprefs.demServerJson = json.dumps(data)\n\t\t\tcontext.area.tag_redraw()\n\t\telse:\n\t\t\tself.report({'ERROR'}, 'Invalid URL')\n\t\treturn {'FINISHED'}\n\nclass BGIS_OT_rmv_dem_server(Operator):\n\n\tbl_idname = \"bgis.rmv_dem_server\"\n\tbl_description = 'Remove a given topography web service'\n\tbl_label = \"Remove\"\n\tbl_options = {'INTERNAL'}\n\n\tdef execute(self, context):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\tkey = prefs.demServer\n\t\tif key != '':\n\t\t\tdata = json.loads(prefs.demServerJson)\n\t\t\tdata = [e for e in data if e[0] != key]\n\t\t\tprefs.demServerJson = json.dumps(data)\n\t\tcontext.area.tag_redraw()\n\t\treturn {'FINISHED'}\n\nclass BGIS_OT_reset_dem_server(Operator):\n\n\tbl_idname = \"bgis.reset_dem_server\"\n\tbl_description = 'Reset default topographic web server'\n\tbl_label = \"Reset\"\n\tbl_options = {'INTERNAL'}\n\n\tdef execute(self, context):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\tprefs.demServerJson = json.dumps(DEFAULT_DEM_SERVER)\n\t\tcontext.area.tag_redraw()\n\t\treturn {'FINISHED'}\n\nclass BGIS_OT_edit_dem_server(Operator):\n\n\tbl_idname = \"bgis.edit_dem_server\"\n\tbl_description = 'Edit a topographic web server'\n\tbl_label = \"Edit\"\n\tbl_options = {'INTERNAL'}\n\n\turl: StringProperty(name = \"Url template\",  description = \"Define url template string. Bounding box varaibles are {W}, {E}, {S} and {N}\")\n\tname: StringProperty(name = \"Description\", description = \"Choose a convenient name for this server\")\n\tdesc: StringProperty(name = \"Description\", description = \"Add a description or comment about this remote datasource\")\n\n\tdef invoke(self, context, event):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\tkey = prefs.demServer\n\t\tif key == '':\n\t\t\treturn {'CANCELLED'}\n\t\tdata = json.loads(prefs.demServerJson)\n\t\tentry = [entry for entry in data if entry[0] == key][0]\n\t\tself.url, self.name, self.desc = entry\n\t\treturn context.window_manager.invoke_props_dialog(self)\n\n\tdef execute(self, context):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\tkey = prefs.demServer\n\t\tdata = json.loads(prefs.demServerJson)\n\t\ttemplates = ['{W}', '{E}', '{S}', '{N}']\n\t\tif all([t in self.url for t in templates]):\n\t\t\tdata = [entry for entry in data if entry[0] != key] #deleting\n\t\t\tdata.append((self.url, self.name, self.desc))\n\t\t\tprefs.demServerJson = json.dumps(data)\n\t\t\tcontext.area.tag_redraw()\n\t\telse:\n\t\t\tself.report({'ERROR'}, 'Invalid URL')\n\t\treturn {'FINISHED'}\n\n#################\n\nclass EditEnum():\n\t'''\n\tHelper to deal with an enum property that use a serialized json backend\n\tCan be used by others operators to edit and EnumProperty\n\tWORK IN PROGRESS\n\t'''\n\n\tdef __init__(self, enumName):\n\t\tself.prefs = bpy.context.preferences.addons[PKG].preferences\n\t\tself.enumName = enumName\n\t\tself.jsonName = enumName + 'Json'\n\n\tdef getData(self):\n\t\t'''Load the json string'''\n\t\tdata = json.loads(getattr(self.prefs, self.jsonName))\n\t\treturn [tuple(entry) for entry in data]\n\n\tdef append(self, value, label, tooltip, check=lambda x: True):\n\t\tif not check(value):\n\t\t\treturn\n\t\tdata = self.getData()\n\t\tdata.append((value, label, tooltip))\n\t\tsetattr(self.prefs, self.jsonName, json.dumps(data))\n\n\tdef remove(self, key):\n\t\tif key != '':\n\t\t\tdata = self.getData()\n\t\t\tdata = [e for e in data if e[0] != key]\n\t\t\tsetattr(self.prefs, self.jsonName, json.dumps(data))\n\n\tdef edit(self, key, value, label, tooltip):\n\t\tself.remove(key)\n\t\tself.append(value, label, tooltip)\n\n\tdef reset(self):\n\t\tsetattr(self.prefs, self.jsonName, json.dumps(DEFAULT_OVERPASS_SERVER))\n\n#################\n# Collection of operators to manage Overpass server urls\n\nclass BGIS_OT_add_overpass_server(Operator):\n\tbl_idname = \"bgis.add_overpass_server\"\n\tbl_description = 'Add new OSM overpass server url'\n\tbl_label = \"Add\"\n\tbl_options = {'INTERNAL'}\n\n\turl: StringProperty(name = \"Url template\",  description = \"Define the url end point of the overpass server\")\n\tname: StringProperty(name = \"Description\", description = \"Choose a convenient name for this server\")\n\tdesc: StringProperty(name = \"Description\", description = \"Add a description or comment about this remote server\")\n\n\tdef invoke(self, context, event):\n\t\treturn context.window_manager.invoke_props_dialog(self)#, width=300)\n\n\tdef execute(self, context):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\tdata = json.loads(prefs.overpassServerJson)\n\t\tdata.append( (self.url, self.name, self.desc) )\n\t\tprefs.overpassServerJson = json.dumps(data)\n\t\t#EditEnum('overpassServer').append(self.url, self.name, self.desc, check=lambda url: url.startswith('http'))\n\t\tcontext.area.tag_redraw()\n\t\treturn {'FINISHED'}\n\nclass BGIS_OT_rmv_overpass_server(Operator):\n\n\tbl_idname = \"bgis.rmv_overpass_server\"\n\tbl_description = 'Remove a given overpass server'\n\tbl_label = \"Remove\"\n\tbl_options = {'INTERNAL'}\n\n\tdef execute(self, context):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\tkey = prefs.overpassServer\n\t\tif key != '':\n\t\t\tdata = json.loads(prefs.overpassServerJson)\n\t\t\tdata = [e for e in data if e[0] != key]\n\t\t\tprefs.overpassServerJson = json.dumps(data)\n\t\tcontext.area.tag_redraw()\n\t\treturn {'FINISHED'}\n\nclass BGIS_OT_reset_overpass_server(Operator):\n\n\tbl_idname = \"bgis.reset_overpass_server\"\n\tbl_description = 'Reset default overpass server'\n\tbl_label = \"Rest\"\n\tbl_options = {'INTERNAL'}\n\n\tdef execute(self, context):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\tprefs.overpassServerJson = json.dumps(DEFAULT_OVERPASS_SERVER)\n\t\tcontext.area.tag_redraw()\n\t\treturn {'FINISHED'}\n\nclass BGIS_OT_edit_overpass_server(Operator):\n\n\tbl_idname = \"bgis.edit_overpass_server\"\n\tbl_description = 'Edit an overpass server url'\n\tbl_label = \"Edit\"\n\tbl_options = {'INTERNAL'}\n\n\turl: StringProperty(name = \"Url template\",  description = \"Define the url end point of the overpass server\")\n\tname: StringProperty(name = \"Description\", description = \"Choose a convenient name for this server\")\n\tdesc: StringProperty(name = \"Description\", description = \"Add a description or comment about this remote server\")\n\n\tdef invoke(self, context, event):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\tkey = prefs.overpassServer\n\t\tif key == '':\n\t\t\treturn {'CANCELLED'}\n\t\tdata = json.loads(prefs.overpassServerJson)\n\t\tentry = [entry for entry in data if entry[0] == key][0]\n\t\tself.url, self.name, self.desc = entry\n\t\treturn context.window_manager.invoke_props_dialog(self)\n\n\tdef execute(self, context):\n\t\tprefs = context.preferences.addons[PKG].preferences\n\t\tkey = prefs.overpassServer\n\t\tdata = json.loads(prefs.overpassServerJson)\n\t\tdata = [entry for entry in data if entry[0] != key] #deleting\n\t\tdata.append((self.url, self.name, self.desc))\n\t\tprefs.overpassServerJson = json.dumps(data)\n\t\tcontext.area.tag_redraw()\n\t\treturn {'FINISHED'}\n\n\nclasses = [\nBGIS_OT_pref_show,\nBGIS_PREFS,\nBGIS_OT_add_predef_crs,\nBGIS_OT_rmv_predef_crs,\nBGIS_OT_reset_predef_crs,\nBGIS_OT_edit_predef_crs,\nBGIS_OT_add_osm_tag,\nBGIS_OT_rmv_osm_tag,\nBGIS_OT_reset_osm_tags,\nBGIS_OT_edit_osm_tag,\nBGIS_OT_add_dem_server,\nBGIS_OT_rmv_dem_server,\nBGIS_OT_reset_dem_server,\nBGIS_OT_edit_dem_server,\nBGIS_OT_add_overpass_server,\nBGIS_OT_rmv_overpass_server,\nBGIS_OT_reset_overpass_server,\nBGIS_OT_edit_overpass_server\n]\n\ndef register():\n\tfor cls in classes:\n\t\ttry:\n\t\t\tbpy.utils.register_class(cls)\n\t\texcept ValueError as e:\n\t\t\t#log.error('Cannot register {}'.format(cls), exc_info=True)\n\t\t\tlog.warning('{} is already registered, now unregister and retry... '.format(cls))\n\t\t\tbpy.utils.unregister_class(cls)\n\t\t\tbpy.utils.register_class(cls)\n\n\t# set default cache folder\n\tprefs = bpy.context.preferences.addons[PKG].preferences\n\tif prefs.cacheFolder == '':\n\t\tprefs.cacheFolder = APP_DATA\n\n\ndef unregister():\n\tfor cls in classes:\n\t\tbpy.utils.unregister_class(cls)\n"
  }
]