Repository: domlysz/BlenderGIS Branch: master Commit: 2add45ffec54 Files: 90 Total size: 1.2 MB Directory structure: gitextract_f4gt1_zi/ ├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── clients/ │ ├── QtMapServiceClient.py │ └── QtMapServiceClient.ui ├── core/ │ ├── __init__.py │ ├── basemaps/ │ │ ├── __init__.py │ │ ├── gpkg.py │ │ ├── mapservice.py │ │ └── servicesDefs.py │ ├── checkdeps.py │ ├── errors.py │ ├── georaster/ │ │ ├── __init__.py │ │ ├── bigtiffwriter.py │ │ ├── georaster.py │ │ ├── georef.py │ │ ├── img_utils.py │ │ └── npimg.py │ ├── lib/ │ │ ├── Tyf/ │ │ │ ├── VERSION │ │ │ ├── __init__.py │ │ │ ├── decoders.py │ │ │ ├── encoders.py │ │ │ ├── gkd.py │ │ │ ├── ifd.py │ │ │ ├── tags.py │ │ │ └── values.py │ │ ├── imageio/ │ │ │ ├── README.md │ │ │ ├── __init__.py │ │ │ ├── core/ │ │ │ │ ├── __init__.py │ │ │ │ ├── fetching.py │ │ │ │ ├── findlib.py │ │ │ │ ├── format.py │ │ │ │ ├── functions.py │ │ │ │ ├── request.py │ │ │ │ └── util.py │ │ │ ├── freeze.py │ │ │ ├── plugins/ │ │ │ │ ├── __init__.py │ │ │ │ ├── _freeimage.py │ │ │ │ └── freeimage.py │ │ │ ├── resources/ │ │ │ │ └── shipped_resources_go_here │ │ │ └── testing.py │ │ ├── imghdr.py │ │ ├── shapefile.py │ │ └── shapefile123.py │ ├── maths/ │ │ ├── __init__.py │ │ ├── akima.py │ │ ├── fillnodata.py │ │ ├── interpo.py │ │ └── kmeans1D.py │ ├── proj/ │ │ ├── __init__.py │ │ ├── ellps.py │ │ ├── reproj.py │ │ ├── srs.py │ │ ├── srv.py │ │ └── utm.py │ ├── settings.json │ ├── settings.py │ └── utils/ │ ├── __init__.py │ ├── bbox.py │ ├── gradient.py │ ├── timing.py │ └── xy.py ├── geoscene.py ├── issue_template.md ├── operators/ │ ├── __init__.py │ ├── add_camera_exif.py │ ├── add_camera_georef.py │ ├── io_export_shp.py │ ├── io_get_dem.py │ ├── io_import_asc.py │ ├── io_import_georaster.py │ ├── io_import_osm.py │ ├── io_import_shp.py │ ├── lib/ │ │ └── osm/ │ │ ├── nominatim.py │ │ └── overpy/ │ │ ├── __about__.py │ │ ├── __init__.py │ │ ├── exception.py │ │ └── helper.py │ ├── mesh_delaunay_voronoi.py │ ├── mesh_earth_sphere.py │ ├── nodes_terrain_analysis_builder.py │ ├── nodes_terrain_analysis_reclassify.py │ ├── object_drop.py │ ├── utils/ │ │ ├── __init__.py │ │ ├── bgis_utils.py │ │ ├── delaunay_voronoi.py │ │ └── georaster_utils.py │ └── view3d_mapviewer.py └── prefs.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ __pycache__/ *.py[cod] *$py.class ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: README.md ================================================ Blender GIS ========== Blender minimum version required : v2.83 Note : Since 2022, the OpenTopography web service requires an API key. Please register to opentopography.org and request a key. This service is still free. [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) -------------------- ## Functionalities overview **GIS datafile import :** Import in Blender most commons GIS data format : Shapefile vector, raster image, geotiff DEM, OpenStreetMap xml. There 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. Exemple : import vector contour lines, create faces by triangulation and put a topographic raster texture. ![](https://raw.githubusercontent.com/wiki/domlysz/blenderGIS/Blender28x/gif/bgis_demo_delaunay.gif) **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. ![](https://raw.githubusercontent.com/wiki/domlysz/blenderGIS/Blender28x/gif/bgis_demo_webdata.gif) **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. ================================================ FILE: __init__.py ================================================ # -*- coding:utf-8 -*- # ***** GPL LICENSE BLOCK ***** # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # All rights reserved. # ***** GPL LICENSE BLOCK ***** import bpy bl_info = { 'name': 'BlenderGIS', 'description': 'Various tools for handle geodata', 'author': 'domlysz', 'license': 'GPL', 'deps': '', 'version': (2, 2, 14), 'blender': (2, 83, 0), 'location': 'View3D > Tools > GIS', 'warning': '', 'wiki_url': 'https://github.com/domlysz/BlenderGIS/wiki', 'tracker_url': 'https://github.com/domlysz/BlenderGIS/issues', 'link': '', 'support': 'COMMUNITY', 'category': '3D View' } class BlenderVersionError(Exception): pass if bl_info['blender'] > bpy.app.version: raise BlenderVersionError(f"This addon requires Blender >= {bl_info['blender']}") if bpy.app.version[0] > 5: #prevent breaking changes on major release raise BlenderVersionError(f"This addon is not tested against Blender {bpy.app.version[0]}.x breaking changes") #Modules CAM_GEOPHOTO = True CAM_GEOREF = True EXPORT_SHP = True GET_DEM = True IMPORT_GEORASTER = True IMPORT_OSM = True IMPORT_SHP = True IMPORT_ASC = True DELAUNAY = True TERRAIN_NODES = True TERRAIN_RECLASS = True BASEMAPS = True DROP = True EARTH_SPHERE = True import os, sys, tempfile from datetime import datetime def getAppData(): home = os.path.expanduser('~') loc = os.path.join(home, '.bgis') if not os.path.exists(loc): os.mkdir(loc) return loc APP_DATA = getAppData() import logging from logging.handlers import RotatingFileHandler #temporary set log level, will be overriden reading addon prefs #logsFormat = "%(levelname)s:%(name)s:%(lineno)d:%(message)s" logsFormat = '{levelname}:{name}:{lineno}:{message}' logsFileName = 'bgis.log' try: #logsFilePath = os.path.join(os.path.dirname(__file__), logsFileName) logsFilePath = os.path.join(APP_DATA, logsFileName) #logging.basicConfig(level=logging.getLevelName('DEBUG'), format=logsFormat, style='{', filename=logsFilePath, filemode='w') logHandler = RotatingFileHandler(logsFilePath, mode='a', maxBytes=512000, backupCount=1) except PermissionError: #logsFilePath = os.path.join(bpy.app.tempdir, logsFileName) logsFilePath = os.path.join(tempfile.gettempdir(), logsFileName) logHandler = RotatingFileHandler(logsFilePath, mode='a', maxBytes=512000, backupCount=1) logHandler.setFormatter(logging.Formatter(logsFormat, style='{')) logger = logging.getLogger(__name__) logger.addHandler(logHandler) logger.setLevel(logging.DEBUG) logger.info('###### Starting new Blender session : {}'.format(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))) def _excepthook(exc_type, exc_value, exc_traceback): if 'BlenderGIS' in exc_traceback.tb_frame.f_code.co_filename: logger.error("Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)) sys.__excepthook__(exc_type, exc_value, exc_traceback) sys.excepthook = _excepthook #warn, this is a global variable, can be overrided by another addon #### ''' Workaround for `sys.excepthook` thread https://stackoverflow.com/questions/1643327/sys-excepthook-and-threading ''' import threading init_original = threading.Thread.__init__ def init(self, *args, **kwargs): init_original(self, *args, **kwargs) run_original = self.run def run_with_except_hook(*args2, **kwargs2): try: run_original(*args2, **kwargs2) except Exception: sys.excepthook(*sys.exc_info()) self.run = run_with_except_hook threading.Thread.__init__ = init #### import ssl if (not os.environ.get('PYTHONHTTPSVERIFY', '') and getattr(ssl, '_create_unverified_context', None)): ssl._create_default_https_context = ssl._create_unverified_context #from .core.checkdeps import HAS_GDAL, HAS_PYPROJ, HAS_PIL, HAS_IMGIO from .core.settings import settings #Import all modules which contains classes that must be registed (classes derived from bpy.types.*) from . import prefs from . import geoscene if CAM_GEOPHOTO: from .operators import add_camera_exif if CAM_GEOREF: from .operators import add_camera_georef if EXPORT_SHP: from .operators import io_export_shp if GET_DEM: from .operators import io_get_dem if IMPORT_GEORASTER: from .operators import io_import_georaster if IMPORT_OSM: from .operators import io_import_osm if IMPORT_SHP: from .operators import io_import_shp if IMPORT_ASC: from .operators import io_import_asc if DELAUNAY: from .operators import mesh_delaunay_voronoi if TERRAIN_NODES: from .operators import nodes_terrain_analysis_builder if TERRAIN_RECLASS: from .operators import nodes_terrain_analysis_reclassify if BASEMAPS: from .operators import view3d_mapviewer if DROP: from .operators import object_drop if EARTH_SPHERE: from .operators import mesh_earth_sphere import bpy.utils.previews as iconsLib icons_dict = {} class BGIS_OT_logs(bpy.types.Operator): bl_idname = "bgis.logs" bl_description = 'Display BlenderGIS logs' bl_label = "Logs" def execute(self, context): if logsFileName in bpy.data.texts: logs = bpy.data.texts[logsFileName] else: logs = bpy.data.texts.load(logsFilePath) bpy.ops.screen.area_split(direction='VERTICAL', factor=0.5) area = bpy.context.area area.type = 'TEXT_EDITOR' area.spaces[0].text = logs bpy.ops.text.reload() return {'FINISHED'} class VIEW3D_MT_menu_gis_import(bpy.types.Menu): bl_label = "Import" def draw(self, context): if IMPORT_SHP: self.layout.operator("importgis.shapefile_file_dialog", icon_value=icons_dict["shp"].icon_id, text='Shapefile (.shp)') if IMPORT_GEORASTER: self.layout.operator("importgis.georaster", icon_value=icons_dict["raster"].icon_id, text="Georeferenced raster (.tif .jpg .jp2 .png)") if IMPORT_OSM: self.layout.operator("importgis.osm_file", icon_value=icons_dict["osm"].icon_id, text="Open Street Map xml (.osm)") if IMPORT_ASC: self.layout.operator('importgis.asc_file', icon_value=icons_dict["asc"].icon_id, text="ESRI ASCII Grid (.asc)") class VIEW3D_MT_menu_gis_export(bpy.types.Menu): bl_label = "Export" def draw(self, context): if EXPORT_SHP: self.layout.operator('exportgis.shapefile', text="Shapefile (.shp)", icon_value=icons_dict["shp"].icon_id) class VIEW3D_MT_menu_gis_webgeodata(bpy.types.Menu): bl_label = "Web geodata" def draw(self, context): if BASEMAPS: self.layout.operator("view3d.map_start", icon_value=icons_dict["layers"].icon_id) if IMPORT_OSM: self.layout.operator("importgis.osm_query", icon_value=icons_dict["osm"].icon_id) if GET_DEM: self.layout.operator("importgis.dem_query", icon_value=icons_dict["raster"].icon_id) class VIEW3D_MT_menu_gis_camera(bpy.types.Menu): bl_label = "Camera" def draw(self, context): if CAM_GEOREF: self.layout.operator("camera.georender", icon_value=icons_dict["georefCam"].icon_id, text='Georender') if CAM_GEOPHOTO: self.layout.operator("camera.geophotos", icon_value=icons_dict["exifCam"].icon_id, text='Geophotos') self.layout.operator("camera.geophotos_setactive", icon='FILE_REFRESH') class VIEW3D_MT_menu_gis_mesh(bpy.types.Menu): bl_label = "Mesh" def draw(self, context): if DELAUNAY: self.layout.operator("tesselation.delaunay", icon_value=icons_dict["delaunay"].icon_id, text='Delaunay') self.layout.operator("tesselation.voronoi", icon_value=icons_dict["voronoi"].icon_id, text='Voronoi') if EARTH_SPHERE: self.layout.operator("earth.sphere", icon="WORLD", text='lonlat to sphere') #self.layout.operator("earth.curvature", icon="SPHERECURVE", text='Earth curvature correction') self.layout.operator("earth.curvature", icon_value=icons_dict["curve"].icon_id, text='Earth curvature correction') class VIEW3D_MT_menu_gis_object(bpy.types.Menu): bl_label = "Object" def draw(self, context): if DROP: self.layout.operator("object.drop", icon_value=icons_dict["drop"].icon_id, text='Drop') class VIEW3D_MT_menu_gis_nodes(bpy.types.Menu): bl_label = "Nodes" def draw(self, context): if TERRAIN_NODES: self.layout.operator("analysis.nodes", icon_value=icons_dict["terrain"].icon_id, text='Terrain analysis') class VIEW3D_MT_menu_gis(bpy.types.Menu): bl_label = "GIS" # Set the menu operators and draw functions def draw(self, context): layout = self.layout layout.operator("bgis.pref_show", icon='PREFERENCES') layout.separator() layout.menu('VIEW3D_MT_menu_gis_webgeodata', icon="URL") layout.menu('VIEW3D_MT_menu_gis_import', icon='IMPORT') layout.menu('VIEW3D_MT_menu_gis_export', icon='EXPORT') layout.menu('VIEW3D_MT_menu_gis_camera', icon='CAMERA_DATA') layout.menu('VIEW3D_MT_menu_gis_mesh', icon='MESH_DATA') layout.menu('VIEW3D_MT_menu_gis_object', icon='CUBE') layout.menu('VIEW3D_MT_menu_gis_nodes', icon='NODETREE') layout.separator() layout.operator("bgis.logs", icon='TEXT') menus = [ VIEW3D_MT_menu_gis, VIEW3D_MT_menu_gis_webgeodata, VIEW3D_MT_menu_gis_import, VIEW3D_MT_menu_gis_export, VIEW3D_MT_menu_gis_camera, VIEW3D_MT_menu_gis_mesh, VIEW3D_MT_menu_gis_object, VIEW3D_MT_menu_gis_nodes ] def add_gis_menu(self, context): if context.mode == 'OBJECT': self.layout.menu('VIEW3D_MT_menu_gis') def register(): #icons global icons_dict icons_dict = iconsLib.new() icons_dir = os.path.join(os.path.dirname(__file__), "icons") for icon in os.listdir(icons_dir): name, ext = os.path.splitext(icon) icons_dict.load(name, os.path.join(icons_dir, icon), 'IMAGE') #operators prefs.register() geoscene.register() for menu in menus: try: bpy.utils.register_class(menu) except ValueError as e: logger.warning('{} is already registered, now unregister and retry... '.format(menu)) bpy.utils.unregister_class(menu) bpy.utils.register_class(menu) bpy.utils.register_class(BGIS_OT_logs) if BASEMAPS: view3d_mapviewer.register() if IMPORT_GEORASTER: io_import_georaster.register() if IMPORT_SHP: io_import_shp.register() if EXPORT_SHP: io_export_shp.register() if IMPORT_OSM: io_import_osm.register() if IMPORT_ASC: io_import_asc.register() if DELAUNAY: mesh_delaunay_voronoi.register() if DROP: object_drop.register() if GET_DEM: io_get_dem.register() if CAM_GEOPHOTO: add_camera_exif.register() if CAM_GEOREF: add_camera_georef.register() if TERRAIN_NODES: nodes_terrain_analysis_builder.register() if TERRAIN_RECLASS: nodes_terrain_analysis_reclassify.register() if EARTH_SPHERE: mesh_earth_sphere.register() #menus bpy.types.VIEW3D_MT_editor_menus.append(add_gis_menu) #shortcuts if not bpy.app.background: #no ui when running as background wm = bpy.context.window_manager kc = wm.keyconfigs.active if '3D View' in kc.keymaps: km = kc.keymaps['3D View'] if BASEMAPS: kmi = km.keymap_items.new(idname='view3d.map_start', type='NUMPAD_ASTERIX', value='PRESS') #Setup prefs preferences = bpy.context.preferences.addons[__package__].preferences logger.setLevel(logging.getLevelName(preferences.logLevel)) #will affect all child logger #update core settings according to addon prefs settings.proj_engine = preferences.projEngine settings.img_engine = preferences.imgEngine settings.maptiler_api_key = preferences.maptiler_api_key def unregister(): global icons_dict iconsLib.remove(icons_dict) if not bpy.app.background: #no ui when running as background wm = bpy.context.window_manager if '3D View' in wm.keyconfigs.active.keymaps: km = wm.keyconfigs.active.keymaps['3D View'] if BASEMAPS: if 'view3d.map_start' in km.keymap_items: kmi = km.keymap_items.remove(km.keymap_items['view3d.map_start']) bpy.types.VIEW3D_MT_editor_menus.remove(add_gis_menu) for menu in menus: bpy.utils.unregister_class(menu) bpy.utils.unregister_class(BGIS_OT_logs) prefs.unregister() geoscene.unregister() if BASEMAPS: view3d_mapviewer.unregister() if IMPORT_GEORASTER: io_import_georaster.unregister() if IMPORT_SHP: io_import_shp.unregister() if EXPORT_SHP: io_export_shp.unregister() if IMPORT_OSM: io_import_osm.unregister() if IMPORT_ASC: io_import_asc.unregister() if DELAUNAY: mesh_delaunay_voronoi.unregister() if DROP: object_drop.unregister() if GET_DEM: io_get_dem.unregister() if CAM_GEOPHOTO: add_camera_exif.unregister() if CAM_GEOREF: add_camera_georef.unregister() if TERRAIN_NODES: nodes_terrain_analysis_builder.unregister() if TERRAIN_RECLASS: nodes_terrain_analysis_reclassify.unregister() if EARTH_SPHERE: mesh_earth_sphere.unregister() if __name__ == "__main__": register() ================================================ FILE: clients/QtMapServiceClient.py ================================================ # -*- coding:utf-8 -*- import sys, os, time import sys, os sys.path.append(os.path.abspath('..')) from PyQt4 import QtGui, QtCore, uic import threading import tempfile from core.basemaps import GRIDS, SOURCES, MapService, BBoxRequest, BBoxRequestMZ from core.lib import shapefile from core.proj import reprojPts from xml.etree import ElementTree as etree import re #on the fly ui dialogs compilation mainForm, mainBase = uic.loadUiType('QtMapServiceClient.ui') projSysLst={ 2154 : "Lambert 93", 3942 : "Lambert CC42", 3943 : "Lambert CC43", 3944 : "Lambert CC44", 3945 : "Lambert CC45", 3946 :"Lambert CC46", 3947 : "Lambert CC47", 3948 : "Lambert CC48", 3949 : "Lambert CC49", 3950 : "Lambert CC50" } def getShpExtent(pathShp): shp = shapefile.Reader(pathShp) shapes = shp.shapes() #we expect only one feature ! if len(shapes) != 1: return else: extent = shapes[0].bbox #xmin, ymin, xmax, ymax return extent def getKmlExtent(kmlFile, crs2): def formatCoor(coorText): coorText = coorText.strip() coordinates = [] for elem in str(coorText).split(" "): coordinates.append(tuple(map(float, elem.split(",")))) return coordinates def namespace(element): m = re.match('\{.*\}', element.tag) return m.group(0) if m else '' root = etree.parse(kmlFile).getroot() ns = namespace(root) polygons = [] for poly in root.iter(ns+"Polygon"): for attributes in poly.iter(ns+"coordinates"): polygons.append(formatCoor(attributes.text)) if len(polygons) != 1: return else: pts = polygons[0] #first feature pts = reprojPts(4326, crs2, pts) xmin = min([pt[0] for pt in pts]) ymin = min([pt[1] for pt in pts]) xmax = max([pt[0] for pt in pts]) ymax = max([pt[1] for pt in pts]) extent = [xmin, ymin, xmax, ymax] return list(map(round,extent)) class QtMapServiceClient(QtGui.QMainWindow, mainForm): def __init__(self): #UI init QtGui.QMainWindow.__init__(self) self.setupUi(self) # for k, v in SOURCES.items(): self.cbProvider.addItem(v['name'], k) #text, data self.extent = None self.inCacheFolder.setText(tempfile.gettempdir()) self.btCacheFolder.clicked.connect(self.setCacheFolder) self.btBrowseOutFolder.clicked.connect(self.setInOutFolder) self.btOkMosaic.clicked.connect(self.uiDoProcess) self.btCancel.clicked.connect(self.uiDoCancelThread) self.btExtentShp.clicked.connect(self.uiDoReadShpExtent) self.cbProvider.currentIndexChanged.connect(self.uiDoUpdateProvider) self.cbLayer.currentIndexChanged.connect(self.uiDoUpdateScales) self.cbZoom.currentIndexChanged.connect(self.uiDoUpdateRes) self.chkJPG.stateChanged.connect(self.uiUpdateMaskOption) self.chkSeedCache.stateChanged.connect(self.uiUpdateSeedOption) # self.uiDoUpdateProvider() self.inVectorFile.setText("*.kml *.shp...") @property def provider(self): k = self.cbProvider.itemData(self.cbProvider.currentIndex()) cacheFolder = str(self.inCacheFolder.text()) return MapService(k, cacheFolder) @property def layer(self): return self.cbLayer.itemData(self.cbLayer.currentIndex()) @property def outProj(self): return self.cbOutProj.itemData(self.cbOutProj.currentIndex()) @property def zoom(self): z = self.cbZoom.itemData(self.cbZoom.currentIndex()) if z is not None: return int(z) @property def rq(self): if self.extent is not None and self.zoom is not None: rq = self.provider.srcTms.bboxRequest(self.extent, self.zoom) return rq def uiUpdateMaskOption(self): if self.chkJPG.isChecked(): self.chkMask.setEnabled(True) else: self.chkMask.setEnabled(False) def uiUpdateSeedOption(self): if self.chkSeedCache.isChecked(): self.chkRecurseUpZoomLevels.setEnabled(True) self.chkReproj.setEnabled(False) self.cbOutProj.setEnabled(False) self.chkBuildOverview.setEnabled(False) self.chkJPG.setEnabled(False) self.chkMask.setEnabled(False) self.chkBigtiff.setEnabled(False) self.inName.setEnabled(False) self.inOutFolder.setEnabled(False) self.btBrowseOutFolder.setEnabled(False) else: self.chkRecurseUpZoomLevels.setEnabled(False) self.chkReproj.setEnabled(True) self.cbOutProj.setEnabled(True) self.chkBuildOverview.setEnabled(True) self.chkJPG.setEnabled(True) self.chkMask.setEnabled(True) self.chkBigtiff.setEnabled(True) self.inName.setEnabled(True) self.inOutFolder.setEnabled(True) self.btBrowseOutFolder.setEnabled(True) def uiDoUpdateProvider(self): '''Triggered when cbProvider idx change''' #clear comboboxes self.cbLayer.clear() self.cbOutProj.clear() #seed layers combobox for layerKey, layer in self.provider.layers.items(): self.cbLayer.addItem(layer.name, layerKey) #reproj sys for k, v in projSysLst.items(): self.cbOutProj.addItem(v, k) self.cbOutProj.setCurrentIndex(self.cbOutProj.findData(2154)) # self.updateExtent() def uiDoUpdateScales(self): '''Triggered when cbLayer idx change''' if self.layer is not None: lay = self.provider.layers[self.layer] self.cbZoom.clear() for z in range(lay.zmin, lay.zmax): self.cbZoom.addItem(str(z), str(z)) def uiDoUpdateRes(self, zoomLevel): '''Triggered when cbZoom idx change''' if self.rq is not None: self.lbRes.setText(str(round(self.rq.res, 2))+" m/px") self.uiDoRequestInfos() def uiDoReadShpExtent(self): path = str(self.setOpenFileName('Shapefile (*.shp *.kml)')) self.inVectorFile.setText(path) self.updateExtent() def updateExtent(self): path = self.inVectorFile.text() if not os.path.exists(path): pass else: ext = path[-3:] if ext == 'shp': self.extent = getShpExtent(path) #xmin, ymin, xmax, ymax elif ext == 'kml': self.extent = getKmlExtent(path, self.provider.srcTms.CRS) if not self.extent: QtGui.QMessageBox.information(self, "Cannot read vector extent file", "This file must contains only one polygon") return # self.uiDoRequestInfos() self.inVectorFile.setText(path) def uiDoRequestInfos(self): if self.rq is not None: tileSize = self.rq.tileSize res = self.rq.res cols, rows = self.rq.nbTilesX, self.rq.nbTilesY n = self.rq.nbTiles #rqTiles = rq.tiles #[(x,y,z)] # xmin, ymin, xmax, ymax = self.extent dstX = xmax-xmin dstY = ymax-ymin txtEmprise = str(round(dstX)) + " x " + str(round(dstY)) + " m" # nbPx = int(cols * tileSize * rows * tileSize) if nbPx > 1000000: txtNbPx = str(int(nbPx/1000000)) + " Mpix" else: txtNbPx = str(nbPx) + " pix" # txtNbTiles = str(n) + " tile(s)" # resultStr = txtNbTiles + " (" + str(cols) + 'x' + str(rows) + ") - " + txtNbPx + " - " + txtEmprise self.requestInfos.setText(resultStr) def uiDoProcess(self): outFolder = str(self.inOutFolder.text()) nameTemplate = str(self.inName.text()) cacheFolder = str(self.inCacheFolder.text()) if not self.chkSeedCache: if not os.path.exists(outFolder): QtGui.QMessageBox.information(self, "Error", "Output folder does not exists") return if not nameTemplate: QtGui.QMessageBox.information(self, "Error", "Basename is not defined") return if not os.path.exists(cacheFolder): QtGui.QMessageBox.information(self, "Error", "Cache folder does not exists") return #Options reproj = self.chkReproj.isChecked() outProj = self.cbOutProj.itemData(self.cbOutProj.currentIndex()) reprojOptions = (reproj, outProj) buildOvv = self.chkBuildOverview.isChecked() jpgInTiff = self.chkJPG.isChecked() mask = self.chkMask.isChecked() bigTiff = self.chkBigtiff.isChecked() #Start map service self.btOkMosaic.setEnabled(False) if self.chkReproj: outCRS = self.outProj else: outCRS = None outFile = outFolder + os.sep + nameTemplate + '.tif' seedOnly = self.chkSeedCache.isChecked() recurseUpZoomLevels = self.chkRecurseUpZoomLevels.isChecked() self.thread = DownloadTiles(self.provider, self.layer, self.extent, self.zoom, outFile, outCRS, seedOnly, recurseUpZoomLevels) self.thread.finished.connect(self.uiProcessFinished) self.thread.terminated.connect(self.uiProcessFinished) self.thread.updateBar1.connect(self.uiDoUpdateBar1) self.thread.configBar1.connect(self.uiDoConfigBar1) self.thread.processInfo.connect(self.updateProcessInfo) self.thread.start() def uiProcessFinished(self): self.updateUi() QtGui.QMessageBox.information(self, "Info", "Finished") def uiDoCancelThread(self): try: self.thread.cancel() except: pass def uiSendQuestion(self, titre, msg): choice = QtGui.QMessageBox.question(self, titre, msg, QtGui.QMessageBox.Yes, QtGui.QMessageBox.No) if choice == QtGui.QMessageBox.Yes: return True else: return False def updateUi(self): self.btOkMosaic.setEnabled(True) def uiDoUpdateBar1(self, num): self.pBar1.setValue(num) def uiDoConfigBar1(self, nb): self.pBar1.setMinimum(0) self.pBar1.setMaximum(nb) def updateProcessInfo(self, txt): self.processInfo.setText(txt) #Set des inputbox def setInOutFolder(self): path = self.setExistingDirectory() if path: self.inOutFolder.setText(path) def setCacheFolder(self): path = self.setExistingDirectory() if path: self.inCacheFolder.setText(path) def setInFolder(self): path = self.setExistingDirectory() if path: self.inVectorFile.setText(path) #Standard dialogs def setOpenFileName(self, filtre): fileName = QtGui.QFileDialog.getOpenFileName(self, "Select file", QtCore.QDir.rootPath(),filtre) return QtCore.QDir.toNativeSeparators(fileName) def setExistingDirectory(self): directory = QtGui.QFileDialog.getExistingDirectory(self, "Select directory", QtCore.QDir.rootPath(), QtGui.QFileDialog.ShowDirsOnly) return QtCore.QDir.toNativeSeparators(directory) def setSaveFileName(self): saveFileName = QtGui.QFileDialog.getSaveFileName(self, "Save file", QtCore.QDir.rootPath()) return QtCore.QDir.toNativeSeparators(saveFileName) class DownloadTiles(QtCore.QThread): #custum signals configBar1 = QtCore.pyqtSignal(int) updateBar1 = QtCore.pyqtSignal(int) processInfo = QtCore.pyqtSignal(str) def __init__(self, srv, layer, extent, zoom, outFile, outCRS, seedOnly, recurseUpZoomLevels): QtCore.QThread.__init__(self, None) self.srv = srv self.layer = layer self.extent = extent self.outFile = outFile self.outCRS = outCRS self.seedOnly = seedOnly if recurseUpZoomLevels and seedOnly: self.zoom = list(range(self.srv.layers[self.layer].zmin, zoom+1)) self.rq = BBoxRequestMZ(self.srv.srcTms, self.extent, self.zoom) print(self.rq.nbTiles, self.srv.srcTms.bboxRequest(self.extent, zoom).nbTiles) else: self.zoom = zoom self.rq = self.srv.srcTms.bboxRequest(self.extent, self.zoom) def run(self): self.srv.start() self.configBar1.emit(self.rq.nbTiles) #self.configBar1.emit(0) #alternative moves if self.seedOnly: thread = threading.Thread(target=self.seedCache) else: thread = threading.Thread(target=self.getImage) #thread.setDaemon(True) #daemon threads will die when the main non-daemon thread have exited. thread.start() while thread.isAlive(): time.sleep(0.05) self.processInfo.emit(self.srv.report) self.updateBar1.emit(self.srv.cptTiles) self.srv.stop() def seedCache(self): self.srv.seedCache(self.layer, self.extent, self.zoom, toDstGrid=False) def getImage(self): self.srv.getImage(self.layer, self.extent, self.zoom, path=self.outFile, bigTiff=True, outCRS=self.outCRS, toDstGrid=False) def cancel(self): self.srv.stop() ''' #no need for pausing because downloading tiles are saved in cache, #so restarting an aborted process will reuse existing tiles def pause(self): self.srv.pause() def resume(self): self.srv.resume() ''' if __name__ == "__main__": app = QtGui.QApplication(sys.argv) window = QtMapServiceClient() window.show() sys.exit(app.exec_()) ================================================ FILE: clients/QtMapServiceClient.ui ================================================ QtMapService 0 0 350 380 350 380 350 380 MapService Qt Client 8 316 291 23 Liberation Sans 0 106 252 135 20 Liberation Sans 300 310 41 35 Liberation Sans Go true false 128 95 81 19 Liberation Sans false Zoom 164 91 77 25 Liberation Sans 300 354 41 21 Liberation Sans Stop true false 302 280 31 21 Liberation Sans ... 305 63 29 21 Liberation Sans ... 6 280 101 20 Liberation Sans true Output folder 105 280 195 20 Liberation Sans 5 92 117 25 Liberation Sans false 8 354 286 19 Liberation Sans 6 254 83 16 Liberation Sans true Basename 11 171 101 21 Liberation Sans Reprojection false 112 168 153 23 Liberation Sans 82 63 219 20 Liberation Sans false 5 124 331 19 Liberation Sans false 248 92 85 23 Liberation Sans 8 10 55 20 Liberation Sans true Provider 82 8 147 25 Liberation Sans 8 62 71 20 Liberation Sans true Extent file 8 198 153 16 Liberation Sans true Tiff format options 11 216 337 33 Liberation Sans Build pyramids for speed up display performance Pyramids true Liberation Sans Use JPEG compression (destructive, no alpha channel) Comp. jpeg true Liberation Sans Use an internal mask to store alpha and nodata values (useful with jpeg compression) Mask true Liberation Sans Allows creating raster greater than 4GB Bigtiff true 11 146 135 21 Liberation Sans Only seed cache false 82 39 217 20 Liberation Sans 304 39 29 21 Liberation Sans ... 8 38 79 20 Liberation Sans true Cache folder false 163 146 215 21 Liberation Sans Get recurse up zoom levels false inVectorFile btExtentShp cbLayer cbZoom lbRes requestInfos chkReproj cbOutProj chkBuildOverview inName inOutFolder btBrowseOutFolder btOkMosaic processInfo btCancel 2 2 true true true ================================================ FILE: core/__init__.py ================================================ import logging logging.basicConfig(level=logging.getLevelName('INFO')) from .checkdeps import HAS_GDAL, HAS_PYPROJ, HAS_IMGIO, HAS_PIL from .settings import settings from .errors import OverlapError from .utils import XY, BBOX from .proj import SRS, Reproj, reprojPt, reprojPts, reprojBbox, reprojImg from .georaster import GeoRef, GeoRaster, NpImage from .basemaps import GRIDS, SOURCES, MapService, GeoPackage, TileMatrix from .lib import shapefile ================================================ FILE: core/basemaps/__init__.py ================================================ from .servicesDefs import GRIDS, SOURCES from .mapservice import MapService, TileMatrix, BBoxRequest, BBoxRequestMZ from .gpkg import GeoPackage ================================================ FILE: core/basemaps/gpkg.py ================================================ # -*- coding:utf-8 -*- # ***** GPL LICENSE BLOCK ***** # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # All rights reserved. # ***** GPL LICENSE BLOCK ***** import logging log = logging.getLogger(__name__) import os import io import math import datetime import sqlite3 #http://www.geopackage.org/spec/#tiles #https://github.com/GitHubRGI/geopackage-python/blob/master/Packaging/tiles2gpkg_parallel.py #https://github.com/Esri/raster2gpkg/blob/master/raster2gpkg.py #table_name refer to the name of the table witch contains tiles data #here for simplification, table_name will always be named "gpkg_tiles" class GeoPackage(): MAX_DAYS = 90 def __init__(self, path, tm): self.dbPath = path self.name = os.path.splitext(os.path.basename(path))[0] #Get props from TileMatrix object self.auth, self.code = tm.CRS.split(':') self.code = int(self.code) self.tileSize = tm.tileSize self.xmin, self.ymin, self.xmax, self.ymax = tm.globalbbox self.resolutions = tm.getResList() if not self.isGPKG(): self.create() self.insertMetadata() self.insertCRS(self.code, str(self.code), self.auth) #self.insertCRS(3857, "Web Mercator") #self.insertCRS(4326, "WGS84") self.insertTileMatrixSet() def isGPKG(self): if not os.path.exists(self.dbPath): return False db = sqlite3.connect(self.dbPath) #check application id app_id = db.execute("PRAGMA application_id").fetchone() if not app_id[0] == 1196437808: db.close() return False #quick check of table schema try: db.execute('SELECT table_name FROM gpkg_contents LIMIT 1') db.execute('SELECT srs_name FROM gpkg_spatial_ref_sys LIMIT 1') db.execute('SELECT table_name FROM gpkg_tile_matrix_set LIMIT 1') db.execute('SELECT table_name FROM gpkg_tile_matrix LIMIT 1') db.execute('SELECT zoom_level, tile_column, tile_row, tile_data FROM gpkg_tiles LIMIT 1') except Exception as e: log.error('Incorrect GPKG schema', exc_info=True) db.close() return False else: db.close() return True def create(self): """Create default geopackage schema on the database.""" db = sqlite3.connect(self.dbPath) #this attempt will create a new file if not exist cursor = db.cursor() # Add GeoPackage version 1.0 ("GP10" in ASCII) to the Sqlite header cursor.execute("PRAGMA application_id = 1196437808;") cursor.execute(""" CREATE TABLE gpkg_contents ( table_name TEXT NOT NULL PRIMARY KEY, data_type TEXT NOT NULL, identifier TEXT UNIQUE, description TEXT DEFAULT '', last_change DATETIME NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ','now')), min_x DOUBLE, min_y DOUBLE, max_x DOUBLE, max_y DOUBLE, srs_id INTEGER, CONSTRAINT fk_gc_r_srs_id FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys(srs_id)); """) cursor.execute(""" CREATE TABLE gpkg_spatial_ref_sys ( srs_name TEXT NOT NULL, srs_id INTEGER NOT NULL PRIMARY KEY, organization TEXT NOT NULL, organization_coordsys_id INTEGER NOT NULL, definition TEXT NOT NULL, description TEXT); """) cursor.execute(""" CREATE TABLE gpkg_tile_matrix_set ( table_name TEXT NOT NULL PRIMARY KEY, srs_id INTEGER NOT NULL, min_x DOUBLE NOT NULL, min_y DOUBLE NOT NULL, max_x DOUBLE NOT NULL, max_y DOUBLE NOT NULL, CONSTRAINT fk_gtms_table_name FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name), CONSTRAINT fk_gtms_srs FOREIGN KEY (srs_id) REFERENCES gpkg_spatial_ref_sys(srs_id)); """) cursor.execute(""" CREATE TABLE gpkg_tile_matrix ( table_name TEXT NOT NULL, zoom_level INTEGER NOT NULL, matrix_width INTEGER NOT NULL, matrix_height INTEGER NOT NULL, tile_width INTEGER NOT NULL, tile_height INTEGER NOT NULL, pixel_x_size DOUBLE NOT NULL, pixel_y_size DOUBLE NOT NULL, CONSTRAINT pk_ttm PRIMARY KEY (table_name, zoom_level), CONSTRAINT fk_ttm_table_name FOREIGN KEY (table_name) REFERENCES gpkg_contents(table_name)); """) cursor.execute(""" CREATE TABLE gpkg_tiles ( id INTEGER PRIMARY KEY AUTOINCREMENT, zoom_level INTEGER NOT NULL, tile_column INTEGER NOT NULL, tile_row INTEGER NOT NULL, tile_data BLOB NOT NULL, last_modified TIMESTAMP DEFAULT (datetime('now','localtime')), UNIQUE (zoom_level, tile_column, tile_row)); """) db.close() def insertMetadata(self): db = sqlite3.connect(self.dbPath) query = """INSERT INTO gpkg_contents ( table_name, data_type, identifier, description, min_x, min_y, max_x, max_y, srs_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);""" db.execute(query, ("gpkg_tiles", "tiles", self.name, "Created with BlenderGIS", self.xmin, self.ymin, self.xmax, self.ymax, self.code)) db.commit() db.close() def insertCRS(self, code, name, auth='EPSG', wkt=''): db = sqlite3.connect(self.dbPath) db.execute(""" INSERT INTO gpkg_spatial_ref_sys ( srs_id, organization, organization_coordsys_id, srs_name, definition) VALUES (?, ?, ?, ?, ?) """, (code, auth, code, name, wkt)) db.commit() db.close() def insertTileMatrixSet(self): db = sqlite3.connect(self.dbPath) #Tile matrix set query = """INSERT OR REPLACE INTO gpkg_tile_matrix_set ( table_name, srs_id, min_x, min_y, max_x, max_y) VALUES (?, ?, ?, ?, ?, ?);""" db.execute(query, ('gpkg_tiles', self.code, self.xmin, self.ymin, self.xmax, self.ymax)) #Tile matrix of each levels for level, res in enumerate(self.resolutions): w = math.ceil( (self.xmax - self.xmin) / (self.tileSize * res) ) h = math.ceil( (self.ymax - self.ymin) / (self.tileSize * res) ) query = """INSERT OR REPLACE INTO gpkg_tile_matrix ( table_name, zoom_level, matrix_width, matrix_height, tile_width, tile_height, pixel_x_size, pixel_y_size) VALUES (?, ?, ?, ?, ?, ?, ?, ?);""" db.execute(query, ('gpkg_tiles', level, w, h, self.tileSize, self.tileSize, res, res)) db.commit() db.close() def hasTile(self, x, y, z): if self.getTile(x ,y, z) is not None: return True else: return False def getTile(self, x, y, z): '''return tilde_data if tile exists otherwie return None''' #connect with detect_types parameter for automatically convert date to Python object db = sqlite3.connect(self.dbPath, detect_types=sqlite3.PARSE_DECLTYPES) query = 'SELECT tile_data, last_modified FROM gpkg_tiles WHERE zoom_level=? AND tile_column=? AND tile_row=?' result = db.execute(query, (z, x, y)).fetchone() db.close() if result is None: return None timeDelta = datetime.datetime.now() - result[1] if timeDelta.days > self.MAX_DAYS: return None return result[0] def putTile(self, x, y, z, data): db = sqlite3.connect(self.dbPath) query = """INSERT OR REPLACE INTO gpkg_tiles (tile_column, tile_row, zoom_level, tile_data) VALUES (?,?,?,?)""" db.execute(query, (x, y, z, data)) db.commit() db.close() def listExistingTiles(self, tiles): """ input : tiles list [(x,y,z)] output : tiles list set [(x,y,z)] of existing records in cache db""" db = sqlite3.connect(self.dbPath, detect_types=sqlite3.PARSE_DECLTYPES) # split out the axises x, y, z = zip(*tiles) query = "SELECT tile_column, tile_row, zoom_level FROM gpkg_tiles " \ "WHERE julianday() - julianday(last_modified) < ?" \ "AND zoom_level BETWEEN ? AND ? AND tile_column BETWEEN ? AND ? AND tile_row BETWEEN ? AND ?" result = db.execute( query, ( GeoPackage.MAX_DAYS, min(z), max(z), min(x), max(x), min(y), max(y) ) ).fetchall() db.close() return set(result) def listMissingTiles(self, tiles): existing = self.listExistingTiles(tiles) return set(tiles) - existing # difference def getTiles(self, tiles): """tiles = list of (x,y,z) tuple return list of (x,y,z,data) tuple""" db = sqlite3.connect(self.dbPath, detect_types=sqlite3.PARSE_DECLTYPES) # split out the axises x, y, z = zip(*tiles) query = "SELECT tile_column, tile_row, zoom_level, tile_data FROM gpkg_tiles " \ "WHERE julianday() - julianday(last_modified) < ?" \ "AND zoom_level BETWEEN ? AND ? AND tile_column BETWEEN ? AND ? AND tile_row BETWEEN ? AND ?" result = db.execute( query, ( GeoPackage.MAX_DAYS, min(z), max(z), min(x), max(x), min(y), max(y) ) ).fetchall() db.close() return result def putTiles(self, tiles): """tiles = list of (x,y,z,data) tuple""" db = sqlite3.connect(self.dbPath) query = """INSERT OR REPLACE INTO gpkg_tiles (tile_column, tile_row, zoom_level, tile_data) VALUES (?,?,?,?)""" db.executemany(query, tiles) db.commit() db.close() ================================================ FILE: core/basemaps/mapservice.py ================================================ # -*- coding:utf-8 -*- # ***** GPL LICENSE BLOCK ***** # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # All rights reserved. # ***** GPL LICENSE BLOCK ***** #built-in imports import logging log = logging.getLogger(__name__) import math import threading import queue import time import urllib.request from ..lib import imghdr import sys, time, os #core imports from .servicesDefs import GRIDS, SOURCES from .gpkg import GeoPackage from ..georaster import NpImage, GeoRef, BigTiffWriter from ..utils import BBOX from ..proj.reproj import reprojPt, reprojBbox, reprojImg from ..proj.ellps import dd2meters, meters2dd from ..proj.srs import SRS from .. import settings USER_AGENT = settings.user_agent TIMEOUT = 4 # Set mosaic backgroung image color, it will be the base color for area not covered # by the map service (ie when requests return non valid data) MOSAIC_BKG_COLOR = (128,128,128,255) EMPTY_TILE_COLOR = (255,192,203,255) #color for cached tile with empty data CORRUPTED_TILE_COLOR = (255,0,0,255) #color for cached tile which is non valid image data class TileMatrix(): """ Will inherit attributes from grid source definition "CRS" >> epsg code "bbox" >> (xmin, ymin, xmax, ymax) "bboxCRS" >> epsg code "tileSize" "originLoc" >> "NW" or SW "resFactor" "initRes" >> optional "nbLevels" >> optional or "resolutions" # Three ways to define a grid: # - submit a list of "resolutions" (This parameters override the others) # - submit "resFactor" and "initRes" # - submit just "resFactor" (initRes will be computed) """ defaultNbLevels = 24 def __init__(self, gridDef): #create class attributes from grid dictionnary for k, v in gridDef.items(): setattr(self, k, v) #Convert bbox to grid crs is needed if self.bboxCRS != self.CRS: #WARN here we assume crs is 4326, TODO lonMin, latMin, lonMax, latMax = self.bbox self.xmin, self.ymax = self.geoToProj(lonMin, latMax) self.xmax, self.ymin = self.geoToProj(lonMax, latMin) else: self.xmin, self.xmax = self.bbox[0], self.bbox[2] self.ymin, self.ymax = self.bbox[1], self.bbox[3] if not hasattr(self, 'resolutions'): #Set resFactor if not submited if not hasattr(self, 'resFactor'): self.resFactor = 2 #Set initial resolution if not submited if not hasattr(self, 'initRes'): # at zoom level zero, 1 tile covers whole bounding box dx = abs(self.xmax - self.xmin) dy = abs(self.ymax - self.ymin) dst = max(dx, dy) self.initRes = dst / self.tileSize #Set number of levels if not submited if not hasattr(self, 'nbLevels'): self.nbLevels = self.defaultNbLevels else: self.resolutions.sort(reverse=True) self.nbLevels = len(self.resolutions) # Define tile matrix origin if self.originLoc == "NW": self.originx, self.originy = self.xmin, self.ymax elif self.originLoc == "SW": self.originx, self.originy = self.xmin, self.ymin else: raise NotImplementedError #Determine unit of CRS (decimal degrees or meters) self.crs = SRS(self.CRS) if self.crs.isGeo: self.units = 'degrees' else: #(if units cannot be determined we assume its meters) self.units = 'meters' @property def globalbbox(self): return self.xmin, self.ymin, self.xmax, self.ymax def geoToProj(self, long, lat): """convert longitude latitude in decimal degrees to grid crs""" if self.CRS == 'EPSG:4326': return long, lat else: return reprojPt(4326, self.CRS, long, lat) def projToGeo(self, x, y): """convert grid crs coords to longitude latitude in decimal degrees""" if self.CRS == 'EPSG:4326': return x, y else: return reprojPt(self.CRS, 4326, x, y) def getResList(self): if hasattr(self, 'resolutions'): return self.resolutions else: return [self.initRes / self.resFactor**zoom for zoom in range(self.nbLevels)] def getRes(self, zoom): """Resolution (meters/pixel) for given zoom level (measured at Equator)""" if hasattr(self, 'resolutions'): if zoom > len(self.resolutions): zoom = len(self.resolutions) return self.resolutions[zoom] else: return self.initRes / self.resFactor**zoom def getNearestZoom(self, res, rule='closer'): """ Return the zoom level closest to the submited resolution rule in ['closer', 'lower', 'higher'] lower return the previous zoom level, higher return the next """ resLst = self.getResList() #ordered for z1, v1 in enumerate(resLst): if v1 == res: return z1 if z1 == len(resLst) - 1: return z1 z2 = z1+1 v2 = resLst[z2] if v2 == res: return z2 if v1 > res > v2: if rule == 'lower': return z1 elif rule == 'higher': return z2 else: #closer d1 = v1 - res d2 = res - v2 if d1 < d2: return z1 else: return z2 def getPrevResFac(self, z): """return res factor to previous zoom level""" return self.getFromToResFac(z, z-1) def getNextResFac(self, z): """return res factor to next zoom level""" return self.getFromToResFac(z, z+1) def getFromToResFac(self, z1, z2): """return res factor from z1 to z2""" if z1 == z2: return 1 if z1 < z2: if z2 >= self.nbLevels - 1: return 1 else: return self.getRes(z2) / self.getRes(z1) elif z1 > z2: if z2 <= 0: return 1 else: return self.getRes(z2) / self.getRes(z1) def getTileNumber(self, x, y, zoom): """Convert projeted coords to tiles number""" res = self.getRes(zoom) geoTileSize = self.tileSize * res dx = x - self.originx if self.originLoc == "NW": dy = self.originy - y else: dy = y - self.originy col = dx / geoTileSize row = dy / geoTileSize col = int(math.floor(col)) row = int(math.floor(row)) return col, row def getTileCoords(self, col, row, zoom): """ Convert tiles number to projeted coords (top left pixel if matrix origin is NW) """ res = self.getRes(zoom) geoTileSize = self.tileSize * res x = self.originx + (col * geoTileSize) if self.originLoc == "NW": y = self.originy - (row * geoTileSize) else: y = self.originy + (row * geoTileSize) #bottom left y += geoTileSize #top left return x, y def getTileBbox(self, col, row, zoom): xmin, ymax = self.getTileCoords(col, row, zoom) xmax = xmin + (self.tileSize * self.getRes(zoom)) ymin = ymax - (self.tileSize * self.getRes(zoom)) return xmin, ymin, xmax, ymax def bboxRequest(self, bbox, zoom): return BBoxRequest(self, bbox, zoom) class BBoxRequestMZ(): '''Multiple Zoom BBox request''' def __init__(self, tm, bbox, zooms): self.tm = tm self.bboxrequests = {} for z in zooms: self.bboxrequests[z] = BBoxRequest(tm, bbox, z) @property def tiles(self): tiles = [] for bboxrequest in self.bboxrequests.values(): tiles.extend(bboxrequest.tiles) return tiles @property def nbTiles(self): return len(self.tiles) def __getitem__(self, z): return self.bboxrequests[z] class BBoxRequest(): def __init__(self, tm, bbox, zoom): self.tm = tm self.zoom = zoom self.tileSize = tm.tileSize self.res = tm.getRes(zoom) xmin, ymin, xmax, ymax = bbox #Get first tile indices (top left of requested bbox) self.firstCol, self.firstRow = tm.getTileNumber(xmin, ymax, zoom) #correction of top left coord xmin, ymax = tm.getTileCoords(self.firstCol, self.firstRow, zoom) self.bbox = BBOX(xmin, ymin, xmax, ymax) #Total number of tiles required self.nbTilesX = math.ceil( (xmax - xmin) / (self.tileSize * self.res) ) self.nbTilesY = math.ceil( (ymax - ymin) / (self.tileSize * self.res) ) @property def cols(self): return [self.firstCol+i for i in range(self.nbTilesX)] @property def rows(self): if self.tm.originLoc == "NW": return [self.firstRow+i for i in range(self.nbTilesY)] else: return [self.firstRow-i for i in range(self.nbTilesY)] @property def tiles(self): return [(c, r, self.zoom) for c in self.cols for r in self.rows] @property def nbTiles(self): return self.nbTilesX * self.nbTilesY #megapixel, geosize class MapService(): """ Represent a tile service from source Will inherit attributes from source definition name description service >> 'WMS', 'TMS' or 'WMTS' grid >> key identifier of the tile matrix used by this source matrix >> for WMTS only, name of the matrix as refered in url quadTree >> boolean, for TMS only. Flag if tile coords are stord through a quadkey layers >> a list layers with the following attributes urlkey name description format >> 'jpeg' or 'png' style zmin & zmax urlTemplate referer Service status code 0 = no running tasks 1 = getting cache (create a new db if needed) 2 = downloading 3 = building mosaic 4 = reprojecting """ # resampling algo for reprojection RESAMP_ALG = 'BL' #NN:Nearest Neighboor, BL:Bilinear, CB:Cubic, CBS:Cubic Spline, LCZ:Lanczos def __init__(self, srckey, cacheFolder, dstGridKey=None): #create class attributes from source dictionnary self.srckey = srckey source = SOURCES[self.srckey] for k, v in source.items(): setattr(self, k, v) #Build objects from layers definitions class Layer(): pass layersObj = {} for layKey, layDict in self.layers.items(): lay = Layer() for k, v in layDict.items(): setattr(lay, k, v) layersObj[layKey] = lay self.layers = layersObj #Build source tile matrix set self.srcGridKey = self.grid self.srcTms = TileMatrix(GRIDS[self.srcGridKey]) #Build destination tile matrix set self.setDstGrid(dstGridKey) #Init cache dict self.cacheFolder = cacheFolder self.caches = {} #Fake browser header self.headers = { 'Accept' : 'image/png,image/*;q=0.8,*/*;q=0.5' , 'Accept-Charset' : 'ISO-8859-1,utf-8;q=0.7,*;q=0.7' , #'Accept-Encoding' : 'gzip,deflate', #urllib2 doesn't automatically uncompress the data 'Accept-Language' : 'fr,en-us,en;q=0.5' , #'Keep-Alive': 115 , 'Proxy-Connection' : 'keep-alive', 'User-Agent' : USER_AGENT, 'Referer' : self.referer} #Downloading progress self.running = False #flag using to stop getTiles() / getImage() process self.nbTiles = 0 self.cptTiles = 0 #codes that indicate the current status of the service self.status = 0 self.lock = threading.RLock() def reportLoop(self): msg = self.report while self.running: time.sleep(0.05) if self.report != msg: #sys.stdout.write("\033[F") #back to previous line sys.stdout.write("\033[K") #clear line sys.stdout.flush() print(self.report, end='\r') #'\r' will move the cursor back to the beginning of the line msg = self.report def start(self): self.running = True reporter = threading.Thread(target=self.reportLoop) reporter.setDaemon(True) #daemon threads will die when the main non-daemon thread have exited. reporter.start() def stop(self): self.running = False @property def report(self): if self.status == 0: return '' if self.status == 1: return 'Get cache database...' if self.status == 2: return 'Downloading... ' + str(self.cptTiles)+'/'+str(self.nbTiles) if self.status == 3: return 'Building mosaic...' if self.status == 4: return 'Reprojecting...' def setDstGrid(self, grdkey): '''Set destination tile matrix''' if grdkey is not None and grdkey != self.srcGridKey: self.dstGridKey = grdkey self.dstTms = TileMatrix(GRIDS[grdkey]) else: self.dstGridKey = None self.dstTms = None def getCache(self, laykey, useDstGrid): '''Return existing cache for requested layer or built it if not exists''' if useDstGrid: if self.dstGridKey is not None: grdkey = self.dstGridKey tm = self.dstTms else: raise ValueError('No destination grid defined') else: grdkey = self.srcGridKey tm = self.srcTms mapKey = self.srckey + '_' + laykey + '_' + grdkey cache = self.caches.get(mapKey) if cache is None: dbPath = os.path.join(self.cacheFolder, mapKey + ".gpkg") self.caches[mapKey] = GeoPackage(dbPath, tm) return self.caches[mapKey] else: return cache def getTM(self, dstGrid=False): if dstGrid: if self.dstTms is not None: return self.dstTms else: raise ValueError('No destination grid defined') else: return self.srcTms def buildUrl(self, laykey, col, row, zoom): """ Receive tiles coords in source tile matrix space and build request url """ url = self.urlTemplate lay = self.layers[laykey] tm = self.srcTms if self.service == 'TMS': url = url.replace("{LAY}", lay.urlKey) if not self.quadTree: url = url.replace("{X}", str(col)) url = url.replace("{Y}", str(row)) url = url.replace("{Z}", str(zoom)) else: quadkey = self.getQuadKey(col, row, zoom) url = url.replace("{QUADKEY}", quadkey) if self.service == 'WMTS': url = self.urlTemplate['BASE_URL'] if url[-1] != '?' : url += '?' params = ['='.join([k,v]) for k, v in self.urlTemplate.items() if k != 'BASE_URL'] url += '&'.join(params) url = url.replace("{LAY}", lay.urlKey) url = url.replace("{FORMAT}", lay.format) url = url.replace("{STYLE}", lay.style) url = url.replace("{MATRIX}", self.matrix) url = url.replace("{X}", str(col)) url = url.replace("{Y}", str(row)) url = url.replace("{Z}", str(zoom)) if self.service == 'WMS': url = self.urlTemplate['BASE_URL'] if url[-1] != '?' : url += '?' params = ['='.join([k,v]) for k, v in self.urlTemplate.items() if k != 'BASE_URL'] url += '&'.join(params) url = url.replace("{LAY}", lay.urlKey) url = url.replace("{FORMAT}", lay.format) url = url.replace("{STYLE}", lay.style) url = url.replace("{CRS}", str(tm.CRS)) url = url.replace("{WIDTH}", str(tm.tileSize)) url = url.replace("{HEIGHT}", str(tm.tileSize)) xmin, ymax = tm.getTileCoords(col, row, zoom) xmax = xmin + tm.tileSize * tm.getRes(zoom) ymin = ymax - tm.tileSize * tm.getRes(zoom) if self.urlTemplate['VERSION'] == '1.3.0' and tm.CRS == 'EPSG:4326': bbox = ','.join(map(str,[ymin,xmin,ymax,xmax])) else: bbox = ','.join(map(str,[xmin,ymin,xmax,ymax])) url = url.replace("{BBOX}", bbox) return url def getQuadKey(self, x, y, z): "Converts TMS tile coordinates to Microsoft QuadTree" quadKey = "" for i in range(z, 0, -1): digit = 0 mask = 1 << (i-1) if (x & mask) != 0: digit += 1 if (y & mask) != 0: digit += 2 quadKey += str(digit) return quadKey def isTileInMapsBounds(self, col, row, zoom, tm): '''Test if the tile is not out of tile matrix bounds''' x,y = tm.getTileCoords(col, row, zoom) #top left if row < 0 or col < 0: return False elif not tm.xmin <= x < tm.xmax or not tm.ymin < y <= tm.ymax: return False else: return True def downloadTile(self, laykey, col, row, zoom): """ Download bytes data of requested tile in source tile matrix space Return None if unable to download a valid stream """ url = self.buildUrl(laykey, col, row, zoom) log.debug(url) try: #make request req = urllib.request.Request(url, None, self.headers) handle = urllib.request.urlopen(req, timeout=TIMEOUT) #open image stream data = handle.read() handle.close() except Exception as e: log.error("Can't download tile x{} y{}. Error {}".format(col, row, e)) data = None #Make sure the stream is correct if data is not None: format = imghdr.what(None, data) if format is None: data = None if data is None: log.debug("Invalid tile data for request {}".format(url)) return data def tileRequest(self, laykey, col, row, zoom, toDstGrid=True): """ Return bytes data of the requested tile or None if unable to get valid data Tile is downloaded from map service and, if needed, reprojected to fit the destination grid """ #Select tile matrix set tm = self.getTM(toDstGrid) #don't try to get tiles out of map bounds if not self.isTileInMapsBounds(col, row, zoom, tm): return None if not toDstGrid: data = self.downloadTile(laykey, col, row, zoom) else: data = self.buildDstTile(laykey, col, row, zoom) return data def buildDstTile(self, laykey, col, row, zoom): '''build a tile that fit the destination tile matrix''' #get tile bbox bbox = self.dstTms.getTileBbox(col, row, zoom) xmin, ymin, xmax, ymax = bbox #get closest zoom level res = self.dstTms.getRes(zoom) if self.dstTms.units == 'degrees' and self.srcTms.units == 'meters': res2 = dd2meters(res) elif self.srcTms.units == 'degrees' and self.dstTms.units == 'meters': res2 = meters2dd(res) else: res2 = res _zoom = self.srcTms.getNearestZoom(res2) _res = self.srcTms.getRes(_zoom) #reproj bbox crs1, crs2 = self.srcTms.CRS, self.dstTms.CRS try: _bbox = reprojBbox(crs2, crs1, bbox) except Exception as e: log.warning('Cannot reproj tile bbox - ' + str(e)) return None #list, download and merge the tiles required to build this one (recursive call) mosaic = self.getImage(laykey, _bbox, _zoom, toDstGrid=False, nbThread=4, cpt=False) if mosaic is None: return None #Reprojection tileSize = self.dstTms.tileSize img = NpImage(reprojImg(crs1, crs2, mosaic.toGDAL(), out_ul=(xmin,ymax), out_size=(tileSize,tileSize), out_res=res, sqPx=True, resamplAlg=self.RESAMP_ALG)) return img.toBLOB() def seedTiles(self, laykey, tiles, toDstGrid=True, nbThread=10, buffSize=5000, cpt=True): """ Seed the cache by downloading the requested tiles from map service Downloads are performed through thread to speed up buffSize : maximum number of tiles keeped in memory before put them in cache database """ def downloading(laykey, tilesQueue, tilesData, toDstGrid): '''Worker that process the queue and seed tilesData array [(x,y,z,data)]''' #infinite loop that processes items into the queue while not tilesQueue.empty(): #empty is True if all item was get but it not tell if all task was done #cancel thread if requested if not self.running: break #Get a job into the queue col, row, zoom = tilesQueue.get() #get() pop the item from queue #do the job data = self.tileRequest(laykey, col, row, zoom, toDstGrid) if data is not None: tilesData.put( (col, row, zoom, data) ) #will block if the queue is full if cpt: self.cptTiles += 1 #self.nTaskDone += 1 #flag it's done tilesQueue.task_done() #it's just a count of finished tasks used by join() to know if the work is finished def finished(): #return self.nTaskDone == nMissing #self.nTaskDone is not reliable because the recursive call to getImage will #start multiple threads to seedTiles() and all these process will increments nTaskDone return not any([t.is_alive() for t in threads]) def putInCache(tilesData, jobs, cache): while True: if tilesData.full() or \ ( (finished() or not self.running) and not tilesData.empty()): data = [tilesData.get() for i in range(tilesData.qsize())] with self.lock: cache.putTiles(data) if finished() and tilesData.empty(): break if not self.running: break if cpt: #init cpt progress self.nbTiles = len(tiles) self.cptTiles = 0 #self.nTaskDone = 0 #Get cache db if cpt: self.status = 1 cache = self.getCache(laykey, toDstGrid) missing = cache.listMissingTiles(tiles) nMissing = len(missing) nExists = self.nbTiles - len(missing) log.debug("{} tiles requested, {} already in cache, {} remains to download".format(self.nbTiles, nExists, nMissing)) if cpt: self.cptTiles += nExists #Downloading tiles if cpt: self.status = 2 if len(missing) > 0: #Result queue tilesData = queue.Queue(maxsize=buffSize) #Seed the queue jobs = queue.Queue() for tile in missing: jobs.put(tile) #Launch threads threads = [] for i in range(nbThread): t = threading.Thread(target=downloading, args=(laykey, jobs, tilesData, toDstGrid)) t.setDaemon(True) threads.append(t) t.start() seeder = threading.Thread(target=putInCache, args=(tilesData, jobs, cache)) seeder.setDaemon(True) seeder.start() seeder.join() #Make sure all threads has finished for t in threads: t.join() #Reinit status and cpt progress if cpt: self.status = 0 self.nbTiles, self.cptTiles = 0, 0 def getTiles(self, laykey, tiles, toDstGrid=True, nbThread=10, cpt=True): """ Return bytes data of requested tiles input: [(x,y,z)] >> output: [(x,y,z,data)] Tiles are downloaded from map service or directly pick up from cache database. """ #seed the cache self.seedTiles(laykey, tiles, toDstGrid=toDstGrid, nbThread=10, cpt=cpt) #request the cache and return cache = self.getCache(laykey, toDstGrid) return cache.getTiles(tiles) #[(x,y,z,data)] def getTile(self, laykey, col, row, zoom, toDstGrid=True): return self.getTiles(laykey, [col, row, zoom], toDstGrid)[0] def bboxRequest(self, bbox, zoom, dstGrid=True): #Select tile matrix set tm = self.getTM(dstGrid) return BBoxRequest(tm, bbox, zoom) def seedCache(self, laykey, bbox, zoom, toDstGrid=True, nbThread=10, buffSize=5000): """ Seed the cache with the tiles covering the requested bbox """ #Select tile matrix set tm = self.getTM(toDstGrid) if isinstance(zoom, list): rq = BBoxRequestMZ(tm, bbox, zoom) else: rq = BBoxRequest(tm, bbox, zoom) self.seedTiles(laykey, rq.tiles, toDstGrid=toDstGrid, nbThread=10, buffSize=5000) def getImage(self, laykey, bbox, zoom, path=None, bigTiff=False, outCRS=None, toDstGrid=True, nbThread=10, cpt=True): """ Build a mosaic of tiles covering the requested bounding box #laykey (str) #bbox #zoom (int) #path (str): if None the function will return a georeferenced NpImage object. If not None, then the resulting output will be writen as geotif file on disk and the function will return None #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 writen at one, in this case all the tiles must fit in memory otherwise it will raise a memory overflow error #outCRS : destination CRS if a reprojection if expected (require GDAL support) #toDstGrid (bool) : decide if the function will seed the destination tile matrix sets for this MapService instance (different from the source tile matrix set) #nbThread (int) : nimber of threads that will be used for downloading tiles #cpt (bool) : define if the service must report or not tiles downloading count for this request """ #Select tile matrix set tm = self.getTM(toDstGrid) #Get request rq = BBoxRequest(tm, bbox, zoom) tileSize = rq.tileSize res = rq.res cols, rows = rq.cols, rq.rows rqTiles = rq.tiles #[(x,y,z)] ##method 1) Seed the cache with all required tiles self.seedCache(laykey, bbox, zoom, toDstGrid=toDstGrid, nbThread=nbThread, buffSize=5000) cache = self.getCache(laykey, toDstGrid) if not self.running: if cpt: self.status = 0 return #Get georef parameters img_w, img_h = len(cols) * tileSize, len(rows) * tileSize xmin, ymin, xmax, ymax = rq.bbox georef = GeoRef((img_w, img_h), (res, -res), (xmin, ymax), pxCenter=False, crs=tm.crs) if bigTiff and path is None: raise ValueError('No output path defined for creating bigTiff') if not bigTiff: #Create numpy image in memory mosaic = NpImage.new(img_w, img_h, bkgColor=MOSAIC_BKG_COLOR, georef=georef) chunkSize = rq.nbTiles else: #Create bigtiff file on disk mosaic = BigTiffWriter(path, img_w, img_h, georef) ds = mosaic.ds chunkSize = 5 #number of tiles to extract in one cache request #Build mosaic for i in range(0, rq.nbTiles, chunkSize): chunkTiles = rqTiles[i:i+chunkSize] ##method 1) Get cached tiles tiles = cache.getTiles(chunkTiles) #[(x,y,z,data)] ##method 2) Get tiles from www or cache (all tiles must fit in memory) #tiles = self.getTiles(laykey, chunkTiles, toDstGrid, nbThread, cpt) if cpt: self.status = 3 for tile in tiles: if not self.running: if cpt: self.status = 0 return None col, row, z, data = tile #TODO corrupted or empty tiles must be deleted from cache are fetched again if data is None: #create an empty tile img = NpImage.new(tileSize, tileSize, bkgColor=EMPTY_TILE_COLOR) else: try: img = NpImage(data) except Exception as e: log.error('Corrupted tile on cache', exc_info=True) #create an empty tile if we are unable to get a valid stream img = NpImage.new(tileSize, tileSize, bkgColor=CORRUPTED_TILE_COLOR) posx = (col - rq.firstCol) * tileSize posy = abs((row - rq.firstRow)) * tileSize mosaic.paste(img, posx, posy) if not self.running: if cpt: self.status = 0 return None #Reproject if needed if outCRS is not None and outCRS != tm.CRS: if cpt: self.status = 4 time.sleep(0.1) #make sure client have enough time to get the new status... if not bigTiff: mosaic = NpImage(reprojImg(tm.CRS, outCRS, mosaic.toGDAL(), sqPx=True, resamplAlg=self.RESAMP_ALG)) else: outPath = path[:-4] + '_' + str(outCRS) + '.tif' ds = reprojImg(tm.CRS, outCRS, mosaic.ds, sqPx=True, resamplAlg=self.RESAMP_ALG, path=outPath) #build overviews for file output if bigTiff: ds.BuildOverviews(overviewlist=[2,4,8,16,32]) ds = None if not bigTiff and path is not None: mosaic.save(path) #Finish if cpt: self.status = 0 if path is None: return mosaic else: return None ================================================ FILE: core/basemaps/servicesDefs.py ================================================ # -*- coding:utf-8 -*- import math #################################### # Tiles maxtrix definitions #################################### # Three ways to define a grid (inpired by http://mapproxy.org/docs/1.8.0/configuration.html#id6): # - submit a list of resolutions > "resolutions": [32,16,8,4] (This parameters override the others) # - submit just "resFactor", initial res is computed such as at zoom level zero, 1 tile covers whole bounding box # - submit "resFactor" and "initRes" # About Web Mercator # Technically, the Mercator projection is defined for any latitude up to (but not including) # 90 degrees, but it makes sense to cut it off sooner because it grows exponentially with # increasing latitude. The logic behind this particular cutoff value, which is the one used # by Google Maps, is that it makes the projection square. That is, the rectangle is equal in # the X and Y directions. In this case the maximum latitude attained must correspond to y = w/2. # y = 2*pi*R / 2 = pi*R --> y/R = pi # lat = atan(sinh(y/R)) = atan(sinh(pi)) # wm_origin = (-20037508, 20037508) with 20037508 = GRS80.perimeter / 2 cutoff_lat = math.atan(math.sinh(math.pi)) * 180/math.pi #= 85.05112° GRIDS = { "WM" : { "name" : 'Web Mercator', "description" : 'Global grid in web mercator projection', "CRS": 'EPSG:3857', "bbox": [-180, -cutoff_lat, 180, cutoff_lat], #w,s,e,n "bboxCRS": 'EPSG:4326', #"bbox": [-20037508, -20037508, 20037508, 20037508], #"bboxCRS": 3857, "tileSize": 256, "originLoc": "NW", #North West or South West "resFactor" : 2 }, "WGS84" : { "name" : 'WGS84', "description" : 'Global grid in wgs84 projection', "CRS": 'EPSG:4326', "bbox": [-180, -90, 180, 90], #w,s,e,n "bboxCRS": 'EPSG:4326', "tileSize": 256, "originLoc": "NW", #North West or South West "resFactor" : 2 }, #this one produce valid MBtiles files, because origin is bottom left "WM_SW" : { "name" : 'Web Mercator TMS', "description" : 'Global grid in web mercator projection, origin South West', "CRS": 'EPSG:3857', "bbox": [-180, -cutoff_lat, 180, cutoff_lat], #w,s,e,n "bboxCRS": 'EPSG:4326', #"bbox": [-20037508, -20037508, 20037508, 20037508], #"bboxCRS": 'EPSG:3857', "tileSize": 256, "originLoc": "SW", #North West or South West "resFactor" : 2 }, ##################### #Custom grid example ###################### # >> France Lambert 93 "LB93" : { "name" : 'Fr Lambert 93', "description" : 'Local grid in French Lambert 93 projection', "CRS": 'EPSG:2154', "bbox": [99200, 6049600, 1242500, 7110500], #w,s,e,n "bboxCRS": 'EPSG:2154', "tileSize": 256, "originLoc": "NW", #North West or South West "resFactor" : 2 }, # >> Another France Lambert 93 (submited list of resolution) "LB93_2" : { "name" : 'Fr Lambert 93 v2', "description" : 'Local grid in French Lambert 93 projection', "CRS": 'EPSG:2154', "bbox": [99200, 6049600, 1242500, 7110500], #w,s,e,n "bboxCRS": 'EPSG:2154', "tileSize": 256, "originLoc": "SW", #North West or South West "resolutions" : [4000, 2000, 1000, 500, 250, 100, 50, 25, 10, 5, 2, 1, 0.5, 0.25, 0.1] #15 levels }, # >> France Lambert 93 used by CRAIG WMTS # WMTS resolution = ScaleDenominator * 0.00028 # (0.28 mm = physical distance of a pixel (WMTS assumes a DPI 90.7) "LB93_CRAIG" : { "name" : 'Fr Lambert 93 CRAIG', "description" : 'Local grid in French Lambert 93 projection', "CRS": 'EPSG:2154', "bbox": [-357823.23, 6037001.46, 1313634.34, 7230727.37], #w,s,e,n "bboxCRS": 'EPSG:2154', "tileSize": 256, "originLoc": "NW", "initRes": 1354.666, "resFactor" : 2 }, } #################################### # Sources definitions #################################### #With TMS or WMTS, grid must match the one used by the service #With WMS you can use any grid you want but the grid CRS must #match one of those provided by the WMS service #The grid associated to the source define the CRS #A source can have multiple layers but have only one grid #so to support multiple grid it's necessary to duplicate source definition SOURCES = { ############### # TMS examples ############### "GOOGLE" : { "name" : 'Google', "description" : 'Google map', "service": 'TMS', "grid": 'WM', "quadTree": False, "layers" : { "SAT" : {"urlKey" : 's', "name" : 'Satellite', "description" : '', "format" : 'jpeg', "zmin" : 0, "zmax" : 22}, "MAP" : {"urlKey" : 'm', "name" : 'Map', "description" : '', "format" : 'png', "zmin" : 0, "zmax" : 22} }, "urlTemplate": "http://mt0.google.com/vt/lyrs={LAY}&x={X}&y={Y}&z={Z}", "referer": "https://www.google.com/maps" }, "OSM" : { "name" : 'OSM', "description" : 'Open Street Map', "service": 'TMS', "grid": 'WM', "quadTree": False, "layers" : { "MAPNIK" : {"urlKey" : '', "name" : 'Mapnik', "description" : '', "format" : 'png', "zmin" : 0, "zmax" : 19} }, "urlTemplate": "https://tile.openstreetmap.org/{Z}/{X}/{Y}.png", "referer": "" #https://www.openstreetmap.org will return 418 error }, "BING" : { "name" : 'Bing', "description" : 'Microsoft Bing Map', "service": 'TMS', "grid": 'WM', "quadTree": True, "layers" : { "SAT" : {"urlKey" : 'A', "name" : 'Satellite', "description" : '', "format" : 'jpeg', "zmin" : 0, "zmax" : 22}, "MAP" : {"urlKey" : 'G', "name" : 'Map', "description" : '', "format" : 'png', "zmin" : 0, "zmax" : 22} }, "urlTemplate": "http://ak.dynamic.t0.tiles.virtualearth.net/comp/ch/{QUADKEY}?it={LAY}", "referer": "http://www.bing.com/maps" }, "ESRI" : { "name" : 'Esri', "description" : 'Esri ArcGIS', "service": 'TMS', "grid": 'WM', "quadTree": False, "layers" : { "AERIAL" : {"urlKey" : 'World_Imagery', "name" : 'Aerial', "description" : '', "format" : 'jpeg', "zmin" : 0, "zmax" : 23}, "NATGEO" : {"urlKey" : 'NatGeo_World_Map', "name" : 'National Geographic', "description" : '', "format" : 'jpeg', "zmin" : 0, "zmax" : 16}, "USATOPO" : {"urlKey" : 'USA_Topo_Maps', "name" : 'USA Topo', "description" : '', "format" : 'jpeg', "zmin" : 0, "zmax" : 15}, "PHYSICAL" : {"urlKey" : 'World_Physical_Map', "name" : 'Physical', "description" : '', "format" : 'jpeg', "zmin" : 0, "zmax" : 8}, "RELIEF" : {"urlKey" : 'World_Shaded_Relief', "name" : 'Shaded Relief', "description" : '', "format" : 'jpeg', "zmin" : 0, "zmax" : 13}, "STREET" : {"urlKey" : 'World_Street_Map', "name" : 'Street Map', "description" : '', "format" : 'jpeg', "zmin" : 0, "zmax" : 23}, "TOPO" : {"urlKey" : 'World_Topo_Map', "name" : 'Topo with relief', "description" : '', "format" : 'jpeg', "zmin" : 0, "zmax" : 23}, "TERRAINB" : {"urlKey" : 'World_Terrain_Base', "name" : 'Terrain Base', "description" : '', "format" : 'jpeg', "zmin" : 0, "zmax" : 13}, "CANVASLIGHTB" : {"urlKey" : 'Canvas/World_Light_Gray_Base', "name" : 'Canvas Light Gray Base', "description" : '', "format" : 'jpeg', "zmin" : 0, "zmax" : 23}, "CANVASDARKB" : {"urlKey" : 'Canvas/World_Dark_Gray_Base', "name" : 'Canvas Dark Gray Base', "description" : '', "format" : 'jpeg', "zmin" : 0, "zmax" : 23}, "OCEANB" : {"urlKey" : 'Ocean/World_Ocean_Base', "name" : 'Ocean Base', "description" : '', "format" : 'jpeg', "zmin" : 0, "zmax" : 23} }, "urlTemplate": "https://server.arcgisonline.com/ArcGIS/rest/services/{LAY}/MapServer/tile/{Z}/{Y}/{X}", "referer": "https://server.arcgisonline.com/arcgis/rest/services" }, ############### # WMS examples ############### #with WMS you can set source grid as you want, the only condition is that the grid #crs must match one on crs provided by WMS "OSM_WMS" : { "name" : 'OSM WMS', "description" : 'Open Street Map WMS', "service": 'WMS', "grid": 'WM', "layers" : { "WRLD" : {"urlKey" : 'osm_auto:all', "name" : 'WMS', "description" : '', "format" : 'png', "style" : '', "zmin" : 0, "zmax" : 20} }, "urlTemplate": { "BASE_URL" : ' http://maps.heigit.org/osm-wms/service?', "SERVICE" : 'WMS', "VERSION" : '1.1.1', "REQUEST" : 'GetMap', "SRS" : '{CRS}', #EPSG:xxxx "LAYERS" : '{LAY}', "FORMAT" : 'image/{FORMAT}', "STYLES" : '{STYLE}', "BBOX" : '{BBOX}', #xmin,ymin,xmax,ymax, in "SRS" projection "WIDTH" : '{WIDTH}', "HEIGHT" : '{HEIGHT}', "TRANSPARENT" : "False" }, "referer": "http://www.osm-wms.de/" }, "GEOPORTAIL" : { "name" : 'Geoportail', "description" : 'Geoportail.fr', "service": 'WMTS', "grid": 'WM', "matrix" : 'PM', "layers" : { "ORTHO" : {"urlKey" : 'ORTHOIMAGERY.ORTHOPHOTOS', "name" : 'Orthophotos', "description" : '', "format" : 'jpeg', "style" : 'normal', "zmin" : 0, "zmax" : 22}, "CAD" : {"urlKey" : 'CADASTRALPARCELS.PARCELS', "name" : 'Cadastre', "description" : '', "format" : 'png', "style" : 'bdparcellaire', "zmin" : 0, "zmax" : 22} }, "urlTemplate": { "BASE_URL" : 'https://data.geopf.fr/wmts?', "SERVICE" : 'WMTS', "VERSION" : '1.0.0', "REQUEST" : 'GetTile', "LAYER" : '{LAY}', "STYLE" : '{STYLE}', "FORMAT" : 'image/{FORMAT}', "TILEMATRIXSET" : '{MATRIX}', "TILEMATRIX" : '{Z}', "TILEROW" : '{Y}', "TILECOL" : '{X}' }, "referer": "http://www.geoportail.gouv.fr/accueil" }, "GEOPORTAIL2" : { "name" : 'Geoportail ©scan', "description" : 'Geoportail.fr', "service": 'WMTS', "grid": 'WM', "matrix" : 'PM', "layers" : { "SCAN25" : {"urlKey" : 'GEOGRAPHICALGRIDSYSTEMS.MAPS.SCAN25TOUR', "name" : 'Scan25', "description" : '', "format" : 'jpeg', "style" : 'normal', "zmin" : 0, "zmax" : 22}, "SCAN" : {"urlKey" : 'GEOGRAPHICALGRIDSYSTEMS.MAPS', "name" : 'Scan', "description" : '', "format" : 'jpeg', "style" : 'normal', "zmin" : 0, "zmax" : 22} }, "urlTemplate": { "BASE_URL" : 'https://data.geopf.fr/private/wmts?', "SERVICE" : 'WMTS', "VERSION" : '1.0.0', "REQUEST" : 'GetTile', "LAYER" : '{LAY}', "STYLE" : '{STYLE}', "FORMAT" : 'image/{FORMAT}', "TILEMATRIXSET" : '{MATRIX}', "TILEMATRIX" : '{Z}', "TILEROW" : '{Y}', "TILECOL" : '{X}', "apikey" : "ign_scan_ws" }, "referer": "http://www.geoportail.gouv.fr/accueil" } } """ #http://wms.craig.fr/ortho?SERVICE=WMS&REQUEST=GetCapabilities # example of valid location in Auvergne : lat 45.77 long 3.082 "CRAIG_WMS" : { "name" : 'CRAIG WMS', "description" : "Centre Régional Auvergnat de l'Information Géographique", "service": 'WMS', "grid": 'LB93', "layers" : { "ORTHO" : {"urlKey" : 'auvergne', "name" : 'Auv25cm_2013', "description" : '', "format" : 'png', "style" : 'default', "zmin" : 0, "zmax" : 22} }, "urlTemplate": { "BASE_URL" : 'http://wms.craig.fr/ortho?', "SERVICE" : 'WMS', "VERSION" : '1.3.0', "REQUEST" : 'GetMap', "CRS" : '{CRS}', "LAYERS" : '{LAY}', "FORMAT" : 'image/{FORMAT}', "STYLES" : '{STYLE}', "BBOX" : '{BBOX}', #xmin,ymin,xmax,ymax, in "SRS" projection "WIDTH" : '{WIDTH}', "HEIGHT" : '{HEIGHT}', "TRANSPARENT" : "False" }, "referer": "http://www.craig.fr/" }, ############### # WMTS examples ############### # http://tiles.craig.fr/ortho/service?service=WMTS&REQUEST=GetCapabilities # example of valid location in Auvergne : lat 45.77 long 3.082 "CRAIG_WMTS93" : { "name" : 'CRAIG WMTS93', "description" : "Centre Régional Auvergnat de l'Information Géographique", "service": 'WMTS', "grid": 'LB93_CRAIG', "matrix" : 'lambert93', "layers" : { "ORTHO" : {"urlKey" : 'ortho_2013', "name" : 'Auv25cm_2013', "description" : '', "format" : 'jpeg', "style" : 'default', "zmin" : 0, "zmax" : 15} }, "urlTemplate": { "BASE_URL" : 'http://tiles.craig.fr/ortho/service?', "SERVICE" : 'WMTS', "VERSION" : '1.0.0', "REQUEST" : 'GetTile', "LAYER" : '{LAY}', "STYLE" : '{STYLE}', "FORMAT" : 'image/{FORMAT}', "TILEMATRIXSET" : '{MATRIX}', "TILEMATRIX" : '{Z}', "TILEROW" : '{Y}', "TILECOL" : '{X}' }, "referer": "http://www.craig.fr/" }, } """ ================================================ FILE: core/checkdeps.py ================================================ import logging log = logging.getLogger(__name__) #GDAL try: from osgeo import gdal except: HAS_GDAL = False log.debug('GDAL Python binding unavailable') else: HAS_GDAL = True log.debug('GDAL Python binding available') #PyProj try: import pyproj except: HAS_PYPROJ = False log.debug('PyProj unavailable') else: HAS_PYPROJ = True log.debug('PyProj available') #PIL/Pillow try: from PIL import Image except: HAS_PIL = False log.debug('Pillow unavailable') else: HAS_PIL = True log.debug('Pillow available') #Imageio freeimage plugin try: from .lib import imageio imageio.plugins._freeimage.get_freeimage_lib() #try to download freeimage lib except Exception as e: log.error("Cannot install ImageIO's Freeimage plugin", exc_info=True) HAS_IMGIO = False else: HAS_IMGIO = True log.debug('ImageIO Freeimage plugin available') ================================================ FILE: core/errors.py ================================================ class OverlapError(Exception): def __init__(self): pass def __str__(self): return "Non overlap data" class ReprojError(Exception): def __init__(self, value): self.value = value def __str__(self): return repr(self.value) class ApiKeyError(Exception): def __init__(self): pass def __str__(self): return "Missing or wrong API key" ================================================ FILE: core/georaster/__init__.py ================================================ from .georef import GeoRef from .georaster import GeoRaster from .npimg import NpImage from .bigtiffwriter import BigTiffWriter from .img_utils import getImgFormat, getImgDim, isValidStream ================================================ FILE: core/georaster/bigtiffwriter.py ================================================ # -*- coding:utf-8 -*- # This file is part of BlenderGIS # ***** GPL LICENSE BLOCK ***** # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # All rights reserved. # ***** GPL LICENSE BLOCK ***** import os import numpy as np from .npimg import NpImage from ..checkdeps import HAS_GDAL, HAS_PIL, HAS_IMGIO if HAS_GDAL: from osgeo import gdal class BigTiffWriter(): ''' This class is designed to write a bigtif with jpeg compression writing a large tiff file without trigger a memory overflow is possible with the help of GDAL library jpeg compression allows to maintain a reasonable file size transparency or nodata are stored in an internal tiff mask because it's not possible to have an alpha channel when using jpg compression ''' def __del__(self): # properly close gdal dataset self.ds = None def __init__(self, path, w, h, georef, geoTiffOptions={'TFW':'YES', 'TILED':'YES', 'BIGTIFF':'YES', 'COMPRESS':'JPEG', 'JPEG_QUALITY':80, 'PHOTOMETRIC':'YCBCR'}): ''' path = fule system path for the ouput tiff w, h = width and height in pixels georef : a Georef object used to set georeferencing informations, optional geoTiffOptions : GDAL create option for tiff format ''' if not HAS_GDAL: raise ImportError("GDAL interface unavailable") #control path validity self.w = w self.h = h self.size = (w, h) self.path = path self.georef = georef if geoTiffOptions.get('COMPRESS', None) == 'JPEG': #JPEG in tiff cannot have an alpha band, workaround is to use internal tiff mask self.useMask = True gdal.SetConfigOption('GDAL_TIFF_INTERNAL_MASK', 'YES') n = 3 #RGB else: self.useMask = False n = 4 #RGBA self.nbBands = n options = [str(k) + '=' + str(v) for k, v in geoTiffOptions.items()] driver = gdal.GetDriverByName("GTiff") gdtype = gdal.GDT_Byte #GDT_UInt16, GDT_Int16, GDT_UInt32, GDT_Int32 self.dtype = 'uint8' self.ds = driver.Create(path, w, h, n, gdtype, options) if self.useMask: self.ds.CreateMaskBand(gdal.GMF_PER_DATASET)#The mask band is shared between all bands on the dataset self.mask = self.ds.GetRasterBand(1).GetMaskBand() self.mask.Fill(255) elif n == 4: self.ds.GetRasterBand(4).Fill(255) #Write georef infos self.ds.SetGeoTransform(self.georef.toGDAL()) if self.georef.crs is not None: self.ds.SetProjection(self.georef.crs.getOgrSpatialRef().ExportToWkt()) #self.georef.toWorldFile(os.path.splitext(path)[0] + '.tfw') def paste(self, data, x, y): '''data = numpy array or NpImg''' img = NpImage(data) data = img.data #Write RGB for bandIdx in range(3): #writearray is available only at band level bandArray = data[:,:,bandIdx] self.ds.GetRasterBand(bandIdx+1).WriteArray(bandArray, x, y) #Process alpha hasAlpha = data.shape[2] == 4 if hasAlpha: alpha = data[:,:,3] if self.useMask: self.mask.WriteArray(alpha, x, y) else: self.ds.GetRasterBand(4).WriteArray(alpha, x, y) else: pass # replaced by fill method ''' #make alpha band or internal mask fully opaque h, w = data.shape[0], data.shape[1] alpha = np.full((h, w), 255, np.uint8) if self.useMask: self.mask.WriteArray(alpha, x, y) else: self.ds.GetRasterBand(4).WriteArray(alpha, x, y) ''' def __repr__(self): return '\n'.join([ "* Data infos :", " size {}".format(self.size), " type {}".format(self.dtype), " number of bands {}".format(self.nbBands), "* Georef & Geometry : \n{}".format(self.georef) ]) ================================================ FILE: core/georaster/georaster.py ================================================ # -*- coding:utf-8 -*- # This file is part of BlenderGIS # ***** GPL LICENSE BLOCK ***** # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # All rights reserved. # ***** GPL LICENSE BLOCK ***** import os import logging log = logging.getLogger(__name__) from ..lib import Tyf #geotags reader from .georef import GeoRef from .npimg import NpImage from .img_utils import getImgFormat, getImgDim from ..utils import XY as xy from ..errors import OverlapError from ..checkdeps import HAS_GDAL if HAS_GDAL: from osgeo import gdal class GeoRaster(): '''A class to represent a georaster file''' def __init__(self, path, subBoxGeo=None, useGDAL=False): ''' subBoxGeo : a BBOX object in CRS coordinate space useGDAL : use GDAL (if available) for extract raster informations ''' self.path = path self.wfPath = self._getWfPath() self.format = None #image file format (jpeg, tiff, png ...) self.size = None #raster dimension (width, height) in pixel self.depth = None #8, 16, 32 self.dtype = None #int, uint, float self.nbBands = None #number of bands self.noData = None self.georef = None if not useGDAL or not HAS_GDAL: self.format = getImgFormat(path) if self.format not in ['TIFF', 'BMP', 'PNG', 'JPEG', 'JPEG2000']: raise IOError("Unsupported format {}".format(self.format)) if self.isTiff: self._fromTIFF() if not self.isGeoref and self.hasWorldFile: self.georef = GeoRef.fromWorldFile(self.wfPath, self.size) else: pass else: # Try to read file header w, h = getImgDim(self.path) if w is None or h is None: raise IOError("Unable to read raster size") else: self.size = xy(w, h) #georef if self.hasWorldFile: self.georef = GeoRef.fromWorldFile(self.wfPath, self.size) #TODO add function to extract dtype, nBands & depth from jpg, png, bmp or jpeg2000 else: self._fromGDAL() if not self.isGeoref: raise IOError("Unable to read georef infos from worldfile or geotiff tags") if subBoxGeo is not None: self.georef.setSubBoxGeo(subBoxGeo) #GeoGef delegation by composition instead of inheritance #this special method is called whenever the requested attribute or method is not found in the object def __getattr__(self, attr): return getattr(self.georef, attr) ############################################ # Initialization Helpers ############################################ def _getWfPath(self): '''Try to find a worlfile path for this raster''' ext = self.path[-3:].lower() extTest = [] extTest.append(ext[0] + ext[2] +'w')# tfx, jgw, pgw ... extTest.append(extTest[0]+'x')# tfwx extTest.append(ext+'w')# tifw extTest.append('wld')#*.wld extTest.extend( [ext.upper() for ext in extTest] ) for wfExt in extTest: pathTest = self.path[0:len(self.path)-3] + wfExt if os.path.isfile(pathTest): return pathTest return None def _fromTIFF(self): '''Use Tyf to extract raster infos from geotiff tags''' if not self.isTiff or not self.fileExists: return tif = Tyf.open(self.path)[0] #Warning : Tyf object does not support k in dict test syntax nor get() method, use try block instead self.size = xy(tif['ImageWidth'], tif['ImageLength']) self.nbBands = tif['SamplesPerPixel'] self.depth = tif['BitsPerSample'] if self.nbBands > 1: self.depth = self.depth[0] sampleFormatMap = {1:'uint', 2:'int', 3:'float', None:'uint', 6:'complex'} try: self.dtype = sampleFormatMap[tif['SampleFormat']] except KeyError: self.dtype = 'uint' try: self.noData = float(tif['GDAL_NODATA']) except KeyError: self.noData = None #Get Georef try: self.georef = GeoRef.fromTyf(tif) except Exception as e: log.warning('Cannot extract georefencing informations from tif tags')#, exc_info=True) pass def _fromGDAL(self): '''Use GDAL to extract raster infos and init''' if self.path is None or not self.fileExists: raise IOError("Cannot find file on disk") ds = gdal.Open(self.path, gdal.GA_ReadOnly) self.size = xy(ds.RasterXSize, ds.RasterYSize) self.format = ds.GetDriver().ShortName if self.format in ['JP2OpenJPEG', 'JP2ECW', 'JP2KAK', 'JP2MrSID'] : self.format = 'JPEG2000' self.nbBands = ds.RasterCount b1 = ds.GetRasterBand(1) #first band (band index does not count from 0) self.noData = b1.GetNoDataValue() ddtype = gdal.GetDataTypeName(b1.DataType)#Byte, UInt16, Int16, UInt32, Int32, Float32, Float64 if ddtype == "Byte": self.dtype = 'uint' self.depth = 8 else: self.dtype = ddtype[0:len(ddtype)-2].lower() self.depth = int(ddtype[-2:]) #Get Georef self.georef = GeoRef.fromGDAL(ds) #Close (gdal has no garbage collector) ds, b1 = None, None ####################################### # Dynamic properties ####################################### @property def fileExists(self): '''Test if the file exists on disk''' return os.path.isfile(self.path) @property def baseName(self): if self.path is not None: folder, fileName = os.path.split(self.path) baseName, ext = os.path.splitext(fileName) return baseName @property def isTiff(self): '''Flag if the image format is TIFF''' if self.format in ['TIFF', 'GTiff']: return True else: return False @property def hasWorldFile(self): return self.wfPath is not None @property def isGeoref(self): '''Flag if georef parameters have been extracted''' if self.georef is not None: if self.origin is not None and self.pxSize is not None and self.rotation is not None: return True else: return False else: return False @property def isOneBand(self): return self.nbBands == 1 @property def isFloat(self): return self.dtype in ['Float', 'float'] @property def ddtype(self): ''' Get data type and depth in a concatenate string like 'int8', 'int16', 'uint16', 'int32', 'uint32', 'float32' ... Can be used to define numpy or gdal data type ''' if self.dtype is None or self.depth is None: return None else: return self.dtype + str(self.depth) def __repr__(self): return '\n'.join([ '* Paths infos :', ' path {}'.format(self.path), ' worldfile {}'.format(self.wfPath), ' format {}'.format(self.format), "* Data infos :", " size {}".format(self.size), " bit depth {}".format(self.depth), " data type {}".format(self.dtype), " number of bands {}".format(self.nbBands), " nodata value {}".format(self.noData), "* Georef & Geometry : \n{}".format(self.georef) ]) ####################################### # Methods ####################################### def toGDAL(self): '''Get GDAL dataset''' return gdal.Open(self.path, gdal.GA_ReadOnly) def readAsNpArray(self, subset=True): '''Read raster pixels values as Numpy Array''' if subset and self.subBoxGeo is not None: #georef = GeoRef(self.size, self.pxSize, self.subBoxGeoOrigin, rot=self.rotation, pxCenter=True) img = NpImage(self.path, subBoxPx=self.subBoxPx, noData=self.noData, georef=self.georef, adjustGeoref=True) else: img = NpImage(self.path, noData=self.noData, georef=self.georef) return img ================================================ FILE: core/georaster/georef.py ================================================ # -*- coding:utf-8 -*- # This file is part of BlenderGIS # ***** GPL LICENSE BLOCK ***** # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # All rights reserved. # ***** GPL LICENSE BLOCK ***** import math from ..proj import SRS from ..utils import XY as xy, BBOX from ..errors import OverlapError class GeoRef(): ''' Represents georefencing informations of a raster image Note : image origin is upper-left whereas map origin is lower-left ''' def __init__(self, rSize, pxSize, origin, rot=xy(0,0), pxCenter=True, subBoxGeo=None, crs=None): ''' rSize : dimensions of the raster in pixels (width, height) tuple pxSize : dimension of a pixel in map units (x scale, y scale) tuple. y is always negative origin : upper left geo coords of pixel center, (x, y) tuple pxCenter : set it to True is the origin anchor point is located at pixel center or False if it's lolcated at pixel corner rotation : rotation terms (xrot, yrot) <--> (yskew, xskew) subBoxGeo : a BBOX object that define the working extent (subdataset) in geographic coordinate space ''' self.rSize = xy(*rSize) self.origin = xy(*origin) self.pxSize = xy(*pxSize) if not pxCenter: #adjust topleft coord to pixel center self.origin[0] += abs(self.pxSize.x/2) self.origin[1] -= abs(self.pxSize.y/2) self.rotation = xy(*rot) if subBoxGeo is not None: # Define a subbox at init is optionnal, we can also do it later # Setting the subBox will check if the box overlap the raster extent self.setSubBoxGeo(subBoxGeo) else: self.subBoxGeo = None if crs is not None: if isinstance(crs, SRS): self.crs = crs else: raise IOError("CRS must be SRS() class object not " + str(type(crs))) else: self.crs = crs ############################################ # Alternative constructors ############################################ @classmethod def fromGDAL(cls, ds): '''init from gdal dataset instance''' geoTrans = ds.GetGeoTransform() if geoTrans is not None: xmin, resx, rotx, ymax, roty, resy = geoTrans w, h = ds.RasterXSize, ds.RasterYSize try: crs = SRS.fromGDAL(ds) except Exception as e: crs = None return cls((w, h), (resx, resy), (xmin, ymax), rot=(rotx, roty), pxCenter=False, crs=crs) else: return None @classmethod def fromWorldFile(cls, wfPath, rasterSize): '''init from a worldfile''' try: with open(wfPath,'r') as f: wf = f.readlines() pxSize = xy(float(wf[0].replace(',','.')), float(wf[3].replace(',','.'))) rotation = xy(float(wf[1].replace(',','.')), float(wf[2].replace(',','.'))) origin = xy(float(wf[4].replace(',','.')), float(wf[5].replace(',','.'))) return cls(rasterSize, pxSize, origin, rot=rotation, pxCenter=True, crs=None) except Exception as e: raise IOError("Unable to read worldfile. {}".format(e)) @classmethod def fromTyf(cls, tif): '''read geotags from Tyf instance''' #Warning : Tyf object does not support k in dict test syntax nor get() method, use try block instead w, h = tif['ImageWidth'], tif['ImageLength'] #Search for a transformation matrix try: #34264: ("ModelTransformation", "a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p") # 4x4 transform matrix in 3D space transfoMatrix = tif['ModelTransformationTag'] except KeyError: transfoMatrix = None #Search for upper left coord and pixel scales try: #33922: ("ModelTiepoint", "I,J,K,X,Y,Z") modelTiePoint = tif['ModelTiepointTag'] #33550 ("ModelPixelScale", "ScaleX, ScaleY, ScaleZ") modelPixelScale = tif['ModelPixelScaleTag'] except KeyError: modelTiePoint = None modelPixelScale = None if transfoMatrix is not None: a,b,c,d, \ e,f,g,h, \ i,j,k,l, \ m,n,o,p = transfoMatrix #get only 2d affine parameters origin = xy(d, h) pxSize = xy(a, f) rotation = xy(e, b) elif modelTiePoint is not None and modelPixelScale is not None: origin = xy(*modelTiePoint[3:5]) pxSize = xy(*modelPixelScale[0:2]) pxSize[1] = -pxSize.y #make negative value rotation = xy(0, 0) else: raise IOError("Unable to read geotags") #Define anchor point for top left coord # http://www.remotesensing.org/geotiff/spec/geotiff2.5.html#2.5.2 # http://www.remotesensing.org/geotiff/spec/geotiff6.html#6.3.1.2 # >> 1 = area (cell anchor = top left corner) # >> 2 = point (cell anchor = center) #geotags = Tyf.gkd.Gkd(tif) #cellAnchor = geotags['GTRasterTypeGeoKey'] try: geotags = tif['GeoKeyDirectoryTag'] except KeyError: cellAnchor = 1 #if this key is missing then RasterPixelIsArea is the default else: #get GTRasterTypeGeoKey value cellAnchor = geotags[geotags.index(1025)+3] #http://www.remotesensing.org/geotiff/spec/geotiff2.4.html if cellAnchor == 1: #adjust topleft coord to pixel center origin[0] += abs(pxSize.x/2) origin[1] -= abs(pxSize.y/2) #TODO extract crs (transcript geokeys to proj4 string) return cls((w, h), pxSize, origin, rot=rotation, pxCenter=True, crs=None) ############################################ # Export ############################################ def toGDAL(self): '''return a tuple of georef parameters ordered to define geotransformation of a gdal datasource''' xmin, ymax = self.corners[0] xres, yres = self.pxSize xrot, yrot = self.rotation return (xmin, xres, xrot, ymax, yrot, yres) def toWorldFile(self, path): '''export geo transformation to a worldfile''' xmin, ymax = self.origin xres, yres = self.pxSize xrot, yrot = self.rotation wf = (xres, xrot, yrot, yres, xmin, ymax) f = open(path,'w') f.write( '\n'.join(list(map(str, wf))) ) f.close() ############################################ # Dynamic properties ############################################ @property def hasCRS(self): return self.crs is not None @property def hasRotation(self): return self.rotation.x != 0 or self.rotation.y != 0 #TODO #def getCorners(self, center=True): #def getUL(self, center=True) """ @property def ul(self): '''upper left corner''' return self.geoFromPx(0, yPxRange, True) @property def ur(self): '''upper right corner''' return self.geoFromPx(xPxRange, yPxRange, True) @property def bl(self): '''bottom left corner''' return self.geoFromPx(0, 0, True) @property def br(self): '''bottom right corner''' return self.geoFromPx(xPxRange, 0, True) """ @property def cornersCenter(self): ''' (x,y) geo coordinates of image corners (upper left, upper right, bottom right, bottom left) (pt1, pt2, pt3, pt4) <--> (upper left, upper right, bottom right, bottom left) The coords are located at the pixel center ''' xPxRange = self.rSize.x - 1 yPxRange = self.rSize.y - 1 #pixel center pt1 = self.geoFromPx(0, 0, pxCenter=True)#upperLeft pt2 = self.geoFromPx(xPxRange, 0, pxCenter=True)#upperRight pt3 = self.geoFromPx(xPxRange, yPxRange, pxCenter=True)#bottomRight pt4 = self.geoFromPx(0, yPxRange, pxCenter=True)#bottomLeft return (pt1, pt2, pt3, pt4) @property def corners(self): ''' (x,y) geo coordinates of image corners (upper left, upper right, bottom right, bottom left) (pt1, pt2, pt3, pt4) <--> (upper left, upper right, bottom right, bottom left) Represent the true corner location (upper left for pt1, upper right for pt2 ...) ''' #get corners at center pt1, pt2, pt3, pt4 = self.cornersCenter #pixel center offset xOffset = abs(self.pxSize.x/2) yOffset = abs(self.pxSize.y/2) pt1 = xy(pt1.x - xOffset, pt1.y + yOffset) pt2 = xy(pt2.x + xOffset, pt2.y + yOffset) pt3 = xy(pt3.x + xOffset, pt3.y - yOffset) pt4 = xy(pt4.x - xOffset, pt4.y - yOffset) return (pt1, pt2, pt3, pt4) @property def bbox(self): '''Return a bbox class object''' pts = self.corners xmin = min([pt.x for pt in pts]) xmax = max([pt.x for pt in pts]) ymin = min([pt.y for pt in pts]) ymax = max([pt.y for pt in pts]) return BBOX(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax) @property def bboxPx(self): return BBOX(xmin=0, ymin=0, xmax=self.rSize.x, ymax=self.rSize.y) @property def center(self): '''(x,y) geo coordinates of image center''' return xy(self.corners[0].x + self.geoSize.x/2, self.corners[0].y - self.geoSize.y/2) @property def geoSize(self): '''raster dimensions (width, height) in map units''' return xy(self.rSize.x * abs(self.pxSize.x), self.rSize.y * abs(self.pxSize.y)) @property def orthoGeoSize(self): '''ortho geo size when affine transfo applied a rotation''' pxWidth = math.sqrt(self.pxSize.x**2 + self.rotation.x**2) pxHeight = math.sqrt(self.pxSize.y**2 + self.rotation.y**2) return xy(self.rSize.x*pxWidth, self.rSize.y*pxHeight) @property def orthoPxSize(self): '''ortho pixels size when affine transfo applied a rotation''' pxWidth = math.sqrt(self.pxSize.x**2 + self.rotation.x**2) pxHeight = math.sqrt(self.pxSize.y**2 + self.rotation.y**2) return xy(pxWidth, pxHeight) def geoFromPx(self, xPx, yPx, reverseY=False, pxCenter=True): """ Affine transformation (cf. ESRI WorldFile spec.) Return geo coords of the center of an given pixel xPx = the column number of the pixel in the image counting from left yPx = the row number of the pixel in the image counting from top use reverseY option is yPx is counting from bottom instead of top Number of pixels is range from 0 (not 1) """ if pxCenter: #force pixel center, in this case we need to cast the inputs to floor integer xPx, yPx = math.floor(xPx), math.floor(yPx) ox, oy = self.origin.x, self.origin.y else: #normal behaviour, coord at pixel's top left corner ox = self.origin.x - abs(self.pxSize.x/2) oy = self.origin.y + abs(self.pxSize.y/2) if reverseY:#the users given y pixel in the image counting from bottom yPxRange = self.rSize.y - 1 yPx = yPxRange - yPx x = self.pxSize.x * xPx + self.rotation.y * yPx + ox y = self.pxSize.y * yPx + self.rotation.x * xPx + oy return xy(x, y) def pxFromGeo(self, x, y, reverseY=False, round2Floor=False): """ Affine transformation (cf. ESRI WorldFile spec.) Return pixel position of given geographic coords use reverseY option to get y pixels counting from bottom Pixels position is range from 0 (not 1) """ # aliases for more readability pxSizex, pxSizey = self.pxSize rotx, roty = self.rotation offx = self.origin.x - abs(self.pxSize.x/2) offy = self.origin.y + abs(self.pxSize.y/2) # transfo xPx = (pxSizey*x - rotx*y + rotx*offy - pxSizey*offx) / (pxSizex*pxSizey - rotx*roty) yPx = (-roty*x + pxSizex*y + roty*offx - pxSizex*offy) / (pxSizex*pxSizey - rotx*roty) if reverseY:#the users want y pixel position counting from bottom yPxRange = self.rSize.y - 1 yPx = yPxRange - yPx yPx += 1 #adjust because the coord start at pixel's top left coord #round to floor if round2Floor: xPx, yPx = math.floor(xPx), math.floor(yPx) return xy(xPx, yPx) #Alias def pxToGeo(self, xPx, yPx, reverseY=False): return self.geoFromPx(xPx, yPx, reverseY) def geoToPx(self, x, y, reverseY=False, round2Floor=False): return self.pxFromGeo(x, y, reverseY, round2Floor) ############################################ # Subbox handlers ############################################ def setSubBoxGeo(self, subBoxGeo): '''set a subbox in geographic coordinate space if needed, coords will be adjusted to avoid being outside raster size''' if self.hasRotation: raise IOError("A subbox cannot be define if the raster has rotation parameter") #Before set the property, ensure that the desired subbox overlap the raster extent if not self.bbox.overlap(subBoxGeo): raise OverlapError() elif self.bbox.isWithin(subBoxGeo): #Ignore because subbox is greater than raster extent return else: #convert the subbox in pixel coordinate space xminPx, ymaxPx = self.pxFromGeo(subBoxGeo.xmin, subBoxGeo.ymin, round2Floor=True)#y pixels counting from top xmaxPx, yminPx = self.pxFromGeo(subBoxGeo.xmax, subBoxGeo.ymax, round2Floor=True)#idem subBoxPx = BBOX(xmin=xminPx, ymin=yminPx, xmax=xmaxPx, ymax=ymaxPx)#xmax and ymax include #set the subbox self.setSubBoxPx(subBoxPx) def setSubBoxPx(self, subBoxPx): if not self.bboxPx.overlap(subBoxPx): raise OverlapError() xminPx, xmaxPx = subBoxPx.xmin, subBoxPx.xmax yminPx, ymaxPx = subBoxPx.ymin, subBoxPx.ymax #adjust against raster size if needed #we count pixel number from 0 but size represents total number of pixel (counting from 1), so we must use size-1 sizex, sizey = self.rSize if xminPx < 0: xminPx = 0 if xmaxPx >= sizex: xmaxPx = sizex - 1 if yminPx < 0: yminPx = 0 if ymaxPx >= sizey: ymaxPx = sizey - 1 #get the adjusted geo coords at pixels center xmin, ymin = self.geoFromPx(xminPx, ymaxPx) xmax, ymax = self.geoFromPx(xmaxPx, yminPx) #set the subbox self.subBoxGeo = BBOX(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax) def applySubBox(self): if self.subBoxGeo is not None: self.rSize = self.subBoxPxSize self.origin = self.subBoxGeoOrigin self.subBoxGeo = None def getSubBoxGeoRef(self): return GeoRef(self.subBoxPxSize, self.pxSize, self.subBoxGeoOrigin, pxCenter=True, crs=self.crs) @property def subBoxPx(self): '''return the subbox as bbox object in pixels coordinates space''' if self.subBoxGeo is None: return None xmin, ymax = self.pxFromGeo(self.subBoxGeo.xmin, self.subBoxGeo.ymin, round2Floor=True)#y pixels counting from top xmax, ymin = self.pxFromGeo(self.subBoxGeo.xmax, self.subBoxGeo.ymax, round2Floor=True) return BBOX(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax)#xmax and ymax include @property def subBoxPxSize(self): '''dimension of the subbox in pixels''' if self.subBoxGeo is None: return None bbpx = self.subBoxPx w, h = bbpx.xmax - bbpx.xmin, bbpx.ymax - bbpx.ymin return xy(w+1, h+1) @property def subBoxGeoSize(self): '''dimension of the subbox in map units''' if self.subBoxGeo is None: return None sizex, sizey = self.subBoxPxSize return xy(sizex * abs(self.pxSize.x), sizey * abs(self.pxSize.y)) @property def subBoxPxOrigin(self): '''pixel coordinate of subbox origin''' if self.subBoxGeo is None: return None return xy(self.subBoxPx.xmin, self.subBoxPx.ymin) @property def subBoxGeoOrigin(self): '''geo coordinate of subbox origin, adjusted at pixel center''' if self.subBoxGeo is None: return None return xy(self.subBoxGeo.xmin, self.subBoxGeo.ymax) #### def __repr__(self): s = [ ' spatial ref system {}'.format(self.crs), ' origin geo {}'.format(self.origin), ' pixel size {}'.format(self.pxSize), ' rotation {}'.format(self.rotation), ' bounding box {}'.format(self.bbox), ' geoSize {}'.format(self.geoSize) ] if self.subBoxGeo is not None: s.extend([ ' subbox origin (geo space) {}'.format(self.subBoxGeoOrigin), ' subbox origin (px space) {}'.format(self.subBoxPxOrigin), ' subbox (geo space) {}'.format(self.subBoxGeo), ' subbox (px space) {}'.format(self.subBoxPx), ' sub geoSize {}'.format(self.subBoxGeoSize), ' sub pxSize {}'.format(self.subBoxPxSize), ]) return '\n'.join(s) ================================================ FILE: core/georaster/img_utils.py ================================================ # -*- coding:utf-8 -*- # This file is part of BlenderGIS # ***** GPL LICENSE BLOCK ***** # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # All rights reserved. # ***** GPL LICENSE BLOCK ***** import struct from ..lib import imghdr def isValidStream(data): if data is None: return False format = imghdr.what(None, data) if format is None: return False return True def getImgFormat(filepath): """ Read header of an image file and try to determine it's format no requirements, support JPEG, JPEG2000, PNG, GIF, BMP, TIFF, EXR """ format = None with open(filepath, 'rb') as fhandle: head = fhandle.read(32) # handle GIFs if head[:6] in (b'GIF87a', b'GIF89a'): format = 'GIF' # handle PNG elif head.startswith(b'\211PNG\r\n\032\n'): format = 'PNG' # handle JPEGs #elif head[6:10] in (b'JFIF', b'Exif') elif (b'JFIF' in head or b'Exif' in head or b'8BIM' in head) or head.startswith(b'\xff\xd8'): format = 'JPEG' # handle JPEG2000s elif head.startswith(b'\x00\x00\x00\x0cjP \r\n\x87\n'): format = 'JPEG2000' # handle BMP elif head.startswith(b'BM'): format = 'BMP' # handle TIFF elif head[:2] in (b'MM', b'II'): format = 'TIFF' # handle EXR elif head.startswith(b'\x76\x2f\x31\x01'): format = 'EXR' return format def getImgDim(filepath): """ Return (width, height) for a given img file content no requirements, support JPEG, JPEG2000, PNG, GIF, BMP """ width, height = None, None with open(filepath, 'rb') as fhandle: head = fhandle.read(32) # handle GIFs if head[:6] in (b'GIF87a', b'GIF89a'): try: width, height = struct.unpack("LL", head[16:24]) except struct.error: # Maybe this is for an older PNG version. try: width, height = struct.unpack(">LL", head[8:16]) except struct.error: raise ValueError("Invalid PNG file") # handle JPEGs elif (b'JFIF' in head or b'Exif' in head or b'8BIM' in head) or head.startswith(b'\xff\xd8'): try: fhandle.seek(0) # Read 0xff next size = 2 ftype = 0 while not 0xc0 <= ftype <= 0xcf: fhandle.seek(size, 1) byte = fhandle.read(1) while ord(byte) == 0xff: byte = fhandle.read(1) ftype = ord(byte) size = struct.unpack('>H', fhandle.read(2))[0] - 2 # We are at a SOFn block fhandle.seek(1, 1) # Skip `precision' byte. height, width = struct.unpack('>HH', fhandle.read(4)) except struct.error: raise ValueError("Invalid JPEG file") # handle JPEG2000s elif head.startswith(b'\x00\x00\x00\x0cjP \r\n\x87\n'): fhandle.seek(48) try: height, width = struct.unpack('>LL', fhandle.read(8)) except struct.error: raise ValueError("Invalid JPEG2000 file") # handle BMP elif head.startswith(b'BM'): imgtype = 'BMP' try: width, height = struct.unpack(". # All rights reserved. # ***** GPL LICENSE BLOCK ***** import os import io import random import numpy as np from .georef import GeoRef from ..proj.reproj import reprojImg from ..maths.fillnodata import replace_nans #inpainting function (ie fill nodata) from ..utils import XY as xy from ..checkdeps import HAS_GDAL, HAS_PIL, HAS_IMGIO from .. import settings if HAS_PIL: from PIL import Image if HAS_GDAL: from osgeo import gdal if HAS_IMGIO: from ..lib import imageio class NpImage(): '''Represent an image as Numpy array''' def _getIFACE(self): engine = settings.img_engine if engine == 'AUTO': if HAS_GDAL: return 'GDAL' elif HAS_IMGIO: return 'IMGIO' elif HAS_PIL: return 'PIL' else: raise ImportError("No image engine available") elif engine == 'GDAL'and HAS_GDAL: return 'GDAL' elif engine == 'IMGIO' and HAS_IMGIO: return 'IMGIO' elif engine == 'PIL'and HAS_PIL: return 'PIL' else: raise ImportError(str(engine) + " interface unavailable") #GeoGef delegation by composition instead of inheritance #this special method is called whenever the requested attribute or method is not found in the object def __getattr__(self, attr): if self.isGeoref: return getattr(self.georef, attr) else:#TODO raise specific msg if request for a georef attribute and not self.isgeoref raise AttributeError(str(type(self)) + 'object has no attribute' + str(attr)) def __init__(self, data, subBoxPx=None, noData=None, georef=None, adjustGeoref=False): ''' init from file path, bytes data, Numpy array, NpImage, PIL Image or GDAL dataset subBoxPx : a BBOX object in pixel coordinates space used as data filter (will by applyed) (y counting from top) noData : the value used to represent nodata, will be used to define a numpy mask georef : a Georef object used to set georeferencing informations, optional adjustGeoref: determine if the submited georef must be adjusted against the subbox or if its already correct Notes : * With GDAL the subbox filter can be applyed at reading level whereas with others imaging library, all the data must be extracted before we can extract the subset (using numpy slice). In this case, the dataset must fit entirely in memory otherwise it will raise an overflow error * If no georef was submited and when the class is init using gdal support or from another npImage instance, existing georef of input data will be automatically extracted and adjusted against the subbox ''' self.IFACE = self._getIFACE() self.data = None self.subBoxPx = subBoxPx self.noData = noData self.georef = georef if self.subBoxPx is not None and self.georef is not None: if adjustGeoref: self.georef.setSubBoxPx(subBoxPx) self.georef.applySubBox() #init from another NpImage instance if isinstance(data, NpImage): self.data = self._applySubBox(data.data) if data.isGeoref and not self.isGeoref: self.georef = data.georef #adjust georef against subbox if self.subBoxPx is not None: self.georef.setSubBoxPx(subBoxPx) self.georef.applySubBox() #init from numpy array if isinstance(data, np.ndarray): self.data = self._applySubBox(data) #init from bytes data (BLOB) if isinstance(data, bytes): self.data = self._npFromBLOB(data) #init from file path if isinstance(data, str): if os.path.exists(data): self.data = self._npFromPath(data) else: raise ValueError('Unable to load image data') #init from GDAL dataset instance if HAS_GDAL: if isinstance(data, gdal.Dataset): self.data = self._npFromGDAL(data) #init from PIL Image instance if HAS_PIL: if isinstance(data, Image.Image): self.data = self._npFromPIL(data) if self.data is None: raise ValueError('Unable to load image data') #Mask nodata value to avoid bias when computing min or max statistics if self.noData is not None: self.data = np.ma.masked_array(self.data, self.data == self.noData) @property def size(self): return xy(self.data.shape[1], self.data.shape[0]) @property def isGeoref(self): '''Flag if georef parameters have been extracted''' if self.georef is not None: return True else: return False @property def nbBands(self): if len(self.data.shape) == 2: return 1 elif len(self.data.shape) == 3: return self.data.shape[2] @property def hasAlpha(self): return self.nbBands == 4 @property def isOneBand(self): return self.nbBands == 1 @property def dtype(self): '''return string ['int8', 'uint8', 'int16', 'uint16', 'int32', 'uint32', 'float32', 'float64']''' return self.data.dtype @property def isFloat(self): if self.dtype in ['float16', 'float32', 'float64']: return True else: return False def getMin(self, bandIdx=0): if self.nbBands == 1: return self.data.min() else: return self.data[:,:,bandIdx].min() def getMax(self, bandIdx=0): if self.nbBands == 1: return self.data.max() else: return self.data[:,:,bandIdx].max() @classmethod def new(cls, w, h, bkgColor=(255,255,255,255), noData=None, georef=None): r, g, b, a = bkgColor data = np.empty((h, w, 4), np.uint8) data[:,:,0] = r data[:,:,1] = g data[:,:,2] = b data[:,:,3] = a return cls(data, noData=noData, georef=georef) def _applySubBox(self, data): '''Use numpy slice to extract subset of data''' if self.subBoxPx is not None: x1, x2 = self.subBoxPx.xmin, self.subBoxPx.xmax+1 y1, y2 = self.subBoxPx.ymin, self.subBoxPx.ymax+1 if len(data.shape) == 2: #one band data = data[y1:y2, x1:x2] else: data = data[y1:y2, x1:x2, :] self.subBoxPx = None return data def _npFromPath(self, path): '''Get Numpy array from a file path''' if self.IFACE == 'PIL': img = Image.open(path) return self._npFromPIL(img) elif self.IFACE == 'IMGIO': return self._npFromImgIO(path) elif self.IFACE == 'GDAL': ds = gdal.Open(path) return self._npFromGDAL(ds) def _npFromBLOB(self, data): '''Get Numpy array from Bytes data''' if self.IFACE == 'PIL': #convert bytes object to bytesio (stream buffer) and open it with PIL img = Image.open(io.BytesIO(data)) data = self._npFromPIL(img) elif self.IFACE == 'IMGIO': img = io.BytesIO(data) data = self._npFromImgIO(img) elif self.IFACE == 'GDAL': #Use a virtual memory file to create gdal dataset from buffer #build a random name to make the function thread safe vsipath = '/vsimem/' + ''.join(random.choice('abcdefghijklmnopqrstuvwxyz') for i in range(5)) gdal.FileFromMemBuffer(vsipath, data) ds = gdal.Open(vsipath) data = self._npFromGDAL(ds) ds = None gdal.Unlink(vsipath) return data def _npFromImgIO(self, img): '''Use ImageIO to extract numpy array from image path or bytesIO''' data = imageio.imread(img) return self._applySubBox(data) def _npFromPIL(self, img): '''Get Numpy array from PIL Image instance''' if img.mode == 'P': #palette (indexed color) img = img.convert('RGBA') data = np.asarray(img) #data.setflags(write=True) #PIL return a non writable array return self._applySubBox(data) def _npFromGDAL(self, ds): '''Get Numpy array from GDAL dataset instance''' if self.subBoxPx is not None: startx, starty = self.subBoxPx.xmin, self.subBoxPx.ymin width = (self.subBoxPx.xmax - self.subBoxPx.xmin) + 1 height = (self.subBoxPx.ymax - self.subBoxPx.ymin) + 1 data = ds.ReadAsArray(startx, starty, width, height) else: data = ds.ReadAsArray() if len(data.shape) == 3: #multiband data = np.rollaxis(data, 0, 3) # because first axis is band index else: #one band raster or indexed color (= palette = pseudo color table (pct)) ctable = ds.GetRasterBand(1).GetColorTable() if ctable is not None: #Swap index values to their corresponding color (rgba) nbColors = ctable.GetCount() keys = np.array( [i for i in range(nbColors)] ) values = np.array( [ctable.GetColorEntry(i) for i in range(nbColors)] ) sortIdx = np.argsort(keys) idx = np.searchsorted(keys, data, sorter=sortIdx) data = values[sortIdx][idx] #Try to extract georef if not self.isGeoref: self.georef = GeoRef.fromGDAL(ds) #adjust georef against subbox if self.subBoxPx is not None and self.georef is not None: self.georef.applySubBox() return data def toBLOB(self, ext='PNG'): '''Get bytes raw data''' if ext == 'JPG': ext = 'JPEG' if self.IFACE == 'PIL': b = io.BytesIO() img = Image.fromarray(self.data) img.save(b, format=ext) data = b.getvalue() #convert bytesio to bytes elif self.IFACE == 'IMGIO': if ext == 'JPEG' and self.hasAlpha: self.removeAlpha() data = imageio.imwrite(imageio.RETURN_BYTES, self.data, format=ext) elif self.IFACE == 'GDAL': mem = self.toGDAL() #build a random name to make the function thread safe name = ''.join(random.choice('abcdefghijklmnopqrstuvwxyz') for i in range(5)) vsiname = '/vsimem/' + name + '.png' out = gdal.GetDriverByName(ext).CreateCopy(vsiname, mem) # Read /vsimem/output.png f = gdal.VSIFOpenL(vsiname, 'rb') gdal.VSIFSeekL(f, 0, 2) # seek to end size = gdal.VSIFTellL(f) gdal.VSIFSeekL(f, 0, 0) # seek to beginning data = gdal.VSIFReadL(1, size, f) gdal.VSIFCloseL(f) # Cleanup gdal.Unlink(vsiname) mem = None return data def toPIL(self): '''Get PIL Image instance''' return Image.fromarray(self.data) def toGDAL(self): '''Get GDAL memory driver dataset''' w, h = self.size n = self.nbBands dtype = str(self.dtype) if dtype == 'uint8': dtype = 'byte' dtype = gdal.GetDataTypeByName(dtype) mem = gdal.GetDriverByName('MEM').Create('', w, h, n, dtype) #writearray is available only at band level if self.isOneBand: mem.GetRasterBand(1).WriteArray(self.data) else: for bandIdx in range(n): bandArray = self.data[:,:,bandIdx] mem.GetRasterBand(bandIdx+1).WriteArray(bandArray) #write georef if self.isGeoref: mem.SetGeoTransform(self.georef.toGDAL()) if self.georef.crs is not None: mem.SetProjection(self.georef.crs.getOgrSpatialRef().ExportToWkt()) return mem def removeAlpha(self): if self.hasAlpha: self.data = self.data[:, :, 0:3] def addAlpha(self, opacity=255): if self.nbBands == 3: w, h = self.size alpha = np.empty((h,w), dtype=self.dtype) alpha.fill(opacity) alpha = np.expand_dims(alpha, axis=2) self.data = np.append(self.data, alpha, axis=2) def save(self, path): ''' save the numpy array to a new image file output format is defined by path extension ''' imgFormat = path[-3:] if self.IFACE == 'PIL': self.toPIL().save(path) elif self.IFACE == 'IMGIO': if imgFormat == 'jpg' and self.hasAlpha: self.removeAlpha() imageio.imwrite(path, self.data)#float32 support ok elif self.IFACE == 'GDAL': if imgFormat == 'png': driver = 'PNG' elif imgFormat == 'jpg': driver = 'JPEG' elif imgFormat == 'tif': driver = 'Gtiff' else: raise ValueError('Cannot write to '+ imgFormat + ' image format') #Some format like jpg or png has no create method implemented #because we can't write data at random with these formats #so we must use an intermediate memory driver, write data to it #and then write the output file with the createcopy method mem = self.toGDAL() out = gdal.GetDriverByName(driver).CreateCopy(path, mem) mem = out = None if self.isGeoref: self.georef.toWorldFile(os.path.splitext(path)[0] + '.wld') def paste(self, data, x, y): img = NpImage(data) data = img.data w, h = img.size if img.isOneBand and self.isOneBand: self.data[y:y+h, x:x+w] = data elif (not img.isOneBand and self.isOneBand) or (img.isOneBand and not self.isOneBand): raise ValueError('Paste error, cannot mix one band with multiband') if self.hasAlpha: n = img.nbBands self.data[y:y+h, x:x+w, 0:n] = data else: n = self.nbBands self.data[y:y+h, x:x+w, :] = data[:, :, 0:n] def cast2float(self): if not self.isFloat: self.data = self.data.astype('float32') def fillNodata(self): #if not self.noData in self.data: if not np.ma.is_masked(self.data): #do not process it if its not necessary return if self.IFACE == 'GDAL': # gdal.FillNodata need a band object to apply on # so we create a memory datasource (1 band, float) height, width = self.data.shape ds = gdal.GetDriverByName('MEM').Create('', width, height, 1, gdal.GetDataTypeByName('float32')) b = ds.GetRasterBand(1) b.SetNoDataValue(self.noData) self.data = np.ma.filled(self.data, self.noData)# Fill mask with nodata value b.WriteArray(self.data) gdal.FillNodata(targetBand=b, maskBand=None, maxSearchDist=max(self.size.xy), smoothingIterations=0) self.data = b.ReadAsArray() ds, b = None, None else: #Call the inpainting function # Cast to float self.cast2float() # Fill mask with NaN (warning NaN is a special value for float arrays only) self.data = np.ma.filled(self.data, np.NaN) # Inpainting self.data = replace_nans(self.data, max_iter=5, tolerance=0.5, kernel_size=2, method='localmean') def reproj(self, crs1, crs2, out_ul=None, out_size=None, out_res=None, sqPx=False, resamplAlg='BL'): ds1 = self.toGDAL() if not self.isGeoref: raise IOError('Unable to reproject non georeferenced image') ds2 = reprojImg(crs1, crs2, ds1, out_ul=out_ul, out_size=out_size, out_res=out_res, sqPx=sqPx, resamplAlg=resamplAlg) return NpImage(ds2) def __repr__(self): return '\n'.join([ "* Data infos :", " size {}".format(self.size), " type {}".format(self.dtype), " number of bands {}".format(self.nbBands), " nodata value {}".format(self.noData), "* Statistics : min {} max {}".format(self.getMin(), self.getMax()), "* Georef & Geometry : \n{}".format(self.georef) ]) ================================================ FILE: core/lib/Tyf/VERSION ================================================ 1.2.5 ================================================ FILE: core/lib/Tyf/__init__.py ================================================ # -*- encoding:utf-8 -*- __copyright__ = "Copyright © 2012-2015, THOORENS Bruno - http://bruno.thoorens.free.fr/licences/tyf.html" __author__ = "THOORENS Bruno" __tiff__ = (6, 0) __geotiff__ = (1, 8, 1) import io, os, sys, struct, operator, collections __PY3__ = True if sys.version_info[0] >= 3 else False unpack = lambda fmt, fileobj: struct.unpack(fmt, fileobj.read(struct.calcsize(fmt))) pack = lambda fmt, fileobj, value: fileobj.write(struct.pack(fmt, *value)) TYPES = { 1: ("B", "UCHAR or USHORT"), 2: ("c", "ASCII"), 3: ("H", "UBYTE"), 4: ("L", "ULONG"), 5: ("LL", "URATIONAL"), 6: ("b", "CHAR or SHORT"), 7: ("c", "UNDEFINED"), 8: ("h", "BYTE"), 9: ("l", "LONG"), 10: ("ll", "RATIONAL"), 11: ("f", "FLOAT"), 12: ("d", "DOUBLE"), } # assure compatibility python 2 & 3 if __PY3__: from io import BytesIO as StringIO TYPES[2] = ("s", "ASCII") TYPES[7] = ("s", "UDEFINED") import functools reduce = functools.reduce long = int import urllib.request as urllib else: from StringIO import StringIO import urllib reduce = __builtins__["reduce"] from . import ifd, gkd, tags def _read_IFD(obj, fileobj, offset, byteorder="<"): # fileobj seek must be on the start offset fileobj.seek(offset) # get number of entry nb_entry, = unpack(byteorder+"H", fileobj) # for each entry for i in range(nb_entry): # read tag, type and count values tag, typ, count = unpack(byteorder+"HHL", fileobj) # extract data data = fileobj.read(struct.calcsize("=L")) if not isinstance(data, bytes): data = data.encode() _typ = TYPES[typ][0] # create a tifftag tt = ifd.TiffTag(tag, typ, name=obj.tagname) # initialize what we already know # tt.type = typ tt.count = count # to know if ifd entry value is an offset tt._determine_if_offset() # if value is offset if tt.value_is_offset: # read offset value value, = struct.unpack(byteorder+"L", data) fmt = byteorder + _typ*count bckp = fileobj.tell() # go to offset in the file fileobj.seek(value) # if ascii type, convert to bytes if typ == 2: tt.value = b"".join(e for e in unpack(fmt, fileobj)) # else if undefined type, read data elif typ == 7: tt.value = fileobj.read(count) # else unpack data else: tt.value = unpack(fmt, fileobj) # go back to ifd entry fileobj.seek(bckp) # if value is in the ifd entry else: if typ in [2, 7]: tt.value = data[:count] else: fmt = byteorder + _typ*count tt.value = struct.unpack(fmt, data[:count*struct.calcsize("="+_typ)]) obj.addtag(tt) def from_buffer(obj, fileobj, offset, byteorder="<", custom_sub_ifd={}): # read data from offset _read_IFD(obj, fileobj, offset, byteorder) # get next ifd offset next_ifd, = unpack(byteorder+"L", fileobj) # finding by default those SubIFD sub_ifd = {34665:"Exif tag", 34853:"GPS tag", 40965:"Interoperability tag"} # adding other SubIFD if asked sub_ifd.update(custom_sub_ifd) ## read registered SubIFD for key,value in sub_ifd.items(): if key in obj: obj.sub_ifd[key] = ifd.Ifd(tagname=value) _read_IFD(obj.sub_ifd[key], fileobj, obj[key], byteorder) return next_ifd # for speed reason : load raster only if asked or if needed def _load_raster(obj, fileobj): # striped raster data if 273 in obj: for offset,bytecount in zip(obj.get(273).value, obj.get(279).value): fileobj.seek(offset) obj.stripes += (fileobj.read(bytecount), ) # free raster data elif 288 in obj: for offset,bytecount in zip(obj.get(288).value, obj.get(289).value): fileobj.seek(offset) obj.free += (fileobj.read(bytecount), ) # tiled raster data elif 324 in obj: for offset,bytecount in zip(obj.get(324).value, obj.get(325).value): fileobj.seek(offset) obj.tiles += (fileobj.read(bytecount), ) # get interExchange (thumbnail data for JPEG/EXIF data) if 513 in obj: fileobj.seek(obj[513]) obj.jpegIF = fileobj.read(obj[514]) def _write_IFD(obj, fileobj, offset, byteorder="<"): # go where obj have to be written fileobj.seek(offset) # sort data to be writen tags = sorted(list(dict.values(obj)), key=lambda e:e.tag) # write number of entries pack(byteorder+"H", fileobj, (len(tags),)) first_entry_offset = fileobj.tell() # write all ifd entries for t in tags: # write tag, type & count pack(byteorder+"HHL", fileobj, (t.tag, t.type, t.count)) # if value is not an offset if not t.value_is_offset: value = t._fill() n = len(value) if __PY3__ and t.type in [2, 7]: fmt = str(n)+TYPES[t.type][0] value = (value,) else: fmt = n*TYPES[t.type][0] pack(byteorder+fmt, fileobj, value) else: pack(byteorder+"L", fileobj, (0,)) next_ifd_offset = fileobj.tell() pack(byteorder+"L", fileobj, (0,)) # prepare jumps data_offset = fileobj.tell() step1 = struct.calcsize("=HHLL") step2 = struct.calcsize("=HHL") # comme back to first ifd entry fileobj.seek(first_entry_offset) for t in tags: # for each tag witch value needs offset if t.value_is_offset: # go to offset value location (jump over tag, type, count) fileobj.seek(step2, 1) # write offset where value is about to be stored pack(byteorder+"L", fileobj, (data_offset,)) # remember where i am in ifd entries bckp = fileobj.tell() # go to offset where value is about to be stored fileobj.seek(data_offset) # prepare value according to python version if __PY3__ and t.type in [2, 7]: fmt = str(t.count)+TYPES[t.type][0] value = (t.value,) else: fmt = t.count*TYPES[t.type][0] value = t.value # write value # print(">>>", fmt, value) pack(byteorder+fmt, fileobj, value) # remmember where to put next value data_offset = fileobj.tell() # go to where I was in ifd entries fileobj.seek(bckp) else: fileobj.seek(step1, 1) return next_ifd_offset def to_buffer(obj, fileobj, offset, byteorder="<"): obj._check() size = obj.size raw_offset = offset + size["ifd"] + size["data"] # add SubIFD sizes... for tag, p_ifd in sorted(obj.sub_ifd.items(), key=lambda e:e[0]): obj.set(tag, 4, raw_offset) size = p_ifd.size raw_offset = raw_offset + size["ifd"] + size["data"] # knowing where raw image have to be writen, update [Strip/Free/Tile]Offsets if 273 in obj: _279 = obj.get(279).value stripoffsets = (raw_offset,) for bytecount in _279[:-1]: stripoffsets += (stripoffsets[-1]+bytecount, ) obj.set(273, 4, stripoffsets) next_ifd = stripoffsets[-1] + _279[-1] elif 288 in obj: _289 = obj.get(289).value freeoffsets = (raw_offset,) for bytecount in _289[:-1]: freeoffsets += (freeoffsets[-1]+bytecount, ) obj.set(288, 4, freeoffsets) next_ifd = freeoffsets[-1] + _289[-1] elif 324 in obj: _325 = obj.get(325).value tileoffsets = (raw_offset,) for bytecount in _325[:-1]: tileoffsets += (tileoffsets[-1]+bytecount, ) obj.set(324, 4, tileoffsets) next_ifd = tileoffsets[-1] + _325[-1] elif 513 in obj: interexchangeoffset = raw_offset obj.set(513, 4, raw_offset) next_ifd = interexchangeoffset + obj[514] else: next_ifd = raw_offset # write IFD next_ifd_offset = _write_IFD(obj, fileobj, offset, byteorder) # write SubIFD for tag, p_ifd in sorted(obj.sub_ifd.items(), key=lambda e:e[0]): _write_IFD(p_ifd, fileobj, obj[tag], byteorder) # write raster data if len(obj.stripes): for offset,data in zip(stripoffsets, obj.stripes): fileobj.seek(offset) fileobj.write(data) elif len(obj.free): for offset,data in zip(freeoffsets, obj.stripes): fileobj.seek(offset) fileobj.write(data) elif len(obj.tiles): for offset,data in zip(tileoffsets, obj.tiles): fileobj.seek(offset) fileobj.write(data) elif obj.jpegIF != b"": fileobj.seek(interexchangeoffset) fileobj.write(obj.jpegIF) fileobj.seek(next_ifd_offset) return next_ifd def _fileobj(f, mode): if hasattr(f, "close"): fileobj = f _close = False else: fileobj = io.open(f, mode) _close = True return fileobj, _close class TiffFile(list): gkd = property(lambda obj: [gkd.Gkd(ifd) for ifd in obj], None, None, "list of geotiff directory") has_raster = property(lambda obj: reduce(operator.__or__, [ifd.has_raster for ifd in obj]), None, None, "") raster_loaded = property(lambda obj: reduce(operator.__and__, [ifd.raster_loaded for ifd in obj]), None, None, "") def __init__(self, fileobj): # Initialize a TiffFile object from buffer fileobj, fileobj have to be in 'wb' mode # determine byteorder first, = unpack(">H", fileobj) byteorder = "<" if first == 0x4949 else ">" magic_number, = unpack(byteorder+"H", fileobj) if magic_number not in [0x732E,0x2A]: #29486, 42 fileobj.close() if magic_number == 0x2B: # 43 raise IOError("BigTIFF file not supported") else: raise IOError("Bad magic number. Not a valid TIFF file") next_ifd, = unpack(byteorder+"L", fileobj) ifds = [] while next_ifd != 0: i = ifd.Ifd(sub_ifd={ 34665:[tags.exfT,"Exif tag"], 34853:[tags.gpsT,"GPS tag"] }) next_ifd = from_buffer(i, fileobj, next_ifd, byteorder) ifds.append(i) if hasattr(fileobj, "name"): self._filename = fileobj.name else: for i in ifds: _load_raster(i, fileobj) list.__init__(self, ifds) def __getitem__(self, item): if isinstance(item, tuple): return list.__getitem__(self, item[0])[item[-1]] else: return list.__getitem__(self, item) def __add__(self, value): self.load_raster() if isinstance(value, TiffFile): value.load_raster() for i in value: self.append(i) elif isinstance(value, ifd.Ifd): self.append(value) return self __iadd__ = __add__ def load_raster(self, idx=None): if hasattr(self, "_filename"): in_ = io.open(self._filename, "rb") for ifd in iter(self) if idx == None else [self[idx]]: if not ifd.raster_loaded: _load_raster(ifd, in_) in_.close() def save(self, f, byteorder="<", idx=None): self.load_raster() fileobj, _close = _fileobj(f, "wb") pack(byteorder+"HH", fileobj, (0x4949 if byteorder == "<" else 0x4d4d, 0x2A,)) next_ifd = 8 for i in iter(self) if idx == None else [self[idx]]: pack(byteorder+"L", fileobj, (next_ifd,)) next_ifd = to_buffer(i, fileobj, next_ifd, byteorder) if _close: fileobj.close() class JpegFile(collections.OrderedDict): jfif = property(lambda obj: collections.OrderedDict.__getitem__(obj, 0xffe0), None, None, "JFIF data") exif = property(lambda obj: collections.OrderedDict.__getitem__(obj, 0xffe1)[0], None, None, "Image IFD") ifd1 = property(lambda obj: collections.OrderedDict.__getitem__(obj, 0xffe1)[1], None, None, "Thumbnail IFD") def __init__(self, fileobj): markers = collections.OrderedDict() marker, = unpack(">H", fileobj) if marker != 0xffd8: raise Exception("not a valid jpeg file") while marker != 0xffd9: # EOI (End Of Image) Marker marker, count = unpack(">HH", fileobj) # here is raster data marker, copy all after marker id if marker == 0xffda: fileobj.seek(-2, 1) markers[0xffda] = fileobj.read()[:-2] # say it is the end of the file marker = 0xffd9 elif marker == 0xffe1: string = StringIO(fileobj.read(count-2)[6:]) try: markers[marker] = TiffFile(string) except: setattr(markers, "_0xffe1", string.getvalue()) string.close() else: markers[marker] = fileobj.read(count-2) collections.OrderedDict.__init__(self, markers) def __getitem__(self, item): try: return collections.OrderedDict.__getitem__(self, 0xffe1)[0,item] except KeyError: return collections.OrderedDict.__getitem__(self, item) def _pack(self, marker, fileobj): data = self[marker] if marker == 0xffda: pack(">H", fileobj, (marker,)) elif marker == 0xffe1: string = StringIO() self[marker].save(string) data = b"Exif\x00\x00" + string.getvalue() pack(">HH", fileobj, (marker, len(data) + 2)) string.close() else: pack(">HH", fileobj, (marker, len(data) + 2)) fileobj.write(data) def save(self, f): fileobj, _close = _fileobj(f, "wb") pack(">H", fileobj, (0xffd8,)) for key in self: self._pack(key, fileobj) pack(">H", fileobj, (0xffd9,)) if _close: fileobj.close() def save_thumbnail(self, f): try: ifd = self.ifd1 except IndexError: pass else: compression = ifd[259] if hasattr(f, "close"): fileobj = f _close = False else: fileobj = io.open(os.path.splitext(f)[0] + (".jpg" if compression == 6 else ".tif"), "wb") _close = True if compression == 6: fileobj.write(ifd.jpegIF) elif compression == 1: self[0xffe1].save(fileobj, idx=1) if _close: fileobj.close() def dump_exif(self, f): fileobj, _close = _fileobj(f, "wb") self[0xffe1].save(fileobj) if _close: fileobj.close() def load_exif(self, f): fileobj, _close = _fileobj(f, "rb") self[0xffe1] = TiffFile(fileobj) self[0xffe1].load_raster() if _close: fileobj.close() def strip_exif(self): for key in [k for k in self.exif.sub_ifd if k in self.exif]: self.exif.pop(key) self.exif.sub_ifd = {} for key in list(k for k in self.exif if k not in tags.bTT): self.exif.pop(key) while len(self[0xffe1]) > 1: self[0xffe1].pop(-1) def jpeg_extract(f): fileobj, _close = _fileobj(f, "rb") ifd = False marker, = unpack(">H", fileobj) if marker != 0xffd8: raise Exception("not a valid jpeg file") while marker != 0xffd9: marker, count = unpack(">HH", fileobj) if marker == 0xffe1: string = StringIO(fileobj.read(count-2)[6:]) ifd = TiffFile(string) string.close() marker = 0xffd9 else: fileobj.read(count-2) if _close: fileobj.close() return ifd def open(f): fileobj, _close = _fileobj(f, "rb") first, = unpack(">H", fileobj) fileobj.seek(0) if first == 0xffd8: obj = JpegFile(fileobj) elif first in [0x4d4d, 0x4949]: obj = TiffFile(fileobj) if _close: fileobj.close() try: return obj except: raise Exception("file is not a valid JPEG nor TIFF image") ''' # if PIL exists do some overridings try: from PIL import Image as _Image except ImportError: pass else: def _getexif(im): try: data = im.info["exif"] except KeyError: return None fileobj = io.BytesIO(data[6:]) exif = TiffFile(fileobj) fileobj.close() return exif class Image(_Image.Image): _image_ = _Image.Image @staticmethod def open(*args, **kwargs): return _Image.open(*args, **kwargs) def save(self, fp, format="JPEG", **params): ifd = params.pop("ifd", False) if ifd != False: fileobj = StringIO() if isinstance(ifd, TiffFile): ifd.load_raster() ifd.save(fileobj) elif isinstance(ifd, JpegFile): ifd[0xffe1].save(fileobj) data = fileobj.getvalue() fileobj.close() if len(data) > 0: params["exif"] = b"Exif\x00\x00" + (data.encode() if isinstance(data, str) else data) Image._image_.save(self, fp, format="JPEG", **params) _Image.Image = Image from PIL import JpegImagePlugin JpegImagePlugin._getexif = _getexif del _getexif ''' ================================================ FILE: core/lib/Tyf/decoders.py ================================================ # -*- encoding:utf-8 -*- # Copyright 2012-2015, THOORENS Bruno - http://bruno.thoorens.free.fr/licences/tyf.html import datetime ############### # type decoders _1 = _3 = _4 = _6 = _8 = _9 = _11 = _12 = lambda value: value[0] if len(value) == 1 else value _2 = lambda value: value[:-1] def _5(value): result = tuple((float(n)/(1 if d==0 else d)) for n,d in zip(value[0::2], value[1::2])) return result[0] if len(result) == 1 else result _7 = lambda value: value _10 = _5 ####################### # Tag-specific decoders # XPTitle XPComment XBAuthor _0x9c9b = _0x9c9c = _0x9c9d = lambda value : "".join(chr(e) for e in value[0::2]).encode()[:-1] # UserComment GPSProcessingMethod _0x9286 = _0x1b = lambda value: value[8:] #GPSLatitudeRef _0x1 = lambda value: 1 if value in [b"N\x00", b"N"] else -1 #GPSLatitude def _0x2(value): degrees, minutes, seconds = _5(value) return (seconds/60 + minutes)/60 + degrees #GPSLatitudeRef _0x3 = lambda value: 1 if value in [b"E\x00", b"E"] else -1 #GPSLongitude _0x4 = _0x2 #GPSAltitudeRef _0x5 = lambda value: 1 if value == 0 else -1 # GPSTimeStamp _0x7 = lambda value: datetime.time(*[int(e) for e in _5(value)]) # GPSDateStamp _0x1d = lambda value: datetime.datetime.strptime(_2(value).decode(), "%Y:%m:%d") # DateTime DateTimeOriginal DateTimeDigitized _0x132 = _0x9003 = _0x9004 = lambda value: datetime.datetime.strptime(_2(value).decode(), "%Y:%m:%d %H:%M:%S") ================================================ FILE: core/lib/Tyf/encoders.py ================================================ # -*- encoding:utf-8 -*- # Copyright 2012-2015, THOORENS Bruno - http://bruno.thoorens.free.fr/licences/tyf.html from . import reduce import math, fractions, datetime ############### # type encoders _m_short = 0 _M_short = 2**8 def _1(value): value = int(value) return (_m_short, ) if value < _m_short else \ (_M_short, ) if value > _M_short else \ (value, ) def _2(value): if not isinstance(value, bytes): value = value.encode() value += b"\x00" if value[-1] != b"\x00" else "" return value _m_byte = 0 _M_byte = 2**16 def _3(value): value = int(value) return (_m_byte, ) if value < _m_byte else \ (_M_byte, ) if value > _M_byte else \ (value, ) _m_long = 0 _M_long = 2**32 def _4(value): value = int(value) return (_m_long, ) if value < _m_long else \ (_M_long, ) if value > _M_long else \ (value, ) def _5(value): if not isinstance(value, tuple): value = (value, ) return reduce(tuple.__add__, [(f.numerator, f.denominator) for f in [fractions.Fraction(str(v)).limit_denominator(10000000) for v in value]]) _m_s_short = -_M_short/2 _M_s_short = _M_short/2-1 def _6(value): value = int(value) return (_m_s_short, ) if value < _m_s_short else \ (_M_s_short, ) if value > _M_s_short else \ (value, ) def _7(value): if not isinstance(value, bytes): value = value.encode() return value _m_s_byte = -_M_byte/2 _M_s_byte = _M_byte/2-1 def _8(value): value = int(value) return (_m_s_byte, ) if value < _m_s_byte else \ (_M_s_byte, ) if value > _M_s_byte else \ (value, ) _m_s_long = -_M_long/2 _M_s_long = _M_long/2-1 def _9(value): value = int(value) return (_m_s_long, ) if value < _m_s_long else \ (_M_s_long, ) if value > _M_s_long else \ (value, ) _10 = _5 def _11(value): return (float(value), ) _12 = _11 ####################### # Tag-specific encoders # XPTitle XPComment XBAuthor _0x9c9b = _0x9c9c = _0x9c9d = lambda value : reduce(tuple.__add__, [(ord(e), 0) for e in value]) # UserComment GPSProcessingMethod _0x9286 = _0x1b = lambda value: b"ASCII\x00\x00\x00" + (value.encode() if not isinstance(value, bytes) else value) # GPSLatitudeRef _0x1 = lambda value: b"N\x00" if bool(value >= 0) == True else b"S\x00" # GPSLatitude def _0x2(value): value = abs(value) degrees = math.floor(value) minutes = (value - degrees) * 60 seconds = (minutes - math.floor(minutes)) * 60 minutes = math.floor(minutes) if seconds >= (60.-0.0001): seconds = 0. minutes += 1 if minutes >= (60.-0.0001): minutes = 0. degrees += 1 return _5((degrees, minutes, seconds)) #GPSLongitudeRef _0x3 = lambda value: b"E\x00" if bool(value >= 0) == True else b"W\x00" #GPSLongitude _0x4 = _0x2 #GPSAltitudeRef _0x5 = lambda value: _3(1 if value < 0 else 0) #GPSAltitude _0x6 = lambda value: _5(abs(value)) # GPSTimeStamp _0x7 = lambda value: _5(tuple(float(e) for e in [value.hour, value.minute, value.second])) # GPSDateStamp _0x1d = lambda value: _2(value.strftime("%Y:%m:%d")) # DateTime DateTimeOriginal DateTimeDigitized _0x132 = _0x9003 = _0x9004 = lambda value: _2(value.strftime("%Y:%m:%d %H:%M:%S")) ================================================ FILE: core/lib/Tyf/gkd.py ================================================ # -*- encoding: utf-8 -*- # Copyright 2012-2015, THOORENS Bruno - http://bruno.thoorens.free.fr/licences/tyf.html # ~ http://www.remotesensing.org/geotiff/spec/geotiffhome.html from . import ifd, tags, values, __geotiff__, __PY3__ import collections GeoKeyModel = { 33550: collections.namedtuple("ModelPixelScale", "ScaleX, ScaleY, ScaleZ"), 33922: collections.namedtuple("ModelTiepoint", "I,J,K,X,Y,Z"), 34264: collections.namedtuple("ModelTransformation", "a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p") } def Transform(obj, x=0., y=0., z1=0.,z2=1.): return ( obj[0] * x + obj[1] * y + obj[2] * z1 + obj[3] * z2, obj[4] * x + obj[5] * y + obj[6] * z1 + obj[7] * z2, obj[8] * x + obj[9] * y + obj[10] * z1 + obj[11] * z2, obj[12] * x + obj[13] * y + obj[14] * z1 + obj[15] * z2 ) _TAGS = { # GeoTIFF Configuration GeoKeys 1024: ("GTModelTypeGeoKey", [3], 0, None), 1025: ("GTRasterTypeGeoKey", [3], 1, None), 1026: ("GTCitationGeoKey", [2], None, None), # ASCII text # Geographic CS Parameter GeoKeys 2048: ("GeographicTypeGeoKey", [3], 4326, None), # epsg datum code [4001 - 4999] 2049: ("GeogCitationGeoKey", [2], None, None), # ASCII text 2050: ("GeogGeodeticDatumGeoKey", [3], None, None), # use 2048 ! 2051: ("GeogPrimeMeridianGeoKey", [3], 8901, None), # epsg prime meridian code [8001 - 8999] 2052: ("GeogLinearUnitsGeoKey", [3], 9001, None), # epsg linear unit code [9000 - 9099] 2053: ("GeogLinearUnitSizeGeoKey", [12], None, None), # custom unit in meters 2054: ("GeogAngularUnitsGeoKey", [3], 9101, None), 2055: ("GeogAngularUnitsSizeGeoKey", [12], None, None), # custom unit in radians 2056: ("GeogEllipsoidGeoKey", [3], None, None), # epsg ellipsoid code [7000 - 7999] 2057: ("GeogSemiMajorAxisGeoKey", [12], None, None), 2058: ("GeogSemiMinorAxisGeoKey", [12], None, None), 2059: ("GeogInvFlatteningGeoKey", [12], None, None), 2060: ("GeogAzimuthUnitsGeoKey",[3], None, None), 2061: ("GeogPrimeMeridianLongGeoKey", [12], None, None), # custom prime meridian value in GeogAngularUnits # Projected CS Parameter GeoKeys 3072: ("ProjectedCSTypeGeoKey", [3], None, None), # epsg grid code [20000 - 32760] 3073: ("PCSCitationGeoKey", [2], None, None), # ASCII text 3074: ("ProjectionGeoKey", [3], None, None), # [10000 - 19999] 3075: ("ProjCoordTransGeoKey", [3], None, None), 3076: ("ProjLinearUnitsGeoKey", [3], None, None), 3077: ("ProjLinearUnitSizeGeoKey", [12], None, None), # custom unit in meters 3078: ("ProjStdParallel1GeoKey", [12], None, None), 3079: ("ProjStdParallel2GeoKey", [12], None, None), 3080: ("ProjNatOriginLongGeoKey", [12], None, None), 3081: ("ProjNatOriginLatGeoKey", [12], None, None), 3082: ("ProjFalseEastingGeoKey", [12], None, None), 3083: ("ProjFalseNorthingGeoKey", [12], None, None), 3084: ("ProjFalseOriginLongGeoKey", [12], None, None), 3085: ("ProjFalseOriginLatGeoKey", [12], None, None), 3086: ("ProjFalseOriginEastingGeoKey", [12], None, None), 3087: ("ProjFalseOriginNorthingGeoKey", [12], None, None), 3088: ("ProjCenterLongGeoKey", [12], None, None), 3089: ("ProjCenterLatGeoKey", [12], None, None), 3090: ("ProjCenterEastingGeoKey", [12], None, None), 3091: ("ProjFalseOriginNorthingGeoKey", [12], None, None), 3092: ("ProjScaleAtNatOriginGeoKey", [12], None, None), 3093: ("ProjScaleAtCenterGeoKey", [12], None, None), 3094: ("ProjAzimuthAngleGeoKey", [12], None, None), 3095: ("ProjStraightVertPoleLongGeoKey", [12], None, None), # Vertical CS Parameter Keys 4096: ("VerticalCSTypeGeoKey", [3], None, None), 4097: ("VerticalCitationGeoKey", [2], None, None), 4098: ("VerticalDatumGeoKey", [3], None, None), 4099: ("VerticalUnitsGeoKey", [3], None, None), } _2TAG = dict((v[0], t) for t,v in _TAGS.items()) _2KEY = dict((v, k) for k,v in _2TAG.items()) if __PY3__: import functools reduce = functools.reduce long = int class GkdTag(ifd.TiffTag): strict = True def __init__(self, tag=0x0, value=None, name="GeoTiff Tag"): self.name = name if tag == 0: return self.key, types, default, self.comment = _TAGS.get(tag, ("Unknown", [0,], None, "Undefined tag")) value = default if value == None else value self.tag = tag restricted = getattr(values, self.key, {}) if restricted: reverse = dict((v,k) for k,v in restricted.items()) if value in restricted: self.meaning = restricted.get(value) elif value in reverse: value = reverse[value] self.meaning = value elif GkdTag.strict: raise ValueError('"%s" value must be one of %s, get %s instead' % (self.key, list(restricted.keys()), value)) self.type, self.count, self.value = self._encode(value, types) def __setattr__(self, attr, value): object.__setattr__(self, attr, value) def _encode(self, value, types): if isinstance(value, str): value = value.encode() elif not hasattr(value, "__len__"): value = (value, ) typ = 0 if 2 in types: typ = 34737 elif 12 in types: typ = 34736 return typ, len(value), value def _decode(self): if self.count == 1: return self.value[0] else: return self.value class Gkd(dict): tagname = "Geotiff Tag" version = __geotiff__[0] revision = __geotiff__[1:] def __init__(self, value={}, **pairs): dict.__init__(self) self.from_ifd(value, **pairs) def __getitem__(self, tag): if isinstance(tag, str): tag = _2TAG[tag] return dict.__getitem__(self, tag)._decode() def __setitem__(self, tag, value): if isinstance(tag, str): tag = _2TAG[tag] dict.__setitem__(self, tag, GkdTag(tag, value, name=self.tagname)) def get(self, tag, error=None): if hasattr(self, "_%s" % tag): return getattr(self, "_%s" % tag) else: return dict.get(self, tag, error) def to_ifd(self): _34735, _34736, _34737, nbkey, _ifd = (), (), b"", 0, {} for key,tag in sorted(self.items(), key = lambda a: a[0]): if tag.type == 0: _34735 += (key, 0, 1) + tag.value nbkey += 1 elif tag.type == 34736: # GeoDoubleParamsTag _34735 += (key, 34736, 1, len(_34736)) _34736 += tag.value nbkey += 1 elif tag.type == 34737: # GeoAsciiParamsTag _34735 += (key, 34737, tag.count+1, len(_34737)) _34737 += tag.value + b"|" nbkey += 1 result = ifd.Ifd() result.set(33922, 12, reduce(tuple.__add__, [tuple(e) for e in self.get(33922, ([0.,0.,0.,0.,0.,0.],))])) result.set(33550, 12, tuple(self.get(33550, (1.,1.,1.)))) result.set(34264, 12, tuple(self.get(34264, (1.,0.,0.,0.,0.,-1.,0.,0.,0.,0.,1.,0.,0.,0.,0.,1.)))) result.set(34735, 3, (self.version,) + self.revision + (nbkey,) + _34735) result.set(34736, 12, _34736) result.set(34737, 2, _34737) return result def from_ifd(self, ifd = {}, **kw): pairs = dict(ifd, **kw) for tag in [t for t in [33922, 33550, 34264] if t in pairs]: # ModelTiepointTag, ModelPixelScaleTag, ModelTransformationTag nt = GeoKeyModel[tag] if tag == 33922: # can be more than one TiePoint n = len(nt._fields) seq = ifd[tag] setattr(self, "_%s" % tag, tuple(nt(*seq[i:i+n]) for i in range(0, len(seq), n))) else: setattr(self, "_%s" % tag, nt(*ifd[tag])) if 34736 in pairs: # GeoDoubleParamsTag _34736 = ifd[34736] if 34737 in pairs: # GeoAsciiParamsTag _34737 = ifd[34737] if 34735 in pairs: # GeoKeyDirectoryTag _34735 = ifd[34735] self.version = _34735[0] self.revision = _34735[1:3] for (tag, typ, count, value) in zip(_34735[4::4],_34735[5::4],_34735[6::4],_34735[7::4]): if typ == 0: self[tag] = value elif typ == 34736: self[tag] = _34736[value] elif typ == 34737: self[tag] = _34737[value:value+count-1] def getModelTransformation(self, tie_index=0): if hasattr(self, "_34264"): matrix = GeoKeyModel[34264](*getattr(self, "_34264")) elif hasattr(self, "_33922") and hasattr(self, "_33550"): Sx, Sy, Sz = getattr(self, "_33550") I, J, K, X, Y, Z = getattr(self, "_33922")[tie_index] matrix = GeoKeyModel[34264]( Sx, 0., 0., X - I*Sx, 0., -Sy, 0., Y + J*Sy, 0., 0. , Sz, Z - K*Sz, 0., 0. , 0., 1. ) else: matrix = GeoKeyModel[34264]( 1., 0. , 0., 0., 0., -1., 0., 0., 0., 0. , 1., 0., 0., 0. , 0., 1. ) return lambda x,y,z1=0.,z2=1.,m=matrix: Transform(m, x,y,z1,z2) def tags(self): for v in sorted(dict.values(self), key=lambda e:e.tag): yield v ================================================ FILE: core/lib/Tyf/ifd.py ================================================ # -*- encoding:utf-8 -*- # Copyright 2012-2015, THOORENS Bruno - http://bruno.thoorens.free.fr/licences/tyf.html from . import io, os, tags, encoders, decoders, reduce, values, TYPES, urllib, StringIO import struct, fractions class TiffTag(object): # IFD entries values tag = 0x0 type = 0 count = 0 value = None # end user side values key = "Undefined" name = "Undefined tag" comment = "Nothing about this tag" meaning = None def __init__(self, tag, type=None, value=None, name="Tiff tag"): self.key, _typ, default, self.comment = tags.get(tag) self.tag = tag self.name = name self.type = _typ[-1] if type == None else type if value != None: self._encode(value) elif default != None: self.value = (default,) if not hasattr(default, "len") else default def __setattr__(self, attr, value): if attr == "type": try: object.__setattr__(self, "_encoder", getattr(encoders, "_%s"%hex(self.tag))) except AttributeError: object.__setattr__(self, "_encoder", getattr(encoders, "_%s"%value)) try: object.__setattr__(self, "_decoder", getattr(decoders, "_%s"%hex(self.tag))) except AttributeError: object.__setattr__(self, "_decoder", getattr(decoders, "_%s"%value)) elif attr == "value": restricted = getattr(values, self.key, None) if restricted != None: v = value[0] if isinstance(value, tuple) else value self.meaning = restricted.get(v, "no description found [%r]" % (v,)) self.count = len(value) // (1 if self.type not in [5,10] else 2) self._determine_if_offset() object.__setattr__(self, attr, value) def __repr__(self): return "<%s 0x%x: %s = %r>" % (self.name, self.tag, self.key, self.value) + ("" if not self.meaning else ' := %r'%self.meaning) def _encode(self, value): self.value = self._encoder(value) def _decode(self): return self._decoder(self.value) def _determine_if_offset(self): if self.count == 1 and self.type in [1, 2, 3, 4, 6, 7, 8, 9]: setattr(self, "value_is_offset", False) elif self.count <= 2 and self.type in [3, 8]: setattr(self, "value_is_offset", False) elif self.count <= 4 and self.type in [1, 2, 6, 7]: setattr(self, "value_is_offset", False) else: setattr(self, "value_is_offset", True) def _fill(self): s = struct.calcsize("="+TYPES[self.type][0]) voidspace = (struct.calcsize("=L") - self.count*s)//s if self.type in [2, 7]: return self.value + b"\x00"*voidspace elif self.type in [1, 3, 6, 8]: return self.value + ((0,)*voidspace) return self.value def calcsize(self): return struct.calcsize("=" + TYPES[self.type][0] * (self.count*(2 if self.type in [5,10] else 1))) if self.value_is_offset else 0 class Ifd(dict): tagname = "Tiff Tag" exif_ifd = property(lambda obj: obj.sub_ifd.get(34665, {}), None, None, "shortcut to EXIF sub ifd") gps_ifd = property(lambda obj: obj.sub_ifd.get(34853, {}), None, None, "shortcut to GPS sub ifd") has_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") raster_loaded = property(lambda obj: not(obj.has_raster) or bool(len(obj.stripes+obj.tiles+obj.free)+len(obj.jpegIF)), None, None, "") size = property( lambda obj: { "ifd": struct.calcsize("=H" + (len(obj)*"HHLL") + "L"), "data": reduce(int.__add__, [t.calcsize() for t in dict.values(obj)]) }, None, None, "return ifd-packed size and data-packed size") def __init__(self, sub_ifd={}, **kwargs): self._sub_ifd = sub_ifd setattr(self, "tagname", kwargs.pop("tagname", "Tiff tag")) dict.__init__(self) self.sub_ifd = {} self.stripes = () self.tiles = () self.free = () self.jpegIF = b"" def __setitem__(self, tag, value): for t,(ts,tname) in self._sub_ifd.items(): tag = tags._2tag(tag, family=ts) if tag in ts: if not t in self.sub_ifd: self.sub_ifd[t] = Ifd(sub_ifd={}, tagname=tname) self.sub_ifd[t].addtag(TiffTag(tag, value=value)) return else: tag = tags._2tag(tag) dict.__setitem__(self, tag, TiffTag(tag, value=value, name=self.tagname)) def __getitem__(self, tag): for i in self.sub_ifd.values(): try: return i[tag] except KeyError: pass return dict.__getitem__(self, tags._2tag(tag))._decode() def _check(self): for key in self.sub_ifd: if key not in self: self.addtag(TiffTag(key, 4, 0, name=self.tagname)) def set(self, tag, typ, value): for t,(ts,tname) in self._sub_ifd.items(): if tag in ts: if not t in self.sub_ifd: self.sub_ifd[t] = Ifd(sub_ifd={}, tagname=tname) self.sub_ifd[t].set(tag, typ, value) return tifftag = TiffTag(tag=tag, type=typ, name=self.tagname) tifftag.value = (value,) if not hasattr(value, "__len__") else value tifftag.name = self.tagname dict.__setitem__(self, tag, tifftag) def get(self, tag): for i in self.sub_ifd.values(): if tag in i: return i.get(tag) return dict.get(self, tags._2tag(tag)) def addtag(self, tifftag): if isinstance(tifftag, TiffTag): tifftag.name = self.tagname dict.__setitem__(self, tifftag.tag, tifftag) def tags(self): for v in sorted(dict.values(self), key=lambda e:e.tag): yield v for i in self.sub_ifd.values(): for v in sorted(dict.values(i), key=lambda e:e.tag): yield v def set_location(self, longitude, latitude, altitude=0.): if 34853 not in self._sub_ifd: self._sub_ifd[34853] = [tags.gpsT, "GPS tag"] self[1] = self[2] = latitude self[3] = self[4] = longitude self[5] = self[6] = altitude def get_location(self): if set([1,2,3,4,5,6]) <= set(self.gps_ifd.keys()): return ( self[3] * self[4], self[1] * self[2], self[5] * self[6] ) def load_location(self, zoom=15, size="256x256", mcolor="0xff00ff", format="png", scale=1): if set([1,2,3,4]) <= set(self.gps_ifd.keys()): gps_ifd = self.gps_ifd latitude = gps_ifd[1] * gps_ifd[2] longitude = gps_ifd[3] * gps_ifd[4] try: opener = 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" % ( latitude, longitude, zoom, size, mcolor, latitude, longitude, format, scale )) except: return StringIO() else: return StringIO(opener.read()) print("googleapis connexion error") else: return StringIO() def dump_location(self, tilename, zoom=15, size="256x256", mcolor="0xff00ff", format="png", scale=1): if set([1,2,3,4]) <= set(self.gps_ifd.keys()): gps_ifd = self.gps_ifd latitude = gps_ifd[1] * gps_ifd[2] longitude = gps_ifd[3] * gps_ifd[4] try: urllib.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" % ( latitude, longitude, zoom, size, mcolor, latitude, longitude, format, scale ), os.path.splitext(tilename)[0] + "."+format ) except: print("googleapis connexion error") ================================================ FILE: core/lib/Tyf/tags.py ================================================ # -*- encoding:utf-8 -*- # Copyright 2012-2015, THOORENS Bruno - http://bruno.thoorens.free.fr/licences/tyf.html # ~ http://wwwawaresystemsbe/imaging/tiff/tifftagshtml from . import values # Baseline TIFF tags bTT = { 254: ("NewSubfileType", [1], 0, "A general indication of the kind of data contained in this subfile"), 255: ("SubfileType", [1], None, "Deprecated, use NewSubfiletype instead"), 256: ("ImageWidth", [1,4], None, "Number of columns in the image, ie, the number of pixels per row"), 257: ("ImageLength", [1,4], None, "Number of rows of pixels in the image"), 258: ("BitsPerSample", [1], 1, "Array size = SamplesPerPixel, number of bits per component"), 259: ("Compression", [1], 1, "Compression scheme used on the image data"), 262: ("PhotometricInterpretation", [1], None, "The color space of the image data"), 263: ("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"), 264: ("CellWidth", [1], None, "The width of the dithering or halftoning matrix used to create a dithered or halftoned bilevel file"), 265: ("CellLength", [1], None, "The length of the dithering or halftoning matrix used to create a dithered or halftoned bilevel file"), 266: ("FillOrder", [1], 1, "The logical order of bits within a byt"), 270: ("ImageDescription", [2], None, "A string that describes the subject of the image"), 271: ("Make", [2], None, "The scanner manufacturer"), 272: ("Model", [2], None, "The scanner model name or number"), 273: ("StripOffsets", [1,4], None, "For each strip, the byte offset of that strip"), 274: ("Orientation", [1], 1, "The orientation of the image with respect to the rows and columns"), 277: ("SamplesPerPixel", [1], 1, "The number of components per pixel"), 278: ("RowsPerStrip", [1,4], 2**32-1, "The number of rows per strip"), 279: ("StripByteCounts", [1,4], None, "For each strip, the number of bytes in the strip after compression"), 280: ("MinSampleValue", [1], 0, "The minimum component value used"), 281: ("MaxSampleValue", [1], 1, "The maximum component value used"), 282: ("XResolution", [5], None, "The number of pixels per ResolutionUnit in the ImageWidth direction"), 283: ("YResolution", [5], None, "The number of pixels per ResolutionUnit in the ImageLength direction"), 284: ("PlanarConfiguration", [1], 1, "How the components of each pixel are stored"), 288: ("FreeOffsets", [4], None, "For each string of contiguous unused bytes in a TIFF file, the byte offset of the string"), 289: ("FreeByteCounts", [4], None, "For each string of contiguous unused bytes in a TIFF file, the number of bytes in the string"), 290: ("GrayResponseUnit", [1], 2, "The precision of the information contained in the GrayResponseCurve"), 291: ("GrayResponseCurve", [1], 2, "Array size = 2**SamplesPerPixel"), 296: ("ResolutionUnit", [4], 2, "The unit of measurement for XResolution and YResolution"), 305: ("Software", [2], None, "Name and version number of the software package(s) used to create the image"), 306: ("DateTime", [2], None, "Date and time of image creation, aray size = 20, 'YYYY:MM:DD HH:MM:SS\0'"), 315: ("Artist", [2], None, "Person who created the image"), 316: ("HostComputer", [2], None, "The computer and/or operating system in use at the time of image creation"), 320: ("ColorMap", [1], None, "A color map for palette color images"), 338: ("ExtraSamples", [1], 1, "Description of extra components"), 33432: ("Copyright", [2], None, "Copyright notice"), } # Extension TIFF tags xTT = { 269: ("DocumentName", [2], None, "The name of the document from which this image was scanned"), 285: ("PageName", [2], None, "The name of the page from which this image was scanned"), 286: ("XPosition", [5], None, "X position of the image"), 287: ("YPosition", [5], None, "Y position of the image"), 292: ("T4Options", [4], 0, "Options for Group 3 Fax compression"), 293: ("T6Options", [4], 0, "Options for Group 6 Fax compression"), 297: ("PageNumber", [1], None, "The page number of the page from which this image was scanned"), 301: ("TransferFunction", [1], 1*(1<<1), "Describes a transfer function for the image in tabular style"), 317: ("Predictor", [3], 1, "A mathematical operator that is applied to the image data before an encoding scheme is applied"), 318: ("WhitePoint", [5], None, "The chromaticity of the white point of the image"), 319: ("PrimaryChromaticies", [5], None, "The chromaticities of the primaries of the image"), 321: ("HalftoneHints", [1], None, "Conveys to the halftone function the range of gray levels within a colorimetrically-specified image that should retain tonal detail"), 322: ("TileWidth", [1,4], None, "The tile width in pixels This is the number of columns in each tile"), 323: ("TileLength", [1,4], None, "The tile length (height) in pixels This is the number of rows in each tile"), 324: ("TileOffsets", [4], None, "For each tile, the byte offset of that tile, as compressed and stored on disk"), 325: ("TileByteCounts", [1,4], None, "For each tile, the number of (compressed) bytes in that tile"), 326: ("BadFaxLinea", [1,4], None, "Used in the TIFF-F standard, denotes the number of 'bad' scan lines encountered by the facsimile device"), 327: ("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"), 328: ("ConsecutiveBadFaxLines", [1,4], None, "Used in the TIFF-F standard, denotes the maximum number of consecutive 'bad' scanlines received"), 328: ("SubIFDs", [2,4], None, "Offset to child IFDs"), # ??? 332: ("InkSet", [1], None, "The set of inks used in a separated (PhotometricInterpretation=5) image"), 333: ("InkNames", [2], None, "The name of each ink used in a separated image"), 334: ("NumberOfInks", [1], 4, "The number of inks"), 336: ("DotRange", [1,3], (0,1), "The component values that correspond to a 0%% dot and 100%% dot"), 337: ("TargetPrinter", [2], None, "A description of the printing environment for which this separation is intended"), 339: ("SampleFormat", [1], 1, "Specifies how to interpret each data sample in a pixel"), 340: ("SMinSampleValue", [3,7,8,12], None, "Specifies the minimum sample value"), 341: ("SMaxSampleValue", [3,7,8,12], None, "Specifies the maximum sample value"), 342: ("TransferRange", [1], None, "Expands the range of the TransferFunction"), 343: ("ClipPath", [3], None, "Mirrors the essentials of PostScript's path creation functionality"), 344: ("XClipPathUnits", [4], None, "The number of units that span the width of the image, in terms of integer ClipPath coordinates"), 345: ("YClipPathUnits", [4], None, "The number of units that span the height of the image, in terms of integer ClipPath coordinates"), 346: ("Indexed", [1], 0, "Aims to broaden the support for indexed images to include support for any color space"), 347: ("JPEGTables", [7], None, "JPEG quantization and/or Huffman tables"), 351: ("OPIProxy", [1], 0, "OPI-related"), 400: ("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"), 401: ("ProfileType", [4], None, "Used in the TIFF-FX standard, denotes the type of data stored in this file or IFD"), 402: ("FaxProfile", [3], None, "Used in the TIFF-FX standard, denotes the 'profile' that applies to this file"), 403: ("CodingMethods", [4], None, "Used in the TIFF-FX standard, indicates which coding methods are used in the file"), 404: ("VersionYear", [3], None, "Used in the TIFF-FX standard, denotes the year of the standard specified by the FaxProfile field"), 405: ("ModeNumber", [3], None, "Used in the TIFF-FX standard, denotes the mode of the standard specified by the FaxProfile field"), 433: ("Decode", [10],None, "Used in the TIFF-F and TIFF-FX standards, holds information about the ITULAB (PhotometricInterpretation = 10) encoding"), 434: ("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"), 512: ("JPEGProc", [1], None, "Old-style JPEG compression field TechNote2 invalidates this part of the specification"), 513: ("JPEGInterchangeFormat", [4], None, "Old-style JPEG compression field TechNote2 invalidates this part of the specification"), 514: ("JPEGInterchangeFormatLength", [4], None, "Old-style JPEG compression field TechNote2 invalidates this part of the specification"), 515: ("JPEGRestartInterval", [1], None, "Old-style JPEG compression field TechNote2 invalidates this part of the specification"), 517: ("JPEGLosslessPredictors", [1], None, "Old-style JPEG compression field TechNote2 invalidates this part of the specification"), 518: ("JPEGPointTransforms", [1], None, "Old-style JPEG compression field TechNote2 invalidates this part of the specification"), 519: ("JPEGQTables", [4], None, "Old-style JPEG compression field TechNote2 invalidates this part of the specification"), 520: ("JPEGDCTables", [4], None, "Old-style JPEG compression field TechNote2 invalidates this part of the specificationl"), 521: ("JPEGACTables", [4], None, "Old-style JPEG compression field TechNote2 invalidates this part of the specification"), 529: ("YCbCrCoefficients", [5], (299,1000,587,1000,114,1000), "The transformation from RGB to YCbCr image data"), 530: ("YCbCrSubSampling", [1], (2,2), "Specifies the subsampling factors used for the chrominance components of a YCbCr image"), 531: ("YCbCrPositioning", [1], 1, "Specifies the positioning of subsampled chrominance components relative to luminance samples"), 532: ("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"), 559: ("StripRowCounts", [4], None, "Defined in the Mixed Raster Content part of RFC 2301, used to replace RowsPerStrip for IFDs with variable-sized strips"), 700: ("XMP", [3], None, "XML packet containing XMP metadata"), 32781: ("ImageID", [2], None, "OPI-related"), 34732: ("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"), } # Private TIFF tags pTT = { 32932: ("Wang Annotation", [3], None, "Annotation data, as used in 'Imaging for Windows'"), 33445: ("MD FileTag", [4], None, "Specifies the pixel data format encoding in the Molecular Dynamics GEL file format"), 33446: ("MD ScalePixel", [5], None, "Specifies a scale factor in the Molecular Dynamics GEL file format"), 33447: ("MD ColorTable", [1], None, "Used to specify the conversion from 16bit to 8bit in the Molecular Dynamics GEL file format"), 33448: ("MD LabName", [2], None, "Name of the lab that scanned this file, as used in the Molecular Dynamics GEL file format"), 33449: ("MD SampleInfo", [2], None, "Information about the sample, as used in the Molecular Dynamics GEL file format"), 33450: ("MD PrepDate", [2], None, "Date the sample was prepared, as used in the Molecular Dynamics GEL file format"), 33451: ("MD PrepTime", [2], None, "Time the sample was prepared, as used in the Molecular Dynamics GEL file format"), 33452: ("MD FileUnits", [2], None, "Units for data in this file, as used in the Molecular Dynamics GEL file format"), 33550: ("ModelPixelScaleTag", [12], None, "Used in interchangeable GeoTIFF files"), 33723: ("IPTC", [3,7], None, "IPTC (International Press Telecommunications Council) metadata"), 33918: ("INGR Packet Data Tag", [3], None, "Intergraph Application specific storage"), 33919: ("INGR Flag Registers", [4], None, "Intergraph Application specific flags"), 33920: ("IrasB Transformation Matrix", [12], None, "Originally part of Intergraph's GeoTIFF tags, but likely understood by IrasB only"), 33922: ("ModelTiepointTag", [12], None, "Originally part of Intergraph's GeoTIFF tags, but now used in interchangeable GeoTIFF files"), 34264: ("ModelTransformationTag", [12], None, "Used in interchangeable GeoTIFF files"), 34377: ("Photoshop", [1], None, "Collection of Photoshop 'Image Resource Blocks'"), 34665: ("Exif IFD", [4], None, "A pointer to the Exif IFD"), 34675: ("ICC Profile", [7], None, "ICC profile data"), 34735: ("GeoKeyDirectoryTag", [3], None, "Used in interchangeable GeoTIFF files"), 34736: ("GeoDoubleParamsTag", [12], None, "Used in interchangeable GeoTIFF files"), 34737: ("GeoAsciiParamsTag", [2], None, "Used in interchangeable GeoTIFF files"), 34853: ("GPS IFD", [4], None, "A pointer to the Exif-related GPS Info IFD"), 34908: ("HylaFAX FaxRecvParams", [4], None, "Used by HylaFAX"), 34909: ("HylaFAX FaxSubAddress", [2], None, "Used by HylaFAX"), 34910: ("HylaFAX FaxRecvTime", [4], None, "Used by HylaFAX"), 37724: ("ImageSourceData", [7], None, "Used by Adobe Photoshop"), 40965: ("Interoperability IFD", [4], None, "A pointer to the Exif-related Interoperability IFD"), 42112: ("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"), 42113: ("GDAL_NODATA", [2], None, "Used by the GDAL library, contains an ASCII encoded nodata or background pixel value"), 50215: ("Oce Scanjob Description", [2], None, "Used in the Oce scanning process"), 50216: ("Oce Application Selector", [2], None, "Used in the Oce scanning process"), 50217: ("Oce Identification Number", [2], None, "Used in the Oce scanning process"), 50218: ("Oce ImageLogic Characteristics", [2], None, "Used in the Oce scanning process"), 50706: ("DNGVersion", [3], None, "Used in IFD 0 of DNG files"), 50707: ("DNGBackwardVersion", [3], None, "Used in IFD 0 of DNG files"), 50708: ("UniqueCameraModel", [2], None, "Used in IFD 0 of DNG files"), 50709: ("LocalizedCameraModel", [2,3], None, "Used in IFD 0 of DNG files"), 50710: ("CFAPlaneColor", [3], None, "Used in Raw IFD of DNG files"), 50711: ("CFALayout", [1], None, "Used in Raw IFD of DNG files"), 50712: ("LinearizationTable", [1], None, "Used in Raw IFD of DNG files"), 50713: ("BlackLevelRepeatDim", [1], None, "Used in Raw IFD of DNG files"), 50714: ("BlackLevel", [1,4,5], None, "Used in Raw IFD of DNG files"), 50715: ("BlackLevelDeltaH", [10], None, "Used in Raw IFD of DNG files"), 50716: ("BlackLevelDeltaV", [10], None, "Used in Raw IFD of DNG files"), 50717: ("WhiteLevel", [1,4], None, "Used in Raw IFD of DNG files"), 50718: ("DefaultScale", [5], None, "Used in Raw IFD of DNG files"), 50719: ("DefaultCropOrigin", [1,4,5], None, "Used in Raw IFD of DNG files"), 50720: ("DefaultCropSize", [1,4,5], None, "Used in Raw IFD of DNG files"), 50721: ("ColorMatrix1", [10], None, "Used in IFD 0 of DNG files"), 50722: ("ColorMatrix2", [10], None, "Used in IFD 0 of DNG files"), 50723: ("CameraCalibration1", [10], None, "Used in IFD 0 of DNG files"), 50724: ("CameraCalibration2", [10], None, "Used in IFD 0 of DNG files"), 50725: ("ReductionMatrix1", [10], None, "Used in IFD 0 of DNG files"), 50726: ("ReductionMatrix2", [10], None, "Used in IFD 0 of DNG files"), 50727: ("AnalogBalance", [5], None, "Used in IFD 0 of DNG files"), 50728: ("AsShotNeutral", [1,5], None, "Used in IFD 0 of DNG files"), 50729: ("AsShotWhiteXY", [5], None, "Used in IFD 0 of DNG files"), 50730: ("BaselineExposure", [10], None, "Used in IFD 0 of DNG files"), 50731: ("BaselineNoise", [10], None, "Used in IFD 0 of DNG files"), 50732: ("BaselineSharpness", [10], None, "Used in IFD 0 of DNG files"), 50733: ("BayerGreenSplit", [4], None, "Used in Raw IFD of DNG files"), 50734: ("LinearResponseLimit", [5], None, "Used in IFD 0 of DNG files"), 50735: ("CameraSerialNumber", [2], None, "Used in IFD 0 of DNG files"), 50736: ("LensInfo", [5], None, "Used in IFD 0 of DNG files"), 50737: ("ChromaBlurRadius", [5], None, "Used in Raw IFD of DNG files"), 50738: ("AntiAliasStrength", [5], None, "Used in Raw IFD of DNG files"), 50740: ("DNGPrivateData", [3], None, "Used in IFD 0 of DNG files"), 50741: ("MakerNoteSafety", [1], None, "Used in IFD 0 of DNG files"), 50778: ("CalibrationIlluminant1", [1], None, "Used in IFD 0 of DNG files"), 50779: ("CalibrationIlluminant2", [1], None, "Used in IFD 0 of DNG files"), 50780: ("BestQualityScale", [5], None, "Used in Raw IFD of DNG files"), 50784: ("Alias Layer Metadata", [2], None, "Alias Sketchbook Pro layer usage description"), # XP tags 0x9c9b: ("XPTitle", [4], None, ""), 0x9c9c: ("XPComment", [4], None, ""), 0x9c9d: ("XPAuthor", [4], None, ""), 0x9c9e: ("XPKeywords", [4], None, ""), 0x9c9f: ("XPSubject", [4], None, ""), 0xea1c: ("Padding", [7], None, ""), 0xea1d: ("OffsetSchema", [9], None, ""), } exfT = { 33434: ("ExposureTime", [5], None, "Exposure time, given in seconds"), 33437: ("FNumber", [5], None, "The F number"), 34850: ("ExposureProgram", [1], 0, "The class of the program used by the camera to set exposure when the picture is taken"), 34852: ("SpectralSensitivity", [2], None, "Indicates the spectral sensitivity of each channel of the camera used"), 34855: ("ISOSpeedRatings", [1], None, "Indicates the ISO Speed and ISO Latitude of the camera or input device as specified in ISO 12232"), 34856: ("OECF", [7], None, "Indicates the Opto-Electric Conversion Function (OECF) specified in ISO 14524"), 36864: ("ExifVersion", [7], b"0220", "The version of the supported Exif standard"), 36867: ("DateTimeOriginal", [2], None, "The date and time when the original image data was generated"), 36868: ("DateTimeDigitized", [2], None, "The date and time when the image was stored as digital data"), 37121: ("ComponentsConfiguration", [7], None, "Specific to compressed data; specifies the channels and complements PhotometricInterpretation"), 37122: ("CompressedBitsPerPixel", [5], None, "Specific to compressed data; states the compressed bits per pixel"), 37377: ("ShutterSpeedValue", [11], None, "Shutter speed"), 37378: ("ApertureValue", [5], None, "The lens aperture"), 37379: ("BrightnessValue", [5], None, "The value of brightness"), 37380: ("ExposureBiasValue", [11], None, "The exposure bias"), 37381: ("MaxApertureValue", [5], None, "The smallest F number of the lens"), 37382: ("SubjectDistance", [5], None, "The distance to the subject, given in meters"), 37383: ("MeteringMode", [1], 0, "The metering mode"), 37384: ("LightSource", [1], 0, "The kind of light source"), 37385: ("Flash", [1], None, "Indicates the status of flash when the image was shot"), 37386: ("FocalLength", [5], None, "The actual focal length of the lens, in mm"), 37396: ("SubjectArea", [1], None, "Indicates the location and area of the main subject in the overall scene"), 37500: ("MakerNote", [7], None, "Manufacturer specific information"), 37510: ("UserComment", [7], None, "Keywords or comments on the image; complements ImageDescription"), 37520: ("SubsecTime", [2], None, "A tag used to record fractions of seconds for the DateTime tag"), 37521: ("SubsecTimeOriginal", [2], None, "A tag used to record fractions of seconds for the DateTimeOriginal tag"), 37522: ("SubsecTimeDigitized", [2], None, "A tag used to record fractions of seconds for the DateTimeDigitized tag"), 40960: ("FlashpixVersion", [7], b"0100", "The Flashpix format version supported by a FPXR file"), 40961: ("ColorSpace", [1], None, "The color space information tag is always recorded as the color space specifier"), 40962: ("PixelXDimension", [1,4], None, "Specific to compressed data; the valid width of the meaningful image"), 40963: ("PixelYDimension", [1,4], None, "Specific to compressed data; the valid height of the meaningful image"), 40964: ("RelatedSoundFile", [2], None, "Used to record the name of an audio file related to the image data"), 41483: ("FlashEnergy", [5], None, "Indicates the strobe energy at the time the image is captured, as measured in Beam Candle Power Seconds"), 41484: ("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"), 41486: ("FocalPlaneXResolution", [5], None, "Indicates the number of pixels in the image width (X) direction per FocalPlaneResolutionUnit on the camera focal plane"), 41487: ("FocalPlaneYResolution", [5], None, "Indicates the number of pixels in the image height (Y) direction per FocalPlaneResolutionUnit on the camera focal plane"), 41488: ("FocalPlaneResolutionUnit", [1], 2, "Indicates the unit for measuring FocalPlaneXResolution and FocalPlaneYResolution"), 41492: ("SubjectLocation", [2], None, "Indicates the location of the main subject in the scene"), 41493: ("ExposureIndex", [5], None, "Indicates the exposure index selected on the camera or input device at the time the image is captured"), 41495: ("SensingMethod", [1], None, "Indicates the image sensor type on the camera or input device"), 41728: ("FileSource", [7], b"3", "Indicates the image source"), 41729: ("SceneType", [7], b"1", "Indicates the type of scene"), 41730: ("CFAPattern", [7], None, "Indicates the color filter array (CFA) geometric pattern of the image sensor when a one-chip color area sensor is used"), 41985: ("CustomRendered", [1], 0, "Indicates the use of special processing on image data, such as rendering geared to output"), 41986: ("ExposureMode", [1], None, "Indicates the exposure mode set when the image was shot"), 41987: ("WhiteBalance", [1], None, "Indicates the white balance mode set when the image was shot"), 41988: ("DigitalZoomRatio", [5], None, "Indicates the digital zoom ratio when the image was shot"), 41989: ("FocalLengthIn35mmFilm", [1], None, "Indicates the equivalent focal length assuming a 35mm film camera, in mm"), 41990: ("SceneCaptureType", [1], 0, "Indicates the type of scene that was shot"), 41991: ("GainControl", [1], None, "Indicates the degree of overall image gain adjustment"), 41992: ("Contrast", [1], 0, "Indicates the direction of contrast processing applied by the camera when the image was shot"), 41993: ("Saturation", [1], 0, "Indicates the direction of saturation processing applied by the camera when the image was shot"), 41994: ("Sharpness", [1], 0, "Indicates the direction of sharpness processing applied by the camera when the image was shot"), 41995: ("DeviceSettingDescription", [7], None, "This tag indicates information on the picture-taking conditions of a particular camera model"), 41996: ("SubjectDistanceRange", [1], None, "Indicates the distance to the subject"), 42016: ("ImageUniqueID", [2], None, "Indicates an identifier assigned uniquely to each image"), } gpsT = { 0: ("GPSVersionID", [3], (2,2,0,0), "Indicates the version of GPSInfoIFD"), 1: ("GPSLatitudeRef", [2], None, "Indicates whether the latitude is north or south latitude"), 2: ("GPSLatitude", [5], None, "Indicates the latitude"), 3: ("GPSLongitudeRef", [2], None, "Indicates whether the longitude is east or west longitude"), 4: ("GPSLongitude", [5], None, "Indicates the longitude"), 5: ("GPSAltitudeRef", [3], None, "Indicates the altitude used as the reference altitude"), 6: ("GPSAltitude", [5], None, "Indicates the altitude based on the reference in GPSAltitudeRef"), 7: ("GPSTimeStamp", [5], None, "Indicates the time as UTC (Coordinated Universal Time)"), 8: ("GPSSatellites", [2], None, "Indicates the GPS satellites used for measurements"), 9: ("GPSStatus", [2], None, "Indicates the status of the GPS receiver when the image is recorded"), 10: ("GPSMeasureMode", [2], None, "Indicates the GPS measurement mode"), 11: ("GPSDOP", [5], None, "Indicates the GPS DOP (data degree of precision)"), 12: ("GPSSpeedRef", [2], b'K\x00', "Indicates the unit used to express the GPS receiver speed of movement"), 13: ("GPSSpeed", [5], None, "Indicates the speed of GPS receiver movem5nt"), 14: ("GPSTrackRef", [2], b'T\x00', "Indicates the reference for giving the direction of GPS receiver movement"), 15: ("GPSTrack", [5], None, "Indicates the direction of GPS receiver movement"), 16: ("GPSImgDirectionRef", [2], b'T\x00', "Indicates the reference for giving the direction of the image when it is captured"), 17: ("GPSImgDirection", [5], None, "Indicates the direction of the image when it was captured"), 18: ("GPSMapDatum", [2], None, "Indicates the geodetic survey data used by the GPS receiver"), 19: ("GPSDestLatitudeRef", [2], None, "Indicates whether the latitude of the destination point is north or south latitude"), 20: ("GPSDestLatitude", [5], None, "Indicates the latitude of the destination point"), 21: ("GPSDestLongitudeRef", [2], None, "Indicates whether the longitude of the destination point is east or west longitude"), 22: ("GPSDestLongitude", [5], None, "Indicates the longitude of the destination point"), 23: ("GPSDestBearingRef", [2], None, "Indicates the reference used for giving the bearing to the destination point"), 24: ("GPSDestBearing", [5], None, "Indicates the bearing to the destination point"), 25: ("GPSDestDistanceRef", [2], None, "Indicates the unit used to express the distance to the destination point"), 26: ("GPSDestDistance", [5], None, "Indicates the distance to the destination point"), 27: ("GPSProcessingMethod", [7], None, "A character string recording the name of the method used for location finding"), 28: ("GPSAreaInformation", [7], None, "A character string recording the name of the GPS area"), 29: ("GPSDateStamp", [2], None, "A character string recording date and time information relative to UTC (Coordinated Universal Time)"), 30: ("GPSDifferential", [1], None, "Indicates whether differential correction is applied to the GPS receiver"), } _TAG_FAMILIES = [bTT, xTT, pTT, exfT, gpsT] _TAG_FAMILIES_2TAG = [dict((v[0], t) for t,v in dic.items()) for dic in _TAG_FAMILIES] _TAG_FAMILIES_2KEY = [dict((v, k) for k,v in dic.items()) for dic in _TAG_FAMILIES_2TAG] def get(tag): idx = 0 for dic in _TAG_FAMILIES: if isinstance(tag, (bytes, str)): tag = _TAG_FAMILIES_2TAG[idx][tag] if tag in dic: return dic[tag] return ("Unknown", [4], None, "Undefined tag 0x%x"%tag) def _2tag(tag, family=None): if family != None: idx = _TAG_FAMILIES.index(family) if isinstance(tag, (bytes, str)): if tag in _TAG_FAMILIES_2TAG[idx]: return _TAG_FAMILIES_2TAG[idx][tag] return tag else: return tag elif isinstance(tag, (bytes, str)): for dic in _TAG_FAMILIES_2TAG: if tag in dic: return dic[tag] return tag else: return tag ================================================ FILE: core/lib/Tyf/values.py ================================================ # -*- encoding:utf-8 -*- # Copyright 2012-2015, THOORENS Bruno - http://bruno.thoorens.free.fr/licences/tyf.html ## Tiff tag values NewSubfileType = { 0: "bit flag 000", 1: "bit flag 001", 2: "bit flag 010", 3: "bit flag 011", 4: "bit flag 100", 5: "bit flag 101", 6: "bit flag 110", 7: "bit flag 111" } SubfileType = { 1: "Full-resolution image data", 2: "Reduced-resolution image data", 3: "Single page of a multi-page image" } Compression = { 1: "Uncompressed", 2: "CCITT 1d", 3: "Group 3 Fax", 4: "Group 4 Fax", 5: "LZW", 6: "JPEG", 7: "JPEG ('new-style' JPEG)", 8: "Deflate ('Adobe-style')", 9: "TIFF-F and TIFF-FX standard (RFC 2301) B&W", 10: "TIFF-F and TIFF-FX standard (RFC 2301) RGB", 32771: "CCITTRLEW", # 16-bit padding 32773: "PACKBITS", 32809: "THUNDERSCAN", 32895: "IT8CTPAD", 32896: "IT8LW", 32897: "IT8MP", 32908: "PIXARFILM", 32909: "PIXARLOG", 32946: "DEFLATE", 32947: "DCS", 34661: "JBIG", 34676: "SGILOG", 34677: "SGILOG24", 34712: "JP2000", } PhotometricInterpretation = { 0: "WhiteIsZero", 1: "BlackIsZero", 2: "RGB", 3: "RGB Palette", 4: "Transparency Mask", 5: "CMYK", 6: "YCbCr", 8: "CIE L*a*b*", 9: "ICC L*a*b*", 10: "ITU L*a*b*", 32803: "CFA", # TIFF/EP, Adobe DNG 32892: "LinearRaw" # Adobe DNG } Thresholding = { 1: "No dithering or halftoning has been applied to the image data", 2: "An ordered dither or halftone technique has been applied to the image data", 3: "A randomized process such as error diffusion has been applied to the image data" } FillOrder = { 1: "Values stored in the higher-order bits of the byte", 2: "Values stored in the lower-order bits of the byte" } Orientation = { # 1 2 3 4 5 6 7 8 # 888888 888888 88 88 8888888888 88 88 8888888888 # 88 88 88 88 88 88 88 88 88 88 88 88 # 8888 8888 8888 8888 88 8888888888 8888888888 88 # 88 88 88 88 # 88 88 888888 888888 1: "Normal", 2: "Fliped left to right", 3: "Rotated 180 deg", 4: "Fliped top to bottom", 5: "Fliped left to right + rotated 90 deg counter clockwise", 6: "Rotated 90 deg counter clockwise", 7: "Fliped left to right + rotated 90 deg clockwise", 8: "Rotated 90 deg clockwise" } PlanarConfiguration = { 1: "Chunky", #, format: RGBARGBARGBA....RGBA 2: "Planar" #, format: RRR.RGGG.GBBB.BAAA.A } GrayResponseUnit = { 1: "Number represents tenths of a unit", 2: "Number represents hundredths of a unit", 3: "Number represents thousandths of a unit", 4: "Number represents ten-thousandths of a unit", 5: "Number represents hundred-thousandths of a unit" } ResolutionUnit = { 1:"No unit", 2:"Inch", 3:"Centimeter" } T4Options = { 0: "bit flag 000", 1: "bit flag 001", 2: "bit flag 010", 3: "bit flag 011", 4: "bit flag 100", 5: "bit flag 101", 6: "bit flag 110", 7: "bit flag 111" } T6Options = { 0: "bit flag 00", 2: "bit flag 10", } Predictor = { 1: "No prediction", 2: "Horizontal differencing", 3: "Floating point horizontal differencing" } CleanFaxData = { 0: "No 'bad' lines", 1: "'bad' lines exist, but were regenerated by the receiver", 2: "'bad' lines exist, but have not been regenerated" } InkSet = { 1:"CMYK", 2:"Not CMYK" } SampleFormat = { 1: "Unsigned integer data", 2: "Two's complement signed integer data", 3: "IEEE floating point data [IEEE]", 4: "Undefined data format" } Indexed = { 0: "Not indexed", 1: "Indexed" } OPIProxy = { 0: "A higher-resolution version of this image does not exist", 1: "A higher-resolution version of this image exists, and the name of that image is found in the ImageID tag" } ProfileType = { 0: "Unspecified", 1: "Group 3 fax" } FaxProfile = { 0: "Does not conform to a profile defined for TIFF for facsimile", 1: "Minimal black & white lossless, Profile S", 2: "Extended black & white lossless, Profile F", 3: "Lossless JBIG black & white, Profile J", 4: "Lossy color and grayscale, Profile C", 5: "Lossless color and grayscale, Profile L", 6: "Mixed Raster Content, Profile M" } CodingMethods = { 0b1 : "Unspecified compression", 0b10 : "1-dimensional coding, ITU-T Rec. T.4 (MH - Modified Huffman)", 0b100 : "2-dimensional coding, ITU-T Rec. T.4 (MR - Modified Read)", 0b1000 : "2-dimensional coding, ITU-T Rec. T.6 (MMR - Modified MR)", 0b10000 : "ITU-T Rec. T.82 coding, using ITU-T Rec. T.85 (JBIG)", 0b100000 : "ITU-T Rec. T.81 (Baseline JPEG)", 0b1000000 : "ITU-T Rec. T.82 coding, using ITU-T Rec. T.43 (JBIG color)" } JPEGProc = { 1: "Baseline sequential process", 14: "Lossless process with Huffman coding" } JPEGLosslessPredictors = { 1: "A", 2: "B", 3: "C", 4: "A+B-C", 5: "A+((B-C)/2)", 6: "B+((A-C)/2)", 7: "(A+B)/2" } YCbCrSubSampling = { (0,1): "YCbCrSubsampleHoriz : ImageWidth of this chroma image is equal to the ImageWidth of the associated luma image", (0,2): "YCbCrSubsampleHoriz : ImageWidth of this chroma image is half the ImageWidth of the associated luma image", (0,4): "YCbCrSubsampleHoriz : ImageWidth of this chroma image is one-quarter the ImageWidth of the associated luma image", (1,1): "YCbCrSubsampleVert : ImageLength (height) of this chroma image is equal to the ImageLength of the associated luma image", (2,2): "YCbCrSubsampleVert : ImageLength (height) of this chroma image is half the ImageLength of the associated luma image", (4,4): "YCbCrSubsampleVert : ImageLength (height) of this chroma image is one-quarter the ImageLength of the associated luma image" } YCbCrPositioning = { 1: "Centered", 2: "Co-sited" } ## EXIF tag values ExposureProgram = { 0: "Not defined", 1: "Manual", 2: "Normal program", 3: "Aperture priority", 4: "Shutter priority", 5: "Creative program (biased toward depth of field)", 6: "Action program (biased toward fast shutter speed)", 7: "Portrait mode (for closeup photos with the background out of focus)", 8: "Landscape mode (for landscape photos with the background in focus)" } MeteringMode = { 0: "Unknown", 1: "Average", 2: "Center Weighted Average", 3: "Spot", 4: "MultiSpot", 5: "Pattern", 6: "Partial", 255: "other" } LightSource = { 0: "Unknown", 1: "Daylight", 2: "Fluorescent", 3: "Tungsten (incandescent light)", 4: "Flash", 9: "Fine weather", 10: "Cloudy weather", 11: "Shade", 12: "Daylight fluorescent (D 5700 - 7100K)", 13: "Day white fluorescent (N 4600 - 5400K)", 14: "Cool white fluorescent (W 3900 - 4500K)", 15: "White fluorescent (WW 3200 - 3700K)", 17: "Standard light A", 18: "Standard light B", 19: "Standard light C", 20: "D55", 21: "D65", 22: "D75", 23: "D50", 24: "ISO studio tungsten", 255: "Other light source" } ColorSpace = { 1: "RGB", 65535: "Uncalibrated" } Flash = { 0x0000: "Flash did not fire", 0x0001: "Flash fired", 0x0005: "Strobe return light not detected", 0x0007: "Strobe return light detected", 0x0008: "On, did not fire", 0x0009: "Flash fired, compulsory flash mode", 0x000D: "Flash fired, compulsory flash mode, return light not detected", 0x000F: "Flash fired, compulsory flash mode, return light detected", 0x0010: "Flash did not fire, compulsory flash mode", 0x0014: "Off, did not fire, return not detected", 0x0018: "Flash did not fire, auto mode", 0x0019: "Flash fired, auto mode", 0x001D: "Flash fired, auto mode, return light not detected", 0x001F: "Flash fired, auto mode, return light detected", 0x0020: "No flash function", 0x0030: "Off, no flash function", 0x0041: "Flash fired, red-eye reduction mode", 0x0045: "Flash fired, red-eye reduction mode, return light not detected", 0x0047: "Flash fired, red-eye reduction mode, return light detected", 0x0049: "Flash fired, compulsory flash mode, red-eye reduction mode", 0x004D: "Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected", 0x004F: "Flash fired, compulsory flash mode, red-eye reduction mode, return light detected", 0x0050: "Off, red-eye reduction", 0x0058: "Auto, Did not fire, red-eye reduction", 0x0059: "Flash fired, auto mode, red-eye reduction mode", 0x005D: "Flash fired, auto mode, return light not detected, red-eye reduction mode", 0x005F: "Flash fired, auto mode, return light detected, red-eye reduction mode" } FocalPlaneResolutionUnit = { 1: "No absolute unit of measurement", 2: "Inch", 3: "Centimeter" } SensingMethod = { 1: "Not defined", 2: "One-chip color area sensor", 3: "Two-chip color area sensor", 4: "Three-chip color area sensor", 5: "Color sequential area sensor", 7: "Trilinear sensor", 8: "Color sequential linear sensor" } CustomRendered = { 0: "Normal process", 1: "Custom process" } ExposureMode = { 0: "Auto exposure", 1: "Manual exposure", 2: "Auto bracket" } WhiteBalance = { 0: "Auto white balance", 1: "Manual white balance" } SceneCaptureType = { 0: "Standard", 1: "Landscape", 2: "Portrait", 3: "Night scene" } GainControl = { 0: "None", 1: "Low gain up", 2: "High gain up", 3: "Low gain down", 4: "High gain down" } Contrast = { 0: "Normal", 1: "Soft", 2: "Hard" } Saturation = { 0: "Normal", 1: "Low saturation", 2: "High saturation" } Sharpness = Contrast SubjectDistanceRange = { 0: "Unknown", 1: "Macro", 2: "Close view", 3: "Distant view" } ## GPS tag values GPSAltitudeRef = { 0: "Above sea level", 1: "Below sea level" } GPSMeasureMode = { b'2': "2-dimensional measurement", b'3': "3-dimensional measurement", b'2\x00': "2-dimensional measurement", b'3\x00': "3-dimensional measurement" } GPSSpeedRef = { b'K': "Kilometers per hour", b'M': "Miles per hour", b'N': "Knots", b'K\x00': "Kilometers per hour", b'M\x00': "Miles per hour", b'N\x00': "Knots" } GPSTrackRef = { b'T': "True direction", b'M': "Magnetic direction", b'T\x00': "True direction", b'M\x00': "Magnetic direction" } GPSImgDirectionRef = GPSTrackRef GPSLatitudeRef = { b'N': "North latitude", b'S': "South latitude", b'N\x00': "North latitude", b'S\x00': "South latitude" } GPSDestLatitudeRef = GPSLatitudeRef GPSLongitudeRef = { b'E': "East longitude", b'W': "West longitude", b'E\x00': "East longitude", b'W\x00': "West longitude" } GPSDestLongitudeRef = GPSLongitudeRef GPSDestBearingRef = GPSTrackRef GPSDestDistanceRef = GPSSpeedRef GPSDifferential = { 0: "Measurement without differential correction", 1: "Differential correction applied" } ## Geotiff tag values GTModelTypeGeoKey = { 0: "Undefined", 1: "Projection Coordinate System", 2: "Geographic (latitude,longitude) System", 3: "Geocentric (X,Y,Z) Coordinate System", } GTRasterTypeGeoKey = { 1: "Raster pixel is area", 2: "Raster pixel is point", } ProjCoordTransGeoKey = { 1: "CT_TransverseMercator", 2: "CT_TransvMercator_Modified_Alaska", 3: "CT_ObliqueMercator", 4: "CT_ObliqueMercator_Laborde", 5: "CT_ObliqueMercator_Rosenmund", 6: "CT_ObliqueMercator_Spherical", 7: "CT_Mercator", 8: "CT_LambertConfConic_2SP", 9: "CT_LambertConfConic_Helmert", 10: "CT_LambertAzimEqualArea", 11: "CT_AlbersEqualArea", 12: "CT_AzimuthalEquidistant", 13: "CT_EquidistantConic", 14: "CT_Stereographic", 15: "CT_PolarStereographic", 16: "CT_ObliqueStereographic", 17: "CT_Equirectangular", 18: "CT_CassiniSoldner", 19: "CT_Gnomonic", 20: "CT_MillerCylindrical", 21: "CT_Orthographic", 22: "CT_Polyconic", 23: "CT_Robinson", 24: "CT_Sinusoidal", 25: "CT_VanDerGrinten", 26: "CT_NewZealandMapGrid", 27: "CT_TransvMercator_SouthOriented", 28: "User-defined", 32767: "User-defined" } GeogPrimeMeridianGeoKey = { 8901: "Greenwich", 8902: "Lisbon", 8903: "Paris", 8904: "Bogota", 8905: "Madrid", 8906: "Rome", 8907: "Bern", 8908: "Jakarta", 8909: "Ferro", 8910: "Brussels", 8911: "Stockholm", 8912: "Athens", 8913: "Oslo", 8914: "Paris RGS" } GeogLinearUnitsGeoKey = { 1025: "millimetre", 1026: "metres per second", 1027: "millimetres per year", 1033: "centimetre", 1034: "centimetres per year", 1042: "metres per year", 9001: "metre", 9002: "foot", 9003: "US survey foot", 9005: "Clarke's foot", 9014: "fathom", 9030: "nautical mile", 9031: "German legal metre", 9033: "US survey chain", 9034: "US survey link", 9035: "US survey mile", 9036: "kilometre", 9037: "Clarke's yard", 9038: "Clarke's chain", 9039: "Clarke's link", 9040: "British yard (Sears 1922)", 9041: "British foot (Sears 1922)", 9042: "British chain (Sears 1922)", 9043: "British link (Sears 1922)", 9050: "British yard (Benoit 1895 A)", 9051: "British foot (Benoit 1895 A)", 9052: "British chain (Benoit 1895 A)", 9053: "British link (Benoit 1895 A)", 9060: "British yard (Benoit 1895 B)", 9061: "British foot (Benoit 1895 B)", 9062: "British chain (Benoit 1895 B)", 9063: "British link (Benoit 1895 B)", 9070: "British foot (1865)", 9080: "Indian foot", 9081: "Indian foot (1937)", 9082: "Indian foot (1962)", 9083: "Indian foot (1975)", 9084: "Indian yard", 9085: "Indian yard (1937)", 9086: "Indian yard (1962)", 9087: "Indian yard (1975)", 9093: "Statute mile", 9094: "Gold Coast foot", 9095: "British foot (1936)", 9096: "yard", 9097: "chain", 9098: "link", 9099: "British yard (Sears 1922 truncated)", 9204: "Bin width 330 US survey feet", 9205: "Bin width 165 US survey feet", 9206: "Bin width 82.5 US survey feet", 9207: "Bin width 37.5 metres", 9208: "Bin width 25 metres", 9209: "Bin width 12.5 metres", 9210: "Bin width 6.25 metres", 9211: "Bin width 3.125 metres", 9300: "British foot (Sears 1922 truncated)", 9301: "British chain (Sears 1922 truncated)", 9302: "British link (Sears 1922 truncated)" } GeogAngularUnitsGeoKey = { 1031: "milliarc-second", 1032: "milliarc-seconds per year", 1035: "radians per second", 1043: "arc-seconds per year", 9101: "radian", 9102: "degree", 9103: "arc-minute", 9104: "arc-second", 9105: "grad", 9106: "gon", 9107: "degree minute second", 9108: "degree minute second hemisphere", 9109: "microradian", 9110: "sexagesimal DMS", 9111: "sexagesimal DM", 9112: "centesimal minute", 9113: "centesimal second", 9114: "mil_6400", 9115: "degree minute", 9116: "degree hemisphere", 9117: "hemisphere degree", 9118: "degree minute hemisphere", 9119: "hemisphere degree minute", 9120: "hemisphere degree minute second", 9121: "sexagesimal DMS.s", 9122: "degree (supplier to define representation)" } GeogAzimuthUnitsGeoKey = GeogAngularUnitsGeoKey ProjLinearUnitsGeoKey = GeogLinearUnitsGeoKey VerticalUnitsGeoKey = GeogLinearUnitsGeoKey GeogEllipsoidGeoKey = { 1024: "CGCS2000", 7001: "Airy 1830", 7002: "Airy Modified 1849", 7003: "Australian National Spheroid", 7004: "Bessel 1841", 7005: "Bessel Modified", 7006: "Bessel Namibia", 7007: "Clarke 1858", 7008: "Clarke 1866", 7009: "Clarke 1866 Michigan", 7010: "Clarke 1880 (Benoit)", 7011: "Clarke 1880 (IGN)", 7012: "Clarke 1880 (RGS)", 7013: "Clarke 1880 (Arc)", 7014: "Clarke 1880 (SGA 1922)", 7015: "Everest 1830 (1937 Adjustment)", 7016: "Everest 1830 (1967 Definition)", 7018: "Everest 1830 Modified", 7019: "GRS 1980", 7020: "Helmert 1906", 7021: "Indonesian National Spheroid", 7022: "International 1924", 7024: "Krassowsky 1940", 7025: "NWL 9D", 7027: "Plessis 1817", 7028: "Struve 1860", 7029: "War Office", 7030: "WGS 84", 7031: "GEM 10C", 7032: "OSU86F", 7033: "OSU91A", 7034: "Clarke 1880", 7035: "Sphere", 7036: "GRS 1967", 7041: "Average Terrestrial System 1977", 7042: "Everest (1830 Definition)", 7043: "WGS 72", 7044: "Everest 1830 (1962 Definition)", 7045: "Everest 1830 (1975 Definition)", 7046: "Bessel Namibia (GLM)", 7047: "GRS 1980 Authalic Sphere", 7048: "GRS 1980 Authalic Sphere", 7049: "IAG 1975", 7050: "GRS 1967 Modified", 7051: "Danish 1876", 7052: "Clarke 1866 Authalic Sphere", 7053: "Hough 1960", 7054: "PZ-90", 7055: "Clarke 1880 (international foot)", 7056: "Everest 1830 (RSO 1969)", 7057: "International 1924 Authalic Sphere", 7058: "Hughes 1980", 7059: "Popular Visualisation Sphere", 32767: "User-defined" } GeogGeodeticDatumGeoKey = { 1024: "Hungarian Datum 1909", 1025: "Taiwan Datum 1967", 1026: "Taiwan Datum 1997", 1029: "Iraqi Geospatial Reference System", 1031: "MGI 1901", 1032: "MOLDREF99", 1033: "Reseau Geodesique de la RDC 2005", 1034: "Serbian Reference Network 1998", 1035: "Red Geodesica de Canarias 1995", 1036: "Reseau Geodesique de Mayotte 2004", 1037: "Cadastre 1997", 1038: "Reseau Geodesique de Saint Pierre et Miquelon 2006", 1041: "Autonomous Regions of Portugal 2008", 1042: "Mexico ITRF92", 1043: "China 2000", 1044: "Sao Tome", 1045: "New Beijing", 1046: "Principe", 1047: "Reseau de Reference des Antilles Francaises 1991", 1048: "Tokyo 1892", 1052: "System Jednotne Trigonometricke Site Katastralni/05", 1053: "Sri Lanka Datum 1999", 1055: "System Jednotne Trigonometricke Site Katastralni/05 (Ferro)", 1056: "Geocentric Datum Brunei Darussalam 2009", 1057: "Turkish National Reference Frame", 1058: "Bhutan National Geodetic Datum", 1060: "Islands Net 2004", 1061: "International Terrestrial Reference Frame 2008", 1062: "Posiciones Geodesicas Argentinas 2007", 1063: "Marco Geodesico Nacional", 1064: "SIRGAS-Chile", 1065: "Costa Rica 2005", 1066: "Sistema Geodesico Nacional de Panama MACARIO SOLIS", 1067: "Peru96", 1068: "SIRGAS-ROU98", 1069: "SIRGAS_ES2007.8", 1070: "Ocotepeque 1935", 1071: "Sibun Gorge 1922", 1072: "Panama-Colon 1911", 1073: "Reseau Geodesique des Antilles Francaises 2009", 1074: "Corrego Alegre 1961", 1075: "South American Datum 1969(96)", 1076: "Papua New Guinea Geodetic Datum 1994", 1077: "Ukraine 2000", 1078: "Fehmarnbelt Datum 2010", 1081: "Deutsche Bahn Reference System", 1095: "Tonga Geodetic Datum 2005", 1100: "Cayman Islands Geodetic Datum 2011", 1111: "Nepal 1981", 1112: "Cyprus Geodetic Reference System 1993", 1113: "Reseau Geodesique des Terres Australes et Antarctiques Francaises 2007", 1114: "Israeli Geodetic Datum 2005", 1115: "Israeli Geodetic Datum 2005(2012)", 1116: "NAD83 (National Spatial Reference System 2011)", 1117: "NAD83 (National Spatial Reference System PA11)", 1118: "NAD83 (National Spatial Reference System MA11)", 1120: "Mexico ITRF2008", 1128: "Japanese Geodetic Datum 2011", 1132: "Rete Dinamica Nazionale 2008", 1133: "NAD83 (Continuously Operating Reference Station 1996)", 1135: "Aden 1925", 1136: "Bioko", 1137: "Bekaa Valley 1920", 1138: "South East Island 1943", 1139: "Gambia", 1141: "IGS08", 1142: "IG05 Intermediate Datum", 1143: "Israeli Geodetic Datum 2005", 1144: "IG05/12 Intermediate Datum", 1145: "Israeli Geodetic Datum 2005(2012)", 1147: "Oman National Geodetic Datum 2014", 1160: "Kyrgyzstan Geodetic Datum 2006", 6001: "Not specified (based on Airy 1830 ellipsoid)", 6002: "Not specified (based on Airy Modified 1849 ellipsoid)", 6003: "Not specified (based on Australian National Spheroid)", 6004: "Not specified (based on Bessel 1841 ellipsoid)", 6005: "Not specified (based on Bessel Modified ellipsoid)", 6006: "Not specified (based on Bessel Namibia ellipsoid)", 6007: "Not specified (based on Clarke 1858 ellipsoid)", 6008: "Not specified (based on Clarke 1866 ellipsoid)", 6009: "Not specified (based on Clarke 1866 Michigan ellipsoid)", 6010: "Not specified (based on Clarke 1880 (Benoit) ellipsoid)", 6011: "Not specified (based on Clarke 1880 (IGN) ellipsoid)", 6012: "Not specified (based on Clarke 1880 (RGS) ellipsoid)", 6013: "Not specified (based on Clarke 1880 (Arc) ellipsoid)", 6014: "Not specified (based on Clarke 1880 (SGA 1922) ellipsoid)", 6015: "Not specified (based on Everest 1830 (1937 Adjustment) ellipsoid)", 6016: "Not specified (based on Everest 1830 (1967 Definition) ellipsoid)", 6018: "Not specified (based on Everest 1830 Modified ellipsoid)", 6019: "Not specified (based on GRS 1980 ellipsoid)", 6020: "Not specified (based on Helmert 1906 ellipsoid)", 6021: "Not specified (based on Indonesian National Spheroid)", 6022: "Not specified (based on International 1924 ellipsoid)", 6024: "Not specified (based on Krassowsky 1940 ellipsoid)", 6025: "Not specified (based on NWL 9D ellipsoid)", 6027: "Not specified (based on Plessis 1817 ellipsoid)", 6028: "Not specified (based on Struve 1860 ellipsoid)", 6029: "Not specified (based on War Office ellipsoid)", 6030: "Not specified (based on WGS 84 ellipsoid)", 6031: "Not specified (based on GEM 10C ellipsoid)", 6032: "Not specified (based on OSU86F ellipsoid)", 6033: "Not specified (based on OSU91A ellipsoid)", 6034: "Not specified (based on Clarke 1880 ellipsoid)", 6035: "Not specified (based on Authalic Sphere)", 6036: "Not specified (based on GRS 1967 ellipsoid)", 6041: "Not specified (based on Average Terrestrial System 1977 ellipsoid)", 6042: "Not specified (based on Everest (1830 Definition) ellipsoid)", 6043: "Not specified (based on WGS 72 ellipsoid)", 6044: "Not specified (based on Everest 1830 (1962 Definition) ellipsoid)", 6045: "Not specified (based on Everest 1830 (1975 Definition) ellipsoid)", 6047: "Not specified (based on GRS 1980 Authalic Sphere)", 6052: "Not specified (based on Clarke 1866 Authalic Sphere)", 6053: "Not specified (based on International 1924 Authalic Sphere)", 6054: "Not specified (based on Hughes 1980 ellipsoid)", 6055: "Popular Visualisation Datum", 6120: "Greek", 6121: "Greek Geodetic Reference System 1987", 6122: "Average Terrestrial System 1977", 6123: "Kartastokoordinaattijarjestelma (1966)", 6124: "Rikets koordinatsystem 1990", 6125: "Samboja", 6126: "Lithuania 1994 (ETRS89)", 6127: "Tete", 6128: "Madzansua", 6129: "Observatario", 6130: "Moznet (ITRF94)", 6131: "Indian 1960", 6132: "Final Datum 1958", 6133: "Estonia 1992", 6134: "PDO Survey Datum 1993", 6135: "Old Hawaiian", 6136: "St. Lawrence Island", 6137: "St. Paul Island", 6138: "St. George Island", 6139: "Puerto Rico", 6140: "NAD83 Canadian Spatial Reference System", 6141: "Israel 1993", 6142: "Locodjo 1965", 6143: "Abidjan 1987", 6144: "Kalianpur 1937", 6145: "Kalianpur 1962", 6146: "Kalianpur 1975", 6147: "Hanoi 1972", 6148: "Hartebeesthoek94", 6149: "CH1903", 6150: "CH1903+", 6151: "Swiss Terrestrial Reference Frame 1995", 6152: "NAD83 (High Accuracy Reference Network)", 6153: "Rassadiran", 6154: "European Datum 1950(1977)", 6155: "Dabola 1981", 6156: "System Jednotne Trigonometricke Site Katastralni", 6157: "Mount Dillon", 6158: "Naparima 1955", 6159: "European Libyan Datum 1979", 6160: "Chos Malal 1914", 6161: "Pampa del Castillo", 6162: "Korean Datum 1985", 6163: "Yemen National Geodetic Network 1996", 6164: "South Yemen", 6165: "Bissau", 6166: "Korean Datum 1995", 6167: "New Zealand Geodetic Datum 2000", 6168: "Accra", 6169: "American Samoa 1962", 6170: "Sistema de Referencia Geocentrico para America del Sur 1995", 6171: "Reseau Geodesique Francais 1993", 6172: "Posiciones Geodesicas Argentinas", 6173: "IRENET95", 6174: "Sierra Leone Colony 1924", 6175: "Sierra Leone 1968", 6176: "Australian Antarctic Datum 1998", 6178: "Pulkovo 1942(83)", 6179: "Pulkovo 1942(58)", 6180: "Estonia 1997", 6181: "Luxembourg 1930", 6182: "Azores Occidental Islands 1939", 6183: "Azores Central Islands 1948", 6184: "Azores Oriental Islands 1940", 6185: "Madeira 1936", 6188: "OSNI 1952", 6189: "Red Geodesica Venezolana", 6190: "Posiciones Geodesicas Argentinas 1998", 6191: "Albanian 1987", 6192: "Douala 1948", 6193: "Manoca 1962", 6194: "Qornoq 1927", 6195: "Scoresbysund 1952", 6196: "Ammassalik 1958", 6197: "Garoua", 6198: "Kousseri", 6199: "Egypt 1930", 6200: "Pulkovo 1995", 6201: "Adindan", 6202: "Australian Geodetic Datum 1966", 6203: "Australian Geodetic Datum 1984", 6204: "Ain el Abd 1970", 6205: "Afgooye", 6206: "Agadez", 6207: "Lisbon 1937", 6208: "Aratu", 6209: "Arc 1950", 6210: "Arc 1960", 6211: "Batavia", 6212: "Barbados 1938", 6213: "Beduaram", 6214: "Beijing 1954", 6215: "Reseau National Belge 1950", 6216: "Bermuda 1957", 6218: "Bogota 1975", 6219: "Bukit Rimpah", 6220: "Camacupa", 6221: "Campo Inchauspe", 6222: "Cape", 6223: "Carthage", 6224: "Chua", 6225: "Corrego Alegre 1970-72", 6226: "Cote d'Ivoire", 6227: "Deir ez Zor", 6228: "Douala", 6229: "Egypt 1907", 6230: "European Datum 1950", 6231: "European Datum 1987", 6232: "Fahud", 6233: "Gandajika 1970", 6234: "Garoua", 6235: "Guyane Francaise", 6236: "Hu Tzu Shan 1950", 6237: "Hungarian Datum 1972", 6238: "Indonesian Datum 1974", 6239: "Indian 1954", 6240: "Indian 1975", 6241: "Jamaica 1875", 6242: "Jamaica 1969", 6243: "Kalianpur 1880", 6244: "Kandawala", 6245: "Kertau 1968", 6246: "Kuwait Oil Company", 6247: "La Canoa", 6248: "Provisional South American Datum 1956", 6249: "Lake", 6250: "Leigon", 6251: "Liberia 1964", 6252: "Lome", 6253: "Luzon 1911", 6254: "Hito XVIII 1963", 6255: "Herat North", 6256: "Mahe 1971", 6257: "Makassar", 6258: "European Terrestrial Reference System 1989", 6259: "Malongo 1987", 6260: "Manoca", 6261: "Merchich", 6262: "Massawa", 6263: "Minna", 6264: "Mhast", 6265: "Monte Mario", 6266: "M'poraloko", 6267: "North American Datum 1927", 6268: "NAD27 Michigan", 6269: "North American Datum 1983", 6270: "Nahrwan 1967", 6271: "Naparima 1972", 6272: "New Zealand Geodetic Datum 1949", 6273: "NGO 1948", 6274: "Datum 73", 6275: "Nouvelle Triangulation Francaise", 6276: "NSWC 9Z-2", 6277: "OSGB 1936", 6278: "OSGB 1970 (SN)", 6279: "OS (SN) 1980", 6280: "Padang 1884", 6281: "Palestine 1923", 6282: "Congo 1960 Pointe Noire", 6283: "Geocentric Datum of Australia 1994", 6284: "Pulkovo 1942", 6285: "Qatar 1974", 6286: "Qatar 1948", 6287: "Qornoq", 6288: "Loma Quintana", 6289: "Amersfoort", 6291: "South American Datum 1969", 6292: "Sapper Hill 1943", 6293: "Schwarzeck", 6294: "Segora", 6295: "Serindung", 6296: "Sudan", 6297: "Tananarive 1925", 6298: "Timbalai 1948", 6299: "TM65", 6300: "Geodetic Datum of 1965", 6301: "Tokyo", 6302: "Trinidad 1903", 6303: "Trucial Coast 1948", 6304: "Voirol 1875", 6306: "Bern 1938", 6307: "Nord Sahara 1959", 6308: "Stockholm 1938", 6309: "Yacare", 6310: "Yoff", 6311: "Zanderij", 6312: "Militar-Geographische Institut", 6313: "Reseau National Belge 1972", 6314: "Deutsches Hauptdreiecksnetz", 6315: "Conakry 1905", 6316: "Dealul Piscului 1930", 6317: "Dealul Piscului 1970", 6318: "National Geodetic Network", 6319: "Kuwait Utility", 6322: "World Geodetic System 1972", 6324: "WGS 72 Transit Broadcast Ephemeris", 6326: "World Geodetic System 1984", 6600: "Anguilla 1957", 6601: "Antigua 1943", 6602: "Dominica 1945", 6603: "Grenada 1953", 6604: "Montserrat 1958", 6605: "St. Kitts 1955", 6606: "St. Lucia 1955", 6607: "St. Vincent 1945", 6608: "North American Datum 1927 (1976)", 6609: "North American Datum 1927 (CGQ77)", 6610: "Xian 1980", 6611: "Hong Kong 1980", 6612: "Japanese Geodetic Datum 2000", 6613: "Gunung Segara", 6614: "Qatar National Datum 1995", 6615: "Porto Santo 1936", 6616: "Selvagem Grande", 6618: "South American Datum 1969", 6619: "SWEREF99", 6620: "Point 58", 6621: "Fort Marigot", 6622: "Guadeloupe 1948", 6623: "Centre Spatial Guyanais 1967", 6624: "Reseau Geodesique Francais Guyane 1995", 6625: "Martinique 1938", 6626: "Reunion 1947", 6627: "Reseau Geodesique de la Reunion 1992", 6628: "Tahiti 52", 6629: "Tahaa 54", 6630: "IGN72 Nuku Hiva", 6631: "K0 1949", 6632: "Combani 1950", 6633: "IGN56 Lifou", 6634: "IGN72 Grande Terre", 6635: "ST87 Ouvea", 6636: "Petrels 1972", 6637: "Pointe Geologie Perroud 1950", 6638: "Saint Pierre et Miquelon 1950", 6639: "MOP78", 6640: "Reseau de Reference des Antilles Francaises 1991", 6641: "IGN53 Mare", 6642: "ST84 Ile des Pins", 6643: "ST71 Belep", 6644: "NEA74 Noumea", 6645: "Reseau Geodesique Nouvelle Caledonie 1991", 6646: "Grand Comoros", 6647: "International Terrestrial Reference Frame 1988", 6648: "International Terrestrial Reference Frame 1989", 6649: "International Terrestrial Reference Frame 1990", 6650: "International Terrestrial Reference Frame 1991", 6651: "International Terrestrial Reference Frame 1992", 6652: "International Terrestrial Reference Frame 1993", 6653: "International Terrestrial Reference Frame 1994", 6654: "International Terrestrial Reference Frame 1996", 6655: "International Terrestrial Reference Frame 1997", 6656: "International Terrestrial Reference Frame 2000", 6657: "Reykjavik 1900", 6658: "Hjorsey 1955", 6659: "Islands Net 1993", 6660: "Helle 1954", 6661: "Latvia 1992", 6663: "Porto Santo 1995", 6664: "Azores Oriental Islands 1995", 6665: "Azores Central Islands 1995", 6666: "Lisbon 1890", 6667: "Iraq-Kuwait Boundary Datum 1992", 6668: "European Datum 1979", 6670: "Istituto Geografico Militaire 1995", 6671: "Voirol 1879", 6672: "Chatham Islands Datum 1971", 6673: "Chatham Islands Datum 1979", 6674: "Sistema de Referencia Geocentrico para las AmericaS 2000", 6675: "Guam 1963", 6676: "Vientiane 1982", 6677: "Lao 1993", 6678: "Lao National Datum 1997", 6679: "Jouik 1961", 6680: "Nouakchott 1965", 6681: "Mauritania 1999", 6682: "Gulshan 303", 6683: "Philippine Reference System 1992", 6684: "Gan 1970", 6685: "Gandajika", 6686: "Marco Geocentrico Nacional de Referencia", 6687: "Reseau Geodesique de la Polynesie Francaise", 6688: "Fatu Iva 72", 6689: "IGN63 Hiva Oa", 6690: "Tahiti 79", 6691: "Moorea 87", 6692: "Maupiti 83", 6693: "Nakhl-e Ghanem", 6694: "Posiciones Geodesicas Argentinas 1994", 6695: "Katanga 1955", 6696: "Kasai 1953", 6697: "IGC 1962 Arc of the 6th Parallel South", 6698: "IGN 1962 Kerguelen", 6699: "Le Pouce 1934", 6700: "IGN Astro 1960", 6701: "Institut Geographique du Congo Belge 1955", 6702: "Mauritania 1999", 6703: "Missao Hidrografico Angola y Sao Tome 1951", 6704: "Mhast (onshore)", 6705: "Mhast (offshore)", 6706: "Egypt Gulf of Suez S-650 TL", 6707: "Tern Island 1961", 6708: "Cocos Islands 1965", 6709: "Iwo Jima 1945", 6710: "St. Helena 1971", 6711: "Marcus Island 1952", 6712: "Ascension Island 1958", 6713: "Ayabelle Lighthouse", 6714: "Bellevue", 6715: "Camp Area Astro", 6716: "Phoenix Islands 1966", 6717: "Cape Canaveral", 6718: "Solomon 1968", 6719: "Easter Island 1967", 6720: "Fiji Geodetic Datum 1986", 6721: "Fiji 1956", 6722: "South Georgia 1968", 6723: "Grand Cayman Geodetic Datum 1959", 6724: "Diego Garcia 1969", 6725: "Johnston Island 1961", 6726: "Sister Islands Geodetic Datum 1961", 6727: "Midway 1961", 6728: "Pico de las Nieves 1984", 6729: "Pitcairn 1967", 6730: "Santo 1965", 6731: "Viti Levu 1916", 6732: "Marshall Islands 1960", 6733: "Wake Island 1952", 6734: "Tristan 1968", 6735: "Kusaie 1951", 6736: "Deception Island", 6737: "Geocentric datum of Korea", 6738: "Hong Kong 1963", 6739: "Hong Kong 1963(67)", 6740: "Parametrop Zemp 1990", 6741: "Faroe Datum 1954", 6742: "Geodetic Datum of Malaysia 2000", 6743: "Karbala 1979", 6744: "Nahrwan 1934", 6745: "Rauenberg Datum/83", 6746: "Potsdam Datum/83", 6747: "Greenland 1996", 6748: "Vanua Levu 1915", 6749: "Reseau Geodesique de Nouvelle Caledonie 91-93", 6750: "ST87 Ouvea", 6751: "Kertau (RSO)", 6752: "Viti Levu 1912", 6753: "fk89", 6754: "Libyan Geodetic Datum 2006", 6755: "Datum Geodesi Nasional 1995", 6756: "Vietnam 2000", 6757: "SVY21", 6758: "Jamaica 2001", 6759: "NAD83 (National Spatial Reference System 2007)", 6760: "World Geodetic System 1966", 6761: "Croatian Terrestrial Reference System", 6762: "Bermuda 2000", 6763: "Pitcairn 2006", 6764: "Ross Sea Region Geodetic Datum 2000", 6765: "Slovenia Geodetic Datum 1996", 6801: "CH1903 (Bern)", 6802: "Bogota 1975 (Bogota)", 6803: "Lisbon 1937 (Lisbon)", 6804: "Makassar (Jakarta)", 6805: "Militar-Geographische Institut (Ferro)", 6806: "Monte Mario (Rome)", 6807: "Nouvelle Triangulation Francaise (Paris)", 6808: "Padang 1884 (Jakarta)", 6809: "Reseau National Belge 1950 (Brussels)", 6810: "Tananarive 1925 (Paris)", 6811: "Voirol 1875 (Paris)", 6813: "Batavia (Jakarta)", 6814: "Stockholm 1938 (Stockholm)", 6815: "Greek (Athens)", 6816: "Carthage (Paris)", 6817: "NGO 1948 (Oslo)", 6818: "System Jednotne Trigonometricke Site Katastralni (Ferro)", 6819: "Nord Sahara 1959 (Paris)", 6820: "Gunung Segara (Jakarta)", 6821: "Voirol 1879 (Paris)", 6896: "International Terrestrial Reference Frame 2005", 6901: "Ancienne Triangulation Francaise (Paris)", 6902: "Nord de Guerre (Paris)", 6903: "Madrid 1870 (Madrid)", 6904: "Lisbon 1890 (Lisbon)", 32767: "User-defined" } VerticalDatumGeoKey = { 1027: "EGM2008 geoid", 1028: "Fao 1979", 1030: "N2000", 1039: "New Zealand Vertical Datum 2009", 1040: "Dunedin-Bluff 1960", 1049: "Incheon", 1050: "Trieste", 1051: "Genoa", 1054: "Sri Lanka Vertical Datum", 1059: "Faroe Islands Vertical Reference 2009", 1079: "Fehmarnbelt Vertical Reference 2010", 1080: "Lowest Astronomic Tide", 1082: "Highest Astronomic Tide", 1083: "Lower Low Water Large Tide", 1084: "Higher High Water Large Tide", 1085: "Indian Spring Low Water", 1086: "Mean Lower Low Water Spring Tides", 1087: "Mean Low Water Spring Tides", 1088: "Mean High Water Spring Tides", 1089: "Mean Lower Low Water", 1090: "Mean Higher High Water", 1091: "Mean Low Water", 1092: "Mean High Water", 1093: "Low Water", 1094: "High Water", 1096: "Norway Normal Null 2000", 1097: "Grand Cayman Vertical Datum 1954", 1098: "Little Cayman Vertical Datum 1961", 1099: "Cayman Brac Vertical Datum 1961", 1101: "Cais da Pontinha - Funchal", 1102: "Cais da Vila - Porto Santo", 1103: "Cais das Velas", 1104: "Horta", 1105: "Cais da Madalena", 1106: "Santa Cruz da Graciosa", 1107: "Cais da Figueirinha - Angra do Heroismo", 1108: "Santa Cruz das Flores", 1109: "Cais da Vila do Porto", 1110: "Ponta Delgada", 1119: "Northern Marianas Vertical Datum of 2003", 1121: "Tutuila Vertical Datum of 1962", 1122: "Guam Vertical Datum of 1963", 1123: "Puerto Rico Vertical Datum of 2002", 1124: "Virgin Islands Vertical Datum of 2009", 1125: "American Samoa Vertical Datum of 2002", 1126: "Guam Vertical Datum of 2004", 1127: "Canadian Geodetic Vertical Datum of 2013", 1129: "Japanese Standard Levelling Datum 1972", 1130: "Japanese Geodetic Datum 2000 (vertical)", 1131: "Japanese Geodetic Datum 2011 (vertical)", 1140: "Singapore Height Datum", 1146: "Ras Ghumays", 1148: "Famagusta 1960", 1149: "PNG08", 1150: "Kumul 34", 1151: "Kiunga", 1161: "Deutsches Haupthoehennetz 1912", 1162: "Latvian Height System 2000", 5100: "Mean Sea Level", 5101: "Ordnance Datum Newlyn", 5102: "National Geodetic Vertical Datum 1929", 5103: "North American Vertical Datum 1988", 5104: "Yellow Sea 1956", 5105: "Baltic Sea", 5106: "Caspian Sea", 5107: "Nivellement general de la France", 5109: "Normaal Amsterdams Peil", 5110: "Ostend", 5111: "Australian Height Datum", 5112: "Australian Height Datum (Tasmania)", 5113: "Instantaneous Water Level", 5114: "Canadian Geodetic Vertical Datum of 1928", 5115: "Piraeus Harbour 1986", 5116: "Helsinki 1960", 5117: "Rikets hojdsystem 1970", 5118: "Nivellement General de la France - Lallemand", 5119: "Nivellement General de la France - IGN69", 5120: "Nivellement General de la France - IGN78", 5121: "Maputo", 5122: "Japanese Standard Levelling Datum 1969", 5123: "PDO Height Datum 1993", 5124: "Fahud Height Datum", 5125: "Ha Tien 1960", 5126: "Hon Dau 1992", 5127: "Landesnivellement 1902", 5128: "Landeshohennetz 1995", 5129: "European Vertical Reference Frame 2000", 5130: "Malin Head", 5131: "Belfast Lough", 5132: "Dansk Normal Nul", 5133: "AIOC 1995", 5134: "Black Sea", 5135: "Hong Kong Principal Datum", 5136: "Hong Kong Chart Datum", 5137: "Yellow Sea 1985", 5138: "Ordnance Datum Newlyn (Orkney Isles)", 5139: "Fair Isle", 5140: "Lerwick", 5141: "Foula", 5142: "Sule Skerry", 5143: "North Rona", 5144: "Stornoway", 5145: "St Kilda", 5146: "Flannan Isles", 5147: "St Marys", 5148: "Douglas", 5149: "Fao", 5150: "Bandar Abbas", 5151: "Nivellement General de Nouvelle Caledonie", 5152: "Poolbeg", 5153: "Nivellement General Guyanais 1977", 5154: "Martinique 1987", 5155: "Guadeloupe 1988", 5156: "Reunion 1989", 5157: "Auckland 1946", 5158: "Bluff 1955", 5159: "Dunedin 1958", 5160: "Gisborne 1926", 5161: "Lyttelton 1937", 5162: "Moturiki 1953", 5163: "Napier 1962", 5164: "Nelson 1955", 5165: "One Tree Point 1964", 5166: "Tararu 1952", 5167: "Taranaki 1970", 5168: "Wellington 1953", 5169: "Waitangi (Chatham Island) 1959", 5170: "Stewart Island 1977", 5171: "EGM96 geoid", 5172: "Nivellement General du Luxembourg", 5173: "Antalya", 5174: "Norway Normal Null 1954", 5175: "Durres", 5176: "Gebrauchshohen ADRIA", 5177: "National Vertical Network 1999", 5178: "Cascais", 5179: "Constanta", 5180: "Alicante", 5181: "Deutsches Haupthoehennetz 1992", 5182: "Deutsches Haupthoehennetz 1985", 5183: "Staatlichen Nivellementnetzes 1976", 5184: "Baltic 1982", 5185: "Baltic 1980", 5186: "Kuwait PWD", 5187: "KOC Well Datum", 5188: "KOC Construction Datum", 5189: "Nivellement General de la Corse 1948", 5190: "Danger 1950", 5191: "Mayotte 1950", 5192: "Martinique 1955", 5193: "Guadeloupe 1951", 5194: "Lagos 1955", 5195: "Nivellement General de Polynesie Francaise", 5196: "IGN 1966", 5197: "Moorea SAU 1981", 5198: "Raiatea SAU 2001", 5199: "Maupiti SAU 2001", 5200: "Huahine SAU 2001", 5201: "Tahaa SAU 2001", 5202: "Bora Bora SAU 2001", 5203: "EGM84 geoid", 5204: "International Great Lakes Datum 1955", 5205: "International Great Lakes Datum 1985", 5206: "Dansk Vertikal Reference 1990", 5207: "Croatian Vertical Reference System 1971", 5208: "Rikets hojdsystem 2000", 5209: "Rikets hojdsystem 1900", 5210: "IGN 1988 LS", 5211: "IGN 1988 MG", 5212: "IGN 1992 LD", 5213: "IGN 1988 SB", 5214: "IGN 1988 SM", 5215: "European Vertical Reference Frame 2007", 32767: "User-defined" } VerticalCSTypeGeoKey = { 3855: "EGM2008 height", 3886: "Fao 1979 height", 3900: "N2000 height", 4440: "NZVD2009 height", 4458: "Dunedin-Bluff 1960 height", 5193: "Incheon height", 5195: "Trieste height", 5214: "Genoa height", 5237: "SLVD height", 5317: "FVR09 height", 5336: "Black Sea depth", 5597: "FCSVR10 height", 5600: "NGPF height", 5601: "IGN 1966 height", 5602: "Moorea SAU 1981 height", 5603: "Raiatea SAU 2001 height", 5604: "Maupiti SAU 2001 height", 5605: "Huahine SAU 2001 height", 5606: "Tahaa SAU 2001 height", 5607: "Bora Bora SAU 2001 height", 5608: "IGLD 1955 height", 5609: "IGLD 1985 height", 5610: "HVRS71 height", 5611: "Caspian height", 5612: "Baltic depth", 5613: "RH2000 height", 5614: "KOC WD depth (ft)", 5615: "RH00 height", 5616: "IGN 1988 LS height", 5617: "IGN 1988 MG height", 5618: "IGN 1992 LD height", 5619: "IGN 1988 SB height", 5620: "IGN 1988 SM height", 5621: "EVRF2007 height", 5701: "ODN height", 5702: "NGVD29 height", 5703: "NAVD88 height", 5704: "Yellow Sea", 5705: "Baltic height", 5706: "Caspian depth", 5709: "NAP height", 5710: "Ostend height", 5711: "AHD height", 5712: "AHD (Tasmania) height", 5713: "CGVD28 height", 5714: "MSL height", 5715: "MSL depth", 5716: "Piraeus height", 5717: "N60 height", 5718: "RH70 height", 5719: "NGF Lallemand height", 5720: "NGF-IGN69 height", 5721: "NGF-IGN78 height", 5722: "Maputo height", 5723: "JSLD69 height", 5724: "PHD93 height", 5725: "Fahud HD height", 5726: "Ha Tien 1960 height", 5727: "Hon Dau 1992 height", 5728: "LN02 height", 5729: "LHN95 height", 5730: "EVRF2000 height", 5731: "Malin Head height", 5732: "Belfast height", 5733: "DNN height", 5734: "AIOC95 depth", 5735: "Black Sea height", 5736: "Yellow Sea 1956 height", 5737: "Yellow Sea 1985 height", 5738: "HKPD height", 5739: "HKCD depth", 5740: "ODN Orkney height", 5741: "Fair Isle height", 5742: "Lerwick height", 5743: "Foula height", 5744: "Sule Skerry height", 5745: "North Rona height", 5746: "Stornoway height", 5747: "St Kilda height", 5748: "Flannan Isles height", 5749: "St Marys height", 5750: "Douglas height", 5751: "Fao height", 5752: "Bandar Abbas height", 5753: "NGNC height", 5754: "Poolbeg height", 5755: "NGG1977 height", 5756: "Martinique 1987 height", 5757: "Guadeloupe 1988 height", 5758: "Reunion 1989 height", 5759: "Auckland 1946 height", 5760: "Bluff 1955 height", 5761: "Dunedin 1958 height", 5762: "Gisborne 1926 height", 5763: "Lyttelton 1937 height", 5764: "Moturiki 1953 height", 5765: "Napier 1962 height", 5766: "Nelson 1955 height", 5767: "One Tree Point 1964 height", 5768: "Tararu 1952 height", 5769: "Taranaki 1970 height", 5770: "Wellington 1953 height", 5771: "Chatham Island 1959 height", 5772: "Stewart Island 1977 height", 5773: "EGM96 height", 5774: "NG-L height", 5775: "Antalya height", 5776: "NN54 height", 5777: "Durres height", 5778: "GHA height", 5779: "NVN99 height", 5780: "Cascais height", 5781: "Constanta height", 5782: "Alicante height", 5783: "DHHN92 height", 5784: "DHHN85 height", 5785: "SNN76 height", 5786: "Baltic 1982 height", 5787: "EOMA 1980 height", 5788: "Kuwait PWD height", 5789: "KOC WD depth", 5790: "KOC CD height", 5791: "NGC 1948 height", 5792: "Danger 1950 height", 5793: "Mayotte 1950 height", 5794: "Martinique 1955 height", 5795: "Guadeloupe 1951 height", 5796: "Lagos 1955 height", 5797: "AIOC95 height", 5798: "EGM84 height", 5799: "DVR90 height", 5829: "Instantaneous Water Level height", 5831: "Instantaneous Water Level depth", 5843: "Ras Ghumays height", 5861: "LAT depth", 5862: "LLWLT depth", 5863: "ISLW depth", 5864: "MLLWS depth", 5865: "MLWS depth", 5866: "MLLW depth", 5867: "MLW depth", 5868: "MHW height", 5869: "MHHW height", 5870: "MHWS height", 5871: "HHWLT height", 5872: "HAT height", 5873: "Low Water depth", 5874: "High Water height", 5941: "NN2000 height", 6130: "GCVD54 height", 6131: "LCVD61 height", 6132: "CBVD61 height", 6178: "Cais da Pontinha - Funchal height", 6179: "Cais da Vila - Porto Santo height", 6180: "Cais das Velas height", 6181: "Horta height", 6182: "Cais da Madalena height", 6183: "Santa Cruz da Graciosa height", 6184: "Cais da Figueirinha - Angra do Heroismo height", 6185: "Santa Cruz das Flores height", 6186: "Cais da Vila do Porto height", 6187: "Ponta Delgada height", 6357: "NAVD88 depth", 6358: "NAVD88 depth (ftUS)", 6359: "NGVD29 depth", 6360: "NAVD88 height (ftUS)", 6638: "Tutuila 1962 height", 6639: "Guam 1963 height", 6640: "NMVD03 height", 6641: "PRVD02 height", 6642: "VIVD09 height", 6643: "ASVD02 height", 6644: "GUVD04 height", 6647: "CGVD2013 height", 6693: "JSLD72 height", 6694: "JGD2000 (vertical) height", 6695: "JGD2011 (vertical) height", 6916: "SHD height", 7446: "Famagusta 1960 height", 7447: "PNG08 height", 7651: "Kumul 34 height", 7652: "Kiunga height", 7699: "DHHN12 height", 7700: "Latvia 2000 height", 32767: "User-defined" } GeographicTypeGeoKey = { 3819: "HD1909", 3821: "TWD67", 3824: "TWD97", 3889: "IGRS", 3906: "MGI 1901", 4001: "Unknown datum based upon the Airy 1830 ellipsoid", 4002: "Unknown datum based upon the Airy Modified 1849 ellipsoid", 4003: "Unknown datum based upon the Australian National Spheroid", 4004: "Unknown datum based upon the Bessel 1841 ellipsoid", 4005: "Unknown datum based upon the Bessel Modified ellipsoid", 4006: "Unknown datum based upon the Bessel Namibia ellipsoid", 4007: "Unknown datum based upon the Clarke 1858 ellipsoid", 4008: "Unknown datum based upon the Clarke 1866 ellipsoid", 4009: "Unknown datum based upon the Clarke 1866 Michigan ellipsoid", 4010: "Unknown datum based upon the Clarke 1880 (Benoit) ellipsoid", 4011: "Unknown datum based upon the Clarke 1880 (IGN) ellipsoid", 4012: "Unknown datum based upon the Clarke 1880 (RGS) ellipsoid", 4013: "Unknown datum based upon the Clarke 1880 (Arc) ellipsoid", 4014: "Unknown datum based upon the Clarke 1880 (SGA 1922) ellipsoid", 4015: "Unknown datum based upon the Everest 1830 (1937 Adjustment) ellipsoid", 4016: "Unknown datum based upon the Everest 1830 (1967 Definition) ellipsoid", 4018: "Unknown datum based upon the Everest 1830 Modified ellipsoid", 4019: "Unknown datum based upon the GRS 1980 ellipsoid", 4020: "Unknown datum based upon the Helmert 1906 ellipsoid", 4021: "Unknown datum based upon the Indonesian National Spheroid", 4022: "Unknown datum based upon the International 1924 ellipsoid", 4023: "MOLDREF99", 4024: "Unknown datum based upon the Krassowsky 1940 ellipsoid", 4025: "Unknown datum based upon the NWL 9D ellipsoid", 4027: "Unknown datum based upon the Plessis 1817 ellipsoid", 4028: "Unknown datum based upon the Struve 1860 ellipsoid", 4029: "Unknown datum based upon the War Office ellipsoid", 4030: "Unknown datum based upon the WGS 84 ellipsoid", 4031: "Unknown datum based upon the GEM 10C ellipsoid", 4032: "Unknown datum based upon the OSU86F ellipsoid", 4033: "Unknown datum based upon the OSU91A ellipsoid", 4034: "Unknown datum based upon the Clarke 1880 ellipsoid", 4035: "Unknown datum based upon the Authalic Sphere", 4036: "Unknown datum based upon the GRS 1967 ellipsoid", 4041: "Unknown datum based upon the Average Terrestrial System 1977 ellipsoid", 4042: "Unknown datum based upon the Everest (1830 Definition) ellipsoid", 4043: "Unknown datum based upon the WGS 72 ellipsoid", 4044: "Unknown datum based upon the Everest 1830 (1962 Definition) ellipsoid", 4045: "Unknown datum based upon the Everest 1830 (1975 Definition) ellipsoid", 4046: "RGRDC 2005", 4047: "Unspecified datum based upon the GRS 1980 Authalic Sphere", 4052: "Unspecified datum based upon the Clarke 1866 Authalic Sphere", 4053: "Unspecified datum based upon the International 1924 Authalic Sphere", 4054: "Unspecified datum based upon the Hughes 1980 ellipsoid", 4055: "Popular Visualisation CRS", 4075: "SREF98", 4081: "REGCAN95", 4120: "Greek", 4121: "GGRS87", 4122: "ATS77", 4123: "KKJ", 4124: "RT90", 4125: "Samboja", 4126: "LKS94 (ETRS89)", 4127: "Tete", 4128: "Madzansua", 4129: "Observatario", 4130: "Moznet", 4131: "Indian 1960", 4132: "FD58", 4133: "EST92", 4134: "PSD93", 4135: "Old Hawaiian", 4136: "St. Lawrence Island", 4137: "St. Paul Island", 4138: "St. George Island", 4139: "Puerto Rico", 4140: "NAD83(CSRS98)", 4141: "Israel 1993", 4142: "Locodjo 1965", 4143: "Abidjan 1987", 4144: "Kalianpur 1937", 4145: "Kalianpur 1962", 4146: "Kalianpur 1975", 4147: "Hanoi 1972", 4148: "Hartebeesthoek94", 4149: "CH1903", 4150: "CH1903+", 4151: "CHTRF95", 4152: "NAD83(HARN)", 4153: "Rassadiran", 4154: "ED50(ED77)", 4155: "Dabola 1981", 4156: "S-JTSK", 4157: "Mount Dillon", 4158: "Naparima 1955", 4159: "ELD79", 4160: "Chos Malal 1914", 4161: "Pampa del Castillo", 4162: "Korean 1985", 4163: "Yemen NGN96", 4164: "South Yemen", 4165: "Bissau", 4166: "Korean 1995", 4167: "NZGD2000", 4168: "Accra", 4169: "American Samoa 1962", 4170: "SIRGAS 1995", 4171: "RGF93", 4172: "POSGAR", 4173: "IRENET95", 4174: "Sierra Leone 1924", 4175: "Sierra Leone 1968", 4176: "Australian Antarctic", 4178: "Pulkovo 1942(83)", 4179: "Pulkovo 1942(58)", 4180: "EST97", 4181: "Luxembourg 1930", 4182: "Azores Occidental 1939", 4183: "Azores Central 1948", 4184: "Azores Oriental 1940", 4185: "Madeira 1936", 4188: "OSNI 1952", 4189: "REGVEN", 4190: "POSGAR 98", 4191: "Albanian 1987", 4192: "Douala 1948", 4193: "Manoca 1962", 4194: "Qornoq 1927", 4195: "Scoresbysund 1952", 4196: "Ammassalik 1958", 4197: "Garoua", 4198: "Kousseri", 4199: "Egypt 1930", 4200: "Pulkovo 1995", 4201: "Adindan", 4202: "AGD66", 4203: "AGD84", 4204: "Ain el Abd", 4205: "Afgooye", 4206: "Agadez", 4207: "Lisbon", 4208: "Aratu", 4209: "Arc 1950", 4210: "Arc 1960", 4211: "Batavia", 4212: "Barbados 1938", 4213: "Beduaram", 4214: "Beijing 1954", 4215: "Belge 1950", 4216: "Bermuda 1957", 4218: "Bogota 1975", 4219: "Bukit Rimpah", 4220: "Camacupa", 4221: "Campo Inchauspe", 4222: "Cape", 4223: "Carthage", 4224: "Chua", 4225: "Corrego Alegre 1970-72", 4226: "Cote d'Ivoire", 4227: "Deir ez Zor", 4228: "Douala", 4229: "Egypt 1907", 4230: "ED50", 4231: "ED87", 4232: "Fahud", 4233: "Gandajika 1970", 4234: "Garoua", 4235: "Guyane Francaise", 4236: "Hu Tzu Shan 1950", 4237: "HD72", 4238: "ID74", 4239: "Indian 1954", 4240: "Indian 1975", 4241: "Jamaica 1875", 4242: "JAD69", 4243: "Kalianpur 1880", 4244: "Kandawala", 4245: "Kertau 1968", 4246: "KOC", 4247: "La Canoa", 4248: "PSAD56", 4249: "Lake", 4250: "Leigon", 4251: "Liberia 1964", 4252: "Lome", 4253: "Luzon 1911", 4254: "Hito XVIII 1963", 4255: "Herat North", 4256: "Mahe 1971", 4257: "Makassar", 4258: "ETRS89", 4259: "Malongo 1987", 4260: "Manoca", 4261: "Merchich", 4262: "Massawa", 4263: "Minna", 4264: "Mhast", 4265: "Monte Mario", 4266: "M'poraloko", 4267: "NAD27", 4268: "NAD27 Michigan", 4269: "NAD83", 4270: "Nahrwan 1967", 4271: "Naparima 1972", 4272: "NZGD49", 4273: "NGO 1948", 4274: "Datum 73", 4275: "NTF", 4276: "NSWC 9Z-2", 4277: "OSGB 1936", 4278: "OSGB70", 4279: "OS(SN)80", 4280: "Padang", 4281: "Palestine 1923", 4282: "Pointe Noire", 4283: "GDA94", 4284: "Pulkovo 1942", 4285: "Qatar 1974", 4286: "Qatar 1948", 4287: "Qornoq", 4288: "Loma Quintana", 4289: "Amersfoort", 4291: "SAD69", 4292: "Sapper Hill 1943", 4293: "Schwarzeck", 4294: "Segora", 4295: "Serindung", 4296: "Sudan", 4297: "Tananarive", 4298: "Timbalai 1948", 4299: "TM65", 4300: "TM75", 4301: "Tokyo", 4302: "Trinidad 1903", 4303: "TC(1948)", 4304: "Voirol 1875", 4306: "Bern 1938", 4307: "Nord Sahara 1959", 4308: "RT38", 4309: "Yacare", 4310: "Yoff", 4311: "Zanderij", 4312: "MGI", 4313: "Belge 1972", 4314: "DHDN", 4315: "Conakry 1905", 4316: "Dealul Piscului 1930", 4317: "Dealul Piscului 1970", 4318: "NGN", 4319: "KUDAMS", 4322: "WGS 72", 4324: "WGS 72BE", 4326: "WGS 84", 4463: "RGSPM06", 4470: "RGM04", 4475: "Cadastre 1997", 4483: "Mexico ITRF92", 4490: "China Geodetic Coordinate System 2000", 4555: "New Beijing", 4558: "RRAF 1991", 4600: "Anguilla 1957", 4601: "Antigua 1943", 4602: "Dominica 1945", 4603: "Grenada 1953", 4604: "Montserrat 1958", 4605: "St. Kitts 1955", 4606: "St. Lucia 1955", 4607: "St. Vincent 1945", 4608: "NAD27(76)", 4609: "NAD27(CGQ77)", 4610: "Xian 1980", 4611: "Hong Kong 1980", 4612: "JGD2000", 4613: "Segara", 4614: "QND95", 4615: "Porto Santo", 4616: "Selvagem Grande", 4617: "NAD83(CSRS)", 4618: "SAD69", 4619: "SWEREF99", 4620: "Point 58", 4621: "Fort Marigot", 4622: "Guadeloupe 1948", 4623: "CSG67", 4624: "RGFG95", 4625: "Martinique 1938", 4626: "Reunion 1947", 4627: "RGR92", 4628: "Tahiti 52", 4629: "Tahaa 54", 4630: "IGN72 Nuku Hiva", 4631: "K0 1949", 4632: "Combani 1950", 4633: "IGN56 Lifou", 4634: "IGN72 Grand Terre", 4635: "ST87 Ouvea", 4636: "Petrels 1972", 4637: "Perroud 1950", 4638: "Saint Pierre et Miquelon 1950", 4639: "MOP78", 4640: "RRAF 1991", 4641: "IGN53 Mare", 4642: "ST84 Ile des Pins", 4643: "ST71 Belep", 4644: "NEA74 Noumea", 4645: "RGNC 1991", 4646: "Grand Comoros", 4657: "Reykjavik 1900", 4658: "Hjorsey 1955", 4659: "ISN93", 4660: "Helle 1954", 4661: "LKS92", 4662: "IGN72 Grande Terre", 4663: "Porto Santo 1995", 4664: "Azores Oriental 1995", 4665: "Azores Central 1995", 4666: "Lisbon 1890", 4667: "IKBD-92", 4668: "ED79", 4669: "LKS94", 4670: "IGM95", 4671: "Voirol 1879", 4672: "Chatham Islands 1971", 4673: "Chatham Islands 1979", 4674: "SIRGAS 2000", 4675: "Guam 1963", 4676: "Vientiane 1982", 4677: "Lao 1993", 4678: "Lao 1997", 4679: "Jouik 1961", 4680: "Nouakchott 1965", 4681: "Mauritania 1999", 4682: "Gulshan 303", 4683: "PRS92", 4684: "Gan 1970", 4685: "Gandajika", 4686: "MAGNA-SIRGAS", 4687: "RGPF", 4688: "Fatu Iva 72", 4689: "IGN63 Hiva Oa", 4690: "Tahiti 79", 4691: "Moorea 87", 4692: "Maupiti 83", 4693: "Nakhl-e Ghanem", 4694: "POSGAR 94", 4695: "Katanga 1955", 4696: "Kasai 1953", 4697: "IGC 1962 6th Parallel South", 4698: "IGN 1962 Kerguelen", 4699: "Le Pouce 1934", 4700: "IGN Astro 1960", 4701: "IGCB 1955", 4702: "Mauritania 1999", 4703: "Mhast 1951", 4704: "Mhast (onshore)", 4705: "Mhast (offshore)", 4706: "Egypt Gulf of Suez S-650 TL", 4707: "Tern Island 1961", 4708: "Cocos Islands 1965", 4709: "Iwo Jima 1945", 4710: "St. Helena 1971", 4711: "Marcus Island 1952", 4712: "Ascension Island 1958", 4713: "Ayabelle Lighthouse", 4714: "Bellevue", 4715: "Camp Area Astro", 4716: "Phoenix Islands 1966", 4717: "Cape Canaveral", 4718: "Solomon 1968", 4719: "Easter Island 1967", 4720: "Fiji 1986", 4721: "Fiji 1956", 4722: "South Georgia 1968", 4723: "GCGD59", 4724: "Diego Garcia 1969", 4725: "Johnston Island 1961", 4726: "SIGD61", 4727: "Midway 1961", 4728: "Pico de las Nieves 1984", 4729: "Pitcairn 1967", 4730: "Santo 1965", 4731: "Viti Levu 1916", 4732: "Marshall Islands 1960", 4733: "Wake Island 1952", 4734: "Tristan 1968", 4735: "Kusaie 1951", 4736: "Deception Island", 4737: "Korea 2000", 4738: "Hong Kong 1963", 4739: "Hong Kong 1963(67)", 4740: "PZ-90", 4741: "FD54", 4742: "GDM2000", 4743: "Karbala 1979", 4744: "Nahrwan 1934", 4745: "RD/83", 4746: "PD/83", 4747: "GR96", 4748: "Vanua Levu 1915", 4749: "RGNC91-93", 4750: "ST87 Ouvea", 4751: "Kertau (RSO)", 4752: "Viti Levu 1912", 4753: "fk89", 4754: "LGD2006", 4755: "DGN95", 4756: "VN-2000", 4757: "SVY21", 4758: "JAD2001", 4759: "NAD83(NSRS2007)", 4760: "WGS 66", 4761: "HTRS96", 4762: "BDA2000", 4763: "Pitcairn 2006", 4764: "RSRGD2000", 4765: "Slovenia 1996", 4801: "Bern 1898 (Bern)", 4802: "Bogota 1975 (Bogota)", 4803: "Lisbon (Lisbon)", 4804: "Makassar (Jakarta)", 4805: "MGI (Ferro)", 4806: "Monte Mario (Rome)", 4807: "NTF (Paris)", 4808: "Padang (Jakarta)", 4809: "Belge 1950 (Brussels)", 4810: "Tananarive (Paris)", 4811: "Voirol 1875 (Paris)", 4813: "Batavia (Jakarta)", 4814: "RT38 (Stockholm)", 4815: "Greek (Athens)", 4816: "Carthage (Paris)", 4817: "NGO 1948 (Oslo)", 4818: "S-JTSK (Ferro)", 4819: "Nord Sahara 1959 (Paris)", 4820: "Segara (Jakarta)", 4821: "Voirol 1879 (Paris)", 4823: "Sao Tome", 4824: "Principe", 4901: "ATF (Paris)", 4902: "NDG (Paris)", 4903: "Madrid 1870 (Madrid)", 4904: "Lisbon 1890 (Lisbon)", 5013: "PTRA08", 5132: "Tokyo 1892", 5228: "S-JTSK/05", 5229: "S-JTSK/05 (Ferro)", 5233: "SLD99", 5246: "GDBD2009", 5252: "TUREF", 5264: "DRUKREF 03", 5324: "ISN2004", 5340: "POSGAR 2007", 5354: "MARGEN", 5360: "SIRGAS-Chile", 5365: "CR05", 5371: "MACARIO SOLIS", 5373: "Peru96", 5381: "SIRGAS-ROU98", 5393: "SIRGAS_ES2007.8", 5451: "Ocotepeque 1935", 5464: "Sibun Gorge 1922", 5467: "Panama-Colon 1911", 5489: "RGAF09", 5524: "Corrego Alegre 1961", 5527: "SAD69(96)", 5546: "PNG94", 5561: "UCS-2000", 5593: "FEH2010", 5681: "DB_REF", 5886: "TGD2005", 6135: "CIGD11", 6207: "Nepal 1981", 6311: "CGRS93", 6318: "NAD83(2011)", 6322: "NAD83(PA11)", 6325: "NAD83(MA11)", 6365: "Mexico ITRF2008", 6668: "JGD2011", 6706: "RDN2008", 6783: "NAD83(CORS96)", 6881: "Aden 1925", 6882: "Bekaa Valley 1920", 6883: "Bioko", 6892: "South East Island 1943", 6894: "Gambia", 6980: "IGD05", 6983: "IG05 Intermediate CRS", 6987: "IGD05/12", 6990: "IG05/12 Intermediate CRS", 7035: "RGSPM06 (lon-lat)", 7037: "RGR92 (lon-lat)", 7039: "RGM04 (lon-lat)", 7041: "RGFG95 (lon-lat)", 7073: "RGTAAF07", 7084: "RGF93 (lon-lat)", 7086: "RGAF09 (lon-lat)", 7088: "RGTAAF07 (lon-lat)", 7133: "RGTAAF07 (lon-lat)", 7136: "IGD05", 7139: "IGD05/12", 7373: "ONGD14", 7686: "Kyrg-06", 32767: "User-defined", 61206405: "Greek (deg)", 61216405: "GGRS87 (deg)", 61226405: "ATS77 (deg)", 61236405: "KKJ (deg)", 61246405: "RT90 (deg)", 61266405: "LKS94 (ETRS89) (deg)", 61266413: "LKS94 (ETRS89) (3D deg)", 61276405: "Tete (deg)", 61286405: "Madzansua (deg)", 61296405: "Observatario (deg)", 61306405: "Moznet (deg)", 61316405: "Indian 1960 (deg)", 61326405: "FD58 (deg)", 61336405: "EST92 (deg)", 61346405: "PDO Survey Datum 1993 (deg)", 61356405: "Old Hawaiian (deg)", 61366405: "St. Lawrence Island (deg)", 61376405: "St. Paul Island (deg)", 61386405: "St. George Island (deg)", 61396405: "Puerto Rico (deg)", 61406405: "NAD83(CSRS) (deg)", 61416405: "Israel (deg)", 61426405: "Locodjo 1965 (deg)", 61436405: "Abidjan 1987 (deg)", 61446405: "Kalianpur 1937 (deg)", 61456405: "Kalianpur 1962 (deg)", 61466405: "Kalianpur 1975 (deg)", 61476405: "Hanoi 1972 (deg)", 61486405: "Hartebeesthoek94 (deg)", 61496405: "CH1903 (deg)", 61506405: "CH1903+ (deg)", 61516405: "CHTRF95 (deg)", 61526405: "NAD83(HARN) (deg)", 61536405: "Rassadiran (deg)", 61546405: "ED50(ED77) (deg)", 61556405: "Dabola 1981 (deg)", 61566405: "S-JTSK (deg)", 61576405: "Mount Dillon (deg)", 61586405: "Naparima 1955 (deg)", 61596405: "ELD79 (deg)", 61606405: "Chos Malal 1914 (deg)", 61616405: "Pampa del Castillo (deg)", 61626405: "Korean 1985 (deg)", 61636405: "Yemen NGN96 (deg)", 61646405: "South Yemen (deg)", 61656405: "Bissau (deg)", 61666405: "Korean 1995 (deg)", 61676405: "NZGD2000 (deg)", 61686405: "Accra (deg)", 61696405: "American Samoa 1962 (deg)", 61706405: "SIRGAS (deg)", 61716405: "RGF93 (deg)", 61736405: "IRENET95 (deg)", 61746405: "Sierra Leone 1924 (deg)", 61756405: "Sierra Leone 1968 (deg)", 61766405: "Australian Antarctic (deg)", 61786405: "Pulkovo 1942(83) (deg)", 61796405: "Pulkovo 1942(58) (deg)", 61806405: "EST97 (deg)", 61816405: "Luxembourg 1930 (deg)", 61826405: "Azores Occidental 1939 (deg)", 61836405: "Azores Central 1948 (deg)", 61846405: "Azores Oriental 1940 (deg)", 61886405: "OSNI 1952 (deg)", 61896405: "REGVEN (deg)", 61906405: "POSGAR 98 (deg)", 61916405: "Albanian 1987 (deg)", 61926405: "Douala 1948 (deg)", 61936405: "Manoca 1962 (deg)", 61946405: "Qornoq 1927 (deg)", 61956405: "Scoresbysund 1952 (deg)", 61966405: "Ammassalik 1958 (deg)", 61976405: "Garoua (deg)", 61986405: "Kousseri (deg)", 61996405: "Egypt 1930 (deg)", 62006405: "Pulkovo 1995 (deg)", 62016405: "Adindan (deg)", 62026405: "AGD66 (deg)", 62036405: "AGD84 (deg)", 62046405: "Ain el Abd (deg)", 62056405: "Afgooye (deg)", 62066405: "Agadez (deg)", 62076405: "Lisbon (deg)", 62086405: "Aratu (deg)", 62096405: "Arc 1950 (deg)", 62106405: "Arc 1960 (deg)", 62116405: "Batavia (deg)", 62126405: "Barbados 1938 (deg)", 62136405: "Beduaram (deg)", 62146405: "Beijing 1954 (deg)", 62156405: "Belge 1950 (deg)", 62166405: "Bermuda 1957 (deg)", 62186405: "Bogota 1975 (deg)", 62196405: "Bukit Rimpah (deg)", 62206405: "Camacupa (deg)", 62216405: "Campo Inchauspe (deg)", 62226405: "Cape (deg)", 62236405: "Carthage (deg)", 62246405: "Chua (deg)", 62256405: "Corrego Alegre (deg)", 62276405: "Deir ez Zor (deg)", 62296405: "Egypt 1907 (deg)", 62306405: "ED50 (deg)", 62316405: "ED87 (deg)", 62326405: "Fahud (deg)", 62336405: "Gandajika 1970 (deg)", 62366405: "Hu Tzu Shan (deg)", 62376405: "HD72 (deg)", 62386405: "ID74 (deg)", 62396405: "Indian 1954 (deg)", 62406405: "Indian 1975 (deg)", 62416405: "Jamaica 1875 (deg)", 62426405: "JAD69 (deg)", 62436405: "Kalianpur 1880 (deg)", 62446405: "Kandawala (deg)", 62456405: "Kertau (deg)", 62466405: "KOC (deg)", 62476405: "La Canoa (deg)", 62486405: "PSAD56 (deg)", 62496405: "Lake (deg)", 62506405: "Leigon (deg)", 62516405: "Liberia 1964 (deg)", 62526405: "Lome (deg)", 62536405: "Luzon 1911 (deg)", 62546405: "Hito XVIII 1963 (deg)", 62556405: "Herat North (deg)", 62566405: "Mahe 1971 (deg)", 62576405: "Makassar (deg)", 62586405: "ETRS89 (deg)", 62596405: "Malongo 1987 (deg)", 62616405: "Merchich (deg)", 62626405: "Massawa (deg)", 62636405: "Minna (deg)", 62646405: "Mhast (deg)", 62656405: "Monte Mario (deg)", 62666405: "M'poraloko (deg)", 62676405: "NAD27 (deg)", 62686405: "NAD27 Michigan (deg)", 62696405: "NAD83 (deg)", 62706405: "Nahrwan 1967 (deg)", 62716405: "Naparima 1972 (deg)", 62726405: "NZGD49 (deg)", 62736405: "NGO 1948 (deg)", 62746405: "Datum 73 (deg)", 62756405: "NTF (deg)", 62766405: "NSWC 9Z-2 (deg)", 62776405: "OSGB 1936 (deg)", 62786405: "OSGB70 (deg)", 62796405: "OS(SN)80 (deg)", 62806405: "Padang (deg)", 62816405: "Palestine 1923 (deg)", 62826405: "Pointe Noire (deg)", 62836405: "GDA94 (deg)", 62846405: "Pulkovo 1942 (deg)", 62856405: "Qatar 1974 (deg)", 62866405: "Qatar 1948 (deg)", 62886405: "Loma Quintana (deg)", 62896405: "Amersfoort (deg)", 62926405: "Sapper Hill 1943 (deg)", 62936405: "Schwarzeck (deg)", 62956405: "Serindung (deg)", 62976405: "Tananarive (deg)", 62986405: "Timbalai 1948 (deg)", 62996405: "TM65 (deg)", 63006405: "TM75 (deg)", 63016405: "Tokyo (deg)", 63026405: "Trinidad 1903 (deg)", 63036405: "TC(1948) (deg)", 63046405: "Voirol 1875 (deg)", 63066405: "Bern 1938 (deg)", 63076405: "Nord Sahara 1959 (deg)", 63086405: "RT38 (deg)", 63096405: "Yacare (deg)", 63106405: "Yoff (deg)", 63116405: "Zanderij (deg)", 63126405: "MGI (deg)", 63136405: "Belge 1972 (deg)", 63146405: "DHDN (deg)", 63156405: "Conakry 1905 (deg)", 63166405: "Dealul Piscului 1933 (deg)", 63176405: "Dealul Piscului 1970 (deg)", 63186405: "NGN (deg)", 63196405: "KUDAMS (deg)", 63226405: "WGS 72 (deg)", 63246405: "WGS 72BE (deg)", 63266405: "WGS 84 (deg)", 63266406: "WGS 84 (degH)", 63266407: "WGS 84 (Hdeg)", 63266408: "WGS 84 (DM)", 63266409: "WGS 84 (DMH)", 63266410: "WGS 84 (HDM)", 63266411: "WGS 84 (DMS)", 63266412: "WGS 84 (HDMS)", 66006405: "Anguilla 1957 (deg)", 66016405: "Antigua 1943 (deg)", 66026405: "Dominica 1945 (deg)", 66036405: "Grenada 1953 (deg)", 66046405: "Montserrat 1958 (deg)", 66056405: "St. Kitts 1955 (deg)", 66066405: "St. Lucia 1955 (deg)", 66076405: "St. Vincent 1945 (deg)", 66086405: "NAD27(76) (deg)", 66096405: "NAD27(CGQ77) (deg)", 66106405: "Xian 1980 (deg)", 66116405: "Hong Kong 1980 (deg)", 66126405: "JGD2000 (deg)", 66136405: "Segara (deg)", 66146405: "QND95 (deg)", 66156405: "Porto Santo (deg)", 66166405: "Selvagem Grande (deg)", 66186405: "SAD69 (deg)", 66196405: "SWEREF99 (deg)", 66206405: "Point 58 (deg)", 66216405: "Fort Marigot (deg)", 66226405: "Sainte Anne (deg)", 66236405: "CSG67 (deg)", 66246405: "RGFG95 (deg)", 66256405: "Fort Desaix (deg)", 66266405: "Piton des Neiges (deg)", 66276405: "RGR92 (deg)", 66286405: "Tahiti (deg)", 66296405: "Tahaa (deg)", 66306405: "IGN72 Nuku Hiva (deg)", 66316405: "K0 1949 (deg)", 66326405: "Combani 1950 (deg)", 66336405: "IGN56 Lifou (deg)", 66346405: "IGN72 Grande Terre (deg)", 66356405: "ST87 Ouvea (deg)", 66366405: "Petrels 1972 (deg)", 66376405: "Perroud 1950 (deg)", 66386405: "Saint Pierre et Miquelon 1950 (deg)", 66396405: "MOP78 (deg)", 66406405: "RRAF 1991 (deg)", 66416405: "IGN53 Mare (deg)", 66426405: "ST84 Ile des Pins (deg)", 66436405: "ST71 Belep (deg)", 66446405: "NEA74 Noumea (deg)", 66456405: "RGNC 1991 (deg)", 66466405: "Grand Comoros (deg)", 66576405: "Reykjavik 1900 (deg)", 66586405: "Hjorsey 1955 (deg)", 66596405: "ISN93 (deg)", 66606405: "Helle 1954 (deg)", 66616405: "LKS92 (deg)", 66636405: "Porto Santo 1995 (deg)", 66646405: "Azores Oriental 1995 (deg)", 66656405: "Azores Central 1995 (deg)", 66666405: "Lisbon 1890 (deg)", 66676405: "IKBD-92 (deg)", 68016405: "Bern 1898 (Bern) (deg)", 68026405: "Bogota 1975 (Bogota) (deg)", 68036405: "Lisbon (Lisbon) (deg)", 68046405: "Makassar (Jakarta) (deg)", 68056405: "MGI (Ferro) (deg)", 68066405: "Monte Mario (Rome) (deg)", 68086405: "Padang (Jakarta) (deg)", 68096405: "Belge 1950 (Brussels) (deg)", 68136405: "Batavia (Jakarta) (deg)", 68146405: "RT38 (Stockholm) (deg)", 68156405: "Greek (Athens) (deg)", 68186405: "S-JTSK (Ferro) (deg)", 68206405: "Segara (Jakarta) (deg)", 69036405: "Madrid 1870 (Madrid) (deg)" } ProjectedCSTypeGeoKey = { 2000: "Anguilla 1957 / British West Indies Grid", 2001: "Antigua 1943 / British West Indies Grid", 2002: "Dominica 1945 / British West Indies Grid", 2003: "Grenada 1953 / British West Indies Grid", 2004: "Montserrat 1958 / British West Indies Grid", 2005: "St. Kitts 1955 / British West Indies Grid", 2006: "St. Lucia 1955 / British West Indies Grid", 2007: "St. Vincent 45 / British West Indies Grid", 2008: "NAD27(CGQ77) / SCoPQ zone 2", 2009: "NAD27(CGQ77) / SCoPQ zone 3", 2010: "NAD27(CGQ77) / SCoPQ zone 4", 2011: "NAD27(CGQ77) / SCoPQ zone 5", 2012: "NAD27(CGQ77) / SCoPQ zone 6", 2013: "NAD27(CGQ77) / SCoPQ zone 7", 2014: "NAD27(CGQ77) / SCoPQ zone 8", 2015: "NAD27(CGQ77) / SCoPQ zone 9", 2016: "NAD27(CGQ77) / SCoPQ zone 10", 2017: "NAD27(76) / MTM zone 8", 2018: "NAD27(76) / MTM zone 9", 2019: "NAD27(76) / MTM zone 10", 2020: "NAD27(76) / MTM zone 11", 2021: "NAD27(76) / MTM zone 12", 2022: "NAD27(76) / MTM zone 13", 2023: "NAD27(76) / MTM zone 14", 2024: "NAD27(76) / MTM zone 15", 2025: "NAD27(76) / MTM zone 16", 2026: "NAD27(76) / MTM zone 17", 2027: "NAD27(76) / UTM zone 15N", 2028: "NAD27(76) / UTM zone 16N", 2029: "NAD27(76) / UTM zone 17N", 2030: "NAD27(76) / UTM zone 18N", 2031: "NAD27(CGQ77) / UTM zone 17N", 2032: "NAD27(CGQ77) / UTM zone 18N", 2033: "NAD27(CGQ77) / UTM zone 19N", 2034: "NAD27(CGQ77) / UTM zone 20N", 2035: "NAD27(CGQ77) / UTM zone 21N", 2036: "NAD83(CSRS98) / New Brunswick Stereo", 2037: "NAD83(CSRS98) / UTM zone 19N", 2038: "NAD83(CSRS98) / UTM zone 20N", 2039: "Israel 1993 / Israeli TM Grid", 2040: "Locodjo 1965 / UTM zone 30N", 2041: "Abidjan 1987 / UTM zone 30N", 2042: "Locodjo 1965 / UTM zone 29N", 2043: "Abidjan 1987 / UTM zone 29N", 2044: "Hanoi 1972 / Gauss-Kruger zone 18", 2045: "Hanoi 1972 / Gauss-Kruger zone 19", 2046: "Hartebeesthoek94 / Lo15", 2047: "Hartebeesthoek94 / Lo17", 2048: "Hartebeesthoek94 / Lo19", 2049: "Hartebeesthoek94 / Lo21", 2050: "Hartebeesthoek94 / Lo23", 2051: "Hartebeesthoek94 / Lo25", 2052: "Hartebeesthoek94 / Lo27", 2053: "Hartebeesthoek94 / Lo29", 2054: "Hartebeesthoek94 / Lo31", 2055: "Hartebeesthoek94 / Lo33", 2056: "CH1903+ / LV95", 2057: "Rassadiran / Nakhl e Taqi", 2058: "ED50(ED77) / UTM zone 38N", 2059: "ED50(ED77) / UTM zone 39N", 2060: "ED50(ED77) / UTM zone 40N", 2061: "ED50(ED77) / UTM zone 41N", 2062: "Madrid 1870 (Madrid) / Spain", 2063: "Dabola 1981 / UTM zone 28N", 2064: "Dabola 1981 / UTM zone 29N", 2065: "S-JTSK (Ferro) / Krovak", 2066: "Mount Dillon / Tobago Grid", 2067: "Naparima 1955 / UTM zone 20N", 2068: "ELD79 / Libya zone 5", 2069: "ELD79 / Libya zone 6", 2070: "ELD79 / Libya zone 7", 2071: "ELD79 / Libya zone 8", 2072: "ELD79 / Libya zone 9", 2073: "ELD79 / Libya zone 10", 2074: "ELD79 / Libya zone 11", 2075: "ELD79 / Libya zone 12", 2076: "ELD79 / Libya zone 13", 2077: "ELD79 / UTM zone 32N", 2078: "ELD79 / UTM zone 33N", 2079: "ELD79 / UTM zone 34N", 2080: "ELD79 / UTM zone 35N", 2081: "Chos Malal 1914 / Argentina 2", 2082: "Pampa del Castillo / Argentina 2", 2083: "Hito XVIII 1963 / Argentina 2", 2084: "Hito XVIII 1963 / UTM zone 19S", 2085: "NAD27 / Cuba Norte", 2086: "NAD27 / Cuba Sur", 2087: "ELD79 / TM 12 NE", 2088: "Carthage / TM 11 NE", 2089: "Yemen NGN96 / UTM zone 38N", 2090: "Yemen NGN96 / UTM zone 39N", 2091: "South Yemen / Gauss Kruger zone 8", 2092: "South Yemen / Gauss Kruger zone 9", 2093: "Hanoi 1972 / GK 106 NE", 2094: "WGS 72BE / TM 106 NE", 2095: "Bissau / UTM zone 28N", 2096: "Korean 1985 / East Belt", 2097: "Korean 1985 / Central Belt", 2098: "Korean 1985 / West Belt", 2099: "Qatar 1948 / Qatar Grid", 2100: "GGRS87 / Greek Grid", 2101: "Lake / Maracaibo Grid M1", 2102: "Lake / Maracaibo Grid", 2103: "Lake / Maracaibo Grid M3", 2104: "Lake / Maracaibo La Rosa Grid", 2105: "NZGD2000 / Mount Eden 2000", 2106: "NZGD2000 / Bay of Plenty 2000", 2107: "NZGD2000 / Poverty Bay 2000", 2108: "NZGD2000 / Hawkes Bay 2000", 2109: "NZGD2000 / Taranaki 2000", 2110: "NZGD2000 / Tuhirangi 2000", 2111: "NZGD2000 / Wanganui 2000", 2112: "NZGD2000 / Wairarapa 2000", 2113: "NZGD2000 / Wellington 2000", 2114: "NZGD2000 / Collingwood 2000", 2115: "NZGD2000 / Nelson 2000", 2116: "NZGD2000 / Karamea 2000", 2117: "NZGD2000 / Buller 2000", 2118: "NZGD2000 / Grey 2000", 2119: "NZGD2000 / Amuri 2000", 2120: "NZGD2000 / Marlborough 2000", 2121: "NZGD2000 / Hokitika 2000", 2122: "NZGD2000 / Okarito 2000", 2123: "NZGD2000 / Jacksons Bay 2000", 2124: "NZGD2000 / Mount Pleasant 2000", 2125: "NZGD2000 / Gawler 2000", 2126: "NZGD2000 / Timaru 2000", 2127: "NZGD2000 / Lindis Peak 2000", 2128: "NZGD2000 / Mount Nicholas 2000", 2129: "NZGD2000 / Mount York 2000", 2130: "NZGD2000 / Observation Point 2000", 2131: "NZGD2000 / North Taieri 2000", 2132: "NZGD2000 / Bluff 2000", 2133: "NZGD2000 / UTM zone 58S", 2134: "NZGD2000 / UTM zone 59S", 2135: "NZGD2000 / UTM zone 60S", 2136: "Accra / Ghana National Grid", 2137: "Accra / TM 1 NW", 2138: "NAD27(CGQ77) / Quebec Lambert", 2139: "NAD83(CSRS98) / SCoPQ zone 2", 2140: "NAD83(CSRS98) / MTM zone 3", 2141: "NAD83(CSRS98) / MTM zone 4", 2142: "NAD83(CSRS98) / MTM zone 5", 2143: "NAD83(CSRS98) / MTM zone 6", 2144: "NAD83(CSRS98) / MTM zone 7", 2145: "NAD83(CSRS98) / MTM zone 8", 2146: "NAD83(CSRS98) / MTM zone 9", 2147: "NAD83(CSRS98) / MTM zone 10", 2148: "NAD83(CSRS98) / UTM zone 21N", 2149: "NAD83(CSRS98) / UTM zone 18N", 2150: "NAD83(CSRS98) / UTM zone 17N", 2151: "NAD83(CSRS98) / UTM zone 13N", 2152: "NAD83(CSRS98) / UTM zone 12N", 2153: "NAD83(CSRS98) / UTM zone 11N", 2154: "RGF93 / Lambert-93", 2155: "American Samoa 1962 / American Samoa Lambert", 2156: "NAD83(HARN) / UTM zone 59S", 2157: "IRENET95 / Irish Transverse Mercator", 2158: "IRENET95 / UTM zone 29N", 2159: "Sierra Leone 1924 / New Colony Grid", 2160: "Sierra Leone 1924 / New War Office Grid", 2161: "Sierra Leone 1968 / UTM zone 28N", 2162: "Sierra Leone 1968 / UTM zone 29N", 2163: "US National Atlas Equal Area", 2164: "Locodjo 1965 / TM 5 NW", 2165: "Abidjan 1987 / TM 5 NW", 2166: "Pulkovo 1942(83) / Gauss Kruger zone 3", 2167: "Pulkovo 1942(83) / Gauss Kruger zone 4", 2168: "Pulkovo 1942(83) / Gauss Kruger zone 5", 2169: "Luxembourg 1930 / Gauss", 2170: "MGI / Slovenia Grid", 2171: "Pulkovo 1942(58) / Poland zone I", 2172: "Pulkovo 1942(58) / Poland zone II", 2173: "Pulkovo 1942(58) / Poland zone III", 2174: "Pulkovo 1942(58) / Poland zone IV", 2175: "Pulkovo 1942(58) / Poland zone V", 2176: "ETRS89 / Poland CS2000 zone 5", 2177: "ETRS89 / Poland CS2000 zone 6", 2178: "ETRS89 / Poland CS2000 zone 7", 2179: "ETRS89 / Poland CS2000 zone 8", 2180: "ETRS89 / Poland CS92", 2188: "Azores Occidental 1939 / UTM zone 25N", 2189: "Azores Central 1948 / UTM zone 26N", 2190: "Azores Oriental 1940 / UTM zone 26N", 2191: "Madeira 1936 / UTM zone 28N", 2192: "ED50 / France EuroLambert", 2193: "NZGD2000 / New Zealand Transverse Mercator 2000", 2194: "American Samoa 1962 / American Samoa Lambert", 2195: "NAD83(HARN) / UTM zone 2S", 2196: "ETRS89 / Kp2000 Jutland", 2197: "ETRS89 / Kp2000 Zealand", 2198: "ETRS89 / Kp2000 Bornholm", 2199: "Albanian 1987 / Gauss Kruger zone 4", 2200: "ATS77 / New Brunswick Stereographic (ATS77)", 2201: "REGVEN / UTM zone 18N", 2202: "REGVEN / UTM zone 19N", 2203: "REGVEN / UTM zone 20N", 2204: "NAD27 / Tennessee", 2205: "NAD83 / Kentucky North", 2206: "ED50 / 3-degree Gauss-Kruger zone 9", 2207: "ED50 / 3-degree Gauss-Kruger zone 10", 2208: "ED50 / 3-degree Gauss-Kruger zone 11", 2209: "ED50 / 3-degree Gauss-Kruger zone 12", 2210: "ED50 / 3-degree Gauss-Kruger zone 13", 2211: "ED50 / 3-degree Gauss-Kruger zone 14", 2212: "ED50 / 3-degree Gauss-Kruger zone 15", 2213: "ETRS89 / TM 30 NE", 2214: "Douala 1948 / AOF west", 2215: "Manoca 1962 / UTM zone 32N", 2216: "Qornoq 1927 / UTM zone 22N", 2217: "Qornoq 1927 / UTM zone 23N", 2218: "Scoresbysund 1952 / Greenland zone 5 east", 2219: "ATS77 / UTM zone 19N", 2220: "ATS77 / UTM zone 20N", 2221: "Scoresbysund 1952 / Greenland zone 6 east", 2222: "NAD83 / Arizona East (ft)", 2223: "NAD83 / Arizona Central (ft)", 2224: "NAD83 / Arizona West (ft)", 2225: "NAD83 / California zone 1 (ftUS)", 2226: "NAD83 / California zone 2 (ftUS)", 2227: "NAD83 / California zone 3 (ftUS)", 2228: "NAD83 / California zone 4 (ftUS)", 2229: "NAD83 / California zone 5 (ftUS)", 2230: "NAD83 / California zone 6 (ftUS)", 2231: "NAD83 / Colorado North (ftUS)", 2232: "NAD83 / Colorado Central (ftUS)", 2233: "NAD83 / Colorado South (ftUS)", 2234: "NAD83 / Connecticut (ftUS)", 2235: "NAD83 / Delaware (ftUS)", 2236: "NAD83 / Florida East (ftUS)", 2237: "NAD83 / Florida West (ftUS)", 2238: "NAD83 / Florida North (ftUS)", 2239: "NAD83 / Georgia East (ftUS)", 2240: "NAD83 / Georgia West (ftUS)", 2241: "NAD83 / Idaho East (ftUS)", 2242: "NAD83 / Idaho Central (ftUS)", 2243: "NAD83 / Idaho West (ftUS)", 2244: "NAD83 / Indiana East (ftUS)", 2245: "NAD83 / Indiana West (ftUS)", 2246: "NAD83 / Kentucky North (ftUS)", 2247: "NAD83 / Kentucky South (ftUS)", 2248: "NAD83 / Maryland (ftUS)", 2249: "NAD83 / Massachusetts Mainland (ftUS)", 2250: "NAD83 / Massachusetts Island (ftUS)", 2251: "NAD83 / Michigan North (ft)", 2252: "NAD83 / Michigan Central (ft)", 2253: "NAD83 / Michigan South (ft)", 2254: "NAD83 / Mississippi East (ftUS)", 2255: "NAD83 / Mississippi West (ftUS)", 2256: "NAD83 / Montana (ft)", 2257: "NAD83 / New Mexico East (ftUS)", 2258: "NAD83 / New Mexico Central (ftUS)", 2259: "NAD83 / New Mexico West (ftUS)", 2260: "NAD83 / New York East (ftUS)", 2261: "NAD83 / New York Central (ftUS)", 2262: "NAD83 / New York West (ftUS)", 2263: "NAD83 / New York Long Island (ftUS)", 2264: "NAD83 / North Carolina (ftUS)", 2265: "NAD83 / North Dakota North (ft)", 2266: "NAD83 / North Dakota South (ft)", 2267: "NAD83 / Oklahoma North (ftUS)", 2268: "NAD83 / Oklahoma South (ftUS)", 2269: "NAD83 / Oregon North (ft)", 2270: "NAD83 / Oregon South (ft)", 2271: "NAD83 / Pennsylvania North (ftUS)", 2272: "NAD83 / Pennsylvania South (ftUS)", 2273: "NAD83 / South Carolina (ft)", 2274: "NAD83 / Tennessee (ftUS)", 2275: "NAD83 / Texas North (ftUS)", 2276: "NAD83 / Texas North Central (ftUS)", 2277: "NAD83 / Texas Central (ftUS)", 2278: "NAD83 / Texas South Central (ftUS)", 2279: "NAD83 / Texas South (ftUS)", 2280: "NAD83 / Utah North (ft)", 2281: "NAD83 / Utah Central (ft)", 2282: "NAD83 / Utah South (ft)", 2283: "NAD83 / Virginia North (ftUS)", 2284: "NAD83 / Virginia South (ftUS)", 2285: "NAD83 / Washington North (ftUS)", 2286: "NAD83 / Washington South (ftUS)", 2287: "NAD83 / Wisconsin North (ftUS)", 2288: "NAD83 / Wisconsin Central (ftUS)", 2289: "NAD83 / Wisconsin South (ftUS)", 2290: "ATS77 / Prince Edward Isl. Stereographic (ATS77)", 2291: "NAD83(CSRS98) / Prince Edward Isl. Stereographic (NAD83)", 2292: "NAD83(CSRS98) / Prince Edward Isl. Stereographic (NAD83)", 2294: "ATS77 / MTM Nova Scotia zone 4", 2295: "ATS77 / MTM Nova Scotia zone 5", 2296: "Ammassalik 1958 / Greenland zone 7 east", 2297: "Qornoq 1927 / Greenland zone 1 east", 2298: "Qornoq 1927 / Greenland zone 2 east", 2299: "Qornoq 1927 / Greenland zone 2 west", 2300: "Qornoq 1927 / Greenland zone 3 east", 2301: "Qornoq 1927 / Greenland zone 3 west", 2302: "Qornoq 1927 / Greenland zone 4 east", 2303: "Qornoq 1927 / Greenland zone 4 west", 2304: "Qornoq 1927 / Greenland zone 5 west", 2305: "Qornoq 1927 / Greenland zone 6 west", 2306: "Qornoq 1927 / Greenland zone 7 west", 2307: "Qornoq 1927 / Greenland zone 8 east", 2308: "Batavia / TM 109 SE", 2309: "WGS 84 / TM 116 SE", 2310: "WGS 84 / TM 132 SE", 2311: "WGS 84 / TM 6 NE", 2312: "Garoua / UTM zone 33N", 2313: "Kousseri / UTM zone 33N", 2314: "Trinidad 1903 / Trinidad Grid (ftCla)", 2315: "Campo Inchauspe / UTM zone 19S", 2316: "Campo Inchauspe / UTM zone 20S", 2317: "PSAD56 / ICN Regional", 2318: "Ain el Abd / Aramco Lambert", 2319: "ED50 / TM27", 2320: "ED50 / TM30", 2321: "ED50 / TM33", 2322: "ED50 / TM36", 2323: "ED50 / TM39", 2324: "ED50 / TM42", 2325: "ED50 / TM45", 2326: "Hong Kong 1980 Grid System", 2327: "Xian 1980 / Gauss-Kruger zone 13", 2328: "Xian 1980 / Gauss-Kruger zone 14", 2329: "Xian 1980 / Gauss-Kruger zone 15", 2330: "Xian 1980 / Gauss-Kruger zone 16", 2331: "Xian 1980 / Gauss-Kruger zone 17", 2332: "Xian 1980 / Gauss-Kruger zone 18", 2333: "Xian 1980 / Gauss-Kruger zone 19", 2334: "Xian 1980 / Gauss-Kruger zone 20", 2335: "Xian 1980 / Gauss-Kruger zone 21", 2336: "Xian 1980 / Gauss-Kruger zone 22", 2337: "Xian 1980 / Gauss-Kruger zone 23", 2338: "Xian 1980 / Gauss-Kruger CM 75E", 2339: "Xian 1980 / Gauss-Kruger CM 81E", 2340: "Xian 1980 / Gauss-Kruger CM 87E", 2341: "Xian 1980 / Gauss-Kruger CM 93E", 2342: "Xian 1980 / Gauss-Kruger CM 99E", 2343: "Xian 1980 / Gauss-Kruger CM 105E", 2344: "Xian 1980 / Gauss-Kruger CM 111E", 2345: "Xian 1980 / Gauss-Kruger CM 117E", 2346: "Xian 1980 / Gauss-Kruger CM 123E", 2347: "Xian 1980 / Gauss-Kruger CM 129E", 2348: "Xian 1980 / Gauss-Kruger CM 135E", 2349: "Xian 1980 / 3-degree Gauss-Kruger zone 25", 2350: "Xian 1980 / 3-degree Gauss-Kruger zone 26", 2351: "Xian 1980 / 3-degree Gauss-Kruger zone 27", 2352: "Xian 1980 / 3-degree Gauss-Kruger zone 28", 2353: "Xian 1980 / 3-degree Gauss-Kruger zone 29", 2354: "Xian 1980 / 3-degree Gauss-Kruger zone 30", 2355: "Xian 1980 / 3-degree Gauss-Kruger zone 31", 2356: "Xian 1980 / 3-degree Gauss-Kruger zone 32", 2357: "Xian 1980 / 3-degree Gauss-Kruger zone 33", 2358: "Xian 1980 / 3-degree Gauss-Kruger zone 34", 2359: "Xian 1980 / 3-degree Gauss-Kruger zone 35", 2360: "Xian 1980 / 3-degree Gauss-Kruger zone 36", 2361: "Xian 1980 / 3-degree Gauss-Kruger zone 37", 2362: "Xian 1980 / 3-degree Gauss-Kruger zone 38", 2363: "Xian 1980 / 3-degree Gauss-Kruger zone 39", 2364: "Xian 1980 / 3-degree Gauss-Kruger zone 40", 2365: "Xian 1980 / 3-degree Gauss-Kruger zone 41", 2366: "Xian 1980 / 3-degree Gauss-Kruger zone 42", 2367: "Xian 1980 / 3-degree Gauss-Kruger zone 43", 2368: "Xian 1980 / 3-degree Gauss-Kruger zone 44", 2369: "Xian 1980 / 3-degree Gauss-Kruger zone 45", 2370: "Xian 1980 / 3-degree Gauss-Kruger CM 75E", 2371: "Xian 1980 / 3-degree Gauss-Kruger CM 78E", 2372: "Xian 1980 / 3-degree Gauss-Kruger CM 81E", 2373: "Xian 1980 / 3-degree Gauss-Kruger CM 84E", 2374: "Xian 1980 / 3-degree Gauss-Kruger CM 87E", 2375: "Xian 1980 / 3-degree Gauss-Kruger CM 90E", 2376: "Xian 1980 / 3-degree Gauss-Kruger CM 93E", 2377: "Xian 1980 / 3-degree Gauss-Kruger CM 96E", 2378: "Xian 1980 / 3-degree Gauss-Kruger CM 99E", 2379: "Xian 1980 / 3-degree Gauss-Kruger CM 102E", 2380: "Xian 1980 / 3-degree Gauss-Kruger CM 105E", 2381: "Xian 1980 / 3-degree Gauss-Kruger CM 108E", 2382: "Xian 1980 / 3-degree Gauss-Kruger CM 111E", 2383: "Xian 1980 / 3-degree Gauss-Kruger CM 114E", 2384: "Xian 1980 / 3-degree Gauss-Kruger CM 117E", 2385: "Xian 1980 / 3-degree Gauss-Kruger CM 120E", 2386: "Xian 1980 / 3-degree Gauss-Kruger CM 123E", 2387: "Xian 1980 / 3-degree Gauss-Kruger CM 126E", 2388: "Xian 1980 / 3-degree Gauss-Kruger CM 129E", 2389: "Xian 1980 / 3-degree Gauss-Kruger CM 132E", 2390: "Xian 1980 / 3-degree Gauss-Kruger CM 135E", 2391: "KKJ / Finland zone 1", 2392: "KKJ / Finland zone 2", 2393: "KKJ / Finland Uniform Coordinate System", 2394: "KKJ / Finland zone 4", 2395: "South Yemen / Gauss-Kruger zone 8", 2396: "South Yemen / Gauss-Kruger zone 9", 2397: "Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 3", 2398: "Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 4", 2399: "Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 5", 2400: "RT90 2.5 gon W", 2401: "Beijing 1954 / 3-degree Gauss-Kruger zone 25", 2402: "Beijing 1954 / 3-degree Gauss-Kruger zone 26", 2403: "Beijing 1954 / 3-degree Gauss-Kruger zone 27", 2404: "Beijing 1954 / 3-degree Gauss-Kruger zone 28", 2405: "Beijing 1954 / 3-degree Gauss-Kruger zone 29", 2406: "Beijing 1954 / 3-degree Gauss-Kruger zone 30", 2407: "Beijing 1954 / 3-degree Gauss-Kruger zone 31", 2408: "Beijing 1954 / 3-degree Gauss-Kruger zone 32", 2409: "Beijing 1954 / 3-degree Gauss-Kruger zone 33", 2410: "Beijing 1954 / 3-degree Gauss-Kruger zone 34", 2411: "Beijing 1954 / 3-degree Gauss-Kruger zone 35", 2412: "Beijing 1954 / 3-degree Gauss-Kruger zone 36", 2413: "Beijing 1954 / 3-degree Gauss-Kruger zone 37", 2414: "Beijing 1954 / 3-degree Gauss-Kruger zone 38", 2415: "Beijing 1954 / 3-degree Gauss-Kruger zone 39", 2416: "Beijing 1954 / 3-degree Gauss-Kruger zone 40", 2417: "Beijing 1954 / 3-degree Gauss-Kruger zone 41", 2418: "Beijing 1954 / 3-degree Gauss-Kruger zone 42", 2419: "Beijing 1954 / 3-degree Gauss-Kruger zone 43", 2420: "Beijing 1954 / 3-degree Gauss-Kruger zone 44", 2421: "Beijing 1954 / 3-degree Gauss-Kruger zone 45", 2422: "Beijing 1954 / 3-degree Gauss-Kruger CM 75E", 2423: "Beijing 1954 / 3-degree Gauss-Kruger CM 78E", 2424: "Beijing 1954 / 3-degree Gauss-Kruger CM 81E", 2425: "Beijing 1954 / 3-degree Gauss-Kruger CM 84E", 2426: "Beijing 1954 / 3-degree Gauss-Kruger CM 87E", 2427: "Beijing 1954 / 3-degree Gauss-Kruger CM 90E", 2428: "Beijing 1954 / 3-degree Gauss-Kruger CM 93E", 2429: "Beijing 1954 / 3-degree Gauss-Kruger CM 96E", 2430: "Beijing 1954 / 3-degree Gauss-Kruger CM 99E", 2431: "Beijing 1954 / 3-degree Gauss-Kruger CM 102E", 2432: "Beijing 1954 / 3-degree Gauss-Kruger CM 105E", 2433: "Beijing 1954 / 3-degree Gauss-Kruger CM 108E", 2434: "Beijing 1954 / 3-degree Gauss-Kruger CM 111E", 2435: "Beijing 1954 / 3-degree Gauss-Kruger CM 114E", 2436: "Beijing 1954 / 3-degree Gauss-Kruger CM 117E", 2437: "Beijing 1954 / 3-degree Gauss-Kruger CM 120E", 2438: "Beijing 1954 / 3-degree Gauss-Kruger CM 123E", 2439: "Beijing 1954 / 3-degree Gauss-Kruger CM 126E", 2440: "Beijing 1954 / 3-degree Gauss-Kruger CM 129E", 2441: "Beijing 1954 / 3-degree Gauss-Kruger CM 132E", 2442: "Beijing 1954 / 3-degree Gauss-Kruger CM 135E", 2443: "JGD2000 / Japan Plane Rectangular CS I", 2444: "JGD2000 / Japan Plane Rectangular CS II", 2445: "JGD2000 / Japan Plane Rectangular CS III", 2446: "JGD2000 / Japan Plane Rectangular CS IV", 2447: "JGD2000 / Japan Plane Rectangular CS V", 2448: "JGD2000 / Japan Plane Rectangular CS VI", 2449: "JGD2000 / Japan Plane Rectangular CS VII", 2450: "JGD2000 / Japan Plane Rectangular CS VIII", 2451: "JGD2000 / Japan Plane Rectangular CS IX", 2452: "JGD2000 / Japan Plane Rectangular CS X", 2453: "JGD2000 / Japan Plane Rectangular CS XI", 2454: "JGD2000 / Japan Plane Rectangular CS XII", 2455: "JGD2000 / Japan Plane Rectangular CS XIII", 2456: "JGD2000 / Japan Plane Rectangular CS XIV", 2457: "JGD2000 / Japan Plane Rectangular CS XV", 2458: "JGD2000 / Japan Plane Rectangular CS XVI", 2459: "JGD2000 / Japan Plane Rectangular CS XVII", 2460: "JGD2000 / Japan Plane Rectangular CS XVIII", 2461: "JGD2000 / Japan Plane Rectangular CS XIX", 2462: "Albanian 1987 / Gauss-Kruger zone 4", 2463: "Pulkovo 1995 / Gauss-Kruger CM 21E", 2464: "Pulkovo 1995 / Gauss-Kruger CM 27E", 2465: "Pulkovo 1995 / Gauss-Kruger CM 33E", 2466: "Pulkovo 1995 / Gauss-Kruger CM 39E", 2467: "Pulkovo 1995 / Gauss-Kruger CM 45E", 2468: "Pulkovo 1995 / Gauss-Kruger CM 51E", 2469: "Pulkovo 1995 / Gauss-Kruger CM 57E", 2470: "Pulkovo 1995 / Gauss-Kruger CM 63E", 2471: "Pulkovo 1995 / Gauss-Kruger CM 69E", 2472: "Pulkovo 1995 / Gauss-Kruger CM 75E", 2473: "Pulkovo 1995 / Gauss-Kruger CM 81E", 2474: "Pulkovo 1995 / Gauss-Kruger CM 87E", 2475: "Pulkovo 1995 / Gauss-Kruger CM 93E", 2476: "Pulkovo 1995 / Gauss-Kruger CM 99E", 2477: "Pulkovo 1995 / Gauss-Kruger CM 105E", 2478: "Pulkovo 1995 / Gauss-Kruger CM 111E", 2479: "Pulkovo 1995 / Gauss-Kruger CM 117E", 2480: "Pulkovo 1995 / Gauss-Kruger CM 123E", 2481: "Pulkovo 1995 / Gauss-Kruger CM 129E", 2482: "Pulkovo 1995 / Gauss-Kruger CM 135E", 2483: "Pulkovo 1995 / Gauss-Kruger CM 141E", 2484: "Pulkovo 1995 / Gauss-Kruger CM 147E", 2485: "Pulkovo 1995 / Gauss-Kruger CM 153E", 2486: "Pulkovo 1995 / Gauss-Kruger CM 159E", 2487: "Pulkovo 1995 / Gauss-Kruger CM 165E", 2488: "Pulkovo 1995 / Gauss-Kruger CM 171E", 2489: "Pulkovo 1995 / Gauss-Kruger CM 177E", 2490: "Pulkovo 1995 / Gauss-Kruger CM 177W", 2491: "Pulkovo 1995 / Gauss-Kruger CM 171W", 2492: "Pulkovo 1942 / Gauss-Kruger CM 9E", 2493: "Pulkovo 1942 / Gauss-Kruger CM 15E", 2494: "Pulkovo 1942 / Gauss-Kruger CM 21E", 2495: "Pulkovo 1942 / Gauss-Kruger CM 27E", 2496: "Pulkovo 1942 / Gauss-Kruger CM 33E", 2497: "Pulkovo 1942 / Gauss-Kruger CM 39E", 2498: "Pulkovo 1942 / Gauss-Kruger CM 45E", 2499: "Pulkovo 1942 / Gauss-Kruger CM 51E", 2500: "Pulkovo 1942 / Gauss-Kruger CM 57E", 2501: "Pulkovo 1942 / Gauss-Kruger CM 63E", 2502: "Pulkovo 1942 / Gauss-Kruger CM 69E", 2503: "Pulkovo 1942 / Gauss-Kruger CM 75E", 2504: "Pulkovo 1942 / Gauss-Kruger CM 81E", 2505: "Pulkovo 1942 / Gauss-Kruger CM 87E", 2506: "Pulkovo 1942 / Gauss-Kruger CM 93E", 2507: "Pulkovo 1942 / Gauss-Kruger CM 99E", 2508: "Pulkovo 1942 / Gauss-Kruger CM 105E", 2509: "Pulkovo 1942 / Gauss-Kruger CM 111E", 2510: "Pulkovo 1942 / Gauss-Kruger CM 117E", 2511: "Pulkovo 1942 / Gauss-Kruger CM 123E", 2512: "Pulkovo 1942 / Gauss-Kruger CM 129E", 2513: "Pulkovo 1942 / Gauss-Kruger CM 135E", 2514: "Pulkovo 1942 / Gauss-Kruger CM 141E", 2515: "Pulkovo 1942 / Gauss-Kruger CM 147E", 2516: "Pulkovo 1942 / Gauss-Kruger CM 153E", 2517: "Pulkovo 1942 / Gauss-Kruger CM 159E", 2518: "Pulkovo 1942 / Gauss-Kruger CM 165E", 2519: "Pulkovo 1942 / Gauss-Kruger CM 171E", 2520: "Pulkovo 1942 / Gauss-Kruger CM 177E", 2521: "Pulkovo 1942 / Gauss-Kruger CM 177W", 2522: "Pulkovo 1942 / Gauss-Kruger CM 171W", 2523: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 7", 2524: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 8", 2525: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 9", 2526: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 10", 2527: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 11", 2528: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 12", 2529: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 13", 2530: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 14", 2531: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 15", 2532: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 16", 2533: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 17", 2534: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 18", 2535: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 19", 2536: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 20", 2537: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 21", 2538: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 22", 2539: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 23", 2540: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 24", 2541: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 25", 2542: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 26", 2543: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 27", 2544: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 28", 2545: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 29", 2546: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 30", 2547: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 31", 2548: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 32", 2549: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 33", 2550: "Samboja / UTM zone 50S", 2551: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 34", 2552: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 35", 2553: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 36", 2554: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 37", 2555: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 38", 2556: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 39", 2557: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 40", 2558: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 41", 2559: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 42", 2560: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 43", 2561: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 44", 2562: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 45", 2563: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 46", 2564: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 47", 2565: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 48", 2566: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 49", 2567: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 50", 2568: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 51", 2569: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 52", 2570: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 53", 2571: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 54", 2572: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 55", 2573: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 56", 2574: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 57", 2575: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 58", 2576: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 59", 2577: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 60", 2578: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 61", 2579: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 62", 2580: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 63", 2581: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 64", 2582: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 21E", 2583: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 24E", 2584: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 27E", 2585: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 30E", 2586: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 33E", 2587: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 36E", 2588: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 39E", 2589: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 42E", 2590: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 45E", 2591: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 48E", 2592: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 51E", 2593: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 54E", 2594: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 57E", 2595: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 60E", 2596: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 63E", 2597: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 66E", 2598: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 69E", 2599: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 72E", 2600: "Lietuvos Koordinoei Sistema 1994", 2601: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 75E", 2602: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 78E", 2603: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 81E", 2604: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 84E", 2605: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 87E", 2606: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 90E", 2607: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 93E", 2608: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 96E", 2609: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 99E", 2610: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 102E", 2611: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 105E", 2612: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 108E", 2613: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 111E", 2614: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 114E", 2615: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 117E", 2616: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 120E", 2617: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 123E", 2618: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 126E", 2619: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 129E", 2620: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 132E", 2621: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 135E", 2622: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 138E", 2623: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 141E", 2624: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 144E", 2625: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 147E", 2626: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 150E", 2627: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 153E", 2628: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 156E", 2629: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 159E", 2630: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 162E", 2631: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 165E", 2632: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 168E", 2633: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 171E", 2634: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 174E", 2635: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 177E", 2636: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 180E", 2637: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 177W", 2638: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 174W", 2639: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 171W", 2640: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 168W", 2641: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 7", 2642: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 8", 2643: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 9", 2644: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 10", 2645: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 11", 2646: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 12", 2647: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 13", 2648: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 14", 2649: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 15", 2650: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 16", 2651: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 17", 2652: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 18", 2653: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 19", 2654: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 20", 2655: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 21", 2656: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 22", 2657: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 23", 2658: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 24", 2659: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 25", 2660: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 26", 2661: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 27", 2662: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 28", 2663: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 29", 2664: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 30", 2665: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 31", 2666: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 32", 2667: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 33", 2668: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 34", 2669: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 35", 2670: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 36", 2671: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 37", 2672: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 38", 2673: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 39", 2674: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 40", 2675: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 41", 2676: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 42", 2677: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 43", 2678: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 44", 2679: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 45", 2680: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 46", 2681: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 47", 2682: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 48", 2683: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 49", 2684: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 50", 2685: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 51", 2686: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 52", 2687: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 53", 2688: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 54", 2689: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 55", 2690: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 56", 2691: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 57", 2692: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 58", 2693: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 59", 2694: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 60", 2695: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 61", 2696: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 62", 2697: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 63", 2698: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 64", 2699: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 21E", 2700: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 24E", 2701: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 27E", 2702: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 30E", 2703: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 33E", 2704: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 36E", 2705: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 39E", 2706: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 42E", 2707: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 45E", 2708: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 48E", 2709: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 51E", 2710: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 54E", 2711: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 57E", 2712: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 60E", 2713: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 63E", 2714: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 66E", 2715: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 69E", 2716: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 72E", 2717: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 75E", 2718: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 78E", 2719: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 81E", 2720: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 84E", 2721: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 87E", 2722: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 90E", 2723: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 93E", 2724: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 96E", 2725: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 99E", 2726: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 102E", 2727: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 105E", 2728: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 108E", 2729: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 111E", 2730: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 114E", 2731: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 117E", 2732: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 120E", 2733: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 123E", 2734: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 126E", 2735: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 129E", 2736: "Tete / UTM zone 36S", 2737: "Tete / UTM zone 37S", 2738: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 132E", 2739: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 135E", 2740: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 138E", 2741: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 141E", 2742: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 144E", 2743: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 147E", 2744: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 150E", 2745: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 153E", 2746: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 156E", 2747: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 159E", 2748: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 162E", 2749: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 165E", 2750: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 168E", 2751: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 171E", 2752: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 174E", 2753: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 177E", 2754: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 180E", 2755: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 177W", 2756: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 174W", 2757: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 171W", 2758: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 168W", 2759: "NAD83(HARN) / Alabama East", 2760: "NAD83(HARN) / Alabama West", 2761: "NAD83(HARN) / Arizona East", 2762: "NAD83(HARN) / Arizona Central", 2763: "NAD83(HARN) / Arizona West", 2764: "NAD83(HARN) / Arkansas North", 2765: "NAD83(HARN) / Arkansas South", 2766: "NAD83(HARN) / California zone 1", 2767: "NAD83(HARN) / California zone 2", 2768: "NAD83(HARN) / California zone 3", 2769: "NAD83(HARN) / California zone 4", 2770: "NAD83(HARN) / California zone 5", 2771: "NAD83(HARN) / California zone 6", 2772: "NAD83(HARN) / Colorado North", 2773: "NAD83(HARN) / Colorado Central", 2774: "NAD83(HARN) / Colorado South", 2775: "NAD83(HARN) / Connecticut", 2776: "NAD83(HARN) / Delaware", 2777: "NAD83(HARN) / Florida East", 2778: "NAD83(HARN) / Florida West", 2779: "NAD83(HARN) / Florida North", 2780: "NAD83(HARN) / Georgia East", 2781: "NAD83(HARN) / Georgia West", 2782: "NAD83(HARN) / Hawaii zone 1", 2783: "NAD83(HARN) / Hawaii zone 2", 2784: "NAD83(HARN) / Hawaii zone 3", 2785: "NAD83(HARN) / Hawaii zone 4", 2786: "NAD83(HARN) / Hawaii zone 5", 2787: "NAD83(HARN) / Idaho East", 2788: "NAD83(HARN) / Idaho Central", 2789: "NAD83(HARN) / Idaho West", 2790: "NAD83(HARN) / Illinois East", 2791: "NAD83(HARN) / Illinois West", 2792: "NAD83(HARN) / Indiana East", 2793: "NAD83(HARN) / Indiana West", 2794: "NAD83(HARN) / Iowa North", 2795: "NAD83(HARN) / Iowa South", 2796: "NAD83(HARN) / Kansas North", 2797: "NAD83(HARN) / Kansas South", 2798: "NAD83(HARN) / Kentucky North", 2799: "NAD83(HARN) / Kentucky South", 2800: "NAD83(HARN) / Louisiana North", 2801: "NAD83(HARN) / Louisiana South", 2802: "NAD83(HARN) / Maine East", 2803: "NAD83(HARN) / Maine West", 2804: "NAD83(HARN) / Maryland", 2805: "NAD83(HARN) / Massachusetts Mainland", 2806: "NAD83(HARN) / Massachusetts Island", 2807: "NAD83(HARN) / Michigan North", 2808: "NAD83(HARN) / Michigan Central", 2809: "NAD83(HARN) / Michigan South", 2810: "NAD83(HARN) / Minnesota North", 2811: "NAD83(HARN) / Minnesota Central", 2812: "NAD83(HARN) / Minnesota South", 2813: "NAD83(HARN) / Mississippi East", 2814: "NAD83(HARN) / Mississippi West", 2815: "NAD83(HARN) / Missouri East", 2816: "NAD83(HARN) / Missouri Central", 2817: "NAD83(HARN) / Missouri West", 2818: "NAD83(HARN) / Montana", 2819: "NAD83(HARN) / Nebraska", 2820: "NAD83(HARN) / Nevada East", 2821: "NAD83(HARN) / Nevada Central", 2822: "NAD83(HARN) / Nevada West", 2823: "NAD83(HARN) / New Hampshire", 2824: "NAD83(HARN) / New Jersey", 2825: "NAD83(HARN) / New Mexico East", 2826: "NAD83(HARN) / New Mexico Central", 2827: "NAD83(HARN) / New Mexico West", 2828: "NAD83(HARN) / New York East", 2829: "NAD83(HARN) / New York Central", 2830: "NAD83(HARN) / New York West", 2831: "NAD83(HARN) / New York Long Island", 2832: "NAD83(HARN) / North Dakota North", 2833: "NAD83(HARN) / North Dakota South", 2834: "NAD83(HARN) / Ohio North", 2835: "NAD83(HARN) / Ohio South", 2836: "NAD83(HARN) / Oklahoma North", 2837: "NAD83(HARN) / Oklahoma South", 2838: "NAD83(HARN) / Oregon North", 2839: "NAD83(HARN) / Oregon South", 2840: "NAD83(HARN) / Rhode Island", 2841: "NAD83(HARN) / South Dakota North", 2842: "NAD83(HARN) / South Dakota South", 2843: "NAD83(HARN) / Tennessee", 2844: "NAD83(HARN) / Texas North", 2845: "NAD83(HARN) / Texas North Central", 2846: "NAD83(HARN) / Texas Central", 2847: "NAD83(HARN) / Texas South Central", 2848: "NAD83(HARN) / Texas South", 2849: "NAD83(HARN) / Utah North", 2850: "NAD83(HARN) / Utah Central", 2851: "NAD83(HARN) / Utah South", 2852: "NAD83(HARN) / Vermont", 2853: "NAD83(HARN) / Virginia North", 2854: "NAD83(HARN) / Virginia South", 2855: "NAD83(HARN) / Washington North", 2856: "NAD83(HARN) / Washington South", 2857: "NAD83(HARN) / West Virginia North", 2858: "NAD83(HARN) / West Virginia South", 2859: "NAD83(HARN) / Wisconsin North", 2860: "NAD83(HARN) / Wisconsin Central", 2861: "NAD83(HARN) / Wisconsin South", 2862: "NAD83(HARN) / Wyoming East", 2863: "NAD83(HARN) / Wyoming East Central", 2864: "NAD83(HARN) / Wyoming West Central", 2865: "NAD83(HARN) / Wyoming West", 2866: "NAD83(HARN) / Puerto Rico and Virgin Is.", 2867: "NAD83(HARN) / Arizona East (ft)", 2868: "NAD83(HARN) / Arizona Central (ft)", 2869: "NAD83(HARN) / Arizona West (ft)", 2870: "NAD83(HARN) / California zone 1 (ftUS)", 2871: "NAD83(HARN) / California zone 2 (ftUS)", 2872: "NAD83(HARN) / California zone 3 (ftUS)", 2873: "NAD83(HARN) / California zone 4 (ftUS)", 2874: "NAD83(HARN) / California zone 5 (ftUS)", 2875: "NAD83(HARN) / California zone 6 (ftUS)", 2876: "NAD83(HARN) / Colorado North (ftUS)", 2877: "NAD83(HARN) / Colorado Central (ftUS)", 2878: "NAD83(HARN) / Colorado South (ftUS)", 2879: "NAD83(HARN) / Connecticut (ftUS)", 2880: "NAD83(HARN) / Delaware (ftUS)", 2881: "NAD83(HARN) / Florida East (ftUS)", 2882: "NAD83(HARN) / Florida West (ftUS)", 2883: "NAD83(HARN) / Florida North (ftUS)", 2884: "NAD83(HARN) / Georgia East (ftUS)", 2885: "NAD83(HARN) / Georgia West (ftUS)", 2886: "NAD83(HARN) / Idaho East (ftUS)", 2887: "NAD83(HARN) / Idaho Central (ftUS)", 2888: "NAD83(HARN) / Idaho West (ftUS)", 2889: "NAD83(HARN) / Indiana East (ftUS)", 2890: "NAD83(HARN) / Indiana West (ftUS)", 2891: "NAD83(HARN) / Kentucky North (ftUS)", 2892: "NAD83(HARN) / Kentucky South (ftUS)", 2893: "NAD83(HARN) / Maryland (ftUS)", 2894: "NAD83(HARN) / Massachusetts Mainland (ftUS)", 2895: "NAD83(HARN) / Massachusetts Island (ftUS)", 2896: "NAD83(HARN) / Michigan North (ft)", 2897: "NAD83(HARN) / Michigan Central (ft)", 2898: "NAD83(HARN) / Michigan South (ft)", 2899: "NAD83(HARN) / Mississippi East (ftUS)", 2900: "NAD83(HARN) / Mississippi West (ftUS)", 2901: "NAD83(HARN) / Montana (ft)", 2902: "NAD83(HARN) / New Mexico East (ftUS)", 2903: "NAD83(HARN) / New Mexico Central (ftUS)", 2904: "NAD83(HARN) / New Mexico West (ftUS)", 2905: "NAD83(HARN) / New York East (ftUS)", 2906: "NAD83(HARN) / New York Central (ftUS)", 2907: "NAD83(HARN) / New York West (ftUS)", 2908: "NAD83(HARN) / New York Long Island (ftUS)", 2909: "NAD83(HARN) / North Dakota North (ft)", 2910: "NAD83(HARN) / North Dakota South (ft)", 2911: "NAD83(HARN) / Oklahoma North (ftUS)", 2912: "NAD83(HARN) / Oklahoma South (ftUS)", 2913: "NAD83(HARN) / Oregon North (ft)", 2914: "NAD83(HARN) / Oregon South (ft)", 2915: "NAD83(HARN) / Tennessee (ftUS)", 2916: "NAD83(HARN) / Texas North (ftUS)", 2917: "NAD83(HARN) / Texas North Central (ftUS)", 2918: "NAD83(HARN) / Texas Central (ftUS)", 2919: "NAD83(HARN) / Texas South Central (ftUS)", 2920: "NAD83(HARN) / Texas South (ftUS)", 2921: "NAD83(HARN) / Utah North (ft)", 2922: "NAD83(HARN) / Utah Central (ft)", 2923: "NAD83(HARN) / Utah South (ft)", 2924: "NAD83(HARN) / Virginia North (ftUS)", 2925: "NAD83(HARN) / Virginia South (ftUS)", 2926: "NAD83(HARN) / Washington North (ftUS)", 2927: "NAD83(HARN) / Washington South (ftUS)", 2928: "NAD83(HARN) / Wisconsin North (ftUS)", 2929: "NAD83(HARN) / Wisconsin Central (ftUS)", 2930: "NAD83(HARN) / Wisconsin South (ftUS)", 2931: "Beduaram / TM 13 NE", 2932: "QND95 / Qatar National Grid", 2933: "Segara / UTM zone 50S", 2934: "Segara (Jakarta) / NEIEZ", 2935: "Pulkovo 1942 / CS63 zone A1", 2936: "Pulkovo 1942 / CS63 zone A2", 2937: "Pulkovo 1942 / CS63 zone A3", 2938: "Pulkovo 1942 / CS63 zone A4", 2939: "Pulkovo 1942 / CS63 zone K2", 2940: "Pulkovo 1942 / CS63 zone K3", 2941: "Pulkovo 1942 / CS63 zone K4", 2942: "Porto Santo / UTM zone 28N", 2943: "Selvagem Grande / UTM zone 28N", 2944: "NAD83(CSRS) / SCoPQ zone 2", 2945: "NAD83(CSRS) / MTM zone 3", 2946: "NAD83(CSRS) / MTM zone 4", 2947: "NAD83(CSRS) / MTM zone 5", 2948: "NAD83(CSRS) / MTM zone 6", 2949: "NAD83(CSRS) / MTM zone 7", 2950: "NAD83(CSRS) / MTM zone 8", 2951: "NAD83(CSRS) / MTM zone 9", 2952: "NAD83(CSRS) / MTM zone 10", 2953: "NAD83(CSRS) / New Brunswick Stereographic", 2954: "NAD83(CSRS) / Prince Edward Isl. Stereographic (NAD83)", 2955: "NAD83(CSRS) / UTM zone 11N", 2956: "NAD83(CSRS) / UTM zone 12N", 2957: "NAD83(CSRS) / UTM zone 13N", 2958: "NAD83(CSRS) / UTM zone 17N", 2959: "NAD83(CSRS) / UTM zone 18N", 2960: "NAD83(CSRS) / UTM zone 19N", 2961: "NAD83(CSRS) / UTM zone 20N", 2962: "NAD83(CSRS) / UTM zone 21N", 2963: "Lisbon 1890 (Lisbon) / Portugal Bonne", 2964: "NAD27 / Alaska Albers", 2965: "NAD83 / Indiana East (ftUS)", 2966: "NAD83 / Indiana West (ftUS)", 2967: "NAD83(HARN) / Indiana East (ftUS)", 2968: "NAD83(HARN) / Indiana West (ftUS)", 2969: "Fort Marigot / UTM zone 20N", 2970: "Guadeloupe 1948 / UTM zone 20N", 2971: "CSG67 / UTM zone 22N", 2972: "RGFG95 / UTM zone 22N", 2973: "Martinique 1938 / UTM zone 20N", 2975: "RGR92 / UTM zone 40S", 2976: "Tahiti 52 / UTM zone 6S", 2977: "Tahaa 54 / UTM zone 5S", 2978: "IGN72 Nuku Hiva / UTM zone 7S", 2979: "K0 1949 / UTM zone 42S", 2980: "Combani 1950 / UTM zone 38S", 2981: "IGN56 Lifou / UTM zone 58S", 2982: "IGN72 Grand Terre / UTM zone 58S", 2983: "ST87 Ouvea / UTM zone 58S", 2984: "RGNC 1991 / Lambert New Caledonia", 2985: "Petrels 1972 / Terre Adelie Polar Stereographic", 2986: "Perroud 1950 / Terre Adelie Polar Stereographic", 2987: "Saint Pierre et Miquelon 1950 / UTM zone 21N", 2988: "MOP78 / UTM zone 1S", 2989: "RRAF 1991 / UTM zone 20N", 2990: "Reunion 1947 / TM Reunion", 2991: "NAD83 / Oregon LCC (m)", 2992: "NAD83 / Oregon GIC Lambert (ft)", 2993: "NAD83(HARN) / Oregon LCC (m)", 2994: "NAD83(HARN) / Oregon GIC Lambert (ft)", 2995: "IGN53 Mare / UTM zone 58S", 2996: "ST84 Ile des Pins / UTM zone 58S", 2997: "ST71 Belep / UTM zone 58S", 2998: "NEA74 Noumea / UTM zone 58S", 2999: "Grand Comoros / UTM zone 38S", 3000: "Segara / NEIEZ", 3001: "Batavia / NEIEZ", 3002: "Makassar / NEIEZ", 3003: "Monte Mario / Italy zone 1", 3004: "Monte Mario / Italy zone 2", 3005: "NAD83 / BC Albers", 3006: "SWEREF99 TM", 3007: "SWEREF99 12 00", 3008: "SWEREF99 13 30", 3009: "SWEREF99 15 00", 3010: "SWEREF99 16 30", 3011: "SWEREF99 18 00", 3012: "SWEREF99 14 15", 3013: "SWEREF99 15 45", 3014: "SWEREF99 17 15", 3015: "SWEREF99 18 45", 3016: "SWEREF99 20 15", 3017: "SWEREF99 21 45", 3018: "SWEREF99 23 15", 3019: "RT90 7.5 gon V", 3020: "RT90 5 gon V", 3021: "RT90 2.5 gon V", 3022: "RT90 0 gon", 3023: "RT90 2.5 gon O", 3024: "RT90 5 gon O", 3025: "RT38 7.5 gon V", 3026: "RT38 5 gon V", 3027: "RT38 2.5 gon V", 3028: "RT38 0 gon", 3029: "RT38 2.5 gon O", 3030: "RT38 5 gon O", 3031: "WGS 84 / Antarctic Polar Stereographic", 3032: "WGS 84 / Australian Antarctic Polar Stereographic", 3033: "WGS 84 / Australian Antarctic Lambert", 3034: "ETRS89 / LCC Europe", 3035: "ETRS89 / LAEA Europe", 3036: "Moznet / UTM zone 36S", 3037: "Moznet / UTM zone 37S", 3038: "ETRS89 / TM26", 3039: "ETRS89 / TM27", 3040: "ETRS89 / UTM zone 28N (N-E)", 3041: "ETRS89 / UTM zone 29N (N-E)", 3042: "ETRS89 / UTM zone 30N (N-E)", 3043: "ETRS89 / UTM zone 31N (N-E)", 3044: "ETRS89 / UTM zone 32N (N-E)", 3045: "ETRS89 / UTM zone 33N (N-E)", 3046: "ETRS89 / UTM zone 34N (N-E)", 3047: "ETRS89 / UTM zone 35N (N-E)", 3048: "ETRS89 / UTM zone 36N (N-E)", 3049: "ETRS89 / UTM zone 37N (N-E)", 3050: "ETRS89 / TM38", 3051: "ETRS89 / TM39", 3052: "Reykjavik 1900 / Lambert 1900", 3053: "Hjorsey 1955 / Lambert 1955", 3054: "Hjorsey 1955 / UTM zone 26N", 3055: "Hjorsey 1955 / UTM zone 27N", 3056: "Hjorsey 1955 / UTM zone 28N", 3057: "ISN93 / Lambert 1993", 3058: "Helle 1954 / Jan Mayen Grid", 3059: "LKS92 / Latvia TM", 3060: "IGN72 Grande Terre / UTM zone 58S", 3061: "Porto Santo 1995 / UTM zone 28N", 3062: "Azores Oriental 1995 / UTM zone 26N", 3063: "Azores Central 1995 / UTM zone 26N", 3064: "IGM95 / UTM zone 32N", 3065: "IGM95 / UTM zone 33N", 3066: "ED50 / Jordan TM", 3067: "ETRS89 / TM35FIN(E,N)", 3068: "DHDN / Soldner Berlin", 3069: "NAD27 / Wisconsin Transverse Mercator", 3070: "NAD83 / Wisconsin Transverse Mercator", 3071: "NAD83(HARN) / Wisconsin Transverse Mercator", 3072: "NAD83 / Maine CS2000 East", 3073: "NAD83 / Maine CS2000 Central", 3074: "NAD83 / Maine CS2000 West", 3075: "NAD83(HARN) / Maine CS2000 East", 3076: "NAD83(HARN) / Maine CS2000 Central", 3077: "NAD83(HARN) / Maine CS2000 West", 3078: "NAD83 / Michigan Oblique Mercator", 3079: "NAD83(HARN) / Michigan Oblique Mercator", 3080: "NAD27 / Shackleford", 3081: "NAD83 / Texas State Mapping System", 3082: "NAD83 / Texas Centric Lambert Conformal", 3083: "NAD83 / Texas Centric Albers Equal Area", 3084: "NAD83(HARN) / Texas Centric Lambert Conformal", 3085: "NAD83(HARN) / Texas Centric Albers Equal Area", 3086: "NAD83 / Florida GDL Albers", 3087: "NAD83(HARN) / Florida GDL Albers", 3088: "NAD83 / Kentucky Single Zone", 3089: "NAD83 / Kentucky Single Zone (ftUS)", 3090: "NAD83(HARN) / Kentucky Single Zone", 3091: "NAD83(HARN) / Kentucky Single Zone (ftUS)", 3092: "Tokyo / UTM zone 51N", 3093: "Tokyo / UTM zone 52N", 3094: "Tokyo / UTM zone 53N", 3095: "Tokyo / UTM zone 54N", 3096: "Tokyo / UTM zone 55N", 3097: "JGD2000 / UTM zone 51N", 3098: "JGD2000 / UTM zone 52N", 3099: "JGD2000 / UTM zone 53N", 3100: "JGD2000 / UTM zone 54N", 3101: "JGD2000 / UTM zone 55N", 3102: "American Samoa 1962 / American Samoa Lambert", 3103: "Mauritania 1999 / UTM zone 28N", 3104: "Mauritania 1999 / UTM zone 29N", 3105: "Mauritania 1999 / UTM zone 30N", 3106: "Gulshan 303 / Bangladesh Transverse Mercator", 3107: "GDA94 / SA Lambert", 3108: "ETRS89 / Guernsey Grid", 3109: "ETRS89 / Jersey Transverse Mercator", 3110: "AGD66 / Vicgrid66", 3111: "GDA94 / Vicgrid94", 3112: "GDA94 / Geoscience Australia Lambert", 3113: "GDA94 / BCSG02", 3114: "MAGNA-SIRGAS / Colombia Far West zone", 3115: "MAGNA-SIRGAS / Colombia West zone", 3116: "MAGNA-SIRGAS / Colombia Bogota zone", 3117: "MAGNA-SIRGAS / Colombia East Central zone", 3118: "MAGNA-SIRGAS / Colombia East zone", 3119: "Douala 1948 / AEF west", 3120: "Pulkovo 1942(58) / Poland zone I", 3121: "PRS92 / Philippines zone 1", 3122: "PRS92 / Philippines zone 2", 3123: "PRS92 / Philippines zone 3", 3124: "PRS92 / Philippines zone 4", 3125: "PRS92 / Philippines zone 5", 3126: "ETRS89 / ETRS-GK19FIN", 3127: "ETRS89 / ETRS-GK20FIN", 3128: "ETRS89 / ETRS-GK21FIN", 3129: "ETRS89 / ETRS-GK22FIN", 3130: "ETRS89 / ETRS-GK23FIN", 3131: "ETRS89 / ETRS-GK24FIN", 3132: "ETRS89 / ETRS-GK25FIN", 3133: "ETRS89 / ETRS-GK26FIN", 3134: "ETRS89 / ETRS-GK27FIN", 3135: "ETRS89 / ETRS-GK28FIN", 3136: "ETRS89 / ETRS-GK29FIN", 3137: "ETRS89 / ETRS-GK30FIN", 3138: "ETRS89 / ETRS-GK31FIN", 3139: "Vanua Levu 1915 / Vanua Levu Grid", 3140: "Viti Levu 1912 / Viti Levu Grid", 3141: "Fiji 1956 / UTM zone 60S", 3142: "Fiji 1956 / UTM zone 1S", 3143: "Fiji 1986 / Fiji Map Grid", 3144: "FD54 / Faroe Lambert", 3145: "ETRS89 / Faroe Lambert", 3146: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 6", 3147: "Pulkovo 1942 / 3-degree Gauss-Kruger CM 18E", 3148: "Indian 1960 / UTM zone 48N", 3149: "Indian 1960 / UTM zone 49N", 3150: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 6", 3151: "Pulkovo 1995 / 3-degree Gauss-Kruger CM 18E", 3152: "ST74", 3153: "NAD83(CSRS) / BC Albers", 3154: "NAD83(CSRS) / UTM zone 7N", 3155: "NAD83(CSRS) / UTM zone 8N", 3156: "NAD83(CSRS) / UTM zone 9N", 3157: "NAD83(CSRS) / UTM zone 10N", 3158: "NAD83(CSRS) / UTM zone 14N", 3159: "NAD83(CSRS) / UTM zone 15N", 3160: "NAD83(CSRS) / UTM zone 16N", 3161: "NAD83 / Ontario MNR Lambert", 3162: "NAD83(CSRS) / Ontario MNR Lambert", 3163: "RGNC91-93 / Lambert New Caledonia", 3164: "ST87 Ouvea / UTM zone 58S", 3165: "NEA74 Noumea / Noumea Lambert", 3166: "NEA74 Noumea / Noumea Lambert 2", 3167: "Kertau (RSO) / RSO Malaya (ch)", 3168: "Kertau (RSO) / RSO Malaya (m)", 3169: "RGNC91-93 / UTM zone 57S", 3170: "RGNC91-93 / UTM zone 58S", 3171: "RGNC91-93 / UTM zone 59S", 3172: "IGN53 Mare / UTM zone 59S", 3173: "fk89 / Faroe Lambert FK89", 3174: "NAD83 / Great Lakes Albers", 3175: "NAD83 / Great Lakes and St Lawrence Albers", 3176: "Indian 1960 / TM 106 NE", 3177: "LGD2006 / Libya TM", 3178: "GR96 / UTM zone 18N", 3179: "GR96 / UTM zone 19N", 3180: "GR96 / UTM zone 20N", 3181: "GR96 / UTM zone 21N", 3182: "GR96 / UTM zone 22N", 3183: "GR96 / UTM zone 23N", 3184: "GR96 / UTM zone 24N", 3185: "GR96 / UTM zone 25N", 3186: "GR96 / UTM zone 26N", 3187: "GR96 / UTM zone 27N", 3188: "GR96 / UTM zone 28N", 3189: "GR96 / UTM zone 29N", 3190: "LGD2006 / Libya TM zone 5", 3191: "LGD2006 / Libya TM zone 6", 3192: "LGD2006 / Libya TM zone 7", 3193: "LGD2006 / Libya TM zone 8", 3194: "LGD2006 / Libya TM zone 9", 3195: "LGD2006 / Libya TM zone 10", 3196: "LGD2006 / Libya TM zone 11", 3197: "LGD2006 / Libya TM zone 12", 3198: "LGD2006 / Libya TM zone 13", 3199: "LGD2006 / UTM zone 32N", 3200: "FD58 / Iraq zone", 3201: "LGD2006 / UTM zone 33N", 3202: "LGD2006 / UTM zone 34N", 3203: "LGD2006 / UTM zone 35N", 3204: "WGS 84 / SCAR IMW SP19-20", 3205: "WGS 84 / SCAR IMW SP21-22", 3206: "WGS 84 / SCAR IMW SP23-24", 3207: "WGS 84 / SCAR IMW SQ01-02", 3208: "WGS 84 / SCAR IMW SQ19-20", 3209: "WGS 84 / SCAR IMW SQ21-22", 3210: "WGS 84 / SCAR IMW SQ37-38", 3211: "WGS 84 / SCAR IMW SQ39-40", 3212: "WGS 84 / SCAR IMW SQ41-42", 3213: "WGS 84 / SCAR IMW SQ43-44", 3214: "WGS 84 / SCAR IMW SQ45-46", 3215: "WGS 84 / SCAR IMW SQ47-48", 3216: "WGS 84 / SCAR IMW SQ49-50", 3217: "WGS 84 / SCAR IMW SQ51-52", 3218: "WGS 84 / SCAR IMW SQ53-54", 3219: "WGS 84 / SCAR IMW SQ55-56", 3220: "WGS 84 / SCAR IMW SQ57-58", 3221: "WGS 84 / SCAR IMW SR13-14", 3222: "WGS 84 / SCAR IMW SR15-16", 3223: "WGS 84 / SCAR IMW SR17-18", 3224: "WGS 84 / SCAR IMW SR19-20", 3225: "WGS 84 / SCAR IMW SR27-28", 3226: "WGS 84 / SCAR IMW SR29-30", 3227: "WGS 84 / SCAR IMW SR31-32", 3228: "WGS 84 / SCAR IMW SR33-34", 3229: "WGS 84 / SCAR IMW SR35-36", 3230: "WGS 84 / SCAR IMW SR37-38", 3231: "WGS 84 / SCAR IMW SR39-40", 3232: "WGS 84 / SCAR IMW SR41-42", 3233: "WGS 84 / SCAR IMW SR43-44", 3234: "WGS 84 / SCAR IMW SR45-46", 3235: "WGS 84 / SCAR IMW SR47-48", 3236: "WGS 84 / SCAR IMW SR49-50", 3237: "WGS 84 / SCAR IMW SR51-52", 3238: "WGS 84 / SCAR IMW SR53-54", 3239: "WGS 84 / SCAR IMW SR55-56", 3240: "WGS 84 / SCAR IMW SR57-58", 3241: "WGS 84 / SCAR IMW SR59-60", 3242: "WGS 84 / SCAR IMW SS04-06", 3243: "WGS 84 / SCAR IMW SS07-09", 3244: "WGS 84 / SCAR IMW SS10-12", 3245: "WGS 84 / SCAR IMW SS13-15", 3246: "WGS 84 / SCAR IMW SS16-18", 3247: "WGS 84 / SCAR IMW SS19-21", 3248: "WGS 84 / SCAR IMW SS25-27", 3249: "WGS 84 / SCAR IMW SS28-30", 3250: "WGS 84 / SCAR IMW SS31-33", 3251: "WGS 84 / SCAR IMW SS34-36", 3252: "WGS 84 / SCAR IMW SS37-39", 3253: "WGS 84 / SCAR IMW SS40-42", 3254: "WGS 84 / SCAR IMW SS43-45", 3255: "WGS 84 / SCAR IMW SS46-48", 3256: "WGS 84 / SCAR IMW SS49-51", 3257: "WGS 84 / SCAR IMW SS52-54", 3258: "WGS 84 / SCAR IMW SS55-57", 3259: "WGS 84 / SCAR IMW SS58-60", 3260: "WGS 84 / SCAR IMW ST01-04", 3261: "WGS 84 / SCAR IMW ST05-08", 3262: "WGS 84 / SCAR IMW ST09-12", 3263: "WGS 84 / SCAR IMW ST13-16", 3264: "WGS 84 / SCAR IMW ST17-20", 3265: "WGS 84 / SCAR IMW ST21-24", 3266: "WGS 84 / SCAR IMW ST25-28", 3267: "WGS 84 / SCAR IMW ST29-32", 3268: "WGS 84 / SCAR IMW ST33-36", 3269: "WGS 84 / SCAR IMW ST37-40", 3270: "WGS 84 / SCAR IMW ST41-44", 3271: "WGS 84 / SCAR IMW ST45-48", 3272: "WGS 84 / SCAR IMW ST49-52", 3273: "WGS 84 / SCAR IMW ST53-56", 3274: "WGS 84 / SCAR IMW ST57-60", 3275: "WGS 84 / SCAR IMW SU01-05", 3276: "WGS 84 / SCAR IMW SU06-10", 3277: "WGS 84 / SCAR IMW SU11-15", 3278: "WGS 84 / SCAR IMW SU16-20", 3279: "WGS 84 / SCAR IMW SU21-25", 3280: "WGS 84 / SCAR IMW SU26-30", 3281: "WGS 84 / SCAR IMW SU31-35", 3282: "WGS 84 / SCAR IMW SU36-40", 3283: "WGS 84 / SCAR IMW SU41-45", 3284: "WGS 84 / SCAR IMW SU46-50", 3285: "WGS 84 / SCAR IMW SU51-55", 3286: "WGS 84 / SCAR IMW SU56-60", 3287: "WGS 84 / SCAR IMW SV01-10", 3288: "WGS 84 / SCAR IMW SV11-20", 3289: "WGS 84 / SCAR IMW SV21-30", 3290: "WGS 84 / SCAR IMW SV31-40", 3291: "WGS 84 / SCAR IMW SV41-50", 3292: "WGS 84 / SCAR IMW SV51-60", 3293: "WGS 84 / SCAR IMW SW01-60", 3294: "WGS 84 / USGS Transantarctic Mountains", 3295: "Guam 1963 / Yap Islands", 3296: "RGPF / UTM zone 5S", 3297: "RGPF / UTM zone 6S", 3298: "RGPF / UTM zone 7S", 3299: "RGPF / UTM zone 8S", 3300: "Estonian Coordinate System of 1992", 3301: "Estonian Coordinate System of 1997", 3302: "IGN63 Hiva Oa / UTM zone 7S", 3303: "Fatu Iva 72 / UTM zone 7S", 3304: "Tahiti 79 / UTM zone 6S", 3305: "Moorea 87 / UTM zone 6S", 3306: "Maupiti 83 / UTM zone 5S", 3307: "Nakhl-e Ghanem / UTM zone 39N", 3308: "GDA94 / NSW Lambert", 3309: "NAD27 / California Albers", 3310: "NAD83 / California Albers", 3311: "NAD83(HARN) / California Albers", 3312: "CSG67 / UTM zone 21N", 3313: "RGFG95 / UTM zone 21N", 3314: "Katanga 1955 / Katanga Lambert", 3315: "Katanga 1955 / Katanga TM", 3316: "Kasai 1953 / Congo TM zone 22", 3317: "Kasai 1953 / Congo TM zone 24", 3318: "IGC 1962 / Congo TM zone 12", 3319: "IGC 1962 / Congo TM zone 14", 3320: "IGC 1962 / Congo TM zone 16", 3321: "IGC 1962 / Congo TM zone 18", 3322: "IGC 1962 / Congo TM zone 20", 3323: "IGC 1962 / Congo TM zone 22", 3324: "IGC 1962 / Congo TM zone 24", 3325: "IGC 1962 / Congo TM zone 26", 3326: "IGC 1962 / Congo TM zone 28", 3327: "IGC 1962 / Congo TM zone 30", 3328: "Pulkovo 1942(58) / GUGiK-80", 3329: "Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 5", 3330: "Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 6", 3331: "Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 7", 3332: "Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 8", 3333: "Pulkovo 1942(58) / Gauss-Kruger zone 3", 3334: "Pulkovo 1942(58) / Gauss-Kruger zone 4", 3335: "Pulkovo 1942(58) / Gauss-Kruger zone 5", 3336: "IGN 1962 Kerguelen / UTM zone 42S", 3337: "Le Pouce 1934 / Mauritius Grid", 3338: "NAD83 / Alaska Albers", 3339: "IGCB 1955 / Congo TM zone 12", 3340: "IGCB 1955 / Congo TM zone 14", 3341: "IGCB 1955 / Congo TM zone 16", 3342: "IGCB 1955 / UTM zone 33S", 3343: "Mauritania 1999 / UTM zone 28N", 3344: "Mauritania 1999 / UTM zone 29N", 3345: "Mauritania 1999 / UTM zone 30N", 3346: "LKS94 / Lithuania TM", 3347: "NAD83 / Statistics Canada Lambert", 3348: "NAD83(CSRS) / Statistics Canada Lambert", 3349: "WGS 84 / PDC Mercator", 3350: "Pulkovo 1942 / CS63 zone C0", 3351: "Pulkovo 1942 / CS63 zone C1", 3352: "Pulkovo 1942 / CS63 zone C2", 3353: "Mhast (onshore) / UTM zone 32S", 3354: "Mhast (offshore) / UTM zone 32S", 3355: "Egypt Gulf of Suez S-650 TL / Red Belt", 3356: "Grand Cayman 1959 / UTM zone 17N", 3357: "Little Cayman 1961 / UTM zone 17N", 3358: "NAD83(HARN) / North Carolina", 3359: "NAD83(HARN) / North Carolina (ftUS)", 3360: "NAD83(HARN) / South Carolina", 3361: "NAD83(HARN) / South Carolina (ft)", 3362: "NAD83(HARN) / Pennsylvania North", 3363: "NAD83(HARN) / Pennsylvania North (ftUS)", 3364: "NAD83(HARN) / Pennsylvania South", 3365: "NAD83(HARN) / Pennsylvania South (ftUS)", 3366: "Hong Kong 1963 Grid System", 3367: "IGN Astro 1960 / UTM zone 28N", 3368: "IGN Astro 1960 / UTM zone 29N", 3369: "IGN Astro 1960 / UTM zone 30N", 3370: "NAD27 / UTM zone 59N", 3371: "NAD27 / UTM zone 60N", 3372: "NAD83 / UTM zone 59N", 3373: "NAD83 / UTM zone 60N", 3374: "FD54 / UTM zone 29N", 3375: "GDM2000 / Peninsula RSO", 3376: "GDM2000 / East Malaysia BRSO", 3377: "GDM2000 / Johor Grid", 3378: "GDM2000 / Sembilan and Melaka Grid", 3379: "GDM2000 / Pahang Grid", 3380: "GDM2000 / Selangor Grid", 3381: "GDM2000 / Terengganu Grid", 3382: "GDM2000 / Pinang Grid", 3383: "GDM2000 / Kedah and Perlis Grid", 3384: "GDM2000 / Perak Grid", 3385: "GDM2000 / Kelantan Grid", 3386: "KKJ / Finland zone 0", 3387: "KKJ / Finland zone 5", 3388: "Pulkovo 1942 / Caspian Sea Mercator", 3389: "Pulkovo 1942 / 3-degree Gauss-Kruger zone 60", 3390: "Pulkovo 1995 / 3-degree Gauss-Kruger zone 60", 3391: "Karbala 1979 / UTM zone 37N", 3392: "Karbala 1979 / UTM zone 38N", 3393: "Karbala 1979 / UTM zone 39N", 3394: "Nahrwan 1934 / Iraq zone", 3395: "WGS 84 / World Mercator", 3396: "PD/83 / 3-degree Gauss-Kruger zone 3", 3397: "PD/83 / 3-degree Gauss-Kruger zone 4", 3398: "RD/83 / 3-degree Gauss-Kruger zone 4", 3399: "RD/83 / 3-degree Gauss-Kruger zone 5", 3400: "NAD83 / Alberta 10-TM (Forest)", 3401: "NAD83 / Alberta 10-TM (Resource)", 3402: "NAD83(CSRS) / Alberta 10-TM (Forest)", 3403: "NAD83(CSRS) / Alberta 10-TM (Resource)", 3404: "NAD83(HARN) / North Carolina (ftUS)", 3405: "VN-2000 / UTM zone 48N", 3406: "VN-2000 / UTM zone 49N", 3407: "Hong Kong 1963 Grid System", 3408: "NSIDC EASE-Grid North", 3409: "NSIDC EASE-Grid South", 3410: "NSIDC EASE-Grid Global", 3411: "NSIDC Sea Ice Polar Stereographic North", 3412: "NSIDC Sea Ice Polar Stereographic South", 3413: "WGS 84 / NSIDC Sea Ice Polar Stereographic North", 3414: "SVY21 / Singapore TM", 3415: "WGS 72BE / South China Sea Lambert", 3416: "ETRS89 / Austria Lambert", 3417: "NAD83 / Iowa North (ftUS)", 3418: "NAD83 / Iowa South (ftUS)", 3419: "NAD83 / Kansas North (ftUS)", 3420: "NAD83 / Kansas South (ftUS)", 3421: "NAD83 / Nevada East (ftUS)", 3422: "NAD83 / Nevada Central (ftUS)", 3423: "NAD83 / Nevada West (ftUS)", 3424: "NAD83 / New Jersey (ftUS)", 3425: "NAD83(HARN) / Iowa North (ftUS)", 3426: "NAD83(HARN) / Iowa South (ftUS)", 3427: "NAD83(HARN) / Kansas North (ftUS)", 3428: "NAD83(HARN) / Kansas South (ftUS)", 3429: "NAD83(HARN) / Nevada East (ftUS)", 3430: "NAD83(HARN) / Nevada Central (ftUS)", 3431: "NAD83(HARN) / Nevada West (ftUS)", 3432: "NAD83(HARN) / New Jersey (ftUS)", 3433: "NAD83 / Arkansas North (ftUS)", 3434: "NAD83 / Arkansas South (ftUS)", 3435: "NAD83 / Illinois East (ftUS)", 3436: "NAD83 / Illinois West (ftUS)", 3437: "NAD83 / New Hampshire (ftUS)", 3438: "NAD83 / Rhode Island (ftUS)", 3439: "PSD93 / UTM zone 39N", 3440: "PSD93 / UTM zone 40N", 3441: "NAD83(HARN) / Arkansas North (ftUS)", 3442: "NAD83(HARN) / Arkansas South (ftUS)", 3443: "NAD83(HARN) / Illinois East (ftUS)", 3444: "NAD83(HARN) / Illinois West (ftUS)", 3445: "NAD83(HARN) / New Hampshire (ftUS)", 3446: "NAD83(HARN) / Rhode Island (ftUS)", 3447: "ETRS89 / Belgian Lambert 2005", 3448: "JAD2001 / Jamaica Metric Grid", 3449: "JAD2001 / UTM zone 17N", 3450: "JAD2001 / UTM zone 18N", 3451: "NAD83 / Louisiana North (ftUS)", 3452: "NAD83 / Louisiana South (ftUS)", 3453: "NAD83 / Louisiana Offshore (ftUS)", 3454: "NAD83 / South Dakota North (ftUS)", 3455: "NAD83 / South Dakota South (ftUS)", 3456: "NAD83(HARN) / Louisiana North (ftUS)", 3457: "NAD83(HARN) / Louisiana South (ftUS)", 3458: "NAD83(HARN) / South Dakota North (ftUS)", 3459: "NAD83(HARN) / South Dakota South (ftUS)", 3460: "Fiji 1986 / Fiji Map Grid", 3461: "Dabola 1981 / UTM zone 28N", 3462: "Dabola 1981 / UTM zone 29N", 3463: "NAD83 / Maine CS2000 Central", 3464: "NAD83(HARN) / Maine CS2000 Central", 3465: "NAD83(NSRS2007) / Alabama East", 3466: "NAD83(NSRS2007) / Alabama West", 3467: "NAD83(NSRS2007) / Alaska Albers", 3468: "NAD83(NSRS2007) / Alaska zone 1", 3469: "NAD83(NSRS2007) / Alaska zone 2", 3470: "NAD83(NSRS2007) / Alaska zone 3", 3471: "NAD83(NSRS2007) / Alaska zone 4", 3472: "NAD83(NSRS2007) / Alaska zone 5", 3473: "NAD83(NSRS2007) / Alaska zone 6", 3474: "NAD83(NSRS2007) / Alaska zone 7", 3475: "NAD83(NSRS2007) / Alaska zone 8", 3476: "NAD83(NSRS2007) / Alaska zone 9", 3477: "NAD83(NSRS2007) / Alaska zone 10", 3478: "NAD83(NSRS2007) / Arizona Central", 3479: "NAD83(NSRS2007) / Arizona Central (ft)", 3480: "NAD83(NSRS2007) / Arizona East", 3481: "NAD83(NSRS2007) / Arizona East (ft)", 3482: "NAD83(NSRS2007) / Arizona West", 3483: "NAD83(NSRS2007) / Arizona West (ft)", 3484: "NAD83(NSRS2007) / Arkansas North", 3485: "NAD83(NSRS2007) / Arkansas North (ftUS)", 3486: "NAD83(NSRS2007) / Arkansas South", 3487: "NAD83(NSRS2007) / Arkansas South (ftUS)", 3488: "NAD83(NSRS2007) / California Albers", 3489: "NAD83(NSRS2007) / California zone 1", 3490: "NAD83(NSRS2007) / California zone 1 (ftUS)", 3491: "NAD83(NSRS2007) / California zone 2", 3492: "NAD83(NSRS2007) / California zone 2 (ftUS)", 3493: "NAD83(NSRS2007) / California zone 3", 3494: "NAD83(NSRS2007) / California zone 3 (ftUS)", 3495: "NAD83(NSRS2007) / California zone 4", 3496: "NAD83(NSRS2007) / California zone 4 (ftUS)", 3497: "NAD83(NSRS2007) / California zone 5", 3498: "NAD83(NSRS2007) / California zone 5 (ftUS)", 3499: "NAD83(NSRS2007) / California zone 6", 3500: "NAD83(NSRS2007) / California zone 6 (ftUS)", 3501: "NAD83(NSRS2007) / Colorado Central", 3502: "NAD83(NSRS2007) / Colorado Central (ftUS)", 3503: "NAD83(NSRS2007) / Colorado North", 3504: "NAD83(NSRS2007) / Colorado North (ftUS)", 3505: "NAD83(NSRS2007) / Colorado South", 3506: "NAD83(NSRS2007) / Colorado South (ftUS)", 3507: "NAD83(NSRS2007) / Connecticut", 3508: "NAD83(NSRS2007) / Connecticut (ftUS)", 3509: "NAD83(NSRS2007) / Delaware", 3510: "NAD83(NSRS2007) / Delaware (ftUS)", 3511: "NAD83(NSRS2007) / Florida East", 3512: "NAD83(NSRS2007) / Florida East (ftUS)", 3513: "NAD83(NSRS2007) / Florida GDL Albers", 3514: "NAD83(NSRS2007) / Florida North", 3515: "NAD83(NSRS2007) / Florida North (ftUS)", 3516: "NAD83(NSRS2007) / Florida West", 3517: "NAD83(NSRS2007) / Florida West (ftUS)", 3518: "NAD83(NSRS2007) / Georgia East", 3519: "NAD83(NSRS2007) / Georgia East (ftUS)", 3520: "NAD83(NSRS2007) / Georgia West", 3521: "NAD83(NSRS2007) / Georgia West (ftUS)", 3522: "NAD83(NSRS2007) / Idaho Central", 3523: "NAD83(NSRS2007) / Idaho Central (ftUS)", 3524: "NAD83(NSRS2007) / Idaho East", 3525: "NAD83(NSRS2007) / Idaho East (ftUS)", 3526: "NAD83(NSRS2007) / Idaho West", 3527: "NAD83(NSRS2007) / Idaho West (ftUS)", 3528: "NAD83(NSRS2007) / Illinois East", 3529: "NAD83(NSRS2007) / Illinois East (ftUS)", 3530: "NAD83(NSRS2007) / Illinois West", 3531: "NAD83(NSRS2007) / Illinois West (ftUS)", 3532: "NAD83(NSRS2007) / Indiana East", 3533: "NAD83(NSRS2007) / Indiana East (ftUS)", 3534: "NAD83(NSRS2007) / Indiana West", 3535: "NAD83(NSRS2007) / Indiana West (ftUS)", 3536: "NAD83(NSRS2007) / Iowa North", 3537: "NAD83(NSRS2007) / Iowa North (ftUS)", 3538: "NAD83(NSRS2007) / Iowa South", 3539: "NAD83(NSRS2007) / Iowa South (ftUS)", 3540: "NAD83(NSRS2007) / Kansas North", 3541: "NAD83(NSRS2007) / Kansas North (ftUS)", 3542: "NAD83(NSRS2007) / Kansas South", 3543: "NAD83(NSRS2007) / Kansas South (ftUS)", 3544: "NAD83(NSRS2007) / Kentucky North", 3545: "NAD83(NSRS2007) / Kentucky North (ftUS)", 3546: "NAD83(NSRS2007) / Kentucky Single Zone", 3547: "NAD83(NSRS2007) / Kentucky Single Zone (ftUS)", 3548: "NAD83(NSRS2007) / Kentucky South", 3549: "NAD83(NSRS2007) / Kentucky South (ftUS)", 3550: "NAD83(NSRS2007) / Louisiana North", 3551: "NAD83(NSRS2007) / Louisiana North (ftUS)", 3552: "NAD83(NSRS2007) / Louisiana South", 3553: "NAD83(NSRS2007) / Louisiana South (ftUS)", 3554: "NAD83(NSRS2007) / Maine CS2000 Central", 3555: "NAD83(NSRS2007) / Maine CS2000 East", 3556: "NAD83(NSRS2007) / Maine CS2000 West", 3557: "NAD83(NSRS2007) / Maine East", 3558: "NAD83(NSRS2007) / Maine West", 3559: "NAD83(NSRS2007) / Maryland", 3560: "NAD83 / Utah North (ftUS)", 3561: "Old Hawaiian / Hawaii zone 1", 3562: "Old Hawaiian / Hawaii zone 2", 3563: "Old Hawaiian / Hawaii zone 3", 3564: "Old Hawaiian / Hawaii zone 4", 3565: "Old Hawaiian / Hawaii zone 5", 3566: "NAD83 / Utah Central (ftUS)", 3567: "NAD83 / Utah South (ftUS)", 3568: "NAD83(HARN) / Utah North (ftUS)", 3569: "NAD83(HARN) / Utah Central (ftUS)", 3570: "NAD83(HARN) / Utah South (ftUS)", 3571: "WGS 84 / North Pole LAEA Bering Sea", 3572: "WGS 84 / North Pole LAEA Alaska", 3573: "WGS 84 / North Pole LAEA Canada", 3574: "WGS 84 / North Pole LAEA Atlantic", 3575: "WGS 84 / North Pole LAEA Europe", 3576: "WGS 84 / North Pole LAEA Russia", 3577: "GDA94 / Australian Albers", 3578: "NAD83 / Yukon Albers", 3579: "NAD83(CSRS) / Yukon Albers", 3580: "NAD83 / NWT Lambert", 3581: "NAD83(CSRS) / NWT Lambert", 3582: "NAD83(NSRS2007) / Maryland (ftUS)", 3583: "NAD83(NSRS2007) / Massachusetts Island", 3584: "NAD83(NSRS2007) / Massachusetts Island (ftUS)", 3585: "NAD83(NSRS2007) / Massachusetts Mainland", 3586: "NAD83(NSRS2007) / Massachusetts Mainland (ftUS)", 3587: "NAD83(NSRS2007) / Michigan Central", 3588: "NAD83(NSRS2007) / Michigan Central (ft)", 3589: "NAD83(NSRS2007) / Michigan North", 3590: "NAD83(NSRS2007) / Michigan North (ft)", 3591: "NAD83(NSRS2007) / Michigan Oblique Mercator", 3592: "NAD83(NSRS2007) / Michigan South", 3593: "NAD83(NSRS2007) / Michigan South (ft)", 3594: "NAD83(NSRS2007) / Minnesota Central", 3595: "NAD83(NSRS2007) / Minnesota North", 3596: "NAD83(NSRS2007) / Minnesota South", 3597: "NAD83(NSRS2007) / Mississippi East", 3598: "NAD83(NSRS2007) / Mississippi East (ftUS)", 3599: "NAD83(NSRS2007) / Mississippi West", 3600: "NAD83(NSRS2007) / Mississippi West (ftUS)", 3601: "NAD83(NSRS2007) / Missouri Central", 3602: "NAD83(NSRS2007) / Missouri East", 3603: "NAD83(NSRS2007) / Missouri West", 3604: "NAD83(NSRS2007) / Montana", 3605: "NAD83(NSRS2007) / Montana (ft)", 3606: "NAD83(NSRS2007) / Nebraska", 3607: "NAD83(NSRS2007) / Nevada Central", 3608: "NAD83(NSRS2007) / Nevada Central (ftUS)", 3609: "NAD83(NSRS2007) / Nevada East", 3610: "NAD83(NSRS2007) / Nevada East (ftUS)", 3611: "NAD83(NSRS2007) / Nevada West", 3612: "NAD83(NSRS2007) / Nevada West (ftUS)", 3613: "NAD83(NSRS2007) / New Hampshire", 3614: "NAD83(NSRS2007) / New Hampshire (ftUS)", 3615: "NAD83(NSRS2007) / New Jersey", 3616: "NAD83(NSRS2007) / New Jersey (ftUS)", 3617: "NAD83(NSRS2007) / New Mexico Central", 3618: "NAD83(NSRS2007) / New Mexico Central (ftUS)", 3619: "NAD83(NSRS2007) / New Mexico East", 3620: "NAD83(NSRS2007) / New Mexico East (ftUS)", 3621: "NAD83(NSRS2007) / New Mexico West", 3622: "NAD83(NSRS2007) / New Mexico West (ftUS)", 3623: "NAD83(NSRS2007) / New York Central", 3624: "NAD83(NSRS2007) / New York Central (ftUS)", 3625: "NAD83(NSRS2007) / New York East", 3626: "NAD83(NSRS2007) / New York East (ftUS)", 3627: "NAD83(NSRS2007) / New York Long Island", 3628: "NAD83(NSRS2007) / New York Long Island (ftUS)", 3629: "NAD83(NSRS2007) / New York West", 3630: "NAD83(NSRS2007) / New York West (ftUS)", 3631: "NAD83(NSRS2007) / North Carolina", 3632: "NAD83(NSRS2007) / North Carolina (ftUS)", 3633: "NAD83(NSRS2007) / North Dakota North", 3634: "NAD83(NSRS2007) / North Dakota North (ft)", 3635: "NAD83(NSRS2007) / North Dakota South", 3636: "NAD83(NSRS2007) / North Dakota South (ft)", 3637: "NAD83(NSRS2007) / Ohio North", 3638: "NAD83(NSRS2007) / Ohio South", 3639: "NAD83(NSRS2007) / Oklahoma North", 3640: "NAD83(NSRS2007) / Oklahoma North (ftUS)", 3641: "NAD83(NSRS2007) / Oklahoma South", 3642: "NAD83(NSRS2007) / Oklahoma South (ftUS)", 3643: "NAD83(NSRS2007) / Oregon LCC (m)", 3644: "NAD83(NSRS2007) / Oregon GIC Lambert (ft)", 3645: "NAD83(NSRS2007) / Oregon North", 3646: "NAD83(NSRS2007) / Oregon North (ft)", 3647: "NAD83(NSRS2007) / Oregon South", 3648: "NAD83(NSRS2007) / Oregon South (ft)", 3649: "NAD83(NSRS2007) / Pennsylvania North", 3650: "NAD83(NSRS2007) / Pennsylvania North (ftUS)", 3651: "NAD83(NSRS2007) / Pennsylvania South", 3652: "NAD83(NSRS2007) / Pennsylvania South (ftUS)", 3653: "NAD83(NSRS2007) / Rhode Island", 3654: "NAD83(NSRS2007) / Rhode Island (ftUS)", 3655: "NAD83(NSRS2007) / South Carolina", 3656: "NAD83(NSRS2007) / South Carolina (ft)", 3657: "NAD83(NSRS2007) / South Dakota North", 3658: "NAD83(NSRS2007) / South Dakota North (ftUS)", 3659: "NAD83(NSRS2007) / South Dakota South", 3660: "NAD83(NSRS2007) / South Dakota South (ftUS)", 3661: "NAD83(NSRS2007) / Tennessee", 3662: "NAD83(NSRS2007) / Tennessee (ftUS)", 3663: "NAD83(NSRS2007) / Texas Central", 3664: "NAD83(NSRS2007) / Texas Central (ftUS)", 3665: "NAD83(NSRS2007) / Texas Centric Albers Equal Area", 3666: "NAD83(NSRS2007) / Texas Centric Lambert Conformal", 3667: "NAD83(NSRS2007) / Texas North", 3668: "NAD83(NSRS2007) / Texas North (ftUS)", 3669: "NAD83(NSRS2007) / Texas North Central", 3670: "NAD83(NSRS2007) / Texas North Central (ftUS)", 3671: "NAD83(NSRS2007) / Texas South", 3672: "NAD83(NSRS2007) / Texas South (ftUS)", 3673: "NAD83(NSRS2007) / Texas South Central", 3674: "NAD83(NSRS2007) / Texas South Central (ftUS)", 3675: "NAD83(NSRS2007) / Utah Central", 3676: "NAD83(NSRS2007) / Utah Central (ft)", 3677: "NAD83(NSRS2007) / Utah Central (ftUS)", 3678: "NAD83(NSRS2007) / Utah North", 3679: "NAD83(NSRS2007) / Utah North (ft)", 3680: "NAD83(NSRS2007) / Utah North (ftUS)", 3681: "NAD83(NSRS2007) / Utah South", 3682: "NAD83(NSRS2007) / Utah South (ft)", 3683: "NAD83(NSRS2007) / Utah South (ftUS)", 3684: "NAD83(NSRS2007) / Vermont", 3685: "NAD83(NSRS2007) / Virginia North", 3686: "NAD83(NSRS2007) / Virginia North (ftUS)", 3687: "NAD83(NSRS2007) / Virginia South", 3688: "NAD83(NSRS2007) / Virginia South (ftUS)", 3689: "NAD83(NSRS2007) / Washington North", 3690: "NAD83(NSRS2007) / Washington North (ftUS)", 3691: "NAD83(NSRS2007) / Washington South", 3692: "NAD83(NSRS2007) / Washington South (ftUS)", 3693: "NAD83(NSRS2007) / West Virginia North", 3694: "NAD83(NSRS2007) / West Virginia South", 3695: "NAD83(NSRS2007) / Wisconsin Central", 3696: "NAD83(NSRS2007) / Wisconsin Central (ftUS)", 3697: "NAD83(NSRS2007) / Wisconsin North", 3698: "NAD83(NSRS2007) / Wisconsin North (ftUS)", 3699: "NAD83(NSRS2007) / Wisconsin South", 3700: "NAD83(NSRS2007) / Wisconsin South (ftUS)", 3701: "NAD83(NSRS2007) / Wisconsin Transverse Mercator", 3702: "NAD83(NSRS2007) / Wyoming East", 3703: "NAD83(NSRS2007) / Wyoming East Central", 3704: "NAD83(NSRS2007) / Wyoming West Central", 3705: "NAD83(NSRS2007) / Wyoming West", 3706: "NAD83(NSRS2007) / UTM zone 59N", 3707: "NAD83(NSRS2007) / UTM zone 60N", 3708: "NAD83(NSRS2007) / UTM zone 1N", 3709: "NAD83(NSRS2007) / UTM zone 2N", 3710: "NAD83(NSRS2007) / UTM zone 3N", 3711: "NAD83(NSRS2007) / UTM zone 4N", 3712: "NAD83(NSRS2007) / UTM zone 5N", 3713: "NAD83(NSRS2007) / UTM zone 6N", 3714: "NAD83(NSRS2007) / UTM zone 7N", 3715: "NAD83(NSRS2007) / UTM zone 8N", 3716: "NAD83(NSRS2007) / UTM zone 9N", 3717: "NAD83(NSRS2007) / UTM zone 10N", 3718: "NAD83(NSRS2007) / UTM zone 11N", 3719: "NAD83(NSRS2007) / UTM zone 12N", 3720: "NAD83(NSRS2007) / UTM zone 13N", 3721: "NAD83(NSRS2007) / UTM zone 14N", 3722: "NAD83(NSRS2007) / UTM zone 15N", 3723: "NAD83(NSRS2007) / UTM zone 16N", 3724: "NAD83(NSRS2007) / UTM zone 17N", 3725: "NAD83(NSRS2007) / UTM zone 18N", 3726: "NAD83(NSRS2007) / UTM zone 19N", 3727: "Reunion 1947 / TM Reunion", 3728: "NAD83(NSRS2007) / Ohio North (ftUS)", 3729: "NAD83(NSRS2007) / Ohio South (ftUS)", 3730: "NAD83(NSRS2007) / Wyoming East (ftUS)", 3731: "NAD83(NSRS2007) / Wyoming East Central (ftUS)", 3732: "NAD83(NSRS2007) / Wyoming West Central (ftUS)", 3733: "NAD83(NSRS2007) / Wyoming West (ftUS)", 3734: "NAD83 / Ohio North (ftUS)", 3735: "NAD83 / Ohio South (ftUS)", 3736: "NAD83 / Wyoming East (ftUS)", 3737: "NAD83 / Wyoming East Central (ftUS)", 3738: "NAD83 / Wyoming West Central (ftUS)", 3739: "NAD83 / Wyoming West (ftUS)", 3740: "NAD83(HARN) / UTM zone 10N", 3741: "NAD83(HARN) / UTM zone 11N", 3742: "NAD83(HARN) / UTM zone 12N", 3743: "NAD83(HARN) / UTM zone 13N", 3744: "NAD83(HARN) / UTM zone 14N", 3745: "NAD83(HARN) / UTM zone 15N", 3746: "NAD83(HARN) / UTM zone 16N", 3747: "NAD83(HARN) / UTM zone 17N", 3748: "NAD83(HARN) / UTM zone 18N", 3749: "NAD83(HARN) / UTM zone 19N", 3750: "NAD83(HARN) / UTM zone 4N", 3751: "NAD83(HARN) / UTM zone 5N", 3752: "WGS 84 / Mercator 41", 3753: "NAD83(HARN) / Ohio North (ftUS)", 3754: "NAD83(HARN) / Ohio South (ftUS)", 3755: "NAD83(HARN) / Wyoming East (ftUS)", 3756: "NAD83(HARN) / Wyoming East Central (ftUS)", 3757: "NAD83(HARN) / Wyoming West Central (ftUS)", 3758: "NAD83(HARN) / Wyoming West (ftUS)", 3759: "NAD83 / Hawaii zone 3 (ftUS)", 3760: "NAD83(HARN) / Hawaii zone 3 (ftUS)", 3761: "NAD83(CSRS) / UTM zone 22N", 3762: "WGS 84 / South Georgia Lambert", 3763: "ETRS89 / Portugal TM06", 3764: "NZGD2000 / Chatham Island Circuit 2000", 3765: "HTRS96 / Croatia TM", 3766: "HTRS96 / Croatia LCC", 3767: "HTRS96 / UTM zone 33N", 3768: "HTRS96 / UTM zone 34N", 3769: "Bermuda 1957 / UTM zone 20N", 3770: "BDA2000 / Bermuda 2000 National Grid", 3771: "NAD27 / Alberta 3TM ref merid 111 W", 3772: "NAD27 / Alberta 3TM ref merid 114 W", 3773: "NAD27 / Alberta 3TM ref merid 117 W", 3774: "NAD27 / Alberta 3TM ref merid 120 W", 3775: "NAD83 / Alberta 3TM ref merid 111 W", 3776: "NAD83 / Alberta 3TM ref merid 114 W", 3777: "NAD83 / Alberta 3TM ref merid 117 W", 3778: "NAD83 / Alberta 3TM ref merid 120 W", 3779: "NAD83(CSRS) / Alberta 3TM ref merid 111 W", 3780: "NAD83(CSRS) / Alberta 3TM ref merid 114 W", 3781: "NAD83(CSRS) / Alberta 3TM ref merid 117 W", 3782: "NAD83(CSRS) / Alberta 3TM ref merid 120 W", 3783: "Pitcairn 2006 / Pitcairn TM 2006", 3784: "Pitcairn 1967 / UTM zone 9S", 3785: "Popular Visualisation CRS / Mercator", 3786: "World Equidistant Cylindrical (Sphere)", 3787: "MGI / Slovene National Grid", 3788: "NZGD2000 / Auckland Islands TM 2000", 3789: "NZGD2000 / Campbell Island TM 2000", 3790: "NZGD2000 / Antipodes Islands TM 2000", 3791: "NZGD2000 / Raoul Island TM 2000", 3793: "NZGD2000 / Chatham Islands TM 2000", 3794: "Slovenia 1996 / Slovene National Grid", 3795: "NAD27 / Cuba Norte", 3796: "NAD27 / Cuba Sur", 3797: "NAD27 / MTQ Lambert", 3798: "NAD83 / MTQ Lambert", 3799: "NAD83(CSRS) / MTQ Lambert", 3800: "NAD27 / Alberta 3TM ref merid 120 W", 3801: "NAD83 / Alberta 3TM ref merid 120 W", 3802: "NAD83(CSRS) / Alberta 3TM ref merid 120 W", 3812: "ETRS89 / Belgian Lambert 2008", 3814: "NAD83 / Mississippi TM", 3815: "NAD83(HARN) / Mississippi TM", 3816: "NAD83(NSRS2007) / Mississippi TM", 3825: "TWD97 / TM2 zone 119", 3826: "TWD97 / TM2 zone 121", 3827: "TWD67 / TM2 zone 119", 3828: "TWD67 / TM2 zone 121", 3829: "Hu Tzu Shan 1950 / UTM zone 51N", 3832: "WGS 84 / PDC Mercator", 3833: "Pulkovo 1942(58) / Gauss-Kruger zone 2", 3834: "Pulkovo 1942(83) / Gauss-Kruger zone 2", 3835: "Pulkovo 1942(83) / Gauss-Kruger zone 3", 3836: "Pulkovo 1942(83) / Gauss-Kruger zone 4", 3837: "Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 3", 3838: "Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 4", 3839: "Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 9", 3840: "Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 10", 3841: "Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 6", 3842: "Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 7", 3843: "Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 8", 3844: "Pulkovo 1942(58) / Stereo70", 3845: "SWEREF99 / RT90 7.5 gon V emulation", 3846: "SWEREF99 / RT90 5 gon V emulation", 3847: "SWEREF99 / RT90 2.5 gon V emulation", 3848: "SWEREF99 / RT90 0 gon emulation", 3849: "SWEREF99 / RT90 2.5 gon O emulation", 3850: "SWEREF99 / RT90 5 gon O emulation", 3851: "NZGD2000 / NZCS2000", 3852: "RSRGD2000 / DGLC2000", 3854: "County ST74", 3857: "WGS 84 / Pseudo-Mercator", 3873: "ETRS89 / GK19FIN", 3874: "ETRS89 / GK20FIN", 3875: "ETRS89 / GK21FIN", 3876: "ETRS89 / GK22FIN", 3877: "ETRS89 / GK23FIN", 3878: "ETRS89 / GK24FIN", 3879: "ETRS89 / GK25FIN", 3880: "ETRS89 / GK26FIN", 3881: "ETRS89 / GK27FIN", 3882: "ETRS89 / GK28FIN", 3883: "ETRS89 / GK29FIN", 3884: "ETRS89 / GK30FIN", 3885: "ETRS89 / GK31FIN", 3890: "IGRS / UTM zone 37N", 3891: "IGRS / UTM zone 38N", 3892: "IGRS / UTM zone 39N", 3893: "ED50 / Iraq National Grid", 3907: "MGI 1901 / Balkans zone 5", 3908: "MGI 1901 / Balkans zone 6", 3909: "MGI 1901 / Balkans zone 7", 3910: "MGI 1901 / Balkans zone 8", 3911: "MGI 1901 / Slovenia Grid", 3912: "MGI 1901 / Slovene National Grid", 3920: "Puerto Rico / UTM zone 20N", 3942: "RGF93 / CC42", 3943: "RGF93 / CC43", 3944: "RGF93 / CC44", 3945: "RGF93 / CC45", 3946: "RGF93 / CC46", 3947: "RGF93 / CC47", 3948: "RGF93 / CC48", 3949: "RGF93 / CC49", 3950: "RGF93 / CC50", 3968: "NAD83 / Virginia Lambert", 3969: "NAD83(HARN) / Virginia Lambert", 3970: "NAD83(NSRS2007) / Virginia Lambert", 3973: "WGS 84 / NSIDC EASE-Grid North", 3974: "WGS 84 / NSIDC EASE-Grid South", 3975: "WGS 84 / NSIDC EASE-Grid Global", 3976: "WGS 84 / NSIDC Sea Ice Polar Stereographic South", 3978: "NAD83 / Canada Atlas Lambert", 3979: "NAD83(CSRS) / Canada Atlas Lambert", 3985: "Katanga 1955 / Katanga Lambert", 3986: "Katanga 1955 / Katanga Gauss zone A", 3987: "Katanga 1955 / Katanga Gauss zone B", 3988: "Katanga 1955 / Katanga Gauss zone C", 3989: "Katanga 1955 / Katanga Gauss zone D", 3991: "Puerto Rico State Plane CS of 1927", 3992: "Puerto Rico / St. Croix", 3993: "Guam 1963 / Guam SPCS", 3994: "WGS 84 / Mercator 41", 3995: "WGS 84 / Arctic Polar Stereographic", 3996: "WGS 84 / IBCAO Polar Stereographic", 3997: "WGS 84 / Dubai Local TM", 4026: "MOLDREF99 / Moldova TM", 4037: "WGS 84 / TMzn35N", 4038: "WGS 84 / TMzn36N", 4048: "RGRDC 2005 / Congo TM zone 12", 4049: "RGRDC 2005 / Congo TM zone 14", 4050: "RGRDC 2005 / Congo TM zone 16", 4051: "RGRDC 2005 / Congo TM zone 18", 4056: "RGRDC 2005 / Congo TM zone 20", 4057: "RGRDC 2005 / Congo TM zone 22", 4058: "RGRDC 2005 / Congo TM zone 24", 4059: "RGRDC 2005 / Congo TM zone 26", 4060: "RGRDC 2005 / Congo TM zone 28", 4061: "RGRDC 2005 / UTM zone 33S", 4062: "RGRDC 2005 / UTM zone 34S", 4063: "RGRDC 2005 / UTM zone 35S", 4071: "Chua / UTM zone 23S", 4082: "REGCAN95 / UTM zone 27N", 4083: "REGCAN95 / UTM zone 28N", 4087: "WGS 84 / World Equidistant Cylindrical", 4088: "World Equidistant Cylindrical (Sphere)", 4093: "ETRS89 / DKTM1", 4094: "ETRS89 / DKTM2", 4095: "ETRS89 / DKTM3", 4096: "ETRS89 / DKTM4", 4217: "NAD83 / BLM 59N (ftUS)", 4390: "Kertau 1968 / Johor Grid", 4391: "Kertau 1968 / Sembilan and Melaka Grid", 4392: "Kertau 1968 / Pahang Grid", 4393: "Kertau 1968 / Selangor Grid", 4394: "Kertau 1968 / Terengganu Grid", 4395: "Kertau 1968 / Pinang Grid", 4396: "Kertau 1968 / Kedah and Perlis Grid", 4397: "Kertau 1968 / Perak Revised Grid", 4398: "Kertau 1968 / Kelantan Grid", 4399: "NAD27 / BLM 59N (ftUS)", 4400: "NAD27 / BLM 60N (ftUS)", 4401: "NAD27 / BLM 1N (ftUS)", 4402: "NAD27 / BLM 2N (ftUS)", 4403: "NAD27 / BLM 3N (ftUS)", 4404: "NAD27 / BLM 4N (ftUS)", 4405: "NAD27 / BLM 5N (ftUS)", 4406: "NAD27 / BLM 6N (ftUS)", 4407: "NAD27 / BLM 7N (ftUS)", 4408: "NAD27 / BLM 8N (ftUS)", 4409: "NAD27 / BLM 9N (ftUS)", 4410: "NAD27 / BLM 10N (ftUS)", 4411: "NAD27 / BLM 11N (ftUS)", 4412: "NAD27 / BLM 12N (ftUS)", 4413: "NAD27 / BLM 13N (ftUS)", 4414: "NAD83(HARN) / Guam Map Grid", 4415: "Katanga 1955 / Katanga Lambert", 4417: "Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 7", 4418: "NAD27 / BLM 18N (ftUS)", 4419: "NAD27 / BLM 19N (ftUS)", 4420: "NAD83 / BLM 60N (ftUS)", 4421: "NAD83 / BLM 1N (ftUS)", 4422: "NAD83 / BLM 2N (ftUS)", 4423: "NAD83 / BLM 3N (ftUS)", 4424: "NAD83 / BLM 4N (ftUS)", 4425: "NAD83 / BLM 5N (ftUS)", 4426: "NAD83 / BLM 6N (ftUS)", 4427: "NAD83 / BLM 7N (ftUS)", 4428: "NAD83 / BLM 8N (ftUS)", 4429: "NAD83 / BLM 9N (ftUS)", 4430: "NAD83 / BLM 10N (ftUS)", 4431: "NAD83 / BLM 11N (ftUS)", 4432: "NAD83 / BLM 12N (ftUS)", 4433: "NAD83 / BLM 13N (ftUS)", 4434: "Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 8", 4437: "NAD83(NSRS2007) / Puerto Rico and Virgin Is.", 4438: "NAD83 / BLM 18N (ftUS)", 4439: "NAD83 / BLM 19N (ftUS)", 4455: "NAD27 / Pennsylvania South", 4456: "NAD27 / New York Long Island", 4457: "NAD83 / South Dakota North (ftUS)", 4462: "WGS 84 / Australian Centre for Remote Sensing Lambert", 4467: "RGSPM06 / UTM zone 21N", 4471: "RGM04 / UTM zone 38S", 4474: "Cadastre 1997 / UTM zone 38S", 4484: "Mexico ITRF92 / UTM zone 11N", 4485: "Mexico ITRF92 / UTM zone 12N", 4486: "Mexico ITRF92 / UTM zone 13N", 4487: "Mexico ITRF92 / UTM zone 14N", 4488: "Mexico ITRF92 / UTM zone 15N", 4489: "Mexico ITRF92 / UTM zone 16N", 4491: "CGCS2000 / Gauss-Kruger zone 13", 4492: "CGCS2000 / Gauss-Kruger zone 14", 4493: "CGCS2000 / Gauss-Kruger zone 15", 4494: "CGCS2000 / Gauss-Kruger zone 16", 4495: "CGCS2000 / Gauss-Kruger zone 17", 4496: "CGCS2000 / Gauss-Kruger zone 18", 4497: "CGCS2000 / Gauss-Kruger zone 19", 4498: "CGCS2000 / Gauss-Kruger zone 20", 4499: "CGCS2000 / Gauss-Kruger zone 21", 4500: "CGCS2000 / Gauss-Kruger zone 22", 4501: "CGCS2000 / Gauss-Kruger zone 23", 4502: "CGCS2000 / Gauss-Kruger CM 75E", 4503: "CGCS2000 / Gauss-Kruger CM 81E", 4504: "CGCS2000 / Gauss-Kruger CM 87E", 4505: "CGCS2000 / Gauss-Kruger CM 93E", 4506: "CGCS2000 / Gauss-Kruger CM 99E", 4507: "CGCS2000 / Gauss-Kruger CM 105E", 4508: "CGCS2000 / Gauss-Kruger CM 111E", 4509: "CGCS2000 / Gauss-Kruger CM 117E", 4510: "CGCS2000 / Gauss-Kruger CM 123E", 4511: "CGCS2000 / Gauss-Kruger CM 129E", 4512: "CGCS2000 / Gauss-Kruger CM 135E", 4513: "CGCS2000 / 3-degree Gauss-Kruger zone 25", 4514: "CGCS2000 / 3-degree Gauss-Kruger zone 26", 4515: "CGCS2000 / 3-degree Gauss-Kruger zone 27", 4516: "CGCS2000 / 3-degree Gauss-Kruger zone 28", 4517: "CGCS2000 / 3-degree Gauss-Kruger zone 29", 4518: "CGCS2000 / 3-degree Gauss-Kruger zone 30", 4519: "CGCS2000 / 3-degree Gauss-Kruger zone 31", 4520: "CGCS2000 / 3-degree Gauss-Kruger zone 32", 4521: "CGCS2000 / 3-degree Gauss-Kruger zone 33", 4522: "CGCS2000 / 3-degree Gauss-Kruger zone 34", 4523: "CGCS2000 / 3-degree Gauss-Kruger zone 35", 4524: "CGCS2000 / 3-degree Gauss-Kruger zone 36", 4525: "CGCS2000 / 3-degree Gauss-Kruger zone 37", 4526: "CGCS2000 / 3-degree Gauss-Kruger zone 38", 4527: "CGCS2000 / 3-degree Gauss-Kruger zone 39", 4528: "CGCS2000 / 3-degree Gauss-Kruger zone 40", 4529: "CGCS2000 / 3-degree Gauss-Kruger zone 41", 4530: "CGCS2000 / 3-degree Gauss-Kruger zone 42", 4531: "CGCS2000 / 3-degree Gauss-Kruger zone 43", 4532: "CGCS2000 / 3-degree Gauss-Kruger zone 44", 4533: "CGCS2000 / 3-degree Gauss-Kruger zone 45", 4534: "CGCS2000 / 3-degree Gauss-Kruger CM 75E", 4535: "CGCS2000 / 3-degree Gauss-Kruger CM 78E", 4536: "CGCS2000 / 3-degree Gauss-Kruger CM 81E", 4537: "CGCS2000 / 3-degree Gauss-Kruger CM 84E", 4538: "CGCS2000 / 3-degree Gauss-Kruger CM 87E", 4539: "CGCS2000 / 3-degree Gauss-Kruger CM 90E", 4540: "CGCS2000 / 3-degree Gauss-Kruger CM 93E", 4541: "CGCS2000 / 3-degree Gauss-Kruger CM 96E", 4542: "CGCS2000 / 3-degree Gauss-Kruger CM 99E", 4543: "CGCS2000 / 3-degree Gauss-Kruger CM 102E", 4544: "CGCS2000 / 3-degree Gauss-Kruger CM 105E", 4545: "CGCS2000 / 3-degree Gauss-Kruger CM 108E", 4546: "CGCS2000 / 3-degree Gauss-Kruger CM 111E", 4547: "CGCS2000 / 3-degree Gauss-Kruger CM 114E", 4548: "CGCS2000 / 3-degree Gauss-Kruger CM 117E", 4549: "CGCS2000 / 3-degree Gauss-Kruger CM 120E", 4550: "CGCS2000 / 3-degree Gauss-Kruger CM 123E", 4551: "CGCS2000 / 3-degree Gauss-Kruger CM 126E", 4552: "CGCS2000 / 3-degree Gauss-Kruger CM 129E", 4553: "CGCS2000 / 3-degree Gauss-Kruger CM 132E", 4554: "CGCS2000 / 3-degree Gauss-Kruger CM 135E", 4559: "RRAF 1991 / UTM zone 20N", 4568: "New Beijing / Gauss-Kruger zone 13", 4569: "New Beijing / Gauss-Kruger zone 14", 4570: "New Beijing / Gauss-Kruger zone 15", 4571: "New Beijing / Gauss-Kruger zone 16", 4572: "New Beijing / Gauss-Kruger zone 17", 4573: "New Beijing / Gauss-Kruger zone 18", 4574: "New Beijing / Gauss-Kruger zone 19", 4575: "New Beijing / Gauss-Kruger zone 20", 4576: "New Beijing / Gauss-Kruger zone 21", 4577: "New Beijing / Gauss-Kruger zone 22", 4578: "New Beijing / Gauss-Kruger zone 23", 4579: "New Beijing / Gauss-Kruger CM 75E", 4580: "New Beijing / Gauss-Kruger CM 81E", 4581: "New Beijing / Gauss-Kruger CM 87E", 4582: "New Beijing / Gauss-Kruger CM 93E", 4583: "New Beijing / Gauss-Kruger CM 99E", 4584: "New Beijing / Gauss-Kruger CM 105E", 4585: "New Beijing / Gauss-Kruger CM 111E", 4586: "New Beijing / Gauss-Kruger CM 117E", 4587: "New Beijing / Gauss-Kruger CM 123E", 4588: "New Beijing / Gauss-Kruger CM 129E", 4589: "New Beijing / Gauss-Kruger CM 135E", 4647: "ETRS89 / UTM zone 32N (zE-N)", 4652: "New Beijing / 3-degree Gauss-Kruger zone 25", 4653: "New Beijing / 3-degree Gauss-Kruger zone 26", 4654: "New Beijing / 3-degree Gauss-Kruger zone 27", 4655: "New Beijing / 3-degree Gauss-Kruger zone 28", 4656: "New Beijing / 3-degree Gauss-Kruger zone 29", 4766: "New Beijing / 3-degree Gauss-Kruger zone 30", 4767: "New Beijing / 3-degree Gauss-Kruger zone 31", 4768: "New Beijing / 3-degree Gauss-Kruger zone 32", 4769: "New Beijing / 3-degree Gauss-Kruger zone 33", 4770: "New Beijing / 3-degree Gauss-Kruger zone 34", 4771: "New Beijing / 3-degree Gauss-Kruger zone 35", 4772: "New Beijing / 3-degree Gauss-Kruger zone 36", 4773: "New Beijing / 3-degree Gauss-Kruger zone 37", 4774: "New Beijing / 3-degree Gauss-Kruger zone 38", 4775: "New Beijing / 3-degree Gauss-Kruger zone 39", 4776: "New Beijing / 3-degree Gauss-Kruger zone 40", 4777: "New Beijing / 3-degree Gauss-Kruger zone 41", 4778: "New Beijing / 3-degree Gauss-Kruger zone 42", 4779: "New Beijing / 3-degree Gauss-Kruger zone 43", 4780: "New Beijing / 3-degree Gauss-Kruger zone 44", 4781: "New Beijing / 3-degree Gauss-Kruger zone 45", 4782: "New Beijing / 3-degree Gauss-Kruger CM 75E", 4783: "New Beijing / 3-degree Gauss-Kruger CM 78E", 4784: "New Beijing / 3-degree Gauss-Kruger CM 81E", 4785: "New Beijing / 3-degree Gauss-Kruger CM 84E", 4786: "New Beijing / 3-degree Gauss-Kruger CM 87E", 4787: "New Beijing / 3-degree Gauss-Kruger CM 90E", 4788: "New Beijing / 3-degree Gauss-Kruger CM 93E", 4789: "New Beijing / 3-degree Gauss-Kruger CM 96E", 4790: "New Beijing / 3-degree Gauss-Kruger CM 99E", 4791: "New Beijing / 3-degree Gauss-Kruger CM 102E", 4792: "New Beijing / 3-degree Gauss-Kruger CM 105E", 4793: "New Beijing / 3-degree Gauss-Kruger CM 108E", 4794: "New Beijing / 3-degree Gauss-Kruger CM 111E", 4795: "New Beijing / 3-degree Gauss-Kruger CM 114E", 4796: "New Beijing / 3-degree Gauss-Kruger CM 117E", 4797: "New Beijing / 3-degree Gauss-Kruger CM 120E", 4798: "New Beijing / 3-degree Gauss-Kruger CM 123E", 4799: "New Beijing / 3-degree Gauss-Kruger CM 126E", 4800: "New Beijing / 3-degree Gauss-Kruger CM 129E", 4812: "New Beijing / 3-degree Gauss-Kruger CM 132E", 4822: "New Beijing / 3-degree Gauss-Kruger CM 135E", 4826: "WGS 84 / Cape Verde National", 4839: "ETRS89 / LCC Germany (N-E)", 4855: "ETRS89 / NTM zone 5", 4856: "ETRS89 / NTM zone 6", 4857: "ETRS89 / NTM zone 7", 4858: "ETRS89 / NTM zone 8", 4859: "ETRS89 / NTM zone 9", 4860: "ETRS89 / NTM zone 10", 4861: "ETRS89 / NTM zone 11", 4862: "ETRS89 / NTM zone 12", 4863: "ETRS89 / NTM zone 13", 4864: "ETRS89 / NTM zone 14", 4865: "ETRS89 / NTM zone 15", 4866: "ETRS89 / NTM zone 16", 4867: "ETRS89 / NTM zone 17", 4868: "ETRS89 / NTM zone 18", 4869: "ETRS89 / NTM zone 19", 4870: "ETRS89 / NTM zone 20", 4871: "ETRS89 / NTM zone 21", 4872: "ETRS89 / NTM zone 22", 4873: "ETRS89 / NTM zone 23", 4874: "ETRS89 / NTM zone 24", 4875: "ETRS89 / NTM zone 25", 4876: "ETRS89 / NTM zone 26", 4877: "ETRS89 / NTM zone 27", 4878: "ETRS89 / NTM zone 28", 4879: "ETRS89 / NTM zone 29", 4880: "ETRS89 / NTM zone 30", 5014: "PTRA08 / UTM zone 25N", 5015: "PTRA08 / UTM zone 26N", 5016: "PTRA08 / UTM zone 28N", 5017: "Lisbon 1890 / Portugal Bonne New", 5018: "Lisbon / Portuguese Grid New", 5041: "WGS 84 / UPS North (E,N)", 5042: "WGS 84 / UPS South (E,N)", 5048: "ETRS89 / TM35FIN(N,E)", 5069: "NAD27 / Conus Albers", 5070: "NAD83 / Conus Albers", 5071: "NAD83(HARN) / Conus Albers", 5072: "NAD83(NSRS2007) / Conus Albers", 5105: "ETRS89 / NTM zone 5", 5106: "ETRS89 / NTM zone 6", 5107: "ETRS89 / NTM zone 7", 5108: "ETRS89 / NTM zone 8", 5109: "ETRS89 / NTM zone 9", 5110: "ETRS89 / NTM zone 10", 5111: "ETRS89 / NTM zone 11", 5112: "ETRS89 / NTM zone 12", 5113: "ETRS89 / NTM zone 13", 5114: "ETRS89 / NTM zone 14", 5115: "ETRS89 / NTM zone 15", 5116: "ETRS89 / NTM zone 16", 5117: "ETRS89 / NTM zone 17", 5118: "ETRS89 / NTM zone 18", 5119: "ETRS89 / NTM zone 19", 5120: "ETRS89 / NTM zone 20", 5121: "ETRS89 / NTM zone 21", 5122: "ETRS89 / NTM zone 22", 5123: "ETRS89 / NTM zone 23", 5124: "ETRS89 / NTM zone 24", 5125: "ETRS89 / NTM zone 25", 5126: "ETRS89 / NTM zone 26", 5127: "ETRS89 / NTM zone 27", 5128: "ETRS89 / NTM zone 28", 5129: "ETRS89 / NTM zone 29", 5130: "ETRS89 / NTM zone 30", 5167: "Korean 1985 / East Sea Belt", 5168: "Korean 1985 / Central Belt Jeju", 5169: "Tokyo 1892 / Korea West Belt", 5170: "Tokyo 1892 / Korea Central Belt", 5171: "Tokyo 1892 / Korea East Belt", 5172: "Tokyo 1892 / Korea East Sea Belt", 5173: "Korean 1985 / Modified West Belt", 5174: "Korean 1985 / Modified Central Belt", 5175: "Korean 1985 / Modified Central Belt Jeju", 5176: "Korean 1985 / Modified East Belt", 5177: "Korean 1985 / Modified East Sea Belt", 5178: "Korean 1985 / Unified CS", 5179: "Korea 2000 / Unified CS", 5180: "Korea 2000 / West Belt", 5181: "Korea 2000 / Central Belt", 5182: "Korea 2000 / Central Belt Jeju", 5183: "Korea 2000 / East Belt", 5184: "Korea 2000 / East Sea Belt", 5185: "Korea 2000 / West Belt 2010", 5186: "Korea 2000 / Central Belt 2010", 5187: "Korea 2000 / East Belt 2010", 5188: "Korea 2000 / East Sea Belt 2010", 5221: "S-JTSK (Ferro) / Krovak East North", 5223: "WGS 84 / Gabon TM", 5224: "S-JTSK/05 (Ferro) / Modified Krovak", 5225: "S-JTSK/05 (Ferro) / Modified Krovak East North", 5234: "Kandawala / Sri Lanka Grid", 5235: "SLD99 / Sri Lanka Grid 1999", 5243: "ETRS89 / LCC Germany (E-N)", 5247: "GDBD2009 / Brunei BRSO", 5253: "TUREF / TM27", 5254: "TUREF / TM30", 5255: "TUREF / TM33", 5256: "TUREF / TM36", 5257: "TUREF / TM39", 5258: "TUREF / TM42", 5259: "TUREF / TM45", 5266: "DRUKREF 03 / Bhutan National Grid", 5269: "TUREF / 3-degree Gauss-Kruger zone 9", 5270: "TUREF / 3-degree Gauss-Kruger zone 10", 5271: "TUREF / 3-degree Gauss-Kruger zone 11", 5272: "TUREF / 3-degree Gauss-Kruger zone 12", 5273: "TUREF / 3-degree Gauss-Kruger zone 13", 5274: "TUREF / 3-degree Gauss-Kruger zone 14", 5275: "TUREF / 3-degree Gauss-Kruger zone 15", 5292: "DRUKREF 03 / Bumthang TM", 5293: "DRUKREF 03 / Chhukha TM", 5294: "DRUKREF 03 / Dagana TM", 5295: "DRUKREF 03 / Gasa TM", 5296: "DRUKREF 03 / Ha TM", 5297: "DRUKREF 03 / Lhuentse TM", 5298: "DRUKREF 03 / Mongar TM", 5299: "DRUKREF 03 / Paro TM", 5300: "DRUKREF 03 / Pemagatshel TM", 5301: "DRUKREF 03 / Punakha TM", 5302: "DRUKREF 03 / Samdrup Jongkhar TM", 5303: "DRUKREF 03 / Samtse TM", 5304: "DRUKREF 03 / Sarpang TM", 5305: "DRUKREF 03 / Thimphu TM", 5306: "DRUKREF 03 / Trashigang TM", 5307: "DRUKREF 03 / Trongsa TM", 5308: "DRUKREF 03 / Tsirang TM", 5309: "DRUKREF 03 / Wangdue Phodrang TM", 5310: "DRUKREF 03 / Yangtse TM", 5311: "DRUKREF 03 / Zhemgang TM", 5316: "ETRS89 / Faroe TM", 5320: "NAD83 / Teranet Ontario Lambert", 5321: "NAD83(CSRS) / Teranet Ontario Lambert", 5325: "ISN2004 / Lambert 2004", 5329: "Segara (Jakarta) / NEIEZ", 5330: "Batavia (Jakarta) / NEIEZ", 5331: "Makassar (Jakarta) / NEIEZ", 5337: "Aratu / UTM zone 25S", 5343: "POSGAR 2007 / Argentina 1", 5344: "POSGAR 2007 / Argentina 2", 5345: "POSGAR 2007 / Argentina 3", 5346: "POSGAR 2007 / Argentina 4", 5347: "POSGAR 2007 / Argentina 5", 5348: "POSGAR 2007 / Argentina 6", 5349: "POSGAR 2007 / Argentina 7", 5355: "MARGEN / UTM zone 20S", 5356: "MARGEN / UTM zone 19S", 5357: "MARGEN / UTM zone 21S", 5361: "SIRGAS-Chile / UTM zone 19S", 5362: "SIRGAS-Chile / UTM zone 18S", 5367: "CR05 / CRTM05", 5382: "SIRGAS-ROU98 / UTM zone 21S", 5383: "SIRGAS-ROU98 / UTM zone 22S", 5387: "Peru96 / UTM zone 18S", 5388: "Peru96 / UTM zone 17S", 5389: "Peru96 / UTM zone 19S", 5396: "SIRGAS 2000 / UTM zone 26S", 5456: "Ocotepeque 1935 / Costa Rica Norte", 5457: "Ocotepeque 1935 / Costa Rica Sur", 5458: "Ocotepeque 1935 / Guatemala Norte", 5459: "Ocotepeque 1935 / Guatemala Sur", 5460: "Ocotepeque 1935 / El Salvador Lambert", 5461: "Ocotepeque 1935 / Nicaragua Norte", 5462: "Ocotepeque 1935 / Nicaragua Sur", 5463: "SAD69 / UTM zone 17N", 5466: "Sibun Gorge 1922 / Colony Grid", 5469: "Panama-Colon 1911 / Panama Lambert", 5472: "Panama-Colon 1911 / Panama Polyconic", 5479: "RSRGD2000 / MSLC2000", 5480: "RSRGD2000 / BCLC2000", 5481: "RSRGD2000 / PCLC2000", 5482: "RSRGD2000 / RSPS2000", 5490: "RGAF09 / UTM zone 20N", 5513: "S-JTSK / Krovak", 5514: "S-JTSK / Krovak East North", 5515: "S-JTSK/05 / Modified Krovak", 5516: "S-JTSK/05 / Modified Krovak East North", 5518: "CI1971 / Chatham Islands Map Grid", 5519: "CI1979 / Chatham Islands Map Grid", 5520: "DHDN / 3-degree Gauss-Kruger zone 1", 5523: "WGS 84 / Gabon TM 2011", 5530: "SAD69(96) / Brazil Polyconic", 5531: "SAD69(96) / UTM zone 21S", 5532: "SAD69(96) / UTM zone 22S", 5533: "SAD69(96) / UTM zone 23S", 5534: "SAD69(96) / UTM zone 24S", 5535: "SAD69(96) / UTM zone 25S", 5536: "Corrego Alegre 1961 / UTM zone 21S", 5537: "Corrego Alegre 1961 / UTM zone 22S", 5538: "Corrego Alegre 1961 / UTM zone 23S", 5539: "Corrego Alegre 1961 / UTM zone 24S", 5550: "PNG94 / PNGMG94 zone 54", 5551: "PNG94 / PNGMG94 zone 55", 5552: "PNG94 / PNGMG94 zone 56", 5559: "Ocotepeque 1935 / Guatemala Norte", 5562: "UCS-2000 / Gauss-Kruger zone 4", 5563: "UCS-2000 / Gauss-Kruger zone 5", 5564: "UCS-2000 / Gauss-Kruger zone 6", 5565: "UCS-2000 / Gauss-Kruger zone 7", 5566: "UCS-2000 / Gauss-Kruger CM 21E", 5567: "UCS-2000 / Gauss-Kruger CM 27E", 5568: "UCS-2000 / Gauss-Kruger CM 33E", 5569: "UCS-2000 / Gauss-Kruger CM 39E", 5570: "UCS-2000 / 3-degree Gauss-Kruger zone 7", 5571: "UCS-2000 / 3-degree Gauss-Kruger zone 8", 5572: "UCS-2000 / 3-degree Gauss-Kruger zone 9", 5573: "UCS-2000 / 3-degree Gauss-Kruger zone 10", 5574: "UCS-2000 / 3-degree Gauss-Kruger zone 11", 5575: "UCS-2000 / 3-degree Gauss-Kruger zone 12", 5576: "UCS-2000 / 3-degree Gauss-Kruger zone 13", 5577: "UCS-2000 / 3-degree Gauss-Kruger CM 21E", 5578: "UCS-2000 / 3-degree Gauss-Kruger CM 24E", 5579: "UCS-2000 / 3-degree Gauss-Kruger CM 27E", 5580: "UCS-2000 / 3-degree Gauss-Kruger CM 30E", 5581: "UCS-2000 / 3-degree Gauss-Kruger CM 33E", 5582: "UCS-2000 / 3-degree Gauss-Kruger CM 36E", 5583: "UCS-2000 / 3-degree Gauss-Kruger CM 39E", 5588: "NAD27 / New Brunswick Stereographic (NAD27)", 5589: "Sibun Gorge 1922 / Colony Grid", 5596: "FEH2010 / Fehmarnbelt TM", 5623: "NAD27 / Michigan East", 5624: "NAD27 / Michigan Old Central", 5625: "NAD27 / Michigan West", 5627: "ED50 / TM 6 NE", 5629: "Moznet / UTM zone 38S", 5631: "Pulkovo 1942(58) / Gauss-Kruger zone 2 (E-N)", 5632: "PTRA08 / LCC Europe", 5633: "PTRA08 / LAEA Europe", 5634: "REGCAN95 / LCC Europe", 5635: "REGCAN95 / LAEA Europe", 5636: "TUREF / LAEA Europe", 5637: "TUREF / LCC Europe", 5638: "ISN2004 / LAEA Europe", 5639: "ISN2004 / LCC Europe", 5641: "SIRGAS 2000 / Brazil Mercator", 5643: "ED50 / SPBA LCC", 5644: "RGR92 / UTM zone 39S", 5646: "NAD83 / Vermont (ftUS)", 5649: "ETRS89 / UTM zone 31N (zE-N)", 5650: "ETRS89 / UTM zone 33N (zE-N)", 5651: "ETRS89 / UTM zone 31N (N-zE)", 5652: "ETRS89 / UTM zone 32N (N-zE)", 5653: "ETRS89 / UTM zone 33N (N-zE)", 5654: "NAD83(HARN) / Vermont (ftUS)", 5655: "NAD83(NSRS2007) / Vermont (ftUS)", 5659: "Monte Mario / TM Emilia-Romagna", 5663: "Pulkovo 1942(58) / Gauss-Kruger zone 3 (E-N)", 5664: "Pulkovo 1942(83) / Gauss-Kruger zone 2 (E-N)", 5665: "Pulkovo 1942(83) / Gauss-Kruger zone 3 (E-N)", 5666: "PD/83 / 3-degree Gauss-Kruger zone 3 (E-N)", 5667: "PD/83 / 3-degree Gauss-Kruger zone 4 (E-N)", 5668: "RD/83 / 3-degree Gauss-Kruger zone 4 (E-N)", 5669: "RD/83 / 3-degree Gauss-Kruger zone 5 (E-N)", 5670: "Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 3 (E-N)", 5671: "Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 4 (E-N)", 5672: "Pulkovo 1942(58) / 3-degree Gauss-Kruger zone 5 (E-N)", 5673: "Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 3 (E-N)", 5674: "Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 4 (E-N)", 5675: "Pulkovo 1942(83) / 3-degree Gauss-Kruger zone 5 (E-N)", 5676: "DHDN / 3-degree Gauss-Kruger zone 2 (E-N)", 5677: "DHDN / 3-degree Gauss-Kruger zone 3 (E-N)", 5678: "DHDN / 3-degree Gauss-Kruger zone 4 (E-N)", 5679: "DHDN / 3-degree Gauss-Kruger zone 5 (E-N)", 5680: "DHDN / 3-degree Gauss-Kruger zone 1 (E-N)", 5682: "DB_REF / 3-degree Gauss-Kruger zone 2 (E-N)", 5683: "DB_REF / 3-degree Gauss-Kruger zone 3 (E-N)", 5684: "DB_REF / 3-degree Gauss-Kruger zone 4 (E-N)", 5685: "DB_REF / 3-degree Gauss-Kruger zone 5 (E-N)", 5700: "NZGD2000 / UTM zone 1S", 5819: "EPSG topocentric example A", 5820: "EPSG topocentric example B", 5821: "EPSG vertical perspective example", 5825: "AGD66 / ACT Standard Grid", 5836: "Yemen NGN96 / UTM zone 37N", 5837: "Yemen NGN96 / UTM zone 40N", 5839: "Peru96 / UTM zone 17S", 5842: "WGS 84 / TM 12 SE", 5844: "RGRDC 2005 / Congo TM zone 30", 5858: "SAD69(96) / UTM zone 22S", 5875: "SAD69(96) / UTM zone 18S", 5876: "SAD69(96) / UTM zone 19S", 5877: "SAD69(96) / UTM zone 20S", 5879: "Cadastre 1997 / UTM zone 38S", 5880: "SIRGAS 2000 / Brazil Polyconic", 5887: "TGD2005 / Tonga Map Grid", 5890: "JAXA Snow Depth Polar Stereographic North", 5921: "WGS 84 / EPSG Arctic Regional zone A1", 5922: "WGS 84 / EPSG Arctic Regional zone A2", 5923: "WGS 84 / EPSG Arctic Regional zone A3", 5924: "WGS 84 / EPSG Arctic Regional zone A4", 5925: "WGS 84 / EPSG Arctic Regional zone A5", 5926: "WGS 84 / EPSG Arctic Regional zone B1", 5927: "WGS 84 / EPSG Arctic Regional zone B2", 5928: "WGS 84 / EPSG Arctic Regional zone B3", 5929: "WGS 84 / EPSG Arctic Regional zone B4", 5930: "WGS 84 / EPSG Arctic Regional zone B5", 5931: "WGS 84 / EPSG Arctic Regional zone C1", 5932: "WGS 84 / EPSG Arctic Regional zone C2", 5933: "WGS 84 / EPSG Arctic Regional zone C3", 5934: "WGS 84 / EPSG Arctic Regional zone C4", 5935: "WGS 84 / EPSG Arctic Regional zone C5", 5936: "WGS 84 / EPSG Alaska Polar Stereographic", 5937: "WGS 84 / EPSG Canada Polar Stereographic", 5938: "WGS 84 / EPSG Greenland Polar Stereographic", 5939: "WGS 84 / EPSG Norway Polar Stereographic", 5940: "WGS 84 / EPSG Russia Polar Stereographic", 6050: "GR96 / EPSG Arctic zone 1-25", 6051: "GR96 / EPSG Arctic zone 2-18", 6052: "GR96 / EPSG Arctic zone 2-20", 6053: "GR96 / EPSG Arctic zone 3-29", 6054: "GR96 / EPSG Arctic zone 3-31", 6055: "GR96 / EPSG Arctic zone 3-33", 6056: "GR96 / EPSG Arctic zone 4-20", 6057: "GR96 / EPSG Arctic zone 4-22", 6058: "GR96 / EPSG Arctic zone 4-24", 6059: "GR96 / EPSG Arctic zone 5-41", 6060: "GR96 / EPSG Arctic zone 5-43", 6061: "GR96 / EPSG Arctic zone 5-45", 6062: "GR96 / EPSG Arctic zone 6-26", 6063: "GR96 / EPSG Arctic zone 6-28", 6064: "GR96 / EPSG Arctic zone 6-30", 6065: "GR96 / EPSG Arctic zone 7-11", 6066: "GR96 / EPSG Arctic zone 7-13", 6067: "GR96 / EPSG Arctic zone 8-20", 6068: "GR96 / EPSG Arctic zone 8-22", 6069: "ETRS89 / EPSG Arctic zone 2-22", 6070: "ETRS89 / EPSG Arctic zone 3-11", 6071: "ETRS89 / EPSG Arctic zone 4-26", 6072: "ETRS89 / EPSG Arctic zone 4-28", 6073: "ETRS89 / EPSG Arctic zone 5-11", 6074: "ETRS89 / EPSG Arctic zone 5-13", 6075: "WGS 84 / EPSG Arctic zone 2-24", 6076: "WGS 84 / EPSG Arctic zone 2-26", 6077: "WGS 84 / EPSG Arctic zone 3-13", 6078: "WGS 84 / EPSG Arctic zone 3-15", 6079: "WGS 84 / EPSG Arctic zone 3-17", 6080: "WGS 84 / EPSG Arctic zone 3-19", 6081: "WGS 84 / EPSG Arctic zone 4-30", 6082: "WGS 84 / EPSG Arctic zone 4-32", 6083: "WGS 84 / EPSG Arctic zone 4-34", 6084: "WGS 84 / EPSG Arctic zone 4-36", 6085: "WGS 84 / EPSG Arctic zone 4-38", 6086: "WGS 84 / EPSG Arctic zone 4-40", 6087: "WGS 84 / EPSG Arctic zone 5-15", 6088: "WGS 84 / EPSG Arctic zone 5-17", 6089: "WGS 84 / EPSG Arctic zone 5-19", 6090: "WGS 84 / EPSG Arctic zone 5-21", 6091: "WGS 84 / EPSG Arctic zone 5-23", 6092: "WGS 84 / EPSG Arctic zone 5-25", 6093: "WGS 84 / EPSG Arctic zone 5-27", 6094: "NAD83(NSRS2007) / EPSG Arctic zone 5-29", 6095: "NAD83(NSRS2007) / EPSG Arctic zone 5-31", 6096: "NAD83(NSRS2007) / EPSG Arctic zone 6-14", 6097: "NAD83(NSRS2007) / EPSG Arctic zone 6-16", 6098: "NAD83(CSRS) / EPSG Arctic zone 1-23", 6099: "NAD83(CSRS) / EPSG Arctic zone 2-14", 6100: "NAD83(CSRS) / EPSG Arctic zone 2-16", 6101: "NAD83(CSRS) / EPSG Arctic zone 3-25", 6102: "NAD83(CSRS) / EPSG Arctic zone 3-27", 6103: "NAD83(CSRS) / EPSG Arctic zone 3-29", 6104: "NAD83(CSRS) / EPSG Arctic zone 4-14", 6105: "NAD83(CSRS) / EPSG Arctic zone 4-16", 6106: "NAD83(CSRS) / EPSG Arctic zone 4-18", 6107: "NAD83(CSRS) / EPSG Arctic zone 5-33", 6108: "NAD83(CSRS) / EPSG Arctic zone 5-35", 6109: "NAD83(CSRS) / EPSG Arctic zone 5-37", 6110: "NAD83(CSRS) / EPSG Arctic zone 5-39", 6111: "NAD83(CSRS) / EPSG Arctic zone 6-18", 6112: "NAD83(CSRS) / EPSG Arctic zone 6-20", 6113: "NAD83(CSRS) / EPSG Arctic zone 6-22", 6114: "NAD83(CSRS) / EPSG Arctic zone 6-24", 6115: "WGS 84 / EPSG Arctic zone 1-27", 6116: "WGS 84 / EPSG Arctic zone 1-29", 6117: "WGS 84 / EPSG Arctic zone 1-31", 6118: "WGS 84 / EPSG Arctic zone 1-21", 6119: "WGS 84 / EPSG Arctic zone 2-28", 6120: "WGS 84 / EPSG Arctic zone 2-10", 6121: "WGS 84 / EPSG Arctic zone 2-12", 6122: "WGS 84 / EPSG Arctic zone 3-21", 6123: "WGS 84 / EPSG Arctic zone 3-23", 6124: "WGS 84 / EPSG Arctic zone 4-12", 6125: "ETRS89 / EPSG Arctic zone 5-47", 6128: "Grand Cayman National Grid 1959", 6129: "Sister Islands National Grid 1961", 6141: "Cayman Islands National Grid 2011", 6200: "NAD27 / Michigan North", 6201: "NAD27 / Michigan Central", 6202: "NAD27 / Michigan South", 6204: "Macedonia State Coordinate System", 6210: "SIRGAS 2000 / UTM zone 23N", 6211: "SIRGAS 2000 / UTM zone 24N", 6244: "MAGNA-SIRGAS / Arauca urban grid", 6245: "MAGNA-SIRGAS / Armenia urban grid", 6246: "MAGNA-SIRGAS / Barranquilla urban grid", 6247: "MAGNA-SIRGAS / Bogota urban grid", 6248: "MAGNA-SIRGAS / Bucaramanga urban grid", 6249: "MAGNA-SIRGAS / Cali urban grid", 6250: "MAGNA-SIRGAS / Cartagena urban grid", 6251: "MAGNA-SIRGAS / Cucuta urban grid", 6252: "MAGNA-SIRGAS / Florencia urban grid", 6253: "MAGNA-SIRGAS / Ibague urban grid", 6254: "MAGNA-SIRGAS / Inirida urban grid", 6255: "MAGNA-SIRGAS / Leticia urban grid", 6256: "MAGNA-SIRGAS / Manizales urban grid", 6257: "MAGNA-SIRGAS / Medellin urban grid", 6258: "MAGNA-SIRGAS / Mitu urban grid", 6259: "MAGNA-SIRGAS / Mocoa urban grid", 6260: "MAGNA-SIRGAS / Monteria urban grid", 6261: "MAGNA-SIRGAS / Neiva urban grid", 6262: "MAGNA-SIRGAS / Pasto urban grid", 6263: "MAGNA-SIRGAS / Pereira urban grid", 6264: "MAGNA-SIRGAS / Popayan urban grid", 6265: "MAGNA-SIRGAS / Puerto Carreno urban grid", 6266: "MAGNA-SIRGAS / Quibdo urban grid", 6267: "MAGNA-SIRGAS / Riohacha urban grid", 6268: "MAGNA-SIRGAS / San Andres urban grid", 6269: "MAGNA-SIRGAS / San Jose del Guaviare urban grid", 6270: "MAGNA-SIRGAS / Santa Marta urban grid", 6271: "MAGNA-SIRGAS / Sucre urban grid", 6272: "MAGNA-SIRGAS / Tunja urban grid", 6273: "MAGNA-SIRGAS / Valledupar urban grid", 6274: "MAGNA-SIRGAS / Villavicencio urban grid", 6275: "MAGNA-SIRGAS / Yopal urban grid", 6307: "NAD83(CORS96) / Puerto Rico and Virgin Is.", 6312: "CGRS93 / Cyprus Local Transverse Mercator", 6316: "Macedonia State Coordinate System zone 7", 6328: "NAD83(2011) / UTM zone 59N", 6329: "NAD83(2011) / UTM zone 60N", 6330: "NAD83(2011) / UTM zone 1N", 6331: "NAD83(2011) / UTM zone 2N", 6332: "NAD83(2011) / UTM zone 3N", 6333: "NAD83(2011) / UTM zone 4N", 6334: "NAD83(2011) / UTM zone 5N", 6335: "NAD83(2011) / UTM zone 6N", 6336: "NAD83(2011) / UTM zone 7N", 6337: "NAD83(2011) / UTM zone 8N", 6338: "NAD83(2011) / UTM zone 9N", 6339: "NAD83(2011) / UTM zone 10N", 6340: "NAD83(2011) / UTM zone 11N", 6341: "NAD83(2011) / UTM zone 12N", 6342: "NAD83(2011) / UTM zone 13N", 6343: "NAD83(2011) / UTM zone 14N", 6344: "NAD83(2011) / UTM zone 15N", 6345: "NAD83(2011) / UTM zone 16N", 6346: "NAD83(2011) / UTM zone 17N", 6347: "NAD83(2011) / UTM zone 18N", 6348: "NAD83(2011) / UTM zone 19N", 6350: "NAD83(2011) / Conus Albers", 6351: "NAD83(2011) / EPSG Arctic zone 5-29", 6352: "NAD83(2011) / EPSG Arctic zone 5-31", 6353: "NAD83(2011) / EPSG Arctic zone 6-14", 6354: "NAD83(2011) / EPSG Arctic zone 6-16", 6355: "NAD83(2011) / Alabama East", 6356: "NAD83(2011) / Alabama West", 6362: "Mexico ITRF92 / LCC", 6366: "Mexico ITRF2008 / UTM zone 11N", 6367: "Mexico ITRF2008 / UTM zone 12N", 6368: "Mexico ITRF2008 / UTM zone 13N", 6369: "Mexico ITRF2008 / UTM zone 14N", 6370: "Mexico ITRF2008 / UTM zone 15N", 6371: "Mexico ITRF2008 / UTM zone 16N", 6372: "Mexico ITRF2008 / LCC", 6381: "UCS-2000 / Ukraine TM zone 7", 6382: "UCS-2000 / Ukraine TM zone 8", 6383: "UCS-2000 / Ukraine TM zone 9", 6384: "UCS-2000 / Ukraine TM zone 10", 6385: "UCS-2000 / Ukraine TM zone 11", 6386: "UCS-2000 / Ukraine TM zone 12", 6387: "UCS-2000 / Ukraine TM zone 13", 6391: "Cayman Islands National Grid 2011", 6393: "NAD83(2011) / Alaska Albers", 6394: "NAD83(2011) / Alaska zone 1", 6395: "NAD83(2011) / Alaska zone 2", 6396: "NAD83(2011) / Alaska zone 3", 6397: "NAD83(2011) / Alaska zone 4", 6398: "NAD83(2011) / Alaska zone 5", 6399: "NAD83(2011) / Alaska zone 6", 6400: "NAD83(2011) / Alaska zone 7", 6401: "NAD83(2011) / Alaska zone 8", 6402: "NAD83(2011) / Alaska zone 9", 6403: "NAD83(2011) / Alaska zone 10", 6404: "NAD83(2011) / Arizona Central", 6405: "NAD83(2011) / Arizona Central (ft)", 6406: "NAD83(2011) / Arizona East", 6407: "NAD83(2011) / Arizona East (ft)", 6408: "NAD83(2011) / Arizona West", 6409: "NAD83(2011) / Arizona West (ft)", 6410: "NAD83(2011) / Arkansas North", 6411: "NAD83(2011) / Arkansas North (ftUS)", 6412: "NAD83(2011) / Arkansas South", 6413: "NAD83(2011) / Arkansas South (ftUS)", 6414: "NAD83(2011) / California Albers", 6415: "NAD83(2011) / California zone 1", 6416: "NAD83(2011) / California zone 1 (ftUS)", 6417: "NAD83(2011) / California zone 2", 6418: "NAD83(2011) / California zone 2 (ftUS)", 6419: "NAD83(2011) / California zone 3", 6420: "NAD83(2011) / California zone 3 (ftUS)", 6421: "NAD83(2011) / California zone 4", 6422: "NAD83(2011) / California zone 4 (ftUS)", 6423: "NAD83(2011) / California zone 5", 6424: "NAD83(2011) / California zone 5 (ftUS)", 6425: "NAD83(2011) / California zone 6", 6426: "NAD83(2011) / California zone 6 (ftUS)", 6427: "NAD83(2011) / Colorado Central", 6428: "NAD83(2011) / Colorado Central (ftUS)", 6429: "NAD83(2011) / Colorado North", 6430: "NAD83(2011) / Colorado North (ftUS)", 6431: "NAD83(2011) / Colorado South", 6432: "NAD83(2011) / Colorado South (ftUS)", 6433: "NAD83(2011) / Connecticut", 6434: "NAD83(2011) / Connecticut (ftUS)", 6435: "NAD83(2011) / Delaware", 6436: "NAD83(2011) / Delaware (ftUS)", 6437: "NAD83(2011) / Florida East", 6438: "NAD83(2011) / Florida East (ftUS)", 6439: "NAD83(2011) / Florida GDL Albers", 6440: "NAD83(2011) / Florida North", 6441: "NAD83(2011) / Florida North (ftUS)", 6442: "NAD83(2011) / Florida West", 6443: "NAD83(2011) / Florida West (ftUS)", 6444: "NAD83(2011) / Georgia East", 6445: "NAD83(2011) / Georgia East (ftUS)", 6446: "NAD83(2011) / Georgia West", 6447: "NAD83(2011) / Georgia West (ftUS)", 6448: "NAD83(2011) / Idaho Central", 6449: "NAD83(2011) / Idaho Central (ftUS)", 6450: "NAD83(2011) / Idaho East", 6451: "NAD83(2011) / Idaho East (ftUS)", 6452: "NAD83(2011) / Idaho West", 6453: "NAD83(2011) / Idaho West (ftUS)", 6454: "NAD83(2011) / Illinois East", 6455: "NAD83(2011) / Illinois East (ftUS)", 6456: "NAD83(2011) / Illinois West", 6457: "NAD83(2011) / Illinois West (ftUS)", 6458: "NAD83(2011) / Indiana East", 6459: "NAD83(2011) / Indiana East (ftUS)", 6460: "NAD83(2011) / Indiana West", 6461: "NAD83(2011) / Indiana West (ftUS)", 6462: "NAD83(2011) / Iowa North", 6463: "NAD83(2011) / Iowa North (ftUS)", 6464: "NAD83(2011) / Iowa South", 6465: "NAD83(2011) / Iowa South (ftUS)", 6466: "NAD83(2011) / Kansas North", 6467: "NAD83(2011) / Kansas North (ftUS)", 6468: "NAD83(2011) / Kansas South", 6469: "NAD83(2011) / Kansas South (ftUS)", 6470: "NAD83(2011) / Kentucky North", 6471: "NAD83(2011) / Kentucky North (ftUS)", 6472: "NAD83(2011) / Kentucky Single Zone", 6473: "NAD83(2011) / Kentucky Single Zone (ftUS)", 6474: "NAD83(2011) / Kentucky South", 6475: "NAD83(2011) / Kentucky South (ftUS)", 6476: "NAD83(2011) / Louisiana North", 6477: "NAD83(2011) / Louisiana North (ftUS)", 6478: "NAD83(2011) / Louisiana South", 6479: "NAD83(2011) / Louisiana South (ftUS)", 6480: "NAD83(2011) / Maine CS2000 Central", 6481: "NAD83(2011) / Maine CS2000 East", 6482: "NAD83(2011) / Maine CS2000 West", 6483: "NAD83(2011) / Maine East", 6484: "NAD83(2011) / Maine East (ftUS)", 6485: "NAD83(2011) / Maine West", 6486: "NAD83(2011) / Maine West (ftUS)", 6487: "NAD83(2011) / Maryland", 6488: "NAD83(2011) / Maryland (ftUS)", 6489: "NAD83(2011) / Massachusetts Island", 6490: "NAD83(2011) / Massachusetts Island (ftUS)", 6491: "NAD83(2011) / Massachusetts Mainland", 6492: "NAD83(2011) / Massachusetts Mainland (ftUS)", 6493: "NAD83(2011) / Michigan Central", 6494: "NAD83(2011) / Michigan Central (ft)", 6495: "NAD83(2011) / Michigan North", 6496: "NAD83(2011) / Michigan North (ft)", 6497: "NAD83(2011) / Michigan Oblique Mercator", 6498: "NAD83(2011) / Michigan South", 6499: "NAD83(2011) / Michigan South (ft)", 6500: "NAD83(2011) / Minnesota Central", 6501: "NAD83(2011) / Minnesota Central (ftUS)", 6502: "NAD83(2011) / Minnesota North", 6503: "NAD83(2011) / Minnesota North (ftUS)", 6504: "NAD83(2011) / Minnesota South", 6505: "NAD83(2011) / Minnesota South (ftUS)", 6506: "NAD83(2011) / Mississippi East", 6507: "NAD83(2011) / Mississippi East (ftUS)", 6508: "NAD83(2011) / Mississippi TM", 6509: "NAD83(2011) / Mississippi West", 6510: "NAD83(2011) / Mississippi West (ftUS)", 6511: "NAD83(2011) / Missouri Central", 6512: "NAD83(2011) / Missouri East", 6513: "NAD83(2011) / Missouri West", 6514: "NAD83(2011) / Montana", 6515: "NAD83(2011) / Montana (ft)", 6516: "NAD83(2011) / Nebraska", 6517: "NAD83(2011) / Nebraska (ftUS)", 6518: "NAD83(2011) / Nevada Central", 6519: "NAD83(2011) / Nevada Central (ftUS)", 6520: "NAD83(2011) / Nevada East", 6521: "NAD83(2011) / Nevada East (ftUS)", 6522: "NAD83(2011) / Nevada West", 6523: "NAD83(2011) / Nevada West (ftUS)", 6524: "NAD83(2011) / New Hampshire", 6525: "NAD83(2011) / New Hampshire (ftUS)", 6526: "NAD83(2011) / New Jersey", 6527: "NAD83(2011) / New Jersey (ftUS)", 6528: "NAD83(2011) / New Mexico Central", 6529: "NAD83(2011) / New Mexico Central (ftUS)", 6530: "NAD83(2011) / New Mexico East", 6531: "NAD83(2011) / New Mexico East (ftUS)", 6532: "NAD83(2011) / New Mexico West", 6533: "NAD83(2011) / New Mexico West (ftUS)", 6534: "NAD83(2011) / New York Central", 6535: "NAD83(2011) / New York Central (ftUS)", 6536: "NAD83(2011) / New York East", 6537: "NAD83(2011) / New York East (ftUS)", 6538: "NAD83(2011) / New York Long Island", 6539: "NAD83(2011) / New York Long Island (ftUS)", 6540: "NAD83(2011) / New York West", 6541: "NAD83(2011) / New York West (ftUS)", 6542: "NAD83(2011) / North Carolina", 6543: "NAD83(2011) / North Carolina (ftUS)", 6544: "NAD83(2011) / North Dakota North", 6545: "NAD83(2011) / North Dakota North (ft)", 6546: "NAD83(2011) / North Dakota South", 6547: "NAD83(2011) / North Dakota South (ft)", 6548: "NAD83(2011) / Ohio North", 6549: "NAD83(2011) / Ohio North (ftUS)", 6550: "NAD83(2011) / Ohio South", 6551: "NAD83(2011) / Ohio South (ftUS)", 6552: "NAD83(2011) / Oklahoma North", 6553: "NAD83(2011) / Oklahoma North (ftUS)", 6554: "NAD83(2011) / Oklahoma South", 6555: "NAD83(2011) / Oklahoma South (ftUS)", 6556: "NAD83(2011) / Oregon LCC (m)", 6557: "NAD83(2011) / Oregon GIC Lambert (ft)", 6558: "NAD83(2011) / Oregon North", 6559: "NAD83(2011) / Oregon North (ft)", 6560: "NAD83(2011) / Oregon South", 6561: "NAD83(2011) / Oregon South (ft)", 6562: "NAD83(2011) / Pennsylvania North", 6563: "NAD83(2011) / Pennsylvania North (ftUS)", 6564: "NAD83(2011) / Pennsylvania South", 6565: "NAD83(2011) / Pennsylvania South (ftUS)", 6566: "NAD83(2011) / Puerto Rico and Virgin Is.", 6567: "NAD83(2011) / Rhode Island", 6568: "NAD83(2011) / Rhode Island (ftUS)", 6569: "NAD83(2011) / South Carolina", 6570: "NAD83(2011) / South Carolina (ft)", 6571: "NAD83(2011) / South Dakota North", 6572: "NAD83(2011) / South Dakota North (ftUS)", 6573: "NAD83(2011) / South Dakota South", 6574: "NAD83(2011) / South Dakota South (ftUS)", 6575: "NAD83(2011) / Tennessee", 6576: "NAD83(2011) / Tennessee (ftUS)", 6577: "NAD83(2011) / Texas Central", 6578: "NAD83(2011) / Texas Central (ftUS)", 6579: "NAD83(2011) / Texas Centric Albers Equal Area", 6580: "NAD83(2011) / Texas Centric Lambert Conformal", 6581: "NAD83(2011) / Texas North", 6582: "NAD83(2011) / Texas North (ftUS)", 6583: "NAD83(2011) / Texas North Central", 6584: "NAD83(2011) / Texas North Central (ftUS)", 6585: "NAD83(2011) / Texas South", 6586: "NAD83(2011) / Texas South (ftUS)", 6587: "NAD83(2011) / Texas South Central", 6588: "NAD83(2011) / Texas South Central (ftUS)", 6589: "NAD83(2011) / Vermont", 6590: "NAD83(2011) / Vermont (ftUS)", 6591: "NAD83(2011) / Virginia Lambert", 6592: "NAD83(2011) / Virginia North", 6593: "NAD83(2011) / Virginia North (ftUS)", 6594: "NAD83(2011) / Virginia South", 6595: "NAD83(2011) / Virginia South (ftUS)", 6596: "NAD83(2011) / Washington North", 6597: "NAD83(2011) / Washington North (ftUS)", 6598: "NAD83(2011) / Washington South", 6599: "NAD83(2011) / Washington South (ftUS)", 6600: "NAD83(2011) / West Virginia North", 6601: "NAD83(2011) / West Virginia North (ftUS)", 6602: "NAD83(2011) / West Virginia South", 6603: "NAD83(2011) / West Virginia South (ftUS)", 6604: "NAD83(2011) / Wisconsin Central", 6605: "NAD83(2011) / Wisconsin Central (ftUS)", 6606: "NAD83(2011) / Wisconsin North", 6607: "NAD83(2011) / Wisconsin North (ftUS)", 6608: "NAD83(2011) / Wisconsin South", 6609: "NAD83(2011) / Wisconsin South (ftUS)", 6610: "NAD83(2011) / Wisconsin Transverse Mercator", 6611: "NAD83(2011) / Wyoming East", 6612: "NAD83(2011) / Wyoming East (ftUS)", 6613: "NAD83(2011) / Wyoming East Central", 6614: "NAD83(2011) / Wyoming East Central (ftUS)", 6615: "NAD83(2011) / Wyoming West", 6616: "NAD83(2011) / Wyoming West (ftUS)", 6617: "NAD83(2011) / Wyoming West Central", 6618: "NAD83(2011) / Wyoming West Central (ftUS)", 6619: "NAD83(2011) / Utah Central", 6620: "NAD83(2011) / Utah North", 6621: "NAD83(2011) / Utah South", 6622: "NAD83(CSRS) / Quebec Lambert", 6623: "NAD83 / Quebec Albers", 6624: "NAD83(CSRS) / Quebec Albers", 6625: "NAD83(2011) / Utah Central (ftUS)", 6626: "NAD83(2011) / Utah North (ftUS)", 6627: "NAD83(2011) / Utah South (ftUS)", 6628: "NAD83(PA11) / Hawaii zone 1", 6629: "NAD83(PA11) / Hawaii zone 2", 6630: "NAD83(PA11) / Hawaii zone 3", 6631: "NAD83(PA11) / Hawaii zone 4", 6632: "NAD83(PA11) / Hawaii zone 5", 6633: "NAD83(PA11) / Hawaii zone 3 (ftUS)", 6634: "NAD83(PA11) / UTM zone 4N", 6635: "NAD83(PA11) / UTM zone 5N", 6636: "NAD83(PA11) / UTM zone 2S", 6637: "NAD83(MA11) / Guam Map Grid", 6646: "Karbala 1979 / Iraq National Grid", 6669: "JGD2011 / Japan Plane Rectangular CS I", 6670: "JGD2011 / Japan Plane Rectangular CS II", 6671: "JGD2011 / Japan Plane Rectangular CS III", 6672: "JGD2011 / Japan Plane Rectangular CS IV", 6673: "JGD2011 / Japan Plane Rectangular CS V", 6674: "JGD2011 / Japan Plane Rectangular CS VI", 6675: "JGD2011 / Japan Plane Rectangular CS VII", 6676: "JGD2011 / Japan Plane Rectangular CS VIII", 6677: "JGD2011 / Japan Plane Rectangular CS IX", 6678: "JGD2011 / Japan Plane Rectangular CS X", 6679: "JGD2011 / Japan Plane Rectangular CS XI", 6680: "JGD2011 / Japan Plane Rectangular CS XII", 6681: "JGD2011 / Japan Plane Rectangular CS XIII", 6682: "JGD2011 / Japan Plane Rectangular CS XIV", 6683: "JGD2011 / Japan Plane Rectangular CS XV", 6684: "JGD2011 / Japan Plane Rectangular CS XVI", 6685: "JGD2011 / Japan Plane Rectangular CS XVII", 6686: "JGD2011 / Japan Plane Rectangular CS XVIII", 6687: "JGD2011 / Japan Plane Rectangular CS XIX", 6688: "JGD2011 / UTM zone 51N", 6689: "JGD2011 / UTM zone 52N", 6690: "JGD2011 / UTM zone 53N", 6691: "JGD2011 / UTM zone 54N", 6692: "JGD2011 / UTM zone 55N", 6703: "WGS 84 / TM 60 SW", 6707: "RDN2008 / TM32", 6708: "RDN2008 / TM33", 6709: "RDN2008 / TM34", 6720: "WGS 84 / CIG92", 6721: "GDA94 / CIG94", 6722: "WGS 84 / CKIG92", 6723: "GDA94 / CKIG94", 6732: "GDA94 / MGA zone 41", 6733: "GDA94 / MGA zone 42", 6734: "GDA94 / MGA zone 43", 6735: "GDA94 / MGA zone 44", 6736: "GDA94 / MGA zone 46", 6737: "GDA94 / MGA zone 47", 6738: "GDA94 / MGA zone 59", 6784: "NAD83(CORS96) / Oregon Baker zone (m)", 6785: "NAD83(CORS96) / Oregon Baker zone (ft)", 6786: "NAD83(2011) / Oregon Baker zone (m)", 6787: "NAD83(2011) / Oregon Baker zone (ft)", 6788: "NAD83(CORS96) / Oregon Bend-Klamath Falls zone (m)", 6789: "NAD83(CORS96) / Oregon Bend-Klamath Falls zone (ft)", 6790: "NAD83(2011) / Oregon Bend-Klamath Falls zone (m)", 6791: "NAD83(2011) / Oregon Bend-Klamath Falls zone (ft)", 6792: "NAD83(CORS96) / Oregon Bend-Redmond-Prineville zone (m)", 6793: "NAD83(CORS96) / Oregon Bend-Redmond-Prineville zone (ft)", 6794: "NAD83(2011) / Oregon Bend-Redmond-Prineville zone (m)", 6795: "NAD83(2011) / Oregon Bend-Redmond-Prineville zone (ft)", 6796: "NAD83(CORS96) / Oregon Bend-Burns zone (m)", 6797: "NAD83(CORS96) / Oregon Bend-Burns zone (ft)", 6798: "NAD83(2011) / Oregon Bend-Burns zone (m)", 6799: "NAD83(2011) / Oregon Bend-Burns zone (ft)", 6800: "NAD83(CORS96) / Oregon Canyonville-Grants Pass zone (m)", 6801: "NAD83(CORS96) / Oregon Canyonville-Grants Pass zone (ft)", 6802: "NAD83(2011) / Oregon Canyonville-Grants Pass zone (m)", 6803: "NAD83(2011) / Oregon Canyonville-Grants Pass zone (ft)", 6804: "NAD83(CORS96) / Oregon Columbia River East zone (m)", 6805: "NAD83(CORS96) / Oregon Columbia River East zone (ft)", 6806: "NAD83(2011) / Oregon Columbia River East zone (m)", 6807: "NAD83(2011) / Oregon Columbia River East zone (ft)", 6808: "NAD83(CORS96) / Oregon Columbia River West zone (m)", 6809: "NAD83(CORS96) / Oregon Columbia River West zone (ft)", 6810: "NAD83(2011) / Oregon Columbia River West zone (m)", 6811: "NAD83(2011) / Oregon Columbia River West zone (ft)", 6812: "NAD83(CORS96) / Oregon Cottage Grove-Canyonville zone (m)", 6813: "NAD83(CORS96) / Oregon Cottage Grove-Canyonville zone (ft)", 6814: "NAD83(2011) / Oregon Cottage Grove-Canyonville zone (m)", 6815: "NAD83(2011) / Oregon Cottage Grove-Canyonville zone (ft)", 6816: "NAD83(CORS96) / Oregon Dufur-Madras zone (m)", 6817: "NAD83(CORS96) / Oregon Dufur-Madras zone (ft)", 6818: "NAD83(2011) / Oregon Dufur-Madras zone (m)", 6819: "NAD83(2011) / Oregon Dufur-Madras zone (ft)", 6820: "NAD83(CORS96) / Oregon Eugene zone (m)", 6821: "NAD83(CORS96) / Oregon Eugene zone (ft)", 6822: "NAD83(2011) / Oregon Eugene zone (m)", 6823: "NAD83(2011) / Oregon Eugene zone (ft)", 6824: "NAD83(CORS96) / Oregon Grants Pass-Ashland zone (m)", 6825: "NAD83(CORS96) / Oregon Grants Pass-Ashland zone (ft)", 6826: "NAD83(2011) / Oregon Grants Pass-Ashland zone (m)", 6827: "NAD83(2011) / Oregon Grants Pass-Ashland zone (ft)", 6828: "NAD83(CORS96) / Oregon Gresham-Warm Springs zone (m)", 6829: "NAD83(CORS96) / Oregon Gresham-Warm Springs zone (ft)", 6830: "NAD83(2011) / Oregon Gresham-Warm Springs zone (m)", 6831: "NAD83(2011) / Oregon Gresham-Warm Springs zone (ft)", 6832: "NAD83(CORS96) / Oregon La Grande zone (m)", 6833: "NAD83(CORS96) / Oregon La Grande zone (ft)", 6834: "NAD83(2011) / Oregon La Grande zone (m)", 6835: "NAD83(2011) / Oregon La Grande zone (ft)", 6836: "NAD83(CORS96) / Oregon Ontario zone (m)", 6837: "NAD83(CORS96) / Oregon Ontario zone (ft)", 6838: "NAD83(2011) / Oregon Ontario zone (m)", 6839: "NAD83(2011) / Oregon Ontario zone (ft)", 6840: "NAD83(CORS96) / Oregon Coast zone (m)", 6841: "NAD83(CORS96) / Oregon Coast zone (ft)", 6842: "NAD83(2011) / Oregon Coast zone (m)", 6843: "NAD83(2011) / Oregon Coast zone (ft)", 6844: "NAD83(CORS96) / Oregon Pendleton zone (m)", 6845: "NAD83(CORS96) / Oregon Pendleton zone (ft)", 6846: "NAD83(2011) / Oregon Pendleton zone (m)", 6847: "NAD83(2011) / Oregon Pendleton zone (ft)", 6848: "NAD83(CORS96) / Oregon Pendleton-La Grande zone (m)", 6849: "NAD83(CORS96) / Oregon Pendleton-La Grande zone (ft)", 6850: "NAD83(2011) / Oregon Pendleton-La Grande zone (m)", 6851: "NAD83(2011) / Oregon Pendleton-La Grande zone (ft)", 6852: "NAD83(CORS96) / Oregon Portland zone (m)", 6853: "NAD83(CORS96) / Oregon Portland zone (ft)", 6854: "NAD83(2011) / Oregon Portland zone (m)", 6855: "NAD83(2011) / Oregon Portland zone (ft)", 6856: "NAD83(CORS96) / Oregon Salem zone (m)", 6857: "NAD83(CORS96) / Oregon Salem zone (ft)", 6858: "NAD83(2011) / Oregon Salem zone (m)", 6859: "NAD83(2011) / Oregon Salem zone (ft)", 6860: "NAD83(CORS96) / Oregon Santiam Pass zone (m)", 6861: "NAD83(CORS96) / Oregon Santiam Pass zone (ft)", 6862: "NAD83(2011) / Oregon Santiam Pass zone (m)", 6863: "NAD83(2011) / Oregon Santiam Pass (ft)", 6867: "NAD83(CORS96) / Oregon LCC (m)", 6868: "NAD83(CORS96) / Oregon GIC Lambert (ft)", 6870: "ETRS89 / Albania TM 2010", 6875: "RDN2008 / Italy zone", 6876: "RDN2008 / Zone 12", 6879: "NAD83(2011) / Wisconsin Central", 6880: "NAD83(2011) / Nebraska (ftUS)", 6884: "NAD83(CORS96) / Oregon North", 6885: "NAD83(CORS96) / Oregon North (ft)", 6886: "NAD83(CORS96) / Oregon South", 6887: "NAD83(CORS96) / Oregon South (ft)", 6915: "South East Island 1943 / UTM zone 40N", 6922: "NAD83 / Kansas LCC", 6923: "NAD83 / Kansas LCC (ftUS)", 6924: "NAD83(2011) / Kansas LCC", 6925: "NAD83(2011) / Kansas LCC (ftUS)", 6931: "WGS 84 / NSIDC EASE-Grid 2.0 North", 6932: "WGS 84 / NSIDC EASE-Grid 2.0 South", 6933: "WGS 84 / NSIDC EASE-Grid 2.0 Global", 6956: "VN-2000 / TM-3 zone 481", 6957: "VN-2000 / TM-3 zone 482", 6958: "VN-2000 / TM-3 zone 491", 6959: "VN-2000 / TM-3 Da Nang zone", 6962: "ETRS89 / Albania LCC 2010", 6966: "NAD27 / Michigan North", 6984: "Israeli Grid 05", 6991: "Israeli Grid 05/12", 6996: "NAD83(2011) / San Francisco CS13", 6997: "NAD83(2011) / San Francisco CS13 (ftUS)", 7005: "Nahrwan 1934 / UTM zone 37N", 7006: "Nahrwan 1934 / UTM zone 38N", 7007: "Nahrwan 1934 / UTM zone 39N", 7057: "NAD83(2011) / IaRCS zone 1", 7058: "NAD83(2011) / IaRCS zone 2", 7059: "NAD83(2011) / IaRCS zone 3", 7060: "NAD83(2011) / IaRCS zone 4", 7061: "NAD83(2011) / IaRCS zone 5", 7062: "NAD83(2011) / IaRCS zone 6", 7063: "NAD83(2011) / IaRCS zone 7", 7064: "NAD83(2011) / IaRCS zone 8", 7065: "NAD83(2011) / IaRCS zone 9", 7066: "NAD83(2011) / IaRCS zone 10", 7067: "NAD83(2011) / IaRCS zone 11", 7068: "NAD83(2011) / IaRCS zone 12", 7069: "NAD83(2011) / IaRCS zone 13", 7070: "NAD83(2011) / IaRCS zone 14", 7074: "RGTAAF07 / UTM zone 37S", 7075: "RGTAAF07 / UTM zone 38S", 7076: "RGTAAF07 / UTM zone 39S", 7077: "RGTAAF07 / UTM zone 40S", 7078: "RGTAAF07 / UTM zone 41S", 7079: "RGTAAF07 / UTM zone 42S", 7080: "RGTAAF07 / UTM zone 43S", 7081: "RGTAAF07 / UTM zone 44S", 7082: "RGTAAF07 / Terre Adelie Polar Stereographic", 7109: "NAD83(2011) / RMTCRS St Mary (m)", 7110: "NAD83(2011) / RMTCRS Blackfeet (m)", 7111: "NAD83(2011) / RMTCRS Milk River (m)", 7112: "NAD83(2011) / RMTCRS Fort Belknap (m)", 7113: "NAD83(2011) / RMTCRS Fort Peck Assiniboine (m)", 7114: "NAD83(2011) / RMTCRS Fort Peck Sioux (m)", 7115: "NAD83(2011) / RMTCRS Crow (m)", 7116: "NAD83(2011) / RMTCRS Bobcat (m)", 7117: "NAD83(2011) / RMTCRS Billings (m)", 7118: "NAD83(2011) / RMTCRS Wind River (m)", 7119: "NAD83(2011) / RMTCRS St Mary (ft)", 7120: "NAD83(2011) / RMTCRS Blackfeet (ft)", 7121: "NAD83(2011) / RMTCRS Milk River (ft)", 7122: "NAD83(2011) / RMTCRS Fort Belknap (ft)", 7123: "NAD83(2011) / RMTCRS Fort Peck Assiniboine (ft)", 7124: "NAD83(2011) / RMTCRS Fort Peck Sioux (ft)", 7125: "NAD83(2011) / RMTCRS Crow (ft)", 7126: "NAD83(2011) / RMTCRS Bobcat (ft)", 7127: "NAD83(2011) / RMTCRS Billings (ft)", 7128: "NAD83(2011) / RMTCRS Wind River (ftUS)", 7131: "NAD83(2011) / San Francisco CS13", 7132: "NAD83(2011) / San Francisco CS13 (ftUS)", 7142: "Palestine 1923 / Palestine Grid modified", 7257: "NAD83(2011) / InGCS Adams (m)", 7258: "NAD83(2011) / InGCS Adams (ftUS)", 7259: "NAD83(2011) / InGCS Allen (m)", 7260: "NAD83(2011) / InGCS Allen (ftUS)", 7261: "NAD83(2011) / InGCS Bartholomew (m)", 7262: "NAD83(2011) / InGCS Bartholomew (ftUS)", 7263: "NAD83(2011) / InGCS Benton (m)", 7264: "NAD83(2011) / InGCS Benton (ftUS)", 7265: "NAD83(2011) / InGCS Blackford-Delaware (m)", 7266: "NAD83(2011) / InGCS Blackford-Delaware (ftUS)", 7267: "NAD83(2011) / InGCS Boone-Hendricks (m)", 7268: "NAD83(2011) / InGCS Boone-Hendricks (ftUS)", 7269: "NAD83(2011) / InGCS Brown (m)", 7270: "NAD83(2011) / InGCS Brown (ftUS)", 7271: "NAD83(2011) / InGCS Carroll (m)", 7272: "NAD83(2011) / InGCS Carroll (ftUS)", 7273: "NAD83(2011) / InGCS Cass (m)", 7274: "NAD83(2011) / InGCS Cass (ftUS)", 7275: "NAD83(2011) / InGCS Clark-Floyd-Scott (m)", 7276: "NAD83(2011) / InGCS Clark-Floyd-Scott (ftUS)", 7277: "NAD83(2011) / InGCS Clay (m)", 7278: "NAD83(2011) / InGCS Clay (ftUS)", 7279: "NAD83(2011) / InGCS Clinton (m)", 7280: "NAD83(2011) / InGCS Clinton (ftUS)", 7281: "NAD83(2011) / InGCS Crawford-Lawrence-Orange (m)", 7282: "NAD83(2011) / InGCS Crawford-Lawrence-Orange (ftUS)", 7283: "NAD83(2011) / InGCS Daviess-Greene (m)", 7284: "NAD83(2011) / InGCS Daviess-Greene (ftUS)", 7285: "NAD83(2011) / InGCS Dearborn-Ohio-Switzerland (m)", 7286: "NAD83(2011) / InGCS Dearborn-Ohio-Switzerland (ftUS)", 7287: "NAD83(2011) / InGCS Decatur-Rush (m)", 7288: "NAD83(2011) / InGCS Decatur-Rush (ftUS)", 7289: "NAD83(2011) / InGCS DeKalb (m)", 7290: "NAD83(2011) / InGCS DeKalb (ftUS)", 7291: "NAD83(2011) / InGCS Dubois-Martin (m)", 7292: "NAD83(2011) / InGCS Dubois-Martin (ftUS)", 7293: "NAD83(2011) / InGCS Elkhart-Kosciusko-Wabash (m)", 7294: "NAD83(2011) / InGCS Elkhart-Kosciusko-Wabash (ftUS)", 7295: "NAD83(2011) / InGCS Fayette-Franklin-Union (m)", 7296: "NAD83(2011) / InGCS Fayette-Franklin-Union (ftUS)", 7297: "NAD83(2011) / InGCS Fountain-Warren (m)", 7298: "NAD83(2011) / InGCS Fountain-Warren (ftUS)", 7299: "NAD83(2011) / InGCS Fulton-Marshall-St. Joseph (m)", 7300: "NAD83(2011) / InGCS Fulton-Marshall-St. Joseph (ftUS)", 7301: "NAD83(2011) / InGCS Gibson (m)", 7302: "NAD83(2011) / InGCS Gibson (ftUS)", 7303: "NAD83(2011) / InGCS Grant (m)", 7304: "NAD83(2011) / InGCS Grant (ftUS)", 7305: "NAD83(2011) / InGCS Hamilton-Tipton (m)", 7306: "NAD83(2011) / InGCS Hamilton-Tipton (ftUS)", 7307: "NAD83(2011) / InGCS Hancock-Madison (m)", 7308: "NAD83(2011) / InGCS Hancock-Madison (ftUS)", 7309: "NAD83(2011) / InGCS Harrison-Washington (m)", 7310: "NAD83(2011) / InGCS Harrison-Washington (ftUS)", 7311: "NAD83(2011) / InGCS Henry (m)", 7312: "NAD83(2011) / InGCS Henry (ftUS)", 7313: "NAD83(2011) / InGCS Howard-Miami (m)", 7314: "NAD83(2011) / InGCS Howard-Miami (ftUS)", 7315: "NAD83(2011) / InGCS Huntington-Whitley (m)", 7316: "NAD83(2011) / InGCS Huntington-Whitley (ftUS)", 7317: "NAD83(2011) / InGCS Jackson (m)", 7318: "NAD83(2011) / InGCS Jackson (ftUS)", 7319: "NAD83(2011) / InGCS Jasper-Porter (m)", 7320: "NAD83(2011) / InGCS Jasper-Porter (ftUS)", 7321: "NAD83(2011) / InGCS Jay (m)", 7322: "NAD83(2011) / InGCS Jay (ftUS)", 7323: "NAD83(2011) / InGCS Jefferson (m)", 7324: "NAD83(2011) / InGCS Jefferson (ftUS)", 7325: "NAD83(2011) / InGCS Jennings (m)", 7326: "NAD83(2011) / InGCS Jennings (ftUS)", 7327: "NAD83(2011) / InGCS Johnson-Marion (m)", 7328: "NAD83(2011) / InGCS Johnson-Marion (ftUS)", 7329: "NAD83(2011) / InGCS Knox (m)", 7330: "NAD83(2011) / InGCS Knox (ftUS)", 7331: "NAD83(2011) / InGCS LaGrange-Noble (m)", 7332: "NAD83(2011) / InGCS LaGrange-Noble (ftUS)", 7333: "NAD83(2011) / InGCS Lake-Newton (m)", 7334: "NAD83(2011) / InGCS Lake-Newton (ftUS)", 7335: "NAD83(2011) / InGCS LaPorte-Pulaski-Starke (m)", 7336: "NAD83(2011) / InGCS LaPorte-Pulaski-Starke (ftUS)", 7337: "NAD83(2011) / InGCS Monroe-Morgan (m)", 7338: "NAD83(2011) / InGCS Monroe-Morgan (ftUS)", 7339: "NAD83(2011) / InGCS Montgomery-Putnam (m)", 7340: "NAD83(2011) / InGCS Montgomery-Putnam (ftUS)", 7341: "NAD83(2011) / InGCS Owen (m)", 7342: "NAD83(2011) / InGCS Owen (ftUS)", 7343: "NAD83(2011) / InGCS Parke-Vermillion (m)", 7344: "NAD83(2011) / InGCS Parke-Vermillion (ftUS)", 7345: "NAD83(2011) / InGCS Perry (m)", 7346: "NAD83(2011) / InGCS Perry (ftUS)", 7347: "NAD83(2011) / InGCS Pike-Warrick (m)", 7348: "NAD83(2011) / InGCS Pike-Warrick (ftUS)", 7349: "NAD83(2011) / InGCS Posey (m)", 7350: "NAD83(2011) / InGCS Posey (ftUS)", 7351: "NAD83(2011) / InGCS Randolph-Wayne (m)", 7352: "NAD83(2011) / InGCS Randolph-Wayne (ftUS)", 7353: "NAD83(2011) / InGCS Ripley (m)", 7354: "NAD83(2011) / InGCS Ripley (ftUS)", 7355: "NAD83(2011) / InGCS Shelby (m)", 7356: "NAD83(2011) / InGCS Shelby (ftUS)", 7357: "NAD83(2011) / InGCS Spencer (m)", 7358: "NAD83(2011) / InGCS Spencer (ftUS)", 7359: "NAD83(2011) / InGCS Steuben (m)", 7360: "NAD83(2011) / InGCS Steuben (ftUS)", 7361: "NAD83(2011) / InGCS Sullivan (m)", 7362: "NAD83(2011) / InGCS Sullivan (ftUS)", 7363: "NAD83(2011) / InGCS Tippecanoe-White (m)", 7364: "NAD83(2011) / InGCS Tippecanoe-White (ftUS)", 7365: "NAD83(2011) / InGCS Vanderburgh (m)", 7366: "NAD83(2011) / InGCS Vanderburgh (ftUS)", 7367: "NAD83(2011) / InGCS Vigo (m)", 7368: "NAD83(2011) / InGCS Vigo (ftUS)", 7369: "NAD83(2011) / InGCS Wells (m)", 7370: "NAD83(2011) / InGCS Wells (ftUS)", 7374: "ONGD14 / UTM zone 39N", 7375: "ONGD14 / UTM zone 40N", 7376: "ONGD14 / UTM zone 41N", 7528: "NAD83(2011) / WISCRS Adams and Juneau (m)", 7529: "NAD83(2011) / WISCRS Ashland (m)", 7530: "NAD83(2011) / WISCRS Barron (m)", 7531: "NAD83(2011) / WISCRS Bayfield (m)", 7532: "NAD83(2011) / WISCRS Brown (m)", 7533: "NAD83(2011) / WISCRS Buffalo (m)", 7534: "NAD83(2011) / WISCRS Burnett (m)", 7535: "NAD83(2011) / WISCRS Calumet, Fond du Lac, Outagamie and Winnebago (m)", 7536: "NAD83(2011) / WISCRS Chippewa (m)", 7537: "NAD83(2011) / WISCRS Clark (m)", 7538: "NAD83(2011) / WISCRS Columbia (m)", 7539: "NAD83(2011) / WISCRS Crawford (m)", 7540: "NAD83(2011) / WISCRS Dane (m)", 7541: "NAD83(2011) / WISCRS Dodge and Jefferson (m)", 7542: "NAD83(2011) / WISCRS Door (m)", 7543: "NAD83(2011) / WISCRS Douglas (m)", 7544: "NAD83(2011) / WISCRS Dunn (m)", 7545: "NAD83(2011) / WISCRS Eau Claire (m)", 7546: "NAD83(2011) / WISCRS Florence (m)", 7547: "NAD83(2011) / WISCRS Forest (m)", 7548: "NAD83(2011) / WISCRS Grant (m)", 7549: "NAD83(2011) / WISCRS Green and Lafayette (m)", 7550: "NAD83(2011) / WISCRS Green Lake and Marquette (m)", 7551: "NAD83(2011) / WISCRS Iowa (m)", 7552: "NAD83(2011) / WISCRS Iron (m)", 7553: "NAD83(2011) / WISCRS Jackson (m)", 7554: "NAD83(2011) / WISCRS Kenosha, Milwaukee, Ozaukee and Racine (m)", 7555: "NAD83(2011) / WISCRS Kewaunee, Manitowoc and Sheboygan (m)", 7556: "NAD83(2011) / WISCRS La Crosse (m)", 7557: "NAD83(2011) / WISCRS Langlade (m)", 7558: "NAD83(2011) / WISCRS Lincoln (m)", 7559: "NAD83(2011) / WISCRS Marathon (m)", 7560: "NAD83(2011) / WISCRS Marinette (m)", 7561: "NAD83(2011) / WISCRS Menominee (m)", 7562: "NAD83(2011) / WISCRS Monroe (m)", 7563: "NAD83(2011) / WISCRS Oconto (m)", 7564: "NAD83(2011) / WISCRS Oneida (m)", 7565: "NAD83(2011) / WISCRS Pepin and Pierce (m)", 7566: "NAD83(2011) / WISCRS Polk (m)", 7567: "NAD83(2011) / WISCRS Portage (m)", 7568: "NAD83(2011) / WISCRS Price (m)", 7569: "NAD83(2011) / WISCRS Richland (m)", 7570: "NAD83(2011) / WISCRS Rock (m)", 7571: "NAD83(2011) / WISCRS Rusk (m)", 7572: "NAD83(2011) / WISCRS Sauk (m)", 7573: "NAD83(2011) / WISCRS Sawyer (m)", 7574: "NAD83(2011) / WISCRS Shawano (m)", 7575: "NAD83(2011) / WISCRS St. Croix (m)", 7576: "NAD83(2011) / WISCRS Taylor (m)", 7577: "NAD83(2011) / WISCRS Trempealeau (m)", 7578: "NAD83(2011) / WISCRS Vernon (m)", 7579: "NAD83(2011) / WISCRS Vilas (m)", 7580: "NAD83(2011) / WISCRS Walworth (m)", 7581: "NAD83(2011) / WISCRS Washburn (m)", 7582: "NAD83(2011) / WISCRS Washington (m)", 7583: "NAD83(2011) / WISCRS Waukesha (m)", 7584: "NAD83(2011) / WISCRS Waupaca (m)", 7585: "NAD83(2011) / WISCRS Waushara (m)", 7586: "NAD83(2011) / WISCRS Wood (m)", 7587: "NAD83(2011) / WISCRS Adams and Juneau (ftUS)", 7588: "NAD83(2011) / WISCRS Ashland (ftUS)", 7589: "NAD83(2011) / WISCRS Barron (ftUS)", 7590: "NAD83(2011) / WISCRS Bayfield (ftUS)", 7591: "NAD83(2011) / WISCRS Brown (ftUS)", 7592: "NAD83(2011) / WISCRS Buffalo (ftUS)", 7593: "NAD83(2011) / WISCRS Burnett (ftUS)", 7594: "NAD83(2011) / WISCRS Calumet, Fond du Lac, Outagamie and Winnebago (ftUS)", 7595: "NAD83(2011) / WISCRS Chippewa (ftUS)", 7596: "NAD83(2011) / WISCRS Clark (ftUS)", 7597: "NAD83(2011) / WISCRS Columbia (ftUS)", 7598: "NAD83(2011) / WISCRS Crawford (ftUS)", 7599: "NAD83(2011) / WISCRS Dane (ftUS)", 7600: "NAD83(2011) / WISCRS Dodge and Jefferson (ftUS)", 7601: "NAD83(2011) / WISCRS Door (ftUS)", 7602: "NAD83(2011) / WISCRS Douglas (ftUS)", 7603: "NAD83(2011) / WISCRS Dunn (ftUS)", 7604: "NAD83(2011) / WISCRS Eau Claire (ftUS)", 7605: "NAD83(2011) / WISCRS Florence (ftUS)", 7606: "NAD83(2011) / WISCRS Forest (ftUS)", 7607: "NAD83(2011) / WISCRS Grant (ftUS)", 7608: "NAD83(2011) / WISCRS Green and Lafayette (ftUS)", 7609: "NAD83(2011) / WISCRS Green Lake and Marquette (ftUS)", 7610: "NAD83(2011) / WISCRS Iowa (ftUS)", 7611: "NAD83(2011) / WISCRS Iron (ftUS)", 7612: "NAD83(2011) / WISCRS Jackson (ftUS)", 7613: "NAD83(2011) / WISCRS Kenosha, Milwaukee, Ozaukee and Racine (ftUS)", 7614: "NAD83(2011) / WISCRS Kewaunee, Manitowoc and Sheboygan (ftUS)", 7615: "NAD83(2011) / WISCRS La Crosse (ftUS)", 7616: "NAD83(2011) / WISCRS Langlade (ftUS)", 7617: "NAD83(2011) / WISCRS Lincoln (ftUS)", 7618: "NAD83(2011) / WISCRS Marathon (ftUS)", 7619: "NAD83(2011) / WISCRS Marinette (ftUS)", 7620: "NAD83(2011) / WISCRS Menominee (ftUS)", 7621: "NAD83(2011) / WISCRS Monroe (ftUS)", 7622: "NAD83(2011) / WISCRS Oconto (ftUS)", 7623: "NAD83(2011) / WISCRS Oneida (ftUS)", 7624: "NAD83(2011) / WISCRS Pepin and Pierce (ftUS)", 7625: "NAD83(2011) / WISCRS Polk (ftUS)", 7626: "NAD83(2011) / WISCRS Portage (ftUS)", 7627: "NAD83(2011) / WISCRS Price (ftUS)", 7628: "NAD83(2011) / WISCRS Richland (ftUS)", 7629: "NAD83(2011) / WISCRS Rock (ftUS)", 7630: "NAD83(2011) / WISCRS Rusk (ftUS)", 7631: "NAD83(2011) / WISCRS Sauk (ftUS)", 7632: "NAD83(2011) / WISCRS Sawyer (ftUS)", 7633: "NAD83(2011) / WISCRS Shawano (ftUS)", 7634: "NAD83(2011) / WISCRS St. Croix (ftUS)", 7635: "NAD83(2011) / WISCRS Taylor (ftUS)", 7636: "NAD83(2011) / WISCRS Trempealeau (ftUS)", 7637: "NAD83(2011) / WISCRS Vernon (ftUS)", 7638: "NAD83(2011) / WISCRS Vilas (ftUS)", 7639: "NAD83(2011) / WISCRS Walworth (ftUS)", 7640: "NAD83(2011) / WISCRS Washburn (ftUS)", 7641: "NAD83(2011) / WISCRS Washington (ftUS)", 7642: "NAD83(2011) / WISCRS Waukesha (ftUS)", 7643: "NAD83(2011) / WISCRS Waupaca (ftUS)", 7644: "NAD83(2011) / WISCRS Waushara (ftUS)", 7645: "NAD83(2011) / WISCRS Wood (ftUS)", 7692: "Kyrg-06 / zone 1", 7693: "Kyrg-06 / zone 2", 7694: "Kyrg-06 / zone 3", 7695: "Kyrg-06 / zone 4", 7696: "Kyrg-06 / zone 5", 20004: "Pulkovo 1995 / Gauss-Kruger zone 4", 20005: "Pulkovo 1995 / Gauss-Kruger zone 5", 20006: "Pulkovo 1995 / Gauss-Kruger zone 6", 20007: "Pulkovo 1995 / Gauss-Kruger zone 7", 20008: "Pulkovo 1995 / Gauss-Kruger zone 8", 20009: "Pulkovo 1995 / Gauss-Kruger zone 9", 20010: "Pulkovo 1995 / Gauss-Kruger zone 10", 20011: "Pulkovo 1995 / Gauss-Kruger zone 11", 20012: "Pulkovo 1995 / Gauss-Kruger zone 12", 20013: "Pulkovo 1995 / Gauss-Kruger zone 13", 20014: "Pulkovo 1995 / Gauss-Kruger zone 14", 20015: "Pulkovo 1995 / Gauss-Kruger zone 15", 20016: "Pulkovo 1995 / Gauss-Kruger zone 16", 20017: "Pulkovo 1995 / Gauss-Kruger zone 17", 20018: "Pulkovo 1995 / Gauss-Kruger zone 18", 20019: "Pulkovo 1995 / Gauss-Kruger zone 19", 20020: "Pulkovo 1995 / Gauss-Kruger zone 20", 20021: "Pulkovo 1995 / Gauss-Kruger zone 21", 20022: "Pulkovo 1995 / Gauss-Kruger zone 22", 20023: "Pulkovo 1995 / Gauss-Kruger zone 23", 20024: "Pulkovo 1995 / Gauss-Kruger zone 24", 20025: "Pulkovo 1995 / Gauss-Kruger zone 25", 20026: "Pulkovo 1995 / Gauss-Kruger zone 26", 20027: "Pulkovo 1995 / Gauss-Kruger zone 27", 20028: "Pulkovo 1995 / Gauss-Kruger zone 28", 20029: "Pulkovo 1995 / Gauss-Kruger zone 29", 20030: "Pulkovo 1995 / Gauss-Kruger zone 30", 20031: "Pulkovo 1995 / Gauss-Kruger zone 31", 20032: "Pulkovo 1995 / Gauss-Kruger zone 32", 20064: "Pulkovo 1995 / Gauss-Kruger 4N", 20065: "Pulkovo 1995 / Gauss-Kruger 5N", 20066: "Pulkovo 1995 / Gauss-Kruger 6N", 20067: "Pulkovo 1995 / Gauss-Kruger 7N", 20068: "Pulkovo 1995 / Gauss-Kruger 8N", 20069: "Pulkovo 1995 / Gauss-Kruger 9N", 20070: "Pulkovo 1995 / Gauss-Kruger 10N", 20071: "Pulkovo 1995 / Gauss-Kruger 11N", 20072: "Pulkovo 1995 / Gauss-Kruger 12N", 20073: "Pulkovo 1995 / Gauss-Kruger 13N", 20074: "Pulkovo 1995 / Gauss-Kruger 14N", 20075: "Pulkovo 1995 / Gauss-Kruger 15N", 20076: "Pulkovo 1995 / Gauss-Kruger 16N", 20077: "Pulkovo 1995 / Gauss-Kruger 17N", 20078: "Pulkovo 1995 / Gauss-Kruger 18N", 20079: "Pulkovo 1995 / Gauss-Kruger 19N", 20080: "Pulkovo 1995 / Gauss-Kruger 20N", 20081: "Pulkovo 1995 / Gauss-Kruger 21N", 20082: "Pulkovo 1995 / Gauss-Kruger 22N", 20083: "Pulkovo 1995 / Gauss-Kruger 23N", 20084: "Pulkovo 1995 / Gauss-Kruger 24N", 20085: "Pulkovo 1995 / Gauss-Kruger 25N", 20086: "Pulkovo 1995 / Gauss-Kruger 26N", 20087: "Pulkovo 1995 / Gauss-Kruger 27N", 20088: "Pulkovo 1995 / Gauss-Kruger 28N", 20089: "Pulkovo 1995 / Gauss-Kruger 29N", 20090: "Pulkovo 1995 / Gauss-Kruger 30N", 20091: "Pulkovo 1995 / Gauss-Kruger 31N", 20092: "Pulkovo 1995 / Gauss-Kruger 32N", 20135: "Adindan / UTM zone 35N", 20136: "Adindan / UTM zone 36N", 20137: "Adindan / UTM zone 37N", 20138: "Adindan / UTM zone 38N", 20248: "AGD66 / AMG zone 48", 20249: "AGD66 / AMG zone 49", 20250: "AGD66 / AMG zone 50", 20251: "AGD66 / AMG zone 51", 20252: "AGD66 / AMG zone 52", 20253: "AGD66 / AMG zone 53", 20254: "AGD66 / AMG zone 54", 20255: "AGD66 / AMG zone 55", 20256: "AGD66 / AMG zone 56", 20257: "AGD66 / AMG zone 57", 20258: "AGD66 / AMG zone 58", 20348: "AGD84 / AMG zone 48", 20349: "AGD84 / AMG zone 49", 20350: "AGD84 / AMG zone 50", 20351: "AGD84 / AMG zone 51", 20352: "AGD84 / AMG zone 52", 20353: "AGD84 / AMG zone 53", 20354: "AGD84 / AMG zone 54", 20355: "AGD84 / AMG zone 55", 20356: "AGD84 / AMG zone 56", 20357: "AGD84 / AMG zone 57", 20358: "AGD84 / AMG zone 58", 20436: "Ain el Abd / UTM zone 36N", 20437: "Ain el Abd / UTM zone 37N", 20438: "Ain el Abd / UTM zone 38N", 20439: "Ain el Abd / UTM zone 39N", 20440: "Ain el Abd / UTM zone 40N", 20499: "Ain el Abd / Bahrain Grid", 20538: "Afgooye / UTM zone 38N", 20539: "Afgooye / UTM zone 39N", 20790: "Lisbon (Lisbon) / Portuguese National Grid", 20791: "Lisbon (Lisbon) / Portuguese Grid", 20822: "Aratu / UTM zone 22S", 20823: "Aratu / UTM zone 23S", 20824: "Aratu / UTM zone 24S", 20934: "Arc 1950 / UTM zone 34S", 20935: "Arc 1950 / UTM zone 35S", 20936: "Arc 1950 / UTM zone 36S", 21035: "Arc 1960 / UTM zone 35S", 21036: "Arc 1960 / UTM zone 36S", 21037: "Arc 1960 / UTM zone 37S", 21095: "Arc 1960 / UTM zone 35N", 21096: "Arc 1960 / UTM zone 36N", 21097: "Arc 1960 / UTM zone 37N", 21100: "Batavia (Jakarta) / NEIEZ", 21148: "Batavia / UTM zone 48S", 21149: "Batavia / UTM zone 49S", 21150: "Batavia / UTM zone 50S", 21291: "Barbados 1938 / British West Indies Grid", 21292: "Barbados 1938 / Barbados National Grid", 21413: "Beijing 1954 / Gauss-Kruger zone 13", 21414: "Beijing 1954 / Gauss-Kruger zone 14", 21415: "Beijing 1954 / Gauss-Kruger zone 15", 21416: "Beijing 1954 / Gauss-Kruger zone 16", 21417: "Beijing 1954 / Gauss-Kruger zone 17", 21418: "Beijing 1954 / Gauss-Kruger zone 18", 21419: "Beijing 1954 / Gauss-Kruger zone 19", 21420: "Beijing 1954 / Gauss-Kruger zone 20", 21421: "Beijing 1954 / Gauss-Kruger zone 21", 21422: "Beijing 1954 / Gauss-Kruger zone 22", 21423: "Beijing 1954 / Gauss-Kruger zone 23", 21453: "Beijing 1954 / Gauss-Kruger CM 75E", 21454: "Beijing 1954 / Gauss-Kruger CM 81E", 21455: "Beijing 1954 / Gauss-Kruger CM 87E", 21456: "Beijing 1954 / Gauss-Kruger CM 93E", 21457: "Beijing 1954 / Gauss-Kruger CM 99E", 21458: "Beijing 1954 / Gauss-Kruger CM 105E", 21459: "Beijing 1954 / Gauss-Kruger CM 111E", 21460: "Beijing 1954 / Gauss-Kruger CM 117E", 21461: "Beijing 1954 / Gauss-Kruger CM 123E", 21462: "Beijing 1954 / Gauss-Kruger CM 129E", 21463: "Beijing 1954 / Gauss-Kruger CM 135E", 21473: "Beijing 1954 / Gauss-Kruger 13N", 21474: "Beijing 1954 / Gauss-Kruger 14N", 21475: "Beijing 1954 / Gauss-Kruger 15N", 21476: "Beijing 1954 / Gauss-Kruger 16N", 21477: "Beijing 1954 / Gauss-Kruger 17N", 21478: "Beijing 1954 / Gauss-Kruger 18N", 21479: "Beijing 1954 / Gauss-Kruger 19N", 21480: "Beijing 1954 / Gauss-Kruger 20N", 21481: "Beijing 1954 / Gauss-Kruger 21N", 21482: "Beijing 1954 / Gauss-Kruger 22N", 21483: "Beijing 1954 / Gauss-Kruger 23N", 21500: "Belge 1950 (Brussels) / Belge Lambert 50", 21780: "Bern 1898 (Bern) / LV03C", 21781: "CH1903 / LV03", 21782: "CH1903 / LV03C-G", 21817: "Bogota 1975 / UTM zone 17N", 21818: "Bogota 1975 / UTM zone 18N", 21891: "Bogota 1975 / Colombia West zone", 21892: "Bogota 1975 / Colombia Bogota zone", 21893: "Bogota 1975 / Colombia East Central zone", 21894: "Bogota 1975 / Colombia East", 21896: "Bogota 1975 / Colombia West zone", 21897: "Bogota 1975 / Colombia Bogota zone", 21898: "Bogota 1975 / Colombia East Central zone", 21899: "Bogota 1975 / Colombia East", 22032: "Camacupa / UTM zone 32S", 22033: "Camacupa / UTM zone 33S", 22091: "Camacupa / TM 11.30 SE", 22092: "Camacupa / TM 12 SE", 22171: "POSGAR 98 / Argentina 1", 22172: "POSGAR 98 / Argentina 2", 22173: "POSGAR 98 / Argentina 3", 22174: "POSGAR 98 / Argentina 4", 22175: "POSGAR 98 / Argentina 5", 22176: "POSGAR 98 / Argentina 6", 22177: "POSGAR 98 / Argentina 7", 22181: "POSGAR 94 / Argentina 1", 22182: "POSGAR 94 / Argentina 2", 22183: "POSGAR 94 / Argentina 3", 22184: "POSGAR 94 / Argentina 4", 22185: "POSGAR 94 / Argentina 5", 22186: "POSGAR 94 / Argentina 6", 22187: "POSGAR 94 / Argentina 7", 22191: "Campo Inchauspe / Argentina 1", 22192: "Campo Inchauspe / Argentina 2", 22193: "Campo Inchauspe / Argentina 3", 22194: "Campo Inchauspe / Argentina 4", 22195: "Campo Inchauspe / Argentina 5", 22196: "Campo Inchauspe / Argentina 6", 22197: "Campo Inchauspe / Argentina 7", 22234: "Cape / UTM zone 34S", 22235: "Cape / UTM zone 35S", 22236: "Cape / UTM zone 36S", 22275: "Cape / Lo15", 22277: "Cape / Lo17", 22279: "Cape / Lo19", 22281: "Cape / Lo21", 22283: "Cape / Lo23", 22285: "Cape / Lo25", 22287: "Cape / Lo27", 22289: "Cape / Lo29", 22291: "Cape / Lo31", 22293: "Cape / Lo33", 22300: "Carthage (Paris) / Tunisia Mining Grid", 22332: "Carthage / UTM zone 32N", 22391: "Carthage / Nord Tunisie", 22392: "Carthage / Sud Tunisie", 22521: "Corrego Alegre 1970-72 / UTM zone 21S", 22522: "Corrego Alegre 1970-72 / UTM zone 22S", 22523: "Corrego Alegre 1970-72 / UTM zone 23S", 22524: "Corrego Alegre 1970-72 / UTM zone 24S", 22525: "Corrego Alegre 1970-72 / UTM zone 25S", 22700: "Deir ez Zor / Levant Zone", 22770: "Deir ez Zor / Syria Lambert", 22780: "Deir ez Zor / Levant Stereographic", 22832: "Douala / UTM zone 32N", 22991: "Egypt 1907 / Blue Belt", 22992: "Egypt 1907 / Red Belt", 22993: "Egypt 1907 / Purple Belt", 22994: "Egypt 1907 / Extended Purple Belt", 23028: "ED50 / UTM zone 28N", 23029: "ED50 / UTM zone 29N", 23030: "ED50 / UTM zone 30N", 23031: "ED50 / UTM zone 31N", 23032: "ED50 / UTM zone 32N", 23033: "ED50 / UTM zone 33N", 23034: "ED50 / UTM zone 34N", 23035: "ED50 / UTM zone 35N", 23036: "ED50 / UTM zone 36N", 23037: "ED50 / UTM zone 37N", 23038: "ED50 / UTM zone 38N", 23090: "ED50 / TM 0 N", 23095: "ED50 / TM 5 NE", 23239: "Fahud / UTM zone 39N", 23240: "Fahud / UTM zone 40N", 23433: "Garoua / UTM zone 33N", 23700: "HD72 / EOV", 23830: "DGN95 / Indonesia TM-3 zone 46.2", 23831: "DGN95 / Indonesia TM-3 zone 47.1", 23832: "DGN95 / Indonesia TM-3 zone 47.2", 23833: "DGN95 / Indonesia TM-3 zone 48.1", 23834: "DGN95 / Indonesia TM-3 zone 48.2", 23835: "DGN95 / Indonesia TM-3 zone 49.1", 23836: "DGN95 / Indonesia TM-3 zone 49.2", 23837: "DGN95 / Indonesia TM-3 zone 50.1", 23838: "DGN95 / Indonesia TM-3 zone 50.2", 23839: "DGN95 / Indonesia TM-3 zone 51.1", 23840: "DGN95 / Indonesia TM-3 zone 51.2", 23841: "DGN95 / Indonesia TM-3 zone 52.1", 23842: "DGN95 / Indonesia TM-3 zone 52.2", 23843: "DGN95 / Indonesia TM-3 zone 53.1", 23844: "DGN95 / Indonesia TM-3 zone 53.2", 23845: "DGN95 / Indonesia TM-3 zone 54.1", 23846: "ID74 / UTM zone 46N", 23847: "ID74 / UTM zone 47N", 23848: "ID74 / UTM zone 48N", 23849: "ID74 / UTM zone 49N", 23850: "ID74 / UTM zone 50N", 23851: "ID74 / UTM zone 51N", 23852: "ID74 / UTM zone 52N", 23853: "ID74 / UTM zone 53N", 23866: "DGN95 / UTM zone 46N", 23867: "DGN95 / UTM zone 47N", 23868: "DGN95 / UTM zone 48N", 23869: "DGN95 / UTM zone 49N", 23870: "DGN95 / UTM zone 50N", 23871: "DGN95 / UTM zone 51N", 23872: "DGN95 / UTM zone 52N", 23877: "DGN95 / UTM zone 47S", 23878: "DGN95 / UTM zone 48S", 23879: "DGN95 / UTM zone 49S", 23880: "DGN95 / UTM zone 50S", 23881: "DGN95 / UTM zone 51S", 23882: "DGN95 / UTM zone 52S", 23883: "DGN95 / UTM zone 53S", 23884: "DGN95 / UTM zone 54S", 23886: "ID74 / UTM zone 46S", 23887: "ID74 / UTM zone 47S", 23888: "ID74 / UTM zone 48S", 23889: "ID74 / UTM zone 49S", 23890: "ID74 / UTM zone 50S", 23891: "ID74 / UTM zone 51S", 23892: "ID74 / UTM zone 52S", 23893: "ID74 / UTM zone 53S", 23894: "ID74 / UTM zone 54S", 23946: "Indian 1954 / UTM zone 46N", 23947: "Indian 1954 / UTM zone 47N", 23948: "Indian 1954 / UTM zone 48N", 24047: "Indian 1975 / UTM zone 47N", 24048: "Indian 1975 / UTM zone 48N", 24100: "Jamaica 1875 / Jamaica (Old Grid)", 24200: "JAD69 / Jamaica National Grid", 24305: "Kalianpur 1937 / UTM zone 45N", 24306: "Kalianpur 1937 / UTM zone 46N", 24311: "Kalianpur 1962 / UTM zone 41N", 24312: "Kalianpur 1962 / UTM zone 42N", 24313: "Kalianpur 1962 / UTM zone 43N", 24342: "Kalianpur 1975 / UTM zone 42N", 24343: "Kalianpur 1975 / UTM zone 43N", 24344: "Kalianpur 1975 / UTM zone 44N", 24345: "Kalianpur 1975 / UTM zone 45N", 24346: "Kalianpur 1975 / UTM zone 46N", 24347: "Kalianpur 1975 / UTM zone 47N", 24370: "Kalianpur 1880 / India zone 0", 24371: "Kalianpur 1880 / India zone I", 24372: "Kalianpur 1880 / India zone IIa", 24373: "Kalianpur 1880 / India zone IIIa", 24374: "Kalianpur 1880 / India zone IVa", 24375: "Kalianpur 1937 / India zone IIb", 24376: "Kalianpur 1962 / India zone I", 24377: "Kalianpur 1962 / India zone IIa", 24378: "Kalianpur 1975 / India zone I", 24379: "Kalianpur 1975 / India zone IIa", 24380: "Kalianpur 1975 / India zone IIb", 24381: "Kalianpur 1975 / India zone IIIa", 24382: "Kalianpur 1880 / India zone IIb", 24383: "Kalianpur 1975 / India zone IVa", 24500: "Kertau 1968 / Singapore Grid", 24547: "Kertau 1968 / UTM zone 47N", 24548: "Kertau 1968 / UTM zone 48N", 24571: "Kertau / R.S.O. Malaya (ch)", 24600: "KOC Lambert", 24718: "La Canoa / UTM zone 18N", 24719: "La Canoa / UTM zone 19N", 24720: "La Canoa / UTM zone 20N", 24817: "PSAD56 / UTM zone 17N", 24818: "PSAD56 / UTM zone 18N", 24819: "PSAD56 / UTM zone 19N", 24820: "PSAD56 / UTM zone 20N", 24821: "PSAD56 / UTM zone 21N", 24877: "PSAD56 / UTM zone 17S", 24878: "PSAD56 / UTM zone 18S", 24879: "PSAD56 / UTM zone 19S", 24880: "PSAD56 / UTM zone 20S", 24881: "PSAD56 / UTM zone 21S", 24882: "PSAD56 / UTM zone 22S", 24891: "PSAD56 / Peru west zone", 24892: "PSAD56 / Peru central zone", 24893: "PSAD56 / Peru east zone", 25000: "Leigon / Ghana Metre Grid", 25231: "Lome / UTM zone 31N", 25391: "Luzon 1911 / Philippines zone I", 25392: "Luzon 1911 / Philippines zone II", 25393: "Luzon 1911 / Philippines zone III", 25394: "Luzon 1911 / Philippines zone IV", 25395: "Luzon 1911 / Philippines zone V", 25700: "Makassar (Jakarta) / NEIEZ", 25828: "ETRS89 / UTM zone 28N", 25829: "ETRS89 / UTM zone 29N", 25830: "ETRS89 / UTM zone 30N", 25831: "ETRS89 / UTM zone 31N", 25832: "ETRS89 / UTM zone 32N", 25833: "ETRS89 / UTM zone 33N", 25834: "ETRS89 / UTM zone 34N", 25835: "ETRS89 / UTM zone 35N", 25836: "ETRS89 / UTM zone 36N", 25837: "ETRS89 / UTM zone 37N", 25838: "ETRS89 / UTM zone 38N", 25884: "ETRS89 / TM Baltic93", 25932: "Malongo 1987 / UTM zone 32S", 26191: "Merchich / Nord Maroc", 26192: "Merchich / Sud Maroc", 26193: "Merchich / Sahara", 26194: "Merchich / Sahara Nord", 26195: "Merchich / Sahara Sud", 26237: "Massawa / UTM zone 37N", 26331: "Minna / UTM zone 31N", 26332: "Minna / UTM zone 32N", 26391: "Minna / Nigeria West Belt", 26392: "Minna / Nigeria Mid Belt", 26393: "Minna / Nigeria East Belt", 26432: "Mhast / UTM zone 32S", 26591: "Monte Mario (Rome) / Italy zone 1", 26592: "Monte Mario (Rome) / Italy zone 2", 26632: "M'poraloko / UTM zone 32N", 26692: "M'poraloko / UTM zone 32S", 26701: "NAD27 / UTM zone 1N", 26702: "NAD27 / UTM zone 2N", 26703: "NAD27 / UTM zone 3N", 26704: "NAD27 / UTM zone 4N", 26705: "NAD27 / UTM zone 5N", 26706: "NAD27 / UTM zone 6N", 26707: "NAD27 / UTM zone 7N", 26708: "NAD27 / UTM zone 8N", 26709: "NAD27 / UTM zone 9N", 26710: "NAD27 / UTM zone 10N", 26711: "NAD27 / UTM zone 11N", 26712: "NAD27 / UTM zone 12N", 26713: "NAD27 / UTM zone 13N", 26714: "NAD27 / UTM zone 14N", 26715: "NAD27 / UTM zone 15N", 26716: "NAD27 / UTM zone 16N", 26717: "NAD27 / UTM zone 17N", 26718: "NAD27 / UTM zone 18N", 26719: "NAD27 / UTM zone 19N", 26720: "NAD27 / UTM zone 20N", 26721: "NAD27 / UTM zone 21N", 26722: "NAD27 / UTM zone 22N", 26729: "NAD27 / Alabama East", 26730: "NAD27 / Alabama West", 26731: "NAD27 / Alaska zone 1", 26732: "NAD27 / Alaska zone 2", 26733: "NAD27 / Alaska zone 3", 26734: "NAD27 / Alaska zone 4", 26735: "NAD27 / Alaska zone 5", 26736: "NAD27 / Alaska zone 6", 26737: "NAD27 / Alaska zone 7", 26738: "NAD27 / Alaska zone 8", 26739: "NAD27 / Alaska zone 9", 26740: "NAD27 / Alaska zone 10", 26741: "NAD27 / California zone I", 26742: "NAD27 / California zone II", 26743: "NAD27 / California zone III", 26744: "NAD27 / California zone IV", 26745: "NAD27 / California zone V", 26746: "NAD27 / California zone VI", 26747: "NAD27 / California zone VII", 26748: "NAD27 / Arizona East", 26749: "NAD27 / Arizona Central", 26750: "NAD27 / Arizona West", 26751: "NAD27 / Arkansas North", 26752: "NAD27 / Arkansas South", 26753: "NAD27 / Colorado North", 26754: "NAD27 / Colorado Central", 26755: "NAD27 / Colorado South", 26756: "NAD27 / Connecticut", 26757: "NAD27 / Delaware", 26758: "NAD27 / Florida East", 26759: "NAD27 / Florida West", 26760: "NAD27 / Florida North", 26766: "NAD27 / Georgia East", 26767: "NAD27 / Georgia West", 26768: "NAD27 / Idaho East", 26769: "NAD27 / Idaho Central", 26770: "NAD27 / Idaho West", 26771: "NAD27 / Illinois East", 26772: "NAD27 / Illinois West", 26773: "NAD27 / Indiana East", 26774: "NAD27 / Indiana West", 26775: "NAD27 / Iowa North", 26776: "NAD27 / Iowa South", 26777: "NAD27 / Kansas North", 26778: "NAD27 / Kansas South", 26779: "NAD27 / Kentucky North", 26780: "NAD27 / Kentucky South", 26781: "NAD27 / Louisiana North", 26782: "NAD27 / Louisiana South", 26783: "NAD27 / Maine East", 26784: "NAD27 / Maine West", 26785: "NAD27 / Maryland", 26786: "NAD27 / Massachusetts Mainland", 26787: "NAD27 / Massachusetts Island", 26791: "NAD27 / Minnesota North", 26792: "NAD27 / Minnesota Central", 26793: "NAD27 / Minnesota South", 26794: "NAD27 / Mississippi East", 26795: "NAD27 / Mississippi West", 26796: "NAD27 / Missouri East", 26797: "NAD27 / Missouri Central", 26798: "NAD27 / Missouri West", 26799: "NAD27 / California zone VII", 26801: "NAD Michigan / Michigan East", 26802: "NAD Michigan / Michigan Old Central", 26803: "NAD Michigan / Michigan West", 26811: "NAD Michigan / Michigan North", 26812: "NAD Michigan / Michigan Central", 26813: "NAD Michigan / Michigan South", 26814: "NAD83 / Maine East (ftUS)", 26815: "NAD83 / Maine West (ftUS)", 26819: "NAD83 / Minnesota North (ftUS)", 26820: "NAD83 / Minnesota Central (ftUS)", 26821: "NAD83 / Minnesota South (ftUS)", 26822: "NAD83 / Nebraska (ftUS)", 26823: "NAD83 / West Virginia North (ftUS)", 26824: "NAD83 / West Virginia South (ftUS)", 26825: "NAD83(HARN) / Maine East (ftUS)", 26826: "NAD83(HARN) / Maine West (ftUS)", 26830: "NAD83(HARN) / Minnesota North (ftUS)", 26831: "NAD83(HARN) / Minnesota Central (ftUS)", 26832: "NAD83(HARN) / Minnesota South (ftUS)", 26833: "NAD83(HARN) / Nebraska (ftUS)", 26834: "NAD83(HARN) / West Virginia North (ftUS)", 26835: "NAD83(HARN) / West Virginia South (ftUS)", 26836: "NAD83(NSRS2007) / Maine East (ftUS)", 26837: "NAD83(NSRS2007) / Maine West (ftUS)", 26841: "NAD83(NSRS2007) / Minnesota North (ftUS)", 26842: "NAD83(NSRS2007) / Minnesota Central (ftUS)", 26843: "NAD83(NSRS2007) / Minnesota South (ftUS)", 26844: "NAD83(NSRS2007) / Nebraska (ftUS)", 26845: "NAD83(NSRS2007) / West Virginia North (ftUS)", 26846: "NAD83(NSRS2007) / West Virginia South (ftUS)", 26847: "NAD83 / Maine East (ftUS)", 26848: "NAD83 / Maine West (ftUS)", 26849: "NAD83 / Minnesota North (ftUS)", 26850: "NAD83 / Minnesota Central (ftUS)", 26851: "NAD83 / Minnesota South (ftUS)", 26852: "NAD83 / Nebraska (ftUS)", 26853: "NAD83 / West Virginia North (ftUS)", 26854: "NAD83 / West Virginia South (ftUS)", 26855: "NAD83(HARN) / Maine East (ftUS)", 26856: "NAD83(HARN) / Maine West (ftUS)", 26857: "NAD83(HARN) / Minnesota North (ftUS)", 26858: "NAD83(HARN) / Minnesota Central (ftUS)", 26859: "NAD83(HARN) / Minnesota South (ftUS)", 26860: "NAD83(HARN) / Nebraska (ftUS)", 26861: "NAD83(HARN) / West Virginia North (ftUS)", 26862: "NAD83(HARN) / West Virginia South (ftUS)", 26863: "NAD83(NSRS2007) / Maine East (ftUS)", 26864: "NAD83(NSRS2007) / Maine West (ftUS)", 26865: "NAD83(NSRS2007) / Minnesota North (ftUS)", 26866: "NAD83(NSRS2007) / Minnesota Central (ftUS)", 26867: "NAD83(NSRS2007) / Minnesota South (ftUS)", 26868: "NAD83(NSRS2007) / Nebraska (ftUS)", 26869: "NAD83(NSRS2007) / West Virginia North (ftUS)", 26870: "NAD83(NSRS2007) / West Virginia South (ftUS)", 26891: "NAD83(CSRS) / MTM zone 11", 26892: "NAD83(CSRS) / MTM zone 12", 26893: "NAD83(CSRS) / MTM zone 13", 26894: "NAD83(CSRS) / MTM zone 14", 26895: "NAD83(CSRS) / MTM zone 15", 26896: "NAD83(CSRS) / MTM zone 16", 26897: "NAD83(CSRS) / MTM zone 17", 26898: "NAD83(CSRS) / MTM zone 1", 26899: "NAD83(CSRS) / MTM zone 2", 26901: "NAD83 / UTM zone 1N", 26902: "NAD83 / UTM zone 2N", 26903: "NAD83 / UTM zone 3N", 26904: "NAD83 / UTM zone 4N", 26905: "NAD83 / UTM zone 5N", 26906: "NAD83 / UTM zone 6N", 26907: "NAD83 / UTM zone 7N", 26908: "NAD83 / UTM zone 8N", 26909: "NAD83 / UTM zone 9N", 26910: "NAD83 / UTM zone 10N", 26911: "NAD83 / UTM zone 11N", 26912: "NAD83 / UTM zone 12N", 26913: "NAD83 / UTM zone 13N", 26914: "NAD83 / UTM zone 14N", 26915: "NAD83 / UTM zone 15N", 26916: "NAD83 / UTM zone 16N", 26917: "NAD83 / UTM zone 17N", 26918: "NAD83 / UTM zone 18N", 26919: "NAD83 / UTM zone 19N", 26920: "NAD83 / UTM zone 20N", 26921: "NAD83 / UTM zone 21N", 26922: "NAD83 / UTM zone 22N", 26923: "NAD83 / UTM zone 23N", 26929: "NAD83 / Alabama East", 26930: "NAD83 / Alabama West", 26931: "NAD83 / Alaska zone 1", 26932: "NAD83 / Alaska zone 2", 26933: "NAD83 / Alaska zone 3", 26934: "NAD83 / Alaska zone 4", 26935: "NAD83 / Alaska zone 5", 26936: "NAD83 / Alaska zone 6", 26937: "NAD83 / Alaska zone 7", 26938: "NAD83 / Alaska zone 8", 26939: "NAD83 / Alaska zone 9", 26940: "NAD83 / Alaska zone 10", 26941: "NAD83 / California zone 1", 26942: "NAD83 / California zone 2", 26943: "NAD83 / California zone 3", 26944: "NAD83 / California zone 4", 26945: "NAD83 / California zone 5", 26946: "NAD83 / California zone 6", 26948: "NAD83 / Arizona East", 26949: "NAD83 / Arizona Central", 26950: "NAD83 / Arizona West", 26951: "NAD83 / Arkansas North", 26952: "NAD83 / Arkansas South", 26953: "NAD83 / Colorado North", 26954: "NAD83 / Colorado Central", 26955: "NAD83 / Colorado South", 26956: "NAD83 / Connecticut", 26957: "NAD83 / Delaware", 26958: "NAD83 / Florida East", 26959: "NAD83 / Florida West", 26960: "NAD83 / Florida North", 26961: "NAD83 / Hawaii zone 1", 26962: "NAD83 / Hawaii zone 2", 26963: "NAD83 / Hawaii zone 3", 26964: "NAD83 / Hawaii zone 4", 26965: "NAD83 / Hawaii zone 5", 26966: "NAD83 / Georgia East", 26967: "NAD83 / Georgia West", 26968: "NAD83 / Idaho East", 26969: "NAD83 / Idaho Central", 26970: "NAD83 / Idaho West", 26971: "NAD83 / Illinois East", 26972: "NAD83 / Illinois West", 26973: "NAD83 / Indiana East", 26974: "NAD83 / Indiana West", 26975: "NAD83 / Iowa North", 26976: "NAD83 / Iowa South", 26977: "NAD83 / Kansas North", 26978: "NAD83 / Kansas South", 26979: "NAD83 / Kentucky North", 26980: "NAD83 / Kentucky South", 26981: "NAD83 / Louisiana North", 26982: "NAD83 / Louisiana South", 26983: "NAD83 / Maine East", 26984: "NAD83 / Maine West", 26985: "NAD83 / Maryland", 26986: "NAD83 / Massachusetts Mainland", 26987: "NAD83 / Massachusetts Island", 26988: "NAD83 / Michigan North", 26989: "NAD83 / Michigan Central", 26990: "NAD83 / Michigan South", 26991: "NAD83 / Minnesota North", 26992: "NAD83 / Minnesota Central", 26993: "NAD83 / Minnesota South", 26994: "NAD83 / Mississippi East", 26995: "NAD83 / Mississippi West", 26996: "NAD83 / Missouri East", 26997: "NAD83 / Missouri Central", 26998: "NAD83 / Missouri West", 27037: "Nahrwan 1967 / UTM zone 37N", 27038: "Nahrwan 1967 / UTM zone 38N", 27039: "Nahrwan 1967 / UTM zone 39N", 27040: "Nahrwan 1967 / UTM zone 40N", 27120: "Naparima 1972 / UTM zone 20N", 27200: "NZGD49 / New Zealand Map Grid", 27205: "NZGD49 / Mount Eden Circuit", 27206: "NZGD49 / Bay of Plenty Circuit", 27207: "NZGD49 / Poverty Bay Circuit", 27208: "NZGD49 / Hawkes Bay Circuit", 27209: "NZGD49 / Taranaki Circuit", 27210: "NZGD49 / Tuhirangi Circuit", 27211: "NZGD49 / Wanganui Circuit", 27212: "NZGD49 / Wairarapa Circuit", 27213: "NZGD49 / Wellington Circuit", 27214: "NZGD49 / Collingwood Circuit", 27215: "NZGD49 / Nelson Circuit", 27216: "NZGD49 / Karamea Circuit", 27217: "NZGD49 / Buller Circuit", 27218: "NZGD49 / Grey Circuit", 27219: "NZGD49 / Amuri Circuit", 27220: "NZGD49 / Marlborough Circuit", 27221: "NZGD49 / Hokitika Circuit", 27222: "NZGD49 / Okarito Circuit", 27223: "NZGD49 / Jacksons Bay Circuit", 27224: "NZGD49 / Mount Pleasant Circuit", 27225: "NZGD49 / Gawler Circuit", 27226: "NZGD49 / Timaru Circuit", 27227: "NZGD49 / Lindis Peak Circuit", 27228: "NZGD49 / Mount Nicholas Circuit", 27229: "NZGD49 / Mount York Circuit", 27230: "NZGD49 / Observation Point Circuit", 27231: "NZGD49 / North Taieri Circuit", 27232: "NZGD49 / Bluff Circuit", 27258: "NZGD49 / UTM zone 58S", 27259: "NZGD49 / UTM zone 59S", 27260: "NZGD49 / UTM zone 60S", 27291: "NZGD49 / North Island Grid", 27292: "NZGD49 / South Island Grid", 27391: "NGO 1948 (Oslo) / NGO zone I", 27392: "NGO 1948 (Oslo) / NGO zone II", 27393: "NGO 1948 (Oslo) / NGO zone III", 27394: "NGO 1948 (Oslo) / NGO zone IV", 27395: "NGO 1948 (Oslo) / NGO zone V", 27396: "NGO 1948 (Oslo) / NGO zone VI", 27397: "NGO 1948 (Oslo) / NGO zone VII", 27398: "NGO 1948 (Oslo) / NGO zone VIII", 27429: "Datum 73 / UTM zone 29N", 27492: "Datum 73 / Modified Portuguese Grid", 27493: "Datum 73 / Modified Portuguese Grid", 27500: "ATF (Paris) / Nord de Guerre", 27561: "NTF (Paris) / Lambert Nord France", 27562: "NTF (Paris) / Lambert Centre France", 27563: "NTF (Paris) / Lambert Sud France", 27564: "NTF (Paris) / Lambert Corse", 27571: "NTF (Paris) / Lambert zone I", 27572: "NTF (Paris) / Lambert zone II", 27573: "NTF (Paris) / Lambert zone III", 27574: "NTF (Paris) / Lambert zone IV", 27581: "NTF (Paris) / France I", 27582: "NTF (Paris) / France II", 27583: "NTF (Paris) / France III", 27584: "NTF (Paris) / France IV", 27591: "NTF (Paris) / Nord France", 27592: "NTF (Paris) / Centre France", 27593: "NTF (Paris) / Sud France", 27594: "NTF (Paris) / Corse", 27700: "OSGB 1936 / British National Grid", 28191: "Palestine 1923 / Palestine Grid", 28192: "Palestine 1923 / Palestine Belt", 28193: "Palestine 1923 / Israeli CS Grid", 28232: "Pointe Noire / UTM zone 32S", 28348: "GDA94 / MGA zone 48", 28349: "GDA94 / MGA zone 49", 28350: "GDA94 / MGA zone 50", 28351: "GDA94 / MGA zone 51", 28352: "GDA94 / MGA zone 52", 28353: "GDA94 / MGA zone 53", 28354: "GDA94 / MGA zone 54", 28355: "GDA94 / MGA zone 55", 28356: "GDA94 / MGA zone 56", 28357: "GDA94 / MGA zone 57", 28358: "GDA94 / MGA zone 58", 28402: "Pulkovo 1942 / Gauss-Kruger zone 2", 28403: "Pulkovo 1942 / Gauss-Kruger zone 3", 28404: "Pulkovo 1942 / Gauss-Kruger zone 4", 28405: "Pulkovo 1942 / Gauss-Kruger zone 5", 28406: "Pulkovo 1942 / Gauss-Kruger zone 6", 28407: "Pulkovo 1942 / Gauss-Kruger zone 7", 28408: "Pulkovo 1942 / Gauss-Kruger zone 8", 28409: "Pulkovo 1942 / Gauss-Kruger zone 9", 28410: "Pulkovo 1942 / Gauss-Kruger zone 10", 28411: "Pulkovo 1942 / Gauss-Kruger zone 11", 28412: "Pulkovo 1942 / Gauss-Kruger zone 12", 28413: "Pulkovo 1942 / Gauss-Kruger zone 13", 28414: "Pulkovo 1942 / Gauss-Kruger zone 14", 28415: "Pulkovo 1942 / Gauss-Kruger zone 15", 28416: "Pulkovo 1942 / Gauss-Kruger zone 16", 28417: "Pulkovo 1942 / Gauss-Kruger zone 17", 28418: "Pulkovo 1942 / Gauss-Kruger zone 18", 28419: "Pulkovo 1942 / Gauss-Kruger zone 19", 28420: "Pulkovo 1942 / Gauss-Kruger zone 20", 28421: "Pulkovo 1942 / Gauss-Kruger zone 21", 28422: "Pulkovo 1942 / Gauss-Kruger zone 22", 28423: "Pulkovo 1942 / Gauss-Kruger zone 23", 28424: "Pulkovo 1942 / Gauss-Kruger zone 24", 28425: "Pulkovo 1942 / Gauss-Kruger zone 25", 28426: "Pulkovo 1942 / Gauss-Kruger zone 26", 28427: "Pulkovo 1942 / Gauss-Kruger zone 27", 28428: "Pulkovo 1942 / Gauss-Kruger zone 28", 28429: "Pulkovo 1942 / Gauss-Kruger zone 29", 28430: "Pulkovo 1942 / Gauss-Kruger zone 30", 28431: "Pulkovo 1942 / Gauss-Kruger zone 31", 28432: "Pulkovo 1942 / Gauss-Kruger zone 32", 28462: "Pulkovo 1942 / Gauss-Kruger 2N", 28463: "Pulkovo 1942 / Gauss-Kruger 3N", 28464: "Pulkovo 1942 / Gauss-Kruger 4N", 28465: "Pulkovo 1942 / Gauss-Kruger 5N", 28466: "Pulkovo 1942 / Gauss-Kruger 6N", 28467: "Pulkovo 1942 / Gauss-Kruger 7N", 28468: "Pulkovo 1942 / Gauss-Kruger 8N", 28469: "Pulkovo 1942 / Gauss-Kruger 9N", 28470: "Pulkovo 1942 / Gauss-Kruger 10N", 28471: "Pulkovo 1942 / Gauss-Kruger 11N", 28472: "Pulkovo 1942 / Gauss-Kruger 12N", 28473: "Pulkovo 1942 / Gauss-Kruger 13N", 28474: "Pulkovo 1942 / Gauss-Kruger 14N", 28475: "Pulkovo 1942 / Gauss-Kruger 15N", 28476: "Pulkovo 1942 / Gauss-Kruger 16N", 28477: "Pulkovo 1942 / Gauss-Kruger 17N", 28478: "Pulkovo 1942 / Gauss-Kruger 18N", 28479: "Pulkovo 1942 / Gauss-Kruger 19N", 28480: "Pulkovo 1942 / Gauss-Kruger 20N", 28481: "Pulkovo 1942 / Gauss-Kruger 21N", 28482: "Pulkovo 1942 / Gauss-Kruger 22N", 28483: "Pulkovo 1942 / Gauss-Kruger 23N", 28484: "Pulkovo 1942 / Gauss-Kruger 24N", 28485: "Pulkovo 1942 / Gauss-Kruger 25N", 28486: "Pulkovo 1942 / Gauss-Kruger 26N", 28487: "Pulkovo 1942 / Gauss-Kruger 27N", 28488: "Pulkovo 1942 / Gauss-Kruger 28N", 28489: "Pulkovo 1942 / Gauss-Kruger 29N", 28490: "Pulkovo 1942 / Gauss-Kruger 30N", 28491: "Pulkovo 1942 / Gauss-Kruger 31N", 28492: "Pulkovo 1942 / Gauss-Kruger 32N", 28600: "Qatar 1974 / Qatar National Grid", 28991: "Amersfoort / RD Old", 28992: "Amersfoort / RD New", 29100: "SAD69 / Brazil Polyconic", 29101: "SAD69 / Brazil Polyconic", 29118: "SAD69 / UTM zone 18N", 29119: "SAD69 / UTM zone 19N", 29120: "SAD69 / UTM zone 20N", 29121: "SAD69 / UTM zone 21N", 29122: "SAD69 / UTM zone 22N", 29168: "SAD69 / UTM zone 18N", 29169: "SAD69 / UTM zone 19N", 29170: "SAD69 / UTM zone 20N", 29171: "SAD69 / UTM zone 21N", 29172: "SAD69 / UTM zone 22N", 29177: "SAD69 / UTM zone 17S", 29178: "SAD69 / UTM zone 18S", 29179: "SAD69 / UTM zone 19S", 29180: "SAD69 / UTM zone 20S", 29181: "SAD69 / UTM zone 21S", 29182: "SAD69 / UTM zone 22S", 29183: "SAD69 / UTM zone 23S", 29184: "SAD69 / UTM zone 24S", 29185: "SAD69 / UTM zone 25S", 29187: "SAD69 / UTM zone 17S", 29188: "SAD69 / UTM zone 18S", 29189: "SAD69 / UTM zone 19S", 29190: "SAD69 / UTM zone 20S", 29191: "SAD69 / UTM zone 21S", 29192: "SAD69 / UTM zone 22S", 29193: "SAD69 / UTM zone 23S", 29194: "SAD69 / UTM zone 24S", 29195: "SAD69 / UTM zone 25S", 29220: "Sapper Hill 1943 / UTM zone 20S", 29221: "Sapper Hill 1943 / UTM zone 21S", 29333: "Schwarzeck / UTM zone 33S", 29371: "Schwarzeck / Lo22/11", 29373: "Schwarzeck / Lo22/13", 29375: "Schwarzeck / Lo22/15", 29377: "Schwarzeck / Lo22/17", 29379: "Schwarzeck / Lo22/19", 29381: "Schwarzeck / Lo22/21", 29383: "Schwarzeck / Lo22/23", 29385: "Schwarzeck / Lo22/25", 29635: "Sudan / UTM zone 35N", 29636: "Sudan / UTM zone 36N", 29700: "Tananarive (Paris) / Laborde Grid", 29701: "Tananarive (Paris) / Laborde Grid", 29702: "Tananarive (Paris) / Laborde Grid approximation", 29738: "Tananarive / UTM zone 38S", 29739: "Tananarive / UTM zone 39S", 29849: "Timbalai 1948 / UTM zone 49N", 29850: "Timbalai 1948 / UTM zone 50N", 29871: "Timbalai 1948 / RSO Borneo (ch)", 29872: "Timbalai 1948 / RSO Borneo (ft)", 29873: "Timbalai 1948 / RSO Borneo (m)", 29900: "TM65 / Irish National Grid", 29901: "OSNI 1952 / Irish National Grid", 29902: "TM65 / Irish Grid", 29903: "TM75 / Irish Grid", 30161: "Tokyo / Japan Plane Rectangular CS I", 30162: "Tokyo / Japan Plane Rectangular CS II", 30163: "Tokyo / Japan Plane Rectangular CS III", 30164: "Tokyo / Japan Plane Rectangular CS IV", 30165: "Tokyo / Japan Plane Rectangular CS V", 30166: "Tokyo / Japan Plane Rectangular CS VI", 30167: "Tokyo / Japan Plane Rectangular CS VII", 30168: "Tokyo / Japan Plane Rectangular CS VIII", 30169: "Tokyo / Japan Plane Rectangular CS IX", 30170: "Tokyo / Japan Plane Rectangular CS X", 30171: "Tokyo / Japan Plane Rectangular CS XI", 30172: "Tokyo / Japan Plane Rectangular CS XII", 30173: "Tokyo / Japan Plane Rectangular CS XIII", 30174: "Tokyo / Japan Plane Rectangular CS XIV", 30175: "Tokyo / Japan Plane Rectangular CS XV", 30176: "Tokyo / Japan Plane Rectangular CS XVI", 30177: "Tokyo / Japan Plane Rectangular CS XVII", 30178: "Tokyo / Japan Plane Rectangular CS XVIII", 30179: "Tokyo / Japan Plane Rectangular CS XIX", 30200: "Trinidad 1903 / Trinidad Grid", 30339: "TC(1948) / UTM zone 39N", 30340: "TC(1948) / UTM zone 40N", 30491: "Voirol 1875 / Nord Algerie (ancienne)", 30492: "Voirol 1875 / Sud Algerie (ancienne)", 30493: "Voirol 1879 / Nord Algerie (ancienne)", 30494: "Voirol 1879 / Sud Algerie (ancienne)", 30729: "Nord Sahara 1959 / UTM zone 29N", 30730: "Nord Sahara 1959 / UTM zone 30N", 30731: "Nord Sahara 1959 / UTM zone 31N", 30732: "Nord Sahara 1959 / UTM zone 32N", 30791: "Nord Sahara 1959 / Nord Algerie", 30792: "Nord Sahara 1959 / Sud Algerie", 30800: "RT38 2.5 gon W", 31028: "Yoff / UTM zone 28N", 31121: "Zanderij / UTM zone 21N", 31154: "Zanderij / TM 54 NW", 31170: "Zanderij / Suriname Old TM", 31171: "Zanderij / Suriname TM", 31251: "MGI (Ferro) / Austria GK West Zone", 31252: "MGI (Ferro) / Austria GK Central Zone", 31253: "MGI (Ferro) / Austria GK East Zone", 31254: "MGI / Austria GK West", 31255: "MGI / Austria GK Central", 31256: "MGI / Austria GK East", 31257: "MGI / Austria GK M28", 31258: "MGI / Austria GK M31", 31259: "MGI / Austria GK M34", 31265: "MGI / 3-degree Gauss zone 5", 31266: "MGI / 3-degree Gauss zone 6", 31267: "MGI / 3-degree Gauss zone 7", 31268: "MGI / 3-degree Gauss zone 8", 31275: "MGI / Balkans zone 5", 31276: "MGI / Balkans zone 6", 31277: "MGI / Balkans zone 7", 31278: "MGI / Balkans zone 8", 31279: "MGI / Balkans zone 8", 31281: "MGI (Ferro) / Austria West Zone", 31282: "MGI (Ferro) / Austria Central Zone", 31283: "MGI (Ferro) / Austria East Zone", 31284: "MGI / Austria M28", 31285: "MGI / Austria M31", 31286: "MGI / Austria M34", 31287: "MGI / Austria Lambert", 31288: "MGI (Ferro) / M28", 31289: "MGI (Ferro) / M31", 31290: "MGI (Ferro) / M34", 31291: "MGI (Ferro) / Austria West Zone", 31292: "MGI (Ferro) / Austria Central Zone", 31293: "MGI (Ferro) / Austria East Zone", 31294: "MGI / M28", 31295: "MGI / M31", 31296: "MGI / M34", 31297: "MGI / Austria Lambert", 31300: "Belge 1972 / Belge Lambert 72", 31370: "Belge 1972 / Belgian Lambert 72", 31461: "DHDN / 3-degree Gauss zone 1", 31462: "DHDN / 3-degree Gauss zone 2", 31463: "DHDN / 3-degree Gauss zone 3", 31464: "DHDN / 3-degree Gauss zone 4", 31465: "DHDN / 3-degree Gauss zone 5", 31466: "DHDN / 3-degree Gauss-Kruger zone 2", 31467: "DHDN / 3-degree Gauss-Kruger zone 3", 31468: "DHDN / 3-degree Gauss-Kruger zone 4", 31469: "DHDN / 3-degree Gauss-Kruger zone 5", 31528: "Conakry 1905 / UTM zone 28N", 31529: "Conakry 1905 / UTM zone 29N", 31600: "Dealul Piscului 1930 / Stereo 33", 31700: "Dealul Piscului 1970/ Stereo 70", 31838: "NGN / UTM zone 38N", 31839: "NGN / UTM zone 39N", 31900: "KUDAMS / KTM", 31901: "KUDAMS / KTM", 31965: "SIRGAS 2000 / UTM zone 11N", 31966: "SIRGAS 2000 / UTM zone 12N", 31967: "SIRGAS 2000 / UTM zone 13N", 31968: "SIRGAS 2000 / UTM zone 14N", 31969: "SIRGAS 2000 / UTM zone 15N", 31970: "SIRGAS 2000 / UTM zone 16N", 31971: "SIRGAS 2000 / UTM zone 17N", 31972: "SIRGAS 2000 / UTM zone 18N", 31973: "SIRGAS 2000 / UTM zone 19N", 31974: "SIRGAS 2000 / UTM zone 20N", 31975: "SIRGAS 2000 / UTM zone 21N", 31976: "SIRGAS 2000 / UTM zone 22N", 31977: "SIRGAS 2000 / UTM zone 17S", 31978: "SIRGAS 2000 / UTM zone 18S", 31979: "SIRGAS 2000 / UTM zone 19S", 31980: "SIRGAS 2000 / UTM zone 20S", 31981: "SIRGAS 2000 / UTM zone 21S", 31982: "SIRGAS 2000 / UTM zone 22S", 31983: "SIRGAS 2000 / UTM zone 23S", 31984: "SIRGAS 2000 / UTM zone 24S", 31985: "SIRGAS 2000 / UTM zone 25S", 31986: "SIRGAS 1995 / UTM zone 17N", 31987: "SIRGAS 1995 / UTM zone 18N", 31988: "SIRGAS 1995 / UTM zone 19N", 31989: "SIRGAS 1995 / UTM zone 20N", 31990: "SIRGAS 1995 / UTM zone 21N", 31991: "SIRGAS 1995 / UTM zone 22N", 31992: "SIRGAS 1995 / UTM zone 17S", 31993: "SIRGAS 1995 / UTM zone 18S", 31994: "SIRGAS 1995 / UTM zone 19S", 31995: "SIRGAS 1995 / UTM zone 20S", 31996: "SIRGAS 1995 / UTM zone 21S", 31997: "SIRGAS 1995 / UTM zone 22S", 31998: "SIRGAS 1995 / UTM zone 23S", 31999: "SIRGAS 1995 / UTM zone 24S", 32000: "SIRGAS 1995 / UTM zone 25S", 32001: "NAD27 / Montana North", 32002: "NAD27 / Montana Central", 32003: "NAD27 / Montana South", 32005: "NAD27 / Nebraska North", 32006: "NAD27 / Nebraska South", 32007: "NAD27 / Nevada East", 32008: "NAD27 / Nevada Central", 32009: "NAD27 / Nevada West", 32010: "NAD27 / New Hampshire", 32011: "NAD27 / New Jersey", 32012: "NAD27 / New Mexico East", 32013: "NAD27 / New Mexico Central", 32014: "NAD27 / New Mexico West", 32015: "NAD27 / New York East", 32016: "NAD27 / New York Central", 32017: "NAD27 / New York West", 32018: "NAD27 / New York Long Island", 32019: "NAD27 / North Carolina", 32020: "NAD27 / North Dakota North", 32021: "NAD27 / North Dakota South", 32022: "NAD27 / Ohio North", 32023: "NAD27 / Ohio South", 32024: "NAD27 / Oklahoma North", 32025: "NAD27 / Oklahoma South", 32026: "NAD27 / Oregon North", 32027: "NAD27 / Oregon South", 32028: "NAD27 / Pennsylvania North", 32029: "NAD27 / Pennsylvania South", 32030: "NAD27 / Rhode Island", 32031: "NAD27 / South Carolina North", 32033: "NAD27 / South Carolina South", 32034: "NAD27 / South Dakota North", 32035: "NAD27 / South Dakota South", 32036: "NAD27 / Tennessee", 32037: "NAD27 / Texas North", 32038: "NAD27 / Texas North Central", 32039: "NAD27 / Texas Central", 32040: "NAD27 / Texas South Central", 32041: "NAD27 / Texas South", 32042: "NAD27 / Utah North", 32043: "NAD27 / Utah Central", 32044: "NAD27 / Utah South", 32045: "NAD27 / Vermont", 32046: "NAD27 / Virginia North", 32047: "NAD27 / Virginia South", 32048: "NAD27 / Washington North", 32049: "NAD27 / Washington South", 32050: "NAD27 / West Virginia North", 32051: "NAD27 / West Virginia South", 32052: "NAD27 / Wisconsin North", 32053: "NAD27 / Wisconsin Central", 32054: "NAD27 / Wisconsin South", 32055: "NAD27 / Wyoming East", 32056: "NAD27 / Wyoming East Central", 32057: "NAD27 / Wyoming West Central", 32058: "NAD27 / Wyoming West", 32061: "NAD27 / Guatemala Norte", 32062: "NAD27 / Guatemala Sur", 32064: "NAD27 / BLM 14N (ftUS)", 32065: "NAD27 / BLM 15N (ftUS)", 32066: "NAD27 / BLM 16N (ftUS)", 32067: "NAD27 / BLM 17N (ftUS)", 32074: "NAD27 / BLM 14N (feet)", 32075: "NAD27 / BLM 15N (feet)", 32076: "NAD27 / BLM 16N (feet)", 32077: "NAD27 / BLM 17N (feet)", 32081: "NAD27 / MTM zone 1", 32082: "NAD27 / MTM zone 2", 32083: "NAD27 / MTM zone 3", 32084: "NAD27 / MTM zone 4", 32085: "NAD27 / MTM zone 5", 32086: "NAD27 / MTM zone 6", 32098: "NAD27 / Quebec Lambert", 32099: "NAD27 / Louisiana Offshore", 32100: "NAD83 / Montana", 32104: "NAD83 / Nebraska", 32107: "NAD83 / Nevada East", 32108: "NAD83 / Nevada Central", 32109: "NAD83 / Nevada West", 32110: "NAD83 / New Hampshire", 32111: "NAD83 / New Jersey", 32112: "NAD83 / New Mexico East", 32113: "NAD83 / New Mexico Central", 32114: "NAD83 / New Mexico West", 32115: "NAD83 / New York East", 32116: "NAD83 / New York Central", 32117: "NAD83 / New York West", 32118: "NAD83 / New York Long Island", 32119: "NAD83 / North Carolina", 32120: "NAD83 / North Dakota North", 32121: "NAD83 / North Dakota South", 32122: "NAD83 / Ohio North", 32123: "NAD83 / Ohio South", 32124: "NAD83 / Oklahoma North", 32125: "NAD83 / Oklahoma South", 32126: "NAD83 / Oregon North", 32127: "NAD83 / Oregon South", 32128: "NAD83 / Pennsylvania North", 32129: "NAD83 / Pennsylvania South", 32130: "NAD83 / Rhode Island", 32133: "NAD83 / South Carolina", 32134: "NAD83 / South Dakota North", 32135: "NAD83 / South Dakota South", 32136: "NAD83 / Tennessee", 32137: "NAD83 / Texas North", 32138: "NAD83 / Texas North Central", 32139: "NAD83 / Texas Central", 32140: "NAD83 / Texas South Central", 32141: "NAD83 / Texas South", 32142: "NAD83 / Utah North", 32143: "NAD83 / Utah Central", 32144: "NAD83 / Utah South", 32145: "NAD83 / Vermont", 32146: "NAD83 / Virginia North", 32147: "NAD83 / Virginia South", 32148: "NAD83 / Washington North", 32149: "NAD83 / Washington South", 32150: "NAD83 / West Virginia North", 32151: "NAD83 / West Virginia South", 32152: "NAD83 / Wisconsin North", 32153: "NAD83 / Wisconsin Central", 32154: "NAD83 / Wisconsin South", 32155: "NAD83 / Wyoming East", 32156: "NAD83 / Wyoming East Central", 32157: "NAD83 / Wyoming West Central", 32158: "NAD83 / Wyoming West", 32161: "NAD83 / Puerto Rico & Virgin Is.", 32164: "NAD83 / BLM 14N (ftUS)", 32165: "NAD83 / BLM 15N (ftUS)", 32166: "NAD83 / BLM 16N (ftUS)", 32167: "NAD83 / BLM 17N (ftUS)", 32180: "NAD83 / SCoPQ zone 2", 32181: "NAD83 / MTM zone 1", 32182: "NAD83 / MTM zone 2", 32183: "NAD83 / MTM zone 3", 32184: "NAD83 / MTM zone 4", 32185: "NAD83 / MTM zone 5", 32186: "NAD83 / MTM zone 6", 32187: "NAD83 / MTM zone 7", 32188: "NAD83 / MTM zone 8", 32189: "NAD83 / MTM zone 9", 32190: "NAD83 / MTM zone 10", 32191: "NAD83 / MTM zone 11", 32192: "NAD83 / MTM zone 12", 32193: "NAD83 / MTM zone 13", 32194: "NAD83 / MTM zone 14", 32195: "NAD83 / MTM zone 15", 32196: "NAD83 / MTM zone 16", 32197: "NAD83 / MTM zone 17", 32198: "NAD83 / Quebec Lambert", 32199: "NAD83 / Louisiana Offshore", 32201: "WGS 72 / UTM zone 1N", 32202: "WGS 72 / UTM zone 2N", 32203: "WGS 72 / UTM zone 3N", 32204: "WGS 72 / UTM zone 4N", 32205: "WGS 72 / UTM zone 5N", 32206: "WGS 72 / UTM zone 6N", 32207: "WGS 72 / UTM zone 7N", 32208: "WGS 72 / UTM zone 8N", 32209: "WGS 72 / UTM zone 9N", 32210: "WGS 72 / UTM zone 10N", 32211: "WGS 72 / UTM zone 11N", 32212: "WGS 72 / UTM zone 12N", 32213: "WGS 72 / UTM zone 13N", 32214: "WGS 72 / UTM zone 14N", 32215: "WGS 72 / UTM zone 15N", 32216: "WGS 72 / UTM zone 16N", 32217: "WGS 72 / UTM zone 17N", 32218: "WGS 72 / UTM zone 18N", 32219: "WGS 72 / UTM zone 19N", 32220: "WGS 72 / UTM zone 20N", 32221: "WGS 72 / UTM zone 21N", 32222: "WGS 72 / UTM zone 22N", 32223: "WGS 72 / UTM zone 23N", 32224: "WGS 72 / UTM zone 24N", 32225: "WGS 72 / UTM zone 25N", 32226: "WGS 72 / UTM zone 26N", 32227: "WGS 72 / UTM zone 27N", 32228: "WGS 72 / UTM zone 28N", 32229: "WGS 72 / UTM zone 29N", 32230: "WGS 72 / UTM zone 30N", 32231: "WGS 72 / UTM zone 31N", 32232: "WGS 72 / UTM zone 32N", 32233: "WGS 72 / UTM zone 33N", 32234: "WGS 72 / UTM zone 34N", 32235: "WGS 72 / UTM zone 35N", 32236: "WGS 72 / UTM zone 36N", 32237: "WGS 72 / UTM zone 37N", 32238: "WGS 72 / UTM zone 38N", 32239: "WGS 72 / UTM zone 39N", 32240: "WGS 72 / UTM zone 40N", 32241: "WGS 72 / UTM zone 41N", 32242: "WGS 72 / UTM zone 42N", 32243: "WGS 72 / UTM zone 43N", 32244: "WGS 72 / UTM zone 44N", 32245: "WGS 72 / UTM zone 45N", 32246: "WGS 72 / UTM zone 46N", 32247: "WGS 72 / UTM zone 47N", 32248: "WGS 72 / UTM zone 48N", 32249: "WGS 72 / UTM zone 49N", 32250: "WGS 72 / UTM zone 50N", 32251: "WGS 72 / UTM zone 51N", 32252: "WGS 72 / UTM zone 52N", 32253: "WGS 72 / UTM zone 53N", 32254: "WGS 72 / UTM zone 54N", 32255: "WGS 72 / UTM zone 55N", 32256: "WGS 72 / UTM zone 56N", 32257: "WGS 72 / UTM zone 57N", 32258: "WGS 72 / UTM zone 58N", 32259: "WGS 72 / UTM zone 59N", 32260: "WGS 72 / UTM zone 60N", 32301: "WGS 72 / UTM zone 1S", 32302: "WGS 72 / UTM zone 2S", 32303: "WGS 72 / UTM zone 3S", 32304: "WGS 72 / UTM zone 4S", 32305: "WGS 72 / UTM zone 5S", 32306: "WGS 72 / UTM zone 6S", 32307: "WGS 72 / UTM zone 7S", 32308: "WGS 72 / UTM zone 8S", 32309: "WGS 72 / UTM zone 9S", 32310: "WGS 72 / UTM zone 10S", 32311: "WGS 72 / UTM zone 11S", 32312: "WGS 72 / UTM zone 12S", 32313: "WGS 72 / UTM zone 13S", 32314: "WGS 72 / UTM zone 14S", 32315: "WGS 72 / UTM zone 15S", 32316: "WGS 72 / UTM zone 16S", 32317: "WGS 72 / UTM zone 17S", 32318: "WGS 72 / UTM zone 18S", 32319: "WGS 72 / UTM zone 19S", 32320: "WGS 72 / UTM zone 20S", 32321: "WGS 72 / UTM zone 21S", 32322: "WGS 72 / UTM zone 22S", 32323: "WGS 72 / UTM zone 23S", 32324: "WGS 72 / UTM zone 24S", 32325: "WGS 72 / UTM zone 25S", 32326: "WGS 72 / UTM zone 26S", 32327: "WGS 72 / UTM zone 27S", 32328: "WGS 72 / UTM zone 28S", 32329: "WGS 72 / UTM zone 29S", 32330: "WGS 72 / UTM zone 30S", 32331: "WGS 72 / UTM zone 31S", 32332: "WGS 72 / UTM zone 32S", 32333: "WGS 72 / UTM zone 33S", 32334: "WGS 72 / UTM zone 34S", 32335: "WGS 72 / UTM zone 35S", 32336: "WGS 72 / UTM zone 36S", 32337: "WGS 72 / UTM zone 37S", 32338: "WGS 72 / UTM zone 38S", 32339: "WGS 72 / UTM zone 39S", 32340: "WGS 72 / UTM zone 40S", 32341: "WGS 72 / UTM zone 41S", 32342: "WGS 72 / UTM zone 42S", 32343: "WGS 72 / UTM zone 43S", 32344: "WGS 72 / UTM zone 44S", 32345: "WGS 72 / UTM zone 45S", 32346: "WGS 72 / UTM zone 46S", 32347: "WGS 72 / UTM zone 47S", 32348: "WGS 72 / UTM zone 48S", 32349: "WGS 72 / UTM zone 49S", 32350: "WGS 72 / UTM zone 50S", 32351: "WGS 72 / UTM zone 51S", 32352: "WGS 72 / UTM zone 52S", 32353: "WGS 72 / UTM zone 53S", 32354: "WGS 72 / UTM zone 54S", 32355: "WGS 72 / UTM zone 55S", 32356: "WGS 72 / UTM zone 56S", 32357: "WGS 72 / UTM zone 57S", 32358: "WGS 72 / UTM zone 58S", 32359: "WGS 72 / UTM zone 59S", 32360: "WGS 72 / UTM zone 60S", 32401: "WGS 72BE / UTM zone 1N", 32402: "WGS 72BE / UTM zone 2N", 32403: "WGS 72BE / UTM zone 3N", 32404: "WGS 72BE / UTM zone 4N", 32405: "WGS 72BE / UTM zone 5N", 32406: "WGS 72BE / UTM zone 6N", 32407: "WGS 72BE / UTM zone 7N", 32408: "WGS 72BE / UTM zone 8N", 32409: "WGS 72BE / UTM zone 9N", 32410: "WGS 72BE / UTM zone 10N", 32411: "WGS 72BE / UTM zone 11N", 32412: "WGS 72BE / UTM zone 12N", 32413: "WGS 72BE / UTM zone 13N", 32414: "WGS 72BE / UTM zone 14N", 32415: "WGS 72BE / UTM zone 15N", 32416: "WGS 72BE / UTM zone 16N", 32417: "WGS 72BE / UTM zone 17N", 32418: "WGS 72BE / UTM zone 18N", 32419: "WGS 72BE / UTM zone 19N", 32420: "WGS 72BE / UTM zone 20N", 32421: "WGS 72BE / UTM zone 21N", 32422: "WGS 72BE / UTM zone 22N", 32423: "WGS 72BE / UTM zone 23N", 32424: "WGS 72BE / UTM zone 24N", 32425: "WGS 72BE / UTM zone 25N", 32426: "WGS 72BE / UTM zone 26N", 32427: "WGS 72BE / UTM zone 27N", 32428: "WGS 72BE / UTM zone 28N", 32429: "WGS 72BE / UTM zone 29N", 32430: "WGS 72BE / UTM zone 30N", 32431: "WGS 72BE / UTM zone 31N", 32432: "WGS 72BE / UTM zone 32N", 32433: "WGS 72BE / UTM zone 33N", 32434: "WGS 72BE / UTM zone 34N", 32435: "WGS 72BE / UTM zone 35N", 32436: "WGS 72BE / UTM zone 36N", 32437: "WGS 72BE / UTM zone 37N", 32438: "WGS 72BE / UTM zone 38N", 32439: "WGS 72BE / UTM zone 39N", 32440: "WGS 72BE / UTM zone 40N", 32441: "WGS 72BE / UTM zone 41N", 32442: "WGS 72BE / UTM zone 42N", 32443: "WGS 72BE / UTM zone 43N", 32444: "WGS 72BE / UTM zone 44N", 32445: "WGS 72BE / UTM zone 45N", 32446: "WGS 72BE / UTM zone 46N", 32447: "WGS 72BE / UTM zone 47N", 32448: "WGS 72BE / UTM zone 48N", 32449: "WGS 72BE / UTM zone 49N", 32450: "WGS 72BE / UTM zone 50N", 32451: "WGS 72BE / UTM zone 51N", 32452: "WGS 72BE / UTM zone 52N", 32453: "WGS 72BE / UTM zone 53N", 32454: "WGS 72BE / UTM zone 54N", 32455: "WGS 72BE / UTM zone 55N", 32456: "WGS 72BE / UTM zone 56N", 32457: "WGS 72BE / UTM zone 57N", 32458: "WGS 72BE / UTM zone 58N", 32459: "WGS 72BE / UTM zone 59N", 32460: "WGS 72BE / UTM zone 60N", 32501: "WGS 72BE / UTM zone 1S", 32502: "WGS 72BE / UTM zone 2S", 32503: "WGS 72BE / UTM zone 3S", 32504: "WGS 72BE / UTM zone 4S", 32505: "WGS 72BE / UTM zone 5S", 32506: "WGS 72BE / UTM zone 6S", 32507: "WGS 72BE / UTM zone 7S", 32508: "WGS 72BE / UTM zone 8S", 32509: "WGS 72BE / UTM zone 9S", 32510: "WGS 72BE / UTM zone 10S", 32511: "WGS 72BE / UTM zone 11S", 32512: "WGS 72BE / UTM zone 12S", 32513: "WGS 72BE / UTM zone 13S", 32514: "WGS 72BE / UTM zone 14S", 32515: "WGS 72BE / UTM zone 15S", 32516: "WGS 72BE / UTM zone 16S", 32517: "WGS 72BE / UTM zone 17S", 32518: "WGS 72BE / UTM zone 18S", 32519: "WGS 72BE / UTM zone 19S", 32520: "WGS 72BE / UTM zone 20S", 32521: "WGS 72BE / UTM zone 21S", 32522: "WGS 72BE / UTM zone 22S", 32523: "WGS 72BE / UTM zone 23S", 32524: "WGS 72BE / UTM zone 24S", 32525: "WGS 72BE / UTM zone 25S", 32526: "WGS 72BE / UTM zone 26S", 32527: "WGS 72BE / UTM zone 27S", 32528: "WGS 72BE / UTM zone 28S", 32529: "WGS 72BE / UTM zone 29S", 32530: "WGS 72BE / UTM zone 30S", 32531: "WGS 72BE / UTM zone 31S", 32532: "WGS 72BE / UTM zone 32S", 32533: "WGS 72BE / UTM zone 33S", 32534: "WGS 72BE / UTM zone 34S", 32535: "WGS 72BE / UTM zone 35S", 32536: "WGS 72BE / UTM zone 36S", 32537: "WGS 72BE / UTM zone 37S", 32538: "WGS 72BE / UTM zone 38S", 32539: "WGS 72BE / UTM zone 39S", 32540: "WGS 72BE / UTM zone 40S", 32541: "WGS 72BE / UTM zone 41S", 32542: "WGS 72BE / UTM zone 42S", 32543: "WGS 72BE / UTM zone 43S", 32544: "WGS 72BE / UTM zone 44S", 32545: "WGS 72BE / UTM zone 45S", 32546: "WGS 72BE / UTM zone 46S", 32547: "WGS 72BE / UTM zone 47S", 32548: "WGS 72BE / UTM zone 48S", 32549: "WGS 72BE / UTM zone 49S", 32550: "WGS 72BE / UTM zone 50S", 32551: "WGS 72BE / UTM zone 51S", 32552: "WGS 72BE / UTM zone 52S", 32553: "WGS 72BE / UTM zone 53S", 32554: "WGS 72BE / UTM zone 54S", 32555: "WGS 72BE / UTM zone 55S", 32556: "WGS 72BE / UTM zone 56S", 32557: "WGS 72BE / UTM zone 57S", 32558: "WGS 72BE / UTM zone 58S", 32559: "WGS 72BE / UTM zone 59S", 32560: "WGS 72BE / UTM zone 60S", 32600: "WGS 84 / UTM grid system (northern hemisphere)", 32601: "WGS 84 / UTM zone 1N", 32602: "WGS 84 / UTM zone 2N", 32603: "WGS 84 / UTM zone 3N", 32604: "WGS 84 / UTM zone 4N", 32605: "WGS 84 / UTM zone 5N", 32606: "WGS 84 / UTM zone 6N", 32607: "WGS 84 / UTM zone 7N", 32608: "WGS 84 / UTM zone 8N", 32609: "WGS 84 / UTM zone 9N", 32610: "WGS 84 / UTM zone 10N", 32611: "WGS 84 / UTM zone 11N", 32612: "WGS 84 / UTM zone 12N", 32613: "WGS 84 / UTM zone 13N", 32614: "WGS 84 / UTM zone 14N", 32615: "WGS 84 / UTM zone 15N", 32616: "WGS 84 / UTM zone 16N", 32617: "WGS 84 / UTM zone 17N", 32618: "WGS 84 / UTM zone 18N", 32619: "WGS 84 / UTM zone 19N", 32620: "WGS 84 / UTM zone 20N", 32621: "WGS 84 / UTM zone 21N", 32622: "WGS 84 / UTM zone 22N", 32623: "WGS 84 / UTM zone 23N", 32624: "WGS 84 / UTM zone 24N", 32625: "WGS 84 / UTM zone 25N", 32626: "WGS 84 / UTM zone 26N", 32627: "WGS 84 / UTM zone 27N", 32628: "WGS 84 / UTM zone 28N", 32629: "WGS 84 / UTM zone 29N", 32630: "WGS 84 / UTM zone 30N", 32631: "WGS 84 / UTM zone 31N", 32632: "WGS 84 / UTM zone 32N", 32633: "WGS 84 / UTM zone 33N", 32634: "WGS 84 / UTM zone 34N", 32635: "WGS 84 / UTM zone 35N", 32636: "WGS 84 / UTM zone 36N", 32637: "WGS 84 / UTM zone 37N", 32638: "WGS 84 / UTM zone 38N", 32639: "WGS 84 / UTM zone 39N", 32640: "WGS 84 / UTM zone 40N", 32641: "WGS 84 / UTM zone 41N", 32642: "WGS 84 / UTM zone 42N", 32643: "WGS 84 / UTM zone 43N", 32644: "WGS 84 / UTM zone 44N", 32645: "WGS 84 / UTM zone 45N", 32646: "WGS 84 / UTM zone 46N", 32647: "WGS 84 / UTM zone 47N", 32648: "WGS 84 / UTM zone 48N", 32649: "WGS 84 / UTM zone 49N", 32650: "WGS 84 / UTM zone 50N", 32651: "WGS 84 / UTM zone 51N", 32652: "WGS 84 / UTM zone 52N", 32653: "WGS 84 / UTM zone 53N", 32654: "WGS 84 / UTM zone 54N", 32655: "WGS 84 / UTM zone 55N", 32656: "WGS 84 / UTM zone 56N", 32657: "WGS 84 / UTM zone 57N", 32658: "WGS 84 / UTM zone 58N", 32659: "WGS 84 / UTM zone 59N", 32660: "WGS 84 / UTM zone 60N", 32661: "WGS 84 / UPS North (N,E)", 32662: "WGS 84 / Plate Carree", 32663: "WGS 84 / World Equidistant Cylindrical", 32664: "WGS 84 / BLM 14N (ftUS)", 32665: "WGS 84 / BLM 15N (ftUS)", 32666: "WGS 84 / BLM 16N (ftUS)", 32667: "WGS 84 / BLM 17N (ftUS)", 32700: "WGS 84 / UTM grid system (southern hemisphere)", 32701: "WGS 84 / UTM zone 1S", 32702: "WGS 84 / UTM zone 2S", 32703: "WGS 84 / UTM zone 3S", 32704: "WGS 84 / UTM zone 4S", 32705: "WGS 84 / UTM zone 5S", 32706: "WGS 84 / UTM zone 6S", 32707: "WGS 84 / UTM zone 7S", 32708: "WGS 84 / UTM zone 8S", 32709: "WGS 84 / UTM zone 9S", 32710: "WGS 84 / UTM zone 10S", 32711: "WGS 84 / UTM zone 11S", 32712: "WGS 84 / UTM zone 12S", 32713: "WGS 84 / UTM zone 13S", 32714: "WGS 84 / UTM zone 14S", 32715: "WGS 84 / UTM zone 15S", 32716: "WGS 84 / UTM zone 16S", 32717: "WGS 84 / UTM zone 17S", 32718: "WGS 84 / UTM zone 18S", 32719: "WGS 84 / UTM zone 19S", 32720: "WGS 84 / UTM zone 20S", 32721: "WGS 84 / UTM zone 21S", 32722: "WGS 84 / UTM zone 22S", 32723: "WGS 84 / UTM zone 23S", 32724: "WGS 84 / UTM zone 24S", 32725: "WGS 84 / UTM zone 25S", 32726: "WGS 84 / UTM zone 26S", 32727: "WGS 84 / UTM zone 27S", 32728: "WGS 84 / UTM zone 28S", 32729: "WGS 84 / UTM zone 29S", 32730: "WGS 84 / UTM zone 30S", 32731: "WGS 84 / UTM zone 31S", 32732: "WGS 84 / UTM zone 32S", 32733: "WGS 84 / UTM zone 33S", 32734: "WGS 84 / UTM zone 34S", 32735: "WGS 84 / UTM zone 35S", 32736: "WGS 84 / UTM zone 36S", 32737: "WGS 84 / UTM zone 37S", 32738: "WGS 84 / UTM zone 38S", 32739: "WGS 84 / UTM zone 39S", 32740: "WGS 84 / UTM zone 40S", 32741: "WGS 84 / UTM zone 41S", 32742: "WGS 84 / UTM zone 42S", 32743: "WGS 84 / UTM zone 43S", 32744: "WGS 84 / UTM zone 44S", 32745: "WGS 84 / UTM zone 45S", 32746: "WGS 84 / UTM zone 46S", 32747: "WGS 84 / UTM zone 47S", 32748: "WGS 84 / UTM zone 48S", 32749: "WGS 84 / UTM zone 49S", 32750: "WGS 84 / UTM zone 50S", 32751: "WGS 84 / UTM zone 51S", 32752: "WGS 84 / UTM zone 52S", 32753: "WGS 84 / UTM zone 53S", 32754: "WGS 84 / UTM zone 54S", 32755: "WGS 84 / UTM zone 55S", 32756: "WGS 84 / UTM zone 56S", 32757: "WGS 84 / UTM zone 57S", 32758: "WGS 84 / UTM zone 58S", 32759: "WGS 84 / UTM zone 59S", 32760: "WGS 84 / UTM zone 60S", 32761: "WGS 84 / UPS South (N,E)", 32766: "WGS 84 / TM 36 SE", 32767: "User-defined" } ProjectionGeoKey = { 10101: "Proj_Alabama_CS27_East", 10102: "Proj_Alabama_CS27_West", 10131: "Proj_Alabama_CS83_East", 10132: "Proj_Alabama_CS83_West", 10201: "Proj_Arizona_Coordinate_System_east", 10202: "Proj_Arizona_Coordinate_System_Central", 10203: "Proj_Arizona_Coordinate_System_west", 10231: "Proj_Arizona_CS83_east", 10232: "Proj_Arizona_CS83_Central", 10233: "Proj_Arizona_CS83_west", 10301: "Proj_Arkansas_CS27_North", 10302: "Proj_Arkansas_CS27_South", 10331: "Proj_Arkansas_CS83_North", 10332: "Proj_Arkansas_CS83_South", 10401: "Proj_California_CS27_I", 10402: "Proj_California_CS27_II", 10403: "Proj_California_CS27_III", 10404: "Proj_California_CS27_IV", 10405: "Proj_California_CS27_V", 10406: "Proj_California_CS27_VI", 10407: "Proj_California_CS27_VII", 10431: "Proj_California_CS83_1", 10432: "Proj_California_CS83_2", 10433: "Proj_California_CS83_3", 10434: "Proj_California_CS83_4", 10435: "Proj_California_CS83_5", 10436: "Proj_California_CS83_6", 10501: "Proj_Colorado_CS27_North", 10502: "Proj_Colorado_CS27_Central", 10503: "Proj_Colorado_CS27_South", 10531: "Proj_Colorado_CS83_North", 10532: "Proj_Colorado_CS83_Central", 10533: "Proj_Colorado_CS83_South", 10600: "Proj_Connecticut_CS27", 10630: "Proj_Connecticut_CS83", 10700: "Proj_Delaware_CS27", 10730: "Proj_Delaware_CS83", 10901: "Proj_Florida_CS27_East", 10902: "Proj_Florida_CS27_West", 10903: "Proj_Florida_CS27_North", 10931: "Proj_Florida_CS83_East", 10932: "Proj_Florida_CS83_West", 10933: "Proj_Florida_CS83_North", 11001: "Proj_Georgia_CS27_East", 11002: "Proj_Georgia_CS27_West", 11031: "Proj_Georgia_CS83_East", 11032: "Proj_Georgia_CS83_West", 11101: "Proj_Idaho_CS27_East", 11102: "Proj_Idaho_CS27_Central", 11103: "Proj_Idaho_CS27_West", 11131: "Proj_Idaho_CS83_East", 11132: "Proj_Idaho_CS83_Central", 11133: "Proj_Idaho_CS83_West", 11201: "Proj_Illinois_CS27_East", 11202: "Proj_Illinois_CS27_West", 11231: "Proj_Illinois_CS83_East", 11232: "Proj_Illinois_CS83_West", 11301: "Proj_Indiana_CS27_East", 11302: "Proj_Indiana_CS27_West", 11331: "Proj_Indiana_CS83_East", 11332: "Proj_Indiana_CS83_West", 11401: "Proj_Iowa_CS27_North", 11402: "Proj_Iowa_CS27_South", 11431: "Proj_Iowa_CS83_North", 11432: "Proj_Iowa_CS83_South", 11501: "Proj_Kansas_CS27_North", 11502: "Proj_Kansas_CS27_South", 11531: "Proj_Kansas_CS83_North", 11532: "Proj_Kansas_CS83_South", 11601: "Proj_Kentucky_CS27_North", 11602: "Proj_Kentucky_CS27_South", 11631: "Proj_Kentucky_CS83_North", 11632: "Proj_Kentucky_CS83_South", 11701: "Proj_Louisiana_CS27_North", 11702: "Proj_Louisiana_CS27_South", 11731: "Proj_Louisiana_CS83_North", 11732: "Proj_Louisiana_CS83_South", 11801: "Proj_Maine_CS27_East", 11802: "Proj_Maine_CS27_West", 11831: "Proj_Maine_CS83_East", 11832: "Proj_Maine_CS83_West", 11900: "Proj_Maryland_CS27", 11930: "Proj_Maryland_CS83", 12001: "Proj_Massachusetts_CS27_Mainland", 12002: "Proj_Massachusetts_CS27_Island", 12031: "Proj_Massachusetts_CS83_Mainland", 12032: "Proj_Massachusetts_CS83_Island", 12101: "Proj_Michigan_State_Plane_East", 12102: "Proj_Michigan_State_Plane_Old_Central", 12103: "Proj_Michigan_State_Plane_West", 12111: "Proj_Michigan_CS27_North", 12112: "Proj_Michigan_CS27_Central", 12113: "Proj_Michigan_CS27_South", 12141: "Proj_Michigan_CS83_North", 12142: "Proj_Michigan_CS83_Central", 12143: "Proj_Michigan_CS83_South", 12201: "Proj_Minnesota_CS27_North", 12202: "Proj_Minnesota_CS27_Central", 12203: "Proj_Minnesota_CS27_South", 12231: "Proj_Minnesota_CS83_North", 12232: "Proj_Minnesota_CS83_Central", 12233: "Proj_Minnesota_CS83_South", 12301: "Proj_Mississippi_CS27_East", 12302: "Proj_Mississippi_CS27_West", 12331: "Proj_Mississippi_CS83_East", 12332: "Proj_Mississippi_CS83_West", 12401: "Proj_Missouri_CS27_East", 12402: "Proj_Missouri_CS27_Central", 12403: "Proj_Missouri_CS27_West", 12431: "Proj_Missouri_CS83_East", 12432: "Proj_Missouri_CS83_Central", 12433: "Proj_Missouri_CS83_West", 12501: "Proj_Montana_CS27_North", 12502: "Proj_Montana_CS27_Central", 12503: "Proj_Montana_CS27_South", 12530: "Proj_Montana_CS83", 12601: "Proj_Nebraska_CS27_North", 12602: "Proj_Nebraska_CS27_South", 12630: "Proj_Nebraska_CS83", 12701: "Proj_Nevada_CS27_East", 12702: "Proj_Nevada_CS27_Central", 12703: "Proj_Nevada_CS27_West", 12731: "Proj_Nevada_CS83_East", 12732: "Proj_Nevada_CS83_Central", 12733: "Proj_Nevada_CS83_West", 12800: "Proj_New_Hampshire_CS27", 12830: "Proj_New_Hampshire_CS83", 12900: "Proj_New_Jersey_CS27", 12930: "Proj_New_Jersey_CS83", 13001: "Proj_New_Mexico_CS27_East", 13002: "Proj_New_Mexico_CS27_Central", 13003: "Proj_New_Mexico_CS27_West", 13031: "Proj_New_Mexico_CS83_East", 13032: "Proj_New_Mexico_CS83_Central", 13033: "Proj_New_Mexico_CS83_West", 13101: "Proj_New_York_CS27_East", 13102: "Proj_New_York_CS27_Central", 13103: "Proj_New_York_CS27_West", 13104: "Proj_New_York_CS27_Long_Island", 13131: "Proj_New_York_CS83_East", 13132: "Proj_New_York_CS83_Central", 13133: "Proj_New_York_CS83_West", 13134: "Proj_New_York_CS83_Long_Island", 13200: "Proj_North_Carolina_CS27", 13230: "Proj_North_Carolina_CS83", 13301: "Proj_North_Dakota_CS27_North", 13302: "Proj_North_Dakota_CS27_South", 13331: "Proj_North_Dakota_CS83_North", 13332: "Proj_North_Dakota_CS83_South", 13401: "Proj_Ohio_CS27_North", 13402: "Proj_Ohio_CS27_South", 13431: "Proj_Ohio_CS83_North", 13432: "Proj_Ohio_CS83_South", 13501: "Proj_Oklahoma_CS27_North", 13502: "Proj_Oklahoma_CS27_South", 13531: "Proj_Oklahoma_CS83_North", 13532: "Proj_Oklahoma_CS83_South", 13601: "Proj_Oregon_CS27_North", 13602: "Proj_Oregon_CS27_South", 13631: "Proj_Oregon_CS83_North", 13632: "Proj_Oregon_CS83_South", 13701: "Proj_Pennsylvania_CS27_North", 13702: "Proj_Pennsylvania_CS27_South", 13731: "Proj_Pennsylvania_CS83_North", 13732: "Proj_Pennsylvania_CS83_South", 13800: "Proj_Rhode_Island_CS27", 13830: "Proj_Rhode_Island_CS83", 13901: "Proj_South_Carolina_CS27_North", 13902: "Proj_South_Carolina_CS27_South", 13930: "Proj_South_Carolina_CS83", 14001: "Proj_South_Dakota_CS27_North", 14002: "Proj_South_Dakota_CS27_South", 14031: "Proj_South_Dakota_CS83_North", 14032: "Proj_South_Dakota_CS83_South", 14100: "Proj_Tennessee_CS27", 14130: "Proj_Tennessee_CS83", 14201: "Proj_Texas_CS27_North", 14202: "Proj_Texas_CS27_North_Central", 14203: "Proj_Texas_CS27_Central", 14204: "Proj_Texas_CS27_South_Central", 14205: "Proj_Texas_CS27_South", 14231: "Proj_Texas_CS83_North", 14232: "Proj_Texas_CS83_North_Central", 14233: "Proj_Texas_CS83_Central", 14234: "Proj_Texas_CS83_South_Central", 14235: "Proj_Texas_CS83_South", 14301: "Proj_Utah_CS27_North", 14302: "Proj_Utah_CS27_Central", 14303: "Proj_Utah_CS27_South", 14331: "Proj_Utah_CS83_North", 14332: "Proj_Utah_CS83_Central", 14333: "Proj_Utah_CS83_South", 14400: "Proj_Vermont_CS27", 14430: "Proj_Vermont_CS83", 14501: "Proj_Virginia_CS27_North", 14502: "Proj_Virginia_CS27_South", 14531: "Proj_Virginia_CS83_North", 14532: "Proj_Virginia_CS83_South", 14601: "Proj_Washington_CS27_North", 14602: "Proj_Washington_CS27_South", 14631: "Proj_Washington_CS83_North", 14632: "Proj_Washington_CS83_South", 14701: "Proj_West_Virginia_CS27_North", 14702: "Proj_West_Virginia_CS27_South", 14731: "Proj_West_Virginia_CS83_North", 14732: "Proj_West_Virginia_CS83_South", 14801: "Proj_Wisconsin_CS27_North", 14802: "Proj_Wisconsin_CS27_Central", 14803: "Proj_Wisconsin_CS27_South", 14831: "Proj_Wisconsin_CS83_North", 14832: "Proj_Wisconsin_CS83_Central", 14833: "Proj_Wisconsin_CS83_South", 14901: "Proj_Wyoming_CS27_East", 14902: "Proj_Wyoming_CS27_East_Central", 14903: "Proj_Wyoming_CS27_West_Central", 14904: "Proj_Wyoming_CS27_West", 14931: "Proj_Wyoming_CS83_East", 14932: "Proj_Wyoming_CS83_East_Central", 14933: "Proj_Wyoming_CS83_West_Central", 14934: "Proj_Wyoming_CS83_West", 15001: "Proj_Alaska_CS27_1", 15002: "Proj_Alaska_CS27_2", 15003: "Proj_Alaska_CS27_3", 15004: "Proj_Alaska_CS27_4", 15005: "Proj_Alaska_CS27_5", 15006: "Proj_Alaska_CS27_6", 15007: "Proj_Alaska_CS27_7", 15008: "Proj_Alaska_CS27_8", 15009: "Proj_Alaska_CS27_9", 15010: "Proj_Alaska_CS27_10", 15031: "Proj_Alaska_CS83_1", 15032: "Proj_Alaska_CS83_2", 15033: "Proj_Alaska_CS83_3", 15034: "Proj_Alaska_CS83_4", 15035: "Proj_Alaska_CS83_5", 15036: "Proj_Alaska_CS83_6", 15037: "Proj_Alaska_CS83_7", 15038: "Proj_Alaska_CS83_8", 15039: "Proj_Alaska_CS83_9", 15040: "Proj_Alaska_CS83_10", 15101: "Proj_Hawaii_CS27_1", 15102: "Proj_Hawaii_CS27_2", 15103: "Proj_Hawaii_CS27_3", 15104: "Proj_Hawaii_CS27_4", 15105: "Proj_Hawaii_CS27_5", 15131: "Proj_Hawaii_CS83_1", 15132: "Proj_Hawaii_CS83_2", 15133: "Proj_Hawaii_CS83_3", 15134: "Proj_Hawaii_CS83_4", 15135: "Proj_Hawaii_CS83_5", 15201: "Proj_Puerto_Rico_CS27", 15202: "Proj_St_Croix", 15230: "Proj_Puerto_Rico_Virgin_Is", 15914: "Proj_BLM_14N_feet", 15915: "Proj_BLM_15N_feet", 15916: "Proj_BLM_16N_feet", 15917: "Proj_BLM_17N_feet", 17348: "Proj_Map_Grid_of_Australia_48", 17349: "Proj_Map_Grid_of_Australia_49", 17350: "Proj_Map_Grid_of_Australia_50", 17351: "Proj_Map_Grid_of_Australia_51", 17352: "Proj_Map_Grid_of_Australia_52", 17353: "Proj_Map_Grid_of_Australia_53", 17354: "Proj_Map_Grid_of_Australia_54", 17355: "Proj_Map_Grid_of_Australia_55", 17356: "Proj_Map_Grid_of_Australia_56", 17357: "Proj_Map_Grid_of_Australia_57", 17358: "Proj_Map_Grid_of_Australia_58", 17448: "Proj_Australian_Map_Grid_48", 17449: "Proj_Australian_Map_Grid_49", 17450: "Proj_Australian_Map_Grid_50", 17451: "Proj_Australian_Map_Grid_51", 17452: "Proj_Australian_Map_Grid_52", 17453: "Proj_Australian_Map_Grid_53", 17454: "Proj_Australian_Map_Grid_54", 17455: "Proj_Australian_Map_Grid_55", 17456: "Proj_Australian_Map_Grid_56", 17457: "Proj_Australian_Map_Grid_57", 17458: "Proj_Australian_Map_Grid_58", 18031: "Proj_Argentina_1", 18032: "Proj_Argentina_2", 18033: "Proj_Argentina_3", 18034: "Proj_Argentina_4", 18035: "Proj_Argentina_5", 18036: "Proj_Argentina_6", 18037: "Proj_Argentina_7", 18051: "Proj_Colombia_3W", 18052: "Proj_Colombia_Bogota", 18053: "Proj_Colombia_3E", 18054: "Proj_Colombia_6E", 18072: "Proj_Egypt_Red_Belt", 18073: "Proj_Egypt_Purple_Belt", 18074: "Proj_Extended_Purple_Belt", 18141: "Proj_New_Zealand_North_Island_Nat_Grid", 18142: "Proj_New_Zealand_South_Island_Nat_Grid", 19900: "Proj_Bahrain_Grid", 19905: "Proj_Netherlands_E_Indies_Equatorial", 19912: "Proj_RSO_Borneo", 32767: "User-defined" } ================================================ FILE: core/lib/imageio/README.md ================================================ This is the Python package that is installed on the user's system. It consists of a `core` module, which implements the basis of imageio. The `plugins` module contains the code to actually import/export images, organised in plugins. The `freeze` module provides functionality for freezing apps that make use of imageio. ================================================ FILE: core/lib/imageio/__init__.py ================================================ # -*- coding: utf-8 -*- # Copyright (c) 2015, imageio contributors # imageio is distributed under the terms of the (new) BSD License. # This docstring is used at the index of the documentation pages, and # gets inserted into a slightly larger description (in setup.py) for # the page on Pypi: """ Imageio is a Python library that provides an easy interface to read and write a wide range of image data, including animated images, volumetric data, and scientific formats. It is cross-platform, runs on Python 2.x and 3.x, and is easy to install. Main website: http://imageio.github.io """ __version__ = '1.5' # Load some bits from core from .core import FormatManager, RETURN_BYTES # noqa # Instantiate format manager formats = FormatManager() # Load the functions from .core.functions import help # noqa from .core.functions import get_reader, get_writer # noqa from .core.functions import imread, mimread, volread, mvolread # noqa from .core.functions import imwrite, mimwrite, volwrite, mvolwrite # noqa # Load function aliases from .core.functions import read, save # noqa from .core.functions import imsave, mimsave, volsave, mvolsave # noqa # Load all the plugins from . import plugins # noqa # expose the show method of formats show_formats = formats.show # Clean up some names del FormatManager ================================================ FILE: core/lib/imageio/core/__init__.py ================================================ # -*- coding: utf-8 -*- # Copyright (c) 2015, imageio contributors # Distributed under the (new) BSD License. See LICENSE.txt for more info. """ This subpackage provides the core functionality of imageio (everything but the plugins). """ from .util import Image, Dict, asarray, image_as_uint, urlopen # noqa from .util import BaseProgressIndicator, StdoutProgressIndicator # noqa from .util import string_types, text_type, binary_type, IS_PYPY # noqa from .util import get_platform, appdata_dir, resource_dirs, has_module # noqa from .findlib import load_lib # noqa from .fetching import get_remote_file, InternetNotAllowedError # noqa from .request import Request, read_n_bytes, RETURN_BYTES # noqa from .format import Format, FormatManager # noqa ================================================ FILE: core/lib/imageio/core/fetching.py ================================================ # -*- coding: utf-8 -*- # Copyright (c) 2015, imageio contributors # Based on code from the vispy project # Distributed under the (new) BSD License. See LICENSE.txt for more info. """Data downloading and reading functions """ from __future__ import absolute_import, print_function, division from math import log import os from os import path as op import sys import shutil import time from . import appdata_dir, resource_dirs from . import StdoutProgressIndicator, string_types, urlopen class InternetNotAllowedError(IOError): """ Plugins that need resources can just use get_remote_file(), but should catch this error and silently ignore it. """ pass def get_remote_file(fname, directory=None, force_download=False): """ Get a the filename for the local version of a file from the web Parameters ---------- fname : str The relative filename on the remote data repository to download. These correspond to paths on ``https://github.com/imageio/imageio-binaries/``. directory : str | None The directory where the file will be cached if a download was required to obtain the file. By default, the appdata directory is used. This is also the first directory that is checked for a local version of the file. force_download : bool | str If True, the file will be downloaded even if a local copy exists (and this copy will be overwritten). Can also be a YYYY-MM-DD date to ensure a file is up-to-date (modified date of a file on disk, if present, is checked). Returns ------- fname : str The path to the file on the local system. """ #_url_root = 'https://github.com/imageio/imageio-binaries/raw/master/' _url_root = 'https://github.com/domlysz/freeimage_bin/raw/master/' url = _url_root + fname fname = op.normcase(fname) # convert to native # Get dirs to look for the resource directory = directory or appdata_dir('imageio') dirs = resource_dirs() dirs.insert(0, directory) # Given dir has preference # Try to find the resource locally for dir in dirs: filename = op.join(dir, fname) if op.isfile(filename): if not force_download: # we're done return filename if isinstance(force_download, string_types): ntime = time.strptime(force_download, '%Y-%m-%d') ftime = time.gmtime(op.getctime(filename)) if ftime >= ntime: return filename else: print('File older than %s, updating...' % force_download) break # If we get here, we're going to try to download the file if os.getenv('IMAGEIO_NO_INTERNET', '').lower() in ('1', 'true', 'yes'): raise InternetNotAllowedError('Will not download resource from the ' 'internet because enironment variable ' 'IMAGEIO_NO_INTERNET is set.') # Get filename to store to and make sure the dir exists filename = op.join(directory, fname) if not op.isdir(op.dirname(filename)): os.makedirs(op.abspath(op.dirname(filename))) # let's go get the file if os.getenv('CONTINUOUS_INTEGRATION', False): # pragma: no cover # On Travis, we retry a few times ... for i in range(2): try: _fetch_file(url, filename) return filename except IOError: time.sleep(0.5) else: _fetch_file(url, filename) return filename else: # pragma: no cover _fetch_file(url, filename) return filename def _fetch_file(url, file_name, print_destination=True): """Load requested file, downloading it if needed or requested Parameters ---------- url: string The url of file to be downloaded. file_name: string Name, along with the path, of where downloaded file will be saved. print_destination: bool, optional If true, destination of where file was saved will be printed after download finishes. resume: bool, optional If true, try to resume partially downloaded files. """ # Adapted from NISL: # https://github.com/nisl/tutorial/blob/master/nisl/datasets.py print('Imageio: %r was not found on your computer; ' 'downloading it now.' % os.path.basename(file_name)) temp_file_name = file_name + ".part" local_file = None initial_size = 0 errors = [] for tries in range(4): try: # Checking file size and displaying it alongside the download url remote_file = urlopen(url, timeout=5.) file_size = int(remote_file.headers['Content-Length'].strip()) size_str = _sizeof_fmt(file_size) print('Try %i. Download from %s (%s)' % (tries+1, url, size_str)) # Downloading data (can be extended to resume if need be) local_file = open(temp_file_name, "wb") _chunk_read(remote_file, local_file, initial_size=initial_size) # temp file must be closed prior to the move if not local_file.closed: local_file.close() shutil.move(temp_file_name, file_name) if print_destination is True: sys.stdout.write('File saved as %s.\n' % file_name) break except Exception as e: errors.append(e) print('Error while fetching file: %s.' % str(e)) finally: if local_file is not None: if not local_file.closed: local_file.close() else: raise IOError('Unable to download %r. Perhaps there is a no internet ' 'connection? If there is, please report this problem.' % os.path.basename(file_name)) def _chunk_read(response, local_file, chunk_size=8192, initial_size=0): """Download a file chunk by chunk and show advancement Can also be used when resuming downloads over http. Parameters ---------- response: urllib.response.addinfourl Response to the download request in order to get file size. local_file: file Hard disk file where data should be written. chunk_size: integer, optional Size of downloaded chunks. Default: 8192 initial_size: int, optional If resuming, indicate the initial size of the file. """ # Adapted from NISL: # https://github.com/nisl/tutorial/blob/master/nisl/datasets.py bytes_so_far = initial_size # Returns only amount left to download when resuming, not the size of the # entire file total_size = int(response.headers['Content-Length'].strip()) total_size += initial_size progress = StdoutProgressIndicator('Downloading') progress.start('', 'bytes', total_size) while True: chunk = response.read(chunk_size) bytes_so_far += len(chunk) if not chunk: break _chunk_write(chunk, local_file, progress) progress.finish('Done') def _chunk_write(chunk, local_file, progress): """Write a chunk to file and update the progress bar""" local_file.write(chunk) progress.increase_progress(len(chunk)) time.sleep(0.0001) def _sizeof_fmt(num): """Turn number of bytes into human-readable str""" units = ['bytes', 'kB', 'MB', 'GB', 'TB', 'PB'] decimals = [0, 0, 1, 2, 2, 2] """Human friendly file size""" if num > 1: exponent = min(int(log(num, 1024)), len(units) - 1) quotient = float(num) / 1024 ** exponent unit = units[exponent] num_decimals = decimals[exponent] format_string = '{0:.%sf} {1}' % (num_decimals) return format_string.format(quotient, unit) return '0 bytes' if num == 0 else '1 byte' ================================================ FILE: core/lib/imageio/core/findlib.py ================================================ # -*- coding: utf-8 -*- # Copyright (c) 2015, imageio contributors # Copyright (C) 2013, Zach Pincus, Almar Klein and others """ This module contains generic code to find and load a dynamic library. """ from __future__ import absolute_import, print_function, division import os import sys import ctypes LOCALDIR = os.path.abspath(os.path.dirname(__file__)) # More generic: # def get_local_lib_dirs(*libdirs): # """ Get a list of existing directories that end with one of the given # subdirs, and that are in the (sub)package that this modules is part of. # """ # dirs = [] # parts = __name__.split('.') # for i in reversed(range(len(parts))): # package_name = '.'.join(parts[:i]) # package = sys.modules.get(package_name, None) # if package: # dirs.append(os.path.abspath(os.path.dirname(package.__file__))) # dirs = [os.path.join(d, sub) for sub in libdirs for d in dirs] # return [d for d in dirs if os.path.isdir(d)] def looks_lib(fname): """ Returns True if the given filename looks like a dynamic library. Based on extension, but cross-platform and more flexible. """ fname = fname.lower() if sys.platform.startswith('win'): return fname.endswith('.dll') elif sys.platform.startswith('darwin'): return fname.endswith('.dylib') else: return fname.endswith('.so') or '.so.' in fname def generate_candidate_libs(lib_names, lib_dirs=None): """ Generate a list of candidate filenames of what might be the dynamic library corresponding with the given list of names. Returns (lib_dirs, lib_paths) """ lib_dirs = lib_dirs or [] # Get system dirs to search sys_lib_dirs = ['/lib', '/usr/lib', '/usr/lib/x86_64-linux-gnu', '/usr/local/lib', '/opt/local/lib', ] # Get Python dirs to search (shared if for Pyzo) py_sub_dirs = ['lib', 'DLLs', 'Library/bin', 'shared'] py_lib_dirs = [os.path.join(sys.prefix, d) for d in py_sub_dirs] if hasattr(sys, 'base_prefix'): py_lib_dirs += [os.path.join(sys.base_prefix, d) for d in py_sub_dirs] # Get user dirs to search (i.e. HOME) home_dir = os.path.expanduser('~') user_lib_dirs = [os.path.join(home_dir, d) for d in ['lib']] # Select only the dirs for which a directory exists, and remove duplicates potential_lib_dirs = lib_dirs + sys_lib_dirs + py_lib_dirs + user_lib_dirs lib_dirs = [] for ld in potential_lib_dirs: if os.path.isdir(ld) and ld not in lib_dirs: lib_dirs.append(ld) # Now attempt to find libraries of that name in the given directory # (case-insensitive) lib_paths = [] for lib_dir in lib_dirs: # Get files, prefer short names, last version files = os.listdir(lib_dir) files = reversed(sorted(files)) files = sorted(files, key=len) for lib_name in lib_names: # Test all filenames for name and ext for fname in files: if fname.lower().startswith(lib_name) and looks_lib(fname): lib_paths.append(os.path.join(lib_dir, fname)) # Return (only the items which are files) lib_paths = [lp for lp in lib_paths if os.path.isfile(lp)] return lib_dirs, lib_paths def load_lib(exact_lib_names, lib_names, lib_dirs=None): """ load_lib(exact_lib_names, lib_names, lib_dirs=None) Load a dynamic library. This function first tries to load the library from the given exact names. When that fails, it tries to find the library in common locations. It searches for files that start with one of the names given in lib_names (case insensitive). The search is performed in the given lib_dirs and a set of common library dirs. Returns ``(ctypes_library, library_path)`` """ # Checks assert isinstance(exact_lib_names, list) assert isinstance(lib_names, list) if lib_dirs is not None: assert isinstance(lib_dirs, list) exact_lib_names = [n for n in exact_lib_names if n] lib_names = [n for n in lib_names if n] # Get reference name (for better messages) if lib_names: the_lib_name = lib_names[0] elif exact_lib_names: the_lib_name = exact_lib_names[0] else: raise ValueError("No library name given.") # Collect filenames of potential libraries # First try a few bare library names that ctypes might be able to find # in the default locations for each platform. lib_dirs, lib_paths = generate_candidate_libs(lib_names, lib_dirs) lib_paths = exact_lib_names + lib_paths # Select loader if sys.platform.startswith('win'): loader = ctypes.windll else: loader = ctypes.cdll # Try to load until success the_lib = None errors = [] for fname in lib_paths: try: the_lib = loader.LoadLibrary(fname) break except Exception: # Don't record errors when it couldn't load the library from an # exact name -- this fails often, and doesn't provide any useful # debugging information anyway, beyond "couldn't find library..." if fname not in exact_lib_names: # Get exception instance in Python 2.x/3.x compatible manner e_type, e_value, e_tb = sys.exc_info() del e_tb errors.append((fname, e_value)) # No success ... if the_lib is None: if errors: # No library loaded, and load-errors reported for some # candidate libs err_txt = ['%s:\n%s' % (l, str(e)) for l, e in errors] msg = ('One or more %s libraries were found, but ' + 'could not be loaded due to the following errors:\n%s') raise OSError(msg % (the_lib_name, '\n\n'.join(err_txt))) else: # No errors, because no potential libraries found at all! msg = 'Could not find a %s library in any of:\n%s' raise OSError(msg % (the_lib_name, '\n'.join(lib_dirs))) # Done return the_lib, fname ================================================ FILE: core/lib/imageio/core/format.py ================================================ # -*- coding: utf-8 -*- # Copyright (c) 2015, imageio contributors # imageio is distributed under the terms of the (new) BSD License. """ .. note:: imageio is under construction, some details with regard to the Reader and Writer classes may change. These are the main classes of imageio. They expose an interface for advanced users and plugin developers. A brief overview: * imageio.FormatManager - for keeping track of registered formats. * imageio.Format - representation of a file format reader/writer * imageio.Format.Reader - object used during the reading of a file. * imageio.Format.Writer - object used during saving a file. * imageio.Request - used to store the filename and other info. Plugins need to implement a Format class and register a format object using ``imageio.formats.add_format()``. """ from __future__ import absolute_import, print_function, division # todo: do we even use the known extensions? # Some notes: # # The classes in this module use the Request object to pass filename and # related info around. This request object is instantiated in # imageio.get_reader and imageio.get_writer. from __future__ import with_statement import os import numpy as np from . import Image, asarray from . import string_types, text_type, binary_type # noqa class Format: """ Represents an implementation to read/write a particular file format A format instance is responsible for 1) providing information about a format; 2) determining whether a certain file can be read/written with this format; 3) providing a reader/writer class. Generally, imageio will select the right format and use that to read/write an image. A format can also be explicitly chosen in all read/write functions. Use ``print(format)``, or ``help(format_name)`` to see its documentation. To implement a specific format, one should create a subclass of Format and the Format.Reader and Format.Writer classes. see :doc:`plugins` for details. Parameters ---------- name : str A short name of this format. Users can select a format using its name. description : str A one-line description of the format. extensions : str | list | None List of filename extensions that this format supports. If a string is passed it should be space or comma separated. The extensions are used in the documentation and to allow users to select a format by file extension. It is not used to determine what format to use for reading/saving a file. modes : str A string containing the modes that this format can handle ('iIvV'). This attribute is used in the documentation and to select the formats when reading/saving a file. """ def __init__(self, name, description, extensions=None, modes=None): # Store name and description self._name = name.upper() self._description = description # Store extensions, do some effort to normalize them. # They are stored as a list of lowercase strings without leading dots. if extensions is None: extensions = [] elif isinstance(extensions, string_types): extensions = extensions.replace(',', ' ').split(' ') # if isinstance(extensions, (tuple, list)): self._extensions = tuple(['.' + e.strip('.').lower() for e in extensions if e]) else: raise ValueError('Invalid value for extensions given.') # Store mode self._modes = modes or '' if not isinstance(self._modes, string_types): raise ValueError('Invalid value for modes given.') for m in self._modes: if m not in 'iIvV?': raise ValueError('Invalid value for mode given.') def __repr__(self): # Short description return '' % (self.name, self.description) def __str__(self): return self.doc @property def doc(self): """ The documentation for this format (name + description + docstring). """ # Our docsring is assumed to be indented by four spaces. The # first line needs special attention. return '%s - %s\n\n %s\n' % (self.name, self.description, self.__doc__.strip()) @property def name(self): """ The name of this format. """ return self._name @property def description(self): """ A short description of this format. """ return self._description @property def extensions(self): """ A list of file extensions supported by this plugin. These are all lowercase with a leading dot. """ return self._extensions @property def modes(self): """ A string specifying the modes that this format can handle. """ return self._modes def get_reader(self, request): """ get_reader(request) Return a reader object that can be used to read data and info from the given file. Users are encouraged to use imageio.get_reader() instead. """ select_mode = request.mode[1] if request.mode[1] in 'iIvV' else '' if select_mode not in self.modes: raise RuntimeError('Format %s cannot read in mode %r' % (self.name, select_mode)) return self.Reader(self, request) def get_writer(self, request): """ get_writer(request) Return a writer object that can be used to write data and info to the given file. Users are encouraged to use imageio.get_writer() instead. """ select_mode = request.mode[1] if request.mode[1] in 'iIvV' else '' if select_mode not in self.modes: raise RuntimeError('Format %s cannot write in mode %r' % (self.name, select_mode)) return self.Writer(self, request) def can_read(self, request): """ can_read(request) Get whether this format can read data from the specified uri. """ return self._can_read(request) def can_write(self, request): """ can_write(request) Get whether this format can write data to the speciefed uri. """ return self._can_write(request) def _can_read(self, request): # pragma: no cover return None # Plugins must implement this def _can_write(self, request): # pragma: no cover return None # Plugins must implement this # ----- class _BaseReaderWriter(object): """ Base class for the Reader and Writer class to implement common functionality. It implements a similar approach for opening/closing and context management as Python's file objects. """ def __init__(self, format, request): self.__closed = False self._BaseReaderWriter_last_index = -1 self._format = format self._request = request # Open the reader/writer self._open(**self.request.kwargs.copy()) @property def format(self): """ The :class:`.Format` object corresponding to the current read/write operation. """ return self._format @property def request(self): """ The :class:`.Request` object corresponding to the current read/write operation. """ return self._request def __enter__(self): self._checkClosed() return self def __exit__(self, type, value, traceback): if value is None: # Otherwise error in close hide the real error. self.close() def __del__(self): try: self.close() except Exception: # pragma: no cover pass # Supress noise when called during interpreter shutdown def close(self): """ Flush and close the reader/writer. This method has no effect if it is already closed. """ if self.__closed: return self.__closed = True self._close() # Process results and clean request object self.request.finish() @property def closed(self): """ Whether the reader/writer is closed. """ return self.__closed def _checkClosed(self, msg=None): """Internal: raise an ValueError if reader/writer is closed """ if self.closed: what = self.__class__.__name__ msg = msg or ("I/O operation on closed %s." % what) raise RuntimeError(msg) # To implement def _open(self, **kwargs): """ _open(**kwargs) Plugins should probably implement this. It is called when reader/writer is created. Here the plugin can do its initialization. The given keyword arguments are those that were given by the user at imageio.read() or imageio.write(). """ raise NotImplementedError() def _close(self): """ _close() Plugins should probably implement this. It is called when the reader/writer is closed. Here the plugin can do a cleanup, flush, etc. """ raise NotImplementedError() # ----- class Reader(_BaseReaderWriter): """ The purpose of a reader object is to read data from an image resource, and should be obtained by calling :func:`.get_reader`. A reader can be used as an iterator to read multiple images, and (if the format permits) only reads data from the file when new data is requested (i.e. streaming). A reader can also be used as a context manager so that it is automatically closed. Plugins implement Reader's for different formats. Though rare, plugins may provide additional functionality (beyond what is provided by the base reader class). """ def get_length(self): """ get_length() Get the number of images in the file. (Note: you can also use ``len(reader_object)``.) The result can be: * 0 for files that only have meta data * 1 for singleton images (e.g. in PNG, JPEG, etc.) * N for image series * inf for streams (series of unknown length) """ return self._get_length() def get_data(self, index, **kwargs): """ get_data(index, **kwargs) Read image data from the file, using the image index. The returned image has a 'meta' attribute with the meta data. Some formats may support additional keyword arguments. These are listed in the documentation of those formats. """ self._checkClosed() self._BaseReaderWriter_last_index = index im, meta = self._get_data(index, **kwargs) return Image(im, meta) # Image tests im and meta def get_next_data(self, **kwargs): """ get_next_data(**kwargs) Read the next image from the series. Some formats may support additional keyword arguments. These are listed in the documentation of those formats. """ return self.get_data(self._BaseReaderWriter_last_index+1, **kwargs) def get_meta_data(self, index=None): """ get_meta_data(index=None) Read meta data from the file. using the image index. If the index is omitted or None, return the file's (global) meta data. Note that ``get_data`` also provides the meta data for the returned image as an atrribute of that image. The meta data is a dict, which shape depends on the format. E.g. for JPEG, the dict maps group names to subdicts and each group is a dict with name-value pairs. The groups represent the different metadata formats (EXIF, XMP, etc.). """ self._checkClosed() meta = self._get_meta_data(index) if not isinstance(meta, dict): raise ValueError('Meta data must be a dict, not %r' % meta.__class__.__name__) return meta def iter_data(self): """ iter_data() Iterate over all images in the series. (Note: you can also iterate over the reader object.) """ self._checkClosed() i, n = 0, self.get_length() while i < n: try: im, meta = self._get_data(i) except IndexError: if n == float('inf'): return raise yield Image(im, meta) i += 1 # Compatibility def __iter__(self): return self.iter_data() def __len__(self): return self.get_length() # To implement def _get_length(self): """ _get_length() Plugins must implement this. The retured scalar specifies the number of images in the series. See Reader.get_length for more information. """ raise NotImplementedError() def _get_data(self, index): """ _get_data() Plugins must implement this, but may raise an IndexError in case the plugin does not support random access. It should return the image and meta data: (ndarray, dict). """ raise NotImplementedError() def _get_meta_data(self, index): """ _get_meta_data(index) Plugins must implement this. It should return the meta data as a dict, corresponding to the given index, or to the file's (global) meta data if index is None. """ raise NotImplementedError() # ----- class Writer(_BaseReaderWriter): """ The purpose of a writer object is to write data to an image resource, and should be obtained by calling :func:`.get_writer`. A writer will (if the format permits) write data to the file as soon as new data is provided (i.e. streaming). A writer can also be used as a context manager so that it is automatically closed. Plugins implement Writer's for different formats. Though rare, plugins may provide additional functionality (beyond what is provided by the base writer class). """ def append_data(self, im, meta=None): """ append_data(im, meta={}) Append an image (and meta data) to the file. The final meta data that is used consists of the meta data on the given image (if applicable), updated with the given meta data. """ self._checkClosed() # Check image data if not isinstance(im, np.ndarray): raise ValueError('append_data requires ndarray as first arg') # Get total meta dict total_meta = {} if hasattr(im, 'meta') and isinstance(im.meta, dict): total_meta.update(im.meta) if meta is None: pass elif not isinstance(meta, dict): raise ValueError('Meta must be a dict.') else: total_meta.update(meta) # Decouple meta info im = asarray(im) # Call return self._append_data(im, total_meta) def set_meta_data(self, meta): """ set_meta_data(meta) Sets the file's (global) meta data. The meta data is a dict which shape depends on the format. E.g. for JPEG the dict maps group names to subdicts, and each group is a dict with name-value pairs. The groups represents the different metadata formats (EXIF, XMP, etc.). Note that some meta formats may not be supported for writing, and individual fields may be ignored without warning if they are invalid. """ self._checkClosed() if not isinstance(meta, dict): raise ValueError('Meta must be a dict.') else: return self._set_meta_data(meta) # To implement def _append_data(self, im, meta): # Plugins must implement this raise NotImplementedError() def _set_meta_data(self, meta): # Plugins must implement this raise NotImplementedError() class FormatManager: """ There is exactly one FormatManager object in imageio: ``imageio.formats``. Its purpose it to keep track of the registered formats. The format manager supports getting a format object using indexing (by format name or extension). When used as an iterator, this object yields all registered format objects. See also :func:`.help`. """ def __init__(self): self._formats = [] def __repr__(self): return '' % len(self) def __iter__(self): return iter(self._formats) def __len__(self): return len(self._formats) def __str__(self): ss = [] for format in self._formats: ext = ', '.join(format.extensions) s = '%s - %s [%s]' % (format.name, format.description, ext) ss.append(s) return '\n'.join(ss) def __getitem__(self, name): # Check if not isinstance(name, string_types): raise ValueError('Looking up a format should be done by name ' 'or by extension.') # Test if name is existing file if os.path.isfile(name): from . import Request format = self.search_read_format(Request(name, 'r?')) if format is not None: return format if '.' in name: # Look for extension e1, e2 = os.path.splitext(name.lower()) name = e2 or e1 # Search for format that supports this extension for format in self._formats: if name in format.extensions: return format else: # Look for name name = name.upper() for format in self._formats: if name == format.name: return format else: # Maybe the user meant to specify an extension return self['.'+name.lower()] # Nothing found ... raise IndexError('No format known by name %s.' % name) def add_format(self, format, overwrite=False): """ add_format(format, overwrite=False) Register a format, so that imageio can use it. If a format with the same name already exists, an error is raised, unless overwrite is True, in which case the current format is replaced. """ if not isinstance(format, Format): raise ValueError('add_format needs argument to be a Format object') elif format in self._formats: raise ValueError('Given Format instance is already registered') elif format.name in self.get_format_names(): if overwrite: self._formats.remove(self[format.name]) else: raise ValueError('A Format named %r is already registered, use' ' overwrite=True to replace.' % format.name) self._formats.append(format) def search_read_format(self, request): """ search_read_format(request) Search a format that can read a file according to the given request. Returns None if no appropriate format was found. (used internally) """ select_mode = request.mode[1] if request.mode[1] in 'iIvV' else '' select_ext = request.filename.lower() # Select formats that seem to be able to read it selected_formats = [] for format in self._formats: if select_mode in format.modes: if select_ext.endswith(format.extensions): selected_formats.append(format) # Select the first that can for format in selected_formats: if format.can_read(request): return format # If no format could read it, it could be that file has no or # the wrong extension. We ask all formats again. for format in self._formats: if format not in selected_formats: if format.can_read(request): return format def search_write_format(self, request): """ search_write_format(request) Search a format that can write a file according to the given request. Returns None if no appropriate format was found. (used internally) """ select_mode = request.mode[1] if request.mode[1] in 'iIvV' else '' select_ext = request.filename.lower() # Select formats that seem to be able to write it selected_formats = [] for format in self._formats: if select_mode in format.modes: if select_ext.endswith(format.extensions): selected_formats.append(format) # Select the first that can for format in selected_formats: if format.can_write(request): return format # If none of the selected formats could write it, maybe another # format can still write it. It might prefer a different mode, # or be able to handle more formats than it says by its extensions. for format in self._formats: if format not in selected_formats: if format.can_write(request): return format def get_format_names(self): """ Get the names of all registered formats. """ return [f.name for f in self._formats] def show(self): """ Show a nicely formatted list of available formats """ print(self) ================================================ FILE: core/lib/imageio/core/functions.py ================================================ # -*- coding: utf-8 -*- # Copyright (c) 2015, imageio contributors # imageio is distributed under the terms of the (new) BSD License. """ These functions represent imageio's main interface for the user. They provide a common API to read and write image data for a large variety of formats. All read and write functions accept keyword arguments, which are passed on to the format that does the actual work. To see what keyword arguments are supported by a specific format, use the :func:`.help` function. Functions for reading: * :func:`.imread` - read an image from the specified uri * :func:`.mimread` - read a series of images from the specified uri * :func:`.volread` - read a volume from the specified uri * :func:`.mvolread` - read a series of volumes from the specified uri Functions for saving: * :func:`.imwrite` - write an image to the specified uri * :func:`.mimwrite` - write a series of images to the specified uri * :func:`.volwrite` - write a volume to the specified uri * :func:`.mvolwrite` - write a series of volumes to the specified uri More control: For a larger degree of control, imageio provides functions :func:`.get_reader` and :func:`.get_writer`. They respectively return an :class:`.Reader` and an :class:`.Writer` object, which can be used to read/write data and meta data in a more controlled manner. This also allows specific scientific formats to be exposed in a way that best suits that file-format. .. note:: Some of these functions were renamed in v1.1 to realize a more clear and consistent API. The old functions are still available for backward compatibility (and will be in the foreseeable future). """ from __future__ import absolute_import, print_function, division import numpy as np from . import Request from .. import formats def help(name=None): """ help(name=None) Print the documentation of the format specified by name, or a list of supported formats if name is omitted. Parameters ---------- name : str Can be the name of a format, a filename extension, or a full filename. See also the :doc:`formats page `. """ if not name: print(formats) else: print(formats[name]) ## Base functions that return a reader/writer def get_reader(uri, format=None, mode='?', **kwargs): """ get_reader(uri, format=None, mode='?', **kwargs) Returns a :class:`.Reader` object which can be used to read data and meta data from the specified file. Parameters ---------- uri : {str, bytes, file} The resource to load the image from. This can be a normal filename, a file in a zipfile, an http/ftp address, a file object, or the raw bytes. format : str The format to use to read the file. By default imageio selects the appropriate for you based on the filename and its contents. mode : {'i', 'I', 'v', 'V', '?'} Used to give the reader a hint on what the user expects (default "?"): "i" for an image, "I" for multiple images, "v" for a volume, "V" for multiple volumes, "?" for don't care. kwargs : ... Further keyword arguments are passed to the reader. See :func:`.help` to see what arguments are available for a particular format. """ # Create request object request = Request(uri, 'r' + mode, **kwargs) # Get format if format is not None: format = formats[format] else: format = formats.search_read_format(request) if format is None: raise ValueError('Could not find a format to read the specified file ' 'in mode %r' % mode) # Return its reader object return format.get_reader(request) def get_writer(uri, format=None, mode='?', **kwargs): """ get_writer(uri, format=None, mode='?', **kwargs) Returns a :class:`.Writer` object which can be used to write data and meta data to the specified file. Parameters ---------- uri : {str, file} The resource to write the image to. This can be a normal filename, a file in a zipfile, a file object, or ``imageio.RETURN_BYTES``, in which case the raw bytes are returned. format : str The format to use to read the file. By default imageio selects the appropriate for you based on the filename. mode : {'i', 'I', 'v', 'V', '?'} Used to give the writer a hint on what the user expects (default '?'): "i" for an image, "I" for multiple images, "v" for a volume, "V" for multiple volumes, "?" for don't care. kwargs : ... Further keyword arguments are passed to the writer. See :func:`.help` to see what arguments are available for a particular format. """ # Create request object request = Request(uri, 'w' + mode, **kwargs) # Get format if format is not None: format = formats[format] else: format = formats.search_write_format(request) if format is None: raise ValueError('Could not find a format to write the specified file ' 'in mode %r' % mode) # Return its writer object return format.get_writer(request) ## Images def imread(uri, format=None, **kwargs): """ imread(uri, format=None, **kwargs) Reads an image from the specified file. Returns a numpy array, which comes with a dict of meta data at its 'meta' attribute. Note that the image data is returned as-is, and may not always have a dtype of uint8 (and thus may differ from what e.g. PIL returns). Parameters ---------- uri : {str, bytes, file} The resource to load the image from. This can be a normal filename, a file in a zipfile, an http/ftp address, a file object, or the raw bytes. format : str The format to use to read the file. By default imageio selects the appropriate for you based on the filename and its contents. kwargs : ... Further keyword arguments are passed to the reader. See :func:`.help` to see what arguments are available for a particular format. """ # Get reader and read first reader = read(uri, format, 'i', **kwargs) with reader: return reader.get_data(0) def imwrite(uri, im, format=None, **kwargs): """ imwrite(uri, im, format=None, **kwargs) Write an image to the specified file. Parameters ---------- uri : {str, file} The resource to write the image to. This can be a normal filename, a file in a zipfile, a file object, or ``imageio.RETURN_BYTES``, in which case the raw bytes are returned. im : numpy.ndarray The image data. Must be NxM, NxMx3 or NxMx4. format : str The format to use to read the file. By default imageio selects the appropriate for you based on the filename and its contents. kwargs : ... Further keyword arguments are passed to the writer. See :func:`.help` to see what arguments are available for a particular format. """ # Test image if isinstance(im, np.ndarray): if im.ndim == 2: pass elif im.ndim == 3 and im.shape[2] in [1, 3, 4]: pass else: raise ValueError('Image must be 2D (grayscale, RGB, or RGBA).') else: raise ValueError('Image must be a numpy array.') # Get writer and write first writer = get_writer(uri, format, 'i', **kwargs) with writer: writer.append_data(im) # Return a result if there is any return writer.request.get_result() ## Multiple images def mimread(uri, format=None, **kwargs): """ mimread(uri, format=None, **kwargs) Reads multiple images from the specified file. Returns a list of numpy arrays, each with a dict of meta data at its 'meta' attribute. Parameters ---------- uri : {str, bytes, file} The resource to load the images from. This can be a normal filename, a file in a zipfile, an http/ftp address, a file object, or the raw bytes. format : str The format to use to read the file. By default imageio selects the appropriate for you based on the filename and its contents. kwargs : ... Further keyword arguments are passed to the reader. See :func:`.help` to see what arguments are available for a particular format. Memory consumption ------------------ This function will raise a RuntimeError when the read data consumes over 256 MB of memory. This is to protect the system using so much memory that it needs to resort to swapping, and thereby stall the computer. E.g. ``mimread('hunger_games.avi')``. """ # Get reader reader = read(uri, format, 'I', **kwargs) # Read ims = [] nbytes = 0 for im in reader: ims.append(im) # Memory check nbytes += im.nbytes if nbytes > 256 * 1024 * 1024: ims[:] = [] # clear to free the memory raise RuntimeError('imageio.mimread() has read over 256 MiB of ' 'image data.\nStopped to avoid memory problems.' ' Use imageio.get_reader() instead.') return ims def mimwrite(uri, ims, format=None, **kwargs): """ mimwrite(uri, ims, format=None, **kwargs) Write multiple images to the specified file. Parameters ---------- uri : {str, file} The resource to write the images to. This can be a normal filename, a file in a zipfile, a file object, or ``imageio.RETURN_BYTES``, in which case the raw bytes are returned. ims : sequence of numpy arrays The image data. Each array must be NxM, NxMx3 or NxMx4. format : str The format to use to read the file. By default imageio selects the appropriate for you based on the filename and its contents. kwargs : ... Further keyword arguments are passed to the writer. See :func:`.help` to see what arguments are available for a particular format. """ # Get writer writer = get_writer(uri, format, 'I', **kwargs) with writer: # Iterate over images (ims may be a generator) for im in ims: # Test image if isinstance(im, np.ndarray): if im.ndim == 2: pass elif im.ndim == 3 and im.shape[2] in [1, 3, 4]: pass else: raise ValueError('Image must be 2D ' '(grayscale, RGB, or RGBA).') else: raise ValueError('Image must be a numpy array.') # Add image writer.append_data(im) # Return a result if there is any return writer.request.get_result() ## Volumes def volread(uri, format=None, **kwargs): """ volread(uri, format=None, **kwargs) Reads a volume from the specified file. Returns a numpy array, which comes with a dict of meta data at its 'meta' attribute. Parameters ---------- uri : {str, bytes, file} The resource to load the volume from. This can be a normal filename, a file in a zipfile, an http/ftp address, a file object, or the raw bytes. format : str The format to use to read the file. By default imageio selects the appropriate for you based on the filename and its contents. kwargs : ... Further keyword arguments are passed to the reader. See :func:`.help` to see what arguments are available for a particular format. """ # Get reader and read first reader = read(uri, format, 'v', **kwargs) with reader: return reader.get_data(0) def volwrite(uri, im, format=None, **kwargs): """ volwrite(uri, vol, format=None, **kwargs) Write a volume to the specified file. Parameters ---------- uri : {str, file} The resource to write the image to. This can be a normal filename, a file in a zipfile, a file object, or ``imageio.RETURN_BYTES``, in which case the raw bytes are returned. vol : numpy.ndarray The image data. Must be NxMxL (or NxMxLxK if each voxel is a tuple). format : str The format to use to read the file. By default imageio selects the appropriate for you based on the filename and its contents. kwargs : ... Further keyword arguments are passed to the writer. See :func:`.help` to see what arguments are available for a particular format. """ # Test image if isinstance(im, np.ndarray): if im.ndim == 3: pass elif im.ndim == 4 and im.shape[3] < 32: # How large can a tuple be? pass else: raise ValueError('Image must be 3D, or 4D if each voxel is ' 'a tuple.') else: raise ValueError('Image must be a numpy array.') # Get writer and write first writer = get_writer(uri, format, 'v', **kwargs) with writer: writer.append_data(im) # Return a result if there is any return writer.request.get_result() ## Multiple volumes def mvolread(uri, format=None, **kwargs): """ mvolread(uri, format=None, **kwargs) Reads multiple volumes from the specified file. Returns a list of numpy arrays, each with a dict of meta data at its 'meta' attribute. Parameters ---------- uri : {str, bytes, file} The resource to load the volumes from. This can be a normal filename, a file in a zipfile, an http/ftp address, a file object, or the raw bytes. format : str The format to use to read the file. By default imageio selects the appropriate for you based on the filename and its contents. kwargs : ... Further keyword arguments are passed to the reader. See :func:`.help` to see what arguments are available for a particular format. """ # Get reader and read all reader = read(uri, format, 'V', **kwargs) ims = [] nbytes = 0 for im in reader: ims.append(im) # Memory check nbytes += im.nbytes if nbytes > 1024 * 1024 * 1024: # pragma: no cover ims[:] = [] # clear to free the memory raise RuntimeError('imageio.mvolread() has read over 1 GiB of ' 'image data.\nStopped to avoid memory problems.' ' Use imageio.get_reader() instead.') return ims def mvolwrite(uri, ims, format=None, **kwargs): """ mvolwrite(uri, vols, format=None, **kwargs) Write multiple volumes to the specified file. Parameters ---------- uri : {str, file} The resource to write the volumes to. This can be a normal filename, a file in a zipfile, a file object, or ``imageio.RETURN_BYTES``, in which case the raw bytes are returned. ims : sequence of numpy arrays The image data. Each array must be NxMxL (or NxMxLxK if each voxel is a tuple). format : str The format to use to read the file. By default imageio selects the appropriate for you based on the filename and its contents. kwargs : ... Further keyword arguments are passed to the writer. See :func:`.help` to see what arguments are available for a particular format. """ # Get writer writer = get_writer(uri, format, 'V', **kwargs) with writer: # Iterate over images (ims may be a generator) for im in ims: # Test image if isinstance(im, np.ndarray): if im.ndim == 3: pass elif im.ndim == 4 and im.shape[3] < 32: pass # How large can a tuple be? else: raise ValueError('Image must be 3D, or 4D if each voxel is' 'a tuple.') else: raise ValueError('Image must be a numpy array.') # Add image writer.append_data(im) # Return a result if there is any return writer.request.get_result() ## Aliases read = get_reader save = get_writer imsave = imwrite mimsave = mimwrite volsave = volwrite mvolsave = mvolwrite ================================================ FILE: core/lib/imageio/core/request.py ================================================ # -*- coding: utf-8 -*- # Copyright (c) 2015, imageio contributors # imageio is distributed under the terms of the (new) BSD License. """ Definition of the Request object, which acts as a kind of bridge between what the user wants and what the plugins can. """ from __future__ import absolute_import, print_function, division import sys import os from io import BytesIO import zipfile import tempfile import shutil from ..core import string_types, binary_type, urlopen, get_remote_file # URI types URI_BYTES = 1 URI_FILE = 2 URI_FILENAME = 3 URI_ZIPPED = 4 URI_HTTP = 5 URI_FTP = 6 # The user can use this string in a write call to get the data back as bytes. RETURN_BYTES = '' # Example images that will be auto-downloaded EXAMPLE_IMAGES = { 'astronaut.png': 'Image of the astronaut Eileen Collins', 'camera.png': 'Classic grayscale image of a photographer', 'checkerboard.png': 'Black and white image of a chekerboard', 'clock.png': 'Photo of a clock with motion blur (Stefan van der Walt)', 'coffee.png': 'Image of a cup of coffee (Rachel Michetti)', 'chelsea.png': 'Image of Stefan\'s cat', 'wikkie.png': 'Image of Almar\'s cat', 'coins.png': 'Image showing greek coins from Pompeii', 'horse.png': 'Image showing the silhouette of a horse (Andreas Preuss)', 'hubble_deep_field.png': 'Photograph taken by Hubble telescope (NASA)', 'immunohistochemistry.png': 'Immunohistochemical (IHC) staining', 'lena.png': 'Classic but sometimes controversioal Lena test image', 'moon.png': 'Image showing a portion of the surface of the moon', 'page.png': 'A scanned page of text', 'text.png': 'A photograph of handdrawn text', 'chelsea.zip': 'The chelsea.png in a zipfile (for testing)', 'newtonscradle.gif': 'Animated GIF of a newton\'s cradle', 'cockatoo.mp4': 'Video file of a cockatoo', 'stent.npz': 'Volumetric image showing a stented abdominal aorta', } class Request(object): """ Request(uri, mode, **kwargs) Represents a request for reading or saving an image resource. This object wraps information to that request and acts as an interface for the plugins to several resources; it allows the user to read from filenames, files, http, zipfiles, raw bytes, etc., but offer a simple interface to the plugins via ``get_file()`` and ``get_local_filename()``. For each read/write operation a single Request instance is used and passed to the can_read/can_write method of a format, and subsequently to the Reader/Writer class. This allows rudimentary passing of information between different formats and between a format and associated reader/writer. parameters ---------- uri : {str, bytes, file} The resource to load the image from. mode : str The first character is "r" or "w", indicating a read or write request. The second character is used to indicate the kind of data: "i" for an image, "I" for multiple images, "v" for a volume, "V" for multiple volumes, "?" for don't care. """ def __init__(self, uri, mode, **kwargs): # General self._uri_type = None self._filename = None self._kwargs = kwargs self._result = None # Some write actions may have a result # To handle the user-side self._filename_zip = None # not None if a zipfile is used self._bytes = None # Incoming bytes self._zipfile = None # To store a zipfile instance (if used) # To handle the plugin side self._file = None # To store the file instance self._filename_local = None # not None if using tempfile on this FS self._firstbytes = None # For easy header parsing # To store formats that may be able to fulfil this request #self._potential_formats = [] # Check mode self._mode = mode if not isinstance(mode, string_types): raise ValueError('Request requires mode must be a string') if not len(mode) == 2: raise ValueError('Request requires mode to have two chars') if mode[0] not in 'rw': raise ValueError('Request requires mode[0] to be "r" or "w"') if mode[1] not in 'iIvV?': raise ValueError('Request requires mode[1] to be in "iIvV?"') # Parse what was given self._parse_uri(uri) def _parse_uri(self, uri): """ Try to figure our what we were given """ py3k = sys.version_info[0] == 3 is_read_request = self.mode[0] == 'r' is_write_request = self.mode[0] == 'w' if isinstance(uri, string_types): # Explicit if uri.startswith('http://') or uri.startswith('https://'): self._uri_type = URI_HTTP self._filename = uri elif uri.startswith('ftp://') or uri.startswith('ftps://'): self._uri_type = URI_FTP self._filename = uri elif uri.startswith('file://'): self._uri_type = URI_FILENAME self._filename = uri[7:] elif uri.startswith(' 0: zip_i += 4 self._uri_type = URI_ZIPPED self._filename_zip = (self._filename[:zip_i], self._filename[zip_i:].lstrip('/\\')) break # Check if we could read it if self._uri_type is None: uri_r = repr(uri) if len(uri_r) > 60: uri_r = uri_r[:57] + '...' raise IOError("Cannot understand given URI: %s." % uri_r) # Check if this is supported noWriting = [URI_HTTP, URI_FTP] if is_write_request and self._uri_type in noWriting: raise IOError('imageio does not support writing to http/ftp.') # Check if an example image if is_read_request and self._uri_type in [URI_FILENAME, URI_ZIPPED]: fn = self._filename if self._filename_zip: fn = self._filename_zip[0] if (not os.path.exists(fn)) and (fn in EXAMPLE_IMAGES): fn = get_remote_file('images/' + fn) self._filename = fn if self._filename_zip: self._filename_zip = fn, self._filename_zip[1] self._filename = fn + '/' + self._filename_zip[1] # Make filename absolute if self._uri_type in [URI_FILENAME, URI_ZIPPED]: if self._filename_zip: self._filename_zip = (os.path.abspath(self._filename_zip[0]), self._filename_zip[1]) else: self._filename = os.path.abspath(self._filename) # Check wether file name is valid if self._uri_type in [URI_FILENAME, URI_ZIPPED]: fn = self._filename if self._filename_zip: fn = self._filename_zip[0] if is_read_request: # Reading: check that the file exists (but is allowed a dir) if not os.path.exists(fn): raise IOError("No such file: '%s'" % fn) else: # Writing: check that the directory to write to does exist dn = os.path.dirname(fn) if not os.path.exists(dn): raise IOError("The directory %r does not exist" % dn) @property def filename(self): """ The uri for which reading/saving was requested. This can be a filename, an http address, or other resource identifier. Do not rely on the filename to obtain the data, but use ``get_file()`` or ``get_local_filename()`` instead. """ return self._filename @property def mode(self): """ The mode of the request. The first character is "r" or "w", indicating a read or write request. The second character is used to indicate the kind of data: "i" for an image, "I" for multiple images, "v" for a volume, "V" for multiple volumes, "?" for don't care. """ return self._mode @property def kwargs(self): """ The dict of keyword arguments supplied by the user. """ return self._kwargs ## For obtaining data def get_file(self): """ get_file() Get a file object for the resource associated with this request. If this is a reading request, the file is in read mode, otherwise in write mode. This method is not thread safe. Plugins do not need to close the file when done. This is the preferred way to read/write the data. But if a format cannot handle file-like objects, they should use ``get_local_filename()``. """ want_to_write = self.mode[0] == 'w' # Is there already a file? # Either _uri_type == URI_FILE, or we already opened the file, # e.g. by using firstbytes if self._file is not None: self._file.seek(0) return self._file if self._uri_type == URI_BYTES: if want_to_write: self._file = BytesIO() else: self._file = BytesIO(self._bytes) elif self._uri_type == URI_FILENAME: if want_to_write: self._file = open(self.filename, 'wb') else: self._file = open(self.filename, 'rb') elif self._uri_type == URI_ZIPPED: # Get the correct filename filename, name = self._filename_zip if want_to_write: # Create new file object, we catch the bytes in finish() self._file = BytesIO() else: # Open zipfile and open new file object for specific file self._zipfile = zipfile.ZipFile(filename, 'r') self._file = self._zipfile.open(name, 'r') elif self._uri_type in [URI_HTTP or URI_FTP]: assert not want_to_write # This should have been tested in init self._file = urlopen(self.filename, timeout=5) return self._file def get_local_filename(self): """ get_local_filename() If the filename is an existing file on this filesystem, return that. Otherwise a temporary file is created on the local file system which can be used by the format to read from or write to. """ if self._uri_type == URI_FILENAME: return self._filename else: # Get filename ext = os.path.splitext(self._filename)[1] self._filename_local = tempfile.mktemp(ext, 'imageio_') # Write stuff to it? if self.mode[0] == 'r': with open(self._filename_local, 'wb') as file: shutil.copyfileobj(self.get_file(), file) return self._filename_local def finish(self): """ finish() For internal use (called when the context of the reader/writer exits). Finishes this request. Close open files and process results. """ # Init bytes = None # Collect bytes from temp file if self.mode[0] == 'w' and self._filename_local: with open(self._filename_local, 'rb') as file: bytes = file.read() # Collect bytes from BytesIO file object. written = (self.mode[0] == 'w') and self._file if written and self._uri_type in [URI_BYTES, URI_ZIPPED]: bytes = self._file.getvalue() # Close open files that we know of (and are responsible for) if self._file and self._uri_type != URI_FILE: self._file.close() self._file = None if self._zipfile: self._zipfile.close() self._zipfile = None # Remove temp file if self._filename_local: try: os.remove(self._filename_local) except Exception: # pragma: no cover pass self._filename_local = None # Handle bytes that we collected if bytes is not None: if self._uri_type == URI_BYTES: self._result = bytes # Picked up by imread function elif self._uri_type == URI_ZIPPED: zf = zipfile.ZipFile(self._filename_zip[0], 'a') zf.writestr(self._filename_zip[1], bytes) zf.close() # Detach so gc can clean even if a reference of self lingers self._bytes = None def get_result(self): """ For internal use. In some situations a write action can have a result (bytes data). That is obtained with this function. """ self._result, res = None, self._result return res @property def firstbytes(self): """ The first 256 bytes of the file. These can be used to parse the header to determine the file-format. """ if self._firstbytes is None: self._read_first_bytes() return self._firstbytes def _read_first_bytes(self, N=256): if self._bytes is not None: self._firstbytes = self._bytes[:N] else: # Prepare f = self.get_file() try: i = f.tell() except Exception: i = None # Read self._firstbytes = read_n_bytes(f, N) # Set back try: if i is None: raise Exception('cannot seek with None') f.seek(i) except Exception: # Prevent get_file() from reusing the file self._file = None # If the given URI was a file object, we have a problem, # but that should be tested in get_file(), because we # seek() there. assert self._uri_type != URI_FILE def read_n_bytes(f, N): """ read_n_bytes(file, n) Read n bytes from the given file, or less if the file has less bytes. Returns zero bytes if the file is closed. """ bb = binary_type() while len(bb) < N: extra_bytes = f.read(N-len(bb)) if not extra_bytes: break bb += extra_bytes return bb ================================================ FILE: core/lib/imageio/core/util.py ================================================ # -*- coding: utf-8 -*- # Copyright (c) 2015, imageio contributors # imageio is distributed under the terms of the (new) BSD License. """ Various utilities for imageio """ from __future__ import absolute_import, print_function, division import re import os import sys import time import struct from warnings import warn import platform import numpy as np IS_PYPY = '__pypy__' in sys.builtin_module_names THIS_DIR = os.path.abspath(os.path.dirname(__file__)) # Taken from six.py PY3 = sys.version_info[0] == 3 if PY3: string_types = str, text_type = str binary_type = bytes else: # pragma: no cover string_types = basestring, # noqa text_type = unicode # noqa binary_type = str def urlopen(*args, **kwargs): """ Compatibility function for the urlopen function. Raises an RuntimeError if urlopen could not be imported (which can occur in frozen applications. """ try: from urllib2 import urlopen except ImportError: try: from urllib.request import urlopen # Py3k except ImportError: raise RuntimeError('Could not import urlopen.') return urlopen(*args, **kwargs) def image_as_uint(im, bitdepth=None): """ Convert the given image to uint (default: uint8) If the dtype already matches the desired format, it is returned as-is. If the image is float, and all values are between 0 and 1, the values are multiplied by np.power(2.0, bitdepth). In all other situations, the values are scaled such that the minimum value becomes 0 and the maximum value becomes np.power(2.0, bitdepth)-1 (255 for 8-bit and 65535 for 16-bit). """ if not bitdepth: bitdepth = 8 if not isinstance(im, np.ndarray): raise ValueError('Image must be a numpy array') if bitdepth == 8: out_type = np.uint8 elif bitdepth == 16: out_type = np.uint16 else: raise ValueError('Bitdepth must be either 8 or 16') dtype_str = str(im.dtype) if ((im.dtype == np.uint8 and bitdepth == 8) or (im.dtype == np.uint16 and bitdepth == 16)): # Already the correct format? Return as-is return im if (dtype_str.startswith('float') and np.nanmin(im) >= 0 and np.nanmax(im) <= 1): warn('Lossy conversion from {0} to {1}, range [0, 1]'.format( dtype_str, out_type.__name__)) im = im.astype(np.float64) * (np.power(2.0, bitdepth)-1) elif im.dtype == np.uint16 and bitdepth == 8: warn('Lossy conversion from uint16 to uint8, ' 'loosing 8 bits of resolution') im = np.right_shift(im, 8) elif im.dtype == np.uint32: warn('Lossy conversion from uint32 to {0}, ' 'loosing {1} bits of resolution'.format(out_type.__name__, 32-bitdepth)) im = np.right_shift(im, 32-bitdepth) elif im.dtype == np.uint64: warn('Lossy conversion from uint64 to {0}, ' 'loosing {1} bits of resolution'.format(out_type.__name__, 64-bitdepth)) im = np.right_shift(im, 64-bitdepth) else: mi = np.nanmin(im) ma = np.nanmax(im) if not np.isfinite(mi): raise ValueError('Minimum image value is not finite') if not np.isfinite(ma): raise ValueError('Maximum image value is not finite') if ma == mi: raise ValueError('Max value == min value, ambiguous given dtype') warn('Conversion from {0} to {1}, ' 'range [{2}, {3}]'.format(dtype_str, out_type.__name__, mi, ma)) # Now make float copy before we scale im = im.astype('float64') # Scale the values between 0 and 1 then multiply by the max value im = (im - mi) / (ma - mi) * (np.power(2.0, bitdepth)-1) assert np.nanmin(im) >= 0 assert np.nanmax(im) < np.power(2.0, bitdepth) return im.astype(out_type) # currently not used ... the only use it to easily provide the global meta info class ImageList(list): def __init__(self, meta=None): list.__init__(self) # Check if not (meta is None or isinstance(meta, dict)): raise ValueError('ImageList expects meta data to be a dict.') # Convert and return self._meta = meta if meta is not None else {} @property def meta(self): """ The dict with the meta data of this image. """ return self._meta class Image(np.ndarray): """ Image(array, meta=None) A subclass of np.ndarray that has a meta attribute. Following scikit-image, we leave this as a normal numpy array as much as we can. """ def __new__(cls, array, meta=None): # Check if not isinstance(array, np.ndarray): raise ValueError('Image expects a numpy array.') if not (meta is None or isinstance(meta, dict)): raise ValueError('Image expects meta data to be a dict.') # Convert and return meta = meta if meta is not None else {} try: ob = array.view(cls) except AttributeError: # pragma: no cover # Just return the original; no metadata on the array in Pypy! return array ob._copy_meta(meta) return ob def _copy_meta(self, meta): """ Make a 2-level deep copy of the meta dictionary. """ self._meta = Dict() for key, val in meta.items(): if isinstance(val, dict): val = Dict(val) # Copy this level self._meta[key] = val @property def meta(self): """ The dict with the meta data of this image. """ return self._meta def __array_finalize__(self, ob): """ So the meta info is maintained when doing calculations with the array. """ if isinstance(ob, Image): self._copy_meta(ob.meta) else: self._copy_meta({}) def __array_wrap__(self, out, context=None): """ So that we return a native numpy array (or scalar) when a reducting ufunc is applied (such as sum(), std(), etc.) """ if not out.shape: return out.dtype.type(out) # Scalar elif out.shape != self.shape: return out.view(type=np.ndarray) else: return out # Type Image def asarray(a): """ Pypy-safe version of np.asarray. Pypy's np.asarray consumes a *lot* of memory if the given array is an ndarray subclass. This function does not. """ if isinstance(a, np.ndarray): if IS_PYPY: # pragma: no cover a = a.copy() # pypy has issues with base views plain = a.view(type=np.ndarray) return plain return np.asarray(a) try: from collections import OrderedDict as _dict except ImportError: _dict = dict class Dict(_dict): """ A dict in which the keys can be get and set as if they were attributes. Very convenient in combination with autocompletion. This Dict still behaves as much as possible as a normal dict, and keys can be anything that are otherwise valid keys. However, keys that are not valid identifiers or that are names of the dict class (such as 'items' and 'copy') cannot be get/set as attributes. """ __reserved_names__ = dir(_dict()) # Also from OrderedDict __pure_names__ = dir(dict()) def __getattribute__(self, key): try: return object.__getattribute__(self, key) except AttributeError: if key in self: return self[key] else: raise def __setattr__(self, key, val): if key in Dict.__reserved_names__: # Either let OrderedDict do its work, or disallow if key not in Dict.__pure_names__: return _dict.__setattr__(self, key, val) else: raise AttributeError('Reserved name, this key can only ' + 'be set via ``d[%r] = X``' % key) else: # if isinstance(val, dict): val = Dict(val) -> no, makes a copy! self[key] = val def __dir__(self): isidentifier = lambda x: bool(re.match(r'[a-z_]\w*$', x, re.I)) names = [k for k in self.keys() if (isinstance(k, string_types) and isidentifier(k))] return Dict.__reserved_names__ + names class BaseProgressIndicator: """ BaseProgressIndicator(name) A progress indicator helps display the progres of a task to the user. Progress can be pending, running, finished or failed. Each task has: * a name - a short description of what needs to be done. * an action - the current action in performing the task (e.g. a subtask) * progress - how far the task is completed * max - max number of progress units. If 0, the progress is indefinite * unit - the units in which the progress is counted * status - 0: pending, 1: in progress, 2: finished, 3: failed This class defines an abstract interface. Subclasses should implement _start, _stop, _update_progress(progressText), _write(message). """ def __init__(self, name): self._name = name self._action = '' self._unit = '' self._max = 0 self._status = 0 self._last_progress_update = 0 def start(self, action='', unit='', max=0): """ start(action='', unit='', max=0) Start the progress. Optionally specify an action, a unit, and a maxium progress value. """ if self._status == 1: self.finish() self._action = action self._unit = unit self._max = max # self._progress = 0 self._status = 1 self._start() def status(self): """ status() Get the status of the progress - 0: pending, 1: in progress, 2: finished, 3: failed """ return self._status def set_progress(self, progress=0, force=False): """ set_progress(progress=0, force=False) Set the current progress. To avoid unnecessary progress updates this will only have a visual effect if the time since the last update is > 0.1 seconds, or if force is True. """ self._progress = progress # Update or not? if not (force or (time.time() - self._last_progress_update > 0.1)): return self._last_progress_update = time.time() # Compose new string unit = self._unit or '' progressText = '' if unit == '%': progressText = '%2.1f%%' % progress elif self._max > 0: percent = 100 * float(progress) / self._max progressText = '%i/%i %s (%2.1f%%)' % (progress, self._max, unit, percent) elif progress > 0: if isinstance(progress, float): progressText = '%0.4g %s' % (progress, unit) else: progressText = '%i %s' % (progress, unit) # Update self._update_progress(progressText) def increase_progress(self, extra_progress): """ increase_progress(extra_progress) Increase the progress by a certain amount. """ self.set_progress(self._progress + extra_progress) def finish(self, message=None): """ finish(message=None) Finish the progress, optionally specifying a message. This will not set the progress to the maximum. """ self.set_progress(self._progress, True) # fore update self._status = 2 self._stop() if message is not None: self._write(message) def fail(self, message=None): """ fail(message=None) Stop the progress with a failure, optionally specifying a message. """ self.set_progress(self._progress, True) # fore update self._status = 3 self._stop() message = 'FAIL ' + (message or '') self._write(message) def write(self, message): """ write(message) Write a message during progress (such as a warning). """ if self.__class__ == BaseProgressIndicator: # When this class is used as a dummy, print explicit message print(message) else: return self._write(message) # Implementing classes should implement these def _start(self): pass def _stop(self): pass def _update_progress(self, progressText): pass def _write(self, message): pass class StdoutProgressIndicator(BaseProgressIndicator): """ StdoutProgressIndicator(name) A progress indicator that shows the progress in stdout. It assumes that the tty can appropriately deal with backspace characters. """ def _start(self): self._chars_prefix, self._chars = '', '' # Write message if self._action: self._chars_prefix = '%s (%s): ' % (self._name, self._action) else: self._chars_prefix = '%s: ' % self._name sys.stdout.write(self._chars_prefix) sys.stdout.flush() def _update_progress(self, progressText): # If progress is unknown, at least make something move if not progressText: i1, i2, i3, i4 = '-\\|/' M = {i1: i2, i2: i3, i3: i4, i4: i1} progressText = M.get(self._chars, i1) # Store new string and write delChars = '\b'*len(self._chars) self._chars = progressText sys.stdout.write(delChars+self._chars) sys.stdout.flush() def _stop(self): self._chars = self._chars_prefix = '' sys.stdout.write('\n') sys.stdout.flush() def _write(self, message): # Write message delChars = '\b'*len(self._chars_prefix+self._chars) sys.stdout.write(delChars+' '+message+'\n') # Reprint progress text sys.stdout.write(self._chars_prefix+self._chars) sys.stdout.flush() # From pyzolib/paths.py (https://bitbucket.org/pyzo/pyzolib/src/tip/paths.py) def appdata_dir(appname=None, roaming=False): """ appdata_dir(appname=None, roaming=False) Get the path to the application directory, where applications are allowed to write user specific files (e.g. configurations). For non-user specific data, consider using common_appdata_dir(). If appname is given, a subdir is appended (and created if necessary). If roaming is True, will prefer a roaming directory (Windows Vista/7). """ # Define default user directory userDir = os.path.expanduser('~') if not os.path.isdir(userDir): # pragma: no cover userDir = '/var/tmp' # issue #54 # Get system app data dir path = None if sys.platform.startswith('win'): path1, path2 = os.getenv('LOCALAPPDATA'), os.getenv('APPDATA') path = (path2 or path1) if roaming else (path1 or path2) elif sys.platform.startswith('darwin'): path = os.path.join(userDir, 'Library', 'Application Support') # On Linux and as fallback if not (path and os.path.isdir(path)): path = userDir # Maybe we should store things local to the executable (in case of a # portable distro or a frozen application that wants to be portable) prefix = sys.prefix if getattr(sys, 'frozen', None): prefix = os.path.abspath(os.path.dirname(sys.path[0])) for reldir in ('settings', '../settings'): localpath = os.path.abspath(os.path.join(prefix, reldir)) if os.path.isdir(localpath): # pragma: no cover try: open(os.path.join(localpath, 'test.write'), 'wb').close() os.remove(os.path.join(localpath, 'test.write')) except IOError: pass # We cannot write in this directory else: path = localpath break # Get path specific for this app if appname: if path == userDir: appname = '.' + appname.lstrip('.') # Make it a hidden directory path = os.path.join(path, appname) if not os.path.isdir(path): # pragma: no cover os.mkdir(path) # Done return path def resource_dirs(): """ resource_dirs() Get a list of directories where imageio resources may be located. The first directory in this list is the "resources" directory in the package itself. The second directory is the appdata directory (~/.imageio on Linux). The list further contains the application directory (for frozen apps), and may include additional directories in the future. """ dirs = [] # Resource dir baked in the package dirs.append(os.path.abspath(os.path.join(THIS_DIR, '..', 'resources'))) # Appdata directory try: dirs.append(appdata_dir('imageio')) except Exception: # pragma: no cover pass # The home dir may not be writable # Directory where the app is located (mainly for frozen apps) if sys.path and sys.path[0]: # Get the path. If frozen, sys.path[0] is the name of the executable, # otherwise it is the path to the directory that contains the script. thepath = sys.path[0] if getattr(sys, 'frozen', None): thepath = os.path.dirname(thepath) dirs.append(os.path.abspath(thepath)) return dirs def get_platform(): """ get_platform() Get a string that specifies the platform more specific than sys.platform does. The result can be: linux32, linux64, win32, win64, osx32, osx64, osx-arm64. Other platforms may be added in the future. """ # Get platform if sys.platform.startswith('linux'): plat = 'linux%i' elif sys.platform.startswith('win'): plat = 'win%i' elif sys.platform.startswith('darwin'): if platform.machine() == 'arm64': plat = 'osx-arm64' else: plat = 'osx%i' else: # pragma: no cover return None # Only perform string formatting when plat contains '%i' if '%i' in plat: return plat % (struct.calcsize('P') * 8) # 32 or 64 bits else: return plat def has_module(module_name): """Check to see if a python module is available. """ if sys.version_info > (3, ): import importlib return importlib.find_loader(module_name) is not None else: # pragma: no cover import imp try: imp.find_module(module_name) except ImportError: return False return True ================================================ FILE: core/lib/imageio/freeze.py ================================================ """ Helper functions for freezing imageio. """ import sys def get_includes(): if sys.version_info[0] == 3: urllib = ['email', 'urllib.request', ] else: urllib = ['urllib2'] return urllib + ['numpy', 'zipfile', 'io'] def get_excludes(): return [] ================================================ FILE: core/lib/imageio/plugins/__init__.py ================================================ # -*- coding: utf-8 -*- # Copyright (c) 2015, imageio contributors # imageio is distributed under the terms of the (new) BSD License. """ Imagio is plugin-based. Every supported format is provided with a plugin. You can write your own plugins to make imageio support additional formats. And we would be interested in adding such code to the imageio codebase! What is a plugin ---------------- In imageio, a plugin provides one or more :class:`.Format` objects, and corresponding :class:`.Reader` and :class:`.Writer` classes. Each Format object represents an implementation to read/write a particular file format. Its Reader and Writer classes do the actual reading/saving. The reader and writer objects have a ``request`` attribute that can be used to obtain information about the read or write :class:`.Request`, such as user-provided keyword arguments, as well get access to the raw image data. Registering ----------- Strictly speaking a format can be used stand alone. However, to allow imageio to automatically select it for a specific file, the format must be registered using ``imageio.formats.add_format()``. Note that a plugin is not required to be part of the imageio package; as long as a format is registered, imageio can use it. This makes imageio very easy to extend. What methods to implement -------------------------- Imageio is designed such that plugins only need to implement a few private methods. The public API is implemented by the base classes. In effect, the public methods can be given a descent docstring which does not have to be repeated at the plugins. For the Format class, the following needs to be implemented/specified: * The format needs a short name, a description, and a list of file extensions that are common for the file-format in question. These ase set when instantiation the Format object. * Use a docstring to provide more detailed information about the format/plugin, such as parameters for reading and saving that the user can supply via keyword arguments. * Implement ``_can_read(request)``, return a bool. See also the :class:`.Request` class. * Implement ``_can_write(request)``, dito. For the Format.Reader class: * Implement ``_open(**kwargs)`` to initialize the reader. Deal with the user-provided keyword arguments here. * Implement ``_close()`` to clean up. * Implement ``_get_length()`` to provide a suitable length based on what the user expects. Can be ``inf`` for streaming data. * Implement ``_get_data(index)`` to return an array and a meta-data dict. * Implement ``_get_meta_data(index)`` to return a meta-data dict. If index is None, it should return the 'global' meta-data. For the Format.Writer class: * Implement ``_open(**kwargs)`` to initialize the writer. Deal with the user-provided keyword arguments here. * Implement ``_close()`` to clean up. * Implement ``_append_data(im, meta)`` to add data (and meta-data). * Implement ``_set_meta_data(meta)`` to set the global meta-data. """ # First import plugins that we want to take precedence over freeimage from . import freeimage # noqa ================================================ FILE: core/lib/imageio/plugins/_freeimage.py ================================================ # -*- coding: utf-8 -*- # Copyright (c) 2015, imageio contributors # imageio is distributed under the terms of the (new) BSD License. # styletest: ignore E261 """ Module imageio/freeimage.py This module contains the wrapper code for the freeimage library. The functions defined in this module are relatively thin; just thin enough so that arguments and results are native Python/numpy data types. """ from __future__ import absolute_import, print_function, with_statement import os import sys import ctypes import threading from logging import warn import numpy from ..core import (get_remote_file, load_lib, Dict, resource_dirs, string_types, binary_type, IS_PYPY, get_platform, InternetNotAllowedError) TEST_NUMPY_NO_STRIDES = False # To test pypy fallback FNAME_PER_PLATFORM = { 'osx32': 'libfreeimage-3.16.0-osx10.6.dylib', # universal library 'osx64': 'libfreeimage-3.16.0-osx10.6.dylib', 'osx-arm64': 'libfreeimage.3.18.0.dylib', 'win32': 'FreeImage-3.18.0-win32.dll', 'win64': 'FreeImage-3.18.0-win64.dll', 'linux32': 'libfreeimage-3.16.0-linux32.so', 'linux64': 'libfreeimage-3.16.0-linux64.so', } def get_freeimage_lib(): """ Ensure we have our version of the binary freeimage lib. """ lib = os.getenv('IMAGEIO_FREEIMAGE_LIB', None) if lib: # pragma: no cover return lib # Get filename to load # If we do not provide a binary, the system may still do ... plat = get_platform() if plat and plat in FNAME_PER_PLATFORM: try: #return get_remote_file('freeimage/' + FNAME_PER_PLATFORM[plat]) return get_remote_file(FNAME_PER_PLATFORM[plat]) except InternetNotAllowedError: pass except RuntimeError as e: # pragma: no cover warn(str(e)) # Define function to encode a filename to bytes (for the current system) efn = lambda x: x.encode(sys.getfilesystemencoding()) # 4-byte quads of 0,v,v,v from 0,0,0,0 to 0,255,255,255 GREY_PALETTE = numpy.arange(0, 0x01000000, 0x00010101, dtype=numpy.uint32) class FI_TYPES(object): FIT_UNKNOWN = 0 FIT_BITMAP = 1 FIT_UINT16 = 2 FIT_INT16 = 3 FIT_UINT32 = 4 FIT_INT32 = 5 FIT_FLOAT = 6 FIT_DOUBLE = 7 FIT_COMPLEX = 8 FIT_RGB16 = 9 FIT_RGBA16 = 10 FIT_RGBF = 11 FIT_RGBAF = 12 dtypes = { FIT_BITMAP: numpy.uint8, FIT_UINT16: numpy.uint16, FIT_INT16: numpy.int16, FIT_UINT32: numpy.uint32, FIT_INT32: numpy.int32, FIT_FLOAT: numpy.float32, FIT_DOUBLE: numpy.float64, FIT_COMPLEX: numpy.complex128, FIT_RGB16: numpy.uint16, FIT_RGBA16: numpy.uint16, FIT_RGBF: numpy.float32, FIT_RGBAF: numpy.float32 } fi_types = { (numpy.uint8, 1): FIT_BITMAP, (numpy.uint8, 3): FIT_BITMAP, (numpy.uint8, 4): FIT_BITMAP, (numpy.uint16, 1): FIT_UINT16, (numpy.int16, 1): FIT_INT16, (numpy.uint32, 1): FIT_UINT32, (numpy.int32, 1): FIT_INT32, (numpy.float32, 1): FIT_FLOAT, (numpy.float64, 1): FIT_DOUBLE, (numpy.complex128, 1): FIT_COMPLEX, (numpy.uint16, 3): FIT_RGB16, (numpy.uint16, 4): FIT_RGBA16, (numpy.float32, 3): FIT_RGBF, (numpy.float32, 4): FIT_RGBAF } extra_dims = { FIT_UINT16: [], FIT_INT16: [], FIT_UINT32: [], FIT_INT32: [], FIT_FLOAT: [], FIT_DOUBLE: [], FIT_COMPLEX: [], FIT_RGB16: [3], FIT_RGBA16: [4], FIT_RGBF: [3], FIT_RGBAF: [4] } class IO_FLAGS(object): FIF_LOAD_NOPIXELS = 0x8000 # loading: load the image header only # # (not supported by all plugins) BMP_DEFAULT = 0 BMP_SAVE_RLE = 1 CUT_DEFAULT = 0 DDS_DEFAULT = 0 EXR_DEFAULT = 0 # save data as half with piz-based wavelet compression EXR_FLOAT = 0x0001 # save data as float instead of half (not recommended) EXR_NONE = 0x0002 # save with no compression EXR_ZIP = 0x0004 # save with zlib compression, in blocks of 16 scan lines EXR_PIZ = 0x0008 # save with piz-based wavelet compression EXR_PXR24 = 0x0010 # save with lossy 24-bit float compression EXR_B44 = 0x0020 # save with lossy 44% float compression # # - goes to 22% when combined with EXR_LC EXR_LC = 0x0040 # save images with one luminance and two chroma channels, # # rather than as RGB (lossy compression) FAXG3_DEFAULT = 0 GIF_DEFAULT = 0 GIF_LOAD256 = 1 # Load the image as a 256 color image with ununsed # # palette entries, if it's 16 or 2 color GIF_PLAYBACK = 2 # 'Play' the GIF to generate each frame (as 32bpp) # # instead of returning raw frame data when loading HDR_DEFAULT = 0 ICO_DEFAULT = 0 ICO_MAKEALPHA = 1 # convert to 32bpp and create an alpha channel from the # # AND-mask when loading IFF_DEFAULT = 0 J2K_DEFAULT = 0 # save with a 16:1 rate JP2_DEFAULT = 0 # save with a 16:1 rate JPEG_DEFAULT = 0 # loading (see JPEG_FAST); # # saving (see JPEG_QUALITYGOOD|JPEG_SUBSAMPLING_420) JPEG_FAST = 0x0001 # load the file as fast as possible, # # sacrificing some quality JPEG_ACCURATE = 0x0002 # load the file with the best quality, # # sacrificing some speed JPEG_CMYK = 0x0004 # load separated CMYK "as is" # # (use | to combine with other load flags) JPEG_EXIFROTATE = 0x0008 # load and rotate according to # # Exif 'Orientation' tag if available JPEG_QUALITYSUPERB = 0x80 # save with superb quality (100:1) JPEG_QUALITYGOOD = 0x0100 # save with good quality (75:1) JPEG_QUALITYNORMAL = 0x0200 # save with normal quality (50:1) JPEG_QUALITYAVERAGE = 0x0400 # save with average quality (25:1) JPEG_QUALITYBAD = 0x0800 # save with bad quality (10:1) JPEG_PROGRESSIVE = 0x2000 # save as a progressive-JPEG # # (use | to combine with other save flags) JPEG_SUBSAMPLING_411 = 0x1000 # save with high 4x1 chroma # # subsampling (4:1:1) JPEG_SUBSAMPLING_420 = 0x4000 # save with medium 2x2 medium chroma # # subsampling (4:2:0) - default value JPEG_SUBSAMPLING_422 = 0x8000 # save /w low 2x1 chroma subsampling (4:2:2) JPEG_SUBSAMPLING_444 = 0x10000 # save with no chroma subsampling (4:4:4) JPEG_OPTIMIZE = 0x20000 # on saving, compute optimal Huffman coding tables # # (can reduce a few percent of file size) JPEG_BASELINE = 0x40000 # save basic JPEG, without metadata or any markers KOALA_DEFAULT = 0 LBM_DEFAULT = 0 MNG_DEFAULT = 0 PCD_DEFAULT = 0 PCD_BASE = 1 # load the bitmap sized 768 x 512 PCD_BASEDIV4 = 2 # load the bitmap sized 384 x 256 PCD_BASEDIV16 = 3 # load the bitmap sized 192 x 128 PCX_DEFAULT = 0 PFM_DEFAULT = 0 PICT_DEFAULT = 0 PNG_DEFAULT = 0 PNG_IGNOREGAMMA = 1 # loading: avoid gamma correction PNG_Z_BEST_SPEED = 0x0001 # save using ZLib level 1 compression flag # # (default value is 6) PNG_Z_DEFAULT_COMPRESSION = 0x0006 # save using ZLib level 6 compression # # flag (default recommended value) PNG_Z_BEST_COMPRESSION = 0x0009 # save using ZLib level 9 compression flag # # (default value is 6) PNG_Z_NO_COMPRESSION = 0x0100 # save without ZLib compression PNG_INTERLACED = 0x0200 # save using Adam7 interlacing (use | to combine # # with other save flags) PNM_DEFAULT = 0 PNM_SAVE_RAW = 0 # Writer saves in RAW format (i.e. P4, P5 or P6) PNM_SAVE_ASCII = 1 # Writer saves in ASCII format (i.e. P1, P2 or P3) PSD_DEFAULT = 0 PSD_CMYK = 1 # reads tags for separated CMYK (default is conversion to RGB) PSD_LAB = 2 # reads tags for CIELab (default is conversion to RGB) RAS_DEFAULT = 0 RAW_DEFAULT = 0 # load the file as linear RGB 48-bit RAW_PREVIEW = 1 # try to load the embedded JPEG preview with included # # Exif Data or default to RGB 24-bit RAW_DISPLAY = 2 # load the file as RGB 24-bit SGI_DEFAULT = 0 TARGA_DEFAULT = 0 TARGA_LOAD_RGB888 = 1 # Convert RGB555 and ARGB8888 -> RGB888. TARGA_SAVE_RLE = 2 # Save with RLE compression TIFF_DEFAULT = 0 TIFF_CMYK = 0x0001 # reads/stores tags for separated CMYK # # (use | to combine with compression flags) TIFF_PACKBITS = 0x0100 # save using PACKBITS compression TIFF_DEFLATE = 0x0200 # save using DEFLATE (a.k.a. ZLIB) compression TIFF_ADOBE_DEFLATE = 0x0400 # save using ADOBE DEFLATE compression TIFF_NONE = 0x0800 # save without any compression TIFF_CCITTFAX3 = 0x1000 # save using CCITT Group 3 fax encoding TIFF_CCITTFAX4 = 0x2000 # save using CCITT Group 4 fax encoding TIFF_LZW = 0x4000 # save using LZW compression TIFF_JPEG = 0x8000 # save using JPEG compression TIFF_LOGLUV = 0x10000 # save using LogLuv compression WBMP_DEFAULT = 0 XBM_DEFAULT = 0 XPM_DEFAULT = 0 class METADATA_MODELS(object): FIMD_COMMENTS = 0 FIMD_EXIF_MAIN = 1 FIMD_EXIF_EXIF = 2 FIMD_EXIF_GPS = 3 FIMD_EXIF_MAKERNOTE = 4 FIMD_EXIF_INTEROP = 5 FIMD_IPTC = 6 FIMD_XMP = 7 FIMD_GEOTIFF = 8 FIMD_ANIMATION = 9 class METADATA_DATATYPE(object): FIDT_BYTE = 1 # 8-bit unsigned integer FIDT_ASCII = 2 # 8-bit bytes w/ last byte null FIDT_SHORT = 3 # 16-bit unsigned integer FIDT_LONG = 4 # 32-bit unsigned integer FIDT_RATIONAL = 5 # 64-bit unsigned fraction FIDT_SBYTE = 6 # 8-bit signed integer FIDT_UNDEFINED = 7 # 8-bit untyped data FIDT_SSHORT = 8 # 16-bit signed integer FIDT_SLONG = 9 # 32-bit signed integer FIDT_SRATIONAL = 10 # 64-bit signed fraction FIDT_FLOAT = 11 # 32-bit IEEE floating point FIDT_DOUBLE = 12 # 64-bit IEEE floating point FIDT_IFD = 13 # 32-bit unsigned integer (offset) FIDT_PALETTE = 14 # 32-bit RGBQUAD FIDT_LONG8 = 16 # 64-bit unsigned integer FIDT_SLONG8 = 17 # 64-bit signed integer FIDT_IFD8 = 18 # 64-bit unsigned integer (offset) dtypes = { FIDT_BYTE: numpy.uint8, FIDT_SHORT: numpy.uint16, FIDT_LONG: numpy.uint32, FIDT_RATIONAL: [('numerator', numpy.uint32), ('denominator', numpy.uint32)], FIDT_LONG8: numpy.uint64, FIDT_SLONG8: numpy.int64, FIDT_IFD8: numpy.uint64, FIDT_SBYTE: numpy.int8, FIDT_UNDEFINED: numpy.uint8, FIDT_SSHORT: numpy.int16, FIDT_SLONG: numpy.int32, FIDT_SRATIONAL: [('numerator', numpy.int32), ('denominator', numpy.int32)], FIDT_FLOAT: numpy.float32, FIDT_DOUBLE: numpy.float64, FIDT_IFD: numpy.uint32, FIDT_PALETTE: [('R', numpy.uint8), ('G', numpy.uint8), ('B', numpy.uint8), ('A', numpy.uint8)], } class Freeimage(object): """ Class to represent an interface to the FreeImage library. This class is relatively thin. It provides a Pythonic API that converts Freeimage objects to Python objects, but that's about it. The actual implementation should be provided by the plugins. The recommended way to call into the Freeimage library (so that errors and warnings show up in the right moment) is to use this object as a context manager: with imageio.fi as lib: lib.FreeImage_GetPalette() """ _API = { # All we're doing here is telling ctypes that some of the # FreeImage functions return pointers instead of integers. (On # 64-bit systems, without this information the pointers get # truncated and crashes result). There's no need to list # functions that return ints, or the types of the parameters # to these or other functions -- that's fine to do implicitly. # Note that the ctypes immediately converts the returned void_p # back to a python int again! This is really not helpful, # because then passing it back to another library call will # cause truncation-to-32-bits on 64-bit systems. Thanks, ctypes! # So after these calls one must immediately re-wrap the int as # a c_void_p if it is to be passed back into FreeImage. 'FreeImage_AllocateT': (ctypes.c_void_p, None), 'FreeImage_FindFirstMetadata': (ctypes.c_void_p, None), 'FreeImage_GetBits': (ctypes.c_void_p, None), 'FreeImage_GetPalette': (ctypes.c_void_p, None), 'FreeImage_GetTagKey': (ctypes.c_char_p, None), 'FreeImage_GetTagValue': (ctypes.c_void_p, None), 'FreeImage_CreateTag': (ctypes.c_void_p, None), 'FreeImage_Save': (ctypes.c_void_p, None), 'FreeImage_Load': (ctypes.c_void_p, None), 'FreeImage_LoadFromMemory': (ctypes.c_void_p, None), 'FreeImage_OpenMultiBitmap': (ctypes.c_void_p, None), 'FreeImage_LoadMultiBitmapFromMemory': (ctypes.c_void_p, None), 'FreeImage_LockPage': (ctypes.c_void_p, None), 'FreeImage_OpenMemory': (ctypes.c_void_p, None), #'FreeImage_ReadMemory': (ctypes.c_void_p, None), #'FreeImage_CloseMemory': (ctypes.c_void_p, None), 'FreeImage_GetVersion': (ctypes.c_char_p, None), 'FreeImage_GetFIFExtensionList': (ctypes.c_char_p, None), 'FreeImage_GetFormatFromFIF': (ctypes.c_char_p, None), 'FreeImage_GetFIFDescription': (ctypes.c_char_p, None), 'FreeImage_ColorQuantizeEx': (ctypes.c_void_p, None), # Pypy wants some extra definitions, so here we go ... 'FreeImage_IsLittleEndian': (ctypes.c_int, None), 'FreeImage_SetOutputMessage': (ctypes.c_void_p, None), 'FreeImage_GetFIFCount': (ctypes.c_int, None), 'FreeImage_IsPluginEnabled': (ctypes.c_int, None), 'FreeImage_GetFileType': (ctypes.c_int, None), # 'FreeImage_GetTagType': (ctypes.c_int, None), 'FreeImage_GetTagLength': (ctypes.c_int, None), 'FreeImage_FindNextMetadata': (ctypes.c_int, None), 'FreeImage_FindCloseMetadata': (ctypes.c_void_p, None), # 'FreeImage_GetFIFFromFilename': (ctypes.c_int, None), 'FreeImage_FIFSupportsReading': (ctypes.c_int, None), 'FreeImage_FIFSupportsWriting': (ctypes.c_int, None), 'FreeImage_FIFSupportsExportType': (ctypes.c_int, None), 'FreeImage_FIFSupportsExportBPP': (ctypes.c_int, None), 'FreeImage_GetHeight': (ctypes.c_int, None), 'FreeImage_GetWidth': (ctypes.c_int, None), 'FreeImage_GetImageType': (ctypes.c_int, None), 'FreeImage_GetBPP': (ctypes.c_int, None), 'FreeImage_GetColorsUsed': (ctypes.c_int, None), 'FreeImage_ConvertTo32Bits': (ctypes.c_void_p, None), 'FreeImage_GetPitch': (ctypes.c_int, None), 'FreeImage_Unload': (ctypes.c_void_p, None), } def __init__(self): # Initialize freeimage lib as None self._lib = None # A lock to create thread-safety self._lock = threading.RLock() # Init log messages lists self._messages = [] # Select functype for error handler if sys.platform.startswith('win'): functype = ctypes.WINFUNCTYPE else: functype = ctypes.CFUNCTYPE # Create output message handler @functype(None, ctypes.c_int, ctypes.c_char_p) def error_handler(fif, message): message = message.decode('utf-8') self._messages.append(message) while (len(self._messages)) > 256: self._messages.pop(0) # Make sure to keep a ref to function self._error_handler = error_handler @property def lib(self): if self._lib is None: try: self.load_freeimage() except OSError as err: self._lib = 'The freeimage library could not be loaded: ' self._lib += str(err) if isinstance(self._lib, str): raise RuntimeError(self._lib) return self._lib def has_lib(self): try: self.lib except Exception: return False return True def load_freeimage(self): """ Try to load the freeimage lib from the system. If not successful, try to download the imageio version and try again. """ # Load library and register API success = False try: # Try without forcing a download, but giving preference # to the imageio-provided lib (if previously downloaded) self._load_freeimage() self._register_api() if self.lib.FreeImage_GetVersion().decode('utf-8') >= '3.15': success = True except OSError: pass if not success: # Ensure we have our own lib, try again get_freeimage_lib() self._load_freeimage() self._register_api() # Wrap up self.lib.FreeImage_SetOutputMessage(self._error_handler) self.lib_version = self.lib.FreeImage_GetVersion().decode('utf-8') def _load_freeimage(self): # Define names lib_names = ['freeimage', 'libfreeimage'] exact_lib_names = ['FreeImage', 'libfreeimage.dylib', 'libfreeimage.so', 'libfreeimage.so.3'] # Add names of libraries that we provide (that file may not exist) res_dirs = resource_dirs() plat = get_platform() if plat: # Can be None on e.g. FreeBSD fname = FNAME_PER_PLATFORM[plat] for dir in res_dirs: exact_lib_names.insert(0, os.path.join(dir, 'freeimage', fname)) # Add the path specified with IMAGEIO_FREEIMAGE_LIB: lib = os.getenv('IMAGEIO_FREEIMAGE_LIB', None) if lib is not None: exact_lib_names.insert(0, lib) # Load try: lib, fname = load_lib(exact_lib_names, lib_names, res_dirs) except OSError as err: # pragma: no cover err_msg = str(err) + '\nPlease install the FreeImage library.' raise OSError(err_msg) # Store self._lib = lib self.lib_fname = fname def _register_api(self): # Albert's ctypes pattern for f, (restype, argtypes) in self._API.items(): func = getattr(self.lib, f) func.restype = restype func.argtypes = argtypes ## Handling of output messages def __enter__(self): self._lock.acquire() return self.lib def __exit__(self, *args): self._show_any_warnings() self._lock.release() def _reset_log(self): """ Reset the list of output messages. Call this before loading or saving an image with the FreeImage API. """ self._messages = [] def _get_error_message(self): """ Get the output messages produced since the last reset as one string. Returns 'No known reason.' if there are no messages. Also resets the log. """ if self._messages: res = ' '.join(self._messages) self._reset_log() return res else: return 'No known reason.' def _show_any_warnings(self): """ If there were any messages since the last reset, show them as a warning. Otherwise do nothing. Also resets the messages. """ if self._messages: warn('imageio.freeimage warning: ' + self._get_error_message()) self._reset_log() def get_output_log(self): """ Return a list of the last 256 output messages (warnings and errors) produced by the FreeImage library. """ # This message log is not cleared/reset, but kept to 256 elements. return [m for m in self._messages] def getFIF(self, filename, mode, bytes=None): """ Get the freeimage Format (FIF) from a given filename. If mode is 'r', will try to determine the format by reading the file, otherwise only the filename is used. This function also tests whether the format supports reading/writing. """ with self as lib: # Init ftype = -1 if mode not in 'rw': raise ValueError('Invalid mode (must be "r" or "w").') # Try getting format from the content. Note that some files # do not have a header that allows reading the format from # the file. if mode == 'r': if bytes is not None: fimemory = lib.FreeImage_OpenMemory( ctypes.c_char_p(bytes), len(bytes)) ftype = lib.FreeImage_GetFileTypeFromMemory( ctypes.c_void_p(fimemory), len(bytes)) lib.FreeImage_CloseMemory(ctypes.c_void_p(fimemory)) if (ftype == -1) and os.path.isfile(filename): ftype = lib.FreeImage_GetFileType(efn(filename), 0) # Try getting the format from the extension if ftype == -1: ftype = lib.FreeImage_GetFIFFromFilename(efn(filename)) # Test if ok if ftype == -1: raise ValueError('Cannot determine format of file "%s"' % filename) elif mode == 'w' and not lib.FreeImage_FIFSupportsWriting(ftype): raise ValueError('Cannot write the format of file "%s"' % filename) elif mode == 'r' and not lib.FreeImage_FIFSupportsReading(ftype): raise ValueError('Cannot read the format of file "%s"' % filename) return ftype def create_bitmap(self, filename, ftype, flags=0): """ create_bitmap(filename, ftype, flags=0) Create a wrapped bitmap object. """ return FIBitmap(self, filename, ftype, flags) def create_multipage_bitmap(self, filename, ftype, flags=0): """ create_multipage_bitmap(filename, ftype, flags=0) Create a wrapped multipage bitmap object. """ return FIMultipageBitmap(self, filename, ftype, flags) class FIBaseBitmap(object): def __init__(self, fi, filename, ftype, flags): self._fi = fi self._filename = filename self._ftype = ftype self._flags = flags self._bitmap = None self._close_funcs = [] def __del__(self): self.close() def close(self): if (self._bitmap is not None) and self._close_funcs: for close_func in self._close_funcs: try: with self._fi: fun = close_func[0] fun(*close_func[1:]) except Exception: # pragma: no cover pass self._close_funcs = [] self._bitmap = None def _set_bitmap(self, bitmap, close_func=None): """ Function to set the bitmap and specify the function to unload it. """ if self._bitmap is not None: pass # bitmap is converted if close_func is None: close_func = self._fi.lib.FreeImage_Unload, bitmap self._bitmap = bitmap if close_func: self._close_funcs.append(close_func) def get_meta_data(self): # todo: there is also FreeImage_TagToString, is that useful? # and would that work well when reading and then saving? # Create a list of (model_name, number) tuples models = [(name[5:], number) for name, number in METADATA_MODELS.__dict__.items() if name.startswith('FIMD_')] # Prepare metadata = Dict() tag = ctypes.c_void_p() with self._fi as lib: # Iterate over all FreeImage meta models for model_name, number in models: # Find beginning, get search handle mdhandle = lib.FreeImage_FindFirstMetadata(number, self._bitmap, ctypes.byref(tag)) mdhandle = ctypes.c_void_p(mdhandle) if mdhandle: # Iterate over all tags in this model more = True while more: # Get info about tag tag_name = lib.FreeImage_GetTagKey(tag).decode('utf-8') tag_type = lib.FreeImage_GetTagType(tag) byte_size = lib.FreeImage_GetTagLength(tag) char_ptr = ctypes.c_char * byte_size data = char_ptr.from_address( lib.FreeImage_GetTagValue(tag)) # Convert in a way compatible with Pypy tag_bytes = binary_type(bytearray(data)) # The default value is the raw bytes tag_val = tag_bytes # Convert to a Python value in the metadata dict if tag_type == METADATA_DATATYPE.FIDT_ASCII: tag_val = tag_bytes.decode('utf-8', 'replace') elif tag_type in METADATA_DATATYPE.dtypes: dtype = METADATA_DATATYPE.dtypes[tag_type] if IS_PYPY and isinstance(dtype, (list, tuple)): pass # pragma: no cover - or we get a segfault else: try: tag_val = numpy.fromstring(tag_bytes, dtype=dtype) if len(tag_val) == 1: tag_val = tag_val[0] except Exception: # pragma: no cover pass # Store data in dict subdict = metadata.setdefault(model_name, Dict()) subdict[tag_name] = tag_val # Next more = lib.FreeImage_FindNextMetadata( mdhandle, ctypes.byref(tag)) # Close search handle for current meta model lib.FreeImage_FindCloseMetadata(mdhandle) # Done return metadata def set_meta_data(self, metadata): # Create a dict mapping model_name to number models = {} for name, number in METADATA_MODELS.__dict__.items(): if name.startswith('FIMD_'): models[name[5:]] = number # Create a mapping from numpy.dtype to METADATA_DATATYPE def get_tag_type_number(dtype): for number, numpy_dtype in METADATA_DATATYPE.dtypes.items(): if dtype == numpy_dtype: return number else: return None with self._fi as lib: for model_name, subdict in metadata.items(): # Get model number number = models.get(model_name, None) if number is None: continue # Unknown model, silent ignore for tag_name, tag_val in subdict.items(): # Create new tag tag = lib.FreeImage_CreateTag() tag = ctypes.c_void_p(tag) try: # Convert Python value to FI type, val is_ascii = False if isinstance(tag_val, string_types): try: tag_bytes = tag_val.encode('ascii') is_ascii = True except UnicodeError: pass if is_ascii: tag_type = METADATA_DATATYPE.FIDT_ASCII tag_count = len(tag_bytes) else: if not hasattr(tag_val, 'dtype'): tag_val = numpy.array([tag_val]) tag_type = get_tag_type_number(tag_val.dtype) if tag_type is None: warn('imageio.freeimage warning: Could not ' 'determine tag type of %r.' % tag_name) continue tag_bytes = tag_val.tostring() tag_count = tag_val.size # Set properties lib.FreeImage_SetTagKey(tag, tag_name.encode('utf-8')) lib.FreeImage_SetTagType(tag, tag_type) lib.FreeImage_SetTagCount(tag, tag_count) lib.FreeImage_SetTagLength(tag, len(tag_bytes)) lib.FreeImage_SetTagValue(tag, tag_bytes) # Store tag tag_key = lib.FreeImage_GetTagKey(tag) lib.FreeImage_SetMetadata(number, self._bitmap, tag_key, tag) except Exception as err: # pragma: no cover warn('imagio.freeimage warning: Could not set tag ' '%r: %s, %s' % (tag_name, self._fi._get_error_message(), str(err))) finally: lib.FreeImage_DeleteTag(tag) class FIBitmap(FIBaseBitmap): """ Wrapper for the FI bitmap object. """ def allocate(self, array): # Prepare array assert isinstance(array, numpy.ndarray) shape = array.shape dtype = array.dtype # Get shape and channel info r, c = shape[:2] if len(shape) == 2: n_channels = 1 elif len(shape) == 3: n_channels = shape[2] else: n_channels = shape[0] # Get fi_type try: fi_type = FI_TYPES.fi_types[(dtype.type, n_channels)] self._fi_type = fi_type except KeyError: raise ValueError('Cannot write arrays of given type and shape.') # Allocate bitmap with self._fi as lib: bpp = 8 * dtype.itemsize * n_channels bitmap = lib.FreeImage_AllocateT(fi_type, c, r, bpp, 0, 0, 0) bitmap = ctypes.c_void_p(bitmap) # Check and store if not bitmap: # pragma: no cover raise RuntimeError('Could not allocate bitmap for storage: %s' % self._fi._get_error_message()) self._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap)) def load_from_filename(self, filename=None): if filename is None: filename = self._filename with self._fi as lib: # Create bitmap bitmap = lib.FreeImage_Load(self._ftype, efn(filename), self._flags) bitmap = ctypes.c_void_p(bitmap) # Check and store if not bitmap: # pragma: no cover raise ValueError('Could not load bitmap "%s": %s' % (self._filename, self._fi._get_error_message())) self._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap)) # def load_from_bytes(self, bytes): # with self._fi as lib: # # Create bitmap # fimemory = lib.FreeImage_OpenMemory( # ctypes.c_char_p(bytes), len(bytes)) # bitmap = lib.FreeImage_LoadFromMemory( # self._ftype, ctypes.c_void_p(fimemory), self._flags) # bitmap = ctypes.c_void_p(bitmap) # lib.FreeImage_CloseMemory(ctypes.c_void_p(fimemory)) # # # Check # if not bitmap: # raise ValueError('Could not load bitmap "%s": %s' # % (self._filename, self._fi._get_error_message())) # else: # self._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap)) def save_to_filename(self, filename=None): if filename is None: filename = self._filename ftype = self._ftype bitmap = self._bitmap fi_type = self._fi_type # element type with self._fi as lib: # Check if can write if fi_type == FI_TYPES.FIT_BITMAP: can_write = lib.FreeImage_FIFSupportsExportBPP( ftype, lib.FreeImage_GetBPP(bitmap)) else: can_write = lib.FreeImage_FIFSupportsExportType(ftype, fi_type) if not can_write: raise TypeError('Cannot save image of this format ' 'to this file type') # Save to file res = lib.FreeImage_Save(ftype, bitmap, efn(filename), self._flags) # Check if not res: # pragma: no cover, we do so many checks, this is rare raise RuntimeError('Could not save file "%s": %s' % (self._filename, self._fi._get_error_message())) # def save_to_bytes(self): # ftype = self._ftype # bitmap = self._bitmap # fi_type = self._fi_type # element type # # with self._fi as lib: # # Check if can write # if fi_type == FI_TYPES.FIT_BITMAP: # can_write = lib.FreeImage_FIFSupportsExportBPP(ftype, # lib.FreeImage_GetBPP(bitmap)) # else: # can_write = lib.FreeImage_FIFSupportsExportType(ftype, fi_type) # if not can_write: # raise TypeError('Cannot save image of this format ' # 'to this file type') # # # Extract the bytes # fimemory = lib.FreeImage_OpenMemory(0, 0) # res = lib.FreeImage_SaveToMemory(ftype, bitmap, # ctypes.c_void_p(fimemory), # self._flags) # if res: # N = lib.FreeImage_TellMemory(ctypes.c_void_p(fimemory)) # result = ctypes.create_string_buffer(N) # lib.FreeImage_SeekMemory(ctypes.c_void_p(fimemory), 0) # lib.FreeImage_ReadMemory(result, 1, N, ctypes.c_void_p(fimemory)) # result = result.raw # lib.FreeImage_CloseMemory(ctypes.c_void_p(fimemory)) # # # Check # if not res: # raise RuntimeError('Could not save file "%s": %s' # % (self._filename, self._fi._get_error_message())) # # # Done # return result def get_image_data(self): dtype, shape, bpp = self._get_type_and_shape() array = self._wrap_bitmap_bits_in_array(shape, dtype, False) with self._fi as lib: isle = lib.FreeImage_IsLittleEndian() # swizzle the color components and flip the scanlines to go from # FreeImage's BGR[A] and upside-down internal memory format to # something more normal def n(arr): #return arr[..., ::-1].T # Does not work on numpypy yet if arr.ndim == 1: # pragma: no cover return arr[::-1].T elif arr.ndim == 2: # Always the case here ... return arr[:, ::-1].T elif arr.ndim == 3: # pragma: no cover return arr[:, :, ::-1].T elif arr.ndim == 4: # pragma: no cover return arr[:, :, :, ::-1].T if len(shape) == 3 and isle and dtype.type == numpy.uint8: b = n(array[0]) g = n(array[1]) r = n(array[2]) if shape[0] == 3: return numpy.dstack((r, g, b)) elif shape[0] == 4: a = n(array[3]) return numpy.dstack((r, g, b, a)) else: # pragma: no cover - we check this earlier raise ValueError('Cannot handle images of shape %s' % shape) # We need to copy because array does *not* own its memory # after bitmap is freed. a = n(array).copy() return a def set_image_data(self, array): # Prepare array assert isinstance(array, numpy.ndarray) shape = array.shape dtype = array.dtype with self._fi as lib: isle = lib.FreeImage_IsLittleEndian() # Calculate shape and channels r, c = shape[:2] if len(shape) == 2: n_channels = 1 w_shape = (c, r) elif len(shape) == 3: n_channels = shape[2] w_shape = (n_channels, c, r) else: n_channels = shape[0] def n(arr): # normalise to freeimage's in-memory format return arr.T[:, ::-1] wrapped_array = self._wrap_bitmap_bits_in_array(w_shape, dtype, True) # swizzle the color components and flip the scanlines to go to # FreeImage's BGR[A] and upside-down internal memory format if len(shape) == 3: R = array[:, :, 0] G = array[:, :, 1] B = array[:, :, 2] if isle: if dtype.type == numpy.uint8: wrapped_array[0] = n(B) wrapped_array[1] = n(G) wrapped_array[2] = n(R) elif dtype.type == numpy.uint16: wrapped_array[0] = n(R) wrapped_array[1] = n(G) wrapped_array[2] = n(B) # if shape[2] == 4: A = array[:, :, 3] wrapped_array[3] = n(A) else: wrapped_array[:] = n(array) if self._need_finish: self._finish_wrapped_array(wrapped_array) if len(shape) == 2 and dtype.type == numpy.uint8: with self._fi as lib: palette = lib.FreeImage_GetPalette(self._bitmap) palette = ctypes.c_void_p(palette) if not palette: raise RuntimeError('Could not get image palette') try: palette_data = GREY_PALETTE.ctypes.data except Exception: # pragma: no cover - IS_PYPY palette_data = GREY_PALETTE.__array_interface__['data'][0] ctypes.memmove(palette, palette_data, 1024) def _wrap_bitmap_bits_in_array(self, shape, dtype, save): """Return an ndarray view on the data in a FreeImage bitmap. Only valid for as long as the bitmap is loaded (if single page) / locked in memory (if multipage). This is used in loading data, but also during saving, to prepare a strided numpy array buffer. """ # Get bitmap info with self._fi as lib: pitch = lib.FreeImage_GetPitch(self._bitmap) bits = lib.FreeImage_GetBits(self._bitmap) # Get more info height = shape[-1] byte_size = height * pitch itemsize = dtype.itemsize # Get strides if len(shape) == 3: strides = (itemsize, shape[0]*itemsize, pitch) else: strides = (itemsize, pitch) # Create numpy array and return data = (ctypes.c_char*byte_size).from_address(bits) try: self._need_finish = False if TEST_NUMPY_NO_STRIDES: raise NotImplementedError() return numpy.ndarray(shape, dtype=dtype, buffer=data, strides=strides) except NotImplementedError: # IS_PYPY - not very efficient. We create a C-contiguous # numpy array (because pypy does not support Fortran-order) # and shape it such that the rest of the code can remain. if save: self._need_finish = True # Flag to use _finish_wrapped_array return numpy.zeros(shape, dtype=dtype) else: bytes = binary_type(bytearray(data)) array = numpy.fromstring(bytes, dtype=dtype) # Deal with strides if len(shape) == 3: array.shape = shape[2], strides[-1]/shape[0], shape[0] array2 = array[:shape[2], :shape[1], :shape[0]] array = numpy.zeros(shape, dtype=array.dtype) for i in range(shape[0]): array[i] = array2[:, :, i].T else: array.shape = shape[1], strides[-1] array = array[:shape[1], :shape[0]].T return array def _finish_wrapped_array(self, array): # IS_PYPY """ Hardcore way to inject numpy array in bitmap. """ # Get bitmap info with self._fi as lib: pitch = lib.FreeImage_GetPitch(self._bitmap) bits = lib.FreeImage_GetBits(self._bitmap) bpp = lib.FreeImage_GetBPP(self._bitmap) # Get channels and realwidth nchannels = bpp // 8 // array.itemsize realwidth = pitch // nchannels # Apply padding for pitch if necessary extra = realwidth - array.shape[-2] assert extra >= 0 and extra < 10 # Make sort of Fortran, also take padding (i.e. pitch) into account newshape = array.shape[-1], realwidth, nchannels array2 = numpy.zeros(newshape, array.dtype) if nchannels == 1: array2[:, :array.shape[-2], 0] = array.T else: for i in range(nchannels): array2[:, :array.shape[-2], i] = array[i, :, :].T # copy data data_ptr = array2.__array_interface__['data'][0] ctypes.memmove(bits, data_ptr, array2.nbytes) del array2 def _get_type_and_shape(self): bitmap = self._bitmap # Get info on bitmap with self._fi as lib: w = lib.FreeImage_GetWidth(bitmap) h = lib.FreeImage_GetHeight(bitmap) self._fi_type = fi_type = lib.FreeImage_GetImageType(bitmap) if not fi_type: raise ValueError('Unknown image pixel type') # Determine required props for numpy array bpp = None dtype = FI_TYPES.dtypes[fi_type] if fi_type == FI_TYPES.FIT_BITMAP: with self._fi as lib: bpp = lib.FreeImage_GetBPP(bitmap) has_pallette = lib.FreeImage_GetColorsUsed(bitmap) if has_pallette: # Examine the palette. If it is grayscale, we return as such if has_pallette == 256: palette = lib.FreeImage_GetPalette(bitmap) palette = ctypes.c_void_p(palette) p = (ctypes.c_uint8*(256*4)).from_address(palette.value) p = numpy.frombuffer(p, numpy.uint32) if (GREY_PALETTE == p).all(): extra_dims = [] return numpy.dtype(dtype), extra_dims + [w, h], bpp # Convert bitmap and call this method again newbitmap = lib.FreeImage_ConvertTo32Bits(bitmap) newbitmap = ctypes.c_void_p(newbitmap) self._set_bitmap(newbitmap) return self._get_type_and_shape() elif bpp == 8: extra_dims = [] elif bpp == 24: extra_dims = [3] elif bpp == 32: extra_dims = [4] else: # pragma: no cover #raise ValueError('Cannot convert %d BPP bitmap' % bpp) # Convert bitmap and call this method again newbitmap = lib.FreeImage_ConvertTo32Bits(bitmap) newbitmap = ctypes.c_void_p(newbitmap) self._set_bitmap(newbitmap) return self._get_type_and_shape() else: extra_dims = FI_TYPES.extra_dims[fi_type] # Return dtype and shape return numpy.dtype(dtype), extra_dims + [w, h], bpp def quantize(self, quantizer=0, palettesize=256): """ Quantize the bitmap to make it 8-bit (paletted). Returns a new FIBitmap object. Only for 24 bit images. """ with self._fi as lib: # New bitmap bitmap = lib.FreeImage_ColorQuantizeEx(self._bitmap, quantizer, palettesize, 0, None) bitmap = ctypes.c_void_p(bitmap) # Check and return if not bitmap: raise ValueError('Could not quantize bitmap "%s": %s' % (self._filename, self._fi._get_error_message())) new = FIBitmap(self._fi, self._filename, self._ftype, self._flags) new._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap)) new._fi_type = self._fi_type return new # def convert_to_32bit(self): # """ Convert to 32bit image. # """ # with self._fi as lib: # # New bitmap # bitmap = lib.FreeImage_ConvertTo32Bits(self._bitmap) # bitmap = ctypes.c_void_p(bitmap) # # # Check and return # if not bitmap: # raise ValueError('Could not convert bitmap to 32bit "%s": %s' % # (self._filename, # self._fi._get_error_message())) # else: # new = FIBitmap(self._fi, self._filename, self._ftype, # self._flags) # new._set_bitmap(bitmap, (lib.FreeImage_Unload, bitmap)) # new._fi_type = self._fi_type # return new class FIMultipageBitmap(FIBaseBitmap): """ Wrapper for the multipage FI bitmap object. """ def load_from_filename(self, filename=None): if filename is None: # pragma: no cover filename = self._filename # Prepare create_new = False read_only = True keep_cache_in_memory = False # Try opening with self._fi as lib: # Create bitmap multibitmap = lib.FreeImage_OpenMultiBitmap(self._ftype, efn(filename), create_new, read_only, keep_cache_in_memory, self._flags) multibitmap = ctypes.c_void_p(multibitmap) # Check if not multibitmap: # pragma: no cover err = self._fi._get_error_message() raise ValueError('Could not open file "%s" as multi-image: %s' % (self._filename, err)) self._set_bitmap(multibitmap, (lib.FreeImage_CloseMultiBitmap, multibitmap)) # def load_from_bytes(self, bytes): # with self._fi as lib: # # Create bitmap # fimemory = lib.FreeImage_OpenMemory( # ctypes.c_char_p(bytes), len(bytes)) # multibitmap = lib.FreeImage_LoadMultiBitmapFromMemory( # self._ftype, ctypes.c_void_p(fimemory), self._flags) # multibitmap = ctypes.c_void_p(multibitmap) # #lib.FreeImage_CloseMemory(ctypes.c_void_p(fimemory)) # self._mem = fimemory # self._bytes = bytes # # Check # if not multibitmap: # raise ValueError('Could not load multibitmap "%s": %s' # % (self._filename, self._fi._get_error_message())) # else: # self._set_bitmap(multibitmap, # (lib.FreeImage_CloseMultiBitmap, multibitmap)) def save_to_filename(self, filename=None): if filename is None: # pragma: no cover filename = self._filename # Prepare create_new = True read_only = False keep_cache_in_memory = False # Open the file # todo: Set flags at close func with self._fi as lib: multibitmap = lib.FreeImage_OpenMultiBitmap(self._ftype, efn(filename), create_new, read_only, keep_cache_in_memory, 0) multibitmap = ctypes.c_void_p(multibitmap) # Check if not multibitmap: # pragma: no cover msg = ('Could not open file "%s" for writing multi-image: %s' % (self._filename, self._fi._get_error_message())) raise ValueError(msg) self._set_bitmap(multibitmap, (lib.FreeImage_CloseMultiBitmap, multibitmap)) def __len__(self): with self._fi as lib: return lib.FreeImage_GetPageCount(self._bitmap) def get_page(self, index): """ Return the sub-bitmap for the given page index. Please close the returned bitmap when done. """ with self._fi as lib: # Create low-level bitmap in freeimage bitmap = lib.FreeImage_LockPage(self._bitmap, index) bitmap = ctypes.c_void_p(bitmap) if not bitmap: # pragma: no cover raise ValueError('Could not open sub-image %i in %r: %s' % (index, self._filename, self._fi._get_error_message())) # Get bitmap object to wrap this bitmap bm = FIBitmap(self._fi, self._filename, self._ftype, self._flags) bm._set_bitmap(bitmap, (lib.FreeImage_UnlockPage, self._bitmap, bitmap, False)) return bm def append_bitmap(self, bitmap): """ Add a sub-bitmap to the multi-page bitmap. """ with self._fi as lib: # no return value lib.FreeImage_AppendPage(self._bitmap, bitmap._bitmap) # Create instance fi = Freeimage() ================================================ FILE: core/lib/imageio/plugins/freeimage.py ================================================ # -*- coding: utf-8 -*- # Copyright (c) 2015, imageio contributors # imageio is distributed under the terms of the (new) BSD License. """ Plugin that wraps the freeimage lib. The wrapper for Freeimage is part of the core of imageio, but it's functionality is exposed via the plugin system (therefore this plugin is very thin). """ from __future__ import absolute_import, print_function, division import numpy as np from .. import formats from ..core import Format, image_as_uint from ._freeimage import fi, IO_FLAGS, FNAME_PER_PLATFORM # noqa # todo: support files with only meta data class FreeimageFormat(Format): """ This is the default format used for FreeImage. Each Freeimage format has the 'flags' keyword argument. See the Freeimage documentation for more information. Parameters for reading ---------------------- flags : int A freeimage-specific option. In most cases we provide explicit parameters for influencing image reading. Parameters for saving ---------------------- flags : int A freeimage-specific option. In most cases we provide explicit parameters for influencing image saving. """ _modes = 'i' @property def fif(self): return self._fif # Set when format is created def _can_read(self, request): # Ask freeimage if it can read it, maybe ext missing if fi.has_lib(): if not hasattr(request, '_fif'): try: request._fif = fi.getFIF(request.filename, 'r', request.firstbytes) except Exception: # pragma: no cover request._fif = -1 if request._fif == self.fif: return True def _can_write(self, request): # Ask freeimage, because we are not aware of all formats if fi.has_lib(): if not hasattr(request, '_fif'): try: request._fif = fi.getFIF(request.filename, 'w') except Exception: # pragma: no cover request._fif = -1 if request._fif is self.fif: return True # -- class Reader(Format.Reader): def _get_length(self): return 1 def _open(self, flags=0): self._bm = fi.create_bitmap(self.request.filename, self.format.fif, flags) self._bm.load_from_filename(self.request.get_local_filename()) def _close(self): self._bm.close() def _get_data(self, index): if index != 0: raise IndexError('This format only supports singleton images.') return self._bm.get_image_data(), self._bm.get_meta_data() def _get_meta_data(self, index): if not (index is None or index == 0): raise IndexError() return self._bm.get_meta_data() # -- class Writer(Format.Writer): def _open(self, flags=0): self._flags = flags # Store flags for later use self._bm = None self._is_set = False # To prevent appending more than one image self._meta = {} def _close(self): # Set global meta data self._bm.set_meta_data(self._meta) # Write and close self._bm.save_to_filename(self.request.get_local_filename()) self._bm.close() def _append_data(self, im, meta): # Check if set if not self._is_set: self._is_set = True else: raise RuntimeError('Singleton image; ' 'can only append image data once.') # Pop unit dimension for grayscale images if im.ndim == 3 and im.shape[-1] == 1: im = im[:, :, 0] # Lazy instantaion of the bitmap, we need image data if self._bm is None: self._bm = fi.create_bitmap(self.request.filename, self.format.fif, self._flags) self._bm.allocate(im) # Set data self._bm.set_image_data(im) # There is no distinction between global and per-image meta data # for singleton images self._meta = meta def _set_meta_data(self, meta): self._meta = meta ## Special plugins # todo: there is also FIF_LOAD_NOPIXELS, # but perhaps that should be used with get_meta_data. class FreeimageBmpFormat(FreeimageFormat): """ A BMP format based on the Freeimage library. This format supports grayscale, RGB and RGBA images. Parameters for saving --------------------- compression : bool Whether to compress the bitmap using RLE when saving. Default False. It seems this does not always work, but who cares, you should use PNG anyway. """ class Writer(FreeimageFormat.Writer): def _open(self, flags=0, compression=False): # Build flags from kwargs flags = int(flags) if compression: flags |= IO_FLAGS.BMP_SAVE_RLE else: flags |= IO_FLAGS.BMP_DEFAULT # Act as usual, but with modified flags return FreeimageFormat.Writer._open(self, flags) def _append_data(self, im, meta): im = image_as_uint(im, bitdepth=8) return FreeimageFormat.Writer._append_data(self, im, meta) class FreeimagePngFormat(FreeimageFormat): """ A PNG format based on the Freeimage library. This format supports grayscale, RGB and RGBA images. Parameters for reading ---------------------- ignoregamma : bool Avoid gamma correction. Default False. Parameters for saving --------------------- compression : {0, 1, 6, 9} The compression factor. Higher factors result in more compression at the cost of speed. Note that PNG compression is always lossless. Default 9. quantize : int If specified, turn the given RGB or RGBA image in a paletted image for more efficient storage. The value should be between 2 and 256. If the value of 0 the image is not quantized. interlaced : bool Save using Adam7 interlacing. Default False. """ class Reader(FreeimageFormat.Reader): def _open(self, flags=0, ignoregamma=False): # Build flags from kwargs flags = int(flags) if ignoregamma: flags |= IO_FLAGS.PNG_IGNOREGAMMA # Enter as usual, with modified flags return FreeimageFormat.Reader._open(self, flags) # -- class Writer(FreeimageFormat.Writer): def _open(self, flags=0, compression=9, quantize=0, interlaced=False): compression_map = {0: IO_FLAGS.PNG_Z_NO_COMPRESSION, 1: IO_FLAGS.PNG_Z_BEST_SPEED, 6: IO_FLAGS.PNG_Z_DEFAULT_COMPRESSION, 9: IO_FLAGS.PNG_Z_BEST_COMPRESSION, } # Build flags from kwargs flags = int(flags) if interlaced: flags |= IO_FLAGS.PNG_INTERLACED try: flags |= compression_map[compression] except KeyError: raise ValueError('Png compression must be 0, 1, 6, or 9.') # Act as usual, but with modified flags return FreeimageFormat.Writer._open(self, flags) def _append_data(self, im, meta): if str(im.dtype) == 'uint16': im = image_as_uint(im, bitdepth=16) else: im = image_as_uint(im, bitdepth=8) FreeimageFormat.Writer._append_data(self, im, meta) # Quantize? q = int(self.request.kwargs.get('quantize', False)) if not q: pass elif not (im.ndim == 3 and im.shape[-1] == 3): raise ValueError('Can only quantize RGB images') elif q < 2 or q > 256: raise ValueError('PNG quantize param must be 2..256') else: bm = self._bm.quantize(0, q) self._bm.close() self._bm = bm class FreeimageJpegFormat(FreeimageFormat): """ A JPEG format based on the Freeimage library. This format supports grayscale and RGB images. Parameters for reading ---------------------- exifrotate : bool Automatically rotate the image according to the exif flag. Default True. If 2 is given, do the rotation in Python instead of freeimage. quickread : bool Read the image more quickly, at the expense of quality. Default False. Parameters for saving --------------------- quality : scalar The compression factor of the saved image (1..100), higher numbers result in higher quality but larger file size. Default 75. progressive : bool Save as a progressive JPEG file (e.g. for images on the web). Default False. optimize : bool On saving, compute optimal Huffman coding tables (can reduce a few percent of file size). Default False. baseline : bool Save basic JPEG, without metadata or any markers. Default False. """ class Reader(FreeimageFormat.Reader): def _open(self, flags=0, exifrotate=True, quickread=False): # Build flags from kwargs flags = int(flags) if exifrotate and exifrotate != 2: flags |= IO_FLAGS.JPEG_EXIFROTATE if not quickread: flags |= IO_FLAGS.JPEG_ACCURATE # Enter as usual, with modified flags return FreeimageFormat.Reader._open(self, flags) def _get_data(self, index): im, meta = FreeimageFormat.Reader._get_data(self, index) im = self._rotate(im, meta) return im, meta def _rotate(self, im, meta): """ Use Orientation information from EXIF meta data to orient the image correctly. Freeimage is also supposed to support that, and I am pretty sure it once did, but now it does not, so let's just do it in Python. Edit: and now it works again, just leave in place as a fallback. """ if self.request.kwargs.get('exifrotate', None) == 2: try: ori = meta['EXIF_MAIN']['Orientation'] except KeyError: # pragma: no cover pass # Orientation not available else: # pragma: no cover - we cannot touch all cases # www.impulseadventure.com/photo/exif-orientation.html if ori in [1, 2]: pass if ori in [3, 4]: im = np.rot90(im, 2) if ori in [5, 6]: im = np.rot90(im, 3) if ori in [7, 8]: im = np.rot90(im) if ori in [2, 4, 5, 7]: # Flipped cases (rare) im = np.fliplr(im) return im # -- class Writer(FreeimageFormat.Writer): def _open(self, flags=0, quality=75, progressive=False, optimize=False, baseline=False): # Test quality quality = int(quality) if quality < 1 or quality > 100: raise ValueError('JPEG quality should be between 1 and 100.') # Build flags from kwargs flags = int(flags) flags |= quality if progressive: flags |= IO_FLAGS.JPEG_PROGRESSIVE if optimize: flags |= IO_FLAGS.JPEG_OPTIMIZE if baseline: flags |= IO_FLAGS.JPEG_BASELINE # Act as usual, but with modified flags return FreeimageFormat.Writer._open(self, flags) def _append_data(self, im, meta): if im.ndim == 3 and im.shape[-1] == 4: raise IOError('JPEG does not support alpha channel.') im = image_as_uint(im, bitdepth=8) return FreeimageFormat.Writer._append_data(self, im, meta) ## Create the formats SPECIAL_CLASSES = {'jpeg': FreeimageJpegFormat, 'png': FreeimagePngFormat, 'bmp': FreeimageBmpFormat, 'gif': None, # defined in freeimagemulti 'ico': None, # defined in freeimagemulti 'mng': None, # defined in freeimagemulti } # rename TIFF to make way for the tiffile plugin NAME_MAP = {'TIFF': 'FI_TIFF'} # This is a dump of supported FreeImage formats on Linux fi verion 3.16.0 # > imageio.plugins.freeimage.create_freeimage_formats() # > for i in sorted(imageio.plugins.freeimage.fiformats): print('%r,' % (i, )) fiformats = [ ('BMP', 0, 'Windows or OS/2 Bitmap', 'bmp'), ('CUT', 21, 'Dr. Halo', 'cut'), ('DDS', 24, 'DirectX Surface', 'dds'), ('EXR', 29, 'ILM OpenEXR', 'exr'), ('G3', 27, 'Raw fax format CCITT G.3', 'g3'), ('GIF', 25, 'Graphics Interchange Format', 'gif'), ('HDR', 26, 'High Dynamic Range Image', 'hdr'), ('ICO', 1, 'Windows Icon', 'ico'), ('IFF', 5, 'IFF Interleaved Bitmap', 'iff,lbm'), ('J2K', 30, 'JPEG-2000 codestream', 'j2k,j2c'), ('JNG', 3, 'JPEG Network Graphics', 'jng'), ('JP2', 31, 'JPEG-2000 File Format', 'jp2'), ('JPEG', 2, 'JPEG - JFIF Compliant', 'jpg,jif,jpeg,jpe'), ('JPEG-XR', 36, 'JPEG XR image format', 'jxr,wdp,hdp'), ('KOALA', 4, 'C64 Koala Graphics', 'koa'), ('MNG', 6, 'Multiple-image Network Graphics', 'mng'), ('PBM', 7, 'Portable Bitmap (ASCII)', 'pbm'), ('PBMRAW', 8, 'Portable Bitmap (RAW)', 'pbm'), ('PCD', 9, 'Kodak PhotoCD', 'pcd'), ('PCX', 10, 'Zsoft Paintbrush', 'pcx'), ('PFM', 32, 'Portable floatmap', 'pfm'), ('PGM', 11, 'Portable Greymap (ASCII)', 'pgm'), ('PGMRAW', 12, 'Portable Greymap (RAW)', 'pgm'), ('PICT', 33, 'Macintosh PICT', 'pct,pict,pic'), ('PNG', 13, 'Portable Network Graphics', 'png'), ('PPM', 14, 'Portable Pixelmap (ASCII)', 'ppm'), ('PPMRAW', 15, 'Portable Pixelmap (RAW)', 'ppm'), ('PSD', 20, 'Adobe Photoshop', 'psd'), ('RAS', 16, 'Sun Raster Image', 'ras'), ('RAW', 34, 'RAW camera image', '3fr,arw,bay,bmq,cap,cine,cr2,crw,cs1,dc2,' 'dcr,drf,dsc,dng,erf,fff,ia,iiq,k25,kc2,kdc,mdc,mef,mos,mrw,nef,nrw,orf,' 'pef,ptx,pxn,qtk,raf,raw,rdc,rw2,rwl,rwz,sr2,srf,srw,sti'), ('SGI', 28, 'SGI Image Format', 'sgi,rgb,rgba,bw'), ('TARGA', 17, 'Truevision Targa', 'tga,targa'), ('TIFF', 18, 'Tagged Image File Format', 'tif,tiff'), ('WBMP', 19, 'Wireless Bitmap', 'wap,wbmp,wbm'), ('WebP', 35, 'Google WebP image format', 'webp'), ('XBM', 22, 'X11 Bitmap Format', 'xbm'), ('XPM', 23, 'X11 Pixmap Format', 'xpm'), ] def _create_predefined_freeimage_formats(): for name, i, des, ext in fiformats: name = NAME_MAP.get(name, name) # Get class for format FormatClass = SPECIAL_CLASSES.get(name.lower(), FreeimageFormat) if FormatClass: # Create Format and add format = FormatClass(name, des, ext, FormatClass._modes) format._fif = i formats.add_format(format) def create_freeimage_formats(): """ By default, imageio registers a list of predefined formats that freeimage can handle. If your version of imageio can handle more formats, you can call this function to register them. """ fiformats[:] = [] # Freeimage available? if fi is None: # pragma: no cover return # Init lib = fi._lib # Create formats for i in range(lib.FreeImage_GetFIFCount()): if lib.FreeImage_IsPluginEnabled(i): # Get info name = lib.FreeImage_GetFormatFromFIF(i).decode('ascii') des = lib.FreeImage_GetFIFDescription(i).decode('ascii') ext = lib.FreeImage_GetFIFExtensionList(i).decode('ascii') fiformats.append((name, i, des, ext)) name = NAME_MAP.get(name, name) # Get class for format FormatClass = SPECIAL_CLASSES.get(name.lower(), FreeimageFormat) if FormatClass: # Create Format and add format = FormatClass(name, des, ext, FormatClass._modes) format._fif = i formats.add_format(format, overwrite=True) _create_predefined_freeimage_formats() ================================================ FILE: core/lib/imageio/resources/shipped_resources_go_here ================================================ ================================================ FILE: core/lib/imageio/testing.py ================================================ # -*- coding: utf-8 -*- # Copyright (c) 2015, imageio contributors # Distributed under the (new) BSD License. See LICENSE.txt for more info. """ Functionality used for testing. This code itself is not covered in tests. """ from __future__ import absolute_import, print_function, division import os import sys import inspect import shutil import atexit import pytest # Get root dir THIS_DIR = os.path.abspath(os.path.dirname(__file__)) ROOT_DIR = THIS_DIR for i in range(9): ROOT_DIR = os.path.dirname(ROOT_DIR) if os.path.isfile(os.path.join(ROOT_DIR, '.gitignore')): break STYLE_IGNORES = ['E226', 'E241', 'E265', 'E266', # too many leading '#' for block comment 'E402', # module level import not at top of file 'E731', # do not assign a lambda expression, use a def 'W291', 'W293', 'W503', # line break before binary operator ] ## Functions to use in tests def run_tests_if_main(show_coverage=False): """ Run tests in a given file if it is run as a script Coverage is reported for running this single test. Set show_coverage to launch the report in the web browser. """ local_vars = inspect.currentframe().f_back.f_locals if not local_vars.get('__name__', '') == '__main__': return # we are in a "__main__" os.chdir(ROOT_DIR) fname = str(local_vars['__file__']) _clear_imageio() _enable_faulthandler() pytest.main('-v -x --color=yes --cov imageio ' '--cov-config .coveragerc --cov-report html %s' % repr(fname)) if show_coverage: import webbrowser fname = os.path.join(ROOT_DIR, 'htmlcov', 'index.html') webbrowser.open_new_tab(fname) _the_test_dir = None def get_test_dir(): global _the_test_dir if _the_test_dir is None: # Define dir from imageio.core import appdata_dir _the_test_dir = os.path.join(appdata_dir('imageio'), 'testdir') # Clear and create it now clean_test_dir(True) os.makedirs(_the_test_dir) os.makedirs(os.path.join(_the_test_dir, 'images')) # And later atexit.register(clean_test_dir) return _the_test_dir def clean_test_dir(strict=False): if os.path.isdir(_the_test_dir): try: shutil.rmtree(_the_test_dir) except Exception: if strict: raise def need_internet(): if os.getenv('IMAGEIO_NO_INTERNET', '').lower() in ('1', 'true', 'yes'): pytest.skip('No internet') ## Functions to use from make def test_unit(cov_report='term'): """ Run all unit tests. Returns exit code. """ orig_dir = os.getcwd() os.chdir(ROOT_DIR) try: _clear_imageio() _enable_faulthandler() return pytest.main('-v --cov imageio --cov-config .coveragerc ' '--cov-report %s tests' % cov_report) finally: os.chdir(orig_dir) import imageio print('Tests were performed on', str(imageio)) def test_style(): """ Test style using flake8 """ # Test if flake is there try: from flake8.main import main # noqa except ImportError: print('Skipping flake8 test, flake8 not installed') return # Reporting print('Running flake8 on %s' % ROOT_DIR) sys.stdout = FileForTesting(sys.stdout) # Init ignores = STYLE_IGNORES.copy() fail = False count = 0 # Iterate over files for dir, dirnames, filenames in os.walk(ROOT_DIR): dir = os.path.relpath(dir, ROOT_DIR) # Skip this dir? exclude_dirs = set(['.git', 'docs', 'build', 'dist', '__pycache__']) if exclude_dirs.intersection(dir.split(os.path.sep)): continue # Check all files ... for fname in filenames: if fname.endswith('.py'): # Get test options for this file filename = os.path.join(ROOT_DIR, dir, fname) skip, extra_ignores = _get_style_test_options(filename) if skip: continue # Test count += 1 thisfail = _test_style(filename, ignores + extra_ignores) if thisfail: fail = True print('----') sys.stdout.flush() # Report result sys.stdout.revert() if not count: raise RuntimeError(' Arg! flake8 did not check any files') elif fail: raise RuntimeError(' Arg! flake8 failed (checked %i files)' % count) else: print(' Hooray! flake8 passed (checked %i files)' % count) ## Requirements def _enable_faulthandler(): """ Enable faulthandler (if we can), so that we get tracebacks on segfaults. """ try: import faulthandler faulthandler.enable() print('Faulthandler enabled') except Exception: print('Could not enable faulthandler') def _clear_imageio(): # Remove ourselves from sys.modules to force an import for key in list(sys.modules.keys()): if key.startswith('imageio'): del sys.modules[key] class FileForTesting(object): """ Alternative to stdout that makes path relative to ROOT_DIR """ def __init__(self, original): self._original = original def write(self, msg): if msg.startswith(ROOT_DIR): msg = os.path.relpath(msg, ROOT_DIR) self._original.write(msg) self._original.flush() def flush(self): self._original.flush() def revert(self): sys.stdout = self._original def _get_style_test_options(filename): """ Returns (skip, ignores) for the specifies source file. """ skip = False ignores = [] text = open(filename, 'rb').read().decode('utf-8') # Iterate over lines for i, line in enumerate(text.splitlines()): if i > 20: break if line.startswith('# styletest:'): if 'skip' in line: skip = True elif 'ignore' in line: words = line.replace(',', ' ').split(' ') words = [w.strip() for w in words if w.strip()] words = [w for w in words if (w[1:].isnumeric() and w[0] in 'EWFCN')] ignores.extend(words) return skip, ignores def _test_style(filename, ignore): """ Test style for a certain file. """ if isinstance(ignore, (list, tuple)): ignore = ','.join(ignore) orig_dir = os.getcwd() orig_argv = sys.argv os.chdir(ROOT_DIR) sys.argv[1:] = [filename] sys.argv.append('--ignore=' + ignore) try: from flake8.main import main main() except SystemExit as ex: if ex.code in (None, 0): return False else: return True finally: os.chdir(orig_dir) sys.argv[:] = orig_argv ================================================ FILE: core/lib/imghdr.py ================================================ """Recognize image file formats based on their first few bytes.""" from os import PathLike __all__ = ["what"] #-------------------------# # Recognize image headers # #-------------------------# def what(file, h=None): f = None try: if h is None: if isinstance(file, (str, PathLike)): f = open(file, 'rb') h = f.read(32) else: location = file.tell() h = file.read(32) file.seek(location) for tf in tests: res = tf(h, f) if res: return res finally: if f: f.close() return None #---------------------------------# # Subroutines per image file type # #---------------------------------# tests = [] def test_jpeg(h, f): """JPEG data in JFIF or Exif format""" if h[6:10] in (b'JFIF', b'Exif'): return 'jpeg' tests.append(test_jpeg) def test_png(h, f): if h.startswith(b'\211PNG\r\n\032\n'): return 'png' tests.append(test_png) def test_gif(h, f): """GIF ('87 and '89 variants)""" if h[:6] in (b'GIF87a', b'GIF89a'): return 'gif' tests.append(test_gif) def test_tiff(h, f): """TIFF (can be in Motorola or Intel byte order)""" if h[:2] in (b'MM', b'II'): return 'tiff' tests.append(test_tiff) def test_rgb(h, f): """SGI image library""" if h.startswith(b'\001\332'): return 'rgb' tests.append(test_rgb) def test_pbm(h, f): """PBM (portable bitmap)""" if len(h) >= 3 and \ h[0] == ord(b'P') and h[1] in b'14' and h[2] in b' \t\n\r': return 'pbm' tests.append(test_pbm) def test_pgm(h, f): """PGM (portable graymap)""" if len(h) >= 3 and \ h[0] == ord(b'P') and h[1] in b'25' and h[2] in b' \t\n\r': return 'pgm' tests.append(test_pgm) def test_ppm(h, f): """PPM (portable pixmap)""" if len(h) >= 3 and \ h[0] == ord(b'P') and h[1] in b'36' and h[2] in b' \t\n\r': return 'ppm' tests.append(test_ppm) def test_rast(h, f): """Sun raster file""" if h.startswith(b'\x59\xA6\x6A\x95'): return 'rast' tests.append(test_rast) def test_xbm(h, f): """X bitmap (X10 or X11)""" if h.startswith(b'#define '): return 'xbm' tests.append(test_xbm) def test_bmp(h, f): if h.startswith(b'BM'): return 'bmp' tests.append(test_bmp) def test_webp(h, f): if h.startswith(b'RIFF') and h[8:12] == b'WEBP': return 'webp' tests.append(test_webp) def test_exr(h, f): if h.startswith(b'\x76\x2f\x31\x01'): return 'exr' tests.append(test_exr) #--------------------# # Small test program # #--------------------# def test(): import sys recursive = 0 if sys.argv[1:] and sys.argv[1] == '-r': del sys.argv[1:2] recursive = 1 try: if sys.argv[1:]: testall(sys.argv[1:], recursive, 1) else: testall(['.'], recursive, 1) except KeyboardInterrupt: sys.stderr.write('\n[Interrupted]\n') sys.exit(1) def testall(list, recursive, toplevel): import sys import os for filename in list: if os.path.isdir(filename): print(filename + '/:', end=' ') if recursive or toplevel: print('recursing down:') import glob names = glob.glob(os.path.join(filename, '*')) testall(names, recursive, 0) else: print('*** directory (use -r) ***') else: print(filename + ':', end=' ') sys.stdout.flush() try: print(what(filename)) except OSError: print('*** not found ***') if __name__ == '__main__': test() ================================================ FILE: core/lib/shapefile.py ================================================ """ shapefile.py Provides read and write support for ESRI Shapefiles. author: jlawheadgeospatialpython.com version: 2.1.0 Compatible with Python versions 2.7-3.x """ __version__ = "2.1.0" from struct import pack, unpack, calcsize, error, Struct import os import sys import time import array import tempfile import warnings import io from datetime import date # Constants for shape types NULL = 0 POINT = 1 POLYLINE = 3 POLYGON = 5 MULTIPOINT = 8 POINTZ = 11 POLYLINEZ = 13 POLYGONZ = 15 MULTIPOINTZ = 18 POINTM = 21 POLYLINEM = 23 POLYGONM = 25 MULTIPOINTM = 28 MULTIPATCH = 31 SHAPETYPE_LOOKUP = { 0: 'NULL', 1: 'POINT', 3: 'POLYLINE', 5: 'POLYGON', 8: 'MULTIPOINT', 11: 'POINTZ', 13: 'POLYLINEZ', 15: 'POLYGONZ', 18: 'MULTIPOINTZ', 21: 'POINTM', 23: 'POLYLINEM', 25: 'POLYGONM', 28: 'MULTIPOINTM', 31: 'MULTIPATCH'} TRIANGLE_STRIP = 0 TRIANGLE_FAN = 1 OUTER_RING = 2 INNER_RING = 3 FIRST_RING = 4 RING = 5 PARTTYPE_LOOKUP = { 0: 'TRIANGLE_STRIP', 1: 'TRIANGLE_FAN', 2: 'OUTER_RING', 3: 'INNER_RING', 4: 'FIRST_RING', 5: 'RING'} # Python 2-3 handling PYTHON3 = sys.version_info[0] == 3 if PYTHON3: xrange = range izip = zip else: from itertools import izip # Helpers MISSING = [None,''] NODATA = -10e38 # as per the ESRI shapefile spec, only used for m-values. if PYTHON3: def b(v, encoding='utf-8', encodingErrors='strict'): if isinstance(v, str): # For python 3 encode str to bytes. return v.encode(encoding, encodingErrors) elif isinstance(v, bytes): # Already bytes. return v elif v is None: # Since we're dealing with text, interpret None as "" return b"" else: # Force string representation. return str(v).encode(encoding, encodingErrors) def u(v, encoding='utf-8', encodingErrors='strict'): if isinstance(v, bytes): # For python 3 decode bytes to str. return v.decode(encoding, encodingErrors) elif isinstance(v, str): # Already str. return v elif v is None: # Since we're dealing with text, interpret None as "" return "" else: # Force string representation. return bytes(v).decode(encoding, encodingErrors) def is_string(v): return isinstance(v, str) else: def b(v, encoding='utf-8', encodingErrors='strict'): if isinstance(v, unicode): # For python 2 encode unicode to bytes. return v.encode(encoding, encodingErrors) elif isinstance(v, bytes): # Already bytes. return v elif v is None: # Since we're dealing with text, interpret None as "" return "" else: # Force string representation. return unicode(v).encode(encoding, encodingErrors) def u(v, encoding='utf-8', encodingErrors='strict'): if isinstance(v, bytes): # For python 2 decode bytes to unicode. return v.decode(encoding, encodingErrors) elif isinstance(v, unicode): # Already unicode. return v elif v is None: # Since we're dealing with text, interpret None as "" return u"" else: # Force string representation. return bytes(v).decode(encoding, encodingErrors) def is_string(v): return isinstance(v, basestring) # Begin class _Array(array.array): """Converts python tuples to lits of the appropritate type. Used to unpack different shapefile header parts.""" def __repr__(self): return str(self.tolist()) def signed_area(coords): """Return the signed area enclosed by a ring using the linear time algorithm. A value >= 0 indicates a counter-clockwise oriented ring. """ xs, ys = map(list, zip(*coords)) xs.append(xs[1]) ys.append(ys[1]) return sum(xs[i]*(ys[i+1]-ys[i-1]) for i in range(1, len(coords)))/2.0 class Shape(object): def __init__(self, shapeType=NULL, points=None, parts=None, partTypes=None): """Stores the geometry of the different shape types specified in the Shapefile spec. Shape types are usually point, polyline, or polygons. Every shape type except the "Null" type contains points at some level for example verticies in a polygon. If a shape type has multiple shapes containing points within a single geometry record then those shapes are called parts. Parts are designated by their starting index in geometry record's list of shapes. For MultiPatch geometry, partTypes designates the patch type of each of the parts. """ self.shapeType = shapeType self.points = points or [] self.parts = parts or [] if partTypes: self.partTypes = partTypes @property def __geo_interface__(self): if not self.parts or not self.points: Exception('Invalid shape, cannot create GeoJSON representation. Shape type is "%s" but does not contain any parts and/or points.' % SHAPETYPE_LOOKUP[self.shapeType]) if self.shapeType in [POINT, POINTM, POINTZ]: return { 'type': 'Point', 'coordinates': tuple(self.points[0]) } elif self.shapeType in [MULTIPOINT, MULTIPOINTM, MULTIPOINTZ]: return { 'type': 'MultiPoint', 'coordinates': tuple([tuple(p) for p in self.points]) } elif self.shapeType in [POLYLINE, POLYLINEM, POLYLINEZ]: if len(self.parts) == 1: return { 'type': 'LineString', 'coordinates': tuple([tuple(p) for p in self.points]) } else: ps = None coordinates = [] for part in self.parts: if ps == None: ps = part continue else: coordinates.append(tuple([tuple(p) for p in self.points[ps:part]])) ps = part else: coordinates.append(tuple([tuple(p) for p in self.points[part:]])) return { 'type': 'MultiLineString', 'coordinates': tuple(coordinates) } elif self.shapeType in [POLYGON, POLYGONM, POLYGONZ]: if len(self.parts) == 1: return { 'type': 'Polygon', 'coordinates': (tuple([tuple(p) for p in self.points]),) } else: ps = None rings = [] for part in self.parts: if ps == None: ps = part continue else: rings.append(tuple([tuple(p) for p in self.points[ps:part]])) ps = part else: rings.append(tuple([tuple(p) for p in self.points[part:]])) polys = [] poly = [rings[0]] for ring in rings[1:]: if signed_area(ring) < 0: polys.append(poly) poly = [ring] else: poly.append(ring) polys.append(poly) if len(polys) == 1: return { 'type': 'Polygon', 'coordinates': tuple(polys[0]) } elif len(polys) > 1: return { 'type': 'MultiPolygon', 'coordinates': polys } else: raise Exception('Shape type "%s" cannot be represented as GeoJSON.' % SHAPETYPE_LOOKUP[self.shapeType]) @staticmethod def _from_geojson(geoj): # create empty shape shape = Shape() # set shapeType geojType = geoj["type"] if geoj else "Null" if geojType == "Null": shapeType = NULL elif geojType == "Point": shapeType = POINT elif geojType == "LineString": shapeType = POLYLINE elif geojType == "Polygon": shapeType = POLYGON elif geojType == "MultiPoint": shapeType = MULTIPOINT elif geojType == "MultiLineString": shapeType = POLYLINE elif geojType == "MultiPolygon": shapeType = POLYGON else: raise Exception("Cannot create Shape from GeoJSON type '%s'" % geojType) shape.shapeType = shapeType # set points and parts if geojType == "Point": shape.points = [ geoj["coordinates"] ] shape.parts = [0] elif geojType in ("MultiPoint","LineString"): shape.points = geoj["coordinates"] shape.parts = [0] elif geojType in ("Polygon"): points = [] parts = [] index = 0 for i,ext_or_hole in enumerate(geoj["coordinates"]): if i == 0 and not signed_area(ext_or_hole) < 0: # flip exterior direction ext_or_hole = list(reversed(ext_or_hole)) elif i > 0 and not signed_area(ext_or_hole) >= 0: # flip hole direction ext_or_hole = list(reversed(ext_or_hole)) points.extend(ext_or_hole) parts.append(index) index += len(ext_or_hole) shape.points = points shape.parts = parts elif geojType in ("MultiLineString"): points = [] parts = [] index = 0 for linestring in geoj["coordinates"]: points.extend(linestring) parts.append(index) index += len(linestring) shape.points = points shape.parts = parts elif geojType in ("MultiPolygon"): points = [] parts = [] index = 0 for polygon in geoj["coordinates"]: for i,ext_or_hole in enumerate(polygon): if i == 0 and not signed_area(ext_or_hole) < 0: # flip exterior direction ext_or_hole = list(reversed(ext_or_hole)) elif i > 0 and not signed_area(ext_or_hole) >= 0: # flip hole direction ext_or_hole = list(reversed(ext_or_hole)) points.extend(ext_or_hole) parts.append(index) index += len(ext_or_hole) shape.points = points shape.parts = parts return shape @property def shapeTypeName(self): return SHAPETYPE_LOOKUP[self.shapeType] class _Record(list): """ A class to hold a record. Subclasses list to ensure compatibility with former work and allows to use all the optimazations of the builtin list. In addition to the list interface, the values of the record can also be retrieved using the fields name. Eg. if the dbf contains a field ID at position 0, the ID can be retrieved with the position, the field name as a key or the field name as an attribute. >>> # Create a Record with one field, normally the record is created by the Reader class >>> r = _Record({'ID': 0}, [0]) >>> print(r[0]) >>> print(r['ID']) >>> print(r.ID) """ def __init__(self, field_positions, values, oid=None): """ A Record should be created by the Reader class :param field_positions: A dict mapping field names to field positions :param values: A sequence of values :param oid: The object id, an int (optional) """ self.__field_positions = field_positions if oid is not None: self.__oid = oid else: self.__oid = -1 list.__init__(self, values) def __getattr__(self, item): """ __getattr__ is called if an attribute is used that does not exist in the normal sense. Eg. r=Record(...), r.ID calls r.__getattr__('ID'), but r.index(5) calls list.index(r, 5) :param item: The field name, used as attribute :return: Value of the field :raises: Attribute error, if field does not exist and IndexError, if field exists but not values in the Record """ try: index = self.__field_positions[item] return list.__getitem__(self, index) except KeyError: raise AttributeError('{} is not a field name'.format(item)) except IndexError: raise IndexError('{} found as a field but not enough values available.'.format(item)) def __setattr__(self, key, value): """ Sets a value of a field attribute :param key: The field name :param value: the value of that field :return: None :raises: AttributeError, if key is not a field of the shapefile """ if key.startswith('_'): # Prevent infinite loop when setting mangled attribute return list.__setattr__(self, key, value) try: index = self.__field_positions[key] return list.__setitem__(self, index, value) except KeyError: raise AttributeError('{} is not a field name'.format(key)) def __getitem__(self, item): """ Extends the normal list item access with access using a fieldname Eg. r['ID'], r[0] :param item: Either the position of the value or the name of a field :return: the value of the field """ try: return list.__getitem__(self, item) except TypeError: try: index = self.__field_positions[item] except KeyError: index = None if index is not None: return list.__getitem__(self, index) else: raise IndexError('"{}" is not a field name and not an int'.format(item)) def __setitem__(self, key, value): """ Extends the normal list item access with access using a fieldname Eg. r['ID']=2, r[0]=2 :param key: Either the position of the value or the name of a field :param value: the new value of the field """ try: return list.__setitem__(self, key, value) except TypeError: index = self.__field_positions.get(key) if index is not None: return list.__setitem__(self, index, value) else: raise IndexError('{} is not a field name and not an int'.format(key)) @property def oid(self): """The index position of the record in the original shapefile""" return self.__oid def as_dict(self): """ Returns this Record as a dictionary using the field names as keys :return: dict """ return dict((f, self[i]) for f, i in self.__field_positions.items()) def __repr__(self): return 'Record #{}: {}'.format(self.__oid, list(self)) def __dir__(self): """ Helps to show the field names in an interactive environment like IPython. See: http://ipython.readthedocs.io/en/stable/config/integrating.html :return: List of method names and fields """ default = list(dir(type(self))) # default list methods and attributes of this class fnames = list(self.__field_positions.keys()) # plus field names (random order) return default + fnames class ShapeRecord(object): """A ShapeRecord object containing a shape along with its attributes. Provides the GeoJSON __geo_interface__ to return a Feature dictionary.""" def __init__(self, shape=None, record=None): self.shape = shape self.record = record @property def __geo_interface__(self): return {'type': 'Feature', 'properties': self.record.as_dict(), 'geometry': self.shape.__geo_interface__} class Shapes(list): """A class to hold a list of Shape objects. Subclasses list to ensure compatibility with former work and allows to use all the optimazations of the builtin list. In addition to the list interface, this also provides the GeoJSON __geo_interface__ to return a GeometryCollection dictionary. """ def __repr__(self): return 'Shapes: {}'.format(list(self)) @property def __geo_interface__(self): return {'type': 'GeometryCollection', 'geometries': [g.__geo_interface__ for g in self]} class ShapeRecords(list): """A class to hold a list of ShapeRecord objects. Subclasses list to ensure compatibility with former work and allows to use all the optimazations of the builtin list. In addition to the list interface, this also provides the GeoJSON __geo_interface__ to return a FeatureCollection dictionary. """ def __repr__(self): return 'ShapeRecords: {}'.format(list(self)) @property def __geo_interface__(self): return {'type': 'FeatureCollection', 'features': [f.__geo_interface__ for f in self]} class ShapefileException(Exception): """An exception to handle shapefile specific problems.""" pass class Reader(object): """Reads the three files of a shapefile as a unit or separately. If one of the three files (.shp, .shx, .dbf) is missing no exception is thrown until you try to call a method that depends on that particular file. The .shx index file is used if available for efficiency but is not required to read the geometry from the .shp file. The "shapefile" argument in the constructor is the name of the file you want to open. You can instantiate a Reader without specifying a shapefile and then specify one later with the load() method. Only the shapefile headers are read upon loading. Content within each file is only accessed when required and as efficiently as possible. Shapefiles are usually not large but they can be. """ def __init__(self, *args, **kwargs): self.shp = None self.shx = None self.dbf = None self.shapeName = "Not specified" self._offsets = [] self.shpLength = None self.numRecords = None self.fields = [] self.__dbfHdrLength = 0 self.__fieldposition_lookup = {} self.encoding = kwargs.pop('encoding', 'utf-8') self.encodingErrors = kwargs.pop('encodingErrors', 'strict') # See if a shapefile name was passed as an argument if len(args) > 0: if is_string(args[0]): self.load(args[0]) return if "shp" in kwargs.keys(): if hasattr(kwargs["shp"], "read"): self.shp = kwargs["shp"] # Copy if required try: self.shp.seek(0) except (NameError, io.UnsupportedOperation): self.shp = io.BytesIO(self.shp.read()) if "shx" in kwargs.keys(): if hasattr(kwargs["shx"], "read"): self.shx = kwargs["shx"] # Copy if required try: self.shx.seek(0) except (NameError, io.UnsupportedOperation): self.shx = io.BytesIO(self.shx.read()) if "dbf" in kwargs.keys(): if hasattr(kwargs["dbf"], "read"): self.dbf = kwargs["dbf"] # Copy if required try: self.dbf.seek(0) except (NameError, io.UnsupportedOperation): self.dbf = io.BytesIO(self.dbf.read()) if self.shp or self.dbf: self.load() else: raise ShapefileException("Shapefile Reader requires a shapefile or file-like object.") def __str__(self): """ Use some general info on the shapefile as __str__ """ info = ['shapefile Reader'] if self.shp: info.append(" {} shapes (type '{}')".format( len(self), SHAPETYPE_LOOKUP[self.shapeType])) if self.dbf: info.append(' {} records ({} fields)'.format( len(self), len(self.fields))) return '\n'.join(info) def __enter__(self): """ Enter phase of context manager. """ return self def __exit__(self, exc_type, exc_val, exc_tb): """ Exit phase of context manager, close opened files. """ self.close() def __len__(self): """Returns the number of shapes/records in the shapefile.""" return self.numRecords def __iter__(self): """Iterates through the shapes/records in the shapefile.""" for shaperec in self.iterShapeRecords(): yield shaperec @property def __geo_interface__(self): fieldnames = [f[0] for f in self.fields] features = [] for feat in self.iterShapeRecords(): fdict = {'type': 'Feature', 'properties': dict(zip(fieldnames,feat.record)), 'geometry': feat.shape.__geo_interface__} features.append(fdict) return {'type': 'FeatureCollection', 'bbox': self.bbox, 'features': features} @property def shapeTypeName(self): return SHAPETYPE_LOOKUP[self.shapeType] def load(self, shapefile=None): """Opens a shapefile from a filename or file-like object. Normally this method would be called by the constructor with the file name as an argument.""" if shapefile: (shapeName, ext) = os.path.splitext(shapefile) self.shapeName = shapeName self.load_shp(shapeName) self.load_shx(shapeName) self.load_dbf(shapeName) if not (self.shp or self.dbf): raise ShapefileException("Unable to open %s.dbf or %s.shp." % (shapeName, shapeName)) if self.shp: self.__shpHeader() if self.dbf: self.__dbfHeader() def load_shp(self, shapefile_name): """ Attempts to load file with .shp extension as both lower and upper case """ shp_ext = 'shp' try: self.shp = open("%s.%s" % (shapefile_name, shp_ext), "rb") except IOError: try: self.shp = open("%s.%s" % (shapefile_name, shp_ext.upper()), "rb") except IOError: pass def load_shx(self, shapefile_name): """ Attempts to load file with .shx extension as both lower and upper case """ shx_ext = 'shx' try: self.shx = open("%s.%s" % (shapefile_name, shx_ext), "rb") except IOError: try: self.shx = open("%s.%s" % (shapefile_name, shx_ext.upper()), "rb") except IOError: pass def load_dbf(self, shapefile_name): """ Attempts to load file with .dbf extension as both lower and upper case """ dbf_ext = 'dbf' try: self.dbf = open("%s.%s" % (shapefile_name, dbf_ext), "rb") except IOError: try: self.dbf = open("%s.%s" % (shapefile_name, dbf_ext.upper()), "rb") except IOError: pass def __del__(self): self.close() def close(self): for attribute in (self.shp, self.shx, self.dbf): if hasattr(attribute, 'close'): try: attribute.close() except IOError: pass def __getFileObj(self, f): """Checks to see if the requested shapefile file object is available. If not a ShapefileException is raised.""" if not f: raise ShapefileException("Shapefile Reader requires a shapefile or file-like object.") if self.shp and self.shpLength is None: self.load() if self.dbf and len(self.fields) == 0: self.load() return f def __restrictIndex(self, i): """Provides list-like handling of a record index with a clearer error message if the index is out of bounds.""" if self.numRecords: rmax = self.numRecords - 1 if abs(i) > rmax: raise IndexError("Shape or Record index out of range.") if i < 0: i = range(self.numRecords)[i] return i def __shpHeader(self): """Reads the header information from a .shp or .shx file.""" if not self.shp: raise ShapefileException("Shapefile Reader requires a shapefile or file-like object. (no shp file found") shp = self.shp # File length (16-bit word * 2 = bytes) shp.seek(24) self.shpLength = unpack(">i", shp.read(4))[0] * 2 # Shape type shp.seek(32) self.shapeType= unpack(" NODATA: self.mbox.append(m) else: self.mbox.append(None) def __shape(self): """Returns the header info and geometry for a single shape.""" f = self.__getFileObj(self.shp) record = Shape() nParts = nPoints = zmin = zmax = mmin = mmax = None (recNum, recLength) = unpack(">2i", f.read(8)) # Determine the start of the next record next = f.tell() + (2 * recLength) shapeType = unpack("= 16: (mmin, mmax) = unpack("<2d", f.read(16)) # Measure values less than -10e38 are nodata values according to the spec if next - f.tell() >= nPoints * 8: record.m = [] for m in _Array('d', unpack("<%sd" % nPoints, f.read(nPoints * 8))): if m > NODATA: record.m.append(m) else: record.m.append(None) else: record.m = [None for _ in range(nPoints)] # Read a single point if shapeType in (1,11,21): record.points = [_Array('d', unpack("<2d", f.read(16)))] # Read a single Z value if shapeType == 11: record.z = list(unpack("= 8: (m,) = unpack(" NODATA: record.m = [m] else: record.m = [None] # Seek to the end of this record as defined by the record header because # the shapefile spec doesn't require the actual content to meet the header # definition. Probably allowed for lazy feature deletion. f.seek(next) return record def __shapeIndex(self, i=None): """Returns the offset in a .shp file for a shape based on information in the .shx index file.""" shx = self.shx if not shx: return None if not self._offsets: # File length (16-bit word * 2 = bytes) - header length shx.seek(24) shxRecordLength = (unpack(">i", shx.read(4))[0] * 2) - 100 numRecords = shxRecordLength // 8 # Jump to the first record. shx.seek(100) shxRecords = _Array('i') # Each offset consists of two nrs, only the first one matters shxRecords.fromfile(shx, 2 * numRecords) if sys.byteorder != 'big': shxRecords.byteswap() self._offsets = [2 * el for el in shxRecords[::2]] if not i == None: return self._offsets[i] def shape(self, i=0): """Returns a shape object for a shape in the the geometry record file.""" shp = self.__getFileObj(self.shp) i = self.__restrictIndex(i) offset = self.__shapeIndex(i) if not offset: # Shx index not available so iterate the full list. for j,k in enumerate(self.iterShapes()): if j == i: return k shp.seek(offset) return self.__shape() def shapes(self): """Returns all shapes in a shapefile.""" shp = self.__getFileObj(self.shp) # Found shapefiles which report incorrect # shp file length in the header. Can't trust # that so we seek to the end of the file # and figure it out. shp.seek(0,2) self.shpLength = shp.tell() shp.seek(100) shapes = Shapes() while shp.tell() < self.shpLength: shapes.append(self.__shape()) return shapes def iterShapes(self): """Serves up shapes in a shapefile as an iterator. Useful for handling large shapefiles.""" shp = self.__getFileObj(self.shp) shp.seek(0,2) self.shpLength = shp.tell() shp.seek(100) while shp.tell() < self.shpLength: yield self.__shape() def __dbfHeader(self): """Reads a dbf header. Xbase-related code borrows heavily from ActiveState Python Cookbook Recipe 362715 by Raymond Hettinger""" if not self.dbf: raise ShapefileException("Shapefile Reader requires a shapefile or file-like object. (no dbf file found)") dbf = self.dbf # read relevant header parts self.numRecords, self.__dbfHdrLength, self.__recordLength = \ unpack(" 0: px, py = list(zip(*s.points))[:2] x.extend(px) y.extend(py) else: # this should not happen. # any shape that is not null should have at least one point, and only those should be sent here. # could also mean that earlier code failed to add points to a non-null shape. 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) bbox = [min(x), min(y), max(x), max(y)] # update global if self._bbox: # compare with existing 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])] else: # first time bbox is being set self._bbox = bbox return bbox def __zbox(self, s): z = [] for p in s.points: try: z.append(p[2]) except IndexError: # point did not have z value # setting it to 0 is probably ok, since it means all are on the same elavation z.append(0) zbox = [min(z), max(z)] # update global if self._zbox: # compare with existing self._zbox = [min(zbox[0],self._zbox[0]), max(zbox[1],self._zbox[1])] else: # first time zbox is being set self._zbox = zbox return zbox def __mbox(self, s): mpos = 3 if s.shapeType in (11,13,15,18,31) else 2 m = [] for p in s.points: try: if p[mpos] is not None: # mbox should only be calculated on valid m values m.append(p[mpos]) except IndexError: # point did not have m value so is missing # mbox should only be calculated on valid m values pass if not m: # only if none of the shapes had m values, should mbox be set to missing m values m.append(NODATA) mbox = [min(m), max(m)] # update global if self._mbox: # compare with existing self._mbox = [min(mbox[0],self._mbox[0]), max(mbox[1],self._mbox[1])] else: # first time mbox is being set self._mbox = mbox return mbox @property def shapeTypeName(self): return SHAPETYPE_LOOKUP[self.shapeType] def bbox(self): """Returns the current bounding box for the shapefile which is the lower-left and upper-right corners. It does not contain the elevation or measure extremes.""" return self._bbox def zbox(self): """Returns the current z extremes for the shapefile.""" return self._zbox def mbox(self): """Returns the current m extremes for the shapefile.""" return self._mbox def __shapefileHeader(self, fileObj, headerType='shp'): """Writes the specified header type to the specified file-like object. Several of the shapefile formats are so similar that a single generic method to read or write them is warranted.""" f = self.__getFileObj(fileObj) f.seek(0) # File code, Unused bytes f.write(pack(">6i", 9994,0,0,0,0,0)) # File length (Bytes / 2 = 16-bit words) if headerType == 'shp': f.write(pack(">i", self.__shpFileLength())) elif headerType == 'shx': f.write(pack('>i', ((100 + (self.shpNum * 8)) // 2))) # Version, Shape type if self.shapeType is None: self.shapeType = NULL f.write(pack("<2i", 1000, self.shapeType)) # The shapefile's bounding box (lower left, upper right) if self.shapeType != 0: try: bbox = self.bbox() if bbox is None: # The bbox is initialized with None, so this would mean the shapefile contains no valid geometries. # In such cases of empty shapefiles, ESRI spec says the bbox values are 'unspecified'. # Not sure what that means, so for now just setting to 0s, which is the same behavior as in previous versions. # This would also make sense since the Z and M bounds are similarly set to 0 for non-Z/M type shapefiles. bbox = [0,0,0,0] f.write(pack("<4d", *bbox)) except error: raise ShapefileException("Failed to write shapefile bounding box. Floats required.") else: f.write(pack("<4d", 0,0,0,0)) # Elevation if self.shapeType in (11,13,15,18): # Z values are present in Z type zbox = self.zbox() else: # As per the ESRI shapefile spec, the zbox for non-Z type shapefiles are set to 0s zbox = [0,0] # Measure if self.shapeType in (11,13,15,18,21,23,25,28,31): # M values are present in M or Z type mbox = self.mbox() else: # As per the ESRI shapefile spec, the mbox for non-M type shapefiles are set to 0s mbox = [0,0] # Try writing try: f.write(pack("<4d", zbox[0], zbox[1], mbox[0], mbox[1])) except error: raise ShapefileException("Failed to write shapefile elevation and measure values. Floats required.") def __dbfHeader(self): """Writes the dbf header and field descriptors.""" f = self.__getFileObj(self.dbf) f.seek(0) version = 3 year, month, day = time.localtime()[:3] year -= 1900 # Remove deletion flag placeholder from fields for field in self.fields: if field[0].startswith("Deletion"): self.fields.remove(field) numRecs = self.recNum numFields = len(self.fields) headerLength = numFields * 32 + 33 if headerLength >= 65535: raise ShapefileException( "Shapefile dbf header length exceeds maximum length.") recordLength = sum([int(field[2]) for field in self.fields]) + 1 header = pack('2i", self.shpNum, 0)) start = f.tell() # Shape Type if self.shapeType is None and s.shapeType != NULL: self.shapeType = s.shapeType if s.shapeType != NULL and s.shapeType != self.shapeType: raise Exception("The shape's type (%s) must match the type of the shapefile (%s)." % (s.shapeType, self.shapeType)) f.write(pack(" 2 else 0)) for p in s.points] except error: raise ShapefileException("Failed to write elevation values for record %s. Expected floats." % self.shpNum) # Write m extremes and values # When reading a file, pyshp converts NODATA m values to None, so here we make sure to convert them back to NODATA # Note: missing m values are autoset to NODATA. if s.shapeType in (13,15,18,23,25,28,31): try: f.write(pack("<2d", *self.__mbox(s))) except error: raise ShapefileException("Failed to write measure extremes for record %s. Expected floats" % self.shpNum) try: if hasattr(s,"m"): # if m values are stored in attribute f.write(pack("<%sd" % len(s.m), *[m if m is not None else NODATA for m in s.m])) else: # if m values are stored as 3rd/4th dimension # 0-index position of m value is 3 if z type (x,y,z,m), or 2 if m type (x,y,m) mpos = 3 if s.shapeType in (13,15,18,31) else 2 [f.write(pack(" mpos and p[mpos] is not None else NODATA)) for p in s.points] except error: raise ShapefileException("Failed to write measure values for record %s. Expected floats" % self.shpNum) # Write a single point if s.shapeType in (1,11,21): try: f.write(pack("<2d", s.points[0][0], s.points[0][1])) except error: raise ShapefileException("Failed to write point for record %s. Expected floats." % self.shpNum) # Write a single Z value # Note: missing z values are autoset to 0, but not sure if this is ideal. if s.shapeType == 11: # update the global z box self.__zbox(s) # then write value if hasattr(s, "z"): # if z values are stored in attribute try: if not s.z: s.z = (0,) f.write(pack("i", length)) f.seek(finish) return offset,length def __shxRecord(self, offset, length): """Writes the shx records.""" f = self.__getFileObj(self.shx) f.write(pack(">i", offset // 2)) f.write(pack(">i", length)) def record(self, *recordList, **recordDict): """Creates a dbf attribute record. You can submit either a sequence of field values or keyword arguments of field names and values. Before adding records you must add fields for the record values using the fields() method. If the record values exceed the number of fields the extra ones won't be added. In the case of using keyword arguments to specify field/value pairs only fields matching the already registered fields will be added.""" # Balance if already not balanced if self.autoBalance and self.recNum > self.shpNum: self.balance() record = [] fieldCount = len(self.fields) # Compensate for deletion flag if self.fields[0][0].startswith("Deletion"): fieldCount -= 1 if recordList: record = [recordList[i] for i in range(fieldCount)] elif recordDict: for field in self.fields: if field[0] in recordDict: val = recordDict[field[0]] if val is None: record.append("") else: record.append(val) else: # Blank fields for empty record record = ["" for i in range(fieldCount)] self.__dbfRecord(record) def __dbfRecord(self, record): """Writes the dbf records.""" f = self.__getFileObj(self.dbf) if self.recNum == 0: # first records, so all fields should be set # allowing us to write the dbf header # cannot change the fields after this point self.__dbfHeader() # begin self.recNum += 1 if not self.fields[0][0].startswith("Deletion"): f.write(b' ') # deletion flag for (fieldName, fieldType, size, deci), value in zip(self.fields, record): fieldType = fieldType.upper() size = int(size) if fieldType in ("N","F"): # numeric or float: number stored as a string, right justified, and padded with blanks to the width of the field. if value in MISSING: value = b"*"*size # QGIS NULL elif not deci: # force to int try: # first try to force directly to int. # forcing a large int to float and back to int # will lose information and result in wrong nr. value = int(value) except ValueError: # forcing directly to int failed, so was probably a float. value = int(float(value)) value = format(value, "d")[:size].rjust(size) # caps the size if exceeds the field size else: value = float(value) value = format(value, ".%sf"%deci)[:size].rjust(size) # caps the size if exceeds the field size elif fieldType == "D": # date: 8 bytes - date stored as a string in the format YYYYMMDD. if isinstance(value, date): value = '{:04d}{:02d}{:02d}'.format(value.year, value.month, value.day) elif isinstance(value, list) and len(value) == 3: value = '{:04d}{:02d}{:02d}'.format(*value) elif value in MISSING: value = b'0' * 8 # QGIS NULL for date type elif is_string(value) and len(value) == 8: pass # value is already a date string else: raise ShapefileException("Date values must be either a datetime.date object, a list, a YYYYMMDD string, or a missing value.") elif fieldType == 'L': # logical: 1 byte - initialized to 0x20 (space) otherwise T or F. if value in MISSING: value = b' ' # missing is set to space elif value in [True,1]: value = b'T' elif value in [False,0]: value = b'F' else: value = b' ' # unknown is set to space else: # anything else is forced to string, truncated to the length of the field value = b(value, self.encoding, self.encodingErrors)[:size].ljust(size) if not isinstance(value, bytes): # just in case some of the numeric format() and date strftime() results are still in unicode (Python 3 only) value = b(value, 'ascii', self.encodingErrors) # should be default ascii encoding if len(value) != size: raise ShapefileException( "Shapefile Writer unable to pack incorrect sized value" " (size %d) into field '%s' (size %d)." % (len(value), fieldName, size)) f.write(value) def balance(self): """Adds corresponding empty attributes or null geometry records depending on which type of record was created to make sure all three files are in synch.""" while self.recNum > self.shpNum: self.null() while self.recNum < self.shpNum: self.record() def null(self): """Creates a null shape.""" self.shape(Shape(NULL)) def point(self, x, y): """Creates a POINT shape.""" shapeType = POINT pointShape = Shape(shapeType) pointShape.points.append([x, y]) self.shape(pointShape) def pointm(self, x, y, m=None): """Creates a POINTM shape. If the m (measure) value is not set, it defaults to NoData.""" shapeType = POINTM pointShape = Shape(shapeType) pointShape.points.append([x, y, m]) self.shape(pointShape) def pointz(self, x, y, z=0, m=None): """Creates a POINTZ shape. If the z (elevation) value is not set, it defaults to 0. If the m (measure) value is not set, it defaults to NoData.""" shapeType = POINTZ pointShape = Shape(shapeType) pointShape.points.append([x, y, z, m]) self.shape(pointShape) def multipoint(self, points): """Creates a MULTIPOINT shape. Points is a list of xy values.""" shapeType = MULTIPOINT points = [points] # nest the points inside a list to be compatible with the generic shapeparts method self._shapeparts(parts=points, shapeType=shapeType) def multipointm(self, points): """Creates a MULTIPOINTM shape. Points is a list of xym values. If the m (measure) value is not included, it defaults to None (NoData).""" shapeType = MULTIPOINTM points = [points] # nest the points inside a list to be compatible with the generic shapeparts method self._shapeparts(parts=points, shapeType=shapeType) def multipointz(self, points): """Creates a MULTIPOINTZ shape. Points is a list of xyzm values. If the z (elevation) value is not included, it defaults to 0. If the m (measure) value is not included, it defaults to None (NoData).""" shapeType = MULTIPOINTZ points = [points] # nest the points inside a list to be compatible with the generic shapeparts method self._shapeparts(parts=points, shapeType=shapeType) def line(self, lines): """Creates a POLYLINE shape. Lines is a collection of lines, each made up of a list of xy values.""" shapeType = POLYLINE self._shapeparts(parts=lines, shapeType=shapeType) def linem(self, lines): """Creates a POLYLINEM shape. Lines is a collection of lines, each made up of a list of xym values. If the m (measure) value is not included, it defaults to None (NoData).""" shapeType = POLYLINEM self._shapeparts(parts=lines, shapeType=shapeType) def linez(self, lines): """Creates a POLYLINEZ shape. Lines is a collection of lines, each made up of a list of xyzm values. If the z (elevation) value is not included, it defaults to 0. If the m (measure) value is not included, it defaults to None (NoData).""" shapeType = POLYLINEZ self._shapeparts(parts=lines, shapeType=shapeType) def poly(self, polys): """Creates a POLYGON shape. Polys is a collection of polygons, each made up of a list of xy values. Note that for ordinary polygons the coordinates must run in a clockwise direction. If some of the polygons are holes, these must run in a counterclockwise direction.""" shapeType = POLYGON self._shapeparts(parts=polys, shapeType=shapeType) def polym(self, polys): """Creates a POLYGONM shape. Polys is a collection of polygons, each made up of a list of xym values. Note that for ordinary polygons the coordinates must run in a clockwise direction. If some of the polygons are holes, these must run in a counterclockwise direction. If the m (measure) value is not included, it defaults to None (NoData).""" shapeType = POLYGONM self._shapeparts(parts=polys, shapeType=shapeType) def polyz(self, polys): """Creates a POLYGONZ shape. Polys is a collection of polygons, each made up of a list of xyzm values. Note that for ordinary polygons the coordinates must run in a clockwise direction. If some of the polygons are holes, these must run in a counterclockwise direction. If the z (elevation) value is not included, it defaults to 0. If the m (measure) value is not included, it defaults to None (NoData).""" shapeType = POLYGONZ self._shapeparts(parts=polys, shapeType=shapeType) def multipatch(self, parts, partTypes): """Creates a MULTIPATCH shape. Parts is a collection of 3D surface patches, each made up of a list of xyzm values. PartTypes is a list of types that define each of the surface patches. The types can be any of the following module constants: TRIANGLE_STRIP, TRIANGLE_FAN, OUTER_RING, INNER_RING, FIRST_RING, or RING. If the z (elavation) value is not included, it defaults to 0. If the m (measure) value is not included, it defaults to None (NoData).""" shapeType = MULTIPATCH polyShape = Shape(shapeType) polyShape.parts = [] polyShape.points = [] for part in parts: # set part index position polyShape.parts.append(len(polyShape.points)) # add points for point in part: # Ensure point is list if not isinstance(point, list): point = list(point) polyShape.points.append(point) polyShape.partTypes = partTypes # write the shape self.shape(polyShape) def _shapeparts(self, parts, shapeType): """Internal method for adding a shape that has multiple collections of points (parts): lines, polygons, and multipoint shapes. """ polyShape = Shape(shapeType) polyShape.parts = [] polyShape.points = [] for part in parts: # set part index position polyShape.parts.append(len(polyShape.points)) # add points for point in part: # Ensure point is list if not isinstance(point, list): point = list(point) polyShape.points.append(point) # write the shape self.shape(polyShape) def field(self, name, fieldType="C", size="50", decimal=0): """Adds a dbf field descriptor to the shapefile.""" if fieldType == "D": size = "8" decimal = 0 elif fieldType == "L": size = "1" decimal = 0 if len(self.fields) >= 2046: raise ShapefileException( "Shapefile Writer reached maximum number of fields: 2046.") self.fields.append((name, fieldType, size, decimal)) ## def saveShp(self, target): ## """Save an shp file.""" ## if not hasattr(target, "write"): ## target = os.path.splitext(target)[0] + '.shp' ## self.shp = self.__getFileObj(target) ## self.__shapefileHeader(self.shp, headerType='shp') ## self.shp.seek(100) ## self._shp.seek(0) ## chunk = True ## while chunk: ## chunk = self._shp.read(self.bufsize) ## self.shp.write(chunk) ## ## def saveShx(self, target): ## """Save an shx file.""" ## if not hasattr(target, "write"): ## target = os.path.splitext(target)[0] + '.shx' ## self.shx = self.__getFileObj(target) ## self.__shapefileHeader(self.shx, headerType='shx') ## self.shx.seek(100) ## self._shx.seek(0) ## chunk = True ## while chunk: ## chunk = self._shx.read(self.bufsize) ## self.shx.write(chunk) ## ## def saveDbf(self, target): ## """Save a dbf file.""" ## if not hasattr(target, "write"): ## target = os.path.splitext(target)[0] + '.dbf' ## self.dbf = self.__getFileObj(target) ## self.__dbfHeader() # writes to .dbf ## self._dbf.seek(0) ## chunk = True ## while chunk: ## chunk = self._dbf.read(self.bufsize) ## self.dbf.write(chunk) ## def save(self, target=None, shp=None, shx=None, dbf=None): ## """Save the shapefile data to three files or ## three file-like objects. SHP and DBF files can also ## be written exclusively using saveShp, saveShx, and saveDbf respectively. ## If target is specified but not shp, shx, or dbf then the target path and ## file name are used. If no options or specified, a unique base file name ## is generated to save the files and the base file name is returned as a ## string. ## """ ## # Balance if already not balanced ## if shp and dbf: ## if self.autoBalance: ## self.balance() ## if self.recNum != self.shpNum: ## raise ShapefileException("When saving both the dbf and shp file, " ## "the number of records (%s) must correspond " ## "with the number of shapes (%s)" % (self.recNum, self.shpNum)) ## # Save ## if shp: ## self.saveShp(shp) ## if shx: ## self.saveShx(shx) ## if dbf: ## self.saveDbf(dbf) ## # Create a unique file name if one is not defined ## if not shp and not shx and not dbf: ## generated = False ## if not target: ## temp = tempfile.NamedTemporaryFile(prefix="shapefile_",dir=os.getcwd()) ## target = temp.name ## generated = True ## self.saveShp(target) ## self.shp.close() ## self.saveShx(target) ## self.shx.close() ## self.saveDbf(target) ## self.dbf.close() ## if generated: ## return target # Begin Testing def test(**kwargs): import doctest doctest.NORMALIZE_WHITESPACE = 1 verbosity = kwargs.get('verbose', 0) if verbosity == 0: print('Running doctests...') # ignore py2-3 unicode differences import re class Py23DocChecker(doctest.OutputChecker): def check_output(self, want, got, optionflags): if sys.version_info[0] == 2: got = re.sub("u'(.*?)'", "'\\1'", got) got = re.sub('u"(.*?)"', '"\\1"', got) res = doctest.OutputChecker.check_output(self, want, got, optionflags) return res def summarize(self): doctest.OutputChecker.summarize(True) # run tests runner = doctest.DocTestRunner(checker=Py23DocChecker(), verbose=verbosity) with open("README.md","rb") as fobj: test = doctest.DocTestParser().get_doctest(string=fobj.read().decode("utf8").replace('\r\n','\n'), globs={}, name="README", filename="README.md", lineno=0) failure_count, test_count = runner.run(test) # print results if verbosity: runner.summarize(True) else: if failure_count == 0: print('All test passed successfully') elif failure_count > 0: runner.summarize(verbosity) return failure_count if __name__ == "__main__": """ Doctests are contained in the file 'README.md', and are tested using the built-in testing libraries. """ failure_count = test() sys.exit(failure_count) ================================================ FILE: core/lib/shapefile123.py ================================================ """ shapefile.py Provides read and write support for ESRI Shapefiles. author: jlawheadgeospatialpython.com date: 2015/06/22 version: 1.2.3 Compatible with Python versions 2.4-3.x version changelog: Reader.iterShapeRecords() bugfix for Python 3 """ __version__ = "1.2.3" from struct import pack, unpack, calcsize, error import os import sys import time import array import tempfile import itertools # # Constants for shape types NULL = 0 POINT = 1 POLYLINE = 3 POLYGON = 5 MULTIPOINT = 8 POINTZ = 11 POLYLINEZ = 13 POLYGONZ = 15 MULTIPOINTZ = 18 POINTM = 21 POLYLINEM = 23 POLYGONM = 25 MULTIPOINTM = 28 MULTIPATCH = 31 PYTHON3 = sys.version_info[0] == 3 if PYTHON3: xrange = range izip = zip else: from itertools import izip def b(v): if PYTHON3: if isinstance(v, str): # For python 3 encode str to bytes. return v.encode('utf-8') elif isinstance(v, bytes): # Already bytes. return v else: # Error. raise Exception('Unknown input type') else: # For python 2 assume str passed in and return str. return v def u(v): if PYTHON3: # try/catch added 2014/05/07 # returned error on dbf of shapefile # from www.naturalearthdata.com named # "ne_110m_admin_0_countries". # Just returning v as is seemed to fix # the problem. This function could # be condensed further. try: if isinstance(v, bytes): # For python 3 decode bytes to str. return v.decode('utf-8') elif isinstance(v, str): # Already str. return v else: # Error. raise Exception('Unknown input type') except: return v else: # For python 2 assume str passed in and return str. return v def is_string(v): if PYTHON3: return isinstance(v, str) else: return isinstance(v, basestring) class _Array(array.array): """Converts python tuples to lits of the appropritate type. Used to unpack different shapefile header parts.""" def __repr__(self): return str(self.tolist()) def signed_area(coords): """Return the signed area enclosed by a ring using the linear time algorithm at http://www.cgafaq.info/wiki/Polygon_Area. A value >= 0 indicates a counter-clockwise oriented ring. """ xs, ys = map(list, zip(*coords)) xs.append(xs[1]) ys.append(ys[1]) return sum(xs[i]*(ys[i+1]-ys[i-1]) for i in range(1, len(coords)))/2.0 class _Shape: def __init__(self, shapeType=None): """Stores the geometry of the different shape types specified in the Shapefile spec. Shape types are usually point, polyline, or polygons. Every shape type except the "Null" type contains points at some level for example verticies in a polygon. If a shape type has multiple shapes containing points within a single geometry record then those shapes are called parts. Parts are designated by their starting index in geometry record's list of shapes.""" self.shapeType = shapeType self.points = [] @property def __geo_interface__(self): if self.shapeType in [POINT, POINTM, POINTZ]: return { 'type': 'Point', 'coordinates': tuple(self.points[0]) } elif self.shapeType in [MULTIPOINT, MULTIPOINTM, MULTIPOINTZ]: return { 'type': 'MultiPoint', 'coordinates': tuple([tuple(p) for p in self.points]) } elif self.shapeType in [POLYLINE, POLYLINEM, POLYLINEZ]: if len(self.parts) == 1: return { 'type': 'LineString', 'coordinates': tuple([tuple(p) for p in self.points]) } else: ps = None coordinates = [] for part in self.parts: if ps == None: ps = part continue else: coordinates.append(tuple([tuple(p) for p in self.points[ps:part]])) ps = part else: coordinates.append(tuple([tuple(p) for p in self.points[part:]])) return { 'type': 'MultiLineString', 'coordinates': tuple(coordinates) } elif self.shapeType in [POLYGON, POLYGONM, POLYGONZ]: if len(self.parts) == 1: return { 'type': 'Polygon', 'coordinates': (tuple([tuple(p) for p in self.points]),) } else: ps = None coordinates = [] for part in self.parts: if ps == None: ps = part continue else: coordinates.append(tuple([tuple(p) for p in self.points[ps:part]])) ps = part else: coordinates.append(tuple([tuple(p) for p in self.points[part:]])) polys = [] poly = [coordinates[0]] for coord in coordinates[1:]: if signed_area(coord) < 0: polys.append(poly) poly = [coord] else: poly.append(coord) polys.append(poly) if len(polys) == 1: return { 'type': 'Polygon', 'coordinates': tuple(polys[0]) } elif len(polys) > 1: return { 'type': 'MultiPolygon', 'coordinates': polys } class _ShapeRecord: """A shape object of any type.""" def __init__(self, shape=None, record=None): self.shape = shape self.record = record class ShapefileException(Exception): """An exception to handle shapefile specific problems.""" pass class Reader: """Reads the three files of a shapefile as a unit or separately. If one of the three files (.shp, .shx, .dbf) is missing no exception is thrown until you try to call a method that depends on that particular file. The .shx index file is used if available for efficiency but is not required to read the geometry from the .shp file. The "shapefile" argument in the constructor is the name of the file you want to open. You can instantiate a Reader without specifying a shapefile and then specify one later with the load() method. Only the shapefile headers are read upon loading. Content within each file is only accessed when required and as efficiently as possible. Shapefiles are usually not large but they can be. """ def __init__(self, *args, **kwargs): self.shp = None self.shx = None self.dbf = None self.shapeName = "Not specified" self._offsets = [] self.shpLength = None self.numRecords = None self.fields = [] self.__dbfHdrLength = 0 # See if a shapefile name was passed as an argument if len(args) > 0: if is_string(args[0]): self.load(args[0]) return if "shp" in kwargs.keys(): if hasattr(kwargs["shp"], "read"): self.shp = kwargs["shp"] if hasattr(self.shp, "seek"): self.shp.seek(0) if "shx" in kwargs.keys(): if hasattr(kwargs["shx"], "read"): self.shx = kwargs["shx"] if hasattr(self.shx, "seek"): self.shx.seek(0) if "dbf" in kwargs.keys(): if hasattr(kwargs["dbf"], "read"): self.dbf = kwargs["dbf"] if hasattr(self.dbf, "seek"): self.dbf.seek(0) if self.shp or self.dbf: self.load() else: raise ShapefileException("Shapefile Reader requires a shapefile or file-like object.") def load(self, shapefile=None): """Opens a shapefile from a filename or file-like object. Normally this method would be called by the constructor with the file object or file name as an argument.""" if shapefile: (shapeName, ext) = os.path.splitext(shapefile) self.shapeName = shapeName try: self.shp = open("%s.shp" % shapeName, "rb") except IOError: raise ShapefileException("Unable to open %s.shp" % shapeName) try: self.shx = open("%s.shx" % shapeName, "rb") except IOError: raise ShapefileException("Unable to open %s.shx" % shapeName) try: self.dbf = open("%s.dbf" % shapeName, "rb") except IOError: raise ShapefileException("Unable to open %s.dbf" % shapeName) if self.shp: self.__shpHeader() if self.dbf: self.__dbfHeader() def __getFileObj(self, f): """Checks to see if the requested shapefile file object is available. If not a ShapefileException is raised.""" if not f: raise ShapefileException("Shapefile Reader requires a shapefile or file-like object.") if self.shp and self.shpLength is None: self.load() if self.dbf and len(self.fields) == 0: self.load() return f def __restrictIndex(self, i): """Provides list-like handling of a record index with a clearer error message if the index is out of bounds.""" if self.numRecords: rmax = self.numRecords - 1 if abs(i) > rmax: raise IndexError("Shape or Record index out of range.") if i < 0: i = range(self.numRecords)[i] return i def __shpHeader(self): """Reads the header information from a .shp or .shx file.""" if not self.shp: raise ShapefileException("Shapefile Reader requires a shapefile or file-like object. (no shp file found") shp = self.shp # File length (16-bit word * 2 = bytes) shp.seek(24) self.shpLength = unpack(">i", shp.read(4))[0] * 2 # Shape type shp.seek(32) self.shapeType= unpack("2i", f.read(8)) # Determine the start of the next record next = f.tell() + (2 * recLength) shapeType = unpack(" -10e38: record.m.append(m) else: record.m.append(None) # Read a single point if shapeType in (1,11,21): record.points = [_Array('d', unpack("<2d", f.read(16)))] # Read a single Z value if shapeType == 11: record.z = unpack("i", shx.read(4))[0] * 2) - 100 numRecords = shxRecordLength // 8 # Jump to the first record. shx.seek(100) for r in range(numRecords): # Offsets are 16-bit words just like the file length self._offsets.append(unpack(">i", shx.read(4))[0] * 2) shx.seek(shx.tell() + 4) if not i == None: return self._offsets[i] def shape(self, i=0): """Returns a shape object for a shape in the the geometry record file.""" shp = self.__getFileObj(self.shp) i = self.__restrictIndex(i) offset = self.__shapeIndex(i) if not offset: # Shx index not available so iterate the full list. for j,k in enumerate(self.iterShapes()): if j == i: return k shp.seek(offset) return self.__shape() def shapes(self): """Returns all shapes in a shapefile.""" shp = self.__getFileObj(self.shp) # Found shapefiles which report incorrect # shp file length in the header. Can't trust # that so we seek to the end of the file # and figure it out. shp.seek(0,2) self.shpLength = shp.tell() shp.seek(100) shapes = [] while shp.tell() < self.shpLength: shapes.append(self.__shape()) return shapes def iterShapes(self): """Serves up shapes in a shapefile as an iterator. Useful for handling large shapefiles.""" shp = self.__getFileObj(self.shp) shp.seek(0,2) self.shpLength = shp.tell() shp.seek(100) while shp.tell() < self.shpLength: yield self.__shape() def __dbfHeaderLength(self): """Retrieves the header length of a dbf file header.""" if not self.__dbfHdrLength: if not self.dbf: raise ShapefileException("Shapefile Reader requires a shapefile or file-like object. (no dbf file found)") dbf = self.dbf (self.numRecords, self.__dbfHdrLength) = \ unpack("6i", 9994,0,0,0,0,0)) # File length (Bytes / 2 = 16-bit words) if headerType == 'shp': f.write(pack(">i", self.__shpFileLength())) elif headerType == 'shx': f.write(pack('>i', ((100 + (len(self._shapes) * 8)) // 2))) # Version, Shape type f.write(pack("<2i", 1000, self.shapeType)) # The shapefile's bounding box (lower left, upper right) if self.shapeType != 0: try: f.write(pack("<4d", *self.bbox())) except error: raise ShapefileException("Failed to write shapefile bounding box. Floats required.") else: f.write(pack("<4d", 0,0,0,0)) # Elevation z = self.zbox() # Measure m = self.mbox() try: f.write(pack("<4d", z[0], z[1], m[0], m[1])) except error: raise ShapefileException("Failed to write shapefile elevation and measure values. Floats required.") def __dbfHeader(self): """Writes the dbf header and field descriptors.""" f = self.__getFileObj(self.dbf) f.seek(0) version = 3 year, month, day = time.localtime()[:3] year -= 1900 # Remove deletion flag placeholder from fields for field in self.fields: if field[0].startswith("Deletion"): self.fields.remove(field) numRecs = len(self.records) numFields = len(self.fields) headerLength = numFields * 32 + 33 recordLength = sum([int(field[2]) for field in self.fields]) + 1 header = pack('2i", recNum, 0)) recNum += 1 start = f.tell() # Shape Type if self.shapeType != 31: s.shapeType = self.shapeType f.write(pack("i", length)) f.seek(finish) def __shxRecords(self): """Writes the shx records.""" f = self.__getFileObj(self.shx) f.seek(100) for i in range(len(self._shapes)): f.write(pack(">i", self._offsets[i] // 2)) f.write(pack(">i", self._lengths[i])) def __dbfRecords(self): """Writes the dbf records.""" f = self.__getFileObj(self.dbf) for record in self.records: if not self.fields[0][0].startswith("Deletion"): f.write(b(' ')) # deletion flag for (fieldName, fieldType, size, dec), value in zip(self.fields, record): fieldType = fieldType.upper() size = int(size) if fieldType.upper() == "N": value = str(value).rjust(size) elif fieldType == 'L': value = str(value)[0].upper() else: value = str(value)[:size].ljust(size) if len(value) != size: raise ShapefileException( "Shapefile Writer unable to pack incorrect sized value" " (size %d) into field '%s' (size %d)." % (len(value), fieldName, size)) value = b(value) f.write(value) def null(self): """Creates a null shape.""" self._shapes.append(_Shape(NULL)) def point(self, x, y, z=0, m=0): """Creates a point shape.""" pointShape = _Shape(self.shapeType) pointShape.points.append([x, y, z, m]) self._shapes.append(pointShape) def line(self, parts=[], shapeType=POLYLINE): """Creates a line shape. This method is just a convienience method which wraps 'poly()'. """ self.poly(parts, shapeType, []) def poly(self, parts=[], shapeType=POLYGON, partTypes=[]): """Creates a shape that has multiple collections of points (parts) including lines, polygons, and even multipoint shapes. If no shape type is specified it defaults to 'polygon'. If no part types are specified (which they normally won't be) then all parts default to the shape type. """ polyShape = _Shape(shapeType) polyShape.parts = [] polyShape.points = [] # Make sure polygons are closed if shapeType in (5,15,25,31): for part in parts: if part[0] != part[-1]: part.append(part[0]) for part in parts: polyShape.parts.append(len(polyShape.points)) for point in part: # Ensure point is list if not isinstance(point, list): point = list(point) # Make sure point has z and m values while len(point) < 4: point.append(0) polyShape.points.append(point) if polyShape.shapeType == 31: if not partTypes: for part in parts: partTypes.append(polyShape.shapeType) polyShape.partTypes = partTypes self._shapes.append(polyShape) def field(self, name, fieldType="C", size="50", decimal=0): """Adds a dbf field descriptor to the shapefile.""" self.fields.append((name, fieldType, size, decimal)) def record(self, *recordList, **recordDict): """Creates a dbf attribute record. You can submit either a sequence of field values or keyword arguments of field names and values. Before adding records you must add fields for the record values using the fields() method. If the record values exceed the number of fields the extra ones won't be added. In the case of using keyword arguments to specify field/value pairs only fields matching the already registered fields will be added.""" record = [] fieldCount = len(self.fields) # Compensate for deletion flag if self.fields[0][0].startswith("Deletion"): fieldCount -= 1 if recordList: [record.append(recordList[i]) for i in range(fieldCount)] elif recordDict: for field in self.fields: if field[0] in recordDict: val = recordDict[field[0]] if val is None: record.append("") else: record.append(val) if record: self.records.append(record) def shape(self, i): return self._shapes[i] def shapes(self): """Return the current list of shapes.""" return self._shapes def saveShp(self, target): """Save an shp file.""" if not hasattr(target, "write"): target = os.path.splitext(target)[0] + '.shp' if not self.shapeType: self.shapeType = self._shapes[0].shapeType self.shp = self.__getFileObj(target) self.__shapefileHeader(self.shp, headerType='shp') self.__shpRecords() def saveShx(self, target): """Save an shx file.""" if not hasattr(target, "write"): target = os.path.splitext(target)[0] + '.shx' if not self.shapeType: self.shapeType = self._shapes[0].shapeType self.shx = self.__getFileObj(target) self.__shapefileHeader(self.shx, headerType='shx') self.__shxRecords() def saveDbf(self, target): """Save a dbf file.""" if not hasattr(target, "write"): target = os.path.splitext(target)[0] + '.dbf' self.dbf = self.__getFileObj(target) self.__dbfHeader() self.__dbfRecords() def save(self, target=None, shp=None, shx=None, dbf=None): """Save the shapefile data to three files or three file-like objects. SHP and DBF files can also be written exclusively using saveShp, saveShx, and saveDbf respectively. If target is specified but not shp,shx, or dbf then the target path and file name are used. If no options or specified, a unique base file name is generated to save the files and the base file name is returned as a string. """ # Create a unique file name if one is not defined if shp: self.saveShp(shp) if shx: self.saveShx(shx) if dbf: self.saveDbf(dbf) elif not shp and not shx and not dbf: generated = False if not target: temp = tempfile.NamedTemporaryFile(prefix="shapefile_",dir=os.getcwd()) target = temp.name generated = True self.saveShp(target) self.shp.close() self.saveShx(target) self.shx.close() self.saveDbf(target) self.dbf.close() if generated: return target class Editor(Writer): def __init__(self, shapefile=None, shapeType=POINT, autoBalance=1): self.autoBalance = autoBalance if not shapefile: Writer.__init__(self, shapeType) elif is_string(shapefile): base = os.path.splitext(shapefile)[0] if os.path.isfile("%s.shp" % base): r = Reader(base) Writer.__init__(self, r.shapeType) self._shapes = r.shapes() self.fields = r.fields self.records = r.records() def select(self, expr): """Select one or more shapes (to be implemented)""" # TODO: Implement expressions to select shapes. pass def delete(self, shape=None, part=None, point=None): """Deletes the specified part of any shape by specifying a shape number, part number, or point number.""" # shape, part, point if shape and part and point: del self._shapes[shape][part][point] # shape, part elif shape and part and not point: del self._shapes[shape][part] # shape elif shape and not part and not point: del self._shapes[shape] # point elif not shape and not part and point: for s in self._shapes: if s.shapeType == 1: del self._shapes[point] else: for part in s.parts: del s[part][point] # part, point elif not shape and part and point: for s in self._shapes: del s[part][point] # part elif not shape and part and not point: for s in self._shapes: del s[part] def point(self, x=None, y=None, z=None, m=None, shape=None, part=None, point=None, addr=None): """Creates/updates a point shape. The arguments allows you to update a specific point by shape, part, point of any shape type.""" # shape, part, point if shape and part and point: try: self._shapes[shape] except IndexError: self._shapes.append([]) try: self._shapes[shape][part] except IndexError: self._shapes[shape].append([]) try: self._shapes[shape][part][point] except IndexError: self._shapes[shape][part].append([]) p = self._shapes[shape][part][point] if x: p[0] = x if y: p[1] = y if z: p[2] = z if m: p[3] = m self._shapes[shape][part][point] = p # shape, part elif shape and part and not point: try: self._shapes[shape] except IndexError: self._shapes.append([]) try: self._shapes[shape][part] except IndexError: self._shapes[shape].append([]) points = self._shapes[shape][part] for i in range(len(points)): p = points[i] if x: p[0] = x if y: p[1] = y if z: p[2] = z if m: p[3] = m self._shapes[shape][part][i] = p # shape elif shape and not part and not point: try: self._shapes[shape] except IndexError: self._shapes.append([]) # point # part if addr: shape, part, point = addr self._shapes[shape][part][point] = [x, y, z, m] else: Writer.point(self, x, y, z, m) if self.autoBalance: self.balance() def validate(self): """An optional method to try and validate the shapefile as much as possible before writing it (not implemented).""" #TODO: Implement validation method pass def balance(self): """Adds a corresponding empty attribute or null geometry record depending on which type of record was created to make sure all three files are in synch.""" if len(self.records) > len(self._shapes): self.null() elif len(self.records) < len(self._shapes): self.record() def __fieldNorm(self, fieldName): """Normalizes a dbf field name to fit within the spec and the expectations of certain ESRI software.""" if len(fieldName) > 11: fieldName = fieldName[:11] fieldName = fieldName.upper() fieldName.replace(' ', '_') # Begin Testing def test(): import doctest doctest.NORMALIZE_WHITESPACE = 1 doctest.testfile("README.txt", verbose=1) if __name__ == "__main__": """ Doctests are contained in the file 'README.txt'. This library was originally developed using Python 2.3. Python 2.4 and above have some excellent improvements in the built-in testing libraries but for now unit testing is done using what's available in 2.3. """ test() ================================================ FILE: core/maths/__init__.py ================================================ from .interpo import scale, linearInterpo ''' from .maths.kmeans1D import kmeans1d, getBreaks from . import akima from fillnodata import replace_nans ''' ================================================ FILE: core/maths/akima.py ================================================ # -*- coding: utf-8 -*- # akima.py # Copyright (c) 2007-2015, Christoph Gohlke # Copyright (c) 2007-2015, The Regents of the University of California # Produced at the Laboratory for Fluorescence Dynamics # All rights reserved. # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the copyright holders nor the names of any # contributors may be used to endorse or promote products derived # from this software without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. """Interpolation of data points in a plane based on Akima's method. Akima's interpolation method uses a continuously differentiable sub-spline built from piecewise cubic polynomials. The resultant curve passes through the given data points and will appear smooth and natural. :Author: `Christoph Gohlke `_ :Organization: Laboratory for Fluorescence Dynamics, University of California, Irvine :Version: 2015.01.29 Requirements ------------ * `CPython 2.7 or 3.4 `_ * `Numpy 1.8 `_ * `Akima.c 2015.01.29 `_ (optional speedup) * `Matplotlib 1.4 `_ (optional for plotting) Notes ----- Consider using `scipy.interpolate.Akima1DInterpolator `_. References ---------- (1) A new method of interpolation and smooth curve fitting based on local procedures. Hiroshi Akima, J. ACM, October 1970, 17(4), 589-602. Examples -------- >>> def example(): ... '''Plot interpolated Gaussian noise.''' ... x = numpy.sort(numpy.random.random(10) * 100) ... y = numpy.random.normal(0.0, 0.1, size=len(x)) ... x2 = numpy.arange(x[0], x[-1], 0.05) ... y2 = interpolate(x, y, x2) ... from matplotlib import pyplot ... pyplot.title("Akima interpolation of Gaussian noise") ... pyplot.plot(x2, y2, "b-") ... pyplot.plot(x, y, "ro") ... pyplot.show() >>> example() """ import numpy __version__ = '2015.01.29' __docformat__ = 'restructuredtext en' __all__ = 'interpolate', def interpolate(x, y, x_new, axis=-1, out=None): """Return interpolated data using Akima's method. This Python implementation is inspired by the Matlab(r) code by N. Shamsundar. It lacks certain capabilities of the C implementation such as the output array argument and interpolation along an axis of a multidimensional data array. Parameters ---------- x : array like 1D array of monotonically increasing real values. y : array like N-D array of real values. y's length along the interpolation axis must be equal to the length of x. x_new : array like New independent variables. axis : int Specifies axis of y along which to interpolate. Interpolation defaults to last axis of y. out : array Optional array to receive results. Dimension at axis must equal length of x. Examples -------- >>> interpolate([0, 1, 2], [0, 0, 1], [0.5, 1.5]) array([-0.125, 0.375]) >>> x = numpy.sort(numpy.random.random(10) * 10) >>> y = numpy.random.normal(0.0, 0.1, size=len(x)) >>> z = interpolate(x, y, x) >>> numpy.allclose(y, z) True >>> x = x[:10] >>> y = numpy.reshape(y, (10, -1)) >>> z = numpy.reshape(y, (10, -1)) >>> interpolate(x, y, x, axis=0, out=z) >>> numpy.allclose(y, z) True """ x = numpy.array(x, dtype=numpy.float64, copy=True) y = numpy.array(y, dtype=numpy.float64, copy=True) xi = numpy.array(x_new, dtype=numpy.float64, copy=True) if axis != -1 or out is not None or y.ndim != 1: raise NotImplementedError("implemented in C extension module") if x.ndim != 1 or xi.ndim != 1: raise ValueError("x-arrays must be one dimensional") n = len(x) if n < 2: raise ValueError("array too small") if n != y.shape[axis]: raise ValueError("size of x-array must match data shape") dx = numpy.diff(x) if any(dx <= 0.0): raise ValueError("x-axis not valid") if any(xi < x[0]) or any(xi > x[-1]): raise ValueError("interpolation x-axis out of bounds") m = numpy.diff(y) / dx mm = 2.0 * m[0] - m[1] mmm = 2.0 * mm - m[0] mp = 2.0 * m[n - 2] - m[n - 3] mpp = 2.0 * mp - m[n - 2] m1 = numpy.concatenate(([mmm], [mm], m, [mp], [mpp])) dm = numpy.abs(numpy.diff(m1)) f1 = dm[2:n + 2] f2 = dm[0:n] f12 = f1 + f2 ids = numpy.nonzero(f12 > 1e-9 * numpy.max(f12))[0] b = m1[1:n + 1] b[ids] = (f1[ids] * m1[ids + 1] + f2[ids] * m1[ids + 2]) / f12[ids] c = (3.0 * m - 2.0 * b[0:n - 1] - b[1:n]) / dx d = (b[0:n - 1] + b[1:n] - 2.0 * m) / dx ** 2 bins = numpy.digitize(xi, x) bins = numpy.minimum(bins, n - 1) - 1 bb = bins[0:len(xi)] wj = xi - x[bb] return ((wj * d[bb] + c[bb]) * wj + b[bb]) * wj + y[bb] ================================================ FILE: core/maths/fillnodata.py ================================================ # -*- coding:utf-8 -*- # This file is part of BlenderGIS # ***** GPL LICENSE BLOCK ***** # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # All rights reserved. # ***** GPL LICENSE BLOCK ***** ######################################## # Inpainting function # http://astrolitterbox.blogspot.fr/2012/03/healing-holes-in-arrays-in-python.html # https://github.com/gasagna/openpiv-python/blob/master/openpiv/src/lib.pyx import numpy as np DTYPEf = np.float32 #DTYPEi = np.int32 def replace_nans(array, max_iter, tolerance, kernel_size=1, method='localmean'): """ Replace NaN elements in an array using an iterative image inpainting algorithm. The algorithm is the following: 1) For each element in the input array, replace it by a weighted average of the neighbouring elements which are not NaN themselves. The weights depends of the method type. If ``method=localmean`` weight are equal to 1/( (2*kernel_size+1)**2 -1 ) 2) Several iterations are needed if there are adjacent NaN elements. If this is the case, information is "spread" from the edges of the missing regions iteratively, until the variation is below a certain threshold. Parameters ---------- array : 2d np.ndarray an array containing NaN elements that have to be replaced max_iter : int the number of iterations kernel_size : int the size of the kernel, default is 1 method : str the method used to replace invalid values. Valid options are 'localmean', 'idw'. Returns ------- filled : 2d np.ndarray a copy of the input array, where NaN elements have been replaced. """ filled = np.empty( [array.shape[0], array.shape[1]], dtype=DTYPEf) kernel = np.empty( (2*kernel_size+1, 2*kernel_size+1), dtype=DTYPEf ) # indices where array is NaN inans, jnans = np.nonzero( np.isnan(array) ) # number of NaN elements n_nans = len(inans) # arrays which contain replaced values to check for convergence replaced_new = np.zeros( n_nans, dtype=DTYPEf) replaced_old = np.zeros( n_nans, dtype=DTYPEf) # depending on kernel type, fill kernel array if method == 'localmean': # weight are equal to 1/( (2*kernel_size+1)**2 -1 ) for i in range(2*kernel_size+1): for j in range(2*kernel_size+1): kernel[i,j] = 1 #print(kernel, 'kernel') elif method == 'idw': kernel = np.array([[0, 0.5, 0.5, 0.5,0], [0.5,0.75,0.75,0.75,0.5], [0.5,0.75,1,0.75,0.5], [0.5,0.75,0.75,0.5,1], [0, 0.5, 0.5 ,0.5 ,0]]) #print(kernel, 'kernel') else: raise ValueError("method not valid. Should be one of 'localmean', 'idw'.") # fill new array with input elements for i in range(array.shape[0]): for j in range(array.shape[1]): filled[i,j] = array[i,j] # make several passes # until we reach convergence for it in range(max_iter): #print('Fill NaN iteration', it) # for each NaN element for k in range(n_nans): i = inans[k] j = jnans[k] # initialize to zero filled[i,j] = 0.0 n = 0 # loop over the kernel for I in range(2*kernel_size+1): for J in range(2*kernel_size+1): # if we are not out of the boundaries if i+I-kernel_size < array.shape[0] and i+I-kernel_size >= 0: if j+J-kernel_size < array.shape[1] and j+J-kernel_size >= 0: # if the neighbour element is not NaN itself. if filled[i+I-kernel_size, j+J-kernel_size] == filled[i+I-kernel_size, j+J-kernel_size] : # do not sum itself if I-kernel_size != 0 and J-kernel_size != 0: # convolve kernel with original array filled[i,j] = filled[i,j] + filled[i+I-kernel_size, j+J-kernel_size]*kernel[I, J] n = n + 1*kernel[I,J] # divide value by effective number of added elements if n != 0: filled[i,j] = filled[i,j] / n replaced_new[k] = filled[i,j] else: filled[i,j] = np.nan # check if mean square difference between values of replaced # elements is below a certain tolerance #print('tolerance', np.mean( (replaced_new-replaced_old)**2 )) if np.mean( (replaced_new-replaced_old)**2 ) < tolerance: break else: for l in range(n_nans): replaced_old[l] = replaced_new[l] return filled def sincinterp(image, x, y, kernel_size=3 ): """ Re-sample an image at intermediate positions between pixels. This function uses a cardinal interpolation formula which limits the loss of information in the resampling process. It uses a limited number of neighbouring pixels. The new image :math:`im^+` at fractional locations :math:`x` and :math:`y` is computed as: .. math:: im^+(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}) Parameters ---------- image : np.ndarray, dtype np.int32 the image array. x : two dimensions np.ndarray of floats an array containing fractional pixel row positions at which to interpolate the image y : two dimensions np.ndarray of floats an array containing fractional pixel column positions at which to interpolate the image kernel_size : int interpolation is performed over a ``(2*kernel_size+1)*(2*kernel_size+1)`` submatrix in the neighbourhood of each interpolation point. Returns ------- im : np.ndarray, dtype np.float64 the interpolated value of ``image`` at the points specified by ``x`` and ``y`` """ # the output array r = np.zeros( [x.shape[0], x.shape[1]], dtype=DTYPEf) # fast pi pi = 3.1419 # for each point of the output array for I in range(x.shape[0]): for J in range(x.shape[1]): #loop over all neighbouring grid points for i in range( int(x[I,J])-kernel_size, int(x[I,J])+kernel_size+1 ): for j in range( int(y[I,J])-kernel_size, int(y[I,J])+kernel_size+1 ): # check that we are in the boundaries if i >= 0 and i <= image.shape[0] and j >= 0 and j <= image.shape[1]: if (i-x[I,J]) == 0.0 and (j-y[I,J]) == 0.0: r[I,J] = r[I,J] + image[i,j] elif (i-x[I,J]) == 0.0: r[I,J] = r[I,J] + image[i,j] * np.sin( pi*(j-y[I,J]) )/( pi*(j-y[I,J]) ) elif (j-y[I,J]) == 0.0: r[I,J] = r[I,J] + image[i,j] * np.sin( pi*(i-x[I,J]) )/( pi*(i-x[I,J]) ) else: r[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])) return r ================================================ FILE: core/maths/interpo.py ================================================ #Scale/normalize function : linear stretch from lowest value to highest value ######################################### def scale(inVal, inMin, inMax, outMin, outMax): return (inVal - inMin) * (outMax - outMin) / (inMax - inMin) + outMin def linearInterpo(x1, x2, y1, y2, x): #Linear interpolation = y1 + slope * tx dx = x2 - x1 dy = y2-y1 slope = dy/dx tx = x - x1 #position from x1 (target x) return y1 + slope * tx ================================================ FILE: core/maths/kmeans1D.py ================================================ """ kmeans1D.py Author : domlysz@gmail.com Date : february 2016 License : GPL This file is part of BlenderGIS. This is a kmeans implementation optimized for 1D data. Original kmeans code : https://gist.github.com/iandanforth/5862470 1D optimizations are inspired from this talking : http://stats.stackexchange.com/questions/40454/determine-different-clusters-of-1d-data-from-database Optimizations consists to : -sort the data and initialize clusters with a quantile classification -compute distance in 1D instead of euclidean -optimize only the borders of the clusters instead of test each cluster values Clustering results are similar to Jenks natural break and ckmeans algorithms. There are Python implementations of these alg. based on javascript code from simple-statistics library : * Jenks : https://gist.github.com/llimllib/4974446 (https://gist.github.com/tmcw/4977508) * Ckmeans : https://github.com/llimllib/ckmeans (https://github.com/simple-statistics/simple-statistics/blob/master/src/ckmeans.js) But both are terribly slow because there is a lot of exponential-time looping. These algorithms makes this somewhat inevitable. In contrast, this script works in a reasonable time, but keep in mind it's not Jenks. We just use cluster's centroids (mean) as reference to distribute the values while Jenks try to minimize within-class variance, and maximizes between group variance. """ from ..utils.timing import perf_clock def kmeans1d(data, k, cutoff=False, maxIter=False): ''' Compute natural breaks of a one dimensionnal list through an optimized kmeans algorithm Inputs: * data = input list, must be sorted beforehand * k = number of expected classes * cutoff (optional) = stop algorithm when centroids shift are under this value * maxIter (optional) = stop algorithm when iteration count reach this value Output: * A list of k clusters. A cluster is represented by a tuple containing first and last index of the cluster's values. Use these index on the input data list to retreive the effectives values containing in a cluster. ''' def getClusterValues(cluster): i, j = cluster return data[i:j+1] def getClusterCentroid(cluster): values = getClusterValues(cluster) return sum(values) / len(values) n = len(data) if k >= n: raise ValueError('Too many expected classes') if k == 1: return [ [0, n-1] ] # Step 1: Create k clusters with quantile classification # quantile = number of value per clusters q = int(n // k) #with floor, last cluster will be bigger the others, with ceil it will be smaller if q == 1: raise ValueError('Too many expected classes') # define a cluster with its first and last index clusters = [ [i, i+q-1] for i in range(0, q*k, q)] # adjust the last index of the last cluster to the effective number of value clusters[-1][1] = n-1 # Get centroids before first iter centroids = [getClusterCentroid(c) for c in clusters] # Loop through the dataset until the clusters stabilize loopCounter = 0 changeOccured = True while changeOccured: loopCounter += 1 # Will be set to true if at least one border has been adjusted changeOccured = False # Step 2 : for each border... for i in range(k-1): c1 = clusters[i] #current cluster c2 = clusters[i+1] #next cluster #tag if this border has been adjusted or not adjusted = False # Test the distance between the right border of the current cluster and the neightbors centroids # Move the values if it's closer to the next cluster's centroid. # Then, test the new right border or stop if no more move is needed. while True: if c1[0] == c1[1]: # only one value remaining in the current cluster # stop executing any more move to avoid having an empty cluster break breakValue = data[c1[1]] dst1 = abs(breakValue - centroids[i]) dst2 = abs(breakValue - centroids[i+1]) if dst1 > dst2: # Adjust border : move last value of the current cluster to the next cluster c1[1] -= 1 #decrease right border index of current cluster c2[0] -= 1 #decrease left border index of the next cluster adjusted = True else: break # Test left border of next cluster only if we don't have adjusted the right border of current cluster if not adjusted: # Test the distance between the left border of the next cluster and the neightbors centroids # Move the values if it's closer to the current cluster's centroid. # Then, test the new left border or stop if no more move is needed. while True: if c2[0] == c2[1]: # only one value remaining in the next cluster # stop executing any more move to avoid having an empty cluster break breakValue = data[c2[0]] dst1 = abs(breakValue - centroids[i]) dst2 = abs(breakValue - centroids[i+1]) if dst2 > dst1: # Adjust border : move first value of the next cluster to the current cluster c2[0] += 1 #increase left border index of the next cluster c1[1] += 1 #increase right border index of current cluster adjusted = True else: break # Loop again if some borders were adjusted # or stop looping if no more move are possible if adjusted: changeOccured = True # Update centroids and compute the bigger shift newCentroids = [getClusterCentroid(c) for c in clusters] biggest_shift = max([abs(newCentroids[i] - centroids[i]) for i in range(k)]) centroids = newCentroids # Force stopping the main loop ... # > if the centroids have stopped moving much (in the case we set a cutoff value) # > or if we reach max iteration value (in the case we set a maxIter value) if (cutoff and biggest_shift < cutoff) or (maxIter and loopCounter == maxIter): break #print("Converged after %s iterations" % loopCounter) return clusters #----------------- #Helpers to get values from clusters's indices list returning by kmeans1d function def getClustersValues(data, clusters): return [data[i:j+1] for i, j in clusters] def getBreaks(data, clusters, includeBounds=False): if includeBounds: return [data[0]] + [data[j] for i, j in clusters] else: return [data[j] for i, j in clusters[:-1]] if __name__ == '__main__': import random, time #make data with a gap between 1000 and 2000 data = [random.uniform(0, 1000) for i in range(10000)] data.extend([random.uniform(2000, 4000) for i in range(10000)]) data.sort() k = 4 print('---------------') print('%i values, %i classes' %(len(data),k)) t1 = perf_clock() clusters = kmeans1d(data, k) t2 = perf_clock() print('Completed in %f seconds' %(t2-t1)) print('Breaks :') print(getBreaks(data, clusters)) print('Clusters details (nb values, min, max) :') for clusterValues in getClustersValues(data, clusters): print( len(clusterValues), clusterValues[0], clusterValues[-1] ) ================================================ FILE: core/proj/__init__.py ================================================ from .srs import SRS from .reproj import Reproj, reprojPt, reprojPts, reprojBbox, reprojImg from .srv import EPSGIO, TWCC, MapTilerCoordinates from .ellps import dd2meters, meters2dd, Ellps, GRS80 ================================================ FILE: core/proj/ellps.py ================================================ import math class Ellps(): """ellipsoid""" def __init__(self, a, b): self.a = a#equatorial radius in meters self.b = b#polar radius in meters self.f = (self.a-self.b)/self.a#inverse flat self.perimeter = (2*math.pi*self.a)#perimeter at equator GRS80 = Ellps(6378137, 6356752.314245) def dd2meters(dst): """ Basic function to approximaly convert a short distance in decimal degrees to meters Only true at equator and along horizontal axis """ k = GRS80.perimeter/360 return dst * k def meters2dd(dst): k = GRS80.perimeter/360 return dst / k ================================================ FILE: core/proj/reproj.py ================================================ # -*- coding:utf-8 -*- # ***** GPL LICENSE BLOCK ***** # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # All rights reserved. # ***** GPL LICENSE BLOCK ***** import math from .srs import SRS from .utm import UTM, UTM_EPSG_CODES from .ellps import GRS80 from .srv import MapTilerCoordinates from ..errors import ReprojError from ..utils import BBOX from ..checkdeps import HAS_GDAL, HAS_PYPROJ from .. import settings if HAS_GDAL: from osgeo import osr, gdal if HAS_PYPROJ: import pyproj ###################################### # Build in functions def webMercToLonLat(x, y): k = GRS80.perimeter/360 lon = x / k lat = y / k lat = 180 / math.pi * (2 * math.atan( math.exp( lat * math.pi / 180.0)) - math.pi / 2.0) return lon, lat def lonLatToWebMerc(lon, lat): k = GRS80.perimeter/360 x = lon * k lat = math.log( math.tan((90 + lat) * math.pi / 360.0 )) / (math.pi / 180.0) y = lat * k return x, y ###################################### # Raster reproj using GDAL def 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'}): ''' Use GDAL Python binding to reproject an image crs1, crs2 >> epsg code ds1 >> input GDAL dataset object out_ul >> [tuple] output raster top left coords (same as input if None) out_size >> |tuple], output raster size (same as input is None) out_res >> [number], output raster resolution (same as input if None) (resx = resy) sqPx >> [boolean] force square pixel resolution when resoltion is automatically computed path >> a geotiff file path to store the result into (optional) geoTiffOptions >> GDAL create option for tiff format (optional) return ds2 >> output GDAL dataset object. If path is None, the dataset will be stored in memory however into a geotiff file on disk ''' if not HAS_GDAL: raise NotImplementedError geoTrans = ds1.GetGeoTransform() if geoTrans is not None: xmin, resx, rotx, ymax, roty, resy = geoTrans #Note that instead of worldfile, topleft geotag is at corner not pixel center else: raise IOError("Reprojection fails: input raster is not georeferenced") img_w, img_h = ds1.RasterXSize, ds1.RasterYSize nbBands = ds1.RasterCount dtype = gdal.GetDataTypeName(ds1.GetRasterBand(1).DataType) if rotx == roty == 0: xmax = xmin + img_w * resx ymin = ymax + img_h * resy bbox = BBOX(xmin, ymin, xmax, ymax) else: raise IOError("Raster must be rectified (no rotation parameters)") #TODO reuse the GeoRef class to extract bbox even if there are rotation parameters #Assign input CRS to input datasource prj1 = SRS(crs1).getOgrSpatialRef() wkt1 = prj1.ExportToWkt() ds1.SetProjection(wkt1) #Build destination dataset # ds2 will be a template empty raster to reproject the data into # we can directly set its size, res and top left coord as expected # reproject funtion will match the template (clip and resampling) if out_ul is not None: xmin, ymax = out_ul else: xmin, ymax = reprojPt(crs1, crs2, xmin, ymax) #submit resolution and size if out_res is not None and out_size is not None: resx, resy = out_res, -out_res img_w, img_h = out_size #submit resolution and auto compute the best image size if out_res is not None and out_size is None: resx, resy = out_res, -out_res #reprojected image size depend on final bbox and expected resolution xmin, ymin, xmax, ymax = reprojBbox(crs1, crs2, bbox) img_w = int( (xmax - xmin) / resx ) img_h = int( (ymax - ymin) / resy ) #submit image size and ... if out_res is None and out_size is not None: img_w, img_h = out_size #...let's res as source value ? (image will be croped) #Keep original image px size and compute resolution to approximately preserve geosize if out_res is None and out_size is None: #find the res that match source diagolal size xmin, ymin, xmax, ymax = reprojBbox(crs1, crs2, bbox) ''' dst_diag = math.sqrt( (xmax - xmin)**2 + (ymax - ymin)**2) px_diag = math.sqrt(img_w**2 + img_h**2) res = dst_diag / px_diag ''' resx = (xmax-xmin) / img_w resy = -(ymax-ymin) / img_h if sqPx: resx = max(resx, abs(resy)) resy = -resx if path is None: ds2 = gdal.GetDriverByName('MEM').Create('', img_w, img_h, nbBands, gdal.GetDataTypeByName(dtype)) else: gdal.SetConfigOption('GDAL_TIFF_INTERNAL_MASK', 'YES') options = [str(k) + '=' + str(v) for k, v in geoTiffOptions.items()] ds2 = gdal.GetDriverByName('GTiff').Create(path, img_w, img_h, nbBands, gdal.GetDataTypeByName(dtype), options) if geoTiffOptions.get('COMPRESS', None) == 'JPEG': ds2.CreateMaskBand(gdal.GMF_PER_DATASET) ds2.GetRasterBand(1).GetMaskBand().Fill(255) #WARNING, it seems gdal.ReprojectImage does not honor internal mask ! geoTrans = (xmin, resx, 0, ymax, 0, resy) ds2.SetGeoTransform(geoTrans) prj2 = SRS(crs2).getOgrSpatialRef() wkt2 = prj2.ExportToWkt() ds2.SetProjection(wkt2) #Perform the projection/resampling # Resample algo if resamplAlg == 'NN' : alg = gdal.GRA_NearestNeighbour elif resamplAlg == 'BL' : alg = gdal.GRA_Bilinear elif resamplAlg == 'CB' : alg = gdal.GRA_Cubic elif resamplAlg == 'CBS' : alg = gdal.GRA_CubicSpline elif resamplAlg == 'LCZ' : alg = gdal.GRA_Lanczos # Memory limit (0 = no limit) memLimit = 0 # Error in pixels (0 will use the exact transformer) threshold = 0.25 # Warp options (http://www.gdal.org/structGDALWarpOptions.html) opt = ['NUM_THREADS=ALL_CPUS, SAMPLE_GRID=YES'] #option parameters available since gdal 2.1 a, b, c = gdal.__version__.split('.', 2) if (int(a) == 2 and int(b) >=1) or int(a) > 2: gdal.ReprojectImage(ds1, ds2, wkt1, wkt2, alg, memLimit, threshold, options=opt) else: gdal.ReprojectImage(ds1, ds2, wkt1, wkt2, alg, memLimit, threshold) #ds1 = None return ds2 class Reproj(): def __init__(self, crs1, crs2): #init CRS class try: crs1, crs2 = SRS(crs1), SRS(crs2) except Exception as e: raise ReprojError(str(e)) if crs1 == crs2: self.iproj = 'NO_REPROJ' return #Get proj engine from module settings self.iproj = settings.proj_engine if self.iproj not in ['AUTO', 'GDAL', 'PYPROJ', 'BUILTIN', 'EPSGIO']: raise ReprojError('Wrong engine name') if self.iproj == 'AUTO': # Init proj4 interface for this instance if HAS_GDAL: self.iproj = 'GDAL' elif HAS_PYPROJ: self.iproj = 'PYPROJ' elif ((crs1.isWM or crs1.isUTM) and crs2.isWGS84) or (crs1.isWGS84 and (crs2.isWM or crs2.isUTM)): self.iproj = 'BUILTIN' else: #this is the slower solution, not suitable for reproject lot of points self.iproj = 'EPSGIO' else: if (self.iproj == 'GDAL' and not HAS_GDAL) or (self.iproj == 'PYPROJ' and not HAS_PYPROJ): raise ReprojError('Missing reproj engine') if self.iproj == 'BUILTIN': if not ( ((crs1.isWM or crs1.isUTM) and crs2.isWGS84) or (crs1.isWGS84 and (crs2.isWM or crs2.isUTM)) ): raise ReprojError('Too limited built in reprojection capabilities') if self.iproj == 'GDAL': self.crs1 = crs1.getOgrSpatialRef() self.crs2 = crs2.getOgrSpatialRef() self.osrTransfo = osr.CoordinateTransformation(self.crs1, self.crs2) elif self.iproj == 'PYPROJ': self.crs1 = crs1.getPyProj() self.crs2 = crs2.getPyProj() elif self.iproj == 'EPSGIO': self.mapTilerCoords = MapTilerCoordinates() if crs1.isEPSG and crs2.isEPSG: self.crs1, self.crs2 = crs1.code, crs2.code else: raise ReprojError('EPSG.io support only EPSG code') elif self.iproj == 'BUILTIN': if ((crs1.isWM or crs1.isUTM) and crs2.isWGS84) or (crs1.isWGS84 and (crs2.isWM or crs2.isUTM)): #just store codes self.crs1, self.crs2 = crs1.code, crs2.code else: raise ReprojError('Not implemented transformation') #init UTM class if crs1.isUTM: self.utm = UTM.init_from_epsg(crs1) elif crs2.isUTM: self.utm = UTM.init_from_epsg(crs2) def pts(self, pts): if len(pts) == 0: return [] if len(pts[0]) != 2: raise ReprojError('Points must be [ (x,y) ]') if self.iproj == 'NO_REPROJ': return pts if self.iproj == 'GDAL': #Since PROJ 6, the order of coordinates for geographic crs is latitude first, longitude second. if hasattr(osr, 'GetPROJVersionMajor'): projVersion = osr.GetPROJVersionMajor() else: projVersion = 4 if projVersion >= 6 and self.crs1.IsGeographic(): pts = [ (pt[1], pt[0]) for pt in pts] if self.crs2.IsGeographic(): ys, xs, _zs = zip(*self.osrTransfo.TransformPoints(pts)) else: xs, ys, _zs = zip(*self.osrTransfo.TransformPoints(pts)) return list(zip(xs, ys)) elif self.iproj == 'PYPROJ': if self.crs1.crs.is_geographic: ys, xs = zip(*pts) else: xs, ys = zip(*pts) transformer = pyproj.Transformer.from_proj(self.crs1, self.crs2) if self.crs2.crs.is_geographic: ys, xs = transformer.transform(xs, ys) else: xs, ys = transformer.transform(xs, ys) return list(zip(xs, ys)) elif self.iproj == 'EPSGIO': return self.mapTilerCoords.reprojPts(self.crs1, self.crs2, pts) elif self.iproj == 'BUILTIN': #Web Mercator if self.crs1 == 4326 and self.crs2 == 3857: return [lonLatToWebMerc(*pt) for pt in pts] elif self.crs1 == 3857 and self.crs2 == 4326: return [webMercToLonLat(*pt) for pt in pts] #UTM if self.crs1 == 4326 and self.crs2 in UTM_EPSG_CODES: return [self.utm.lonlat_to_utm(*pt) for pt in pts] elif self.crs1 in UTM_EPSG_CODES and self.crs2 == 4326: return [self.utm.utm_to_lonlat(*pt) for pt in pts] def pt(self, x, y): if x is None or y is None: raise ReprojError('Cannot reproj None coordinates') return self.pts([(x,y)])[0] def bbox(self, bbox): '''io type = BBOX() class''' if not isinstance(bbox, BBOX): bbox = BBOX(*bbox) #list must be ordered from bottom left upper right corners = self.pts(bbox.corners) _xmin = min( pt[0] for pt in corners ) _xmax = max( pt[0] for pt in corners ) _ymin = min( pt[1] for pt in corners ) _ymax = max( pt[1] for pt in corners ) if bbox.hasZ: return BBOX(_xmin, _ymin, bbox.zmin, _xmax, _ymax, bbox.zmax) else: return BBOX(_xmin, _ymin, _xmax, _ymax) def reprojPt(crs1, crs2, x, y): """ Reproject x1,y1 coords from crs1 to crs2 crs can be an EPSG code (interger or string) or a proj4 string WARN : do not use this function in a loop because Reproj() init is slow """ rprj = Reproj(crs1, crs2) return rprj.pt(x, y) def reprojPts(crs1, crs2, pts): """ Reproject [pts] from crs1 to crs2 crs can be an EPSG code (integer or srid string) or a proj4 string pts must be [(x,y)] WARN : do not use this function in a loop because Reproj() init is slow """ rprj = Reproj(crs1, crs2) return rprj.pts(pts) def reprojBbox(crs1, crs2, bbox): rprj = Reproj(crs1, crs2) return rprj.bbox(bbox) ================================================ FILE: core/proj/srs.py ================================================ # -*- coding:utf-8 -*- # ***** GPL LICENSE BLOCK ***** # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # All rights reserved. # ***** GPL LICENSE BLOCK ***** import logging log = logging.getLogger(__name__) from .utm import UTM, UTM_EPSG_CODES from .srv import EPSGIO from ..checkdeps import HAS_GDAL, HAS_PYPROJ if HAS_GDAL: from osgeo import osr, gdal if HAS_PYPROJ: import pyproj class SRS(): ''' A simple class to handle Spatial Ref System inputs ''' @classmethod def validate(cls, crs): try: cls(crs) return True except Exception as e: log.error('Cannot initialize crs', exc_info=True) return False def __init__(self, crs): ''' Valid crs input can be : > an epsg code (integer or string) > a SRID string (AUTH:CODE) > a proj4 string ''' #force cast to string crs = str(crs) #case 1 : crs is just a code if crs.isdigit(): self.auth = 'EPSG' #assume authority is EPSG self.code = int(crs) self.proj4 = '+init=epsg:'+str(self.code) #note : 'epsg' must be lower case to be compatible with gdal osr #case 2 crs is in the form AUTH:CODE elif ':' in crs: self.auth, self.code = crs.split(':') if self.code.isdigit(): #what about non integer code ??? (IGNF:LAMB93) self.code = int(self.code) if self.auth.startswith('+init='): _, self.auth = self.auth.split('=') self.auth = self.auth.upper() self.proj4 = '+init=' + self.auth.lower() + ':' + str(self.code) else: raise ValueError('Invalid CRS : '+crs) #case 3 : crs is proj4 string elif all([param.startswith('+') for param in crs.split(' ') if param]): self.auth = None self.code = None self.proj4 = crs else: raise ValueError('Invalid CRS : '+crs) @classmethod def fromGDAL(cls, ds): if not HAS_GDAL: raise ImportError('GDAL not available') wkt = ds.GetProjection() if not wkt: #empty string raise ImportError('This raster has no projection') crs = osr.SpatialReference() crs.ImportFromWkt(wkt) return cls(crs.ExportToProj4()) @property def SRID(self): if self.isSRID: return self.auth + ':' + str(self.code) else: return None @property def hasCode(self): return self.code is not None @property def hasAuth(self): return self.auth is not None @property def isSRID(self): return self.hasAuth and self.hasCode @property def isEPSG(self): return self.auth == 'EPSG' and self.code is not None @property def isWM(self): return self.auth == 'EPSG' and self.code == 3857 @property def isWGS84(self): return self.auth == 'EPSG' and self.code == 4326 @property def isUTM(self): return self.auth == 'EPSG' and self.code in UTM_EPSG_CODES def __str__(self): '''Return the best string representation for this crs''' if self.isSRID: return self.SRID else: return self.proj4 def __eq__(self, srs2): return self.__str__() == srs2.__str__() def getOgrSpatialRef(self): '''Build gdal osr spatial ref object''' if not HAS_GDAL: raise ImportError('GDAL not available') prj = osr.SpatialReference() if self.isEPSG: r = prj.ImportFromEPSG(self.code) else: r = prj.ImportFromProj4(self.proj4) #ImportFromEPSG and ImportFromProj4 do not raise any exception #but return zero if the projection is valid if r > 0: raise ValueError('Cannot initialize osr : ' + self.proj4) return prj def getPyProj(self): '''Build pyproj object''' if not HAS_PYPROJ: raise ImportError('PYPROJ not available') if self.isSRID: return pyproj.Proj(self.SRID) else: try: return pyproj.Proj(self.proj4) except Exception as e: raise ValueError('Cannot initialize pyproj object for projection {}. Error : {}'.format(self.proj4, e)) def loadProj4(self): '''Return a Python dict of proj4 parameters''' dc = {} if self.proj4 is None: return dc for param in self.proj4.split(' '): if param.count('=') == 1: k, v = param.split('=') try: v = float(v) except ValueError: pass dc[k] = v else: pass return dc @property def isGeo(self): if self.code == 4326: return True elif HAS_GDAL: prj = self.getOgrSpatialRef() isGeo = prj.IsGeographic() return isGeo == 1 elif HAS_PYPROJ: prj = self.getPyProj() return prj.crs.is_geographic else: return None def getWKT(self): if HAS_GDAL: prj = self.getOgrSpatialRef() return prj.ExportToWkt() elif self.isEPSG: return EPSGIO.getEsriWkt(self.code) else: raise NotImplementedError ================================================ FILE: core/proj/srv.py ================================================ # -*- coding:utf-8 -*- # ***** GPL LICENSE BLOCK ***** # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # All rights reserved. # ***** GPL LICENSE BLOCK ***** import logging log = logging.getLogger(__name__) from urllib.request import Request, urlopen from urllib.error import URLError, HTTPError import json from .. import settings from ..errors import ApiKeyError USER_AGENT = settings.user_agent DEFAULT_TIMEOUT = 2 REPROJ_TIMEOUT = 60 ###################################### # MapTiler Coordinates API (formerly EPSG.io) # Migration guide: https://docs.maptiler.com/cloud/api/coordinates/ class MapTilerCoordinates(): def __init__(self, apiKey=None): if apiKey is None: if settings.maptiler_api_key: self.apiKey = settings.maptiler_api_key else: raise ApiKeyError log.error('Missing MapTilerCoordinates API key') else: self.apiKey = apiKey """Test connection to MapTiler API server""" url = "https://api.maptiler.com" try: rq = Request(url, headers={'User-Agent': USER_AGENT}) urlopen(rq, timeout=DEFAULT_TIMEOUT) except URLError as e: log.error('Cannot ping {} web service, {}'.format(url, e.reason)) raise e except HTTPError as e: log.error('Cannot ping {} web service, http error {}'.format(url, e.code)) raise e except: raise def reprojPt(self, epsg1, epsg2, x1, y1): """Reproject a single point using MapTiler Coordinates API""" url = f"https://api.maptiler.com/coordinates/transform/{x1},{y1}.json?s_srs={epsg1}&t_srs={epsg2}&key={self.apiKey}" log.debug(url) try: rq = Request(url, headers={'User-Agent': USER_AGENT}) response = urlopen(rq, timeout=REPROJ_TIMEOUT).read().decode('utf8') except (URLError, HTTPError) as err: log.error('Http request fails url:{}, code:{}, error:{}'.format(url, err.code, err.reason)) raise obj = json.loads(response)['results'][0] return (float(obj['x']), float(obj['y'])) def reprojPts(self, epsg1, epsg2, points): """Reproject multiple points using MapTiler Coordinates API""" if len(points) == 1: x, y = points[0] return [self.reprojPt(epsg1, epsg2, x, y)] urlTemplate = "https://api.maptiler.com/coordinates/transform/{{POINTS}}.json?s_srs={CRS1}&t_srs={CRS2}&key={KEY}".format( CRS1=epsg1, CRS2=epsg2, KEY=self.apiKey ) precision = 4 data = [','.join([str(round(v, precision)) for v in p]) for p in points] # MapTiler API supports up to 50 points per request in batch mode batch_size = 50 batches = [data[i:i + batch_size] for i in range(0, len(data), batch_size)] result = [] for batch in batches: part = ';'.join(batch) url = urlTemplate.replace("{POINTS}", part) log.debug(url) try: rq = Request(url, headers={'User-Agent': USER_AGENT}) response = urlopen(rq, timeout=REPROJ_TIMEOUT).read().decode('utf8') except (URLError, HTTPError) as err: log.error('Http request fails url:{}, code:{}, error:{}'.format(url, err.code, err.reason)) raise obj = json.loads(response)['results'] result.extend([(float(p['x']), float(p['y'])) for p in obj]) return result def search(self, query): """Search coordinate systems using MapTiler Coordinates API""" query = str(query).replace(' ', '+') # New endpoint with API key url = f"https://api.maptiler.com/coordinates/search/{query}.json?exports=true&key={self.apiKey}" log.debug('Search crs : {}'.format(url)) rq = Request(url, headers={'User-Agent': USER_AGENT}) response = urlopen(rq, timeout=DEFAULT_TIMEOUT).read().decode('utf8') obj = json.loads(response) log.debug('Search results : {}'.format([(r['id']['code'], r['name']) for r in obj['results']])) return obj['results'] def getEsriWkt(self, epsg): """Get ESRI WKT for a specific EPSG code using MapTiler Coordinates API""" obj = self.search(epsg) try: return obj[0]['exports']['wkt'] except: log.error('Could not find ESRI WKT for EPSG:{}'.format(epsg)) return None # For backward compatibility, you can keep the EPSGIO class as an alias to MapTilerCoordinates class EPSGIO(MapTilerCoordinates): pass ###################################### # World Coordinate Converter # https://github.com/ClemRz/TWCC class TWCC(): @staticmethod def reprojPt(epsg1, epsg2, x1, y1): url = f"http://twcc.fr/en/ws/?fmt=json&x={x1}&y={y1}&in=EPSG:{epsg1}&out=EPSG:{epsg2}" rq = Request(url, headers={'User-Agent': USER_AGENT}) response = urlopen(rq, timeout=REPROJ_TIMEOUT).read().decode('utf8') obj = json.loads(response) return (float(obj['point']['x']), float(obj['point']['y'])) ###################################### # http://spatialreference.org/ref/epsg/2154/esriwkt/ # class SpatialRefOrg(): ###################################### # http://prj2epsg.org/search ================================================ FILE: core/proj/utm.py ================================================ #Original code from https://github.com/Turbo87/utm #>simplified version that only handle utm zones (and not latitude bands from MGRS grid) #>reverse coord order : latlon --> lonlat #>add support for UTM EPSG codes # more infos : http://geokov.com/education/utm.aspx # formulas : https://en.wikipedia.org/wiki/Universal_Transverse_Mercator_coordinate_system import math K0 = 0.9996 E = 0.00669438 E2 = E * E E3 = E2 * E E_P2 = E / (1.0 - E) SQRT_E = math.sqrt(1 - E) _E = (1 - SQRT_E) / (1 + SQRT_E) _E2 = _E * _E _E3 = _E2 * _E _E4 = _E3 * _E _E5 = _E4 * _E M1 = (1 - E / 4 - 3 * E2 / 64 - 5 * E3 / 256) M2 = (3 * E / 8 + 3 * E2 / 32 + 45 * E3 / 1024) M3 = (15 * E2 / 256 + 45 * E3 / 1024) M4 = (35 * E3 / 3072) P2 = (3. / 2 * _E - 27. / 32 * _E3 + 269. / 512 * _E5) P3 = (21. / 16 * _E2 - 55. / 32 * _E4) P4 = (151. / 96 * _E3 - 417. / 128 * _E5) P5 = (1097. / 512 * _E4) R = 6378137 class OutOfRangeError(ValueError): pass def longitude_to_zone_number(longitude): return int((longitude + 180) / 6) + 1 def latitude_to_northern(latitude): return latitude >= 0 def lonlat_to_zone_northern(lon, lat): zone = longitude_to_zone_number(lon) north = latitude_to_northern(lat) return zone, north def zone_number_to_central_longitude(zone_number): return (zone_number - 1) * 6 - 180 + 3 # Each UTM zone on WGS84 datum has a dedicated EPSG code : 326xx for north hemisphere and 327xx for south # where xx is the zone number from 1 to 60 #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)] UTM_EPSG_CODES = [32600 + i for i in range(1,61)] + [32700 + i for i in range(1,61)] def _code_from_epsg(epsg): '''Return & validate EPSG code str from user input''' epsg = str(epsg) if epsg.isdigit(): code = epsg elif ':' in epsg: auth, code = epsg.split(':') else: raise ValueError('Invalid UTM EPSG code') if code in map(str, UTM_EPSG_CODES): return code else: raise ValueError('Invalid UTM EPSG code') def epsg_to_zone_northern(epsg): code = _code_from_epsg(epsg) zone = int(code[-2:]) if code[2] == '6': northern = True else: northern = False return zone, northern def lonlat_to_epsg(longitude, latitude): zone = longitude_to_zone_number(longitude) if latitude_to_northern(latitude): return 'EPSG:326' + str(zone).zfill(2) else: return 'EPSG:327' + str(zone).zfill(2) def zone_northern_to_epsg(zone, northern): if northern: return 'EPSG:326' + str(zone).zfill(2) else: return 'EPSG:327' + str(zone).zfill(2) ###### class UTM(): def __init__(self, zone, north): ''' zone : UTM zone number north : True if north hemesphere, False if south ''' if not 1 <= zone <= 60: raise OutOfRangeError('zone number out of range (must be between 1 and 60)') self.zone_number = zone self.northern = north @classmethod def init_from_epsg(cls, epsg): zone, north = epsg_to_zone_northern(epsg) return cls(zone, north) @classmethod def init_from_lonlat(cls, lon, lat): zone, north = lonlat_to_zone_northern(lon, lat) return cls(zone, north) def utm_to_lonlat(self, easting, northing): if not 100000 <= easting < 1000000: raise OutOfRangeError('easting out of range (must be between 100.000 m and 999.999 m)') if not 0 <= northing <= 10000000: raise OutOfRangeError('northing out of range (must be between 0 m and 10.000.000 m)') x = easting - 500000 y = northing if not self.northern: y -= 10000000 m = y / K0 mu = m / (R * M1) p_rad = (mu + P2 * math.sin(2 * mu) + P3 * math.sin(4 * mu) + P4 * math.sin(6 * mu) + P5 * math.sin(8 * mu)) p_sin = math.sin(p_rad) p_sin2 = p_sin * p_sin p_cos = math.cos(p_rad) p_tan = p_sin / p_cos p_tan2 = p_tan * p_tan p_tan4 = p_tan2 * p_tan2 ep_sin = 1 - E * p_sin2 ep_sin_sqrt = math.sqrt(1 - E * p_sin2) n = R / ep_sin_sqrt r = (1 - E) / ep_sin c = _E * p_cos**2 c2 = c * c d = x / (n * K0) d2 = d * d d3 = d2 * d d4 = d3 * d d5 = d4 * d d6 = d5 * d latitude = (p_rad - (p_tan / r) * (d2 / 2 - d4 / 24 * (5 + 3 * p_tan2 + 10 * c - 4 * c2 - 9 * E_P2)) + d6 / 720 * (61 + 90 * p_tan2 + 298 * c + 45 * p_tan4 - 252 * E_P2 - 3 * c2)) longitude = (d - d3 / 6 * (1 + 2 * p_tan2 + c) + d5 / 120 * (5 - 2 * c + 28 * p_tan2 - 3 * c2 + 8 * E_P2 + 24 * p_tan4)) / p_cos return (math.degrees(longitude) + zone_number_to_central_longitude(self.zone_number), math.degrees(latitude)) def lonlat_to_utm(self, longitude, latitude): if not -80.0 <= latitude <= 84.0: raise OutOfRangeError('latitude out of range (must be between 80 deg S and 84 deg N)') if not -180.0 <= longitude <= 180.0: raise OutOfRangeError('longitude out of range (must be between 180 deg W and 180 deg E)') lat_rad = math.radians(latitude) lat_sin = math.sin(lat_rad) lat_cos = math.cos(lat_rad) lat_tan = lat_sin / lat_cos lat_tan2 = lat_tan * lat_tan lat_tan4 = lat_tan2 * lat_tan2 lon_rad = math.radians(longitude) central_lon = zone_number_to_central_longitude(self.zone_number) central_lon_rad = math.radians(central_lon) n = R / math.sqrt(1 - E * lat_sin**2) c = E_P2 * lat_cos**2 a = lat_cos * (lon_rad - central_lon_rad) a2 = a * a a3 = a2 * a a4 = a3 * a a5 = a4 * a a6 = a5 * a m = R * (M1 * lat_rad - M2 * math.sin(2 * lat_rad) + M3 * math.sin(4 * lat_rad) - M4 * math.sin(6 * lat_rad)) easting = K0 * n * (a + a3 / 6 * (1 - lat_tan2 + c) + a5 / 120 * (5 - 18 * lat_tan2 + lat_tan4 + 72 * c - 58 * E_P2)) + 500000 northing = K0 * (m + n * lat_tan * (a2 / 2 + a4 / 24 * (5 - lat_tan2 + 9 * c + 4 * c**2) + a6 / 720 * (61 - 58 * lat_tan2 + lat_tan4 + 600 * c - 330 * E_P2))) if not self.northern: northing += 10000000 return easting, northing ================================================ FILE: core/settings.json ================================================ { "proj_engine": "AUTO", "img_engine": "AUTO", "user_agent": "Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:45.0) Gecko/20100101 Firefox/45.0" } ================================================ FILE: core/settings.py ================================================ # -*- coding:utf-8 -*- import os import json from .checkdeps import HAS_GDAL, HAS_PYPROJ, HAS_IMGIO, HAS_PIL def getAvailableProjEngines(): engines = ['AUTO', 'BUILTIN'] #if EPSGIO.ping(): engines.append('EPSGIO') if HAS_GDAL: engines.append('GDAL') if HAS_PYPROJ: engines.append('PYPROJ') return engines def getAvailableImgEngines(): engines = ['AUTO'] if HAS_GDAL: engines.append('GDAL') if HAS_IMGIO: engines.append('IMGIO') if HAS_PIL: engines.append('PIL') return engines class Settings(): def __init__(self, **kwargs): self._proj_engine = kwargs['proj_engine'] self._img_engine = kwargs['img_engine'] self.user_agent = kwargs['user_agent'] if 'maptiler_api_key' in kwargs: self.maptiler_api_key = kwargs['maptiler_api_key'] else: self.maptiler_api_key = None @property def proj_engine(self): return self._proj_engine @proj_engine.setter def proj_engine(self, engine): if engine not in getAvailableProjEngines(): raise IOError else: self._proj_engine = engine @property def img_engine(self): return self._img_engine @img_engine.setter def img_engine(self, engine): if engine not in getAvailableImgEngines(): raise IOError else: self._img_engine = engine cfgFile = os.path.join(os.path.dirname(__file__), "settings.json") with open(cfgFile, 'r') as cfg: prefs = json.load(cfg) settings = Settings(**prefs) ================================================ FILE: core/utils/__init__.py ================================================ from .xy import XY from .bbox import BBOX from .gradient import Color, Stop, Gradient from .timing import perf_clock ================================================ FILE: core/utils/bbox.py ================================================ # -*- coding:utf-8 -*- # This file is part of BlenderGIS # ***** GPL LICENSE BLOCK ***** # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # All rights reserved. # ***** GPL LICENSE BLOCK ***** from . import XY import logging log = logging.getLogger(__name__) class BBOX(dict): '''A class to represent a bounding box''' def __init__(self, *args, **kwargs): ''' Three ways for init a BBOX class: - from a list of values ordered from bottom left to upper right >> BBOX(xmin, ymin, xmax, ymax) or BBOX(xmin, ymin, zmin, xmax, ymax, zmax) - from a tuple contained a list of values ordered from bottom left to upper right >> BBOX( (xmin, ymin, xmax, ymax) ) or BBOX( (xmin, ymin, zmin, xmax, ymax, zmax) ) - from keyword arguments with no particular order >> BBOX(xmin=, ymin=, xmax=, ymax=) or BBOX(xmin=, ymin=, zmin=, xmax=, ymax=, zmax=) ''' if args: if len(args) == 1: #maybee we pass directly a tuple args = args[0] if len(args) == 4: self.xmin, self.ymin, self.xmax, self.ymax = args elif len(args) == 6: self.xmin, self.ymin, self.zmin, self.xmax, self.ymax, self.zmax = args else: raise ValueError('BBOX() initialization expects 4 or 6 arguments, got %g' % len(args)) elif kwargs: if not all( [kw in kwargs for kw in ['xmin', 'ymin', 'xmax', 'ymax']] ): raise ValueError('invalid keyword arguments') self.xmin, self.xmax = kwargs['xmin'], kwargs['xmax'] self.ymin, self.ymax = kwargs['ymin'], kwargs['ymax'] if 'zmin' in kwargs and 'zmax' in kwargs: self.zmin, self.zmax = kwargs['zmin'], kwargs['zmax'] def __str__(self): if self.hasZ: return 'xmin:%g, ymin:%g, zmin:%g, xmax:%g, ymax:%g, zmax:%g' % tuple(self) else: return 'xmin:%g, ymin:%g, xmax:%g, ymax:%g' % tuple(self) def __getitem__(self, attr): '''access attributes like a dictionnary''' return getattr(self, attr) def __setitem__(self, key, value): '''set attributes like a dictionnary''' setattr(self, key, value) def __iter__(self): '''iterate overs values in bottom left to upper right order allows support of unpacking and conversion to tuple or list''' if self.hasZ: return iter([self.xmin, self.ymin, self.zmin, self.xmax, self.ymax, self.ymax]) else: return iter([self.xmin, self.ymin, self.xmax, self.ymax]) def keys(self): '''override dict keys() method''' return self.__dict__.keys() def items(self): '''override dict keys() method''' return self.__dict__.items() def values(self): '''override dict keys() method''' return self.__dict__.values() @classmethod def fromXYZ(cls, lst): '''Create a BBOX from a flat list of values ordered following XYZ axis --> (xmin, xmax, ymin, ymax) or (xmin, xmax, ymin, ymax, zmin, zmax)''' if len(lst) == 4: xmin, xmax, ymin, ymax = lst return cls(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax) elif len(lst) == 6: xmin, xmax, ymin, ymax, zmin, zmax = lst return cls(xmin=xmin, ymin=ymin, zmin=zmin, xmax=xmax, ymax=ymax, zmax=zmax) def toXYZ(self): '''Export to simple tuple of values ordered following XYZ axis''' if self.hasZ: return (self.xmin, self.xmax, self.ymin, self.ymax, self.zmin, self.zmax) else: return (self.xmin, self.xmax, self.ymin, self.ymax) @classmethod def fromLatlon(cls, lst): '''Create a 2D BBOX from a list of values ordered as latlon format (latmin, lonmin, latmax, lonmax) <--> (min, xmin, ymax, xmax)''' ymin, xmin, ymax, xmax = lst return cls(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax) def toLatlon(self): '''Export to simple tuple of values ordered as latlon format in 2D''' return (self.ymin, self.xmin, self.ymax, self.xmax) @property def hasZ(self): '''Check if this bbox is in 3D''' if hasattr(self, 'zmin') and hasattr(self, 'zmax'): return True else: return False def to2D(self): '''Cast 3d bbox to 2d >> discard zmin and zmax values''' return BBOX(self.xmin, self.ymin, self.xmax, self.ymax) def toGeo(self, geoscn): '''Convert the BBOX into Spatial Ref System space defined in Scene''' if geoscn.isBroken or not geoscn.isGeoref: log.warning('Cannot convert bbox, invalid georef') return None xmax = geoscn.crsx + (self.xmax * geoscn.scale) ymax = geoscn.crsy + (self.ymax * geoscn.scale) xmin = geoscn.crsx + (self.xmin * geoscn.scale) ymin = geoscn.crsy + (self.ymin * geoscn.scale) if self.hasZ: return BBOX(xmin, ymin, self.zmin, xmax, ymax, self.zmax) else: return BBOX(xmin, ymin, xmax, ymax) def __eq__(self, bb): '''Test if 2 bbox are equals''' if self.xmin == bb.xmin and self.xmax == bb.xmax and self.ymin == bb.ymin and self.ymax == bb.ymax: if self.hasZ and bb.hasZ: if self.zmin == bb.zmin and self.zmax == bb.zmax: return True else: return True def overlap(self, bb): '''Test if 2 bbox objects have intersection areas (in 2D only)''' def test_overlap(a_min, a_max, b_min, b_max): return not ((a_min > b_max) or (b_min > a_max)) return test_overlap(self.xmin, self.xmax, bb.xmin, bb.xmax) and test_overlap(self.ymin, self.ymax, bb.ymin, bb.ymax) def isWithin(self, bb): '''Test if this bbox is within another bbox''' if bb.xmin <= self.xmin and bb.xmax >= self.xmax and bb.ymin <= self.ymin and bb.ymax >= self.ymax: return True else: return False def contains(self, bb): '''Test if this bbox contains another bbox''' if bb.xmin > self.xmin and bb.xmax < self.xmax and bb.ymin > self.ymin and bb.ymax < self.ymax: return True else: return False def __add__(self, bb): '''Use '+' operator to perform the union of 2 bbox''' xmax = max(self.xmax, bb.xmax) xmin = min(self.xmin, bb.xmin) ymax = max(self.ymax, bb.ymax) ymin = min(self.ymin, bb.ymin) if self.hasZ and bb.hasZ: zmax = max(self.zmax, bb.zmax) zmin = min(self.zmin, bb.zmin) return BBOX(xmin, ymin, zmin, xmax, ymax, zmax) else: return BBOX(xmin, ymin, xmax, ymax) def shift(self, dx, dy): '''translate the bbox in 2D''' self.xmin += dx self.xmax += dx self.ymin += dy self.ymax += dy @property def center(self): x = (self.xmin + self.xmax) / 2 y = (self.ymin + self.ymax) / 2 if self.hasZ: z = (self.zmin + self.zmax) / 2 return XY(x,y,z) else: return XY(x,y) @property def dimensions(self): dx = self.xmax - self.xmin dy = self.ymax - self.ymin if self.hasZ: dz = self.zmax - self.zmin return XY(dx,dy,dz) else: return XY(dx,dy) ################ ## 2D properties @property def corners(self): '''Get the list of corners coords, starting from upperleft and ordered clockwise''' return [ self.ul, self.ur, self.br, self.bl ] @property def ul(self): '''upper left corner''' return XY(self.xmin, self.ymax) @property def ur(self): '''upper right corner''' return XY(self.xmax, self.ymax) @property def bl(self): '''bottom left corner''' return XY(self.xmin, self.ymin) @property def br(self): '''bottom right corner''' return XY(self.xmax, self.ymin) ================================================ FILE: core/utils/gradient.py ================================================ # -*- coding:utf-8 -*- import logging log = logging.getLogger(__name__) import os import colorsys from xml.dom.minidom import parse, parseString from xml.etree import ElementTree as etree from ..maths.interpo import scale, linearInterpo from ..maths import akima class Color(object): def __init__(self, values=None, space='RGBA'): #color data is stored as rgba vector (values range from 0 to 1) self.data = None # if type(values) == dict: #find correct space if all(key in 'RGBA' for key in values.keys()): space = 'RGBA' elif all(key in 'rgba' for key in values.keys()): space = 'rgba' elif all(key in 'HSVA' for key in values.keys()): space = 'HSVA' elif all(key in 'hsva' for key in values.keys()): space = 'hsva' else: space = None # if values is not None and space is not None: if type(values) not in (tuple, list, dict) or space not in ('RGB', 'RGBA', 'rgb', 'rgba', 'HSV', 'HSVA', 'hsv', 'hsva'): raise ValueError("Wrong parameters") # if space in ['RGB', 'RGBA']: if type(values) == dict: self.from_RGB(**values) elif type(values) in [tuple, list]: self.from_RGB(*values) elif space in ['rgb', 'rgba']: if type(values) == dict: self.from_rgb(**values) elif type(values) in [tuple, list]: self.from_rgb(*values) # if space in ['HSV', 'HSVA']: if type(values) == dict: self.from_HSV(**values) elif type(values) in [tuple, list]: self.from_HSV(*values) elif space in ['hsv', 'hsva']: if type(values) == dict: self.from_hsv(**values) elif type(values) in [tuple, list]: self.from_hsv(*values) def __str__(self): if self.data is not None: strRGB = 'RGB ' + str(self.RGB) strHSV = 'HSV ' + str(self.HSV) strAlpha = 'Alpha ' + str(self.alpha) return strRGB + ' - ' + strHSV + ' - ' + strAlpha else: return "No color defined" def __eq__(self, other): return self.data == other.data #All properties will be computed from rgba vector data @property def alpha(self): if self.data is not None: return self.rgba[-1]#range from 0 to 1 else: return None @property def hex(self): if self.data is not None: return "#"+"".join(["0{0:x}".format(v) if v < 16 else "{0:x}".format(v) for v in self.RGB]) else: return None ## props with alpha @property def RGBA(self): #values range from 0 to 255 if self.data is not None: return tuple([int(v*255) for v in self.rgba]) else: return None @property def rgba(self): #values range from 0 to 1 if self.data is not None: return tuple(self.data) else: return None @property def HSVA(self): #H ranges from 0° to 360°. Other values range from 0 to 100% if self.data is not None: h, s, v, a = self.hsva return tuple([h*360, s*100, v*100, a*100]) else: return None @property def hsva(self): #values range from 0 to 1 if self.data is not None: return self.hsv + tuple([self.alpha]) else: return None ## props without alpha @property def RGB(self): if self.data is not None: return tuple(self.RGBA[:-1]) else: return None @property def rgb(self): if self.data is not None: return tuple(self.rgba[:-1]) else: return None @property def HSV(self): if self.data is not None: h, s, v = self.hsv return tuple([h*360, s*100, v*100]) else: return None @property def hsv(self): if self.data is not None: return colorsys.rgb_to_hsv(*self.rgb) else: return None #another way to get color value (dictionary output possible) def getColor(self, space='RGB', asDict=False): if space == 'RGB': if asDict: return {key:self.RGB[i] for i, key in enumerate(space)} else: return self.RGB elif space == 'RGBA': if asDict: return {key:self.RGBA[i] for i, key in enumerate(space)} else: return self.RGBA elif space == 'rgba': if asDict: return {key:self.rgba[i] for i, key in enumerate(space)} else: return self.rgba elif space == 'rgb': if asDict: return {key:self.rgb[i] for i, key in enumerate(space)} else: return self.rgb if space == 'HSV': if asDict: return {key:self.HSV[i] for i, key in enumerate(space)} else: return self.HSV elif space == 'HSVA': if asDict: return {key:self.HSVA[i] for i, key in enumerate(space)} else: return self.HSVA elif space == 'hsva': if asDict: return {key:self.hsva[i] for i, key in enumerate(space)} else: return self.hsva elif space == 'hsv': if asDict: return {key:self.hsv[i] for i, key in enumerate(space)} else: return self.hsv #You can create Color object in many ways: # Color.from_rgb(0, 1, 1, 1) - passing arguments # Color.from_rgb(r=0, g=1, b=1, a=1) - passing keywords arguments # Color.from_rgb(*[0, 1, 1, 1]) - unpacking a list or a tuple (or a generic iterable) # Color.from_rgb(**{'r':0, 'g':1, 'b':1, 'a':1}) - unpacking a dictionary def from_RGB(self, R, G, B, A=255): if all(0<=v<=255 for v in (R, G, B, A)): self.data = [ v/255 for v in (R, G, B, A) ] else: raise ValueError("RGB values must range from 0 to 255") def from_rgb(self, r, g, b, a=1): if all(0<=v<=1 for v in (r, g, b, a)): self.data = [r, g, b, a] else: raise ValueError("rgb values must range from 0 to 1") def from_HSV(self, H, S, V, A=1): if 0<=H<=360 and 0<=S<=100 and 0<=V<=100: self.data = list(colorsys.hsv_to_rgb(H/360, S/100, V/100)) self.data.append(A) else: raise ValueError("Hue must range from 0 to 360°. S and V must range from 0 to 100%") def from_hsv(self, h, s, v, a=1): if all(0<=v<=1 for v in (h, s, v, a)): self.data = list(colorsys.hsv_to_rgb(h, s, v)) self.data.append(a) else: raise ValueError("hsv values must range from 0 to 1") def from_hex(self, hex): R,G,B = [int(hex[i:i+2], 16) for i in range(1,6,2)] self.data = [ v/255 for v in (R, G, B, 255) ] class Stop(): def __init__(self, position, color): self.position = position self.color = color def __lt__(self, other): return self.position < other.position class Gradient(): def __init__(self, svg=False, permissive=False): self.stops = [] #permissive rules allows duplicate position and same color for two followings stops #this option is useful when define discrete ramp self.permissive = permissive if svg: self.__readSVG(svg) def __str__(self): return str(self.asList()) def __readSVG(self, svg): try: domData = parse(svg) except Exception as e: log.error("Cannot parse svg file : {}".format(e)) return False linearGradients = domData.getElementsByTagName('linearGradient') nbGradients = len(linearGradients) if nbGradients == 0: log.error("No gradient in this SVG") return False elif nbGradients > 1: log.error('Only the first gradient will be imported') linearGradient = linearGradients[0] stops = linearGradient.getElementsByTagName('stop') if len(stops) <= 1: log.error('No enough stops') return False #begin import for stop in stops: positionStr = stop.getAttribute('offset') # "33.5%" position = float(positionStr[:-1])/100 colorStr = stop.getAttribute('stop-color') # "rgb(51, 188, 207)" alpha = float(stop.getAttribute('stop-opacity')) #value between 0-1 if ', ' in colorStr: rgba = colorStr[4:-1].split(', ') else: rgba = colorStr[4:-1].split(',') rgba = [int(c)/255 for c in rgba] rgba.append(alpha) color = Color() color.from_rgb(*rgba) self.addStop(position, color, reorder=False) #finish self.sortStops() domData.unlink() return True @property def positions(self): return [stop.position for stop in self.stops] @property def colors(self): return [stop.color for stop in self.stops] def asList(self, space='RGBA'): return [(round(stop.position,2), stop.color.getColor(space)) for stop in self.stops] def asDict(self, space='RGBA'): return {round(stop.position,2):stop.color.getColor(space, asDict=True) for stop in self.stops} def addStop(self, position, color, reorder=True): if not self.permissive: #permissive option allows discrete color ramp definition #avoid same color in two following stops if len(self.colors) >= 1: if color == self.colors[-1]: return False #avoid duplicate position if position in self.positions: return False #check if position is between 0-1 if not 0<=position<=1: return False #check color if type(color) != Color: return False stop = Stop(position, color) self.stops.append(stop) if reorder: self.sortStops() return True def addStops(self, positions, colors): if len(positions) != len(colors): return False for i, pos in enumerate(positions): self.addStop(pos, colors[i], reorder=False) self.sortStops() return True def sortStops(self): self.stops.sort() #sort(key=attrgetter('position')) def rmColor(self, color): if type(color) != Color: return False try: idx = self.colors.index(color) except ValueError as e : log.error('Cannot remove color from this gradient : {}'.format(e)) return False else: self.stops.pop(idx) return True def rmPosition(self, pos): try: idx = self.positions.index(pos) except ValueError as e: log.error('Cannot remove position from this gradient : {}'.format(e)) return False else: self.stops.pop(idx) return True def rescale(self, toMin, toMax): fromMin = min(self.positions) fromMax = max(self.positions) for stop in self.stops: stop.position = scale(stop.position, fromMin, fromMax, toMin, toMax) def evaluate(self, pos, colorSpace = 'RGB', method='LINEAR'): #check interpo method if method not in ['DISCRETE', 'NEAREST', 'LINEAR', 'SPLINE']: method = 'LINEAR' #check color space if colorSpace in ['RGB', 'RGBA', 'rgb', 'rgba']: colorSpace = 'rgba' #we will work with normalized values elif colorSpace in ['HSV', 'HSVA', 'hsv', 'hsva']: colorSpace = 'hsva' else: colorSpace = 'rgba' #default #check position self.sortStops() positions = self.positions #if pos already exist return it's color if pos in positions: idx = positions.index(pos) return self.stops[idx].color #if pos is first or last stop, return corresponding color (no extrapolation) if pos < positions[0]: return self.stops[0].color elif pos > positions[-1]: return self.stops[-1].color #find previous and next stops for i, p in enumerate(positions): if p 0.5: # hue values with delta > 180° # Hue is cyclic # > interpolation must be done through the shortest path (clockwise or counterclockwise) # > to interpolate CCW, add 180° to all hue values, then compute modulo 360° on interpolate result y1, y2 = [hue+0.5 if hue<0 else hue-0.5 for hue in (y1, y2)] y = linearInterpo(x1, x2, y1, y2, pos) % 1 else: y = linearInterpo(x1, x2, y1, y2, pos) interpolateValues.append(round(y,2)) return Color(interpolateValues, colorSpace) elif method == 'SPLINE': xData = self.positions if len(xData) < 3: #spline interpo needs at least 3 pts, otherwise compute a linear interpolation return self.evaluate(pos, colorSpace, method='LINEAR') interpolateValues = [] for i in range(4): #4 channels (rgba or hsva) yData = [color.getColor(colorSpace)[i] for color in self.colors] dy = (nextStop.color.getColor(colorSpace)[i] - prevStop.color.getColor(colorSpace)[i]) if colorSpace == 'hsva' and i == 0 and abs(dy) > 0.5: # hue values with delta > 180° # Hue is cyclic # > interpolation must be done through the shortest path (clockwise or counterclockwise) # > to interpolate CCW, add 180° to all hue values, then compute modulo 360° on interpolate result yData = [hue+0.5 if hue<0 else hue-0.5 for hue in yData] y = akima.interpolate(xData, yData, [pos])[0] % 1 else: y = akima.interpolate(xData, yData, [pos])[0] #Constrain result between 0-1 y = 1 if y>1 else 0 if y<0 else y #append interpolateValues.append(round(y,2)) return Color(interpolateValues, colorSpace) def getRangeColor(self, n, interpoSpace='RGB', interpoMethod='LINEAR'): '''return a new gradient''' ramp = Gradient(permissive=True)#permissive needed because discrete interpo can return same color for 2 or more following stops offset = 1/(n-1) position = 0 for i in range(n): color = self.evaluate(position, interpoSpace, interpoMethod) ramp.addStop(position, color, reorder=False) position += offset return ramp def exportSVG(self, svgPath, discrete=False): name = os.path.splitext(os.path.basename(svgPath))[0] name = name.replace(" ", "_") # create an SVG XML element (see the SVG specification for attribute details) svg = etree.Element('svg', width='300', height='45', version='1.1', xmlns='http://www.w3.org/2000/svg', viewBox='0 0 300 45') gradient = etree.Element('linearGradient', id=name, gradientUnits='objectBoundingBox', spreadMethod='pad', x1='0%', x2='100%', y1='0%', y2='0%') #make discrete svg ramp if discrete: stops = [] for i, stop in enumerate(self.stops): if i>0: stops.append( Stop(stop.position, self.stops[i-1].color) ) stops.append( Stop(stop.position, stop.color) ) else: stops = self.stops for stop in stops: p = stop.position * 100 p = str(round(p,2)) + '%' r,g,b = stop.color.RGB c = "rgb(%d,%d,%d)" % (r, g, b) a = str(stop.color.alpha) etree.SubElement(gradient, 'stop', {'offset':p, 'stop-color':c, 'stop-opacity':a}) #use dict because hyphens in tags svg.append(gradient) rect = etree.Element('rect', {'fill':"url(#%s)" % (name), 'x':'4', 'y':'4', 'width':'292', 'height':'37', 'stroke':'black', 'stroke_width':'1'}) svg.append(rect) # get string xmlstr = etree.tostring(svg, encoding='utf8', method='xml').decode('utf-8') # etree doesn't have pretty xml function, so use minidom tu get a pretty xml ... reparsed = parseString(xmlstr) xmlstr = reparsed.toprettyxml() # write to file f = open(svgPath,"w") f.write(xmlstr) f.close() return ================================================ FILE: core/utils/timing.py ================================================ import time def perf_clock(): if hasattr(time, 'clock'): return time.clock() elif hasattr(time, 'perf_counter'): return time.perf_counter() else: raise Exception("Python time lib doesn't contain a suitable clock function") ================================================ FILE: core/utils/xy.py ================================================ # -*- coding:utf-8 -*- # This file is part of BlenderGIS # ***** GPL LICENSE BLOCK ***** # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # All rights reserved. # ***** GPL LICENSE BLOCK ***** class XY(object): '''A class to represent 2-tuple value''' def __init__(self, x, y, z=None): ''' You can use the constructor in many ways: XY(0, 1) - passing two arguments XY(x=0, y=1) - passing keywords arguments XY(**{'x': 0, 'y': 1}) - unpacking a dictionary XY(*[0, 1]) - unpacking a list or a tuple (or a generic iterable) ''' if z is None: self.data=[x, y] else: self.data=[x, y, z] def __str__(self): if self.z is not None: return "(%s, %s, %s)"%(self.x, self.y, self.z) else: return "(%s, %s)"%(self.x,self.y) def __repr__(self): return self.__str__() def __getitem__(self,item): return self.data[item] def __setitem__(self, idx, value): self.data[idx] = value def __iter__(self): return iter(self.data) def __len__(self): return len(self.data) @property def x(self): return self.data[0] @property def y(self): return self.data[1] @property def z(self): try: return self.data[2] except IndexError: return None @property def xy(self): return self.data[:2] @property def xyz(self): return self.data ================================================ FILE: geoscene.py ================================================ # -*- coding:utf-8 -*- # ***** GPL LICENSE BLOCK ***** # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # All rights reserved. # ***** GPL LICENSE BLOCK ***** import logging log = logging.getLogger(__name__) import bpy from bpy.props import (StringProperty, IntProperty, FloatProperty, BoolProperty, EnumProperty, FloatVectorProperty, PointerProperty) from bpy.types import Operator, Panel, PropertyGroup from .prefs import PredefCRS from .core.proj.reproj import reprojPt from .core.proj.srs import SRS from .operators.utils import mouseTo3d PKG = __package__ ''' Policy : This module manages in priority the CRS coordinates of the scene's origin and updates the corresponding longitude/latitude only if it can to do the math. A scene is considered correctly georeferenced when at least a valid CRS is defined and the coordinates of scene's origin in this CRS space is set. A geoscene will be broken if the origin is set but not the CRS or if the origin is only set as longitude/latitude. Changing the CRS will raise an error if updating existing origin coordinate is not possible. Both methods setOriginGeo() and setOriginPrj() try a projection task to maintain coordinates synchronized. Failing reprojection does not abort the exec, but will trigger deletion of unsynch coordinates. Synchronization can be disable for setOriginPrj() method only. Except setOriginGeo() method, dealing directly with longitude/latitude automatically trigger a reprojection task which will raise an error if failing. Sequences of methods : moveOriginPrj() | updOriginPrj() > setOriginPrj() > [reprojPt()] moveOriginGeo() > updOriginGeo() > reprojPt() > updOriginPrj() > setOriginPrj() Standalone properties (lon, lat, crsx et crsy) can be edited independently without any extra checks. ''' class SK(): """Alias to Scene Keys used to store georef infos""" # latitude and longitude of scene origin in decimal degrees LAT = "latitude" LON = "longitude" #Spatial Reference System Identifier # can be directly an EPSG code or formated following the template "AUTH:4326" # or a proj4 string definition of Coordinate Reference System (CRS) CRS = "SRID" # Coordinates of scene origin in CRS space CRSX = "crs x" CRSY = "crs y" # General scale denominator of the map (1:x) SCALE = "scale" # Current zoom level in the Tile Matrix Set ZOOM = "zoom" class GeoScene(): def __init__(self, scn=None): if scn is None: self.scn = bpy.context.scene else: self.scn = scn self.SK = SK() @property def _rna_ui(self): # get or init the dictionary containing IDprops settings rna_ui = self.scn.get('_RNA_UI', None) if rna_ui is None: self.scn['_RNA_UI'] = {} rna_ui = self.scn['_RNA_UI'] return rna_ui def view3dToProj(self, dx, dy): '''Convert view3d coords to crs coords''' if self.hasOriginPrj: x = self.crsx + (dx * self.scale) y = self.crsy + (dy * self.scale) return x, y else: raise Exception("Scene origin coordinate is unset") def projToView3d(self, dx, dy): '''Convert view3d coords to crs coords''' if self.hasOriginPrj: x = (dx * self.scale) - self.crsx y = (dy * self.scale) - self.crsy return x, y else: raise Exception("Scene origin coordinate is unset") @property def hasCRS(self): return SK.CRS in self.scn @property def hasValidCRS(self): if not self.hasCRS: return False return SRS.validate(self.crs) @property def isGeoref(self): '''A scene is georef if at least a valid CRS is defined and the coordinates of scene's origin in this CRS space is set''' return self.hasValidCRS and self.hasOriginPrj @property def isFullyGeoref(self): return self.hasValidCRS and self.hasOriginPrj and self.hasOriginGeo @property def isPartiallyGeoref(self): return self.hasCRS or self.hasOriginPrj or self.hasOriginGeo @property def isBroken(self): """partial georef infos make the geoscene unusuable and broken""" return (self.hasCRS and not self.hasValidCRS) \ or (not self.hasCRS and (self.hasOriginPrj or self.hasOriginGeo)) \ or (self.hasCRS and self.hasOriginGeo and not self.hasOriginPrj) @property def hasOriginGeo(self): return SK.LAT in self.scn and SK.LON in self.scn @property def hasOriginPrj(self): return SK.CRSX in self.scn and SK.CRSY in self.scn def setOriginGeo(self, lon, lat): self.lon, self.lat = lon, lat try: self.crsx, self.crsy = reprojPt(4326, self.crs, lon, lat) except Exception as e: if self.hasOriginPrj: self.delOriginPrj() log.warning('Origin proj has been deleted because the property could not be updated', exc_info=True) def setOriginPrj(self, x, y, synch=True): self.crsx, self.crsy = x, y if synch: try: self.lon, self.lat = reprojPt(self.crs, 4326, x, y) except Exception as e: if self.hasOriginGeo: self.delOriginGeo() log.warning('Origin geo has been deleted because the property could not be updated', exc_info=True) elif self.hasOriginGeo: self.delOriginGeo() log.warning('Origin geo has been deleted because coordinate synchronization is disable') def updOriginPrj(self, x, y, updObjLoc=True, synch=True): '''Update/move scene origin passing absolute coordinates''' if not self.hasOriginPrj: raise Exception("Cannot update an unset origin.") dx = x - self.crsx dy = y - self.crsy self.setOriginPrj(x, y, synch) if updObjLoc: self._moveObjLoc(dx, dy) def updOriginGeo(self, lon, lat, updObjLoc=True): if not self.isGeoref: raise Exception("Cannot update geo origin of an ungeoref scene.") x, y = reprojPt(4326, self.crs, lon, lat) self.updOriginPrj(x, y, updObjLoc) def moveOriginGeo(self, dx, dy, updObjLoc=True): if not self.hasOriginGeo: raise Exception("Cannot move an unset origin.") x = self.lon + dx y = self.lat + dy self.updOriginGeo(x, y, updObjLoc=updObjLoc) def moveOriginPrj(self, dx, dy, useScale=True, updObjLoc=True, synch=True): '''Move scene origin passing relative deltas''' if not self.hasOriginPrj: raise Exception("Cannot move an unset origin.") if useScale: self.setOriginPrj(self.crsx + dx * self.scale, self.crsy + dy * self.scale, synch) else: self.setOriginPrj(self.crsx + dx, self.crsy + dy, synch) if updObjLoc: self._moveObjLoc(dx, dy) def _moveObjLoc(self, dx, dy): topParents = [obj for obj in self.scn.objects if not obj.parent] for obj in topParents: obj.location.x -= dx obj.location.y -= dy def getOriginGeo(self): return self.lon, self.lat def getOriginPrj(self): return self.crsx, self.crsy def delOriginGeo(self): del self.lat del self.lon def delOriginPrj(self): del self.crsx del self.crsy def delOrigin(self): self.delOriginGeo() self.delOriginPrj() @property def crs(self): return self.scn.get(SK.CRS, None) #always string @crs.setter def crs(self, v): #Make sure input value is a valid crs string representation crs = SRS(v) #will raise an error if the crs is not valid #Reproj existing origin. New CRS will not be set if updating existing origin is not possible # try first to reproj from origin geo because self.crs can be empty or broken if self.hasOriginGeo: if crs.isWGS84: #if destination crs is wgs84, just assign lonlat to originprj self.crsx, self.crsy = self.lon, self.lat self.crsx, self.crsy = reprojPt(4326, str(crs), self.lon, self.lat) elif self.hasOriginPrj and self.hasCRS: if self.hasValidCRS: # will raise an error is current crs is empty or invalid self.crsx, self.crsy = reprojPt(self.crs, str(crs), self.crsx, self.crsy) else: raise Exception("Scene origin coordinates cannot be updated because current CRS is invalid.") #Set ID prop if SK.CRS not in self.scn: self._rna_ui[SK.CRS] = {"description": "Map Coordinate Reference System", "default": ''} self.scn[SK.CRS] = str(crs) @crs.deleter def crs(self): if SK.CRS in self.scn: del self.scn[SK.CRS] @property def lat(self): return self.scn.get(SK.LAT, None) @lat.setter def lat(self, v): if SK.LAT not in self.scn: self._rna_ui[SK.LAT] = {"description": "Scene origin latitude", "default": 0.0, "min":-90.0, "max":90.0} if -90 <= v <= 90: self.scn[SK.LAT] = v else: raise ValueError('Wrong latitude value '+str(v)) @lat.deleter def lat(self): if SK.LAT in self.scn: del self.scn[SK.LAT] @property def lon(self): return self.scn.get(SK.LON, None) @lon.setter def lon(self, v): if SK.LON not in self.scn: self._rna_ui[SK.LON] = {"description": "Scene origin longitude", "default": 0.0, "min":-180.0, "max":180.0} if -180 <= v <= 180: self.scn[SK.LON] = v else: raise ValueError('Wrong longitude value '+str(v)) @lon.deleter def lon(self): if SK.LON in self.scn: del self.scn[SK.LON] @property def crsx(self): return self.scn.get(SK.CRSX, None) @crsx.setter def crsx(self, v): if SK.CRSX not in self.scn: self._rna_ui[SK.CRSX] = {"description": "Scene x origin in CRS space", "default": 0.0} if isinstance(v, (int, float)): self.scn[SK.CRSX] = v else: raise ValueError('Wrong x origin value '+str(v)) @crsx.deleter def crsx(self): if SK.CRSX in self.scn: del self.scn[SK.CRSX] @property def crsy(self): return self.scn.get(SK.CRSY, None) @crsy.setter def crsy(self, v): if SK.CRSY not in self.scn: self._rna_ui[SK.CRSY] = {"description": "Scene y origin in CRS space", "default": 0.0} if isinstance(v, (int, float)): self.scn[SK.CRSY] = v else: raise ValueError('Wrong y origin value '+str(v)) @crsy.deleter def crsy(self): if SK.CRSY in self.scn: del self.scn[SK.CRSY] @property def scale(self): return self.scn.get(SK.SCALE, 1) @scale.setter def scale(self, v): if SK.SCALE not in self.scn: self._rna_ui[SK.SCALE] = {"description": "Map scale denominator", "default": 1, "min": 1} self.scn[SK.SCALE] = v @scale.deleter def scale(self): if SK.SCALE in self.scn: del self.scn[SK.SCALE] @property def zoom(self): return self.scn.get(SK.ZOOM, None) @zoom.setter def zoom(self, v): if SK.ZOOM not in self.scn: self._rna_ui[SK.ZOOM] = {"description": "Basemap zoom level", "default": 1, "min": 0, "max":25} self.scn[SK.ZOOM] = v @zoom.deleter def zoom(self): if SK.ZOOM in self.scn: del self.scn[SK.ZOOM] @property def hasScale(self): #return self.scale is not None return SK.SCALE in self.scn @property def hasZoom(self): return self.zoom is not None ################ OPERATORS ###################### from bpy_extras.view3d_utils import region_2d_to_location_3d, region_2d_to_vector_3d class GEOSCENE_OT_coords_viewer(Operator): bl_idname = "geoscene.coords_viewer" bl_description = '' bl_label = "" bl_options = {'INTERNAL', 'UNDO'} coords: FloatVectorProperty(subtype='XYZ') @classmethod def poll(cls, context): return bpy.context.mode == 'OBJECT' and context.area.type == 'VIEW_3D' def invoke(self, context, event): self.geoscn = GeoScene(context.scene) if not self.geoscn.isGeoref or self.geoscn.isBroken: self.report({'ERROR'}, "Scene is not correctly georeferencing") return {'CANCELLED'} #Add modal handler and init a timer context.window_manager.modal_handler_add(self) self.timer = context.window_manager.event_timer_add(0.05, window=context.window) context.window.cursor_set('CROSSHAIR') return {'RUNNING_MODAL'} def modal(self, context, event): if event.type == 'MOUSEMOVE': loc = mouseTo3d(context, event.mouse_region_x, event.mouse_region_y) x, y = self.geoscn.view3dToProj(loc.x, loc.y) context.area.header_text_set("x {:.3f}, y {:.3f}, z {:.3f}".format(x, y, loc.z)) if event.type == 'ESC' and event.value == 'PRESS': context.window.cursor_set('DEFAULT') context.area.header_text_set(None) return {'CANCELLED'} return {'RUNNING_MODAL'} class GEOSCENE_OT_set_crs(Operator): ''' use the enum of predefinites crs defined in addon prefs to select and switch scene crs definition ''' bl_idname = "geoscene.set_crs" bl_description = 'Switch scene crs' bl_label = "Switch to" bl_options = {'INTERNAL', 'UNDO'} """ #to avoid conflict, make a distinct predef crs enum #instead of reuse the one defined in addon pref def listPredefCRS(self, context): return PredefCRS.getEnumItems() crsEnum = EnumProperty( name = "Predefinate CRS", description = "Choose predefinite Coordinate Reference System", items = listPredefCRS ) """ def draw(self,context): prefs = context.preferences.addons[PKG].preferences layout = self.layout row = layout.row(align=True) #row.prop(self, "crsEnum", text='') row.prop(prefs, "predefCrs", text='') #row.operator("geoscene.show_pref", text='', icon='PREFERENCES') row.operator("bgis.add_predef_crs", text='', icon='ADD') def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self, width=200) def execute(self, context): geoscn = GeoScene(context.scene) prefs = context.preferences.addons[PKG].preferences try: geoscn.crs = prefs.predefCrs except Exception as err: log.error('Cannot update crs', exc_info=True) self.report({'ERROR'}, 'Cannot update crs. Check logs form more info') return {'CANCELLED'} # context.area.tag_redraw() return {'FINISHED'} class GEOSCENE_OT_init_org(Operator): bl_idname = "geoscene.init_org" bl_description = 'Init scene origin custom props at location 0,0' bl_label = "Init origin" bl_options = {'INTERNAL', 'UNDO'} lonlat: BoolProperty( name = "As lonlat", description = "Set origin coordinate as longitude and latitude" ) x: FloatProperty() y: FloatProperty() def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self, width=200) def execute(self, context): geoscn = GeoScene(context.scene) if geoscn.hasOriginGeo or geoscn.hasOriginPrj: log.warning('Cannot init scene origin because it already exist') return {'CANCELLED'} else: #geoscn.lon, geoscn.lat = 0, 0 #geoscn.crsx, geoscn.crsy = 0, 0 if self.lonlat: geoscn.setOriginGeo(self.x, self.y) else: geoscn.setOriginPrj(self.x, self.y) return {'FINISHED'} class GEOSCENE_OT_edit_org_geo(Operator): bl_idname = "geoscene.edit_org_geo" bl_description = 'Edit scene origin longitude/latitude' bl_label = "Edit origin geo" bl_options = {'INTERNAL', 'UNDO'} lon: FloatProperty() lat: FloatProperty() def invoke(self, context, event): geoscn = GeoScene(context.scene) if geoscn.isBroken: self.report({'ERROR'}, "Scene georef is broken") return {'CANCELLED'} self.lon, self.lat = geoscn.getOriginGeo() return context.window_manager.invoke_props_dialog(self) def execute(self, context): geoscn = GeoScene(context.scene) if geoscn.hasOriginGeo: geoscn.updOriginGeo(self.lon, self.lat) else: geoscn.setOriginGeo(self.lon, self.lat) return {'FINISHED'} class GEOSCENE_OT_edit_org_prj(Operator): bl_idname = "geoscene.edit_org_prj" bl_description = 'Edit scene origin in projected system' bl_label = "Edit origin proj" bl_options = {'INTERNAL', 'UNDO'} x: FloatProperty() y: FloatProperty() def invoke(self, context, event): geoscn = GeoScene(context.scene) if geoscn.isBroken: self.report({'ERROR'}, "Scene georef is broken") return {'CANCELLED'} self.x, self.y = geoscn.getOriginPrj() return context.window_manager.invoke_props_dialog(self) def execute(self, context): geoscn = GeoScene(context.scene) if geoscn.hasOriginPrj: geoscn.updOriginPrj(self.x, self.y) else: geoscn.setOriginPrj(self.x, self.y) return {'FINISHED'} class GEOSCENE_OT_link_org_geo(Operator): bl_idname = "geoscene.link_org_geo" bl_description = 'Link scene origin lat long' bl_label = "Link geo" bl_options = {'INTERNAL', 'UNDO'} def execute(self, context): geoscn = GeoScene(context.scene) if geoscn.hasOriginPrj and geoscn.hasCRS: try: geoscn.lon, geoscn.lat = reprojPt(geoscn.crs, 4326, geoscn.crsx, geoscn.crsy) except Exception as err: log.error('Cannot compute lat/lon coordinates', exc_info=True) self.report({'ERROR'}, 'Cannot compute lat/lon. Check logs for more infos.') return {'CANCELLED'} else: self.report({'ERROR'}, 'No enough infos') return {'CANCELLED'} return {'FINISHED'} class GEOSCENE_OT_link_org_prj(Operator): bl_idname = "geoscene.link_org_prj" bl_description = 'Link scene origin in crs space' bl_label = "Link prj" bl_options = {'INTERNAL', 'UNDO'} def execute(self, context): geoscn = GeoScene(context.scene) if geoscn.hasOriginGeo and geoscn.hasCRS: try: geoscn.crsx, geoscn.crsy = reprojPt(4326, geoscn.crs, geoscn.lon, geoscn.lat) except Exception as err: log.error('Cannot compute crs coordinates', exc_info=True) self.report({'ERROR'}, 'Cannot compute crs coordinates. Check logs for more infos.') return {'CANCELLED'} else: self.report({'ERROR'}, 'No enough infos') return {'CANCELLED'} return {'FINISHED'} class GEOSCENE_OT_clear_org(Operator): bl_idname = "geoscene.clear_org" bl_description = 'Clear scene origin coordinates' bl_label = "Clear origin" bl_options = {'INTERNAL', 'UNDO'} def execute(self, context): geoscn = GeoScene(context.scene) geoscn.delOrigin() return {'FINISHED'} class GEOSCENE_OT_clear_georef(Operator): bl_idname = "geoscene.clear_georef" bl_description = 'Clear all georef infos' bl_label = "Clear georef" bl_options = {'INTERNAL', 'UNDO'} def execute(self, context): geoscn = GeoScene(context.scene) geoscn.delOrigin() del geoscn.crs return {'FINISHED'} ################ PROPS GETTERS SETTERS ###################### def getLon(self): geoscn = GeoScene() return geoscn.lon def getLat(self): geoscn = GeoScene() return geoscn.lat def setLon(self, lon): geoscn = GeoScene() prefs = bpy.context.preferences.addons[PKG].preferences if geoscn.hasOriginGeo: geoscn.updOriginGeo(lon, geoscn.lat, updObjLoc=prefs.lockObj) else: geoscn.setOriginGeo(lon, geoscn.lat) def setLat(self, lat): geoscn = GeoScene() prefs = bpy.context.preferences.addons[PKG].preferences if geoscn.hasOriginGeo: geoscn.updOriginGeo(geoscn.lon, lat, updObjLoc=prefs.lockObj) else: geoscn.setOriginGeo(geoscn.lon, lat) def getCrsx(self): geoscn = GeoScene() return geoscn.crsx def getCrsy(self): geoscn = GeoScene() return geoscn.crsy def setCrsx(self, x): geoscn = GeoScene() prefs = bpy.context.preferences.addons[PKG].preferences if geoscn.hasOriginPrj: geoscn.updOriginPrj(x, geoscn.crsy, updObjLoc=prefs.lockObj) else: geoscn.setOriginPrj(x, geoscn.crsy) def setCrsy(self, y): geoscn = GeoScene() prefs = bpy.context.preferences.addons[PKG].preferences if geoscn.hasOriginPrj: geoscn.updOriginPrj(geoscn.crsx, y, updObjLoc=prefs.lockObj) else: geoscn.setOriginPrj(geoscn.crsx, y) ################ PANEL ###################### class GEOSCENE_PT_georef(Panel): bl_category = "View"#"GIS" bl_label = "Geoscene" bl_space_type = "VIEW_3D" bl_context = "objectmode" bl_region_type = "UI" def draw(self, context): layout = self.layout scn = context.scene geoscn = GeoScene(scn) #layout.operator("bgis.pref_show", icon='PREFERENCES') georefManagerLayout(self, context) layout.operator("geoscene.coords_viewer", icon='WORLD', text='Geo-coordinates') #hidden props used as display options in georef manager panel class GLOBAL_PROPS(PropertyGroup): displayOriginGeo: BoolProperty( name='Geo', description='Display longitude and latitude of scene origin') displayOriginPrj: BoolProperty( name='Proj', description='Display coordinates of scene origin in CRS space') lon: FloatProperty(get=getLon, set=setLon) lat: FloatProperty(get=getLat, set=setLat) crsx: FloatProperty(get=getCrsx, set=setCrsx) crsy: FloatProperty(get=getCrsy, set=setCrsy) def georefManagerLayout(self, context): '''Use this method to extend a panel with georef managment tools''' layout = self.layout scn = context.scene wm = bpy.context.window_manager geoscn = GeoScene(scn) prefs = context.preferences.addons[PKG].preferences if geoscn.isBroken: layout.alert = True row = layout.row(align=True) row.label(text='Scene georeferencing :') if geoscn.hasCRS: row.operator("geoscene.clear_georef", text='', icon='CANCEL') #CRS row = layout.row(align=True) #row.alignment = 'LEFT' #row.label(icon='EMPTY_DATA') split = row.split(factor=0.25) if geoscn.hasCRS: split.label(icon='PROP_ON', text='CRS:') elif not geoscn.hasCRS and (geoscn.hasOriginGeo or geoscn.hasOriginPrj): split.label(icon='ERROR', text='CRS:') else: split.label(icon='PROP_OFF', text='CRS:') if geoscn.hasCRS: ##col = split.column(align=True) ##col.enabled = False ##col.prop(scn, '["'+SK.CRS+'"]', text='') crs = scn[SK.CRS] name = PredefCRS.getName(crs) if name is not None: split.label(text=name) else: split.label(text=crs) else: split.label(text="Not set") row.operator("geoscene.set_crs", text='', icon='PREFERENCES') #Origin row = layout.row(align=True) #row.alignment = 'LEFT' #row.label(icon='PIVOT_CURSOR') split = row.split(factor=0.25, align=True) if not geoscn.hasOriginGeo and not geoscn.hasOriginPrj: split.label(icon='PROP_OFF', text="Origin:") elif not geoscn.hasOriginGeo and geoscn.hasOriginPrj: split.label(icon='PROP_CON', text="Origin:") elif geoscn.hasOriginGeo and geoscn.hasOriginPrj: split.label(icon='PROP_ON', text="Origin:") elif geoscn.hasOriginGeo and not geoscn.hasOriginPrj: split.label(icon='ERROR', text="Origin:") col = split.column(align=True) if not geoscn.hasOriginGeo: col.enabled = False col.prop(wm.geoscnProps, 'displayOriginGeo', toggle=True) col = split.column(align=True) if not geoscn.hasOriginPrj: col.enabled = False col.prop(wm.geoscnProps, 'displayOriginPrj', toggle=True) if geoscn.hasOriginGeo or geoscn.hasOriginPrj: if geoscn.hasCRS and not geoscn.hasOriginPrj: row.operator("geoscene.link_org_prj", text="", icon='CONSTRAINT') if geoscn.hasCRS and not geoscn.hasOriginGeo: row.operator("geoscene.link_org_geo", text="", icon='CONSTRAINT') row.operator("geoscene.clear_org", text="", icon='REMOVE') if not geoscn.hasOriginGeo and not geoscn.hasOriginPrj: row.operator("geoscene.init_org", text="", icon='ADD') if geoscn.hasOriginGeo and wm.geoscnProps.displayOriginGeo: row = layout.row() row.prop(wm.geoscnProps, 'lon', text='Lon') row.prop(wm.geoscnProps, 'lat', text='Lat') ''' row.enabled = False row.prop(scn, '["'+SK.LON+'"]', text='Lon') row.prop(scn, '["'+SK.LAT+'"]', text='Lat') ''' if geoscn.hasOriginPrj and wm.geoscnProps.displayOriginPrj: row = layout.row() row.prop(wm.geoscnProps, 'crsx', text='X') row.prop(wm.geoscnProps, 'crsy', text='Y') ''' row.enabled = False row.prop(scn, '["'+SK.CRSX+'"]', text='X') row.prop(scn, '["'+SK.CRSY+'"]', text='Y') ''' if geoscn.hasScale: row = layout.row() row.label(text='Map scale:') col = row.column() col.enabled = False col.prop(scn, '["'+SK.SCALE+'"]', text='') #if geoscn.hasZoom: # layout.prop(scn, '["'+SK.ZOOM+'"]', text='Zoom level', slider=True) ########################### classes = [ GEOSCENE_OT_coords_viewer, GEOSCENE_OT_set_crs, GEOSCENE_OT_init_org, GEOSCENE_OT_edit_org_geo, GEOSCENE_OT_edit_org_prj, GEOSCENE_OT_link_org_geo, GEOSCENE_OT_link_org_prj, GEOSCENE_OT_clear_org, GEOSCENE_OT_clear_georef, GEOSCENE_PT_georef, GLOBAL_PROPS ] def register(): for cls in classes: try: bpy.utils.register_class(cls) except ValueError as e: log.warning('{} is already registered, now unregister and retry... '.format(cls)) bpy.utils.unregister_class(cls) bpy.utils.register_class(cls) bpy.types.WindowManager.geoscnProps = PointerProperty(type=GLOBAL_PROPS) def unregister(): del bpy.types.WindowManager.geoscnProps for cls in classes: bpy.utils.unregister_class(cls) ================================================ FILE: issue_template.md ================================================ # **Blender and OS versions** # **Describe the bug** # **How to Reproduce** # **Error message** ================================================ FILE: operators/__init__.py ================================================ __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"] ================================================ FILE: operators/add_camera_exif.py ================================================ # -*- coding:utf-8 -*- # ***** GPL LICENSE BLOCK ***** # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # All rights reserved. # ***** GPL LICENSE BLOCK ***** import os from math import pi import logging log = logging.getLogger(__name__) import bpy from bpy.props import StringProperty, CollectionProperty, EnumProperty from bpy.types import Panel, Operator, OperatorFileListElement #bgis from ..geoscene import GeoScene #core from ..core.proj import reprojPt from ..core.georaster import getImgFormat #deps from ..core.lib import Tyf def newEmpty(scene, name, location): """Create a new empty""" target = bpy.data.objects.new(name, None) target.empty_display_size = 40 target.empty_display_type = 'PLAIN_AXES' target.location = location scene.collection.objects.link(target) return target def newCamera(scene, name, location, focalLength): """Create a new camera""" cam = bpy.data.cameras.new(name) cam.sensor_width = 35 cam.lens = focalLength cam.display_size = 40 cam_obj = bpy.data.objects.new(name,cam) cam_obj.location = location cam_obj.rotation_euler[0] = pi/2 cam_obj.rotation_euler[2] = pi scene.collection.objects.link(cam_obj) return cam, cam_obj def newTargetCamera(scene, name, location, focalLength): """Create a new camera.target""" cam, cam_obj = newCamera(scene, name, location, focalLength) x, y, z = location[:] target = newEmpty(scene, name+".target", (x, y - 50, z)) constraint = cam_obj.constraints.new(type='TRACK_TO') constraint.track_axis = 'TRACK_NEGATIVE_Z' constraint.up_axis = 'UP_Y' constraint.target = target return cam, cam_obj class CAMERA_OT_geophotos_add(Operator): bl_idname = "camera.geophotos" bl_description = "Create cameras from geotagged photos" bl_label = "Exif cam" bl_options = {"REGISTER"} files: CollectionProperty( name="File Path", type=OperatorFileListElement, ) directory: StringProperty( subtype='DIR_PATH', ) filter_glob: StringProperty( default="*.jpg;*.jpeg;*.tif;*.tiff", options={'HIDDEN'}, ) filename_ext = "" exifMode: EnumProperty( attr="exif_mode", name="Action", description="Choose an action", 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')], default="TARGET_CAMERA" ) def invoke(self, context, event): scn = context.scene geoscn = GeoScene(scn) if not geoscn.isGeoref: self.report({'ERROR'},"The scene must be georeferenced.") return {'CANCELLED'} #File browser context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} def execute(self, context): scn = context.scene geoscn = GeoScene(scn) directory = self.directory for file_elem in self.files: filepath = os.path.join(directory, file_elem.name) if not os.path.isfile(filepath): self.report({'ERROR'},"Invalid file") return {'CANCELLED'} imgFormat = getImgFormat(filepath) if imgFormat not in ['JPEG', 'TIFF']: self.report({'ERROR'},"Invalid format " + str(imgFormat)) return {'CANCELLED'} try: exif = Tyf.open(filepath) except Exception as e: log.error("Unable to open file", exc_info=True) self.report({'ERROR'},"Unable to open file. Checks logs for more infos.") return {'CANCELLED'} #tags = {t.key:exif[t.key] for t in exif.exif.tags() if t.key != 'Unknown' } #print(tags) #Warning : Tyf object does not totally behave like a python dictionnary #testing if a tags exists with the syntax "if k in exif" does not works #using the get method does not work either. For example : alt = exif.get("GPSAltitude", 0) #that's why we proceed with "try except KeyError" blocks instead of conditional block or get() method try: #if not any([k in exif for k in ('GPSLatitude', 'GPSLatitudeRef', 'GPSLongitude', 'GPSLongitudeRef')]): lat = exif["GPSLatitude"] * exif["GPSLatitudeRef"] lon = exif["GPSLongitude"] * exif["GPSLongitudeRef"] except KeyError: self.report({'ERROR'},"Can't find GPS longitude or latitude.") return {'CANCELLED'} #alt = exif.get("GPSAltitude", 0) try: alt = exif["GPSAltitude"] except KeyError: alt = 0 try: x, y = reprojPt(4326, geoscn.crs, lon, lat) except Exception as e: log.error("Reprojection fails", exc_info=True) self.report({'ERROR'},"Reprojection error. Check logs for more infos.") return {'CANCELLED'} try: focalLength = exif["FocalLengthIn35mmFilm"] except KeyError: focalLength = 35 location = (x-geoscn.crsx, y-geoscn.crsy, alt) name = bpy.path.display_name_from_filepath(filepath) if self.exifMode == "TARGET_CAMERA": cam, cam_obj = newTargetCamera(scn, name, location, focalLength) elif self.exifMode == "CAMERA": cam, cam_obj = newCamera(scn, name, location, focalLength) elif self.exifMode == "EMPTY": newEmpty(scn, name, location) else: scn.cursor.location = location if self.exifMode in ["TARGET_CAMERA","CAMERA"]: cam['background'] = filepath ''' try: cam['imageWidth'] = exif["PixelXDimension"] #for jpg, in tif tag is named imageWidth... cam['imageHeight'] = exif["PixelYDimension"] except KeyError: pass ''' img = bpy.data.images.load(filepath) w, h = img.size cam['imageWidth'] = w #exif["PixelXDimension"] #for jpg, in tif file the tag is named imageWidth... cam['imageHeight'] = h try: cam['orientation'] = exif["Orientation"] except KeyError: cam['orientation'] = 1 #no rotation #Set camera rotation (NOT TESTED) if cam['orientation'] == 8: #90° CCW cam_obj.rotation_euler[1] -= pi/2 if cam['orientation'] == 6: #90° CW cam_obj.rotation_euler[1] += pi/2 if cam['orientation'] == 3: #180° cam_obj.rotation_euler[1] += pi if scn.camera is None: bpy.ops.camera.geophotos_setactive('EXEC_DEFAULT', camLst=cam_obj.name) return {'FINISHED'} class CAMERA_OT_geophotos_setactive(Operator): bl_idname = "camera.geophotos_setactive" bl_description = "Switch active geophoto camera" bl_label = "Switch geophoto camera" bl_options = {"REGISTER"} def listGeoCam(self, context): scn = context.scene #put each object in a tuple (key, label, tooltip) return [(obj.name, obj.name, obj.name) for obj in scn.objects if obj.type == 'CAMERA' and 'background' in obj.data] camLst: EnumProperty(name='Camera', description='Select camera', items=listGeoCam) def draw(self, context): layout = self.layout layout.prop(self, 'camLst')#, text='') def invoke(self, context, event): if len(self.camLst) == 0: self.report({'ERROR'},"No valid camera") return {'CANCELLED'} return context.window_manager.invoke_props_dialog(self)#, width=200) def execute(self, context): if context.space_data.type != 'VIEW_3D': self.report({'ERROR'},"Wrong context") return {'CANCELLED'} scn = context.scene view3d = context.space_data #Get cam cam_obj = scn.objects[self.camLst] cam_obj.select_set(True) context.view_layer.objects.active = cam_obj cam = cam_obj.data scn.camera = cam_obj #Set render size scn.render.resolution_x = cam['imageWidth'] scn.render.resolution_y = cam['imageHeight'] scn.render.resolution_percentage = 100 #Get or load bpy image filepath = cam['background'] try: img = [img for img in bpy.data.images if img.filepath == filepath][0] except IndexError: img = bpy.data.images.load(filepath) #Activate view3d background cam.show_background_images = True #Hide all existing camera background for bkg in cam.background_images: bkg.show_background_image = False #Get or load background image bkgs = [bkg for bkg in cam.background_images if bkg.image is not None] try: bkg = [bkg for bkg in bkgs if bkg.image.filepath == filepath][0] except IndexError: bkg = cam.background_images.new() bkg.image = img #Set some props bkg.show_background_image = True bkg.alpha = 1 return {'FINISHED'} classes = [ CAMERA_OT_geophotos_add, CAMERA_OT_geophotos_setactive ] def register(): for cls in classes: try: bpy.utils.register_class(cls) except ValueError as e: log.warning('{} is already registered, now unregister and retry... '.format(cls)) bpy.utils.unregister_class(cls) bpy.utils.register_class(cls) def unregister(): for cls in classes: bpy.utils.unregister_class(cls) ================================================ FILE: operators/add_camera_georef.py ================================================ # -*- coding:utf-8 -*- # ***** GPL LICENSE BLOCK ***** # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # All rights reserved. # ***** GPL LICENSE BLOCK ***** import logging log = logging.getLogger(__name__) import bpy from mathutils import Vector from bpy.props import StringProperty, BoolProperty, EnumProperty, FloatProperty from .utils import getBBOX from ..geoscene import GeoScene class CAMERA_OT_add_georender_cam(bpy.types.Operator): ''' Add a new georef camera or update an existing one A georef camera is a top view orthographic camera that can be used to render a map The camera is setting to encompass the selected object, the output spatial resolution (meters/pixel) can be set by the user A worldfile is writen in BLender text editor, it can be used to georef the output render ''' bl_idname = "camera.georender" bl_label = "Georef cam" bl_description = "Create or update a camera to render a georeferencing map" bl_options = {"REGISTER", "UNDO"} name: StringProperty(name = "Camera name", default="Georef cam", description="") target_res: FloatProperty(name = "Pixel size", default=5, description="Pixel size in map units/pixel", min=0.00001) zLocOffset: FloatProperty(name = "Z loc. off.", default=50, description="Camera z location offet, defined as percentage of z dimension of the target mesh", min=0) redo = 0 bbox = None #global var used to avoid recomputing the bbox at each redo def check(self, context): return True def draw(self, context): layout = self.layout layout.prop(self, 'name') layout.prop(self, 'target_res') layout.prop(self, 'zLocOffset') @classmethod def poll(cls, context): return context.mode == 'OBJECT' def execute(self, context):#every times operator redo options are modified #Operator redo count self.redo += 1 #Check georef scn = context.scene geoscn = GeoScene(scn) if not geoscn.isGeoref: self.report({'ERROR'}, "Scene isn't georef") return {'CANCELLED'} #Validate selection objs = bpy.context.selected_objects if (not objs or len(objs) > 2) or \ (len(objs) == 1 and not objs[0].type == 'MESH') or \ (len(objs) == 2 and not set( (objs[0].type, objs[1].type )) == set( ('MESH','CAMERA') ) ): self.report({'ERROR'}, "Pre-selection is incorrect") return {'CANCELLED'} #Flag new camera creation if len(objs) == 2: newCam = False else: newCam = True #Get georef data dx, dy = geoscn.getOriginPrj() #Allocate obj for obj in objs: if obj.type == 'MESH': georefObj = obj elif obj.type == 'CAMERA': camObj = obj cam = camObj.data #do not recompute bbox at operator redo because zdim is miss-evaluated #when redoing the op on an obj that have a displace modifier on it #TODO find a less hacky fix if self.bbox is None: bbox = getBBOX.fromObj(georefObj, applyTransform = True) self.bbox = bbox else: bbox = self.bbox locx, locy, locz = bbox.center dimx, dimy, dimz = bbox.dimensions if dimz == 0: dimz = 1 #dimx, dimy, dimz = georefObj.dimensions #dimensions property apply object transformations (scale and rot.) #Set active cam if newCam: cam = bpy.data.cameras.new(name=self.name) cam['mapRes'] = self.target_res #custom prop camObj = bpy.data.objects.new(name=self.name, object_data=cam) scn.collection.objects.link(camObj) scn.camera = camObj elif self.redo == 1: #first exec, get initial camera res scn.camera = camObj try: self.target_res = cam['mapRes'] except KeyError: self.report({'ERROR'}, "This camera has not map resolution property") return {'CANCELLED'} else: #following exec, set camera res in redo panel try: cam['mapRes'] = self.target_res except KeyError: self.report({'ERROR'}, "This camera has not map resolution property") return {'CANCELLED'} #Set camera data cam.type = 'ORTHO' cam.ortho_scale = max((dimx, dimy)) #ratio = max((dimx, dimy)) / min((dimx, dimy)) #General offset used to set cam z loc and clip end distance #needed to avoid clipping/black hole effects offset = dimz * self.zLocOffset/100 #Set camera location camLocZ = bbox['zmin'] + dimz + offset camObj.location = (locx, locy, camLocZ) #Set camera clipping cam.clip_start = 0 cam.clip_end = dimz + offset*2 cam.show_limits = True if not newCam: if self.redo == 1:#first exec, get initial camera name self.name = camObj.name else:#following exec, set camera name in redo panel camObj.name = self.name camObj.data.name = self.name #Update selection bpy.ops.object.select_all(action='DESELECT') camObj.select_set(True) context.view_layer.objects.active = camObj #setup scene scn.camera = camObj scn.render.resolution_x = int(dimx / self.target_res) scn.render.resolution_y = int(dimy / self.target_res) scn.render.resolution_percentage = 100 #Write wf res = self.target_res#dimx / scene.render.resolution_x rot = 0 x = bbox['xmin'] + dx y = bbox['ymax'] + dy wf_data = '\n'.join(map(str, [res, rot, rot, -res, x+res/2, y-res/2])) wf_name = camObj.name + '.wld' if wf_name in bpy.data.texts: wfText = bpy.data.texts[wf_name] wfText.clear() else: wfText = bpy.data.texts.new(name=wf_name) wfText.write(wf_data) #Purge old wf text for wfText in bpy.data.texts: name, ext = wfText.name[:-4], wfText.name[-4:] if ext == '.wld' and name not in bpy.data.objects: bpy.data.texts.remove(wfText) return {'FINISHED'} def register(): try: bpy.utils.register_class(CAMERA_OT_add_georender_cam) except ValueError as e: log.warning('{} is already registered, now unregister and retry... '.format(CAMERA_OT_add_georender_cam)) unregister() bpy.utils.register_class(CAMERA_OT_add_georender_cam) def unregister(): bpy.utils.unregister_class(CAMERA_OT_add_georender_cam) ================================================ FILE: operators/io_export_shp.py ================================================ # -*- coding:utf-8 -*- import os import bpy import bmesh import mathutils import logging log = logging.getLogger(__name__) from ..core.lib.shapefile import Writer as shpWriter from ..core.lib.shapefile import POINTZ, POLYLINEZ, POLYGONZ, MULTIPOINTZ from bpy_extras.io_utils import ExportHelper #helper class defines filename and invoke() function which calls the file selector from bpy.props import StringProperty, BoolProperty, EnumProperty, IntProperty from bpy.types import Operator from ..geoscene import GeoScene from ..core.proj import SRS class EXPORTGIS_OT_shapefile(Operator, ExportHelper): """Export from ESRI shapefile file format (.shp)""" bl_idname = "exportgis.shapefile" # important since its how bpy.ops.import.shapefile is constructed (allows calling operator from python console or another script) #bl_idname rules: must contain one '.' (dot) charactere, no capital letters, no reserved words (like 'import') bl_description = 'export to ESRI shapefile file format (.shp)' bl_label = "Export SHP" bl_options = {"UNDO"} # ExportHelper class properties filename_ext = ".shp" filter_glob: StringProperty( default = "*.shp", options = {'HIDDEN'}, ) exportType: EnumProperty( name = "Feature type", description = "Select feature type", items = [ ('POINTZ', 'Point', ""), ('POLYLINEZ', 'Line', ""), ('POLYGONZ', 'Polygon', "") ]) objectsSource: EnumProperty( name = "Objects", description = "Objects to export", items = [ ('COLLEC', 'Collection', "Export a collection of objects"), ('SELECTED', 'Selected objects', "Export the current selection") ], default = 'SELECTED' ) def listCollections(self, context): return [(c.name, c.name, "Collection") for c in bpy.data.collections] selectedColl: EnumProperty( name = "Collection", description = "Select the collection to export", items = listCollections) mode: EnumProperty( name = "Mode", description = "Select the export strategy", items = [ ('OBJ2FEAT', 'Objects to features', "Create one multipart feature per object"), ('MESH2FEAT', 'Mesh to features', "Decompose mesh primitives to separate features") ], default = 'OBJ2FEAT' ) @classmethod def poll(cls, context): return context.mode == 'OBJECT' def draw(self, context): #Function used by blender to draw the panel. layout = self.layout layout.prop(self, 'objectsSource') if self.objectsSource == 'COLLEC': layout.prop(self, 'selectedColl') layout.prop(self, 'mode') layout.prop(self, 'exportType') def execute(self, context): filePath = self.filepath folder = os.path.dirname(filePath) scn = context.scene geoscn = GeoScene(scn) if geoscn.isGeoref: dx, dy = geoscn.getOriginPrj() crs = SRS(geoscn.crs) try: wkt = crs.getWKT() except Exception as e: log.warning('Cannot convert crs to wkt', exc_info=True) wkt = None elif geoscn.isBroken: self.report({'ERROR'}, "Scene georef is broken, please fix it beforehand") return {'CANCELLED'} else: dx, dy = (0, 0) wkt = None if self.objectsSource == 'SELECTED': objects = [obj for obj in bpy.context.selected_objects if obj.type == 'MESH'] elif self.objectsSource == 'COLLEC': objects = bpy.data.collections[self.selectedColl].all_objects objects = [obj for obj in objects if obj.type == 'MESH'] if not objects: self.report({'ERROR'}, "Selection is empty or does not contain any mesh") return {'CANCELLED'} outShp = shpWriter(filePath) if self.exportType == 'POLYGONZ': outShp.shapeType = POLYGONZ #15 if self.exportType == 'POLYLINEZ': outShp.shapeType = POLYLINEZ #13 if self.exportType == 'POINTZ' and self.mode == 'MESH2FEAT': outShp.shapeType = POINTZ if self.exportType == 'POINTZ' and self.mode == 'OBJ2FEAT': outShp.shapeType = MULTIPOINTZ #create fields (all needed fields sould be created before adding any new record) #TODO more robust evaluation, and check for boolean and date types cLen = 255 #string fields default length nLen = 20 #numeric fields default length dLen = 5 #numeric fields default decimal precision maxFieldNameLen = 8 #shp capabilities limit field name length to 8 characters outShp.field('objId','N', nLen) #export id for obj in objects: for k, v in obj.items(): k = k[0:maxFieldNameLen] #evaluate the field type with the first value if k not in [f[0] for f in outShp.fields]: if isinstance(v, float) or isinstance(v, int): fieldType = 'N' elif isinstance(v, str): if v.lstrip("-+").isdigit(): v = int(v) fieldType = 'N' else: try: v = float(v) except ValueError: fieldType = 'C' else: fieldType = 'N' else: continue if fieldType == 'C': outShp.field(k, fieldType, cLen) elif fieldType == 'N': if isinstance(v, int): outShp.field(k, fieldType, nLen, 0) else: outShp.field(k, fieldType, nLen, dLen) for i, obj in enumerate(objects): loc = obj.location bm = bmesh.new() bm.from_object(obj, context.evaluated_depsgraph_get()) #bmesh.from_object 'deform=True' arg allows to consider modifier deformation ->> deprecated since Blender 3.0 bm.transform(obj.matrix_world) nFeat = 1 if self.exportType == 'POINTZ': if len(bm.verts) == 0: continue #Extract coords & adjust values against georef deltas pts = [[v.co.x+dx, v.co.y+dy, v.co.z] for v in bm.verts] if self.mode == 'MESH2FEAT': for j, pt in enumerate(pts): outShp.pointz(*pt) nFeat = len(pts) elif self.mode == 'OBJ2FEAT': outShp.multipointz(pts) if self.exportType == 'POLYLINEZ': if len(bm.edges) == 0: continue lines = [] for edge in bm.edges: #Extract coords & adjust values against georef deltas line = [(vert.co.x+dx, vert.co.y+dy, vert.co.z) for vert in edge.verts] lines.append(line) if self.mode == 'MESH2FEAT': for j, line in enumerate(lines): outShp.linez([line]) nFeat = len(lines) elif self.mode == 'OBJ2FEAT': outShp.linez(lines) if self.exportType == 'POLYGONZ': if len(bm.faces) == 0: continue #build geom polygons = [] for face in bm.faces: #Extract coords & adjust values against georef deltas poly = [(vert.co.x+dx, vert.co.y+dy, vert.co.z) for vert in face.verts] poly.append(poly[0])#close poly #In Blender face is up if points are in anticlockwise order #for shapefiles, face's up with clockwise order poly.reverse() polygons.append(poly) if self.mode == 'MESH2FEAT': for j, polygon in enumerate(polygons): outShp.polyz([polygon]) nFeat = len(polygons) elif self.mode == 'OBJ2FEAT': outShp.polyz(polygons) #Writing attributes Data attributes = {'objId':i} for k, v in obj.items(): k = k[0:maxFieldNameLen] if not any([f[0] == k for f in outShp.fields]): continue fType = next( (f[1] for f in outShp.fields if f[0] == k) ) if fType in ('N', 'F'): try: v = float(v) except ValueError: log.info('Cannot cast value {} to float for appending field {}, NULL value will be inserted instead'.format(v, k)) v = None attributes[k] = v #assign None to orphans shp fields (if the key does not exists in the custom props of this object) attributes.update({f[0]:None for f in outShp.fields if f[0] not in attributes.keys()}) #Write for n in range(nFeat): outShp.record(**attributes) outShp.close() if wkt is not None: prjPath = os.path.splitext(filePath)[0] + '.prj' prj = open(prjPath, "w") prj.write(wkt) prj.close() self.report({'INFO'}, "Export complete") return {'FINISHED'} def register(): try: bpy.utils.register_class(EXPORTGIS_OT_shapefile) except ValueError as e: log.warning('{} is already registered, now unregister and retry... '.format(EXPORTGIS_OT_shapefile)) unregister() bpy.utils.register_class(EXPORTGIS_OT_shapefile) def unregister(): bpy.utils.unregister_class(EXPORTGIS_OT_shapefile) ================================================ FILE: operators/io_get_dem.py ================================================ import os import time import logging log = logging.getLogger(__name__) from urllib.request import Request, urlopen from urllib.error import URLError, HTTPError import bpy import bmesh from bpy.types import Operator, Panel, AddonPreferences from bpy.props import StringProperty, IntProperty, FloatProperty, BoolProperty, EnumProperty, FloatVectorProperty from ..geoscene import GeoScene from .utils import adjust3Dview, getBBOX, isTopView from ..core.proj import SRS, reprojBbox from ..core import settings USER_AGENT = settings.user_agent PKG, SUBPKG = __package__.split('.', maxsplit=1) TIMEOUT = 120 class IMPORTGIS_OT_dem_query(Operator): """Import elevation data from a web service""" bl_idname = "importgis.dem_query" bl_description = 'Query for elevation data from a web service' bl_label = "Get elevation (SRTM)" bl_options = {"UNDO"} def invoke(self, context, event): #check georef geoscn = GeoScene(context.scene) if not geoscn.isGeoref: self.report({'ERROR'}, "Scene is not georef") return {'CANCELLED'} if geoscn.isBroken: self.report({'ERROR'}, "Scene georef is broken, please fix it beforehand") return {'CANCELLED'} #return self.execute(context) return context.window_manager.invoke_props_dialog(self)#, width=350) def draw(self,context): prefs = context.preferences.addons[PKG].preferences layout = self.layout row = layout.row(align=True) row.prop(prefs, "demServer", text='Server') if 'opentopography' in prefs.demServer: row = layout.row(align=True) row.prop(prefs, "opentopography_api_key", text='Api Key') @classmethod def poll(cls, context): return context.mode == 'OBJECT' def execute(self, context): prefs = bpy.context.preferences.addons[PKG].preferences scn = context.scene geoscn = GeoScene(scn) crs = SRS(geoscn.crs) #Validate selection objs = bpy.context.selected_objects aObj = context.active_object if len(objs) == 1 and aObj.type == 'MESH': onMesh = True bbox = getBBOX.fromObj(aObj).toGeo(geoscn) elif isTopView(context): onMesh = False bbox = getBBOX.fromTopView(context).toGeo(geoscn) else: self.report({'ERROR'}, "Please define the query extent in orthographic top view or by selecting a reference object") return {'CANCELLED'} if bbox.dimensions.x > 1000000 or bbox.dimensions.y > 1000000: self.report({'ERROR'}, "Too large extent") return {'CANCELLED'} bbox = reprojBbox(geoscn.crs, 4326, bbox) if 'SRTM' in prefs.demServer: if bbox.ymin > 60: self.report({'ERROR'}, "SRTM is not available beyond 60 degrees north") return {'CANCELLED'} if bbox.ymax < -56: self.report({'ERROR'}, "SRTM is not available below 56 degrees south") return {'CANCELLED'} if 'opentopography' in prefs.demServer: if not prefs.opentopography_api_key: self.report({'ERROR'}, "Please register to opentopography.org and request for an API key") return {'CANCELLED'} #Set cursor representation to 'loading' icon w = context.window w.cursor_set('WAIT') #url template #http://opentopo.sdsc.edu/otr/getdem?demtype=SRTMGL3&west=-120.168457&south=36.738884&east=-118.465576&north=38.091337&outputFormat=GTiff e = 0.002 #opentopo service does not always respect the entire bbox, so request for a little more xmin, xmax = bbox.xmin - e, bbox.xmax + e ymin, ymax = bbox.ymin - e, bbox.ymax + e url = prefs.demServer.format(W=xmin, E=xmax, S=ymin, N=ymax, API_KEY=prefs.opentopography_api_key) log.debug(url) # Download the file from url and save it locally # opentopo return a geotiff object in wgs84 if bpy.data.is_saved: filePath = os.path.join(os.path.dirname(bpy.data.filepath), 'srtm.tif') else: filePath = os.path.join(bpy.app.tempdir, 'srtm.tif') #we can directly init NpImg from blob but if gdal is not used as image engine then georef will not be extracted #Alternatively, we can save on disk, open with GeoRaster class (will use tyf if gdal not available) rq = Request(url, headers={'User-Agent': USER_AGENT}) try: with urlopen(rq, timeout=TIMEOUT) as response, open(filePath, 'wb') as outFile: data = response.read() # a `bytes` object outFile.write(data) # except (URLError, HTTPError) as err: log.error('Http request fails url:{}, code:{}, error:{}'.format(url, getattr(err, 'code', None), err.reason)) self.report({'ERROR'}, "Cannot reach OpenTopography web service, check logs for more infos") return {'CANCELLED'} except TimeoutError: log.error('Http request does not respond. url:{}, code:{}, error:{}'.format(url, getattr(err, 'code', None), err.reason)) info = "Cannot reach SRTM web service provider, server can be down or overloaded. Please retry later" log.info(info) self.report({'ERROR'}, info) return {'CANCELLED'} if not onMesh: bpy.ops.importgis.georaster( 'EXEC_DEFAULT', filepath = filePath, reprojection = True, rastCRS = 'EPSG:4326', importMode = 'DEM', subdivision = 'subsurf', demInterpolation = True) else: bpy.ops.importgis.georaster( 'EXEC_DEFAULT', filepath = filePath, reprojection = True, rastCRS = 'EPSG:4326', importMode = 'DEM', subdivision = 'subsurf', demInterpolation = True, demOnMesh = True, objectsLst = [str(i) for i, obj in enumerate(scn.collection.all_objects) if obj.name == bpy.context.active_object.name][0], clip = False, fillNodata = False) bbox = getBBOX.fromScn(scn) adjust3Dview(context, bbox, zoomToSelect=False) return {'FINISHED'} def register(): try: bpy.utils.register_class(IMPORTGIS_OT_dem_query) except ValueError as e: log.warning('{} is already registered, now unregister and retry... '.format(IMPORTGIS_OT_srtm_query)) unregister() bpy.utils.register_class(IMPORTGIS_OT_dem_query) def unregister(): bpy.utils.unregister_class(IMPORTGIS_OT_dem_query) ================================================ FILE: operators/io_import_asc.py ================================================ # Derived from https://github.com/hrbaer/Blender-ASCII-Grid-Import import re import os import string import bpy import math import string import logging log = logging.getLogger(__name__) from bpy_extras.io_utils import ImportHelper #helper class defines filename and invoke() function which calls the file selector from bpy.props import StringProperty, BoolProperty, EnumProperty, IntProperty from bpy.types import Operator from ..core.proj import Reproj from ..core.utils import XY from ..geoscene import GeoScene, georefManagerLayout from ..prefs import PredefCRS from .utils import bpyGeoRaster as GeoRaster from .utils import placeObj, adjust3Dview, showTextures, addTexture, getBBOX from .utils import rasterExtentToMesh, geoRastUVmap, setDisplacer PKG, SUBPKG = __package__.split('.', maxsplit=1) class IMPORTGIS_OT_ascii_grid(Operator, ImportHelper): """Import ESRI ASCII grid file""" bl_idname = "importgis.asc_file" # important since its how bpy.ops.importgis.asc is constructed (allows calling operator from python console or another script) #bl_idname rules: must contain one '.' (dot) charactere, no capital letters, no reserved words (like 'import') bl_description = 'Import ESRI ASCII grid with world file' bl_label = "Import ASCII Grid" bl_options = {"UNDO"} # ImportHelper class properties filter_glob: StringProperty( default="*.asc;*.grd", options={'HIDDEN'}, ) # Raster CRS definition def listPredefCRS(self, context): return PredefCRS.getEnumItems() fileCRS: EnumProperty( name = "CRS", description = "Choose a Coordinate Reference System", items = listPredefCRS, ) # List of operator properties, the attributes will be assigned # to the class instance from the operator settings before calling. importMode: EnumProperty( name = "Mode", description = "Select import mode", items = [ ('MESH', 'Mesh', "Create triangulated regular network mesh"), ('CLOUD', 'Point cloud', "Create vertex point cloud"), ], ) # Step makes point clouds with billions of points possible to read on consumer hardware step: IntProperty( name = "Step", description = "Only read every Nth point for massive point clouds", default = 1, min = 1 ) # Let the user decide whether to use the faster newline method # Alternatively, use self.total_newlines(filename) to see whether total >= nrows and automatically decide (at the cost of time spent counting lines) newlines: BoolProperty( name = "Newline-delimited rows", description = "Use this method if the file contains newline separated rows for faster import", default = True, ) def draw(self, context): #Function used by blender to draw the panel. layout = self.layout layout.prop(self, 'importMode') layout.prop(self, 'step') layout.prop(self, 'newlines') row = layout.row(align=True) split = row.split(factor=0.35, align=True) split.label(text='CRS:') split.prop(self, "fileCRS", text='') row.operator("bgis.add_predef_crs", text='', icon='ADD') scn = bpy.context.scene geoscn = GeoScene(scn) if geoscn.isPartiallyGeoref: georefManagerLayout(self, context) def total_lines(self, filename): """ Count newlines in file. 512MB file ~3 seconds. """ with open(filename) as f: lines = 0 for _ in f: lines += 1 return lines def read_row_newlines(self, f, ncols): """ Read a row by columns separated by newline. """ return f.readline().split() def read_row_whitespace(self, f, ncols): """ Read a row by columns separated by whitespace (including newlines). 6x slower than readlines() method but faster than any other method I can come up with. See commit 4d337c4 for alternatives. """ # choose a buffer that requires the least reads, but not too much memory (32MB max) # cols * 6 allows us 5 chars plus space, approximating values such as '12345', '-1234', '12.34', '-12.3' buf_size = min(1024 * 32, ncols * 6) row = [] read_f = f.read while True: chunk = read_f(buf_size) # assuming we read a complete chunk, remove end of string up to last whitespace to avoid partial values # if the chunk is smaller than our buffer size, then we've read to the end of file and # can skip truncating the chunk since we know the last value will be complete if len(chunk) == buf_size: for i in range(len(chunk) - 1, -1, -1): if chunk[i].isspace(): f.seek(f.tell() - (len(chunk) - i)) chunk = chunk[:i] break # either read was EOF or chunk was all whitespace if not chunk: return row # eof without reaching ncols? # find each value separated by any whitespace char for m in re.finditer('([^\s]+)', chunk): row.append(m.group(0)) if len(row) == ncols: # completed a row within this chunk, rewind the position to start at the beginning of the next row f.seek(f.tell() - (len(chunk) - m.end())) return row @classmethod def poll(cls, context): return context.mode == 'OBJECT' def execute(self, context): prefs = context.preferences.addons[PKG].preferences bpy.ops.object.select_all(action='DESELECT') #Get scene and some georef data scn = bpy.context.scene geoscn = GeoScene(scn) if geoscn.isBroken: self.report({'ERROR'}, "Scene georef is broken, please fix it beforehand") return {'CANCELLED'} if geoscn.isGeoref: dx, dy = geoscn.getOriginPrj() scale = geoscn.scale #TODO if not geoscn.hasCRS: try: geoscn.crs = self.fileCRS except Exception as e: log.error("Cannot set scene crs", exc_info=True) self.report({'ERROR'}, "Cannot set scene crs, check logs for more infos") return {'CANCELLED'} #build reprojector objects if geoscn.crs != self.fileCRS: rprj = True rprjToRaster = Reproj(geoscn.crs, self.fileCRS) rprjToScene = Reproj(self.fileCRS, geoscn.crs) else: rprj = False rprjToRaster = None rprjToScene = None #Path filename = self.filepath name = os.path.splitext(os.path.basename(filename))[0] log.info('Importing {}...'.format(filename)) f = open(filename, 'r') meta_re = re.compile('^([^\s]+)\s+([^\s]+)$') # 'abc 123' meta = {} for i in range(6): line = f.readline() m = meta_re.match(line) if m: meta[m.group(1).lower()] = m.group(2) log.debug(meta) # step allows reduction during import, only taking every Nth point step = self.step nrows = int(meta['nrows']) ncols = int(meta['ncols']) cellsize = float(meta['cellsize']) nodata = float(meta['nodata_value']) # options are lower left cell corner, or lower left cell centre reprojection = {} offset = XY(0, 0) if 'xllcorner' in meta: llcorner = XY(float(meta['xllcorner']), float(meta['yllcorner'])) reprojection['from'] = llcorner elif 'xllcenter' in meta: centre = XY(float(meta['xllcenter']), float(meta['yllcenter'])) offset = XY(-cellsize / 2, -cellsize / 2) reprojection['from'] = centre # now set the correct offset for the mesh if rprj: reprojection['to'] = XY(*rprjToScene.pt(*reprojection['from'])) log.debug('{name} reprojected from {from} to {to}'.format(**reprojection, name=name)) else: reprojection['to'] = reprojection['from'] if not geoscn.isGeoref: # use the centre of the imported grid as scene origin (calculate only if grid file specified llcorner) centre = (reprojection['from'].x + offset.x + ((ncols / 2) * cellsize), reprojection['from'].y + offset.y + ((nrows / 2) * cellsize)) if rprj: centre = rprjToScene.pt(*centre) geoscn.setOriginPrj(*centre) dx, dy = geoscn.getOriginPrj() index = 0 vertices = [] faces = [] # determine row read method read = self.read_row_whitespace if self.newlines: read = self.read_row_newlines for y in range(nrows - 1, -1, -step): # spec doesn't require newline separated rows so make it handle a single line of all values coldata = read(f, ncols) if len(coldata) != ncols: log.error('Incorrect number of columns for row {row}. Expected {expected}, got {actual}.'.format(row=nrows-y, expected=ncols, actual=len(coldata))) self.report({'ERROR'}, 'Incorrect number of columns for row, check logs for more infos') return {'CANCELLED'} for i in range(step - 1): _ = read(f, ncols) for x in range(0, ncols, step): # TODO: exclude nodata values (implications for face generation) if not (self.importMode == 'CLOUD' and coldata[x] == nodata): pt = (x * cellsize + offset.x, y * cellsize + offset.y) if rprj: # reproject world-space source coordinate, then transform back to target local-space pt = rprjToScene.pt(pt[0] + reprojection['from'].x, pt[1] + reprojection['from'].y) pt = (pt[0] - reprojection['to'].x, pt[1] - reprojection['to'].y) try: vertices.append(pt + (float(coldata[x]),)) except ValueError as e: 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)) self.report({'ERROR'}, 'Cannot convert value to float') return {'CANCELLED'} if self.importMode == 'MESH': step_ncols = math.ceil(ncols / step) for r in range(0, math.ceil(nrows / step) - 1): for c in range(0, step_ncols - 1): v1 = index v2 = v1 + step_ncols v3 = v2 + 1 v4 = v1 + 1 faces.append((v1, v2, v3, v4)) index += 1 index += 1 # Create mesh me = bpy.data.meshes.new(name) ob = bpy.data.objects.new(name, me) ob.location = (reprojection['to'].x - dx, reprojection['to'].y - dy, 0) # Link object to scene and make active scn = bpy.context.scene scn.collection.objects.link(ob) bpy.context.view_layer.objects.active = ob ob.select_set(True) me.from_pydata(vertices, [], faces) me.update() f.close() if prefs.adjust3Dview: bb = getBBOX.fromObj(ob) adjust3Dview(context, bb) return {'FINISHED'} def register(): try: bpy.utils.register_class(IMPORTGIS_OT_ascii_grid) except ValueError as e: log.warning('{} is already registered, now unregister and retry... '.format(IMPORTGIS_OT_ascii_grid)) unregister() bpy.utils.register_class(IMPORTGIS_OT_ascii_grid) def unregister(): bpy.utils.unregister_class(IMPORTGIS_OT_ascii_grid) ================================================ FILE: operators/io_import_georaster.py ================================================ # -*- coding:utf-8 -*- # This file is part of BlenderGIS # ***** GPL LICENSE BLOCK ***** # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # All rights reserved. # ***** GPL LICENSE BLOCK ***** import bpy import bmesh import os import math from mathutils import Vector import numpy as np#Ship with Blender since 2.70 import logging log = logging.getLogger(__name__) from ..geoscene import GeoScene, georefManagerLayout from ..prefs import PredefCRS from ..core.georaster import GeoRaster from .utils import bpyGeoRaster, exportAsMesh from .utils import placeObj, adjust3Dview, showTextures, addTexture, getBBOX from .utils import rasterExtentToMesh, geoRastUVmap, setDisplacer from ..core import HAS_GDAL if HAS_GDAL: from osgeo import gdal from ..core import XY as xy from ..core.errors import OverlapError from ..core.proj import Reproj from bpy_extras.io_utils import ImportHelper #helper class defines filename and invoke() function which calls the file selector from bpy.props import StringProperty, BoolProperty, EnumProperty, IntProperty from bpy.types import Operator PKG, SUBPKG = __package__.split('.', maxsplit=1) class IMPORTGIS_OT_georaster(Operator, ImportHelper): """Import georeferenced raster (need world file)""" bl_idname = "importgis.georaster" # important since its how bpy.ops.importgis.georaster is constructed (allows calling operator from python console or another script) #bl_idname rules: must contain one '.' (dot) charactere, no capital letters, no reserved words (like 'import') bl_description = 'Import raster georeferenced with world file' bl_label = "Import georaster" bl_options = {"UNDO"} def listObjects(self, context): #Function used to update the objects list (obj_list) used by the dropdown box. objs = [] #list containing tuples of each object for index, object in enumerate(bpy.context.scene.objects): #iterate over all objects if object.type == 'MESH': objs.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 return objs # ImportHelper class properties filter_glob: StringProperty( default="*.tif;*.jpg;*.jpeg;*.png;*.bmp", options={'HIDDEN'}, ) # Raster CRS definition def listPredefCRS(self, context): return PredefCRS.getEnumItems() rastCRS: EnumProperty( name = "Raster CRS", description = "Choose a Coordinate Reference System", items = listPredefCRS, ) reprojection: BoolProperty( name="Specifiy raster CRS", description="Specifiy raster CRS if it's different from scene CRS", default=False ) # List of operator properties, the attributes will be assigned # to the class instance from the operator settings before calling. importMode: EnumProperty( name="Mode", description="Select import mode", items=[ ('PLANE', 'Basemap on new plane', "Place raster texture on new plane mesh"), ('BKG', 'Basemap as background', "Place raster as background image"), ('MESH', 'Basemap on mesh', "UV map raster on an existing mesh"), ('DEM', 'DEM as displacement texture', "Use DEM raster as height texture to wrap a base mesh"), ('DEM_RAW', 'DEM raw data build [slow]', "Import a DEM as pixels points cloud with building faces. Do not use with huge dataset.")] ) # objectsLst: EnumProperty(attr="obj_list", name="Objects", description="Choose object to edit", items=listObjects) # #Subdivise (as DEM option) def listSubdivisionModes(self, context): items = [ ('subsurf', 'Subsurf', "Add a subsurf modifier"), ('none', 'None', "No subdivision")] if not self.demOnMesh: #mesh subdivision method can not be applyed on an existing mesh #this option makes sense only when the mesh is created from scratch items.append(('mesh', 'Mesh', "Create vertices at each pixels")) return items subdivision: EnumProperty( name="Subdivision", description="How to subdivise the plane (dispacer needs vertex to work with)", items=listSubdivisionModes ) # demOnMesh: BoolProperty( name="Apply on existing mesh", description="Use DEM as displacer for an existing mesh", default=False ) # clip: BoolProperty( name="Clip to working extent", description="Use the reference bounding box to clip the DEM", default=False ) # demInterpolation: BoolProperty( name="Smooth relief", description="Use texture interpolation to smooth the resulting terrain", default=True ) # fillNodata: BoolProperty( name="Fill nodata values", description="Interpolate existing nodata values to get an usuable displacement texture", default=False ) # step: IntProperty(name = "Step", default=1, description="Pixel step", min=1) buildFaces: BoolProperty(name="Build faces", default=True, description='Build quad faces connecting pixel point cloud') def draw(self, context): #Function used by blender to draw the panel. layout = self.layout layout.prop(self, 'importMode') scn = bpy.context.scene geoscn = GeoScene(scn) # if self.importMode == 'PLANE': pass # if self.importMode == 'BKG': pass # if self.importMode == 'MESH': if geoscn.isGeoref and len(self.objectsLst) > 0: layout.prop(self, 'objectsLst') else: layout.label(text="There isn't georef mesh to UVmap on") # if self.importMode == 'DEM': layout.prop(self, 'demOnMesh') if self.demOnMesh: if geoscn.isGeoref and len(self.objectsLst) > 0: layout.prop(self, 'objectsLst') layout.prop(self, 'clip') else: layout.label(text="There isn't georef mesh to apply on") layout.prop(self, 'subdivision') layout.prop(self, 'demInterpolation') if self.subdivision == 'mesh': layout.prop(self, 'step') layout.prop(self, 'fillNodata') # if self.importMode == 'DEM_RAW': layout.prop(self, 'buildFaces') layout.prop(self, 'step') layout.prop(self, 'clip') if self.clip: if geoscn.isGeoref and len(self.objectsLst) > 0: layout.prop(self, 'objectsLst') else: layout.label(text="There isn't georef mesh to refer") # if geoscn.isPartiallyGeoref: layout.prop(self, 'reprojection') if self.reprojection: self.crsInputLayout(context) # georefManagerLayout(self, context) else: self.crsInputLayout(context) def crsInputLayout(self, context): layout = self.layout row = layout.row(align=True) split = row.split(factor=0.35, align=True) split.label(text='CRS:') split.prop(self, "rastCRS", text='') row.operator("bgis.add_predef_crs", text='', icon='ADD') @classmethod def poll(cls, context): return context.mode == 'OBJECT' def execute(self, context): prefs = context.preferences.addons[PKG].preferences bpy.ops.object.select_all(action='DESELECT') #Get scene and some georef data scn = bpy.context.scene geoscn = GeoScene(scn) if geoscn.isBroken: self.report({'ERROR'}, "Scene georef is broken, please fix it beforehand") return {'CANCELLED'} scale = geoscn.scale #TODO if geoscn.isGeoref: dx, dy = geoscn.getOriginPrj() if self.reprojection: rastCRS = self.rastCRS else: rastCRS = geoscn.crs else: #if not geoscn.hasCRS rastCRS = self.rastCRS try: geoscn.crs = rastCRS except Exception as e: log.error("Cannot set scene crs", exc_info=True) self.report({'ERROR'}, "Cannot set scene crs, check logs for more infos") return {'CANCELLED'} #Raster reprojection throught UV mapping #build reprojector objects if geoscn.crs != rastCRS: rprj = True rprjToRaster = Reproj(geoscn.crs, rastCRS) rprjToScene = Reproj(rastCRS, geoscn.crs) else: rprj = False rprjToRaster = None rprjToScene = None #Path filePath = self.filepath name = os.path.basename(filePath)[:-4] ###################################### if self.importMode == 'PLANE':#on plane #Load raster try: rast = bpyGeoRaster(filePath) except IOError as e: log.error("Unable to open raster", exc_info=True) self.report({'ERROR'}, "Unable to open raster, check logs for more infos") return {'CANCELLED'} #Get or set georef dx, dy if not geoscn.isGeoref: dx, dy = rast.center.x, rast.center.y if rprj: dx, dy = rprjToScene.pt(dx, dy) geoscn.setOriginPrj(dx, dy) #create a new mesh from raster extent mesh = rasterExtentToMesh(name, rast, dx, dy, reproj=rprjToScene) #place obj obj = placeObj(mesh, name) #UV mapping uvTxtLayer = mesh.uv_layers.new(name='rastUVmap')# Add UV map texture layer geoRastUVmap(obj, uvTxtLayer, rast, dx, dy, reproj=rprjToRaster) # Create material mat = bpy.data.materials.new('rastMat') # Add material to current object obj.data.materials.append(mat) # Add texture to material addTexture(mat, rast.bpyImg, uvTxtLayer, name='rastText') ###################################### if self.importMode == 'BKG':#background if rprj: #TODO, do gdal true reproj self.report({'ERROR'}, "Raster reprojection is not possible in background mode") return {'CANCELLED'} #Load raster try: rast = bpyGeoRaster(filePath) except IOError as e: log.error("Unable to open raster", exc_info=True) self.report({'ERROR'}, "Unable to open raster, check logs for more infos") return {'CANCELLED'} #Check pixel size and rotation if rast.rotation.xy != [0,0]: self.report({'ERROR'}, "Cannot apply a rotation in background image mode") return {'CANCELLED'} if abs(round(rast.pxSize.x, 3)) != abs(round(rast.pxSize.y, 3)): self.report({'ERROR'}, "Background image needs equal pixel size in map units in both x ans y axis") return {'CANCELLED'} # trueSizeX = rast.geoSize.x trueSizeY = rast.geoSize.y ratio = rast.size.x / rast.size.y if geoscn.isGeoref: offx, offy = rast.center.x - dx, rast.center.y - dy else: dx, dy = rast.center.x, rast.center.y geoscn.setOriginPrj(dx, dy) offx, offy = 0, 0 bkg = bpy.data.objects.new(self.name, None) #None will create an empty bkg.empty_display_type = 'IMAGE' bkg.empty_image_depth = 'BACK' bkg.data = rast.bpyImg scn.collection.objects.link(bkg) bkg.empty_display_size = 1 #a size of 1 means image width=1bu bkg.scale = (trueSizeX, trueSizeY*ratio, 1) bkg.location = (offx, offy, 0) bpy.context.view_layer.objects.active = bkg bkg.select_set(True) if prefs.adjust3Dview: adjust3Dview(context, rast.bbox) ###################################### if self.importMode == 'MESH': if not geoscn.isGeoref or len(self.objectsLst) == 0: self.report({'ERROR'}, "There isn't georef mesh to apply on") return {'CANCELLED'} # Get choosen object obj = scn.objects[int(self.objectsLst)] # Select and active this obj obj.select_set(True) context.view_layer.objects.active = obj # Compute projeted bbox (in geographic coordinates system) subBox = getBBOX.fromObj(obj).toGeo(geoscn) if rprj: subBox = rprjToRaster.bbox(subBox) #Load raster try: rast = bpyGeoRaster(filePath, subBoxGeo=subBox) except IOError as e: log.error("Unable to open raster", exc_info=True) self.report({'ERROR'}, "Unable to open raster, check logs for more infos") return {'CANCELLED'} except OverlapError: self.report({'ERROR'}, "Non overlap data") return {'CANCELLED'} # Add UV map texture layer mesh = obj.data uvTxtLayer = mesh.uv_layers.new(name='rastUVmap') uvTxtLayer.active = True # UV mapping geoRastUVmap(obj, uvTxtLayer, rast, dx, dy, reproj=rprjToRaster) # Add material and texture mat = bpy.data.materials.new('rastMat') obj.data.materials.append(mat) addTexture(mat, rast.bpyImg, uvTxtLayer, name='rastText') ###################################### if self.importMode == 'DEM': # Get reference plane if self.demOnMesh: if not geoscn.isGeoref or len(self.objectsLst) == 0: self.report({'ERROR'}, "There isn't georef mesh to apply on") return {'CANCELLED'} # Get choosen object obj = scn.objects[int(self.objectsLst)] mesh = obj.data # Select and active this obj obj.select_set(True) context.view_layer.objects.active = obj # Compute projeted bbox (in geographic coordinates system) subBox = getBBOX.fromObj(obj).toGeo(geoscn) if rprj: subBox = rprjToRaster.bbox(subBox) else: subBox = None # Load raster try: grid = bpyGeoRaster(filePath, subBoxGeo=subBox, clip=self.clip, fillNodata=self.fillNodata, useGDAL=HAS_GDAL, raw=True) except IOError as e: log.error("Unable to open raster", exc_info=True) self.report({'ERROR'}, "Unable to open raster, check logs for more infos") return {'CANCELLED'} except OverlapError: self.report({'ERROR'}, "Non overlap data") return {'CANCELLED'} # If needed, create a new plane object from raster extent if not self.demOnMesh: if not geoscn.isGeoref: dx, dy = grid.center.x, grid.center.y if rprj: dx, dy = rprjToScene.pt(dx, dy) geoscn.setOriginPrj(dx, dy) if self.subdivision == 'mesh':#Mesh cut mesh = exportAsMesh(grid, dx, dy, self.step, reproj=rprjToScene, flat=True) else: mesh = rasterExtentToMesh(name, grid, dx, dy, pxLoc='CENTER', reproj=rprjToScene) #use pixel center to avoid displacement glitch obj = placeObj(mesh, name) subBox = getBBOX.fromObj(obj).toGeo(geoscn) # Add UV map texture layer previousUVmapIdx = mesh.uv_layers.active_index uvTxtLayer = mesh.uv_layers.new(name='demUVmap') #UV mapping geoRastUVmap(obj, uvTxtLayer, grid, dx, dy, reproj=rprjToRaster) #Restore previous uv map if previousUVmapIdx != -1: mesh.uv_layers.active_index = previousUVmapIdx #Make subdivision if self.subdivision == 'subsurf':#Add subsurf modifier if not 'SUBSURF' in [mod.type for mod in obj.modifiers]: subsurf = obj.modifiers.new('DEM', type='SUBSURF') subsurf.subdivision_type = 'SIMPLE' subsurf.levels = 6 subsurf.render_levels = 6 #Set displacer dsp = setDisplacer(obj, grid, uvTxtLayer, interpolation=self.demInterpolation) ###################################### if self.importMode == 'DEM_RAW': # Get reference plane subBox = None if self.clip: if not geoscn.isGeoref or len(self.objectsLst) == 0: self.report({'ERROR'}, "No working extent") return {'CANCELLED'} # Get choosen object obj = scn.objects[int(self.objectsLst)] subBox = getBBOX.fromObj(obj).toGeo(geoscn) if rprj: subBox = rprjToRaster.bbox(subBox) # Load raster try: grid = GeoRaster(filePath, subBoxGeo=subBox, useGDAL=HAS_GDAL) except IOError as e: log.error("Unable to open raster", exc_info=True) self.report({'ERROR'}, "Unable to open raster, check logs for more infos") return {'CANCELLED'} except OverlapError: self.report({'ERROR'}, "Non overlap data") return {'CANCELLED'} if not geoscn.isGeoref: dx, dy = grid.center.x, grid.center.y if rprj: dx, dy = rprjToScene.pt(dx, dy) geoscn.setOriginPrj(dx, dy) mesh = exportAsMesh(grid, dx, dy, self.step, reproj=rprjToScene, subset=self.clip, flat=False, buildFaces=self.buildFaces) obj = placeObj(mesh, name) #grid.unload() ###################################### #Flag if a new object as been created... if self.importMode == 'PLANE' or (self.importMode == 'DEM' and not self.demOnMesh) or self.importMode == 'DEM_RAW': newObjCreated = True else: newObjCreated = False #...if so, maybee we need to adjust 3d view settings to it if newObjCreated and prefs.adjust3Dview: bb = getBBOX.fromObj(obj) adjust3Dview(context, bb) #Force view mode with textures if prefs.forceTexturedSolid: showTextures(context) return {'FINISHED'} def register(): try: bpy.utils.register_class(IMPORTGIS_OT_georaster) except ValueError as e: log.warning('{} is already registered, now unregister and retry... '.format(IMPORTGIS_OT_georaster)) unregister() bpy.utils.register_class(IMPORTGIS_OT_georaster) def unregister(): bpy.utils.unregister_class(IMPORTGIS_OT_georaster) ================================================ FILE: operators/io_import_osm.py ================================================ import os import time import json import random import logging log = logging.getLogger(__name__) import bpy import bmesh from bpy.types import Operator, Panel, AddonPreferences from bpy.props import StringProperty, IntProperty, FloatProperty, BoolProperty, EnumProperty, FloatVectorProperty from .lib.osm import overpy from ..geoscene import GeoScene from .utils import adjust3Dview, getBBOX, DropToGround, isTopView from ..core.proj import Reproj, reprojBbox, reprojPt, utm from ..core.utils import perf_clock from ..core import settings USER_AGENT = settings.user_agent PKG, SUBPKG = __package__.split('.', maxsplit=1) #WARNING: There is a known bug with using an enum property with a callback, Python must keep a reference to the strings returned #https://developer.blender.org/T48873 #https://developer.blender.org/T38489 def getTags(): prefs = bpy.context.preferences.addons[PKG].preferences tags = json.loads(prefs.osmTagsJson) return tags #Global variable that will be seed by getTags() at each operator invoke #then callback of dynamic enum will use this global variable OSMTAGS = [] closedWaysArePolygons = ['aeroway', 'amenity', 'boundary', 'building', 'craft', 'geological', 'historic', 'landuse', 'leisure', 'military', 'natural', 'office', 'place', 'shop' , 'sport', 'tourism'] closedWaysAreExtruded = ['building'] def queryBuilder(bbox, tags=['building', 'highway'], types=['node', 'way', 'relation'], format='json'): ''' QL template syntax : [out:json][bbox:ymin,xmin,ymax,xmax];(node[tag1];node[tag2];((way[tag1];way[tag2];);>;);relation;);out; ''' #s,w,n,e <--> ymin,xmin,ymax,xmax bboxStr = ','.join(map(str, bbox.toLatlon())) if not types: #if no type filter is defined then just select all kind of type types = ['node', 'way', 'relation'] head = "[out:"+format+"][bbox:"+bboxStr+"];" union = '(' #all tagged nodes if 'node' in types: if tags: union += ';'.join( ['node['+tag+']' for tag in tags] ) + ';' else: union += 'node;' #all tagged ways with all their nodes (recurse down) if 'way' in types: union += '((' if tags: union += ';'.join( ['way['+tag+']' for tag in tags] ) + ';);' else: union += 'way;);' union += '>;);' #all relations (no filter tag applied) if 'relation' in types or 'rel' in types: union += 'relation;' union += ')' output = ';out;' qry = head + union + output return qry ######################## def joinBmesh(src_bm, dest_bm): ''' Hack to join a bmesh to another TODO: replace this function by bmesh.ops.duplicate when 'dest' argument will be implemented ''' buff = bpy.data.meshes.new(".temp") src_bm.to_mesh(buff) dest_bm.from_mesh(buff) bpy.data.meshes.remove(buff) class OSM_IMPORT(): """Import from Open Street Map""" def enumTags(self, context): items = [] ##prefs = context.preferences.addons[PKG].preferences ##osmTags = json.loads(prefs.osmTagsJson) #we need to use a global variable as workaround to enum callback bug (T48873, T38489) for tag in OSMTAGS: #put each item in a tuple (key, label, tooltip) items.append( (tag, tag, tag) ) return items filterTags: EnumProperty( name = "Tags", description = "Select tags to include", items = enumTags, options = {"ENUM_FLAG"}) featureType: EnumProperty( name = "Type", description = "Select types to include", items = [ ('node', 'Nodes', 'Request all nodes'), ('way', 'Ways', 'Request all ways'), ('relation', 'Relations', 'Request all relations') ], default = {'way'}, options = {"ENUM_FLAG"} ) # Elevation object def listObjects(self, context): objs = [] for index, object in enumerate(bpy.context.scene.objects): if object.type == 'MESH': #put each object in a tuple (key, label, tooltip) and add this to the objects list objs.append((str(index), object.name, "Object named " + object.name)) return objs objElevLst: EnumProperty( name="Elev. object", description="Choose the mesh from which extract z elevation", items=listObjects ) useElevObj: BoolProperty( name="Elevation from object", description="Get z elevation value from an existing ground mesh", default=False ) separate: BoolProperty(name='Separate objects', description='Warning : can be very slow with lot of features', default=False) buildingsExtrusion: BoolProperty(name='Buildings extrusion', description='', default=True) defaultHeight: FloatProperty(name='Default Height', description='Set the height value using for extrude building when the tag is missing', default=20) levelHeight: FloatProperty(name='Level height', description='Set a height for a building level, using for compute extrude height based on number of levels', default=3) randomHeightThreshold: IntProperty(name='Random height threshold', description='Threshold value for randomize default height', default=0) def draw(self, context): layout = self.layout row = layout.row() row.prop(self, "featureType", expand=True) row = layout.row() col = row.column() col.prop(self, "filterTags", expand=True) layout.prop(self, 'useElevObj') if self.useElevObj: layout.prop(self, 'objElevLst') layout.prop(self, 'buildingsExtrusion') if self.buildingsExtrusion: layout.prop(self, 'defaultHeight') layout.prop(self, 'randomHeightThreshold') layout.prop(self, 'levelHeight') layout.prop(self, 'separate') def build(self, context, result, dstCRS): prefs = context.preferences.addons[PKG].preferences scn = context.scene geoscn = GeoScene(scn) scale = geoscn.scale #TODO #Init reprojector class try: rprj = Reproj(4326, dstCRS) except Exception as e: log.error('Unable to reproject data', exc_info=True) self.report({'ERROR'}, "Unable to reproject data ckeck logs for more infos") return {'FINISHED'} if self.useElevObj: if not self.objElevLst: log.error('There is no elevation object in the scene to get elevation from') self.report({'ERROR'}, "There is no elevation object in the scene to get elevation from") return {'FINISHED'} elevObj = scn.objects[int(self.objElevLst)] rayCaster = DropToGround(scn, elevObj) bmeshes = {} vgroupsObj = {} ####### def seed(id, tags, pts): ''' Sub funtion : 1. create a bmesh from [pts] 2. seed a global bmesh or create a new object ''' if len(pts) > 1: if pts[0] == pts[-1] and any(tag in closedWaysArePolygons for tag in tags): type = 'Areas' closed = True pts.pop() #exclude last duplicate node else: type = 'Ways' closed = False else: type = 'Nodes' closed = False #reproj and shift coords pts = rprj.pts(pts) dx, dy = geoscn.crsx, geoscn.crsy if self.useElevObj: #pts = [rayCaster.rayCast(v[0]-dx, v[1]-dy).loc for v in pts] pts = [rayCaster.rayCast(v[0]-dx, v[1]-dy) for v in pts] hits = [pt.hit for pt in pts] if not all(hits) and any(hits): zs = [p.loc.z for p in pts if p.hit] meanZ = sum(zs) / len(zs) for v in pts: if not v.hit: v.loc.z = meanZ pts = [pt.loc for pt in pts] else: pts = [ (v[0]-dx, v[1]-dy, 0) for v in pts] #Create a new bmesh #>using an intermediate bmesh object allows some extra operation like extrusion bm = bmesh.new() if len(pts) == 1: verts = [bm.verts.new(pt) for pt in pts] elif closed: #faces verts = [bm.verts.new(pt) for pt in pts] face = bm.faces.new(verts) #ensure face is up (anticlockwise order) #because in OSM there is no particular order for closed ways face.normal_update() if face.normal.z < 0: face.normal_flip() if self.buildingsExtrusion and any(tag in closedWaysAreExtruded for tag in tags): offset = None if "height" in tags: htag = tags["height"] htag.replace(',', '.') try: offset = int(htag) except: try: offset = float(htag) except: for i, c in enumerate(htag): if not c.isdigit(): try: offset, unit = float(htag[:i]), htag[i:].strip() #todo : parse unit 25, 25m, 25 ft, etc. except: offset = None elif "building:levels" in tags: try: offset = int(tags["building:levels"]) * self.levelHeight except ValueError as e: offset = None if offset is None: minH = self.defaultHeight - self.randomHeightThreshold if minH < 0 : minH = 0 maxH = self.defaultHeight + self.randomHeightThreshold offset = random.randint(int(minH), int(maxH)) #Extrude """ if self.extrusionAxis == 'NORMAL': normal = face.normal vect = normal * offset elif self.extrusionAxis == 'Z': """ vect = (0, 0, offset) faces = bmesh.ops.extrude_discrete_faces(bm, faces=[face]) #return {'faces': [BMFace]} verts = faces['faces'][0].verts if self.useElevObj: #Making flat roof z = max([v.co.z for v in verts]) + offset #get max z coord for v in verts: v.co.z = z else: bmesh.ops.translate(bm, verts=verts, vec=vect) elif len(pts) > 1: #edge verts = [bm.verts.new(pt) for pt in pts] for i in range(len(pts)-1): edge = bm.edges.new( [verts[i], verts[i+1] ]) if self.separate: name = tags.get('name', str(id)) mesh = bpy.data.meshes.new(name) bm.to_mesh(mesh) mesh.update() mesh.validate() obj = bpy.data.objects.new(name, mesh) #Assign tags to custom props obj['id'] = str(id) #cast to str to avoid overflow error "Python int too large to convert to C int" for key in tags.keys(): obj[key] = tags[key] #Put object in right collection if self.filterTags: tagsList = self.filterTags else: tagsList = OSMTAGS if any(tag in tagsList for tag in tags): for k in tagsList: if k in tags: try: tagCollec = layer.children[k] except KeyError: tagCollec = bpy.data.collections.new(k) layer.children.link(tagCollec) tagCollec.objects.link(obj) break else: layer.objects.link(obj) obj.select_set(True) else: #Grouping bm.verts.index_update() #bm.edges.index_update() #bm.faces.index_update() if self.filterTags: #group by tags (there could be some duplicates) for k in self.filterTags: if k in extags: # objName = type + ':' + k kbm = bmeshes.setdefault(objName, bmesh.new()) offset = len(kbm.verts) joinBmesh(bm, kbm) else: #group all into one unique mesh objName = type _bm = bmeshes.setdefault(objName, bmesh.new()) offset = len(_bm.verts) joinBmesh(bm, _bm) #vertex group name = tags.get('name', None) vidx = [v.index + offset for v in bm.verts] vgroups = vgroupsObj.setdefault(objName, {}) for tag in extags: #if tag in osmTags:#filter if not tag.startswith('name'): vgroup = vgroups.setdefault('Tag:'+tag, []) vgroup.extend(vidx) if name is not None: #vgroup['Name:'+name] = [vidx] vgroup = vgroups.setdefault('Name:'+name, []) vgroup.extend(vidx) if 'relation' in self.featureType: for rel in result.relations: name = rel.tags.get('name', str(rel.id)) for member in rel.members: #todo: remove duplicate members if id == member.ref: vgroup = vgroups.setdefault('Relation:'+name, []) vgroup.extend(vidx) bm.free() ###### if self.separate: layer = bpy.data.collections.new('OSM') context.scene.collection.children.link(layer) #Build mesh waysNodesId = [node.id for way in result.ways for node in way.nodes] if 'node' in self.featureType: for node in result.nodes: #extended tags list extags = list(node.tags.keys()) + [k + '=' + v for k, v in node.tags.items()] if node.id in waysNodesId: continue if self.filterTags and not any(tag in self.filterTags for tag in extags): continue pt = (float(node.lon), float(node.lat)) seed(node.id, node.tags, [pt]) if 'way' in self.featureType: for way in result.ways: extags = list(way.tags.keys()) + [k + '=' + v for k, v in way.tags.items()] if self.filterTags and not any(tag in self.filterTags for tag in extags): continue pts = [(float(node.lon), float(node.lat)) for node in way.nodes] seed(way.id, way.tags, pts) if not self.separate: for name, bm in bmeshes.items(): if prefs.mergeDoubles: bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001) mesh = bpy.data.meshes.new(name) bm.to_mesh(mesh) bm.free() mesh.update()#calc_edges=True) mesh.validate() obj = bpy.data.objects.new(name, mesh) scn.collection.objects.link(obj) obj.select_set(True) vgroups = vgroupsObj.get(name, None) if vgroups is not None: #for vgroupName, vgroupIdx in vgroups.items(): for vgroupName in sorted(vgroups.keys()): vgroupIdx = vgroups[vgroupName] g = obj.vertex_groups.new(name=vgroupName) g.add(vgroupIdx, weight=1, type='ADD') elif 'relation' in self.featureType: relations = bpy.data.collections.new('Relations') bpy.data.collections['OSM'].children.link(relations) importedObjects = bpy.data.collections['OSM'].objects for rel in result.relations: name = rel.tags.get('name', str(rel.id)) try: relation = relations.children[name] #or bpy.data.collections[name] except KeyError: relation = bpy.data.collections.new(name) relations.children.link(relation) for member in rel.members: #todo: remove duplicate members for obj in importedObjects: #id = int(obj.get('id', -1)) try: id = int(obj['id']) except: id = None if id == member.ref: try: relation.objects.link(obj) except Exception as e: log.error('Object {} already in group {}'.format(obj.name, name), exc_info=True) #cleanup if not relation.objects: bpy.data.collections.remove(relation) ####################### class IMPORTGIS_OT_osm_file(Operator, OSM_IMPORT): bl_idname = "importgis.osm_file" bl_description = 'Select and import osm xml file' bl_label = "Import OSM" bl_options = {"UNDO"} # Import dialog properties filepath: StringProperty( name="File Path", description="Filepath used for importing the file", maxlen=1024, subtype='FILE_PATH' ) filename_ext = ".osm" filter_glob: StringProperty( default = "*.osm", options = {'HIDDEN'} ) def invoke(self, context, event): #workaround to enum callback bug (T48873, T38489) global OSMTAGS OSMTAGS = getTags() #open file browser context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} def execute(self, context): scn = context.scene if not os.path.exists(self.filepath): self.report({'ERROR'}, "Invalid file") return{'CANCELLED'} try: bpy.ops.object.mode_set(mode='OBJECT') except: pass bpy.ops.object.select_all(action='DESELECT') #Set cursor representation to 'loading' icon w = context.window w.cursor_set('WAIT') #Spatial ref system geoscn = GeoScene(scn) if geoscn.isBroken: self.report({'ERROR'}, "Scene georef is broken, please fix it beforehand") return {'CANCELLED'} #Parse file t0 = perf_clock() api = overpy.Overpass() #with open(self.filepath, "r", encoding"utf-8") as f: # result = api.parse_xml(f.read()) #WARNING read() load all the file into memory result = api.parse_xml(self.filepath) t = perf_clock() - t0 log.info('File parsed in {} seconds'.format(round(t, 2))) #Get bbox bounds = result.bounds lon = (bounds["minlon"] + bounds["maxlon"])/2 lat = (bounds["minlat"] + bounds["maxlat"])/2 #Set CRS if not geoscn.hasCRS: try: geoscn.crs = utm.lonlat_to_epsg(lon, lat) except Exception as e: log.error("Cannot set UTM CRS", exc_info=True) self.report({'ERROR'}, "Cannot set UTM CRS, ckeck logs for more infos") return {'CANCELLED'} #Set scene origin georef if not geoscn.hasOriginPrj: x, y = reprojPt(4326, geoscn.crs, lon, lat) geoscn.setOriginPrj(x, y) #Build meshes t0 = perf_clock() self.build(context, result, geoscn.crs) t = perf_clock() - t0 log.info('Mesh build in {} seconds'.format(round(t, 2))) bbox = getBBOX.fromScn(scn) adjust3Dview(context, bbox) return{'FINISHED'} ######################## class IMPORTGIS_OT_osm_query(Operator, OSM_IMPORT): """Import from Open Street Map""" bl_idname = "importgis.osm_query" bl_description = 'Query for Open Street Map data covering the current view3d area' bl_label = "Get OSM" bl_options = {"UNDO"} #special function to auto redraw an operator popup called through invoke_props_dialog def check(self, context): return True @classmethod def poll(cls, context): return context.mode == 'OBJECT' def invoke(self, context, event): #workaround to enum callback bug (T48873, T38489) global OSMTAGS OSMTAGS = getTags() return context.window_manager.invoke_props_dialog(self) def execute(self, context): prefs = bpy.context.preferences.addons[PKG].preferences scn = context.scene geoscn = GeoScene(scn) objs = context.selected_objects aObj = context.active_object if not geoscn.isGeoref: self.report({'ERROR'}, "Scene is not georef") return {'CANCELLED'} elif geoscn.isBroken: self.report({'ERROR'}, "Scene georef is broken, please fix it beforehand") return {'CANCELLED'} if len(objs) == 1 and aObj.type == 'MESH': bbox = getBBOX.fromObj(aObj).toGeo(geoscn) elif isTopView(context): bbox = getBBOX.fromTopView(context).toGeo(geoscn) else: self.report({'ERROR'}, "Please define the query extent in orthographic top view or by selecting a reference object") return {'CANCELLED'} if bbox.dimensions.x > 20000 or bbox.dimensions.y > 20000: self.report({'ERROR'}, "Too large extent") return {'CANCELLED'} #Get view3d bbox in lonlat bbox = reprojBbox(geoscn.crs, 4326, bbox) #Set cursor representation to 'loading' icon w = context.window w.cursor_set('WAIT') #Download from overpass api log.debug('Requests overpass server : {}'.format(prefs.overpassServer)) api = overpy.Overpass(overpass_server=prefs.overpassServer, user_agent=USER_AGENT) query = queryBuilder(bbox, tags=list(self.filterTags), types=list(self.featureType), format='xml') log.debug('Overpass query : {}'.format(query)) # can fails with non utf8 chars try: result = api.query(query) except Exception as e: log.error("Overpass query failed", exc_info=True) self.report({'ERROR'}, "Overpass query failed, ckeck logs for more infos.") return {'CANCELLED'} else: log.info('Overpass query successful') self.build(context, result, geoscn.crs) bbox = getBBOX.fromScn(scn) adjust3Dview(context, bbox, zoomToSelect=False) return {'FINISHED'} classes = [ IMPORTGIS_OT_osm_file, IMPORTGIS_OT_osm_query ] def register(): for cls in classes: try: bpy.utils.register_class(cls) except ValueError as e: log.warning('{} is already registered, now unregister and retry... '.format(cls)) bpy.utils.unregister_class(cls) bpy.utils.register_class(cls) def unregister(): for cls in classes: bpy.utils.unregister_class(cls) ================================================ FILE: operators/io_import_shp.py ================================================ # -*- coding:utf-8 -*- import os, sys, time import bpy from bpy.props import StringProperty, BoolProperty, EnumProperty, IntProperty from bpy.types import Operator import bmesh import math from mathutils import Vector import logging log = logging.getLogger(__name__) from ..core.lib.shapefile import Reader as shpReader from ..geoscene import GeoScene, georefManagerLayout from ..prefs import PredefCRS from ..core import BBOX from ..core.proj import Reproj from ..core.utils import perf_clock from .utils import adjust3Dview, getBBOX, DropToGround PKG, SUBPKG = __package__.split('.', maxsplit=1) featureType={ 0:'Null', 1:'Point', 3:'PolyLine', 5:'Polygon', 8:'MultiPoint', 11:'PointZ', 13:'PolyLineZ', 15:'PolygonZ', 18:'MultiPointZ', 21:'PointM', 23:'PolyLineM', 25:'PolygonM', 28:'MultiPointM', 31:'MultiPatch' } """ dbf fields type: C is ASCII characters N is a double precision integer limited to around 18 characters in length D is for dates in the YYYYMMDD format, with no spaces or hyphens between the sections F is for floating point numbers with the same length limits as N L 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). The values it can receive are 1, 0, y, n, Y, N, T, F or the python builtins True and False """ class IMPORTGIS_OT_shapefile_file_dialog(Operator): """Select shp file, loads the fields and start importgis.shapefile_props_dialog operator""" bl_idname = "importgis.shapefile_file_dialog" bl_description = 'Import ESRI shapefile (.shp)' bl_label = "Import SHP" bl_options = {'INTERNAL'} # Import dialog properties filepath: StringProperty( name="File Path", description="Filepath used for importing the file", maxlen=1024, subtype='FILE_PATH' ) filename_ext = ".shp" filter_glob: StringProperty( default = "*.shp", options = {'HIDDEN'} ) def invoke(self, context, event): context.window_manager.fileselect_add(self) return {'RUNNING_MODAL'} def draw(self, context): layout = self.layout layout.label(text="Options will be available") layout.label(text="after selecting a file") def execute(self, context): if os.path.exists(self.filepath): bpy.ops.importgis.shapefile_props_dialog('INVOKE_DEFAULT', filepath=self.filepath) else: self.report({'ERROR'}, "Invalid filepath") return{'FINISHED'} class IMPORTGIS_OT_shapefile_props_dialog(Operator): """Shapefile importer properties dialog""" bl_idname = "importgis.shapefile_props_dialog" bl_description = 'Import ESRI shapefile (.shp)' bl_label = "Import SHP" bl_options = {"INTERNAL"} filepath: StringProperty() #special function to auto redraw an operator popup called through invoke_props_dialog def check(self, context): return True def listFields(self, context): fieldsItems = [] try: shp = shpReader(self.filepath) except Exception as e: log.warning("Unable to read shapefile fields", exc_info=True) return fieldsItems fields = [field for field in shp.fields if field[0] != 'DeletionFlag'] #ignore default DeletionFlag field for i, field in enumerate(fields): #put each item in a tuple (key, label, tooltip) fieldsItems.append( (field[0], field[0], '') ) return fieldsItems # Shapefile CRS definition def listPredefCRS(self, context): return PredefCRS.getEnumItems() def listObjects(self, context): objs = [] for index, object in enumerate(bpy.context.scene.objects): if object.type == 'MESH': #put each object in a tuple (key, label, tooltip) and add this to the objects list objs.append((object.name, object.name, "Object named " + object.name)) return objs reprojection: BoolProperty( name="Specifiy shapefile CRS", description="Specifiy shapefile CRS if it's different from scene CRS", default=False ) shpCRS: EnumProperty( name = "Shapefile CRS", description = "Choose a Coordinate Reference System", items = listPredefCRS) # Elevation source vertsElevSource: EnumProperty( name="Elevation source", description="Select the source of vertices z value", items=[ ('NONE', 'None', "Flat geometry"), ('GEOM', 'Geometry', "Use z value from shape geometry if exists"), ('FIELD', 'Field', "Extract z elevation value from an attribute field"), ('OBJ', 'Object', "Get z elevation value from an existing ground mesh") ], default='GEOM') # Elevation object objElevLst: EnumProperty( name="Elev. object", description="Choose the mesh from which extract z elevation", items=listObjects ) # Elevation field ''' useFieldElev: BoolProperty( name="Elevation from field", description="Extract z elevation value from an attribute field", default=False ) ''' fieldElevName: EnumProperty( name = "Elev. field", description = "Choose field", items = listFields ) #Extrusion field useFieldExtrude: BoolProperty( name="Extrusion from field", description="Extract z extrusion value from an attribute field", default=False ) fieldExtrudeName: EnumProperty( name = "Field", description = "Choose field", items = listFields ) #Extrusion axis extrusionAxis: EnumProperty( name="Extrude along", description="Select extrusion axis", items=[ ('Z', 'z axis', "Extrude along Z axis"), ('NORMAL', 'Normal', "Extrude along normal")] ) #Create separate objects separateObjects: BoolProperty( name="Separate objects", description="Warning : can be very slow with lot of features", default=False ) #Name objects from field useFieldName: BoolProperty( name="Object name from field", description="Extract name for created objects from an attribute field", default=False ) fieldObjName: EnumProperty( name = "Field", description = "Choose field", items = listFields ) def draw(self, context): #Function used by blender to draw the panel. scn = context.scene layout = self.layout # layout.prop(self, 'vertsElevSource') # #layout.prop(self, 'useFieldElev') if self.vertsElevSource == 'FIELD': layout.prop(self, 'fieldElevName') elif self.vertsElevSource == 'OBJ': layout.prop(self, 'objElevLst') # layout.prop(self, 'useFieldExtrude') if self.useFieldExtrude: layout.prop(self, 'fieldExtrudeName') layout.prop(self, 'extrusionAxis') # layout.prop(self, 'separateObjects') if self.separateObjects: layout.prop(self, 'useFieldName') else: self.useFieldName = False if self.separateObjects and self.useFieldName: layout.prop(self, 'fieldObjName') # geoscn = GeoScene() #geoscnPrefs = context.preferences.addons['geoscene'].preferences if geoscn.isPartiallyGeoref: layout.prop(self, 'reprojection') if self.reprojection: self.shpCRSInputLayout(context) # georefManagerLayout(self, context) else: self.shpCRSInputLayout(context) def shpCRSInputLayout(self, context): layout = self.layout row = layout.row(align=True) #row.prop(self, "shpCRS", text='CRS') split = row.split(factor=0.35, align=True) split.label(text='CRS:') split.prop(self, "shpCRS", text='') row.operator("bgis.add_predef_crs", text='', icon='ADD') def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self) def execute(self, context): #elevField = self.fieldElevName if self.useFieldElev else "" elevField = self.fieldElevName if self.vertsElevSource == 'FIELD' else "" extrudField = self.fieldExtrudeName if self.useFieldExtrude else "" nameField = self.fieldObjName if self.useFieldName else "" if self.vertsElevSource == 'OBJ': if not self.objElevLst: self.report({'ERROR'}, "No elevation object") return {'CANCELLED'} else: objElevName = self.objElevLst else: objElevName = '' #will not be used geoscn = GeoScene() if geoscn.isBroken: self.report({'ERROR'}, "Scene georef is broken, please fix it beforehand") return {'CANCELLED'} if geoscn.isGeoref: if self.reprojection: shpCRS = self.shpCRS else: shpCRS = geoscn.crs else: shpCRS = self.shpCRS try: bpy.ops.importgis.shapefile('INVOKE_DEFAULT', filepath=self.filepath, shpCRS=shpCRS, elevSource=self.vertsElevSource, fieldElevName=elevField, objElevName=objElevName, fieldExtrudeName=extrudField, fieldObjName=nameField, extrusionAxis=self.extrusionAxis, separateObjects=self.separateObjects) except Exception as e: log.error('Shapefile import fails', exc_info=True) self.report({'ERROR'}, 'Shapefile import fails, check logs.') return {'CANCELLED'} return{'FINISHED'} class IMPORTGIS_OT_shapefile(Operator): """Import from ESRI shapefile file format (.shp)""" bl_idname = "importgis.shapefile" # important since its how bpy.ops.import.shapefile is constructed (allows calling operator from python console or another script) #bl_idname rules: must contain one '.' (dot) charactere, no capital letters, no reserved words (like 'import') bl_description = 'Import ESRI shapefile (.shp)' bl_label = "Import SHP" bl_options = {"UNDO"} filepath: StringProperty() shpCRS: StringProperty(name = "Shapefile CRS", description = "Coordinate Reference System") elevSource: StringProperty(name = "Elevation source", description = "Elevation source", default='GEOM') # [NONE, GEOM, OBJ, FIELD] objElevName: StringProperty(name = "Elevation object name", description = "") fieldElevName: StringProperty(name = "Elevation field", description = "Field name") fieldExtrudeName: StringProperty(name = "Extrusion field", description = "Field name") fieldObjName: StringProperty(name = "Objects names field", description = "Field name") #Extrusion axis extrusionAxis: EnumProperty( name="Extrude along", description="Select extrusion axis", items=[ ('Z', 'z axis', "Extrude along Z axis"), ('NORMAL', 'Normal', "Extrude along normal")] ) #Create separate objects separateObjects: BoolProperty( name="Separate objects", description="Import to separate objects instead one large object", default=False ) @classmethod def poll(cls, context): return context.mode == 'OBJECT' def __del__(self): bpy.context.window.cursor_set('DEFAULT') def execute(self, context): prefs = bpy.context.preferences.addons[PKG].preferences #Set cursor representation to 'loading' icon w = context.window w.cursor_set('WAIT') t0 = perf_clock() bpy.ops.object.select_all(action='DESELECT') #Path shpName = os.path.basename(self.filepath)[:-4] #Get shp reader log.info("Read shapefile...") try: shp = shpReader(self.filepath) except Exception as e: log.error("Unable to read shapefile", exc_info=True) self.report({'ERROR'}, "Unable to read shapefile, check logs") return {'CANCELLED'} #Check shape type shpType = featureType[shp.shapeType] log.info('Feature type : ' + shpType) if shpType not in ['Point','PolyLine','Polygon','PointZ','PolyLineZ','PolygonZ']: self.report({'ERROR'}, "Cannot process multipoint, multipointZ, pointM, polylineM, polygonM and multipatch feature type") return {'CANCELLED'} if self.elevSource != 'FIELD': self.fieldElevName = '' if self.elevSource == 'OBJ': scn = bpy.context.scene elevObj = scn.objects[self.objElevName] rayCaster = DropToGround(scn, elevObj) #Get fields fields = [field for field in shp.fields if field[0] != 'DeletionFlag'] #ignore default DeletionFlag field fieldsNames = [field[0] for field in fields] log.debug("DBF fields : "+str(fieldsNames)) if self.separateObjects or self.fieldElevName or self.fieldObjName or self.fieldExtrudeName: self.useDbf = True else: self.useDbf = False if self.fieldObjName and self.separateObjects: try: nameFieldIdx = fieldsNames.index(self.fieldObjName) except Exception as e: log.error('Unable to find name field', exc_info=True) self.report({'ERROR'}, "Unable to find name field") return {'CANCELLED'} if self.fieldElevName: try: zFieldIdx = fieldsNames.index(self.fieldElevName) except Exception as e: log.error('Unable to find elevation field', exc_info=True) self.report({'ERROR'}, "Unable to find elevation field") return {'CANCELLED'} if fields[zFieldIdx][1] not in ['N', 'F', 'L'] : self.report({'ERROR'}, "Elevation field do not contains numeric values") return {'CANCELLED'} if self.fieldExtrudeName: try: extrudeFieldIdx = fieldsNames.index(self.fieldExtrudeName) except ValueError: log.error('Unable to find extrusion field', exc_info=True) self.report({'ERROR'}, "Unable to find extrusion field") return {'CANCELLED'} if fields[extrudeFieldIdx][1] not in ['N', 'F', 'L'] : self.report({'ERROR'}, "Extrusion field do not contains numeric values") return {'CANCELLED'} #Get shp and scene georef infos shpCRS = self.shpCRS geoscn = GeoScene() if geoscn.isBroken: self.report({'ERROR'}, "Scene georef is broken, please fix it beforehand") return {'CANCELLED'} scale = geoscn.scale #TODO if not geoscn.hasCRS: #if not geoscn.isGeoref: try: geoscn.crs = shpCRS except Exception as e: log.error("Cannot set scene crs", exc_info=True) self.report({'ERROR'}, "Cannot set scene crs, check logs for more infos") return {'CANCELLED'} #Init reprojector class if geoscn.crs != shpCRS: log.info("Data will be reprojected from {} to {}".format(shpCRS, geoscn.crs)) try: rprj = Reproj(shpCRS, geoscn.crs) except Exception as e: log.error('Reprojection fails', exc_info=True) self.report({'ERROR'}, "Unable to reproject data, check logs for more infos.") return {'CANCELLED'} if rprj.iproj == 'EPSGIO': if shp.numRecords > 100: self.report({'ERROR'}, "Reprojection through online epsg.io engine is limited to 100 features. \nPlease install GDAL or pyproj module.") return {'CANCELLED'} #Get bbox bbox = BBOX(shp.bbox) if geoscn.crs != shpCRS: bbox = rprj.bbox(bbox) #Get or set georef dx, dy if not geoscn.isGeoref: dx, dy = bbox.center geoscn.setOriginPrj(dx, dy) else: dx, dy = geoscn.getOriginPrj() #Get reader iterator (using iterator avoids loading all data in memory) #warn, shp with zero field will return an empty shapeRecords() iterator #to prevent this issue, iter only on shapes if there is no field required if self.useDbf: #Note: using shapeRecord solve the issue where number of shapes does not match number of table records #because it iter only on features with geom and record shpIter = shp.iterShapeRecords() else: shpIter = shp.iterShapes() nbFeats = shp.numRecords #Create an empty BMesh bm = bmesh.new() #Extrusion is exponentially slow with large bmesh #it's fastest to extrude a small bmesh and then join it to a final large bmesh if not self.separateObjects and self.fieldExtrudeName: finalBm = bmesh.new() progress = -1 if self.separateObjects: layer = bpy.data.collections.new(shpName) context.scene.collection.children.link(layer) #Main iteration over features for i, feat in enumerate(shpIter): if self.useDbf: shape = feat.shape record = feat.record else: shape = feat #Progress infos pourcent = round(((i+1)*100)/nbFeats) if pourcent in list(range(0, 110, 10)) and pourcent != progress: progress = pourcent if pourcent == 100: print(str(pourcent)+'%') else: print(str(pourcent), end="%, ") sys.stdout.flush() #we need to flush or it won't print anything until after the loop has finished #Deal with multipart features #If the shape record has multiple parts, the 'parts' attribute will contains the index of #the first point of each part. If there is only one part then a list containing 0 is returned if (shpType == 'PointZ' or shpType == 'Point'): #point layer has no attribute 'parts' partsIdx = [0] else: try: #prevent "_shape object has no attribute parts" error partsIdx = shape.parts except Exception as e: log.warning('Cannot access "parts" attribute for feature {} : {}'.format(i, e)) partsIdx = [0] nbParts = len(partsIdx) #Get list of shape's points pts = shape.points nbPts = len(pts) #Skip null geom if nbPts == 0: continue #go to next iteration of the loop #Reproj geom if geoscn.crs != shpCRS: pts = rprj.pts(pts) #Get extrusion offset if self.fieldExtrudeName: try: offset = float(record[extrudeFieldIdx]) except Exception as e: log.warning('Cannot extract extrusion value for feature {} : {}'.format(i, e)) offset = 0 #null values will be set to zero #Iter over parts for j in range(nbParts): # EXTRACT 3D GEOM geom = [] #will contains a list of 3d points #Find first and last part index idx1 = partsIdx[j] if j+1 == nbParts: idx2 = nbPts else: idx2 = partsIdx[j+1] #Build 3d geom for k, pt in enumerate(pts[idx1:idx2]): if self.elevSource == 'OBJ': rcHit = rayCaster.rayCast(x=pt[0]-dx, y=pt[1]-dy) z = rcHit.loc.z #will be automatically set to zero if not rcHit.hit elif self.elevSource == 'FIELD': try: z = float(record[zFieldIdx]) except Exception as e: log.warning('Cannot extract elevation value for feature {} : {}'.format(i, e)) z = 0 #null values will be set to zero elif shpType[-1] == 'Z' and self.elevSource == 'GEOM': z = shape.z[idx1:idx2][k] else: z = 0 geom.append((pt[0], pt[1], z)) #Shift coords geom = [(pt[0]-dx, pt[1]-dy, pt[2]) for pt in geom] # BUILD BMESH # POINTS if (shpType == 'PointZ' or shpType == 'Point'): vert = [bm.verts.new(pt) for pt in geom] #Extrusion if self.fieldExtrudeName and offset > 0: vect = (0, 0, offset) #along Z result = bmesh.ops.extrude_vert_indiv(bm, verts=vert) verts = result['verts'] bmesh.ops.translate(bm, verts=verts, vec=vect) # LINES if (shpType == 'PolyLine' or shpType == 'PolyLineZ'): verts = [bm.verts.new(pt) for pt in geom] edges = [] for i in range(len(geom)-1): edge = bm.edges.new( [verts[i], verts[i+1] ]) edges.append(edge) #Extrusion if self.fieldExtrudeName and offset > 0: vect = (0, 0, offset) # along Z result = bmesh.ops.extrude_edge_only(bm, edges=edges) verts = [elem for elem in result['geom'] if isinstance(elem, bmesh.types.BMVert)] bmesh.ops.translate(bm, verts=verts, vec=vect) # NGONS if (shpType == 'Polygon' or shpType == 'PolygonZ'): #According to the shapefile spec, polygons points are clockwise and polygon holes are counterclockwise #in Blender face is up if points are in anticlockwise order geom.reverse() #face up geom.pop() #exlude last point because it's the same as first pt if len(geom) >= 3: #needs 3 points to get a valid face verts = [bm.verts.new(pt) for pt in geom] face = bm.faces.new(verts) #update normal to avoid null vector face.normal_update() if face.normal.z < 0: #this is a polygon hole, bmesh cannot handle polygon hole pass #TODO #Extrusion if self.fieldExtrudeName and offset > 0: #build translate vector if self.extrusionAxis == 'NORMAL': normal = face.normal vect = normal * offset elif self.extrusionAxis == 'Z': vect = (0, 0, offset) faces = bmesh.ops.extrude_discrete_faces(bm, faces=[face]) #return {'faces': [BMFace]} verts = faces['faces'][0].verts if self.elevSource == 'OBJ': # Making flat roof (TODO add an user input parameter to setup this behaviour) z = max([v.co.z for v in verts]) + offset #get max z coord for v in verts: v.co.z = z else: ##result = bmesh.ops.extrude_face_region(bm, geom=[face]) #return dict {"geom":[BMVert, BMEdge, BMFace]} ##verts = [elem for elem in result['geom'] if isinstance(elem, bmesh.types.BMVert)] #geom type filter bmesh.ops.translate(bm, verts=verts, vec=vect) if self.separateObjects: if self.fieldObjName: try: name = record[nameFieldIdx] except Exception as e: log.warning('Cannot extract name value for feature {} : {}'.format(i, e)) name = '' # null values will return a bytes object containing a blank string of length equal to fields length definition if isinstance(name, bytes): name = '' else: name = str(name) else: name = shpName #Calc bmesh bbox _bbox = getBBOX.fromBmesh(bm) #Calc bmesh geometry origin and translate coords according to it #then object location will be set to initial bmesh origin #its a work around to bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY') ox, oy, oz = _bbox.center oz = _bbox.zmin bmesh.ops.translate(bm, verts=bm.verts, vec=(-ox, -oy, -oz)) #Create new mesh from bmesh mesh = bpy.data.meshes.new(name) bm.to_mesh(mesh) bm.clear() #Validate new mesh mesh.validate(verbose=False) #Place obj obj = bpy.data.objects.new(name, mesh) layer.objects.link(obj) context.view_layer.objects.active = obj obj.select_set(True) obj.location = (ox, oy, oz) # bpy operators can be very cumbersome when scene contains lot of objects # because it cause implicit scene updates calls # so we must avoid using operators when created many objects with the 'separate objects' option) ##bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY') #write attributes data for i, field in enumerate(shp.fields): fieldName, fieldType, fieldLength, fieldDecLength = field if fieldName != 'DeletionFlag': if fieldType in ('N', 'F'): v = record[i-1] if v is not None: #cast to float to avoid overflow error when affecting custom property obj[fieldName] = float(record[i-1]) else: obj[fieldName] = record[i-1] elif self.fieldExtrudeName: #Join to final bmesh (use from_mesh method hack) buff = bpy.data.meshes.new(".temp") bm.to_mesh(buff) finalBm.from_mesh(buff) bpy.data.meshes.remove(buff) bm.clear() #Write back the whole mesh if not self.separateObjects: mesh = bpy.data.meshes.new(shpName) if self.fieldExtrudeName: bm.free() bm = finalBm if prefs.mergeDoubles: bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=0.0001) bm.to_mesh(mesh) #Finish #mesh.update(calc_edges=True) mesh.validate(verbose=False) #return true if the mesh has been corrected obj = bpy.data.objects.new(shpName, mesh) context.scene.collection.objects.link(obj) context.view_layer.objects.active = obj obj.select_set(True) bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY') #free the bmesh bm.free() t = perf_clock() - t0 log.info('Build in %f seconds' % t) #Adjust grid size if prefs.adjust3Dview: bbox.shift(-dx, -dy) #convert shapefile bbox in 3d view space adjust3Dview(context, bbox) return {'FINISHED'} classes = [ IMPORTGIS_OT_shapefile_file_dialog, IMPORTGIS_OT_shapefile_props_dialog, IMPORTGIS_OT_shapefile ] def register(): for cls in classes: try: bpy.utils.register_class(cls) except ValueError as e: log.warning('{} is already registered, now unregister and retry... '.format(cls)) bpy.utils.unregister_class(cls) bpy.utils.register_class(cls) def unregister(): for cls in classes: bpy.utils.unregister_class(cls) ================================================ FILE: operators/lib/osm/nominatim.py ================================================ import os, ssl import logging log = logging.getLogger(__name__) import json from urllib.request import urlopen from urllib.request import Request from urllib.parse import quote_plus TIMEOUT = 2 def nominatimQuery( query, base_url = 'https://nominatim.openstreetmap.org/', referer = None, user_agent = None, format = 'json', limit = 10): url = base_url + 'search?' url += 'format=' + format url += '&q=' + quote_plus(query) url += '&limit=' + str(limit) log.debug('Nominatim search request : {}'.format(url)) req = Request(url) if referer: req.add_header('Referer', referer) if user_agent: req.add_header('User-Agent', user_agent) response = urlopen(req, timeout=TIMEOUT) r = json.loads(response.read().decode('utf-8')) return r ================================================ FILE: operators/lib/osm/overpy/__about__.py ================================================ __all__ = [ "__author__", "__copyright__", "__email__", "__license__", "__summary__", "__title__", "__uri__", "__version__", ] __title__ = "overpy" __summary__ = "Python Wrapper to access the OpenStreepMap Overpass API" __uri__ = "https://github.com/DinoTools/python-overpy" __version__ = "0.3.1" __author__ = "PhiBo (DinoTools)" __email__ = "" __license__ = "MIT" __copyright__ = "Copyright 2014-2015 %s" % __author__ ================================================ FILE: operators/lib/osm/overpy/__init__.py ================================================ from collections import OrderedDict from decimal import Decimal import re import sys import os from . import exception from .__about__ import ( __author__, __copyright__, __email__, __license__, __summary__, __title__, __uri__, __version__ ) import xml.etree.ElementTree as ET import json PY2 = sys.version_info[0] == 2 PY3 = sys.version_info[0] == 3 if PY2: from StringIO import StringIO from urllib2 import urlopen from urllib2 import HTTPError elif PY3: from io import StringIO from urllib.request import urlopen, Request from urllib.error import HTTPError TIMEOUT = 120 def is_valid_type(element, cls): """ Test if an element is of a given type. :param Element() element: The element instance to test :param Element cls: The element class to test :return: False or True :rtype: Boolean """ return isinstance(element, cls) and element.id is not None class Overpass(object): """ Class to access the Overpass API """ default_read_chunk_size = 4096 def __init__(self, overpass_server="http://overpass-api.de/api/interpreter", read_chunk_size=None, referer=None, user_agent=None): """ :param read_chunk_size: Max size of each chunk read from the server response :type read_chunk_size: Integer """ self.referer = referer self.user_agent = user_agent self.url = overpass_server self._regex_extract_error_msg = re.compile(b"\(?P\") self._regex_remove_tag = re.compile(b"<[^>]*?>") if read_chunk_size is None: read_chunk_size = self.default_read_chunk_size self.read_chunk_size = read_chunk_size def query(self, query): """ Query the Overpass API :param String|Bytes query: The query string in Overpass QL :return: The parsed result :rtype: overpy.Result """ if not isinstance(query, bytes): query = query.encode("utf-8") req = Request(self.url) if self.referer: req.add_header('Referer', self.referer) if self.user_agent: req.add_header('User-Agent', self.user_agent) try: f = urlopen(req, query, timeout=TIMEOUT) except HTTPError as e: f = e response = f.read(self.read_chunk_size) while True: data = f.read(self.read_chunk_size) if len(data) == 0: break response = response + data f.close() if f.code == 200: if PY2: http_info = f.info() content_type = http_info.getheader("content-type") else: content_type = f.getheader("Content-Type") if content_type == "application/json": return self.parse_json(response) if content_type == "application/osm3s+xml": return self.parse_xml(response) raise exception.OverpassUnknownContentType(content_type) if f.code == 400: msgs = [] for msg in self._regex_extract_error_msg.finditer(response): tmp = self._regex_remove_tag.sub(b"", msg.group("msg")) try: tmp = tmp.decode("utf-8") except UnicodeDecodeError: tmp = repr(tmp) msgs.append(tmp) raise exception.OverpassBadRequest( query, msgs=msgs ) if f.code == 429: raise exception.OverpassTooManyRequests if f.code == 504: raise exception.OverpassGatewayTimeout raise exception.OverpassUnknownHTTPStatusCode(f.code) def parse_json(self, data, encoding="utf-8"): """ Parse raw response from Overpass service. :param data: Raw JSON Data :type data: String or Bytes :param encoding: Encoding to decode byte string :type encoding: String :return: Result object :rtype: overpy.Result """ if isinstance(data, bytes): data = data.decode(encoding) data = json.loads(data, parse_float=Decimal) return Result.from_json(data, api=self) def parse_xml(self, data, encoding="utf-8"): """ :param data: Raw XML Data :type data: String or Bytes :param encoding: Encoding to decode byte string :type encoding: String :return: Result object :rtype: overpy.Result """ try: isFile = os.path.exists(data) except: isFile = False if not isFile: if isinstance(data, bytes): data = data.decode(encoding) if PY2 and not isinstance(data, str): # Python 2.x: Convert unicode strings data = data.encode(encoding) return Result.from_xml(data, api=self) class Result(object): """ Class to handle the result. """ def __init__(self, elements=None, api=None): """ :param List elements: :param api: :type api: overpy.Overpass """ if elements is None: elements = [] self._nodes = OrderedDict((element.id, element) for element in elements if is_valid_type(element, Node)) self._ways = OrderedDict((element.id, element) for element in elements if is_valid_type(element, Way)) self._relations = OrderedDict((element.id, element) for element in elements if is_valid_type(element, Relation)) self._class_collection_map = {Node: self._nodes, Way: self._ways, Relation: self._relations} self.api = api self._bounds = {} def expand(self, other): """ Add all elements from an other result to the list of elements of this result object. It is used by the auto resolve feature. :param other: Expand the result with the elements from this result. :type other: overpy.Result :raises ValueError: If provided parameter is not instance of :class:`overpy.Result` """ if not isinstance(other, Result): raise ValueError("Provided argument has to be instance of overpy:Result()") other_collection_map = {Node: other.nodes, Way: other.ways, Relation: other.relations} for element_type, own_collection in self._class_collection_map.items(): for element in other_collection_map[element_type]: if is_valid_type(element, element_type) and element.id not in own_collection: own_collection[element.id] = element def append(self, element): """ Append a new element to the result. :param element: The element to append :type element: overpy.Element """ if is_valid_type(element, Element): self._class_collection_map[element.__class__].setdefault(element.id, element) def get_elements(self, filter_cls, elem_id=None): """ Get a list of elements from the result and filter the element type by a class. :param filter_cls: :param elem_id: ID of the object :type elem_id: Integer :return: List of available elements :rtype: List """ result = [] if elem_id is not None: try: result = [self._class_collection_map[filter_cls][elem_id]] except KeyError: result = [] else: for e in self._class_collection_map[filter_cls].values(): result.append(e) return result def get_ids(self, filter_cls): """ :param filter_cls: :return: """ return list(self._class_collection_map[filter_cls].keys()) def get_node_ids(self): return self.get_ids(filter_cls=Node) def get_way_ids(self): return self.get_ids(filter_cls=Way) def get_relation_ids(self): return self.get_ids(filter_cls=Relation) @classmethod def from_json(cls, data, api=None): """ Create a new instance and load data from json object. :param data: JSON data returned by the Overpass API :type data: Dict :param api: :type api: overpy.Overpass :return: New instance of Result object :rtype: overpy.Result """ result = cls(api=api) for elem_cls in [Node, Way, Relation]: for element in data.get("elements", []): e_type = element.get("type") if hasattr(e_type, "lower") and e_type.lower() == elem_cls._type_value: result.append(elem_cls.from_json(element, result=result)) return result @classmethod def from_xml(cls, data, api=None, iterparse=False): """ Create a new instance and load data from xml object. :param data: Root element :type data: xml.etree.ElementTree.Element :param api: :type api: Overpass :return: New instance of Result object :rtype: Result """ result = cls(api=api) try: isFile = os.path.exists(data) except: isFile = False if not iterparse: #Method 1 : full parsing at once if isFile: with open(data, 'r', encoding='utf-8') as f: data = f.read() #all file in memory root = ET.fromstring(data) for elem_cls in [Node, Way, Relation]: for child in root: if child.tag.lower() == elem_cls._type_value: result.append(elem_cls.from_xml(child, result=result)) else: #Method 2 : iter parsing (memory friendly) #WARNING Issue #198 if not isFile: data = StringIO(data) root = ET.iterparse(data, events=("start", "end")) elem_clss = {'node':Node, 'way':Way, 'relation':Relation} for event, child in root: if event == 'start': if child.tag.lower() == 'bounds': result._bounds = {k:float(v) for k, v in child.attrib.items()} if child.tag.lower() in elem_clss: elem_cls = elem_clss[child.tag.lower()] result.append(elem_cls.from_xml(child, result=result)) elif event == 'end': child.clear() return result def get_node(self, node_id, resolve_missing=False): """ Get a node by its ID. :param node_id: The node ID :type node_id: Integer :param resolve_missing: Query the Overpass API if the node is missing in the result set. :return: The node :rtype: overpy.Node :raises overpy.exception.DataIncomplete: At least one referenced node is not available in the result cache. :raises overpy.exception.DataIncomplete: If resolve_missing is True and at least one node can't be resolved. """ nodes = self.get_nodes(node_id=node_id) if len(nodes) == 0: if not resolve_missing: raise exception.DataIncomplete("Resolve missing nodes is disabled") query = ("\n" "[out:json];\n" "node({node_id});\n" "out body;\n" ) query = query.format( node_id=node_id ) tmp_result = self.api.query(query) self.expand(tmp_result) nodes = self.get_nodes(node_id=node_id) if len(nodes) == 0: raise exception.DataIncomplete("Unable to resolve all nodes") return nodes[0] def get_nodes(self, node_id=None, **kwargs): """ Alias for get_elements() but filter the result by Node() :param node_id: The Id of the node :type node_id: Integer :return: List of elements """ return self.get_elements(Node, elem_id=node_id, **kwargs) def get_relation(self, rel_id, resolve_missing=False): """ Get a relation by its ID. :param rel_id: The relation ID :type rel_id: Integer :param resolve_missing: Query the Overpass API if the relation is missing in the result set. :return: The relation :rtype: overpy.Relation :raises overpy.exception.DataIncomplete: The requested relation is not available in the result cache. :raises overpy.exception.DataIncomplete: If resolve_missing is True and the relation can't be resolved. """ relations = self.get_relations(rel_id=rel_id) if len(relations) == 0: if resolve_missing is False: raise exception.DataIncomplete("Resolve missing relations is disabled") query = ("\n" "[out:json];\n" "relation({relation_id});\n" "out body;\n" ) query = query.format( relation_id=rel_id ) tmp_result = self.api.query(query) self.expand(tmp_result) relations = self.get_relations(rel_id=rel_id) if len(relations) == 0: raise exception.DataIncomplete("Unable to resolve requested reference") return relations[0] def get_relations(self, rel_id=None, **kwargs): """ Alias for get_elements() but filter the result by Relation :param rel_id: Id of the relation :type rel_id: Integer :return: List of elements """ return self.get_elements(Relation, elem_id=rel_id, **kwargs) def get_way(self, way_id, resolve_missing=False): """ Get a way by its ID. :param way_id: The way ID :type way_id: Integer :param resolve_missing: Query the Overpass API if the way is missing in the result set. :return: The way :rtype: overpy.Way :raises overpy.exception.DataIncomplete: The requested way is not available in the result cache. :raises overpy.exception.DataIncomplete: If resolve_missing is True and the way can't be resolved. """ ways = self.get_ways(way_id=way_id) if len(ways) == 0: if resolve_missing is False: raise exception.DataIncomplete("Resolve missing way is disabled") query = ("\n" "[out:json];\n" "way({way_id});\n" "out body;\n" ) query = query.format( way_id=way_id ) tmp_result = self.api.query(query) self.expand(tmp_result) ways = self.get_ways(way_id=way_id) if len(ways) == 0: raise exception.DataIncomplete("Unable to resolve requested way") return ways[0] def get_ways(self, way_id=None, **kwargs): """ Alias for get_elements() but filter the result by Way :param way_id: The Id of the way :type way_id: Integer :return: List of elements """ return self.get_elements(Way, elem_id=way_id, **kwargs) def get_bounds(self): if not self._bounds: lons, lats = zip(*[(e.lon, e.lat) for e in self._nodes.values()]) self._bounds['minlon'] = float(min(lons)) self._bounds['maxlon'] = float(max(lons)) self._bounds['minlat'] = float(min(lats)) self._bounds['maxlat'] = float(max(lats)) return self._bounds node_ids = property(get_node_ids) nodes = property(get_nodes) relation_ids = property(get_relation_ids) relations = property(get_relations) way_ids = property(get_way_ids) ways = property(get_ways) bounds = property(get_bounds) class Element(object): """ Base element """ def __init__(self, attributes=None, result=None, tags=None): """ :param attributes: Additional attributes :type attributes: Dict :param result: The result object this element belongs to :param tags: List of tags :type tags: Dict """ self._result = result self.attributes = attributes self.id = None self.tags = tags class Node(Element): """ Class to represent an element of type node """ _type_value = "node" def __init__(self, node_id=None, lat=None, lon=None, **kwargs): """ :param lat: Latitude :type lat: Decimal or Float :param lon: Longitude :type long: Decimal or Float :param node_id: Id of the node element :type node_id: Integer :param kwargs: Additional arguments are passed directly to the parent class """ Element.__init__(self, **kwargs) self.id = node_id self.lat = lat self.lon = lon def __repr__(self): return "".format(self.id, self.lat, self.lon) @classmethod def from_json(cls, data, result=None): """ Create new Node element from JSON data :param data: Element data from JSON :type data: Dict :param result: The result this element belongs to :type result: overpy.Result :return: New instance of Node :rtype: overpy.Node :raises overpy.exception.ElementDataWrongType: If type value of the passed JSON data does not match. """ if data.get("type") != cls._type_value: raise exception.ElementDataWrongType( type_expected=cls._type_value, type_provided=data.get("type") ) tags = data.get("tags", {}) node_id = data.get("id") lat = data.get("lat") lon = data.get("lon") attributes = {} ignore = ["type", "id", "lat", "lon", "tags"] for n, v in data.items(): if n in ignore: continue attributes[n] = v return cls(node_id=node_id, lat=lat, lon=lon, tags=tags, attributes=attributes, result=result) @classmethod def from_xml(cls, child, result=None): """ Create new way element from XML data :param child: XML node to be parsed :type child: xml.etree.ElementTree.Element :param result: The result this node belongs to :type result: overpy.Result :return: New Way oject :rtype: overpy.Node :raises overpy.exception.ElementDataWrongType: If name of the xml child node doesn't match :raises ValueError: If a tag doesn't have a name """ if child.tag.lower() != cls._type_value: raise exception.ElementDataWrongType( type_expected=cls._type_value, type_provided=child.tag.lower() ) tags = {} for sub_child in child: if sub_child.tag.lower() == "tag": name = sub_child.attrib.get("k") if name is None: raise ValueError("Tag without name/key.") value = sub_child.attrib.get("v") tags[name] = value node_id = child.attrib.get("id") if node_id is not None: node_id = int(node_id) lat = child.attrib.get("lat") if lat is not None: lat = Decimal(lat) lon = child.attrib.get("lon") if lon is not None: lon = Decimal(lon) attributes = {} ignore = ["id", "lat", "lon"] for n, v in child.attrib.items(): if n in ignore: continue attributes[n] = v return cls(node_id=node_id, lat=lat, lon=lon, tags=tags, attributes=attributes, result=result) class Way(Element): """ Class to represent an element of type way """ _type_value = "way" def __init__(self, way_id=None, node_ids=None, **kwargs): """ :param node_ids: List of node IDs :type node_ids: List or Tuple :param way_id: Id of the way element :type way_id: Integer :param kwargs: Additional arguments are passed directly to the parent class """ Element.__init__(self, **kwargs) #: The id of the way self.id = way_id #: List of Ids of the associated nodes self._node_ids = node_ids def __repr__(self): return "".format(self.id, self._node_ids) @property def nodes(self): """ List of nodes associated with the way. """ return self.get_nodes() def get_nodes(self, resolve_missing=False): """ Get the nodes defining the geometry of the way :param resolve_missing: Try to resolve missing nodes. :type resolve_missing: Boolean :return: List of nodes :rtype: List of overpy.Node :raises overpy.exception.DataIncomplete: At least one referenced node is not available in the result cache. :raises overpy.exception.DataIncomplete: If resolve_missing is True and at least one node can't be resolved. """ result = [] resolved = False for node_id in self._node_ids: try: node = self._result.get_node(node_id) except exception.DataIncomplete: node = None if node is not None: result.append(node) continue if not resolve_missing: raise exception.DataIncomplete("Resolve missing nodes is disabled") # We tried to resolve the data but some nodes are still missing if resolved: raise exception.DataIncomplete("Unable to resolve all nodes") query = ("\n" "[out:json];\n" "way({way_id});\n" "node(w);\n" "out body;\n" ) query = query.format( way_id=self.id ) tmp_result = self._result.api.query(query) self._result.expand(tmp_result) resolved = True try: node = self._result.get_node(node_id) except exception.DataIncomplete: node = None if node is None: raise exception.DataIncomplete("Unable to resolve all nodes") result.append(node) return result @classmethod def from_json(cls, data, result=None): """ Create new Way element from JSON data :param data: Element data from JSON :type data: Dict :param result: The result this element belongs to :type result: overpy.Result :return: New instance of Way :rtype: overpy.Way :raises overpy.exception.ElementDataWrongType: If type value of the passed JSON data does not match. """ if data.get("type") != cls._type_value: raise exception.ElementDataWrongType( type_expected=cls._type_value, type_provided=data.get("type") ) tags = data.get("tags", {}) way_id = data.get("id") node_ids = data.get("nodes") attributes = {} ignore = ["id", "nodes", "tags", "type"] for n, v in data.items(): if n in ignore: continue attributes[n] = v return cls(way_id=way_id, attributes=attributes, node_ids=node_ids, tags=tags, result=result) @classmethod def from_xml(cls, child, result=None): """ Create new way element from XML data :param child: XML node to be parsed :type child: xml.etree.ElementTree.Element :param result: The result this node belongs to :type result: overpy.Result :return: New Way oject :rtype: overpy.Way :raises overpy.exception.ElementDataWrongType: If name of the xml child node doesn't match :raises ValueError: If the ref attribute of the xml node is not provided :raises ValueError: If a tag doesn't have a name """ if child.tag.lower() != cls._type_value: raise exception.ElementDataWrongType( type_expected=cls._type_value, type_provided=child.tag.lower() ) tags = {} node_ids = [] for sub_child in child: if sub_child.tag.lower() == "tag": name = sub_child.attrib.get("k") if name is None: raise ValueError("Tag without name/key.") value = sub_child.attrib.get("v") tags[name] = value if sub_child.tag.lower() == "nd": ref_id = sub_child.attrib.get("ref") if ref_id is None: raise ValueError("Unable to find required ref value.") ref_id = int(ref_id) node_ids.append(ref_id) way_id = child.attrib.get("id") if way_id is not None: way_id = int(way_id) attributes = {} ignore = ["id"] for n, v in child.attrib.items(): if n in ignore: continue attributes[n] = v return cls(way_id=way_id, attributes=attributes, node_ids=node_ids, tags=tags, result=result) class Relation(Element): """ Class to represent an element of type relation """ _type_value = "relation" def __init__(self, rel_id=None, members=None, **kwargs): """ :param members: :param rel_id: Id of the relation element :type rel_id: Integer :param kwargs: :return: """ Element.__init__(self, **kwargs) self.id = rel_id self.members = members def __repr__(self): return "".format(self.id) @classmethod def from_json(cls, data, result=None): """ Create new Relation element from JSON data :param data: Element data from JSON :type data: Dict :param result: The result this element belongs to :type result: overpy.Result :return: New instance of Relation :rtype: overpy.Relation :raises overpy.exception.ElementDataWrongType: If type value of the passed JSON data does not match. """ if data.get("type") != cls._type_value: raise exception.ElementDataWrongType( type_expected=cls._type_value, type_provided=data.get("type") ) tags = data.get("tags", {}) rel_id = data.get("id") members = [] supported_members = [RelationNode, RelationWay, RelationRelation] for member in data.get("members", []): type_value = member.get("type") for member_cls in supported_members: if member_cls._type_value == type_value: members.append( member_cls.from_json( member, result=result ) ) attributes = {} ignore = ["id", "members", "tags", "type"] for n, v in data.items(): if n in ignore: continue attributes[n] = v return cls(rel_id=rel_id, attributes=attributes, members=members, tags=tags, result=result) @classmethod def from_xml(cls, child, result=None): """ Create new way element from XML data :param child: XML node to be parsed :type child: xml.etree.ElementTree.Element :param result: The result this node belongs to :type result: overpy.Result :return: New Way oject :rtype: overpy.Relation :raises overpy.exception.ElementDataWrongType: If name of the xml child node doesn't match :raises ValueError: If a tag doesn't have a name """ if child.tag.lower() != cls._type_value: raise exception.ElementDataWrongType( type_expected=cls._type_value, type_provided=child.tag.lower() ) tags = {} members = [] supported_members = [RelationNode, RelationWay, RelationRelation] for sub_child in child: if sub_child.tag.lower() == "tag": name = sub_child.attrib.get("k") if name is None: raise ValueError("Tag without name/key.") value = sub_child.attrib.get("v") tags[name] = value if sub_child.tag.lower() == "member": type_value = sub_child.attrib.get("type") for member_cls in supported_members: if member_cls._type_value == type_value: members.append( member_cls.from_xml( sub_child, result=result ) ) rel_id = child.attrib.get("id") if rel_id is not None: rel_id = int(rel_id) attributes = {} ignore = ["id"] for n, v in child.attrib.items(): if n in ignore: continue attributes[n] = v return cls(rel_id=rel_id, attributes=attributes, members=members, tags=tags, result=result) class RelationMember(object): """ Base class to represent a member of a relation. """ def __init__(self, ref=None, role=None, result=None): """ :param ref: Reference Id :type ref: Integer :param role: The role of the relation member :type role: String :param result: """ self.ref = ref self._result = result self.role = role @classmethod def from_json(cls, data, result=None): """ Create new RelationMember element from JSON data :param child: Element data from JSON :type child: Dict :param result: The result this element belongs to :type result: overpy.Result :return: New instance of RelationMember :rtype: overpy.RelationMember :raises overpy.exception.ElementDataWrongType: If type value of the passed JSON data does not match. """ if data.get("type") != cls._type_value: raise exception.ElementDataWrongType( type_expected=cls._type_value, type_provided=data.get("type") ) ref = data.get("ref") role = data.get("role") return cls(ref=ref, role=role, result=result) @classmethod def from_xml(cls, child, result=None): """ Create new RelationMember from XML data :param child: XML node to be parsed :type child: xml.etree.ElementTree.Element :param result: The result this element belongs to :type result: overpy.Result :return: New relation member oject :rtype: overpy.RelationMember :raises overpy.exception.ElementDataWrongType: If name of the xml child node doesn't match """ if child.attrib.get("type") != cls._type_value: raise exception.ElementDataWrongType( type_expected=cls._type_value, type_provided=child.tag.lower() ) ref = child.attrib.get("ref") if ref is not None: ref = int(ref) role = child.attrib.get("role") return cls(ref=ref, role=role, result=result) class RelationNode(RelationMember): _type_value = "node" def resolve(self, resolve_missing=False): return self._result.get_node(self.ref, resolve_missing=resolve_missing) def __repr__(self): return "".format(self.ref, self.role) class RelationWay(RelationMember): _type_value = "way" def resolve(self, resolve_missing=False): return self._result.get_way(self.ref, resolve_missing=resolve_missing) def __repr__(self): return "".format(self.ref, self.role) class RelationRelation(RelationMember): _type_value = "relation" def resolve(self, resolve_missing=False): return self._result.get_relation(self.ref, resolve_missing=resolve_missing) def __repr__(self): return "".format(self.ref, self.role) ================================================ FILE: operators/lib/osm/overpy/exception.py ================================================ class OverPyException(BaseException): """OverPy base exception""" pass class DataIncomplete(OverPyException): """ Raised if the requested data isn't available in the result. Try to improve the query or to resolve the missing data. """ def __init__(self, *args, **kwargs): OverPyException.__init__( self, "Data incomplete try to improve the query to resolve the missing data", *args, **kwargs ) class ElementDataWrongType(OverPyException): """ Raised if the provided element does not match the expected type. :param type_expected: The expected element type :type type_expected: String :param type_provided: The provided element type :type type_provided: String|None """ def __init__(self, type_expected, type_provided=None): self.type_expected = type_expected self.type_provided = type_provided def __str__(self): return "Type expected '%s' but '%s' provided" % ( self.type_expected, str(self.type_provided) ) class OverpassBadRequest(OverPyException): """ Raised if the Overpass API service returns a syntax error. :param query: The encoded query how it was send to the server :type query: Bytes :param msgs: List of error messages :type msgs: List """ def __init__(self, query, msgs=None): self.query = query if msgs is None: msgs = [] self.msgs = msgs def __str__(self): tmp_msgs = [] for tmp_msg in self.msgs: if not isinstance(tmp_msg, str): tmp_msg = str(tmp_msg) tmp_msgs.append(tmp_msg) return "\n".join(tmp_msgs) class OverpassGatewayTimeout(OverPyException): """ Raised if load of the Overpass API service is too high and it can't handle the request. """ def __init__(self): OverPyException.__init__(self, "Server load too high") class OverpassTooManyRequests(OverPyException): """ Raised if the Overpass API service returns a 429 status code. """ def __init__(self): OverPyException.__init__(self, "Too many requests") class OverpassUnknownContentType(OverPyException): """ Raised if the reported content type isn't handled by OverPy. :param content_type: The reported content type :type content_type: None or String """ def __init__(self, content_type): self.content_type = content_type def __str__(self): if self.content_type is None: return "No content type returned" return "Unknown content type: %s" % self.content_type class OverpassUnknownHTTPStatusCode(OverPyException): """ Raised if the returned HTTP status code isn't handled by OverPy. :param code: The HTTP status code :type code: Integer """ def __init__(self, code): self.code = code def __str__(self): return "Unknown/Unhandled status code: %d" % self.code ================================================ FILE: operators/lib/osm/overpy/helper.py ================================================ __author__ = 'mjob' import overpy def get_street(street, areacode, api=None): """ Retrieve streets in a given bounding area :param overpy.Overpass api: First street of intersection :param String street: Name of street :param String areacode: The OSM id of the bounding area :return: Parsed result :raises overpy.exception.OverPyException: If something bad happens. """ if api is None: api = overpy.Overpass() query = """ area(%s)->.location; ( way[highway][name="%s"](area.location); - ( way[highway=service](area.location); way[highway=track](area.location); ); ); out body; >; out skel qt; """ data = api.query(query % (areacode, street)) return data def get_intersection(street1, street2, areacode, api=None): """ Retrieve intersection of two streets in a given bounding area :param overpy.Overpass api: First street of intersection :param String street1: Name of first street of intersection :param String street2: Name of second street of intersection :param String areacode: The OSM id of the bounding area :return: List of intersections :raises overpy.exception.OverPyException: If something bad happens. """ if api is None: api = overpy.Overpass() query = """ area(%s)->.location; ( way[highway][name="%s"](area.location); node(w)->.n1; way[highway][name="%s"](area.location); node(w)->.n2; ); node.n1.n2; out meta; """ data = api.query(query % (areacode, street1, street2)) return data.get_nodes() ================================================ FILE: operators/mesh_delaunay_voronoi.py ================================================ # -*- coding:utf-8 -*- #import DelaunayVoronoi import bpy import time from .utils import computeVoronoiDiagram, computeDelaunayTriangulation from ..core.utils import perf_clock try: from mathutils.geometry import delaunay_2d_cdt except ImportError: NATIVE = False else: NATIVE = True import logging log = logging.getLogger(__name__) class Point: def __init__(self, x, y, z): self.x, self.y, self.z = x, y, z def unique(L): """Return a list of unhashable elements in s, but without duplicates. [[1, 2], [2, 3], [1, 2]] >>> [[1, 2], [2, 3]]""" #For unhashable objects, you can sort the sequence and then scan from the end of the list, deleting duplicates as you go nDupli=0 nZcolinear=0 L.sort()#sort() brings the equal elements together; then duplicates are easy to weed out in a single pass. last = L[-1] for i in range(len(L)-2, -1, -1): if last[:2] == L[i][:2]:#XY coordinates compararison if last[2] == L[i][2]:#Z coordinates compararison nDupli+=1#duplicates vertices else:#Z colinear nZcolinear+=1 del L[i] else: last = L[i] return (nDupli, nZcolinear)#list data type is mutable, input list will automatically update and doesn't need to be returned def checkEqual(lst): return lst[1:] == lst[:-1] class OBJECT_OT_tesselation_delaunay(bpy.types.Operator): bl_idname = "tesselation.delaunay" #name used to refer to this operator (button) bl_label = "Triangulation" #operator's label bl_description = "Terrain points cloud Delaunay triangulation in 2.5D" #tooltip bl_options = {"UNDO"} def execute(self, context): w = context.window w.cursor_set('WAIT') t0 = perf_clock() #Get selected obj objs = context.selected_objects if len(objs) == 0 or len(objs) > 1: self.report({'INFO'}, "Selection is empty or too much object selected") return {'CANCELLED'} obj = objs[0] if obj.type != 'MESH': self.report({'INFO'}, "Selection isn't a mesh") return {'CANCELLED'} #Get points coodinates #bpy.ops.object.transform_apply(rotation=True, scale=True) r = obj.rotation_euler s = obj.scale mesh = obj.data if NATIVE: ''' Use native Delaunay triangulation function : delaunay_2d_cdt(verts, edges, faces, output_type, epsilon) >> [verts, edges, faces, orig_verts, orig_edges, orig_faces] The 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). Output type : # 0 => triangles with convex hull. # 1 => triangles inside constraints. # 2 => the input constraints, intersected. # 3 => like 2 but with extra edges to make valid BMesh faces. ''' log.info("Triangulate {} points...".format(len(mesh.vertices))) verts, edges, faces, overts, oedges, ofaces = delaunay_2d_cdt([v.co.to_2d() for v in mesh.vertices], [], [], 0, 0.1) verts = [ (v.x, v.y, mesh.vertices[overts[i][0]].co.z) for i, v in enumerate(verts)] #retrieve z values log.info("Getting {} triangles".format(len(faces))) log.info("Create mesh...") tinMesh = bpy.data.meshes.new("TIN") tinMesh.from_pydata(verts, edges, faces) tinMesh.update() else: vertsPts = [vertex.co for vertex in mesh.vertices] #Remove duplicate verts = [[vert.x, vert.y, vert.z] for vert in vertsPts] nDupli, nZcolinear = unique(verts) nVerts = len(verts) log.info("{} duplicates points ignored".format(nDupli)) log.info("{} z colinear points excluded".format(nZcolinear)) if nVerts < 3: self.report({'ERROR'}, "Not enough points") return {'CANCELLED'} #Check colinear xValues = [pt[0] for pt in verts] yValues = [pt[1] for pt in verts] if checkEqual(xValues) or checkEqual(yValues): self.report({'ERROR'}, "Points are colinear") return {'CANCELLED'} #Triangulate log.info("Triangulate {} points...".format(nVerts)) vertsPts = [Point(vert[0], vert[1], vert[2]) for vert in verts] faces = computeDelaunayTriangulation(vertsPts) faces = [tuple(reversed(tri)) for tri in faces]#reverse point order --> if all triangles are specified anticlockwise then all faces up log.info("Getting {} triangles".format(len(faces))) #Create new mesh structure log.info("Create mesh...") tinMesh = bpy.data.meshes.new("TIN") #create a new mesh tinMesh.from_pydata(verts, [], faces) #Fill the mesh with triangles tinMesh.update(calc_edges=True) #Update mesh with new data #Create an object with that mesh tinObj = bpy.data.objects.new("TIN", tinMesh) #Place object tinObj.location = obj.location.copy() tinObj.rotation_euler = r tinObj.scale = s #Update scene context.scene.collection.objects.link(tinObj) #Link object to scene context.view_layer.objects.active = tinObj tinObj.select_set(True) obj.select_set(False) #Report t = round(perf_clock() - t0, 2) msg = "{} triangles created in {} seconds".format(len(faces), t) self.report({'INFO'}, msg) #log.info(msg) #duplicate log return {'FINISHED'} class OBJECT_OT_tesselation_voronoi(bpy.types.Operator): bl_idname = "tesselation.voronoi" #name used to refer to this operator (button) bl_label = "Diagram" #operator's label bl_description = "Points cloud Voronoi diagram in 2D" #tooltip bl_options = {"REGISTER","UNDO"}#need register to draw operator options/redo panel (F6) #options meshType: bpy.props.EnumProperty( items = [("Edges", "Edges", ""), ("Faces", "Faces", "")],#(Key, Label, Description) name = "Mesh type", description = "" ) """ def draw(self, context): """ def execute(self, context): w = context.window w.cursor_set('WAIT') t0 = perf_clock() #Get selected obj objs = context.selected_objects if len(objs) == 0 or len(objs) > 1: self.report({'INFO'}, "Selection is empty or too much object selected") return {'CANCELLED'} obj = objs[0] if obj.type != 'MESH': self.report({'INFO'}, "Selection isn't a mesh") return {'CANCELLED'} #Get points coodinates r = obj.rotation_euler s = obj.scale mesh = obj.data vertsPts = [vertex.co for vertex in mesh.vertices] #Remove duplicate verts = [[vert.x, vert.y, vert.z] for vert in vertsPts] nDupli, nZcolinear = unique(verts) nVerts = len(verts) log.info("{} duplicates points ignored".format(nDupli)) log.info("{} z colinear points excluded".format(nZcolinear)) if nVerts < 3: self.report({'ERROR'}, "Not enough points") return {'CANCELLED'} #Check colinear xValues = [pt[0] for pt in verts] yValues = [pt[1] for pt in verts] if checkEqual(xValues) or checkEqual(yValues): self.report({'ERROR'}, "Points are colinear") return {'CANCELLED'} #Create diagram log.info("Tesselation... ({} points)".format(nVerts)) xbuff, ybuff = 5, 5 # % zPosition = 0 vertsPts = [Point(vert[0], vert[1], vert[2]) for vert in verts] if self.meshType == "Edges": pts, edgesIdx = computeVoronoiDiagram(vertsPts, xbuff, ybuff, polygonsOutput=False, formatOutput=True) else: pts, polyIdx = computeVoronoiDiagram(vertsPts, xbuff, ybuff, polygonsOutput=True, formatOutput=True, closePoly=False) # pts = [[pt[0], pt[1], zPosition] for pt in pts] #Create new mesh structure log.info("Create mesh...") voronoiDiagram = bpy.data.meshes.new("VoronoiDiagram") #create a new mesh if self.meshType == "Edges": voronoiDiagram.from_pydata(pts, edgesIdx, []) #Fill the mesh with triangles else: voronoiDiagram.from_pydata(pts, [], list(polyIdx.values())) #Fill the mesh with triangles voronoiDiagram.update(calc_edges=True) #Update mesh with new data #create an object with that mesh voronoiObj = bpy.data.objects.new("VoronoiDiagram", voronoiDiagram) #place object voronoiObj.location = obj.location.copy() voronoiObj.rotation_euler = r voronoiObj.scale = s #update scene context.scene.collection.objects.link(voronoiObj) #Link object to scene context.view_layer.objects.active = voronoiObj voronoiObj.select_set(True) obj.select_set(False) #Report t = round(perf_clock() - t0, 2) if self.meshType == "Edges": self.report({'INFO'}, "{} edges created in {} seconds".format(len(edgesIdx), t)) else: self.report({'INFO'}, "{} polygons created in {} seconds".format(len(polyIdx), t)) return {'FINISHED'} classes = [ OBJECT_OT_tesselation_delaunay, OBJECT_OT_tesselation_voronoi ] def register(): for cls in classes: try: bpy.utils.register_class(cls) except ValueError as e: log.warning('{} is already registered, now unregister and retry... '.format(cls)) bpy.utils.unregister_class(cls) bpy.utils.register_class(cls) def unregister(): for cls in classes: bpy.utils.unregister_class(cls) ================================================ FILE: operators/mesh_earth_sphere.py ================================================ import bpy from bpy.types import Operator from bpy.props import IntProperty from math import cos, sin, radians, sqrt from mathutils import Vector import logging log = logging.getLogger(__name__) def lonlat2xyz(R, lon, lat): lon, lat = radians(lon), radians(lat) x = R * cos(lat) * cos(lon) y = R * cos(lat) * sin(lon) z = R *sin(lat) return Vector((x, y, z)) class OBJECT_OT_earth_sphere(Operator): bl_idname = "earth.sphere" bl_label = "lonlat to sphere" bl_description = "Transform longitude/latitude data to a sphere like earth globe" bl_options = {"REGISTER", "UNDO"} radius: IntProperty(name = "Radius", default=100, description="Sphere radius", min=1) def execute(self, context): scn = bpy.context.scene objs = bpy.context.selected_objects if not objs: self.report({'INFO'}, "No selected object") return {'CANCELLED'} for obj in objs: if obj.type != 'MESH': log.warning("Object {} is not a mesh".format(obj.name)) continue w, h, thick = obj.dimensions if w > 360: log.warning("Longitude of object {} exceed 360°".format(obj.name)) continue if h > 180: log.warning("Latitude of object {} exceed 180°".format(obj.name)) continue mesh = obj.data m = obj.matrix_world for vertex in mesh.vertices: co = m @ vertex.co lon, lat = co.x, co.y vertex.co = m.inverted() @ lonlat2xyz(self.radius, lon, lat) return {'FINISHED'} EARTH_RADIUS = 6378137 #meters def getZDelta(d): '''delta value for adjusting z across earth curvature http://webhelp.infovista.com/Planet/62/Subsystems/Raster/Content/help/analysis/viewshedanalysis.html''' return sqrt(EARTH_RADIUS**2 + d**2) - EARTH_RADIUS class OBJECT_OT_earth_curvature(Operator): bl_idname = "earth.curvature" bl_label = "Earth curvature correction" bl_description = "Apply earth curvature correction for viewsheed analysis" bl_options = {"REGISTER", "UNDO"} def execute(self, context): scn = bpy.context.scene obj = bpy.context.view_layer.objects.active if not obj: self.report({'INFO'}, "No active object") return {'CANCELLED'} if obj.type != 'MESH': self.report({'INFO'}, "Selection isn't a mesh") return {'CANCELLED'} mesh = obj.data viewpt = scn.cursor.location for vertex in mesh.vertices: d = (viewpt.xy - vertex.co.xy).length vertex.co.z = vertex.co.z - getZDelta(d) return {'FINISHED'} classes = [ OBJECT_OT_earth_sphere, OBJECT_OT_earth_curvature ] def register(): for cls in classes: try: bpy.utils.register_class(cls) except ValueError as e: log.warning('{} is already registered, now unregister and retry... '.format(cls)) bpy.utils.unregister_class(cls) bpy.utils.register_class(cls) def unregister(): for cls in classes: bpy.utils.unregister_class(cls) ================================================ FILE: operators/nodes_terrain_analysis_builder.py ================================================ # -*- coding:utf-8 -*- import math import bpy from bpy.types import Panel, Operator import logging log = logging.getLogger(__name__) from .utils import getBBOX from ..core.maths.interpo import scale class TERRAIN_ANALYSIS_OT_build_nodes(Operator): '''Create material node thee to analysis height, slope and aspect''' bl_idname = "analysis.nodes" bl_description = "Create height, slope and aspect material nodes setup for Cycles" bl_label = "Terrain analysis" def execute(self, context): scn = context.scene scn.render.engine = 'CYCLES' #force Cycles render obj = context.view_layer.objects.active if obj is None: self.report({'ERROR'}, "No active object") return {'CANCELLED'} ####################### #HEIGHT ####################### # Create material heightMatName = 'Height_' + obj.name if heightMatName not in [m.name for m in bpy.data.materials]: heightMat = bpy.data.materials.new(heightMatName) else:#edit existing height material heightMat = bpy.data.materials[heightMatName] heightMat.use_nodes = True heightMat.use_fake_user = True node_tree = heightMat.node_tree node_tree.nodes.clear() # create geometry node (world coordinates) geomNode = node_tree.nodes.new('ShaderNodeNewGeometry') geomNode.location = (-600, 200) # create separate xyz node xyzSplitNode = node_tree.nodes.new('ShaderNodeSeparateXYZ') xyzSplitNode.location = (-400, 200) # #Normalize node group groupsTree = bpy.data.node_groups ''' #make a purge (for testing) for nodeTree in groupsTree: name = nodeTree.name try: groupsTree.remove(nodeTree) print(name+' has been deleted') except: print('cannot delete '+name) ''' if 'Normalize' in [nodeTree.name for nodeTree in groupsTree]: #groupsTree.remove(groupsTree['Normalize']) scaleNodesGroupTree = groupsTree['Normalize'] scaleNodesGroupTree.nodes.clear() scaleNodesGroupTree.inputs.clear() scaleNodesGroupTree.outputs.clear() else: scaleNodesGroupTree = groupsTree.new('Normalize', 'ShaderNodeTree') # = bpy.types.node_tree scaleNodesGroupName = scaleNodesGroupTree.name #Normalize.001 if normalize already exists # group inputs scaleInputsNode = scaleNodesGroupTree.nodes.new('NodeGroupInput') scaleInputsNode.location = (-350,0) scaleNodesGroupTree.inputs.new('NodeSocketFloat','val') scaleNodesGroupTree.inputs.new('NodeSocketFloat','min') scaleNodesGroupTree.inputs.new('NodeSocketFloat','max') # group outputs scaleOutputsNode = scaleNodesGroupTree.nodes.new('NodeGroupOutput') scaleOutputsNode.location = (300,0) scaleNodesGroupTree.outputs.new('NodeSocketFloat','val') # create 3 math nodes in a group subtractNode1 = scaleNodesGroupTree.nodes.new('ShaderNodeMath') subtractNode1.operation = 'SUBTRACT' subtractNode1.location = (-100,100) subtractNode2 = scaleNodesGroupTree.nodes.new('ShaderNodeMath') subtractNode2.operation = 'SUBTRACT' subtractNode2.location = (-100,-100) divideNode = scaleNodesGroupTree.nodes.new('ShaderNodeMath') divideNode.operation = 'DIVIDE' divideNode.location = (100,0) # link nodes scaleNodesGroupTree.links.new(scaleInputsNode.outputs['val'], subtractNode1.inputs[0]) scaleNodesGroupTree.links.new(scaleInputsNode.outputs['min'], subtractNode1.inputs[1]) scaleNodesGroupTree.links.new(scaleInputsNode.outputs['min'], subtractNode2.inputs[1]) scaleNodesGroupTree.links.new(scaleInputsNode.outputs['max'], subtractNode2.inputs[0]) scaleNodesGroupTree.links.new(subtractNode1.outputs[0], divideNode.inputs[0]) scaleNodesGroupTree.links.new(subtractNode2.outputs[0], divideNode.inputs[1]) scaleNodesGroupTree.links.new(divideNode.outputs[0], scaleOutputsNode.inputs['val']) # finally add the group to main node_tree scaleNodeGroup = node_tree.nodes.new('ShaderNodeGroup') scaleNodeGroup.node_tree = bpy.data.node_groups[scaleNodesGroupName]#['Normalize'] scaleNodeGroup.location = (-200, 200) # # create z bbox value nodes bbox = getBBOX.fromObj(obj) zmin = node_tree.nodes.new('ShaderNodeValue') zmin.label = 'zmin ' + obj.name zmin.outputs[0].default_value = bbox['zmin'] zmin.location = (-400,0) zmax = node_tree.nodes.new('ShaderNodeValue') zmax.label = 'zmax ' + obj.name zmax.outputs[0].default_value = bbox['zmax'] zmax.location = (-400,-100) # create color ramp node colorRampNode = node_tree.nodes.new('ShaderNodeValToRGB') colorRampNode.location = (0, 200) cr = colorRampNode.color_ramp cr.elements[0].color = (0,1,0,1) cr.elements[1].color = (1,0,0,1) # Create BSDF diffuse node diffuseNode = node_tree.nodes.new('ShaderNodeBsdfDiffuse') diffuseNode.location = (300, 200) # Create output node outputNode = node_tree.nodes.new('ShaderNodeOutputMaterial') outputNode.location = (500, 200) # Connect the nodes node_tree.links.new(geomNode.outputs['Position'] , xyzSplitNode.inputs['Vector']) node_tree.links.new(xyzSplitNode.outputs['Z'] , scaleNodeGroup.inputs['val']) node_tree.links.new(zmin.outputs[0] , scaleNodeGroup.inputs['min']) node_tree.links.new(zmax.outputs[0] , scaleNodeGroup.inputs['max']) node_tree.links.new(scaleNodeGroup.outputs['val'] , colorRampNode.inputs['Fac']) node_tree.links.new(colorRampNode.outputs['Color'] , diffuseNode.inputs['Color']) node_tree.links.new(diffuseNode.outputs['BSDF'] , outputNode.inputs['Surface']) # Deselect nodes for node in node_tree.nodes: node.select = False #select color ramp colorRampNode.select = True node_tree.nodes.active = colorRampNode ####################### #SLOPE ####################### # Create material slopeMatName = 'Slope' if slopeMatName not in [m.name for m in bpy.data.materials]: slopeMat = bpy.data.materials.new(slopeMatName) else: slopeMat = bpy.data.materials[slopeMatName] slopeMat.use_nodes = True slopeMat.use_fake_user = True node_tree = slopeMat.node_tree node_tree.nodes.clear() ''' # create texture coordinate node (local coordinates) texCoordNode = node_tree.nodes.new('ShaderNodeTexCoord') texCoordNode.location = (-600, 0) ''' # create geometry node (world coordinates) geomNode = node_tree.nodes.new('ShaderNodeNewGeometry') geomNode.location = (-600, 0) # create separate xyz node xyzSplitNode = node_tree.nodes.new('ShaderNodeSeparateXYZ') xyzSplitNode.location = (-400, 0) # create arc-cos node arcCosNode = node_tree.nodes.new('ShaderNodeMath') arcCosNode.operation = 'ARCCOSINE' arcCosNode.location = (-200,0) # create math node to convert radians to degrees rad2dg = node_tree.nodes.new('ShaderNodeMath') rad2dg.operation = 'MULTIPLY' rad2dg.location = (0,0) rad2dg.label = "Radians to degrees" rad2dg.inputs[1].default_value = 180/math.pi # create math node to normalize value normalize = node_tree.nodes.new('ShaderNodeMath') normalize.operation = 'DIVIDE' normalize.location = (200,0) normalize.label = "Normalize" normalize.inputs[1].default_value = 100 # create color ramp node colorRampNode = node_tree.nodes.new('ShaderNodeValToRGB') colorRampNode.location = (400, 0) cr = colorRampNode.color_ramp cr.elements[0].color = (0,1,0,1) cr.elements[1].position = 0.5 cr.elements[1].color = (1,0,0,1) # Create BSDF diffuse node diffuseNode = node_tree.nodes.new('ShaderNodeBsdfDiffuse') diffuseNode.location = (800, 0) # Create output node outputNode = node_tree.nodes.new('ShaderNodeOutputMaterial') outputNode.location = (1000, 0) # Connect the nodes #node_tree.links.new(texCoordNode.outputs['Normal'] , xyzSplitNode.inputs['Vector']) node_tree.links.new(geomNode.outputs['True Normal'] , xyzSplitNode.inputs['Vector']) node_tree.links.new(xyzSplitNode.outputs['Z'] , arcCosNode.inputs[0]) node_tree.links.new(arcCosNode.outputs[0] , rad2dg.inputs[0]) node_tree.links.new(rad2dg.outputs[0] , normalize.inputs[0]) node_tree.links.new(normalize.outputs[0] , colorRampNode.inputs['Fac']) node_tree.links.new(colorRampNode.outputs['Color'] , diffuseNode.inputs['Color']) node_tree.links.new(diffuseNode.outputs['BSDF'] , outputNode.inputs['Surface']) # Deselect nodes for node in node_tree.nodes: node.select = False #select color ramp colorRampNode.select = True node_tree.nodes.active = colorRampNode ####################### #ASPECT ####################### # Create material aspectMatName = 'Aspect' if aspectMatName not in [m.name for m in bpy.data.materials]: aspectMat = bpy.data.materials.new(aspectMatName) else: aspectMat = bpy.data.materials[aspectMatName] aspectMat.use_nodes = True aspectMat.use_fake_user = True node_tree = aspectMat.node_tree node_tree.nodes.clear() # create geometry node (world coordinates) geomNode = node_tree.nodes.new('ShaderNodeNewGeometry') geomNode.location = (-600, 200) # create separate xyz node xyzSplitNode = node_tree.nodes.new('ShaderNodeSeparateXYZ') xyzSplitNode.location = (-400, 200) node_tree.links.new(geomNode.outputs['True Normal'] , xyzSplitNode.inputs['Vector']) # create maths nodes to compute aspect angle = atan(x/y) xyDiv = node_tree.nodes.new('ShaderNodeMath') xyDiv.operation = 'DIVIDE' xyDiv.location = (-200,0) node_tree.links.new(xyzSplitNode.outputs['X'] , xyDiv.inputs[0]) node_tree.links.new(xyzSplitNode.outputs['Y'] , xyDiv.inputs[1]) atanNode = node_tree.nodes.new('ShaderNodeMath') atanNode.operation = 'ARCTANGENT' atanNode.label = 'Aspect radians' atanNode.location = (0,0) node_tree.links.new(xyDiv.outputs[0] , atanNode.inputs[0]) # create math node to convert radians to degrees rad2dg = node_tree.nodes.new('ShaderNodeMath') rad2dg.operation = 'MULTIPLY' rad2dg.location = (200,0) rad2dg.label = "Aspect degrees" rad2dg.inputs[1].default_value = 180/math.pi node_tree.links.new(atanNode.outputs[0] , rad2dg.inputs[0]) # maths nodes --> if y < 0 then aspect = aspect + 180 yNegMask = node_tree.nodes.new('ShaderNodeMath') yNegMask.operation = 'LESS_THAN' yNegMask.location = (0,200) yNegMask.label = "y negative ?" yNegMask.inputs[1].default_value = 0 node_tree.links.new(xyzSplitNode.outputs['Y'] , yNegMask.inputs[0]) yNegMutiply = node_tree.nodes.new('ShaderNodeMath') yNegMutiply.operation = 'MULTIPLY' yNegMutiply.location = (200,200) node_tree.links.new(yNegMask.outputs[0] , yNegMutiply.inputs[0]) yNegMutiply.inputs[1].default_value = 180 yNegAdd = node_tree.nodes.new('ShaderNodeMath') yNegAdd.operation = 'ADD' yNegAdd.location = (400,200) node_tree.links.new(yNegMutiply.outputs[0] , yNegAdd.inputs[0]) node_tree.links.new(rad2dg.outputs[0] , yNegAdd.inputs[1]) # if y > 0 & x < 0 then aspect = aspect + 360 xNegMask = node_tree.nodes.new('ShaderNodeMath') xNegMask.operation = 'LESS_THAN' xNegMask.location = (0,600) xNegMask.label = "x negative ?" xNegMask.inputs[1].default_value = 0 node_tree.links.new(xyzSplitNode.outputs['X'] , xNegMask.inputs[0]) yPosMask = node_tree.nodes.new('ShaderNodeMath') yPosMask.operation = 'GREATER_THAN' yPosMask.location = (0,400) yPosMask.label = "y positive ?" yPosMask.inputs[1].default_value = 0 node_tree.links.new(xyzSplitNode.outputs['Y'] , yPosMask.inputs[0]) mask = node_tree.nodes.new('ShaderNodeMath') mask.operation = 'MULTIPLY' mask.location = (200,500) node_tree.links.new(xNegMask.outputs[0] , mask.inputs[0]) node_tree.links.new(yPosMask.outputs[0] , mask.inputs[1]) maskMultiply = node_tree.nodes.new('ShaderNodeMath') maskMultiply.operation = 'MULTIPLY' maskMultiply.location = (400,500) node_tree.links.new(mask.outputs[0] , maskMultiply.inputs[0]) maskMultiply.inputs[1].default_value = 360 maskAdd = node_tree.nodes.new('ShaderNodeMath') maskAdd.operation = 'ADD' maskAdd.location = (600,300) node_tree.links.new(maskMultiply.outputs[0] , maskAdd.inputs[0]) node_tree.links.new(yNegAdd.outputs[0] , maskAdd.inputs[1]) # create math node to normalize value normalize = node_tree.nodes.new('ShaderNodeMath') normalize.operation = 'DIVIDE' normalize.location = (800,300) normalize.label = "Normalize" normalize.inputs[1].default_value = 360 node_tree.links.new(maskAdd.outputs[0] , normalize.inputs[0]) # create color ramp node colorRampNode = node_tree.nodes.new('ShaderNodeValToRGB') colorRampNode.location = (1000, 300) cr = colorRampNode.color_ramp stops = cr.elements cr.elements[0].color = (1,0,0,1)#first stop = red stops.remove(stops[1])#remove last stop #orange, yellow, green, cyan, blue1, blue2, pink, red colors = [(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)] for i, angle in enumerate([22.5, 67.5, 112.5, 157.5, 202.5, 247.5, 292.5, 337.5]): pos = scale(angle, 0, 360, 0, 1) stop = stops.new(pos) stop.color = colors[i] cr.interpolation = 'CONSTANT' node_tree.links.new(normalize.outputs[0] , colorRampNode.inputs['Fac']) # Create BSDF diffuse node diffuseNode = node_tree.nodes.new('ShaderNodeBsdfDiffuse') diffuseNode.location = (1300, 300) node_tree.links.new(colorRampNode.outputs['Color'] , diffuseNode.inputs['Color']) # Flat color diffuse diffuseFlat = node_tree.nodes.new('ShaderNodeBsdfDiffuse') diffuseFlat.location = (1300, 0) diffuseFlat.inputs[0].default_value = (1,1,1,1) # flat test flatMask = node_tree.nodes.new('ShaderNodeMath') flatMask.operation = 'LESS_THAN' flatMask.location = (800,-100) flatMask.label = "is flat?" flatMask.inputs[1].default_value = 0.999 node_tree.links.new(xyzSplitNode.outputs['Z'] , flatMask.inputs[0]) # Mix shader mixNode = node_tree.nodes.new('ShaderNodeMixShader') mixNode.location = (1500, 200) node_tree.links.new(diffuseNode.outputs['BSDF'] , mixNode.inputs[2]) node_tree.links.new(diffuseFlat.outputs['BSDF'] , mixNode.inputs[1]) node_tree.links.new(flatMask.outputs[0] , mixNode.inputs['Fac']) # Create output node outputNode = node_tree.nodes.new('ShaderNodeOutputMaterial') outputNode.location = (1700, 200) node_tree.links.new(mixNode.outputs[0] , outputNode.inputs['Surface']) # Deselect nodes for node in node_tree.nodes: node.select = False #select color ramp colorRampNode.select = True node_tree.nodes.active = colorRampNode ####################### # Add material to current object ''' if heightMat.name not in [m.name for m in obj.data.materials]: #add slot & move ui list index else:#this name already exist, just move ui list index to select it obj.active_material_index = obj.material_slots.find(heightMat.name) ''' #add slot obj.data.materials.append(heightMat) #move ui list index obj.active_material_index = len(obj.material_slots)-1 #Assignmaterial to faces for faces in obj.data.polygons: faces.material_index = obj.active_material_index return {'FINISHED'} def register(): try: bpy.utils.register_class(TERRAIN_ANALYSIS_OT_build_nodes) except ValueError as e: log.warning('{} is already registered, now unregister and retry... '.format(TERRAIN_ANALYSIS_OT_build_nodes)) unregister() bpy.utils.register_class(TERRAIN_ANALYSIS_OT_build_nodes) def unregister(): bpy.utils.unregister_class(TERRAIN_ANALYSIS_OT_build_nodes) ================================================ FILE: operators/nodes_terrain_analysis_reclassify.py ================================================ # -*- coding:utf-8 -*- import os import math import bpy from mathutils import Vector from bpy.props import StringProperty, IntProperty, FloatProperty, BoolProperty, EnumProperty, CollectionProperty, FloatVectorProperty from bpy.types import PropertyGroup, UIList, Panel, Operator from bpy.app.handlers import persistent import logging log = logging.getLogger(__name__) from .utils import getBBOX from ..core.utils.gradient import Color, Stop, Gradient from ..core.maths.interpo import scale from ..core.maths.kmeans1D import kmeans1d, getBreaks #from ..core.maths.jenks_caspall import jenksCaspall #Folder containing SVG gradients svgGradientFolder = os.path.dirname(os.path.realpath(__file__)) + os.sep + "rsrc" + os.sep + "gradients" + os.sep #Global var ######################################## #These variables are the bounds values of the topographic property represented #this is an altitude (zmin & zmax) in meters if material represents a height map #or a slope in degrees if material represents a slope map #bounds values are used to scale user input (altitude or slope) between 0 and 1 #then scale values are used to setup color ramp node inMin = 0 inMax = 0 # other global for handler check scn = None obj = None mat = None node = None #Set up a propertyGroup and populate a CollectionProperty ######################################### class RECLASS_PG_color(PropertyGroup): #Define update function for FloatProperty def updStop(item, context): #first arg is the container of the prop to update, here a customItem if context.space_data is not None: if context.space_data.type == 'NODE_EDITOR': v = item.val i = item.idx node = context.active_node cr = node.color_ramp stops = cr.elements newPos = scale(v, inMin, inMax, 0, 1) #limit move between previous and next stops if i+1 == len(stops):#this is the last stop nextPos = 1 else: nextPos = stops[i+1].position if i == 0:#this is the first stop prevPos = 0 else: prevPos = stops[i-1].position # if newPos > nextPos: stops[i].position = nextPos item.val = scale(nextPos, 0, 1, inMin, inMax) elif newPos < prevPos: stops[i].position = prevPos item.val = scale(prevPos, 0, 1, inMin, inMax) else: stops[i].position = newPos #Define update function for color property def updColor(item, context): if context.space_data is not None: if context.space_data.type == 'NODE_EDITOR': color = item.color i = item.idx node = context.active_node cr = node.color_ramp stops = cr.elements stops[i].color = color #Properties in the group idx: IntProperty() val: FloatProperty(update=updStop) color: FloatVectorProperty(subtype='COLOR', min=0, max=1, update=updColor, size=4) #POPULATE #Make function to populate collection def populateList(colorRampNode): setBounds() if colorRampNode is not None: if colorRampNode.bl_idname == 'ShaderNodeValToRGB': bpy.context.scene.uiListCollec.clear() cr = colorRampNode.color_ramp for i, stop in enumerate(cr.elements): v = scale(stop.position, 0, 1, inMin, inMax, ) item = bpy.context.scene.uiListCollec.add() item.idx = i #warn. : assign idx before val because idx is used in property update function item.val = v #warn. : causes exec. of property update function item.color = stop.color #Set others properties in scene and their update functions ######################################### def updateAnalysisMode(scn, context): if context.space_data.type == 'NODE_EDITOR': #refresh node = context.active_node populateList(node) def setBounds(): scn = bpy.context.scene mode = scn.analysisMode global inMin global inMax global obj if mode == 'HEIGHT': obj = bpy.context.view_layer.objects.active bbox = getBBOX.fromObj(obj) inMin = bbox['zmin'] inMax = bbox['zmax'] elif mode == 'SLOPE': #slope of a terrain won't exceed vertical plane (90°) #so for easiest calculation we consider slope between 0 and 100° inMin = 0 inMax = 100 elif mode == 'ASPECT': inMin = 0 inMax = 360 #Handler to refresh ui list when user # > select another obj # > change active material # > move, delete or add stop on the node # > select another color ramp node ######################################### @persistent def scene_update(scn): 'keep colorramp node and reclass panel in synch' global obj global mat global node #print(node.bl_idname) activeObj = bpy.context.view_layer.objects.active if activeObj is not None: activeMat = activeObj.active_material if activeMat is not None and activeMat.use_nodes: activeNode = activeMat.node_tree.nodes.active #check color ramp node edits #>issue : activeMat.is_updated function is no more available in 2.8, use depsgraph instead ''' depsgraph = bpy.context.evaluated_depsgraph_get() #cause recursion depth error if depsgraph.id_type_updated('MATERIAL'): populateList(activeNode) ''' #check selected obj if obj != activeObj: obj = activeObj populateList(activeNode) #check active material if mat != activeMat: mat = activeMat populateList(activeNode) #check selected node if node != activeNode: node = activeNode populateList(activeNode) #Set up ui list ######################################### class RECLASS_UL_stops(UIList): def getAspectLabels(self): vals = [round(item.val,2) for item in bpy.context.scene.uiListCollec] if vals == [0, 45, 135, 225, 315]: return ['N', 'E', 'S', 'W', 'N'] elif vals == [0, 22.5, 67.5, 112.5, 157.5, 202.5, 247.5, 292.5, 337.5]: return ['N', 'N-E', 'E', 'S-E', 'S', 'S-W', 'W', 'N-W', 'N'] elif vals == [0, 30, 90, 150, 210, 270, 330]: return ['N', 'N-E', 'S-E', 'S', 'S-W', 'N-W', 'N'] elif vals == [0, 60, 120, 180, 240, 300, 360]: return ['N-E', 'E', 'S-E', 'S-W', 'W', 'N-W', 'N-E'] elif vals == [0, 90, 270]: return ['N', 'S', 'N'] elif vals == [0, 180]: return ['E', 'W'] else: return False def draw_item(self, context, layout, data, item, icon, active_data, active_propname, index): ''' called for each item of the collection visible in the list must handle the three layout types 'DEFAULT', 'COMPACT' and 'GRID' data is the object containing the collection (in our case, the scene) item is the current drawn item of the collection (in our case a propertyGroup "customItem") index is index of the current item in the collection (optional) ''' scn = bpy.context.scene mode = scn.analysisMode self.use_filter_show = False if self.layout_type in {'DEFAULT', 'COMPACT'}: if mode == 'ASPECT': aspectLabels = self.getAspectLabels() split = layout.split(factor=0.2) if aspectLabels: split.label(text=aspectLabels[item.idx]) else: split.label(text=str(item.idx+1)) split = split.split(factor=0.4) split.prop(item, "color", text="") split.prop(item, "val", text="") else: split = layout.split(factor=0.2) #split.label(text=str(index)) split.label(text=str(item.idx+1)) split = split.split(factor=0.4) split.prop(item, "color", text="") split.prop(item, "val", text="") elif self.layout_type in {'GRID'}: layout.alignment = 'CENTER' #Make a Panel ######################################### class RECLASS_PT_reclassify(Panel): """Creates a panel in the properties of node editor""" bl_label = "Reclassify" bl_space_type = 'NODE_EDITOR' bl_region_type = 'UI' bl_category = "Item" def draw(self, context): node = context.active_node if node is not None: if node.bl_idname == 'ShaderNodeValToRGB': layout = self.layout scn = context.scene layout.prop(scn, "analysisMode") row = layout.row() #Draw ui list with template_list function row.template_list("RECLASS_UL_stops", "", scn, "uiListCollec", scn, "uiListIndex", rows=10) #Draw side tools col = row.column(align=True) col.operator("reclass.list_add", text="", icon='ADD') col.operator("reclass.list_rm", text="", icon='REMOVE') col.operator("reclass.list_clear", text="", icon='FILE_PARENT') col.separator() col.operator("reclass.list_refresh", text="", icon='FILE_REFRESH') col.separator() col.operator("reclass.switch_interpolation", text="", icon='SMOOTHCURVE') col.operator("reclass.flip", text="", icon='ARROW_LEFTRIGHT') col.operator("reclass.quick_gradient", text="", icon="COLOR") col.operator("reclass.svg_gradient", text="", icon="GROUP_VCOL") col.operator("reclass.export_svg", text="", icon="FORWARD") col.separator() col.operator("reclass.auto", text="", icon='FULLSCREEN_ENTER') ##col.separator() ##col.operator("reclass.settings", text="", icon='PREFERENCES') #Draw infos #row = layout.row() #row.label(text=scn.collection.objects.active.name) row = layout.row() row.label(text="min = " + str(round(inMin,2))) row.label(text="max = " + str(round(inMax,2))) row = layout.row() row.label(text="delta = " + str(round(inMax-inMin,2))) #Make Operators to manage ui list ######################################### class RECLASS_OT_switch_interpolation(Operator): '''Switch color interpolation (continuous / discrete)''' bl_idname = "reclass.switch_interpolation" bl_label = "Switch color interpolation (continuous or discrete)" def execute(self, context): node = context.active_node cr = node.color_ramp cr.color_mode = 'RGB' if cr.interpolation != 'CONSTANT': cr.interpolation = 'CONSTANT' else: cr.interpolation = 'LINEAR' return {'FINISHED'} class RECLASS_OT_flip(Operator): '''Flip color ramp''' bl_idname = "reclass.flip" bl_label = "Flip color ramp" def execute(self, context): node = context.active_node cr = node.color_ramp stops = cr.elements #buid reversed color ramp revStops = [] for i, stop in reversed(list(enumerate(stops))): revPos = 1-stop.position color = tuple(stop.color) revStops.append((revPos, color)) #assign new position and color for i, stop in enumerate(stops): #stop.position = newStops[i][0] stop.color = revStops[i][1] #refresh populateList(node) return {'FINISHED'} class RECLASS_OT_refresh(Operator): """Refresh list to match node setting""" bl_idname = "reclass.list_refresh" bl_label = "Populate list" def execute(self, context): node = context.active_node populateList(node) return {'FINISHED'} class RECLASS_OT_clear(Operator): """Clear color ramp""" bl_idname = "reclass.list_clear" bl_label = "Clear list" def execute(self, context): #bpy.context.scene.uiListCollec.clear() node = context.active_node cr = node.color_ramp stops = cr.elements #remove stops from color ramp for stop in reversed(stops): if len(stops) > 1:#cannot remove last element stops.remove(stop) else: stop.position = 0 #refresh ui list populateList(node) return{'FINISHED'} class RECLASS_OT_add(Operator): """Add stop""" bl_idname = "reclass.list_add" bl_label = "Add stop" def execute(self, context): lst = bpy.context.scene.uiListCollec currentIdx = bpy.context.scene.uiListIndex if currentIdx > len(lst)-1: #return {'CANCELLED'} currentIdx = 0 #move ui selection to first idx #lst.add() # node = context.active_node cr = node.color_ramp stops = cr.elements if len(stops) >=32: self.report({'ERROR'}, "Ramp is limited to 32 colors") return {'CANCELLED'} currentPos = stops[currentIdx].position if currentIdx == len(stops)-1:#last stop nextPos = 1.0 else: nextPos = stops[currentIdx+1].position newPos = currentPos + ((nextPos-currentPos)/2) stops.new(newPos) #Refresh list populateList(node) #Move selection in ui list bpy.context.scene.uiListIndex = currentIdx+1 return {'FINISHED'} class RECLASS_OT_rm(Operator): """Remove stop""" bl_idname = "reclass.list_rm" bl_label = "Remove Stop" def execute(self, context): currentIdx = bpy.context.scene.uiListIndex lst = bpy.context.scene.uiListCollec if currentIdx > len(lst)-1: return {'CANCELLED'} #lst.remove(currentIdx) # node = context.active_node cr = node.color_ramp stops = cr.elements if len(stops) > 1: #cannot remove last element stops.remove(stops[currentIdx]) #Refresh list populateList(node) #Move selecton in ui list if last element has been removed if currentIdx > len(lst)-1: bpy.context.scene.uiListIndex = currentIdx-1 return {'FINISHED'} #Make Operators to auto reclassify ######################################### def clearRamp(stops, startColor=(0,0,0,1), endColor=(1,1,1,1), startPos=0, endPos=1): #clear actual color ramp for stop in reversed(stops): if len(stops) > 1:#cannot remove last element stops.remove(stop) else:#move last element to first position first = stop first.position = startPos first.color = startColor #Add last stop last = stops.new(endPos) last.color = endColor return (first, last) def getValues(): '''Return mesh data values (z, slope or az) for classification''' scn = bpy.context.scene obj = bpy.context.view_layer.objects.active #make a temp mesh with modifiers apply mesh = obj.to_mesh() mesh.transform(obj.matrix_world) # mode = scn.analysisMode if mode == 'HEIGHT': values = [vertex.co.z for vertex in mesh.vertices] elif mode == 'SLOPE': z = Vector((0,0,1)) m = obj.matrix_world values = [math.degrees(z.angle(m * face.normal)) for face in mesh.polygons] elif mode == 'ASPECT': y = Vector((0,1,0)) m = obj.matrix_world #values = [math.degrees(y.angle(m * face.normal)) for face in mesh.polygons] values = [] for face in mesh.polygons: normal = face.normal.copy() normal.z = 0 #project vector into XY plane try: a = math.degrees(y.angle(m * normal)) except ValueError: pass#zero length vector as no angle else: #returned angle is between 0° (north) to 180° (south) #we must correct it to get angle between 0 to 360° if normal.x <0: a = 360 - a values.append(a) values.sort() #remove temp mesh obj.to_mesh_clear() return values class RECLASS_OT_auto(Operator): '''Auto reclass by equal interval or fixed classe number''' bl_idname = "reclass.auto" bl_label = "Reclass by equal interval or fixed classe number" autoReclassMode: EnumProperty( name="Mode", description="Select auto reclassify mode", items=[ ('CLASSES_NB', 'Fixed classes number', "Define the expected number of classes"), ('EQUAL_STEP', 'Equal interval value', "Define step value between classes"), ('TARGET_STEP', 'Target interval value', "Define target step value that stops will match"), ('QUANTILE', 'Quantile', 'Assigns the same number of data values to each class.'), ('1DKMEANS', 'Natural breaks', 'kmeans clustering optimized for one dimensional data'), ('ASPECT', 'Aspect reclassification', "Value define the number of azimuth")] ) color1: FloatVectorProperty(name="Start color", subtype='COLOR', min=0, max=1, size=4) color2: FloatVectorProperty(name="End color", subtype='COLOR', min=0, max=1, size=4) value: IntProperty(name="Value", default=4) def invoke(self, context, event): #Set color to actual ramp node = context.active_node cr = node.color_ramp stops = cr.elements self.color1 = stops[0].color self.color2 = stops[len(stops)-1].color #Show dialog with operator properties wm = context.window_manager return wm.invoke_props_dialog(self) def execute(self, context): node = context.active_node cr = node.color_ramp #switch to linear so new stops will have correctly evaluate color cr.color_mode = 'RGB' cr.interpolation = 'LINEAR' stops = cr.elements #Get colors startColor = self.color1 endColor = self.color2 if self.autoReclassMode == 'TARGET_STEP': interval = self.value delta = inMax-inMin nbClasses = math.ceil(delta/interval) if nbClasses >= 32: self.report({'ERROR'}, "Ramp is limited to 32 colors") return {'CANCELLED'} clearRamp(stops, startColor, endColor) nextStop = inMin + interval - (inMin % interval) while nextStop < inMax: position = scale(nextStop, inMin, inMax, 0, 1) stop = stops.new(position) nextStop += interval if self.autoReclassMode == 'EQUAL_STEP': interval = self.value delta = inMax-inMin nbClasses = math.ceil(delta/interval) if nbClasses >= 32: self.report({'ERROR'}, "Ramp is limited to 32 colors") return {'CANCELLED'} clearRamp(stops, startColor, endColor) val = inMin for i in range(nbClasses-1): val += interval position = scale(val, inMin, inMax, 0, 1) stop = stops.new(position) if self.autoReclassMode == 'CLASSES_NB': nbClasses = self.value if nbClasses >= 32: self.report({'ERROR'}, "Ramp is limited to 32 colors") return {'CANCELLED'} delta = inMax-inMin if nbClasses >= delta: self.report({'ERROR'}, "Too many classes") return {'CANCELLED'} clearRamp(stops, startColor, endColor) interval = delta/nbClasses val = inMin for i in range(nbClasses-1): val += interval position = scale(val, inMin, inMax, 0, 1) stop = stops.new(position) if self.autoReclassMode == 'ASPECT': bpy.context.scene.analysisMode = 'ASPECT' delta = inMax-inMin #360° interval = 360 / self.value nbClasses = self.value #math.ceil(delta/interval) if nbClasses >= 32: self.report({'ERROR'}, "Ramp is limited to 32 colors") return {'CANCELLED'} first, last = clearRamp(stops, startColor, endColor) offset = interval/2 intervalNorm = scale(interval, inMin, inMax, 0, 1) offsetNorm = scale(offset, inMin, inMax, 0, 1) #move actual last stop to before last position last.position -= intervalNorm + offsetNorm #add intermediates stops val = 0 for i in range(nbClasses-2): if i == 0: val += offset else: val += interval position = scale(val, inMin, inMax, 0, 1) stop = stops.new(position) #add last stop = stops.new(1-offsetNorm) stop.color = first.color cr.interpolation = 'CONSTANT' if self.autoReclassMode == 'QUANTILE': nbClasses = self.value values = getValues() if nbClasses >= 32: self.report({'ERROR'}, "Ramp is limited to 32 colors") return {'CANCELLED'} if nbClasses >= len(values): self.report({'ERROR'}, "Too many classes") return {'CANCELLED'} clearRamp(stops, startColor, endColor) n = len(values) q = int(n/nbClasses) #number of value per quantile cumulative_q = q previousVal = scale(0, 0, 1, inMin, inMax) for i in range(nbClasses-1): val = values[cumulative_q] if val != previousVal: position = scale(val, inMin, inMax, 0, 1) stop = stops.new(position) previousVal = val cumulative_q += q if self.autoReclassMode == '1DKMEANS': nbClasses = self.value values = getValues() if nbClasses >= 32: self.report({'ERROR'}, "Ramp is limited to 32 colors") return {'CANCELLED'} if nbClasses >= len(values): self.report({'ERROR'}, "Too many classes") return {'CANCELLED'} clearRamp(stops, startColor, endColor) #compute clusters #clusters = jenksCaspall(values, nbClasses, 4) #for val in clusters.breaks: clusters = kmeans1d(values, nbClasses) for val in getBreaks(values, clusters): position = scale(val, inMin, inMax, 0, 1) stop = stops.new(position) #refresh populateList(node) return {'FINISHED'} #Operators to change color ramp ######################################### colorSpaces = [('RGB', 'RGB', "RGB color space"), ('HSV', 'HSV', "HSV color space")] interpoMethods = [('LINEAR', 'Linear', "Linear interpolation"), ('SPLINE', 'Spline', "Spline interpolation (Akima's method)"), ('DISCRETE', 'Discrete', "No interpolation (return previous color)"), ('NEAREST', 'Nearest', "No interpolation (return nearest color)") ] #QUICK GRADIENT class RECLASS_PG_color_preview(PropertyGroup): color: FloatVectorProperty(subtype='COLOR', min=0, max=1, size=4) class RECLASS_OT_quick_gradient(Operator): '''Quick colors gradient edit''' bl_idname = "reclass.quick_gradient" bl_label = "Quick colors gradient edit" colorSpace: EnumProperty( name="Space", description="Select interpolation color space", items = colorSpaces) method: EnumProperty( name="Method", description="Select interpolation method", items = interpoMethods) #special function to redraw an operator popup called through invoke_props_dialog def check(self, context): return True def initPreview(self, context): context.scene.colorRampPreview.clear() node = context.active_node cr = node.color_ramp stops = cr.elements if self.fitGradient: minPos, maxPos = stops[0].position, stops[-1].position delta = maxPos-minPos else: delta = 1 offset = delta/(self.nbColors-1) position = 0 for i in range(self.nbColors): item = bpy.context.scene.colorRampPreview.add() item.color = cr.evaluate(position) position += offset return def updatePreview(self, context): #Add or remove colors from preview when change nb colors colorItems = bpy.context.scene.colorRampPreview nb = len(colorItems) if nb == self.nbColors: return delta = abs(self.nbColors - nb) for i in range(delta): if self.nbColors > nb: item = colorItems.add() item.color = colorItems[-2].color else: colorItems.remove(nb-1) fitGradient: BoolProperty(update=initPreview) nbColors: IntProperty( name="Number of colors", description="Set the number of colors needed to define the quick quadient", min=2, default=4, update=updatePreview) def invoke(self, context, event): #initialize colors preview self.initPreview(context) #Show dialog with operator properties wm = context.window_manager return wm.invoke_props_dialog(self, width=200, height=200) def draw(self, context): layout = self.layout layout.prop(self, "colorSpace", text='Space') layout.prop(self, "method", text='Method') layout.prop(self, "fitGradient", text="Fit gradient to min/max positions") layout.prop(self, "nbColors", text='Number of colors') row = layout.row(align=True) colorItems = context.scene.colorRampPreview for i in range(self.nbColors): colorItem = colorItems[i] row.prop(colorItem, 'color', text='') def execute(self, context): #build gradient colorList = context.scene.colorRampPreview colorRamp = Gradient() nbColors = len(colorList) offset = 1/(nbColors-1) position = 0 for i, item in enumerate(colorList): color = Color(list(item.color), 'rgb') colorRamp.addStop(round(position,4), color) position += offset #get color ramp node node = context.active_node cr = node.color_ramp stops = cr.elements #rescale if self.fitGradient: minPos, maxPos = stops[0].position, stops[-1].position colorRamp.rescale(minPos, maxPos) #update colors for stop in stops: stop.color = colorRamp.evaluate(stop.position, self.colorSpace, self.method).rgba # if self.colorSpace == 'HSV': cr.color_mode = 'HSV' else: cr.color_mode = 'RGB' #refresh populateList(node) return {'FINISHED'} #SVG COLOR RAMP def filesList(inFolder, ext): if not os.path.exists(inFolder): #os.makedirs(inFolder) return [] lst = os.listdir(inFolder) extLst=[elem for elem in lst if os.path.splitext(elem)[1]==ext] extLst.sort() return extLst svgFiles = filesList(svgGradientFolder, '.svg') colorPreviewRange = 20 class RECLASS_OT_svg_gradient(Operator): '''Define colors gradient with presets''' bl_idname = "reclass.svg_gradient" bl_label = "Define colors gradient with presets" def listSVG(self, context): #Function used to update the gradient list used by the dropdown box. svgs = [] #list containing tuples of each object for index, svg in enumerate(svgFiles): #iterate over all objects svgs.append((str(index), os.path.splitext(svg)[0], svgGradientFolder + svg)) #tuple (key, label, tooltip) return svgs def updatePreview(self, context): if len(self.colorPresets) == 0: return #build gradient enumIdx = int(self.colorPresets) path = svgGradientFolder + svgFiles[enumIdx] colorRamp = Gradient(path) #make preview nbColors = colorPreviewRange interpoGradient = colorRamp.getRangeColor(nbColors, self.colorSpace, self.method) for i, stop in enumerate(interpoGradient.stops): item = bpy.context.scene.colorRampPreview[i] item.color = stop.color.rgba return colorPresets: EnumProperty( name="preset", description="Select a color ramp preset", items=listSVG, update=updatePreview ) colorSpace: EnumProperty( name="Space", description="Select interpolation color space", items = colorSpaces, update = updatePreview ) method: EnumProperty( name="Method", description="Select interpolation method", items = interpoMethods, update = updatePreview ) fitGradient: BoolProperty() def invoke(self, context, event): #clear collection context.scene.colorRampPreview.clear() #feed collection for i in range(colorPreviewRange): bpy.context.scene.colorRampPreview.add() #update colors preview self.updatePreview(context) #Show dialog with operator properties wm = context.window_manager return wm.invoke_props_dialog(self, width=200, height=200) def draw(self, context):#layout for invoke props modal dialog #operator.draw() is different from panel.draw() #because it's only called once (when the pop-up is created) layout = self.layout layout.prop(self, "colorSpace") layout.prop(self, "method") layout.prop(self, "colorPresets", text='') row = layout.row(align=True) row.enabled = False for item in context.scene.colorRampPreview: row.prop(item, 'color', text='') row = layout.row() row.prop(self, "fitGradient", text="Fit gradient to min/max positions") def execute(self, context): if len(self.colorPresets) == 0: return {'CANCELLED'} #build gradient enumIdx = int(self.colorPresets) path = svgGradientFolder + svgFiles[enumIdx] colorRamp = Gradient(path) #get color ramp node node = context.active_node cr = node.color_ramp stops = cr.elements #rescale if self.fitGradient: minPos, maxPos = stops[0].position, stops[-1].position colorRamp.rescale(minPos, maxPos) #update colors for stop in stops: stop.color = colorRamp.evaluate(stop.position, self.colorSpace, self.method).rgba # if self.colorSpace == 'HSV': cr.color_mode = 'HSV' else: cr.color_mode = 'RGB' #refresh populateList(node) return {'FINISHED'} class RECLASS_OT_export_svg(Operator): '''Export current gradient to SVG file''' bl_idname = "reclass.export_svg" bl_label = "Export current gradient to SVG file" name: StringProperty(description="Put name of SVG file") n: IntProperty(default=5, description="Select expected number of interpolate colors") gradientType: EnumProperty( name="Build method", description="Select methods to build gradient", items = [('SELF_STOPS', 'Use actual stops', ""), ('INTERPOLATE', 'Interpolate n colors', "")] ) makeDiscrete: BoolProperty(name="Make discrete", description="Build discrete svg gradient") colorSpace: EnumProperty( name="Color space", description="Select interpolation color space", items = colorSpaces) method: EnumProperty( name="Interp. method", description="Select interpolation method", items = interpoMethods) #special function to redraw an operator popup called through invoke_props_dialog def check(self, context): return True def invoke(self, context, event): #Show dialog with operator properties wm = context.window_manager return wm.invoke_props_dialog(self, width=250, height=200) def draw(self, context): layout = self.layout layout.prop(self, "name", text='Name') layout.prop(self, "gradientType") layout.prop(self, "makeDiscrete") if self.gradientType == "INTERPOLATE": layout.separator() layout.label(text='Interpolation options') layout.prop(self, "colorSpace", text='Color space') layout.prop(self, "method", text='Method') layout.prop(self, "n", text="Number of colors") def execute(self, context): #Get node color ramp node = context.active_node cr = node.color_ramp stops = cr.elements #Build gradient class colorRamp = Gradient() for stop in stops: color = Color(list(stop.color), 'rgba') colorRamp.addStop(stop.position, color) #write svg svgPath = svgGradientFolder + self.name + '.svg' if self.gradientType == "INTERPOLATE": interpoGradient = colorRamp.getRangeColor(self.n, self.colorSpace, self.method) interpoGradient.exportSVG(svgPath, self.makeDiscrete) elif self.gradientType == "SELF_STOPS": colorRamp.exportSVG(svgPath, self.makeDiscrete) #update svg files list global svgFiles svgFiles = filesList(svgGradientFolder , '.svg') return {'FINISHED'} classes = [ RECLASS_PG_color, RECLASS_PG_color_preview, RECLASS_UL_stops, RECLASS_PT_reclassify, RECLASS_OT_switch_interpolation, RECLASS_OT_flip, RECLASS_OT_refresh, RECLASS_OT_clear, RECLASS_OT_add, RECLASS_OT_rm, RECLASS_OT_auto, RECLASS_OT_quick_gradient, RECLASS_OT_svg_gradient, RECLASS_OT_export_svg ] def register(): for cls in classes: try: bpy.utils.register_class(cls) except ValueError as e: log.warning('{} is already registered, now unregister and retry... '.format(cls)) bpy.utils.unregister_class(cls) bpy.utils.register_class(cls) #Create uilist collections bpy.types.Scene.uiListCollec = CollectionProperty(type=RECLASS_PG_color) bpy.types.Scene.uiListIndex = IntProperty() #used to store the index of the selected item in the uilist bpy.types.Scene.colorRampPreview = CollectionProperty(type=RECLASS_PG_color_preview) #Add handlers bpy.app.handlers.depsgraph_update_post.append(scene_update) # bpy.types.Scene.analysisMode = EnumProperty( name = "Mode", description = "Choose the type of analysis this material do", items = [('HEIGHT', 'Height', "Height analysis"), ('SLOPE', 'Slope', "Slope analysis"), ('ASPECT', 'Aspect', "Aspect analysis")], update = updateAnalysisMode ) def unregister(): del bpy.types.Scene.analysisMode #Clear uilist del bpy.types.Scene.uiListCollec del bpy.types.Scene.uiListIndex del bpy.types.Scene.colorRampPreview #Clear handlers bpy.app.handlers.depsgraph_update_post.clear() #Unregister for cls in classes: bpy.utils.unregister_class(cls) ================================================ FILE: operators/object_drop.py ================================================ # ##### BEGIN GPL LICENSE BLOCK ##### # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License # as published by the Free Software Foundation; either version 2 # of the License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ##### END GPL LICENSE BLOCK ##### # Original Drop to Ground addon code from Unnikrishnan(kodemax), Florian Meyer(testscreenings) import logging log = logging.getLogger(__name__) import bpy import bmesh from .utils import DropToGround, getBBOX from mathutils import Vector, Matrix from bpy.types import Operator from bpy.props import BoolProperty, EnumProperty def get_align_matrix(location, normal): up = Vector((0, 0, 1)) angle = normal.angle(up) axis = up.cross(normal) mat_rot = Matrix.Rotation(angle, 4, axis) mat_loc = Matrix.Translation(location) mat_align = mat_rot @ mat_loc return mat_align def get_lowest_world_co(ob, mat_parent=None): bme = bmesh.new() bme.from_mesh(ob.data) mat_to_world = ob.matrix_world.copy() if mat_parent: mat_to_world = mat_parent @ mat_to_world lowest = None for v in bme.verts: if not lowest: lowest = v if (mat_to_world @ v.co).z < (mat_to_world @ lowest.co).z: lowest = v lowest_co = mat_to_world @ lowest.co bme.free() return lowest_co class OBJECT_OT_drop_to_ground(Operator): bl_idname = "object.drop" bl_label = "Drop to Ground" bl_description = ("Drop selected objects on the Active object") bl_options = {"REGISTER", "UNDO"} #register needed to draw operator options/redo panel align: BoolProperty( name="Align to ground", description="Aligns the objects' rotation to the ground", default=False) axisAlign: EnumProperty( items = [("N", "Normal", "Ground normal"), ("X", "X", "Ground X normal"), ("Y", "Y", "Ground Y normal"), ("Z", "Z", "Ground Z normal")], name="Align axis", description="") useOrigin: BoolProperty( name="Use Origins", description="Drop to objects' origins\n" "Use this option for dropping all types of Objects", default=False) #this method will disable the button if the conditions are not respected @classmethod def poll(cls, context): act_obj = context.active_object return (context.mode == 'OBJECT' and len(context.selected_objects) >= 2 and act_obj and act_obj.type in {'MESH', 'FONT', 'META', 'CURVE', 'SURFACE'} ) def draw(self, context): layout = self.layout layout.prop(self, 'align') if self.align: layout.prop(self, 'axisAlign') layout.prop(self, 'useOrigin') def execute(self, context): bpy.context.view_layer.update() #needed to make raycast function redoable (evaluate objects) ground = context.active_object obs = context.selected_objects if ground in obs: obs.remove(ground) scn = context.scene rayCaster = DropToGround(scn, ground) for ob in obs: if self.useOrigin: minLoc = ob.location else: minLoc = get_lowest_world_co(ob) #minLoc = min([(ob.matrix_world * v.co).z for v in ob.data.vertices]) #getBBOX.fromObj(ob).zmin #what xy coords ??? if not minLoc: msg = "Object {} is of type {} works only with Use Center option " \ "checked".format(ob.name, ob.type) log.info(msg) x, y = minLoc.x, minLoc.y hit = rayCaster.rayCast(x, y) if not hit.hit: log.info(ob.name + " did not hit the Active Object") continue # simple drop down down = hit.loc - minLoc ob.location += down #ob.location = hit.loc # drop with align to hit normal if self.align: vect = ob.location - hit.loc # rotate object to align with face normal normal = get_align_matrix(hit.loc, hit.normal) rot = normal.to_euler() if self.axisAlign == "X": rot.y = 0 rot.z = 0 elif self.axisAlign == "Y": rot.x = 0 rot.z = 0 elif self.axisAlign == "Z": rot.x = 0 rot.y = 0 matrix = ob.matrix_world.copy().to_3x3() matrix.rotate(rot) matrix = matrix.to_4x4() ob.matrix_world = matrix # move_object to hit_location ob.location = hit.loc # move object above surface again vect.rotate(rot) ob.location += vect return {'FINISHED'} def register(): try: bpy.utils.register_class(OBJECT_OT_drop_to_ground) except ValueError as e: log.warning('{} is already registered, now unregister and retry... '.format(OBJECT_OT_drop_to_ground)) unregister() bpy.utils.register_class(OBJECT_OT_drop_to_ground) def unregister(): bpy.utils.unregister_class(OBJECT_OT_drop_to_ground) ================================================ FILE: operators/utils/__init__.py ================================================ from .bgis_utils import placeObj, adjust3Dview, showTextures, addTexture, getBBOX, DropToGround, mouseTo3d, isTopView from .georaster_utils import rasterExtentToMesh, geoRastUVmap, setDisplacer, bpyGeoRaster, exportAsMesh from .delaunay_voronoi import computeVoronoiDiagram, computeDelaunayTriangulation ================================================ FILE: operators/utils/bgis_utils.py ================================================ import bpy from mathutils import Vector, Matrix from mathutils.bvhtree import BVHTree from bpy_extras.view3d_utils import region_2d_to_location_3d, region_2d_to_vector_3d from ...core import BBOX def isTopView(context): if context.area.type == 'VIEW_3D': reg3d = context.region_data else: return False return reg3d.view_perspective == 'ORTHO' and tuple(reg3d.view_matrix.to_euler()) == (0,0,0) def mouseTo3d(context, x, y): '''Convert event.mouse_region to world coordinates''' if context.area.type != 'VIEW_3D': raise Exception('Wrong context') coords = (x, y) reg = context.region reg3d = context.region_data vec = region_2d_to_vector_3d(reg, reg3d, coords) loc = region_2d_to_location_3d(reg, reg3d, coords, vec) #WARNING, this function return indeterminate value when view3d clip distance is too large return loc class DropToGround(): '''A class to perform raycasting accross z axis''' def __init__(self, scn, ground, method='OBJ'): self.method = method # 'BVH' or 'OBJ' self.scn = scn self.ground = ground self.bbox = getBBOX.fromObj(ground, applyTransform=True) self.mw = self.ground.matrix_world self.mwi = self.mw.inverted() if self.method == 'BVH': self.bvh = BVHTree.FromObject(self.ground, bpy.context.evaluated_depsgraph_get(), deform=True) def rayCast(self, x, y): #Hit vector offset = 100 orgWldSpace = Vector((x, y, self.bbox.zmax + offset)) orgObjSpace = self.mwi @ orgWldSpace direction = Vector((0,0,-1)) #down #build ray cast hit namespace object class RayCastHit(): pass rcHit = RayCastHit() #raycast if self.method == 'OBJ': rcHit.hit, rcHit.loc, rcHit.normal, rcHit.faceIdx = self.ground.ray_cast(orgObjSpace, direction) elif self.method == 'BVH': rcHit.loc, rcHit.normal, rcHit.faceIdx, rcHit.dst = self.bvh.ray_cast(orgObjSpace, direction) if not rcHit.loc: rcHit.hit = False else: rcHit.hit = True #adjust values if not rcHit.hit: #return same original 2d point with z=0 rcHit.loc = Vector((orgWldSpace.x, orgWldSpace.y, 0)) #elseZero else: rcHit.hit = True rcHit.loc = self.mw @ rcHit.loc return rcHit def placeObj(mesh, objName): '''Build and add a new object from a given mesh''' bpy.ops.object.select_all(action='DESELECT') #create an object with that mesh obj = bpy.data.objects.new(objName, mesh) # Link object to scene bpy.context.scene.collection.objects.link(obj) bpy.context.view_layer.objects.active = obj obj.select_set(True) #bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY') return obj def adjust3Dview(context, bbox, zoomToSelect=True): '''adjust all 3d views clip distance to match the submited bbox''' dst = round(max(bbox.dimensions)) k = 5 #increase factor dst = dst * k # set each 3d view areas = context.screen.areas for area in areas: if area.type == 'VIEW_3D': space = area.spaces.active if dst < 100: space.clip_start = 1 elif dst < 1000: space.clip_start = 10 else: space.clip_start = 100 #Adjust clip end distance if the new obj is largest than actual setting if space.clip_end < dst: if dst > 10000000: dst = 10000000 #too large clip distance broke the 3d view space.clip_end = dst if zoomToSelect: overrideContext = context.copy() overrideContext['area'] = area overrideContext['region'] = area.regions[-1] if bpy.app.version[0] > 3: with context.temp_override(**overrideContext): bpy.ops.view3d.view_selected() else: bpy.ops.view3d.view_selected(overrideContext) def showTextures(context): '''Force view mode with textures''' scn = context.scene for area in context.screen.areas: if area.type == 'VIEW_3D': space = area.spaces.active if space.shading.type == 'SOLID': space.shading.color_type = 'TEXTURE' def addTexture(mat, img, uvLay, name='texture'): '''Set a new image texture to a given material and following a given uv map''' engine = bpy.context.scene.render.engine mat.use_nodes = True node_tree = mat.node_tree node_tree.nodes.clear() # create uv map node uvMapNode = node_tree.nodes.new('ShaderNodeUVMap') uvMapNode.uv_map = uvLay.name uvMapNode.location = (-800, 200) # create image texture node textureNode = node_tree.nodes.new('ShaderNodeTexImage') textureNode.image = img textureNode.extension = 'CLIP' textureNode.show_texture = True textureNode.location = (-400, 200) # Create BSDF diffuse node diffuseNode = node_tree.nodes.new('ShaderNodeBsdfPrincipled')#ShaderNodeBsdfDiffuse diffuseNode.location = (0, 200) # Create output node outputNode = node_tree.nodes.new('ShaderNodeOutputMaterial') outputNode.location = (400, 200) # Connect the nodes node_tree.links.new(uvMapNode.outputs['UV'] , textureNode.inputs['Vector']) node_tree.links.new(textureNode.outputs['Color'] , diffuseNode.inputs['Base Color'])#diffuseNode.inputs['Color']) node_tree.links.new(diffuseNode.outputs['BSDF'] , outputNode.inputs['Surface']) class getBBOX(): '''Utilities to build BBOX object from various Blender context''' @staticmethod def fromObj(obj, applyTransform = True): '''Create a 3D BBOX from Blender object''' if applyTransform: boundPts = [obj.matrix_world @ Vector(corner) for corner in obj.bound_box] else: boundPts = obj.bound_box xmin = min([pt[0] for pt in boundPts]) xmax = max([pt[0] for pt in boundPts]) ymin = min([pt[1] for pt in boundPts]) ymax = max([pt[1] for pt in boundPts]) zmin = min([pt[2] for pt in boundPts]) zmax = max([pt[2] for pt in boundPts]) return BBOX(xmin=xmin, ymin=ymin, zmin=zmin, xmax=xmax, ymax=ymax, zmax=zmax) @classmethod def fromScn(cls, scn): '''Create a 3D BBOX from Blender Scene union of bounding box of all objects containing in the scene''' #objs = scn.collection.objects objs = [obj for obj in scn.collection.all_objects if obj.empty_display_type != 'IMAGE'] if len(objs) == 0: scnBbox = BBOX(0,0,0,0,0,0) else: scnBbox = cls.fromObj(objs[0]) for obj in objs: bbox = cls.fromObj(obj) scnBbox += bbox return scnBbox @staticmethod def fromBmesh(bm): '''Create a 3D bounding box from a bmesh object''' xmin = min([pt.co.x for pt in bm.verts]) xmax = max([pt.co.x for pt in bm.verts]) ymin = min([pt.co.y for pt in bm.verts]) ymax = max([pt.co.y for pt in bm.verts]) zmin = min([pt.co.z for pt in bm.verts]) zmax = max([pt.co.z for pt in bm.verts]) # return BBOX(xmin=xmin, ymin=ymin, zmin=zmin, xmax=xmax, ymax=ymax, zmax=zmax) @staticmethod def fromTopView(context): '''Create a 2D BBOX from Blender 3dview if the view is top left ortho else return None''' scn = context.scene area = context.area if area.type != 'VIEW_3D': return None reg = context.region reg3d = context.region_data if reg3d.view_perspective != 'ORTHO' or tuple(reg3d.view_matrix.to_euler()) != (0,0,0): print("View3d must be in top ortho") return None # loc = mouseTo3d(context, area.width, area.height) xmax, ymax = loc.x, loc.y # loc = mouseTo3d(context, 0, 0) xmin, ymin = loc.x, loc.y # return BBOX(xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax) ================================================ FILE: operators/utils/delaunay_voronoi.py ================================================ # -*- coding: utf-8 -*- ############################################################################# # # Voronoi diagram calculator/ Delaunay triangulator # # - Voronoi Diagram Sweepline algorithm and C code by Steven Fortune, 1987, http://ect.bell-labs.com/who/sjf/ # - Python translation to file voronoi.py by Bill Simons, 2005, http://www.oxfish.com/ # - Additional changes for QGIS by Carson Farmer added November 2010 # - 2012 Ported to Python 3 and additional clip functions by domlysz at gmail.com # # Calculate Delaunay triangulation or the Voronoi polygons for a set of # 2D input points. # # Derived from code bearing the following notice: # # The author of this software is Steven Fortune. Copyright (c) 1994 by AT&T # Bell Laboratories. # Permission to use, copy, modify, and distribute this software for any # purpose without fee is hereby granted, provided that this entire notice # is included in all copies of any software which is or includes a copy # or modification of this software and in all copies of the supporting # documentation for such software. # THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR IMPLIED # WARRANTY. IN PARTICULAR, NEITHER THE AUTHORS NOR AT&T MAKE ANY # REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE MERCHANTABILITY # OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR PURPOSE. # # Comments were incorporated from Shane O'Sullivan's translation of the # original code into C++ (http://mapviewer.skynet.ie/voronoi.html) # # Steve Fortune's homepage: http://netlib.bell-labs.com/cm/cs/who/sjf/index.html # # # # For programmatic use two functions are available: # # computeVoronoiDiagram(points, xBuff, yBuff, polygonsOutput=False, formatOutput=False) : # Takes : # - a list of point objects (which must have x and y fields). # - x and y buffer values which are the expansion percentages of the bounding box rectangle including all input points. # Returns : # - With default options : # A list of 2-tuples, representing the two points of each Voronoi diagram edge. # Each point contains 2-tuples which are the x,y coordinates of point. # if formatOutput is True, returns : # - a list of 2-tuples, which are the x,y coordinates of the Voronoi diagram vertices. # - and a list of 2-tuples (v1, v2) representing edges of the Voronoi diagram. # v1 and v2 are the indices of the vertices at the end of the edge. # - If polygonsOutput option is True, returns : # A dictionary of polygons, keys are the indices of the input points, # values contains n-tuples representing the n points of each Voronoi diagram polygon. # Each point contains 2-tuples which are the x,y coordinates of point. # if formatOutput is True, returns : # - A list of 2-tuples, which are the x,y coordinates of the Voronoi diagram vertices. # - and a dictionary of input points indices. Values contains n-tuples representing the n points of each Voronoi diagram polygon. # Each tuple contains the vertex indices of the polygon vertices. # # computeDelaunayTriangulation(points): # Takes a list of point objects (which must have x and y fields). # Returns a list of 3-tuples: the indices of the points that form a Delaunay triangle. # ############################################################################# import math import sys import getopt TOLERANCE = 1e-9 BIG_FLOAT = 1e38 if sys.version > '3': PY3 = True else: PY3 = False #------------------------------------------------------------------ class Context(object): def __init__(self): self.doPrint = 0 self.debug = 0 self.extent=()#tuple (xmin, xmax, ymin, ymax) self.triangulate = False self.vertices = [] # list of vertex 2-tuples: (x,y) self.lines = [] # equation of line 3-tuple (a b c), for the equation of the line a*x+b*y = c self.edges = [] # edge 3-tuple: (line index, vertex 1 index, vertex 2 index) if either vertex index is -1, the edge extends to infinity self.triangles = [] # 3-tuple of vertex indices self.polygons = {} # a dict of site:[edges] pairs ########Clip functions######## def getClipEdges(self): xmin, xmax, ymin, ymax = self.extent clipEdges=[] for edge in self.edges: equation=self.lines[edge[0]]#line equation if edge[1]!=-1 and edge[2]!=-1:#finite line x1, y1=self.vertices[edge[1]][0], self.vertices[edge[1]][1] x2, y2=self.vertices[edge[2]][0], self.vertices[edge[2]][1] pt1, pt2 = (x1,y1), (x2,y2) inExtentP1, inExtentP2 = self.inExtent(x1,y1), self.inExtent(x2,y2) if inExtentP1 and inExtentP2: clipEdges.append((pt1, pt2)) elif inExtentP1 and not inExtentP2: pt2=self.clipLine(x1, y1, equation, leftDir=False) clipEdges.append((pt1, pt2)) elif not inExtentP1 and inExtentP2: pt1=self.clipLine(x2, y2, equation, leftDir=True) clipEdges.append((pt1, pt2)) else:#infinite line if edge[1]!=-1: x1, y1 = self.vertices[edge[1]][0], self.vertices[edge[1]][1] leftDir=False else: x1, y1 = self.vertices[edge[2]][0], self.vertices[edge[2]][1] leftDir=True if self.inExtent(x1,y1): pt1=(x1,y1) pt2=self.clipLine(x1, y1, equation, leftDir) clipEdges.append((pt1, pt2)) return clipEdges def getClipPolygons(self, closePoly): xmin, xmax, ymin, ymax = self.extent poly={} for inPtsIdx, edges in self.polygons.items(): clipEdges=[] for edge in edges: equation=self.lines[edge[0]]#line equation if edge[1]!=-1 and edge[2]!=-1:#finite line x1, y1=self.vertices[edge[1]][0], self.vertices[edge[1]][1] x2, y2=self.vertices[edge[2]][0], self.vertices[edge[2]][1] pt1, pt2 = (x1,y1), (x2,y2) inExtentP1, inExtentP2 = self.inExtent(x1,y1), self.inExtent(x2,y2) if inExtentP1 and inExtentP2: clipEdges.append((pt1, pt2)) elif inExtentP1 and not inExtentP2: pt2=self.clipLine(x1, y1, equation, leftDir=False) clipEdges.append((pt1, pt2)) elif not inExtentP1 and inExtentP2: pt1=self.clipLine(x2, y2, equation, leftDir=True) clipEdges.append((pt1, pt2)) else:#infinite line if edge[1]!=-1: x1, y1 = self.vertices[edge[1]][0], self.vertices[edge[1]][1] leftDir=False else: x1, y1 = self.vertices[edge[2]][0], self.vertices[edge[2]][1] leftDir=True if self.inExtent(x1,y1): pt1=(x1,y1) pt2=self.clipLine(x1, y1, equation, leftDir) clipEdges.append((pt1, pt2)) #create polygon definition from edges and check if polygon is completely closed polyPts, complete=self.orderPts(clipEdges) if not complete: startPt=polyPts[0] endPt=polyPts[-1] if startPt[0]==endPt[0] or startPt[1]==endPt[1]: #if start & end points are collinear then they are along an extent border polyPts.append(polyPts[0])#simple close else:#close at extent corner if (startPt[0]==xmin and endPt[1]==ymax) or (endPt[0]==xmin and startPt[1]==ymax): #upper left polyPts.append((xmin, ymax))#corner point polyPts.append(polyPts[0])#close polygon if (startPt[0]==xmax and endPt[1]==ymax) or (endPt[0]==xmax and startPt[1]==ymax): #upper right polyPts.append((xmax, ymax)) polyPts.append(polyPts[0]) if (startPt[0]==xmax and endPt[1]==ymin) or (endPt[0]==xmax and startPt[1]==ymin): #bottom right polyPts.append((xmax, ymin)) polyPts.append(polyPts[0]) if (startPt[0]==xmin and endPt[1]==ymin) or (endPt[0]==xmin and startPt[1]==ymin): #bottom left polyPts.append((xmin, ymin)) polyPts.append(polyPts[0]) if not closePoly:#unclose polygon polyPts=polyPts[:-1] poly[inPtsIdx]=polyPts return poly def clipLine(self, x1, y1, equation, leftDir): xmin, xmax, ymin, ymax = self.extent a,b,c=equation if b==0:#vertical line if leftDir:#left is bottom of vertical line return (x1,ymax) else: return (x1,ymin) elif a==0:#horizontal line if leftDir: return (xmin,y1) else: return (xmax,y1) else: y2_at_xmin=(c-a*xmin)/b y2_at_xmax=(c-a*xmax)/b x2_at_ymin=(c-b*ymin)/a x2_at_ymax=(c-b*ymax)/a intersectPts=[] if ymin<=y2_at_xmin<=ymax:#valid intersect point intersectPts.append((xmin, y2_at_xmin)) if ymin<=y2_at_xmax<=ymax: intersectPts.append((xmax, y2_at_xmax)) if xmin<=x2_at_ymin<=xmax: intersectPts.append((x2_at_ymin, ymin)) if xmin<=x2_at_ymax<=xmax: intersectPts.append((x2_at_ymax, ymax)) #delete duplicate (happens if intersect point is at extent corner) intersectPts=set(intersectPts) #choose target intersect point if leftDir: pt=min(intersectPts)#smaller x value else: pt=max(intersectPts) return pt def inExtent(self, x, y): xmin, xmax, ymin, ymax = self.extent return x>=xmin and x<=xmax and y>=ymin and y<=ymax def orderPts(self, edges): poly=[]#returned polygon points list [pt1, pt2, pt3, pt4 ....] pts=[] #get points list for edge in edges: pts.extend([pt for pt in edge]) #try to get start & end point try: startPt, endPt = [pt for pt in pts if pts.count(pt)<2]#start and end point aren't duplicate except:#all points are duplicate --> polygon is complete --> append some or other edge points complete=True firstIdx=0 poly.append(edges[0][0]) poly.append(edges[0][1]) else:#incomplete --> append the first edge points complete=False #search first edge for i, edge in enumerate(edges): if startPt in edge:#find firstIdx=i break poly.append(edges[firstIdx][0]) poly.append(edges[firstIdx][1]) if poly[0]!=startPt: poly.reverse() #append next points in list del edges[firstIdx] while edges:#all points will be treated when edges list will be empty currentPt = poly[-1]#last item for i, edge in enumerate(edges): if currentPt==edge[0]: poly.append(edge[1]) break elif currentPt==edge[1]: poly.append(edge[0]) break del edges[i] return poly, complete def setClipBuffer(self, xpourcent, ypourcent): xmin, xmax, ymin, ymax = self.extent witdh=xmax-xmin height=ymax-ymin xmin=xmin-witdh*xpourcent/100 xmax=xmax+witdh*xpourcent/100 ymin=ymin-height*ypourcent/100 ymax=ymax+height*ypourcent/100 self.extent=xmin, xmax, ymin, ymax ########End clip functions######## def outSite(self,s): if(self.debug): print("site (%d) at %f %f" % (s.sitenum, s.x, s.y)) elif(self.triangulate): pass elif(self.doPrint): print("s %f %f" % (s.x, s.y)) def outVertex(self,s): self.vertices.append((s.x,s.y)) if(self.debug): print("vertex(%d) at %f %f" % (s.sitenum, s.x, s.y)) elif(self.triangulate): pass elif(self.doPrint): print("v %f %f" % (s.x,s.y)) def outTriple(self,s1,s2,s3): self.triangles.append((s1.sitenum, s2.sitenum, s3.sitenum)) if(self.debug): print("circle through left=%d right=%d bottom=%d" % (s1.sitenum, s2.sitenum, s3.sitenum)) elif(self.triangulate and self.doPrint): print("%d %d %d" % (s1.sitenum, s2.sitenum, s3.sitenum)) def outBisector(self,edge): self.lines.append((edge.a, edge.b, edge.c)) if(self.debug): print("line(%d) %gx+%gy=%g, bisecting %d %d" % (edge.edgenum, edge.a, edge.b, edge.c, edge.reg[0].sitenum, edge.reg[1].sitenum)) elif(self.doPrint): print("l %f %f %f" % (edge.a, edge.b, edge.c)) def outEdge(self,edge): sitenumL = -1 if edge.ep[Edge.LE] is not None: sitenumL = edge.ep[Edge.LE].sitenum sitenumR = -1 if edge.ep[Edge.RE] is not None: sitenumR = edge.ep[Edge.RE].sitenum #polygons dict add by CF if edge.reg[0].sitenum not in self.polygons: self.polygons[edge.reg[0].sitenum] = [] if edge.reg[1].sitenum not in self.polygons: self.polygons[edge.reg[1].sitenum] = [] self.polygons[edge.reg[0].sitenum].append((edge.edgenum,sitenumL,sitenumR)) self.polygons[edge.reg[1].sitenum].append((edge.edgenum,sitenumL,sitenumR)) self.edges.append((edge.edgenum,sitenumL,sitenumR)) if(not self.triangulate): if(self.doPrint): print("e %d" % edge.edgenum) print(" %d " % sitenumL) print("%d" % sitenumR) #------------------------------------------------------------------ def voronoi(siteList,context): context.extent=siteList.extent edgeList = EdgeList(siteList.xmin,siteList.xmax,len(siteList)) priorityQ = PriorityQueue(siteList.ymin,siteList.ymax,len(siteList)) siteIter = siteList.iterator() bottomsite = siteIter.next() context.outSite(bottomsite) newsite = siteIter.next() minpt = Site(-BIG_FLOAT,-BIG_FLOAT) while True: if not priorityQ.isEmpty(): minpt = priorityQ.getMinPt() if (newsite and (priorityQ.isEmpty() or newsite top.y: bot,top = top,bot pm = Edge.RE # Create an Edge (or line) that is between the two Sites. This # creates the formula of the line, and assigns a line number to it edge = Edge.bisect(bot, top) context.outBisector(edge) # create a HE from the edge bisector = Halfedge(edge, pm) # insert the new bisector to the right of the left HE # set one endpoint to the new edge to be the vector point 'v' # If the site to the left of this bisector is higher than the right # Site, then this endpoint is put in position 0; otherwise in pos 1 edgeList.insert(llbnd, bisector) if edge.setEndpoint(Edge.RE - pm, v): context.outEdge(edge) # if left HE and the new bisector don't intersect, then delete # the left HE, and reinsert it p = llbnd.intersect(bisector) if p is not None: priorityQ.delete(llbnd); priorityQ.insert(llbnd, p, bot.distance(p)) # if right HE and the new bisector don't intersect, then reinsert it p = bisector.intersect(rrbnd) if p is not None: priorityQ.insert(bisector, p, bot.distance(p)) else: break he = edgeList.leftend.right while he is not edgeList.rightend: context.outEdge(he.edge) he = he.right Edge.EDGE_NUM = 0#CF #------------------------------------------------------------------ def isEqual(a,b,relativeError=TOLERANCE): # is nearly equal to within the allowed relative error norm = max(abs(a),abs(b)) return (norm < relativeError) or (abs(a - b) < (relativeError * norm)) #------------------------------------------------------------------ class Site(object): def __init__(self,x=0.0,y=0.0,sitenum=0): self.x = x self.y = y self.sitenum = sitenum def dump(self): print("Site #%d (%g, %g)" % (self.sitenum,self.x,self.y)) def __lt__(self,other): if self.y < other.y: return True elif self.y > other.y: return False elif self.x < other.x: return True elif self.x > other.x: return False else: return False def __eq__(self,other): if self.y == other.y and self.x == other.x: return True def distance(self,other): dx = self.x - other.x dy = self.y - other.y return math.sqrt(dx*dx + dy*dy) #------------------------------------------------------------------ class Edge(object): LE = 0#left end indice --> edge.ep[Edge.LE] RE = 1#right end indice EDGE_NUM = 0 DELETED = {} # marker value def __init__(self): self.a = 0.0#equation of the line a*x+b*y = c self.b = 0.0 self.c = 0.0 self.ep = [None,None]#end point (2 tuples of site) self.reg = [None,None] self.edgenum = 0 def dump(self): print("(#%d a=%g, b=%g, c=%g)" % (self.edgenum,self.a,self.b,self.c)) print("ep",self.ep) print("reg",self.reg) def setEndpoint(self, lrFlag, site): self.ep[lrFlag] = site if self.ep[Edge.RE - lrFlag] is None: return False return True @staticmethod def bisect(s1,s2): newedge = Edge() newedge.reg[0] = s1 # store the sites that this edge is bisecting newedge.reg[1] = s2 # to begin with, there are no endpoints on the bisector - it goes to infinity # ep[0] and ep[1] are None # get the difference in x dist between the sites dx = float(s2.x - s1.x) dy = float(s2.y - s1.y) adx = abs(dx) # make sure that the difference in positive ady = abs(dy) # get the slope of the line newedge.c = float(s1.x * dx + s1.y * dy + (dx*dx + dy*dy)*0.5) if adx > ady : # set formula of line, with x fixed to 1 newedge.a = 1.0 newedge.b = dy/dx newedge.c /= dx else: # set formula of line, with y fixed to 1 newedge.b = 1.0 newedge.a = dx/dy newedge.c /= dy newedge.edgenum = Edge.EDGE_NUM Edge.EDGE_NUM += 1 return newedge #------------------------------------------------------------------ class Halfedge(object): def __init__(self,edge=None,pm=Edge.LE): self.left = None # left Halfedge in the edge list self.right = None # right Halfedge in the edge list self.qnext = None # priority queue linked list pointer self.edge = edge # edge list Edge self.pm = pm self.vertex = None # Site() self.ystar = BIG_FLOAT def dump(self): print("Halfedge--------------------------") print("left: ", self.left) print("right: ", self.right) print("edge: ", self.edge) print("pm: ", self.pm) print("vertex: "), if self.vertex: self.vertex.dump() else: print("None") print("ystar: ", self.ystar) def __lt__(self,other): if self.ystar < other.ystar: return True elif self.ystar > other.ystar: return False elif self.vertex.x < other.vertex.x: return True elif self.vertex.x > other.vertex.x: return False else: return False def __eq__(self,other): if self.ystar == other.ystar and self.vertex.x == other.vertex.x: return True def leftreg(self,default): if not self.edge: return default elif self.pm == Edge.LE: return self.edge.reg[Edge.LE] else: return self.edge.reg[Edge.RE] def rightreg(self,default): if not self.edge: return default elif self.pm == Edge.LE: return self.edge.reg[Edge.RE] else: return self.edge.reg[Edge.LE] # returns True if p is to right of halfedge self def isPointRightOf(self,pt): e = self.edge topsite = e.reg[1] right_of_site = pt.x > topsite.x if(right_of_site and self.pm == Edge.LE): return True if(not right_of_site and self.pm == Edge.RE): return False if(e.a == 1.0): dyp = pt.y - topsite.y dxp = pt.x - topsite.x fast = 0; if ((not right_of_site and e.b < 0.0) or (right_of_site and e.b >= 0.0)): above = dyp >= e.b * dxp fast = above else: above = pt.x + pt.y * e.b > e.c if(e.b < 0.0): above = not above if (not above): fast = 1 if (not fast): dxs = topsite.x - (e.reg[0]).x above = e.b * (dxp*dxp - dyp*dyp) < dxs*dyp*(1.0+2.0*dxp/dxs + e.b*e.b) if(e.b < 0.0): above = not above else: # e.b == 1.0 yl = e.c - e.a * pt.x t1 = pt.y - yl t2 = pt.x - topsite.x t3 = yl - topsite.y above = t1*t1 > t2*t2 + t3*t3 if(self.pm==Edge.LE): return above else: return not above #-------------------------- # create a new site where the Halfedges el1 and el2 intersect def intersect(self,other): e1 = self.edge e2 = other.edge if (e1 is None) or (e2 is None): return None # if the two edges bisect the same parent return None if e1.reg[1] is e2.reg[1]: return None d = e1.a * e2.b - e1.b * e2.a if isEqual(d,0.0): return None xint = (e1.c*e2.b - e2.c*e1.b) / d yint = (e2.c*e1.a - e1.c*e2.a) / d if e1.reg[1]< e2.reg[1]: he = self e = e1 else: he = other e = e2 rightOfSite = xint >= e.reg[1].x if((rightOfSite and he.pm == Edge.LE) or (not rightOfSite and he.pm == Edge.RE)): return None # create a new site at the point of intersection - this is a new # vector event waiting to happen return Site(xint,yint) #------------------------------------------------------------------ class EdgeList(object): def __init__(self,xmin,xmax,nsites): if xmin > xmax: xmin,xmax = xmax,xmin self.hashsize = int(2*math.sqrt(nsites+4)) self.xmin = xmin self.deltax = float(xmax - xmin) self.hash = [None]*self.hashsize self.leftend = Halfedge() self.rightend = Halfedge() self.leftend.right = self.rightend self.rightend.left = self.leftend self.hash[0] = self.leftend self.hash[-1] = self.rightend def insert(self,left,he): he.left = left he.right = left.right left.right.left = he left.right = he def delete(self,he): he.left.right = he.right he.right.left = he.left he.edge = Edge.DELETED # Get entry from hash table, pruning any deleted nodes def gethash(self,b): if(b < 0 or b >= self.hashsize): return None he = self.hash[b] if he is None or he.edge is not Edge.DELETED: return he # Hash table points to deleted half edge. Patch as necessary. self.hash[b] = None return None def leftbnd(self,pt): # Use hash table to get close to desired halfedge bucket = int(((pt.x - self.xmin)/self.deltax * self.hashsize)) if(bucket < 0): bucket =0; if(bucket >=self.hashsize): bucket = self.hashsize-1 he = self.gethash(bucket) if(he is None): i = 1 while True: he = self.gethash(bucket-i) if (he is not None): break; he = self.gethash(bucket+i) if (he is not None): break; i += 1 # Now search linear list of halfedges for the corect one if (he is self.leftend) or (he is not self.rightend and he.isPointRightOf(pt)): he = he.right while he is not self.rightend and he.isPointRightOf(pt): he = he.right he = he.left; else: he = he.left while (he is not self.leftend and not he.isPointRightOf(pt)): he = he.left # Update hash table and reference counts if(bucket > 0 and bucket < self.hashsize-1): self.hash[bucket] = he return he #------------------------------------------------------------------ class PriorityQueue(object): def __init__(self,ymin,ymax,nsites): self.ymin = ymin self.deltay = ymax - ymin self.hashsize = int(4 * math.sqrt(nsites)) self.count = 0 self.minidx = 0 self.hash = [] for i in range(self.hashsize): self.hash.append(Halfedge()) def __len__(self): return self.count def isEmpty(self): return self.count == 0 def insert(self,he,site,offset): he.vertex = site he.ystar = site.y + offset last = self.hash[self.getBucket(he)] next = last.qnext while((next is not None) and he > next): last = next next = last.qnext he.qnext = last.qnext last.qnext = he self.count += 1 def delete(self,he): if (he.vertex is not None): last = self.hash[self.getBucket(he)] while last.qnext is not he: last = last.qnext last.qnext = he.qnext self.count -= 1 he.vertex = None def getBucket(self,he): bucket = int(((he.ystar - self.ymin) / self.deltay) * self.hashsize) if bucket < 0: bucket = 0 if bucket >= self.hashsize: bucket = self.hashsize-1 if bucket < self.minidx: self.minidx = bucket return bucket def getMinPt(self): while(self.hash[self.minidx].qnext is None): self.minidx += 1 he = self.hash[self.minidx].qnext x = he.vertex.x y = he.ystar return Site(x,y) def popMinHalfedge(self): curr = self.hash[self.minidx].qnext self.hash[self.minidx].qnext = curr.qnext self.count -= 1 return curr #------------------------------------------------------------------ class SiteList(object): def __init__(self,pointList): self.__sites = [] self.__sitenum = 0 self.__xmin = min([pt.x for pt in pointList]) self.__ymin = min([pt.y for pt in pointList]) self.__xmax = max([pt.x for pt in pointList]) self.__ymax = max([pt.y for pt in pointList]) self.__extent=(self.__xmin, self.__xmax, self.__ymin, self.__ymax) for i,pt in enumerate(pointList): self.__sites.append(Site(pt.x,pt.y,i)) self.__sites.sort() def setSiteNumber(self,site): site.sitenum = self.__sitenum self.__sitenum += 1 class Iterator(object): def __init__(this,lst): this.generator = (s for s in lst) def __iter__(this): return this def next(this): try: if PY3: return this.generator.__next__() else: return this.generator.next() except StopIteration: return None def iterator(self): return SiteList.Iterator(self.__sites) def __iter__(self): return SiteList.Iterator(self.__sites) def __len__(self): return len(self.__sites) def _getxmin(self): return self.__xmin def _getymin(self): return self.__ymin def _getxmax(self): return self.__xmax def _getymax(self): return self.__ymax def _getextent(self): return self.__extent xmin = property(_getxmin) ymin = property(_getymin) xmax = property(_getxmax) ymax = property(_getymax) extent = property(_getextent) #------------------------------------------------------------------ def computeVoronoiDiagram(points, xBuff=0, yBuff=0, polygonsOutput=False, formatOutput=False, closePoly=True): """ Takes : - a list of point objects (which must have x and y fields). - x and y buffer values which are the expansion percentages of the bounding box rectangle including all input points. Returns : - With default options : A list of 2-tuples, representing the two points of each Voronoi diagram edge. Each point contains 2-tuples which are the x,y coordinates of point. if formatOutput is True, returns : - a list of 2-tuples, which are the x,y coordinates of the Voronoi diagram vertices. - and a list of 2-tuples (v1, v2) representing edges of the Voronoi diagram. v1 and v2 are the indices of the vertices at the end of the edge. - If polygonsOutput option is True, returns : A dictionary of polygons, keys are the indices of the input points, values contains n-tuples representing the n points of each Voronoi diagram polygon. Each point contains 2-tuples which are the x,y coordinates of point. if formatOutput is True, returns : - A list of 2-tuples, which are the x,y coordinates of the Voronoi diagram vertices. - and a dictionary of input points indices. Values contains n-tuples representing the n points of each Voronoi diagram polygon. Each tuple contains the vertex indices of the polygon vertices. - if closePoly is True then, in the list of points of a polygon, last point will be the same of first point """ siteList = SiteList(points) context = Context() voronoi(siteList,context) context.setClipBuffer(xBuff, yBuff) if not polygonsOutput: clipEdges=context.getClipEdges() if formatOutput: vertices, edgesIdx = formatEdgesOutput(clipEdges) return vertices, edgesIdx else: return clipEdges else: clipPolygons=context.getClipPolygons(closePoly) if formatOutput: vertices, polyIdx = formatPolygonsOutput(clipPolygons) return vertices, polyIdx else: return clipPolygons def formatEdgesOutput(edges): #get list of points pts=[] for edge in edges: pts.extend(edge) #get unique values pts=set(pts)#unique values (tuples are hashable) #get dict {values:index} valuesIdxDict = dict(zip(pts,range(len(pts)))) #get edges index reference edgesIdx=[] for edge in edges: edgesIdx.append([valuesIdxDict[pt] for pt in edge]) return list(pts), edgesIdx def formatPolygonsOutput(polygons): #get list of points pts=[] for poly in polygons.values(): pts.extend(poly) #get unique values pts=set(pts)#unique values (tuples are hashable) #get dict {values:index} valuesIdxDict = dict(zip(pts,range(len(pts)))) #get polygons index reference polygonsIdx={} for inPtsIdx, poly in polygons.items(): polygonsIdx[inPtsIdx]=[valuesIdxDict[pt] for pt in poly] return list(pts), polygonsIdx #------------------------------------------------------------------ def computeDelaunayTriangulation(points): """ Takes a list of point objects (which must have x and y fields). Returns a list of 3-tuples: the indices of the points that form a Delaunay triangle. """ siteList = SiteList(points) context = Context() context.triangulate = True voronoi(siteList,context) return context.triangles #----------------------------------------------------------------------------- #if __name__=="__main__": ================================================ FILE: operators/utils/georaster_utils.py ================================================ # -*- coding:utf-8 -*- # This file is part of BlenderGIS # ***** GPL LICENSE BLOCK ***** # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # All rights reserved. # ***** GPL LICENSE BLOCK ***** import os import numpy as np import bpy, bmesh import math import logging log = logging.getLogger(__name__) from ...core.georaster import GeoRaster def _exportAsMesh(georaster, dx=0, dy=0, step=1, buildFaces=True, flat=False, subset=False, reproj=None): '''Numpy test''' if subset and georaster.subBoxGeo is None: subset = False if not subset: georef = georaster.georef else: georef = georaster.getSubBoxGeoRef() x0, y0 = georef.origin #pxcenter pxSizeX, pxSizeY = georef.pxSize.x, georef.pxSize.y w, h = georef.rSize.x, georef.rSize.y #adjust against step w, h = math.ceil(w/step), math.ceil(h/step) pxSizeX, pxSizeY = pxSizeX * step, pxSizeY * step x = np.array([(x0 + (pxSizeX * i)) - dx for i in range(0, w)]) y = np.array([(y0 + (pxSizeY * i)) - dy for i in range(0, h)]) xx, yy = np.meshgrid(x, y) #TODO reproj if flat: zz = np.zeros((h, w)) else: zz = georaster.readAsNpArray(subset=subset).data[::step,::step] #TODO raise error if multiband verts = np.column_stack((xx.ravel(), yy.ravel(), zz.ravel())) if buildFaces: faces = [(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)] else: faces = [] mesh = bpy.data.meshes.new("DEM") mesh.from_pydata(verts, [], faces) mesh.update() return mesh def exportAsMesh(georaster, dx=0, dy=0, step=1, buildFaces=True, subset=False, reproj=None, flat=False): if subset and georaster.subBoxGeo is None: subset = False if not subset: georef = georaster.georef else: georef = georaster.getSubBoxGeoRef() if not flat: img = georaster.readAsNpArray(subset=subset) #TODO raise error if multiband data = img.data x0, y0 = georef.origin #pxcenter pxSizeX, pxSizeY = georef.pxSize.x, georef.pxSize.y w, h = georef.rSize.x, georef.rSize.y #Build the mesh (Note : avoid using bmesh because it's very slow with large mesh, use from_pydata instead) verts = [] faces = [] nodata = [] idxMap = {} for py in range(0, h, step): for px in range(0, w, step): x = x0 + (pxSizeX * px) y = y0 + (pxSizeY * py) if reproj is not None: x, y = reproj.pt(x, y) #shift x -= dx y -= dy if flat: z = 0 else: z = data[py, px] #vertex index v1 = px + py * w #bottom right #Filter nodata if z == georaster.noData: nodata.append(v1) else: verts.append((x, y, z)) idxMap[v1] = len(verts) - 1 #build face from bottomright to topright (using only points already created) if buildFaces and px > 0 and py > 0: #filter first row and column v2 = v1 - step #bottom left v3 = v2 - w * step #topleft v4 = v3 + step #topright f = [v4, v3, v2, v1] #anticlockwise --> face up if not any(v in f for v in nodata): #TODO too slow ? f = [idxMap[v] for v in f] faces.append(f) mesh = bpy.data.meshes.new("DEM") mesh.from_pydata(verts, [], faces) mesh.update() return mesh def rasterExtentToMesh(name, rast, dx, dy, pxLoc='CORNER', reproj=None, subdivise=False): '''Build a new mesh that represent a georaster extent''' #create mesh bm = bmesh.new() if pxLoc == 'CORNER': pts = [(pt[0], pt[1]) for pt in rast.corners]#shift coords elif pxLoc == 'CENTER': pts = [(pt[0], pt[1]) for pt in rast.cornersCenter] #Reprojection if reproj is not None: pts = reproj.pts(pts) #build shifted flat 3d vertices pts = [bm.verts.new((pt[0]-dx, pt[1]-dy, 0)) for pt in pts]#upper left to botton left (clockwise) pts.reverse()#bottom left to upper left (anticlockwise --> face up) bm.faces.new(pts) #Create mesh from bmesh mesh = bpy.data.meshes.new(name) bm.to_mesh(mesh) bm.free() return mesh def geoRastUVmap(obj, uvLayer, rast, dx, dy, reproj=None): '''uv map a georaster texture on a given mesh''' mesh = obj.data #Assign uv coords loc = obj.location for pg in mesh.polygons: for i in pg.loop_indices: vertIdx = mesh.loops[i].vertex_index pt = list(mesh.vertices[vertIdx].co) #adjust coords against object location and shift values to retrieve original point coords pt = (pt[0] + loc.x + dx, pt[1] + loc.y + dy) if reproj is not None: pt = reproj.pt(*pt) #Compute UV coords --> pourcent from image origin (bottom left) dx_px, dy_px = rast.pxFromGeo(pt[0], pt[1], reverseY=True, round2Floor=False) u = dx_px / rast.size[0] v = dy_px / rast.size[1] #Assign coords #uvLoop = uvLoopLayer.data[i] #uvLoop.uv = [u,v] uvLayer.data[i].uv = [u,v] def setDisplacer(obj, rast, uvTxtLayer, mid=0, interpolation=False): #Config displacer displacer = obj.modifiers.new('DEM', type='DISPLACE') demTex = bpy.data.textures.new('demText', type = 'IMAGE') demTex.image = rast.bpyImg demTex.use_interpolation = interpolation demTex.extension = 'CLIP' demTex.use_clamp = False #Needed to get negative displacement with float32 texture displacer.texture = demTex displacer.texture_coords = 'UV' displacer.uv_layer = uvTxtLayer.name displacer.mid_level = mid #Texture values below this value will result in negative displacement #Setting the displacement strength : #displacement = (texture value - Midlevel) * Strength #>> Strength = displacement / texture value (because mid=0) #If DEM non scaled then # *displacement = alt max - alt min = delta Z # *texture value = delta Z / (2^depth-1) # (because in Blender, pixel values are normalized between 0.0 and 1.0) #>> Strength = delta Z / (delta Z / (2^depth-1)) #>> Strength = 2^depth-1 if rast.depth < 32: #8 or 16 bits unsigned values (signed int16 must be converted to float to be usuable) displacer.strength = 2**rast.depth-1 else: #32 bits values #with float raster, blender give directly raw float values(non normalied) #so a texture value of 100 simply give a displacement of 100 displacer.strength = 1 bpy.ops.object.shade_smooth() return displacer ######################################### class bpyGeoRaster(GeoRaster): def __init__(self, path, subBoxGeo=None, useGDAL=False, clip=False, fillNodata=False, raw=False): #First init parent class GeoRaster.__init__(self, path, subBoxGeo=subBoxGeo, useGDAL=useGDAL) #Before open the raster into blender we need to assert that the file can be correctly loaded and exploited #- it must be in a file format supported by Blender (jpeg, tiff, png, bmp, or jpeg2000) and not a GIS specific format #- it must not be coded in int16 because this datatype cannot be correctly handle as displacement texture (issue with negatives values) #- it must not be too large or it will overflow Blender memory #- it must does not contain nodata values because nodata is coded with a large value that will cause huge unwanted displacement if self.format not in ['GTiff', 'TIFF', 'BMP', 'PNG', 'JPEG', 'JPEG2000'] \ or (clip and self.subBoxGeo is not None) \ or fillNodata \ or self.ddtype == 'int16': #Open the raster as numpy array (read only a subset if we want to clip it) if clip: img = self.readAsNpArray(subset=True) else: img = self.readAsNpArray() #always cast to float because it's the more convenient datatype for displace texture #(will not be normalized from 0.0 to 1.0 in Blender) img.cast2float() #replace nodata with interpolated values if fillNodata: img.fillNodata() #save to a new tiff file on disk filepath = os.path.splitext(self.path)[0] + '_bgis.tif' img.save(filepath) #reinit the parent class GeoRaster.__init__(self, filepath, useGDAL=useGDAL) self.raw = raw #flag non color raster like DEM #Open the file into Blender self._load() def _load(self, pack=False): '''Load the georaster in Blender''' try: self.bpyImg = bpy.data.images.load(self.path) except Exception as e: log.error("Unable to open raster", exc_info=True) raise IOError("Unable to open raster") #it will not print traceback (instead of a bare raise) if pack: #WARN : packed image can only be stored as png and this format does not support float32 datatype self.bpyImg.pack() if self.raw: self.bpyImg.colorspace_settings.is_data = True def unload(self): self.bpyImg.user_clear() bpy.data.images.remove(self.bpyImg) self.bpyImg = None @property def isLoaded(self): '''Flag if the image has been loaded in Blender''' if self.bpyImg is not None: return True else: return False @property def isPacked(self): '''Flag if the image has been packed in Blender''' if self.bpyImg is not None: if len(self.bpyImg.packed_files) == 0: return False else: return True else: return False ############################################### # Old methods that use bpy.image.pixels and numpy, keeped here as history # depreciated because bpy is too slow and we need to process the image before load it in Blender ############################################### def toBitDepth(self, a): """ Convert Blender pixel intensity value (from 0.0 to 1.0) in true pixel value in initial image bit depth range """ return a * (2**self.depth - 1) def fromBitDepth(self, a): """ Convert true pixel value in initial image bit depth range to Blender pixel intensity value (from 0.0 to 1.0) """ return a / (2**self.depth - 1) def getPixelsArray(self, bandIdx=None, subset=False): ''' Use bpy to extract pixels values as numpy array In numpy fist dimension of a 2D matrix represents rows (y) and second dimension represents cols (x) so to get pixel value at a specified location be careful not confusing axes: data[row, column] It's possible to swap axes if you prefere accessing values with [x,y] indices instead of [y,x]: data.swapaxes(0,1) Array origin is top left ''' if not self.isLoaded: raise IOError("Can read only image opened in Blender") if self.ddtype is None: raise IOError("Undefined data type") if subset and self.subBoxGeo is None: return None nbBands = self.bpyImg.channels #Blender will return 4 channels even with a one band tiff # Make a first Numpy array in one dimension a = 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 # Regroup rgba values a = a.reshape(len(a)/nbBands, nbBands)#[[r,g,b,a],[r,g,b,a],[r,g,b,a],[r,g,b,a]...] # Build 2 dimensional array (In numpy first dimension represents rows (y) and second dimension represents cols (x)) a = a.reshape(self.size.y, self.size.x, nbBands)# [ [[rgba], [rgba]...], [lines2], [lines3]...] # Change origin to top left a = np.flipud(a) # Swap axes to access pixels with [x,y] indices instead of [y,x] ##a = a.swapaxes(0,1) # Extract the requested band if bandIdx is not None: a = a[:,:,bandIdx] # In blender, non float raster pixels values are normalized from 0.0 to 1.0 if not self.isFloat: # Multiply by 2**depth - 1 to get raw values a = self.toBitDepth(a) # Round the result to nearest int and cast to orginal data type # when cast signed 16 bits dataset, the negatives values are correctly interpreted by numpy a = np.rint(a).astype(self.ddtype) # Get the negatives values from signed int16 raster # This part is no longer needed because previous numpy's cast already did the job ''' if self.ddtype == 'int16': #16 bits allows coding values from 0 to 65535 (with 65535 == 2**depth / 2 - 1 ) #positives value are coded from 0 to 32767 (from 0.0 to 0.5 in Blender) #negatives values are coded in reverse order from 65535 to 32768 (1.0 to 0.5 in Blender) #corresponding to a range from -1 to -32768 a = np.where(a > 32767, -(65536-a), a) ''' if not subset: return a else: # Get overlay extent (in pixels) subBoxPx = self.subBoxPx # Get subset data (min and max pixel number are both include) a = a[subBoxPx.ymin:subBoxPx.ymax+1, subBoxPx.xmin:subBoxPx.xmax+1] #topleft to bottomright return a def flattenPixelsArray(self, px): ''' Flatten a 3d array of pixels to match the shape of bpy.pixels [ [[rgba], [rgba]...], [lines2], [lines3]...] >> [r,g,b,a,r,g,b,a,r,g,b,a, ... ] If the submited array contains only one band, then the band will be duplicate and an alpha band will be added to get all rgba values. ''' shape = px.shape if len(shape) == 2: px = np.expand_dims(px, axis=2) px = np.repeat(px, 3, axis=2) alpha = np.ones(shape) alpha = np.expand_dims(alpha, axis=2) px = np.append(px, alpha, axis=2) #px = px.swapaxes(0,1) px = np.flipud(px) px = px.flatten() return px ================================================ FILE: operators/view3d_mapviewer.py ================================================ # -*- coding:utf-8 -*- # ***** GPL LICENSE BLOCK ***** # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # All rights reserved. # ***** GPL LICENSE BLOCK ***** #built-in imports import math import os import threading import logging log = logging.getLogger(__name__) #bpy imports import bpy from mathutils import Vector from bpy.types import Operator, Panel, AddonPreferences from bpy.props import StringProperty, IntProperty, FloatProperty, BoolProperty, EnumProperty, FloatVectorProperty import addon_utils import gpu from gpu_extras.batch import batch_for_shader #core imports from ..core import HAS_GDAL, HAS_PIL, HAS_IMGIO from ..core.proj import reprojPt, reprojBbox, dd2meters, meters2dd from ..core.basemaps import GRIDS, SOURCES, MapService from ..core import settings USER_AGENT = settings.user_agent #bgis imports from ..geoscene import GeoScene, SK, georefManagerLayout from ..prefs import PredefCRS #utilities from .utils import getBBOX, mouseTo3d from .utils import placeObj, adjust3Dview, showTextures, rasterExtentToMesh, geoRastUVmap, addTexture #for export to mesh tool #OSM Nominatim API module #https://github.com/damianbraun/nominatim from .lib.osm.nominatim import nominatimQuery PKG, SUBPKG = __package__.split('.', maxsplit=1) #blendergis.basemaps #################### class BaseMap(GeoScene): """Handle a map as background image in Blender""" def __init__(self, context, srckey, laykey, grdkey=None): #Get context self.context = context self.scn = context.scene GeoScene.__init__(self, self.scn) self.area = context.area self.area3d = [r for r in self.area.regions if r.type == 'WINDOW'][0] self.view3d = self.area.spaces.active self.reg3d = self.view3d.region_3d #Get cache destination folder in addon preferences prefs = context.preferences.addons[PKG].preferences cacheFolder = prefs.cacheFolder self.synchOrj = prefs.synchOrj #Get resampling algo preference and set the constant MapService.RESAMP_ALG = prefs.resamplAlg #Init MapService class self.srv = MapService(srckey, cacheFolder) self.name = srckey + '_' + laykey + '_' + grdkey #Set destination tile matrix if grdkey is None: grdkey = self.srv.srcGridKey if grdkey == self.srv.srcGridKey: self.tm = self.srv.srcTms else: #Define destination grid in map service self.srv.setDstGrid(grdkey) self.tm = self.srv.dstTms #Init some geoscene props if needed if not self.hasCRS: self.crs = self.tm.CRS if not self.hasOriginPrj: self.setOriginPrj(0, 0, self.synchOrj) if not self.hasScale: self.scale = 1 if not self.hasZoom: self.zoom = 0 self.lockedZoom = None #Set path to tiles mosaic used as background image in Blender #We need a format that support transparency so jpg is exclude #Writing to tif is generally faster than writing to png if bpy.data.is_saved: folder = os.path.dirname(bpy.data.filepath) + os.sep ##folder = bpy.path.abspath("//")) else: ##folder = bpy.context.preferences.filepaths.temporary_directory #Blender crease a sub-directory within the temp directory, for each session, which is cleared on exit folder = bpy.app.tempdir self.imgPath = folder + self.name + ".tif" #Get layer def obj self.layer = self.srv.layers[laykey] #map keys self.srckey = srckey self.laykey = laykey self.grdkey = grdkey #Thread attributes self.thread = None #Background image attributes self.img = None #bpy image self.bkg = None #empty image obj self.viewDstZ = None #view 3d z distance #Store previous request #TODO def get(self): '''Launch run() function in a new thread''' self.stop() self.srv.start() self.thread = threading.Thread(target=self.run) self.thread.start() def stop(self): '''Stop actual thread''' if self.srv.running: self.srv.stop() self.thread.join() def run(self): """thread method""" self.mosaic = self.request() if self.srv.running and self.mosaic is not None: #save image self.mosaic.save(self.imgPath) if self.srv.running: #Place background image self.place() self.srv.stop() def moveOrigin(self, dx, dy, useScale=True, updObjLoc=True): '''Move scene origin and update props''' self.moveOriginPrj(dx, dy, useScale, updObjLoc, self.synchOrj) #geoscene function def request(self): '''Request map service to build a mosaic of required tiles to cover view3d area''' #Get area dimension w, h = self.area.width, self.area.height #w, h = self.area3d.width, self.area3d.height #WARN return [1,1] !!!!???? #Get area bbox coords in destination tile matrix crs (map origin is bottom lelf) #Method 1 : Get bbox coords in scene crs and then reproject the bbox if needed z = self.lockedZoom if self.lockedZoom is not None else self.zoom res = self.tm.getRes(z) if self.crs == 'EPSG:4326': res = meters2dd(res) dx, dy, dz = self.reg3d.view_location ox = self.crsx + (dx * self.scale) oy = self.crsy + (dy * self.scale) xmin = ox - w/2 * res * self.scale ymax = oy + h/2 * res * self.scale xmax = ox + w/2 * res * self.scale ymin = oy - h/2 * res * self.scale bbox = (xmin, ymin, xmax, ymax) #reproj bbox to destination grid crs if scene crs is different if self.crs != self.tm.CRS: bbox = reprojBbox(self.crs, self.tm.CRS, bbox) ''' #Method 2 bbox = getBBOX.fromTopView(self.context) #ERROR context is None ???? bbox = bbox.toGeo(geoscn=self) if self.crs != self.tm.CRS: bbox = reprojBbox(self.crs, self.tm.CRS, bbox) ''' log.debug('Bounding box request : {}'.format(bbox)) #Stop thread if the request is same as previous #TODO if self.srv.srcGridKey == self.grdkey: toDstGrid = False else: toDstGrid = True mosaic = self.srv.getImage(self.laykey, bbox, self.zoom, toDstGrid=toDstGrid, outCRS=self.crs) return mosaic def place(self): '''Set map as background image''' #Get or load bpy image try: self.img = [img for img in bpy.data.images if img.filepath == self.imgPath and len(img.packed_files) == 0][0] except IndexError: self.img = bpy.data.images.load(self.imgPath) #Get or load background image empties = [obj for obj in self.scn.objects if obj.type == 'EMPTY'] bkgs = [obj for obj in empties if obj.empty_display_type == 'IMAGE'] for bkg in bkgs: bkg.hide_viewport = True try: self.bkg = [bkg for bkg in bkgs if bkg.data.filepath == self.imgPath and len(bkg.data.packed_files) == 0][0] except IndexError: self.bkg = bpy.data.objects.new(self.name, None) #None will create an empty self.bkg.empty_display_type = 'IMAGE' self.bkg.empty_image_depth = 'BACK' self.bkg.data = self.img self.scn.collection.objects.link(self.bkg) else: self.bkg.hide_viewport = False #Get some image props img_ox, img_oy = self.mosaic.center img_w, img_h = self.mosaic.size res = self.mosaic.pxSize.x #res = self.tm.getRes(self.zoom) #Set background size sizex = img_w * res / self.scale sizey = img_h * res / self.scale size = max([sizex, sizey]) #self.bkg.empty_display_size = sizex #limited to 1000 self.bkg.empty_display_size = 1 #a size of 1 means image width=1bu self.bkg.scale = (size, size, 1) #Set background offset (image origin does not match scene origin) dx = (self.crsx - img_ox) / self.scale dy = (self.crsy - img_oy) / self.scale #self.bkg.empty_image_offset = [-0.5, -0.5] #in image unit space self.bkg.location = (-dx, -dy, 0) #ratio = img_w / img_h #self.bkg.offset_y = -dy * ratio #https://developer.blender.org/T48034 #Get 3d area's number of pixels and resulting size at the requested zoom level resolution #dst = max( [self.area3d.width, self.area3d.height] ) #WARN return [1,1] !!!!???? dst = max( [self.area.width, self.area.height] ) z = self.lockedZoom if self.lockedZoom is not None else self.zoom res = self.tm.getRes(z) dst = dst * res / self.scale #Compute 3dview FOV and needed z distance to see the maximum extent that #can be draw at full res (area 3d needs enough pixels otherwise the image will appears downgraded) #WARN seems these formulas does not works properly in Blender2.8 view3D_aperture = 36 #Blender constant (see source code) view3D_zoom = 2 #Blender constant (see source code) fov = 2 * math.atan(view3D_aperture / (self.view3d.lens*2) ) #fov equation fov = math.atan(math.tan(fov/2) * view3D_zoom) * 2 #zoom correction (see source code) zdst = (dst/2) / math.tan(fov/2) #trigo zdst = math.floor(zdst) #make sure no downgrade self.reg3d.view_distance = zdst self.viewDstZ = zdst #Update image drawing self.bkg.data.reload() #################################### def drawInfosText(self, context): #Get contexts scn = context.scene area = context.area area3d = [reg for reg in area.regions if reg.type == 'WINDOW'][0] view3d = area.spaces.active reg3d = view3d.region_3d #Get map props stored in scene geoscn = GeoScene(scn) zoom = geoscn.zoom scale = geoscn.scale # txt = "Map view : " txt += "Zoom " + str(zoom) if self.map.lockedZoom is not None: txt += " (Locked)" txt += " - Scale 1:" + str(int(scale)) ''' # view3d distance dst = reg3d.view_distance if dst > 1000: dst /= 1000 unit = 'km' else: unit = 'm' txt += ' 3D View distance ' + str(int(dst)) + ' ' + unit ''' # cursor crs coords txt += ' ' + str((int(self.posx), int(self.posy))) # progress txt += ' ' + self.progress context.area.header_text_set(txt) def drawZoomBox(self, context): if self.zoomBoxMode and not self.zoomBoxDrag: # before selection starts draw infinite cross px, py = self.zb_xmax, self.zb_ymax p1 = (0, py, 0) p2 = (context.area.width, py, 0) p3 = (px, 0, 0) p4 = (px, context.area.height, 0) coords = [p1, p2, p3, p4] shader = gpu.shader.from_builtin('UNIFORM_COLOR') batch = batch_for_shader(shader, 'LINES', {"pos": coords}) shader.bind() shader.uniform_float("color", (0, 0, 0, 1)) batch.draw(shader) elif self.zoomBoxMode and self.zoomBoxDrag: p1 = (self.zb_xmin, self.zb_ymin, 0) p2 = (self.zb_xmin, self.zb_ymax, 0) p3 = (self.zb_xmax, self.zb_ymax, 0) p4 = (self.zb_xmax, self.zb_ymin, 0) coords = [p1, p2, p2, p3, p3, p4, p4, p1] shader = gpu.shader.from_builtin('UNIFORM_COLOR') batch = batch_for_shader(shader, 'LINES', {"pos": coords}) shader.bind() shader.uniform_float("color", (0, 0, 0, 1)) batch.draw(shader) ############### class VIEW3D_OT_map_start(Operator): bl_idname = "view3d.map_start" bl_description = 'Toggle 2d map navigation' bl_label = "Basemap" bl_options = {'REGISTER'} #special function to auto redraw an operator popup called through invoke_props_dialog def check(self, context): return True def listSources(self, context): srcItems = [] for srckey, src in SOURCES.items(): #put each item in a tuple (key, label, tooltip) srcItems.append( (srckey, src['name'], src['description']) ) return srcItems def listGrids(self, context): grdItems = [] src = SOURCES[self.src] for gridkey, grd in GRIDS.items(): #put each item in a tuple (key, label, tooltip) if gridkey == src['grid']: #insert at first position grdItems.insert(0, (gridkey, grd['name']+' (source)', grd['description']) ) else: grdItems.append( (gridkey, grd['name'], grd['description']) ) return grdItems def listLayers(self, context): layItems = [] src = SOURCES[self.src] for laykey, lay in src['layers'].items(): #put each item in a tuple (key, label, tooltip) layItems.append( (laykey, lay['name'], lay['description']) ) return layItems src: EnumProperty( name = "Map", description = "Choose map service source", items = listSources ) grd: EnumProperty( name = "Grid", description = "Choose cache tiles matrix", items = listGrids ) lay: EnumProperty( name = "Layer", description = "Choose layer", items = listLayers ) dialog: StringProperty(default='MAP') # 'MAP', 'SEARCH', 'OPTIONS' query: StringProperty(name="Go to") zoom: IntProperty(name='Zoom level', min=0, max=25) recenter: BoolProperty(name='Center to existing objects') def draw(self, context): addonPrefs = context.preferences.addons[PKG].preferences scn = context.scene layout = self.layout if self.dialog == 'SEARCH': layout.prop(self, 'query') layout.prop(self, 'zoom', slider=True) elif self.dialog == 'OPTIONS': #viewPrefs = context.preferences.view #layout.prop(viewPrefs, "use_zoom_to_mouse") layout.prop(addonPrefs, "zoomToMouse") layout.prop(addonPrefs, "lockObj") layout.prop(addonPrefs, "lockOrigin") layout.prop(addonPrefs, "synchOrj") elif self.dialog == 'MAP': layout.prop(self, 'src', text='Source') layout.prop(self, 'lay', text='Layer') col = layout.column() if not HAS_GDAL: col.enabled = False col.label(text='(No raster reprojection support)') col.prop(self, 'grd', text='Tile matrix set') #srcCRS = GRIDS[SOURCES[self.src]['grid']]['CRS'] grdCRS = GRIDS[self.grd]['CRS'] row = layout.row() #row.alignment = 'RIGHT' desc = PredefCRS.getName(grdCRS) if desc is not None: row.label(text='CRS: ' + desc) else: row.label(text='CRS: ' + grdCRS) row = layout.row() row.prop(self, 'recenter') geoscn = GeoScene(scn) if geoscn.isPartiallyGeoref: #layout.separator() georefManagerLayout(self, context) #row = layout.row() #row.label(text='Map scale:') #row.prop(scn, '["'+SK.SCALE+'"]', text='') def invoke(self, context, event): if not HAS_PIL and not HAS_GDAL and not HAS_IMGIO: self.report({'ERROR'}, "No imaging library available. ImageIO module was not correctly installed.") return {'CANCELLED'} if not context.area.type == 'VIEW_3D': self.report({'WARNING'}, "View3D not found, cannot run operator") return {'CANCELLED'} #Update zoom geoscn = GeoScene(context.scene) if geoscn.hasZoom: self.zoom = geoscn.zoom #Display dialog return context.window_manager.invoke_props_dialog(self) def execute(self, context): scn = context.scene geoscn = GeoScene(scn) prefs = context.preferences.addons[PKG].preferences #check cache folder folder = prefs.cacheFolder if folder == "" or not os.path.exists(folder): self.report({'ERROR'}, "Please define a valid cache folder path in addon's preferences") return {'CANCELLED'} if not os.access(folder, os.X_OK | os.W_OK): self.report({'ERROR'}, "The selected cache folder has no write access") return {'CANCELLED'} if self.dialog == 'MAP': grdCRS = GRIDS[self.grd]['CRS'] if geoscn.isBroken: self.report({'ERROR'}, "Scene georef is broken, please fix it beforehand") return {'CANCELLED'} #set scene crs as grid crs #if not geoscn.hasCRS: #geoscn.crs = grdCRS #Check if raster reproj is needed if geoscn.hasCRS and geoscn.crs != grdCRS and not HAS_GDAL: self.report({'ERROR'}, "Please install gdal to enable raster reprojection support") return {'CANCELLED'} #Move scene origin to the researched place if self.dialog == 'SEARCH': r = bpy.ops.view3d.map_search('EXEC_DEFAULT', query=self.query) if r == {'CANCELLED'}: self.report({'INFO'}, "No location found") else: geoscn.zoom = self.zoom #Start map viewer operator self.dialog = 'MAP' #reinit dialog type bpy.ops.view3d.map_viewer('INVOKE_DEFAULT', srckey=self.src, laykey=self.lay, grdkey=self.grd, recenter=self.recenter) return {'FINISHED'} ############### class VIEW3D_OT_map_viewer(Operator): bl_idname = "view3d.map_viewer" bl_description = 'Toggle 2d map navigation' bl_label = "Map viewer" bl_options = {'INTERNAL'} srckey: StringProperty() grdkey: StringProperty() laykey: StringProperty() recenter: BoolProperty() @classmethod def poll(cls, context): return context.area.type == 'VIEW_3D' def __del__(self): if getattr(self, 'restart', False): bpy.ops.view3d.map_start('INVOKE_DEFAULT', src=self.srckey, lay=self.laykey, grd=self.grdkey, dialog=self.dialog) def invoke(self, context, event): self.restart = False self.dialog = 'MAP' # dialog name for MAP_START >> string in ['MAP', 'SEARCH', 'OPTIONS'] self.moveFactor = 0.1 self.prefs = context.preferences.addons[PKG].preferences #Option to adjust or not objects location when panning self.updObjLoc = self.prefs.lockObj #if georef is locked then we need to adjust object location after each pan #Add draw callback to view space args = (self, context) self._drawTextHandler = bpy.types.SpaceView3D.draw_handler_add(drawInfosText, args, 'WINDOW', 'POST_PIXEL') self._drawZoomBoxHandler = bpy.types.SpaceView3D.draw_handler_add(drawZoomBox, args, 'WINDOW', 'POST_PIXEL') #Add modal handler and init a timer context.window_manager.modal_handler_add(self) self.timer = context.window_manager.event_timer_add(0.04, window=context.window) #Switch to top view ortho (center to origin) view3d = context.area.spaces.active bpy.ops.view3d.view_axis(type='TOP') view3d.region_3d.view_perspective = 'ORTHO' context.scene.cursor.location = (0, 0, 0) if not self.prefs.lockOrigin: #bpy.ops.view3d.view_center_cursor() view3d.region_3d.view_location = (0, 0, 0) #Init some properties # tag if map is currently drag self.inMove = False # mouse crs coordinates reported in draw callback self.posx, self.posy = 0, 0 # thread progress infos reported in draw callback self.progress = '' # Zoom box self.zoomBoxMode = False self.zoomBoxDrag = False self.zb_xmin, self.zb_xmax = 0, 0 self.zb_ymin, self.zb_ymax = 0, 0 #Get map self.map = BaseMap(context, self.srckey, self.laykey, self.grdkey) if self.recenter and len(context.scene.objects) > 0: scnBbox = getBBOX.fromScn(context.scene).to2D() w, h = scnBbox.dimensions px_diag = math.sqrt(context.area.width**2 + context.area.height**2) dst_diag = math.sqrt( w**2 + h**2 ) targetRes = dst_diag / px_diag z = self.map.tm.getNearestZoom(targetRes, rule='lower') resFactor = self.map.tm.getFromToResFac(self.map.zoom, z) context.region_data.view_distance *= resFactor x, y = scnBbox.center if self.prefs.lockOrigin: context.region_data.view_location = (x, y, 0) else: self.map.moveOrigin(x, y) self.map.zoom = z self.map.get() return {'RUNNING_MODAL'} def modal(self, context, event): context.area.tag_redraw() scn = bpy.context.scene if event.type == 'TIMER': #report thread progression self.progress = self.map.srv.report return {'PASS_THROUGH'} if event.type in ['WHEELUPMOUSE', 'NUMPAD_PLUS']: if event.value == 'PRESS': if event.alt: # map scale up self.map.scale *= 10 self.map.place() #Scale existing objects for obj in scn.objects: obj.location /= 10 obj.scale /= 10 elif event.ctrl: # view3d zoom up dst = context.region_data.view_distance context.region_data.view_distance -= dst * self.moveFactor if self.prefs.zoomToMouse: mouseLoc = mouseTo3d(context, event.mouse_region_x, event.mouse_region_y) viewLoc = context.region_data.view_location deltaVect = (mouseLoc - viewLoc) * self.moveFactor viewLoc += deltaVect else: # map zoom up if self.map.zoom < self.map.layer.zmax and self.map.zoom < self.map.tm.nbLevels-1: self.map.zoom += 1 if self.map.lockedZoom is None: resFactor = self.map.tm.getNextResFac(self.map.zoom) if not self.prefs.zoomToMouse: context.region_data.view_distance *= resFactor else: #Progressibly zoom to cursor dst = context.region_data.view_distance dst2 = dst * resFactor context.region_data.view_distance = dst2 mouseLoc = mouseTo3d(context, event.mouse_region_x, event.mouse_region_y) viewLoc = context.region_data.view_location moveFactor = (dst - dst2) / dst deltaVect = (mouseLoc - viewLoc) * moveFactor if self.prefs.lockOrigin: viewLoc += deltaVect else: dx, dy, dz = deltaVect if not self.prefs.lockObj and self.map.bkg is not None: self.map.bkg.location -= deltaVect self.map.moveOrigin(dx, dy, updObjLoc=self.updObjLoc) self.map.get() if event.type in ['WHEELDOWNMOUSE', 'NUMPAD_MINUS']: if event.value == 'PRESS': if event.alt: #map scale down s = self.map.scale / 10 if s < 1: s = 1 self.map.scale = s self.map.place() #Scale existing objects for obj in scn.objects: obj.location *= 10 obj.scale *= 10 elif event.ctrl: #view3d zoom down dst = context.region_data.view_distance context.region_data.view_distance += dst * self.moveFactor if self.prefs.zoomToMouse: mouseLoc = mouseTo3d(context, event.mouse_region_x, event.mouse_region_y) viewLoc = context.region_data.view_location deltaVect = (mouseLoc - viewLoc) * self.moveFactor viewLoc -= deltaVect else: #map zoom down if self.map.zoom > self.map.layer.zmin and self.map.zoom > 0: self.map.zoom -= 1 if self.map.lockedZoom is None: resFactor = self.map.tm.getPrevResFac(self.map.zoom) if not self.prefs.zoomToMouse: context.region_data.view_distance *= resFactor else: #Progressibly zoom to cursor dst = context.region_data.view_distance dst2 = dst * resFactor context.region_data.view_distance = dst2 mouseLoc = mouseTo3d(context, event.mouse_region_x, event.mouse_region_y) viewLoc = context.region_data.view_location moveFactor = (dst - dst2) / dst deltaVect = (mouseLoc - viewLoc) * moveFactor if self.prefs.lockOrigin: viewLoc += deltaVect else: dx, dy, dz = deltaVect if not self.prefs.lockObj and self.map.bkg is not None: self.map.bkg.location -= deltaVect self.map.moveOrigin(dx, dy, updObjLoc=self.updObjLoc) self.map.get() if event.type == 'MOUSEMOVE': #Report mouse location coords in projeted crs loc = mouseTo3d(context, event.mouse_region_x, event.mouse_region_y) self.posx, self.posy = self.map.view3dToProj(loc.x, loc.y) if self.zoomBoxMode: self.zb_xmax, self.zb_ymax = event.mouse_region_x, event.mouse_region_y #Drag background image (edit its offset values) if self.inMove: loc1 = mouseTo3d(context, self.x1, self.y1) loc2 = mouseTo3d(context, event.mouse_region_x, event.mouse_region_y) dlt = loc1 - loc2 if event.ctrl or self.prefs.lockOrigin: context.region_data.view_location = self.viewLoc1 + dlt else: #Move background image if self.map.bkg is not None: self.map.bkg.location[0] = self.offx1 - dlt.x self.map.bkg.location[1] = self.offy1 - dlt.y #Move existing objects (only top level parent) if self.updObjLoc: topParents = [obj for obj in scn.objects if not obj.parent] for i, obj in enumerate(topParents): if obj == self.map.bkg: #the background empty used as basemap continue loc1 = self.objsLoc1[i] obj.location.x = loc1.x - dlt.x obj.location.y = loc1.y - dlt.y if event.type in {'LEFTMOUSE', 'MIDDLEMOUSE'}: if event.value == 'PRESS' and not self.zoomBoxMode: #Get click mouse position and background image offset (if exist) self.x1, self.y1 = event.mouse_region_x, event.mouse_region_y self.viewLoc1 = context.region_data.view_location.copy() if not event.ctrl: #Stop thread now, because we don't know when the mouse click will be released self.map.stop() if not self.prefs.lockOrigin: if self.map.bkg is not None: self.offx1 = self.map.bkg.location[0] self.offy1 = self.map.bkg.location[1] #Store current location of each objects (only top level parent) self.objsLoc1 = [obj.location.copy() for obj in scn.objects if not obj.parent] #Tag that map is currently draging self.inMove = True if event.value == 'RELEASE' and not self.zoomBoxMode: self.inMove = False if not event.ctrl: if not self.prefs.lockOrigin: #Compute final shift loc1 = mouseTo3d(context, self.x1, self.y1) loc2 = mouseTo3d(context, event.mouse_region_x, event.mouse_region_y) dlt = loc1 - loc2 #Update map (do not update objects location because it was updated while mouse move) self.map.moveOrigin(dlt.x, dlt.y, updObjLoc=False) self.map.get() if event.value == 'PRESS' and self.zoomBoxMode: self.zoomBoxDrag = True self.zb_xmin, self.zb_ymin = event.mouse_region_x, event.mouse_region_y if event.value == 'RELEASE' and self.zoomBoxMode: #Get final zoom box xmax = max(event.mouse_region_x, self.zb_xmin) ymax = max(event.mouse_region_y, self.zb_ymin) xmin = min(event.mouse_region_x, self.zb_xmin) ymin = min(event.mouse_region_y, self.zb_ymin) #Exit zoom box mode self.zoomBoxDrag = False self.zoomBoxMode = False context.window.cursor_set('DEFAULT') #Compute the move to box origin w = xmax - xmin h = ymax - ymin cx = xmin + w/2 cy = ymin + h/2 loc = mouseTo3d(context, cx, cy) #Compute target resolution px_diag = math.sqrt(context.area.width**2 + context.area.height**2) mapRes = self.map.tm.getRes(self.map.zoom) dst_diag = math.sqrt( (w*mapRes)**2 + (h*mapRes)**2) targetRes = dst_diag / px_diag z = self.map.tm.getNearestZoom(targetRes, rule='lower') resFactor = self.map.tm.getFromToResFac(self.map.zoom, z) #Preview context.region_data.view_distance *= resFactor if self.prefs.lockOrigin: context.region_data.view_location = loc else: self.map.moveOrigin(loc.x, loc.y, updObjLoc=self.updObjLoc) self.map.zoom = z self.map.get() if event.type in ['LEFT_CTRL', 'RIGHT_CTRL']: if event.value == 'PRESS': self._viewDstZ = context.region_data.view_distance self._viewLoc = context.region_data.view_location.copy() if event.value == 'RELEASE': #restore view 3d distance and location context.region_data.view_distance = self._viewDstZ context.region_data.view_location = self._viewLoc #NUMPAD MOVES (3D VIEW or MAP) if event.value == 'PRESS' and event.type in ['NUMPAD_2', 'NUMPAD_4', 'NUMPAD_6', 'NUMPAD_8']: delta = self.map.bkg.scale.x * self.moveFactor if event.type == 'NUMPAD_4': if event.ctrl or self.prefs.lockOrigin: context.region_data.view_location += Vector( (-delta, 0, 0) ) else: self.map.moveOrigin(-delta, 0, updObjLoc=self.updObjLoc) if event.type == 'NUMPAD_6': if event.ctrl or self.prefs.lockOrigin: context.region_data.view_location += Vector( (delta, 0, 0) ) else: self.map.moveOrigin(delta, 0, updObjLoc=self.updObjLoc) if event.type == 'NUMPAD_2': if event.ctrl or self.prefs.lockOrigin: context.region_data.view_location += Vector( (0, -delta, 0) ) else: self.map.moveOrigin(0, -delta, updObjLoc=self.updObjLoc) if event.type == 'NUMPAD_8': if event.ctrl or self.prefs.lockOrigin: context.region_data.view_location += Vector( (0, delta, 0) ) else: self.map.moveOrigin(0, delta, updObjLoc=self.updObjLoc) if not event.ctrl: self.map.get() #SWITCH LAYER if event.type == 'SPACE': self.map.stop() bpy.types.SpaceView3D.draw_handler_remove(self._drawTextHandler, 'WINDOW') bpy.types.SpaceView3D.draw_handler_remove(self._drawZoomBoxHandler, 'WINDOW') context.area.header_text_set(None) self.restart = True return {'FINISHED'} #GO TO if event.type == 'G': self.map.stop() bpy.types.SpaceView3D.draw_handler_remove(self._drawTextHandler, 'WINDOW') bpy.types.SpaceView3D.draw_handler_remove(self._drawZoomBoxHandler, 'WINDOW') context.area.header_text_set(None) self.restart = True self.dialog = 'SEARCH' return {'FINISHED'} #OPTIONS if event.type == 'O': self.map.stop() bpy.types.SpaceView3D.draw_handler_remove(self._drawTextHandler, 'WINDOW') bpy.types.SpaceView3D.draw_handler_remove(self._drawZoomBoxHandler, 'WINDOW') context.area.header_text_set(None) self.restart = True self.dialog = 'OPTIONS' return {'FINISHED'} #Lock/unlock 3d view zoom distance if event.type == 'L' and event.value == 'PRESS': if self.map.lockedZoom is None: self.map.lockedZoom = self.map.zoom else: self.map.lockedZoom = None self.map.get() #ZOOM BOX if event.type == 'B' and event.value == 'PRESS': self.map.stop() self.zoomBoxMode = True self.zb_xmax, self.zb_ymax = event.mouse_region_x, event.mouse_region_y context.window.cursor_set('CROSSHAIR') #EXPORT if event.type == 'E' and event.value == 'PRESS': # if not self.map.srv.running and self.map.mosaic is not None: self.map.stop() self.map.bkg.hide_viewport = True bpy.types.SpaceView3D.draw_handler_remove(self._drawTextHandler, 'WINDOW') bpy.types.SpaceView3D.draw_handler_remove(self._drawZoomBoxHandler, 'WINDOW') context.area.header_text_set(None) #Copy image to new datablock bpyImg = bpy.data.images.load(self.map.imgPath) #(self.map.img.filepath) name = 'EXPORT_' + self.map.srckey + '_' + self.map.laykey + '_' + self.map.grdkey bpyImg.name = name bpyImg.pack() #Add new attribute to GeoRaster (used by geoRastUVmap function) rast = self.map.mosaic setattr(rast, 'bpyImg', bpyImg) #Create Mesh dx, dy = self.map.getOriginPrj() mesh = rasterExtentToMesh(name, rast, dx, dy, pxLoc='CORNER') #Create object obj = placeObj(mesh, name) #UV mapping uvTxtLayer = mesh.uv_layers.new(name='rastUVmap')# Add UV map texture layer geoRastUVmap(obj, uvTxtLayer, rast, dx, dy) #Create material mat = bpy.data.materials.new('rastMat') obj.data.materials.append(mat) addTexture(mat, bpyImg, uvTxtLayer) #Adjust 3d view and display textures if self.prefs.adjust3Dview: adjust3Dview(context, getBBOX.fromObj(obj)) if self.prefs.forceTexturedSolid: showTextures(context) return {'FINISHED'} #EXIT if event.type == 'ESC' and event.value == 'PRESS': if self.zoomBoxMode: self.zoomBoxDrag = False self.zoomBoxMode = False context.window.cursor_set('DEFAULT') else: self.map.stop() bpy.types.SpaceView3D.draw_handler_remove(self._drawTextHandler, 'WINDOW') bpy.types.SpaceView3D.draw_handler_remove(self._drawZoomBoxHandler, 'WINDOW') context.area.header_text_set(None) return {'CANCELLED'} return {'RUNNING_MODAL'} #################################### class VIEW3D_OT_map_search(bpy.types.Operator): bl_idname = "view3d.map_search" bl_description = 'Search for a place and move scene origin to it' bl_label = "Map search" bl_options = {'INTERNAL'} query: StringProperty(name="Go to") def invoke(self, context, event): geoscn = GeoScene(context.scene) if geoscn.isBroken: self.report({'ERROR'}, "Scene georef is broken") return {'CANCELLED'} return context.window_manager.invoke_props_dialog(self) def execute(self, context): geoscn = GeoScene(context.scene) prefs = context.preferences.addons[PKG].preferences try: results = nominatimQuery(self.query, referer='bgis', user_agent=USER_AGENT) except Exception as e: log.error('Failed Nominatim query', exc_info=True) return {'CANCELLED'} if len(results) == 0: return {'CANCELLED'} else: log.debug('Nominatim search results : {}'.format([r['display_name'] for r in results])) result = results[0] lat, lon = float(result['lat']), float(result['lon']) if geoscn.isGeoref: geoscn.updOriginGeo(lon, lat, updObjLoc=prefs.lockObj) else: geoscn.setOriginGeo(lon, lat) return {'FINISHED'} classes = [ VIEW3D_OT_map_start, VIEW3D_OT_map_viewer, VIEW3D_OT_map_search ] def register(): for cls in classes: try: bpy.utils.register_class(cls) except ValueError as e: #log.error('Cannot register {}'.format(cls), exc_info=True) log.warning('{} is already registered, now unregister and retry... '.format(cls)) bpy.utils.unregister_class(cls) bpy.utils.register_class(cls) def unregister(): for cls in classes: bpy.utils.unregister_class(cls) ================================================ FILE: prefs.py ================================================ import json import logging log = logging.getLogger(__name__) import sys, os import bpy from bpy.props import StringProperty, IntProperty, FloatProperty, BoolProperty, EnumProperty, FloatVectorProperty from bpy.types import Operator, Panel, AddonPreferences import addon_utils from . import bl_info from .core.proj.reproj import MapTilerCoordinates from .core.proj.srs import SRS from .core.checkdeps import HAS_GDAL, HAS_PYPROJ, HAS_PIL, HAS_IMGIO from .core import settings PKG = __package__ def getAppData(): home = os.path.expanduser('~') loc = os.path.join(home, '.bgis') if not os.path.exists(loc): os.mkdir(loc) return loc APP_DATA = getAppData() ''' Default Enum properties contents (list of tuple (value, label, tootip)) Theses properties are automatically filled from a serialized json string stored in a StringProperty This is workaround to have an editable EnumProperty (ie the user can add, remove or edit an entry) because the Blender Python API does not provides built in functions for these tasks. To edit the content of these enum, we just need to write new operators which will simply update the json string As the json backend is stored in addon preferences, the property will be saved and restored for the next blender session ''' DEFAULT_CRS = [ ('EPSG:3857', 'Web Mercator', 'Worldwide projection, high distortions, not suitable for precision modelling'), ('EPSG:4326', 'WGS84 latlon', 'Longitude and latitude in degrees, DO NOT USE AS SCENE CRS (this system is defined only for reprojection tasks') ] DEFAULT_DEM_SERVER = [ ("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'), ("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'), ("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)') ] DEFAULT_OVERPASS_SERVER = [ ("https://lz4.overpass-api.de/api/interpreter", 'overpass-api.de', 'Main Overpass API instance'), ("http://overpass.openstreetmap.fr/api/interpreter", 'overpass.openstreetmap.fr', 'French Overpass API instance'), ("https://overpass.kumi.systems/api/interpreter", 'overpass.kumi.systems', 'Kumi Systems Overpass Instance') ] #default filter tags for OSM import DEFAULT_OSM_TAGS = [ 'building', 'highway', 'landuse', 'leisure', 'natural', 'railway', 'waterway' ] class BGIS_OT_pref_show(Operator): bl_idname = "bgis.pref_show" bl_description = 'Display BlenderGIS addons preferences' bl_label = "Preferences" bl_options = {'INTERNAL'} def execute(self, context): addon_utils.modules_refresh() context.preferences.active_section = 'ADDONS' bpy.data.window_managers["WinMan"].addon_search = bl_info['name'] #bpy.ops.wm.addon_expand(module=PKG) mod = addon_utils.addons_fake_modules.get(PKG) mod.bl_info['show_expanded'] = True bpy.ops.screen.userpref_show('INVOKE_DEFAULT') return {'FINISHED'} class BGIS_PREFS(AddonPreferences): bl_idname = PKG ################ #Predefined Spatial Ref. Systems def listPredefCRS(self, context): return [tuple(elem) for elem in json.loads(self.predefCrsJson)] #store crs preset as json string into addon preferences predefCrsJson: StringProperty(default=json.dumps(DEFAULT_CRS)) predefCrs: EnumProperty( name = "Predefinate CRS", description = "Choose predefinite Coordinate Reference System", #default = 1, #possible only since Blender 2.90 items = listPredefCRS ) ################ #proj engine def getProjEngineItems(self, context): items = [ ('AUTO', 'Auto detect', 'Auto select the best library for reprojection tasks') ] if HAS_GDAL: items.append( ('GDAL', 'GDAL', 'Force GDAL as reprojection engine') ) if HAS_PYPROJ: items.append( ('PYPROJ', 'pyProj', 'Force pyProj as reprojection engine') ) #if EPSGIO.ping(): #too slow # items.append( ('EPSGIO', 'epsg.io', '') ) items.append( ('EPSGIO', 'epsg.io / MapTilerCoords', 'Force epsg.io as reprojection engine') ) items.append( ('BUILTIN', 'Built in', 'Force reprojection through built in Python functions') ) return items def updateProjEngine(self, context): settings.proj_engine = self.projEngine projEngine: EnumProperty( name = "Projection engine", description = "Select projection engine", items = getProjEngineItems, update = updateProjEngine ) ################ #img engine def getImgEngineItems(self, context): items = [ ('AUTO', 'Auto detect', 'Auto select the best imaging library') ] if HAS_GDAL: items.append( ('GDAL', 'GDAL', 'Force GDAL as image processing engine') ) if HAS_IMGIO: items.append( ('IMGIO', 'ImageIO', 'Force ImageIO as image processing engine') ) if HAS_PIL: items.append( ('PIL', 'PIL', 'Force PIL as image processing engine') ) return items def updateImgEngine(self, context): settings.img_engine = self.imgEngine imgEngine: EnumProperty( name = "Image processing engine", description = "Select image processing engine", items = getImgEngineItems, update = updateImgEngine ) ################ #OSM osmTagsJson: StringProperty(default=json.dumps(DEFAULT_OSM_TAGS)) #just a serialized list of tags def listOsmTags(self, context): prefs = context.preferences.addons[PKG].preferences tags = json.loads(prefs.osmTagsJson) #put each item in a tuple (key, label, tooltip) return [ (tag, tag, tag) for tag in tags] osmTags: EnumProperty( name = "OSM tags", description = "List of registered OSM tags", items = listOsmTags ) ################ #Basemaps def getCacheFolder5x(self, v, isSet): return bpy.path.abspath(v) def getCacheFolder(self): return bpy.path.abspath(self.get("cacheFolder", '')) def setCacheFolder5x(self, newVal, currentVal, isSet): if os.access(newVal, os.X_OK | os.W_OK): return newVal else: log.error("The selected cache folder has no write access") def setCacheFolder(self, value): if os.access(value, os.X_OK | os.W_OK): self["cacheFolder"] = value else: self["cacheFolder"] = "The selected folder has no write access" if bpy.app.version[0] >= 5 : cacheFolder: StringProperty( name = "Cache folder", default = APP_DATA, #Does not works !? description = "Define a folder where to store Geopackage SQlite db", subtype = 'DIR_PATH', get_transform = getCacheFolder5x, set_transform = setCacheFolder5x ) else: cacheFolder: StringProperty( name = "Cache folder", default = APP_DATA, #Does not works !? description = "Define a folder where to store Geopackage SQlite db", subtype = 'DIR_PATH', get = getCacheFolder, set = setCacheFolder ) synchOrj: BoolProperty( name="Synch. lat/long", description='Keep geo origin synchronized with crs origin. Can be slow with remote reprojection services', default=True) zoomToMouse: BoolProperty(name="Zoom to mouse", description='Zoom towards the mouse pointer position', default=True) lockOrigin: BoolProperty(name="Lock origin", description='Do not move scene origin when panning map', default=False) lockObj: BoolProperty(name="Lock objects", description='Retain objects geolocation when moving map origin', default=True) resamplAlg: EnumProperty( name = "Resampling method", description = "Choose GDAL's resampling method used for reprojection", items = [ ('NN', 'Nearest Neighboor', ''), ('BL', 'Bilinear', ''), ('CB', 'Cubic', ''), ('CBS', 'Cubic Spline', ''), ('LCZ', 'Lanczos', '') ] ) ################ #Network def listOverpassServer(self, context): return [tuple(entry) for entry in json.loads(self.overpassServerJson)] #store crs preset as json string into addon preferences overpassServerJson: StringProperty(default=json.dumps(DEFAULT_OVERPASS_SERVER)) overpassServer: EnumProperty( name = "Overpass server", description = "Select an overpass server", #default = 0, items = listOverpassServer ) def listDemServer(self, context): return [tuple(entry) for entry in json.loads(self.demServerJson)] #store crs preset as json string into addon preferences demServerJson: StringProperty(default=json.dumps(DEFAULT_DEM_SERVER)) demServer: EnumProperty( name = "Elevation server", description = "Select a server that provides Digital Elevation Model datasource", #default = 0, items = listDemServer ) opentopography_api_key: StringProperty( name = "", description="you need to register and request a key from opentopography website" ) def updateMapTilerApiKey(self, context): settings.maptiler_api_key = self.maptiler_api_key maptiler_api_key: StringProperty( name = "", description = "API key for MapTiler Coordinates API (required for EPSG.io migration)", update = updateMapTilerApiKey ) ################ #IO options mergeDoubles: BoolProperty( name = "Merge duplicate vertices", description = 'Merge shared vertices between features when importing vector data', default = False) adjust3Dview: BoolProperty( name = "Adjust 3D view", description = "Update 3d view grid size and clip distances according to the new imported object's size", default = True) forceTexturedSolid: BoolProperty( name = "Force textured solid shading", description = "Update shading mode to display raster's texture", default = True) ################ #System def updateLogLevel(self, context): logger = logging.getLogger(PKG) logger.setLevel(logging.getLevelName(self.logLevel)) logLevel: EnumProperty( name = "Logging level", description = "Select the logging level", items = [('DEBUG', 'Debug', ''), ('INFO', 'Info', ''), ('WARNING', 'Warning', ''), ('ERROR', 'Error', ''), ('CRITICAL', 'Critical', '')], update = updateLogLevel, default = 'DEBUG' ) ################ def draw(self, context): layout = self.layout #SRS box = layout.box() box.label(text='Spatial Reference Systems') row = box.row().split(factor=0.5) row.prop(self, "predefCrs", text='') row.operator("bgis.add_predef_crs", icon='ADD') row.operator("bgis.edit_predef_crs", icon='PREFERENCES') row.operator("bgis.rmv_predef_crs", icon='REMOVE') row.operator("bgis.reset_predef_crs", icon='PLAY_REVERSE') #Basemaps box = layout.box() box.label(text='Basemaps') box.prop(self, "cacheFolder") row = box.row() row.prop(self, "zoomToMouse") row.prop(self, "lockObj") row.prop(self, "lockOrigin") row.prop(self, "synchOrj") row = box.row() row.prop(self, "resamplAlg") #IO box = layout.box() box.label(text='Import/Export') row = box.row().split(factor=0.5) split = row.split(factor=0.9, align=True) split.prop(self, "osmTags") split.operator("wm.url_open", icon='INFO').url = "http://wiki.openstreetmap.org/wiki/Map_Features" row.operator("bgis.add_osm_tag", icon='ADD') row.operator("bgis.edit_osm_tag", icon='PREFERENCES') row.operator("bgis.rmv_osm_tag", icon='REMOVE') row.operator("bgis.reset_osm_tags", icon='PLAY_REVERSE') row = box.row() row.prop(self, "mergeDoubles") row.prop(self, "adjust3Dview") row.prop(self, "forceTexturedSolid") #Network box = layout.box() box.label(text='Remote datasource') row = box.row().split(factor=0.5) row.prop(self, "overpassServer") row.operator("bgis.add_overpass_server", icon='ADD') row.operator("bgis.edit_overpass_server", icon='PREFERENCES') row.operator("bgis.rmv_overpass_server", icon='REMOVE') row.operator("bgis.reset_overpass_server", icon='PLAY_REVERSE') row = box.row().split(factor=0.5) row.prop(self, "demServer") row.operator("bgis.add_dem_server", icon='ADD') row.operator("bgis.edit_dem_server", icon='PREFERENCES') row.operator("bgis.rmv_dem_server", icon='REMOVE') row.operator("bgis.reset_dem_server", icon='PLAY_REVERSE') row = box.row().split(factor=0.2) row.label(text="Opentopography Api Key") row.prop(self, "opentopography_api_key") row = box.row().split(factor=0.2) row.label(text="MapTiler API Key") row.prop(self, "maptiler_api_key") #System box = layout.box() box.label(text='System') box.prop(self, "projEngine") box.prop(self, "imgEngine") box.prop(self, "logLevel") ####################### class PredefCRS(): ''' Collection of utility methods (callable at class level) to deal with predefined CRS dictionary Can be used by others operators that need to fill their own crs enum ''' @staticmethod def getData(): '''Load the json string''' prefs = bpy.context.preferences.addons[PKG].preferences return json.loads(prefs.predefCrsJson) @classmethod def getName(cls, key): '''Return the convenient name of a given srid or None if this crs does not exist in the list''' data = cls.getData() try: return [entry[1] for entry in data if entry[0] == key][0] except IndexError: return None @classmethod def getEnumItems(cls): '''Return a list of predefined crs usable to fill a bpy EnumProperty''' return [tuple(entry) for entry in cls.getData()] ################# # Collection of operators to manage predefined CRS class BGIS_OT_add_predef_crs(Operator): bl_idname = "bgis.add_predef_crs" bl_description = 'Add predefinate CRS' bl_label = "Add" bl_options = {'INTERNAL'} crs: StringProperty(name = "Definition", description = "Specify EPSG code or Proj4 string definition for this CRS") name: StringProperty(name = "Description", description = "Choose a convenient name for this CRS") desc: StringProperty(name = "Description", description = "Add a description or comment about this CRS") def check(self, context): return True def search(self, context): apiKey = settings.maptiler_api_key if not apiKey: #self.report({'ERROR'}, "MapTiler API key is required. Please set it in the preferences.") #report is not available outsite of the execute function log.error("No Maptiler API key") return mtc = MapTilerCoordinates(apiKey=apiKey) results = mtc.search(self.query) self.results = json.dumps(results) if results: self.crs = 'EPSG:' + str(results[0]['id']['code']) self.name = results[0]['name'] def updEnum(self, context): crsItems = [] if self.results != '': for result in json.loads(self.results): srid = 'EPSG:' + str(result['id']['code']) crsItems.append( (str(result['id']['code']), result['name'], srid) ) return crsItems def fill(self, context): if self.results != '': crs = [crs for crs in json.loads(self.results) if str(crs['id']['code']) == self.crsEnum][0] self.crs = 'EPSG:' + str(crs['id']['code']) self.desc = crs['name'] query: StringProperty(name='Query', description='Hit enter to process the search', update=search) results: StringProperty() crsEnum: EnumProperty(name='Results', description='Select the desired CRS', items=updEnum, update=fill) search: BoolProperty(name='Search', description='Search for coordinate system into EPSG database', default=False) save: BoolProperty(name='Save to addon preferences', description='Save Blender user settings after the addition', default=False) def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self)#, width=300) def draw(self, context): layout = self.layout layout.prop(self, 'search') if self.search: prefs = context.preferences.addons[PKG].preferences if not prefs.maptiler_api_key: layout.label(text="Searching require a MapTiler API key", icon_value=3) layout.prop(prefs, "maptiler_api_key", text='API Key') else: layout.prop(self, 'query') layout.prop(self, 'crsEnum') layout.separator() layout.prop(self, 'crs') layout.prop(self, 'name') layout.prop(self, 'desc') #layout.prop(self, 'save') #sincce Blender2.8 prefs are autosaved def execute(self, context): if not SRS.validate(self.crs): self.report({'ERROR'}, 'Invalid CRS') if self.crs.isdigit(): self.crs = 'EPSG:' + self.crs #append the new crs def to json string prefs = context.preferences.addons[PKG].preferences data = json.loads(prefs.predefCrsJson) data.append((self.crs, self.name, self.desc)) prefs.predefCrsJson = json.dumps(data) #change enum index to new added crs and redraw #prefs.predefCrs = self.crs context.area.tag_redraw() #end if self.save: bpy.ops.wm.save_userpref() return {'FINISHED'} class BGIS_OT_rmv_predef_crs(Operator): bl_idname = "bgis.rmv_predef_crs" bl_description = 'Remove predefinate CRS' bl_label = "Remove" bl_options = {'INTERNAL'} def execute(self, context): prefs = context.preferences.addons[PKG].preferences key = prefs.predefCrs if key != '': data = json.loads(prefs.predefCrsJson) data = [e for e in data if e[0] != key] prefs.predefCrsJson = json.dumps(data) context.area.tag_redraw() return {'FINISHED'} class BGIS_OT_reset_predef_crs(Operator): bl_idname = "bgis.reset_predef_crs" bl_description = 'Reset predefinate CRS' bl_label = "Reset" bl_options = {'INTERNAL'} def execute(self, context): prefs = context.preferences.addons[PKG].preferences prefs.predefCrsJson = json.dumps(DEFAULT_CRS) context.area.tag_redraw() return {'FINISHED'} class BGIS_OT_edit_predef_crs(Operator): bl_idname = "bgis.edit_predef_crs" bl_description = 'Edit predefinate CRS' bl_label = "Edit" bl_options = {'INTERNAL'} crs: StringProperty(name = "EPSG code or Proj4 string", description = "Specify EPSG code or Proj4 string definition for this CRS") name: StringProperty(name = "Description", description = "Choose a convenient name for this CRS") desc: StringProperty(name = "Name", description = "Add a description or comment about this CRS") def invoke(self, context, event): prefs = context.preferences.addons[PKG].preferences key = prefs.predefCrs if key == '': return {'CANCELLED'} data = json.loads(prefs.predefCrsJson) entry = [entry for entry in data if entry[0] == key][0] self.crs, self.name, self.desc = entry return context.window_manager.invoke_props_dialog(self) def execute(self, context): prefs = context.preferences.addons[PKG].preferences key = prefs.predefCrs data = json.loads(prefs.predefCrsJson) if SRS.validate(self.crs): data = [entry for entry in data if entry[0] != key] #deleting data.append((self.crs, self.name, self.desc)) prefs.predefCrsJson = json.dumps(data) context.area.tag_redraw() else: self.report({'ERROR'}, 'Invalid CRS') return {'FINISHED'} ################# # Collection of operators to manage predefinates OSM Tags class BGIS_OT_add_osm_tag(Operator): bl_idname = "bgis.add_osm_tag" bl_description = 'Add new predefinate OSM filter tag' bl_label = "Add" bl_options = {'INTERNAL'} tag: StringProperty(name = "Tag", description = "Specify the tag (examples : 'building', 'landuse=forest' ...)") def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self)#, width=300) def execute(self, context): prefs = context.preferences.addons[PKG].preferences tags = json.loads(prefs.osmTagsJson) tags.append(self.tag) prefs.osmTagsJson = json.dumps(tags) prefs.osmTags = self.tag #update current idx context.area.tag_redraw() return {'FINISHED'} class BGIS_OT_rmv_osm_tag(Operator): bl_idname = "bgis.rmv_osm_tag" bl_description = 'Remove predefinate OSM filter tag' bl_label = "Remove" bl_options = {'INTERNAL'} def execute(self, context): prefs = context.preferences.addons[PKG].preferences tag = prefs.osmTags if tag != '': tags = json.loads(prefs.osmTagsJson) del tags[tags.index(tag)] prefs.osmTagsJson = json.dumps(tags) context.area.tag_redraw() return {'FINISHED'} class BGIS_OT_reset_osm_tags(Operator): bl_idname = "bgis.reset_osm_tags" bl_description = 'Reset predefinate OSM filter tag' bl_label = "Reset" bl_options = {'INTERNAL'} def execute(self, context): prefs = context.preferences.addons[PKG].preferences prefs.osmTagsJson = json.dumps(DEFAULT_OSM_TAGS) context.area.tag_redraw() return {'FINISHED'} class BGIS_OT_edit_osm_tag(Operator): bl_idname = "bgis.edit_osm_tag" bl_description = 'Edit predefinate OSM filter tag' bl_label = "Edit" bl_options = {'INTERNAL'} tag: StringProperty(name = "Tag", description = "Specify the tag (examples : 'building', 'landuse=forest' ...)") def invoke(self, context, event): prefs = context.preferences.addons[PKG].preferences self.tag = prefs.osmTags if self.tag == '': return {'CANCELLED'} return context.window_manager.invoke_props_dialog(self) def execute(self, context): prefs = context.preferences.addons[PKG].preferences tag = prefs.osmTags tags = json.loads(prefs.osmTagsJson) del tags[tags.index(tag)] tags.append(self.tag) prefs.osmTagsJson = json.dumps(tags) prefs.osmTags = self.tag #update current idx context.area.tag_redraw() return {'FINISHED'} ################# # Collection of operators to manage DEM server urls class BGIS_OT_add_dem_server(Operator): bl_idname = "bgis.add_dem_server" bl_description = 'Add new topography web service' bl_label = "Add" bl_options = {'INTERNAL'} url: StringProperty(name = "Url template", description = "Define url template string. Bounding box varaibles are {W}, {E}, {S} and {N}") name: StringProperty(name = "Description", description = "Choose a convenient name for this server") desc: StringProperty(name = "Description", description = "Add a description or comment about this remote datasource") def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self)#, width=300) def execute(self, context): templates = ['{W}', '{E}', '{S}', '{N}'] if all([t in self.url for t in templates]): prefs = context.preferences.addons[PKG].preferences data = json.loads(prefs.demServerJson) data.append( (self.url, self.name, self.desc) ) prefs.demServerJson = json.dumps(data) context.area.tag_redraw() else: self.report({'ERROR'}, 'Invalid URL') return {'FINISHED'} class BGIS_OT_rmv_dem_server(Operator): bl_idname = "bgis.rmv_dem_server" bl_description = 'Remove a given topography web service' bl_label = "Remove" bl_options = {'INTERNAL'} def execute(self, context): prefs = context.preferences.addons[PKG].preferences key = prefs.demServer if key != '': data = json.loads(prefs.demServerJson) data = [e for e in data if e[0] != key] prefs.demServerJson = json.dumps(data) context.area.tag_redraw() return {'FINISHED'} class BGIS_OT_reset_dem_server(Operator): bl_idname = "bgis.reset_dem_server" bl_description = 'Reset default topographic web server' bl_label = "Reset" bl_options = {'INTERNAL'} def execute(self, context): prefs = context.preferences.addons[PKG].preferences prefs.demServerJson = json.dumps(DEFAULT_DEM_SERVER) context.area.tag_redraw() return {'FINISHED'} class BGIS_OT_edit_dem_server(Operator): bl_idname = "bgis.edit_dem_server" bl_description = 'Edit a topographic web server' bl_label = "Edit" bl_options = {'INTERNAL'} url: StringProperty(name = "Url template", description = "Define url template string. Bounding box varaibles are {W}, {E}, {S} and {N}") name: StringProperty(name = "Description", description = "Choose a convenient name for this server") desc: StringProperty(name = "Description", description = "Add a description or comment about this remote datasource") def invoke(self, context, event): prefs = context.preferences.addons[PKG].preferences key = prefs.demServer if key == '': return {'CANCELLED'} data = json.loads(prefs.demServerJson) entry = [entry for entry in data if entry[0] == key][0] self.url, self.name, self.desc = entry return context.window_manager.invoke_props_dialog(self) def execute(self, context): prefs = context.preferences.addons[PKG].preferences key = prefs.demServer data = json.loads(prefs.demServerJson) templates = ['{W}', '{E}', '{S}', '{N}'] if all([t in self.url for t in templates]): data = [entry for entry in data if entry[0] != key] #deleting data.append((self.url, self.name, self.desc)) prefs.demServerJson = json.dumps(data) context.area.tag_redraw() else: self.report({'ERROR'}, 'Invalid URL') return {'FINISHED'} ################# class EditEnum(): ''' Helper to deal with an enum property that use a serialized json backend Can be used by others operators to edit and EnumProperty WORK IN PROGRESS ''' def __init__(self, enumName): self.prefs = bpy.context.preferences.addons[PKG].preferences self.enumName = enumName self.jsonName = enumName + 'Json' def getData(self): '''Load the json string''' data = json.loads(getattr(self.prefs, self.jsonName)) return [tuple(entry) for entry in data] def append(self, value, label, tooltip, check=lambda x: True): if not check(value): return data = self.getData() data.append((value, label, tooltip)) setattr(self.prefs, self.jsonName, json.dumps(data)) def remove(self, key): if key != '': data = self.getData() data = [e for e in data if e[0] != key] setattr(self.prefs, self.jsonName, json.dumps(data)) def edit(self, key, value, label, tooltip): self.remove(key) self.append(value, label, tooltip) def reset(self): setattr(self.prefs, self.jsonName, json.dumps(DEFAULT_OVERPASS_SERVER)) ################# # Collection of operators to manage Overpass server urls class BGIS_OT_add_overpass_server(Operator): bl_idname = "bgis.add_overpass_server" bl_description = 'Add new OSM overpass server url' bl_label = "Add" bl_options = {'INTERNAL'} url: StringProperty(name = "Url template", description = "Define the url end point of the overpass server") name: StringProperty(name = "Description", description = "Choose a convenient name for this server") desc: StringProperty(name = "Description", description = "Add a description or comment about this remote server") def invoke(self, context, event): return context.window_manager.invoke_props_dialog(self)#, width=300) def execute(self, context): prefs = context.preferences.addons[PKG].preferences data = json.loads(prefs.overpassServerJson) data.append( (self.url, self.name, self.desc) ) prefs.overpassServerJson = json.dumps(data) #EditEnum('overpassServer').append(self.url, self.name, self.desc, check=lambda url: url.startswith('http')) context.area.tag_redraw() return {'FINISHED'} class BGIS_OT_rmv_overpass_server(Operator): bl_idname = "bgis.rmv_overpass_server" bl_description = 'Remove a given overpass server' bl_label = "Remove" bl_options = {'INTERNAL'} def execute(self, context): prefs = context.preferences.addons[PKG].preferences key = prefs.overpassServer if key != '': data = json.loads(prefs.overpassServerJson) data = [e for e in data if e[0] != key] prefs.overpassServerJson = json.dumps(data) context.area.tag_redraw() return {'FINISHED'} class BGIS_OT_reset_overpass_server(Operator): bl_idname = "bgis.reset_overpass_server" bl_description = 'Reset default overpass server' bl_label = "Rest" bl_options = {'INTERNAL'} def execute(self, context): prefs = context.preferences.addons[PKG].preferences prefs.overpassServerJson = json.dumps(DEFAULT_OVERPASS_SERVER) context.area.tag_redraw() return {'FINISHED'} class BGIS_OT_edit_overpass_server(Operator): bl_idname = "bgis.edit_overpass_server" bl_description = 'Edit an overpass server url' bl_label = "Edit" bl_options = {'INTERNAL'} url: StringProperty(name = "Url template", description = "Define the url end point of the overpass server") name: StringProperty(name = "Description", description = "Choose a convenient name for this server") desc: StringProperty(name = "Description", description = "Add a description or comment about this remote server") def invoke(self, context, event): prefs = context.preferences.addons[PKG].preferences key = prefs.overpassServer if key == '': return {'CANCELLED'} data = json.loads(prefs.overpassServerJson) entry = [entry for entry in data if entry[0] == key][0] self.url, self.name, self.desc = entry return context.window_manager.invoke_props_dialog(self) def execute(self, context): prefs = context.preferences.addons[PKG].preferences key = prefs.overpassServer data = json.loads(prefs.overpassServerJson) data = [entry for entry in data if entry[0] != key] #deleting data.append((self.url, self.name, self.desc)) prefs.overpassServerJson = json.dumps(data) context.area.tag_redraw() return {'FINISHED'} classes = [ BGIS_OT_pref_show, BGIS_PREFS, BGIS_OT_add_predef_crs, BGIS_OT_rmv_predef_crs, BGIS_OT_reset_predef_crs, BGIS_OT_edit_predef_crs, BGIS_OT_add_osm_tag, BGIS_OT_rmv_osm_tag, BGIS_OT_reset_osm_tags, BGIS_OT_edit_osm_tag, BGIS_OT_add_dem_server, BGIS_OT_rmv_dem_server, BGIS_OT_reset_dem_server, BGIS_OT_edit_dem_server, BGIS_OT_add_overpass_server, BGIS_OT_rmv_overpass_server, BGIS_OT_reset_overpass_server, BGIS_OT_edit_overpass_server ] def register(): for cls in classes: try: bpy.utils.register_class(cls) except ValueError as e: #log.error('Cannot register {}'.format(cls), exc_info=True) log.warning('{} is already registered, now unregister and retry... '.format(cls)) bpy.utils.unregister_class(cls) bpy.utils.register_class(cls) # set default cache folder prefs = bpy.context.preferences.addons[PKG].preferences if prefs.cacheFolder == '': prefs.cacheFolder = APP_DATA def unregister(): for cls in classes: bpy.utils.unregister_class(cls)