├── .gitignore ├── LICENSE ├── README.md ├── __init__.py ├── apiqtpl.py ├── catalogpl.py ├── catalogpl.svg ├── comandos_git.txt ├── create_zip_plugin.sh ├── legendlayer.py ├── legendlayerpl.py ├── managerloginkey.py ├── messagebarcancel.py ├── metadata.txt ├── pl_expressions.py ├── pl_scenes.qml └── workertms.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore 2 | .directory 3 | *.pyc 4 | *.zip 5 | *.sh 6 | # Not Ignore 7 | !create_zip_plugin.sh 8 | 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | [planetlab_logo]: https://upload.wikimedia.org/wikipedia/commons/thumb/3/39/Planet_logo_New.png/240px-Planet_logo_New.png 3 | 4 | ![][planetlab_logo] 5 | [PlanetLab](https://www.planet.com/explorers/) 6 | 7 | # Catalog Planet Lab Plugin QGIS 8 | 9 | This plugin lets you get images from the Planet Labs API(Version 1), 10 | by performing searches for images that intersect with the extent of the map window. 11 | It is a product from Planet Explorers program (https://www.planet.com/explorers/), 12 | it is not an official Planet Labs's plugin. 13 | You need a key from Planet Labs in order to use this plugin. 14 | This plugin will be create polygon layer (Catalog of images) from intersect with the extent of the map window. 15 | With this plugin, you can download full images(Analytic, UDM, ...), thumbnail images or 16 | add TMS images. 17 | See presentation: https://www.slideshare.net/LuizMotta3/catalog-planet-labs-plugin-for-qgis-v1 18 | Tested with QGIS 2.18.13 19 | 20 | ## Author 21 | Luiz Motta 22 | 23 | ## Changelog 24 | - 2017-12-20 25 | Fixed error ltgRoot in downloadImages(catalogpl.py). 26 | - 2017-11-12 27 | Change menus, catalog group, scene layer, count total dates. Fixed use in Windows 28 | - 2017-10-06 29 | Add groups of images with same date, 30 | add menu Open Form in images, clear cache of TMS, and 31 | add action 'Add selection' to pl_scenes 32 | - 2017-10-04 33 | Reverse images order in group catalog, add directories, 34 | tms, tif, thumbnail, inside download directory 35 | - 2017-10-03 36 | Fix cache_path when save TMS 37 | - 2017-10-02 38 | Change TMS by Server tiler XYZ to GDAL_WMS 39 | Fixed when the first use of plugin(not set the directory), not show the setting 40 | - 2017-10-01 41 | Changed checkbox to radiobutton for images, only one type of image for searching. 42 | - 2017-09-30 43 | Updated to API Planet V1 44 | - 2015-08-17 45 | Fixed message when error download and add message log 46 | - 2015-7-28 47 | Add context menu remove key 48 | - 2015-07-15 49 | Add context menu image full and TMS 50 | - 2015-07-12 51 | Update the checkLayerLegend(), remove clean register 52 | - 2015-07-09 53 | Add cancel for TMS for download 54 | - 2015-07-07 55 | Add TMS for download 56 | - 2015-06-12: 57 | Add feature for download images and thumbnails 58 | Add metadata in table and refactoring codes. 59 | - 2015-04-26: 60 | Create plugin. 61 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | Name : Catalog Planet Labs 5 | Description : Create catalog from Planet Labs 6 | Date : April, 2015 7 | copyright : (C) 2015 by Luiz Motta 8 | email : motta.luiz@gmail.com 9 | 10 | ***************************************************************************/ 11 | 12 | /*************************************************************************** 13 | * * 14 | * This program is free software; you can redistribute it and/or modify * 15 | * it under the terms of the GNU General Public License as published by * 16 | * the Free Software Foundation; either version 2 of the License, or * 17 | * (at your option) any later version. * 18 | * * 19 | ***************************************************************************/ 20 | """ 21 | 22 | import os 23 | 24 | from PyQt4 import QtCore, QtGui 25 | from qgis import core as QgsCore, gui as QgsGui 26 | 27 | from catalogpl import CatalogPL 28 | from apiqtpl import API_PlanetLabs 29 | 30 | def classFactory(iface): 31 | return CatalogPLPlugin( iface ) 32 | 33 | class CatalogPLPlugin: 34 | 35 | icon = QtGui.QIcon( os.path.join( os.path.dirname(__file__), 'catalogpl.svg' ) ) 36 | pluginName = "Catalog Planet Labs" 37 | 38 | def __init__(self, iface): 39 | 40 | self.iface = iface 41 | self.name = u"&Catalog Planet Labs" 42 | self.msgBar = iface.messageBar() 43 | self.action = None 44 | self.ctl = CatalogPL( CatalogPLPlugin.icon ) 45 | 46 | CatalogPL.copyExpression() 47 | 48 | def initGui(self): 49 | dataActions = [ 50 | { 51 | 'isSepatator': False, 52 | 'name': 'Catalog Planet Labs', 53 | 'icon': QtGui.QIcon( CatalogPLPlugin.icon ), 54 | 'method': self.run 55 | }, 56 | { 'isSepatator': True }, 57 | { 58 | 'isSepatator': False, 59 | 'name': 'Setting...', 60 | 'icon': QgsCore.QgsApplication.getThemeIcon('/mActionOptions.svg'), 61 | 'method': self.config 62 | }, 63 | { 'isSepatator': True }, 64 | { 65 | 'isSepatator': False, 66 | 'name': 'Clear key', 67 | 'icon': QgsCore.QgsApplication.getThemeIcon('/mActionOptions.svg'), 68 | 'method': self.clearKey 69 | }, 70 | { 71 | 'isSepatator': False, 72 | 'name': 'Copy key to Clipboard', 73 | 'icon': QgsCore.QgsApplication.getThemeIcon('/mActionOptions.svg'), 74 | 'method': self.clipboardKey 75 | } 76 | ] 77 | 78 | mw = self.iface.mainWindow() 79 | popupMenu = QtGui.QMenu( mw ) 80 | for d in dataActions: 81 | if d['isSepatator']: 82 | a = QtGui.QAction( mw ) 83 | a.setSeparator(True) 84 | else: 85 | a = QtGui.QAction( d['icon'], d['name'], mw ) 86 | a.triggered.connect( d['method'] ) 87 | self.iface.addPluginToRasterMenu( self.name, a ) 88 | popupMenu.addAction( a ) 89 | defaultAction = popupMenu.actions()[0] 90 | self.toolButton = QtGui.QToolButton() 91 | self.toolButton.setPopupMode( QtGui.QToolButton.MenuButtonPopup ) 92 | self.toolButton.setMenu( popupMenu ) 93 | self.toolButton.setDefaultAction( defaultAction ) 94 | 95 | self.actionPopupMenu = self.iface.addToolBarWidget( self.toolButton ) 96 | self.ctl.enableRun.connect( self.actionPopupMenu.setEnabled ) 97 | 98 | def unload(self): 99 | self.iface.removePluginMenu( self.name, self.action ) 100 | self.iface.removeToolBarIcon( self.action ) 101 | del self.action 102 | del self.ctl 103 | 104 | @QtCore.pyqtSlot() 105 | def run(self): 106 | if self.iface.mapCanvas().layerCount() == 0: 107 | msg = "Need layer(s) in map" 108 | self.iface.messageBar().pushMessage( CatalogPLPlugin.pluginName, msg, QgsGui.QgsMessageBar.WARNING, 2 ) 109 | return 110 | 111 | if not self.ctl.isHostLive: 112 | self.ctl.hostLive() 113 | if not self.ctl.isHostLive: 114 | return 115 | 116 | if not self.ctl.hasRegisterKey: 117 | self.ctl.registerKey() 118 | if not self.ctl.hasRegisterKey: 119 | return 120 | 121 | self.ctl.createLayerScenes() 122 | 123 | @QtCore.pyqtSlot() 124 | def config(self): 125 | self.ctl.settingImages() 126 | 127 | @QtCore.pyqtSlot() 128 | def clearKey(self): 129 | self.ctl.clearKey() 130 | 131 | @QtCore.pyqtSlot() 132 | def clipboardKey(self): 133 | self.ctl.clipboardKey() 134 | -------------------------------------------------------------------------------- /apiqtpl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | Name : QtCore.Qt API for Catalog Planet Labs 5 | Description : API for Planet Labs 6 | Date : May, 2015 7 | copyright : (C) 2015 by Luiz Motta 8 | email : motta.luiz@gmail.com 9 | 10 | ***************************************************************************/ 11 | 12 | /*************************************************************************** 13 | * * 14 | * This program is free software; you can redistribute it and/or modify * 15 | * it under the terms of the GNU General Public License as published by * 16 | * the Free Software Foundation; either version 2 of the License, or * 17 | * (at your option) any later version. * 18 | * * 19 | ***************************************************************************/ 20 | """ 21 | 22 | import json, datetime 23 | 24 | from PyQt4 import QtCore, QtGui, QtNetwork 25 | 26 | class AccessSite(QtCore.QObject): 27 | 28 | # Signals 29 | finished = QtCore.pyqtSignal( dict) 30 | send_data = QtCore.pyqtSignal(QtCore.QByteArray) 31 | status_download = QtCore.pyqtSignal(int, int) 32 | status_erros = QtCore.pyqtSignal(list) 33 | 34 | ErrorCodeAttribute = { 35 | 10: 'Canceled request', 36 | 400: 'Bad request syntax', 37 | 401: 'Unauthorized', 38 | 402: 'Payment required', 39 | 403: 'Forbidden', 40 | 404: 'Not found', 41 | 500: 'Internal error', 42 | 501: 'Not implemented', 43 | 502: 'Bad Gateway' 44 | } 45 | 46 | def __init__(self): 47 | super( AccessSite, self ).__init__() 48 | self.networkAccess = QtNetwork.QNetworkAccessManager(self) 49 | self.totalReady = self.reply = self.triedAuthentication = self.isKilled = None 50 | # Input by self.run 51 | self.credential = self.responseAllFinished = None 52 | 53 | def run(self, url, credential=None, responseAllFinished=True, json_request=None): 54 | if credential is None: 55 | credential = {'user': '', 'password': ''} 56 | ( self.credential, self.responseAllFinished ) = ( credential, responseAllFinished ) 57 | self._connect() 58 | self.totalReady = 0 59 | self.isKilled = False 60 | request = QtNetwork.QNetworkRequest( url ) 61 | if json_request is None: 62 | reply = self.networkAccess.get( request ) 63 | else: 64 | request.setHeader( QtNetwork.QNetworkRequest.ContentTypeHeader, "application/json" ) 65 | data = QtCore.QByteArray( json.dumps( json_request ) ) 66 | reply = self.networkAccess.post( request, data ) 67 | if reply is None: 68 | response = { 'isOk': False, 'message': "Network error", 'errorCode': -1 } 69 | self._connect( False ) 70 | self.finished.emit( response ) 71 | return 72 | 73 | self.triedAuthentication = False 74 | self.reply = reply 75 | self._connectReply() 76 | 77 | def kill(self): 78 | self.isKilled = True 79 | 80 | def isRunning(self): 81 | return ( not self.reply is None and self.reply.isRunning() ) 82 | 83 | def _connect(self, isConnect=True): 84 | ss = [ 85 | { 'signal': self.networkAccess.finished, 'slot': self.replyFinished }, 86 | { 'signal': self.networkAccess.authenticationRequired, 'slot': self.authenticationRequired } 87 | ] 88 | if isConnect: 89 | for item in ss: 90 | item['signal'].connect( item['slot'] ) 91 | else: 92 | for item in ss: 93 | item['signal'].disconnect( item['slot'] ) 94 | 95 | def _connectReply(self, isConnect=True): 96 | ss = [ 97 | { 'signal': self.reply.readyRead, 'slot': self.readyRead }, 98 | { 'signal': self.reply.downloadProgress, 'slot': self.downloadProgress }, 99 | { 'signal': self.reply.sslErrors, 'slot': self.sslErrors } 100 | ] 101 | if isConnect: 102 | for item in ss: 103 | item['signal'].connect( item['slot'] ) 104 | else: 105 | for item in ss: 106 | item['signal'].disconnect( item['slot'] ) 107 | 108 | def _clearConnect(self): 109 | self._connect( False ) # self.reply.close() -> emit signal self.networkAccess.finished 110 | self._connectReply( False ) 111 | self.reply.close() 112 | self.reply.deleteLater(); 113 | del self.reply 114 | self.reply = None 115 | 116 | def _redirectionReply(self, url): 117 | self._clearConnect() 118 | self._connect() 119 | if url.isRelative(): 120 | url = url.resolved( url ) 121 | 122 | request = QtNetwork.QNetworkRequest( url ) 123 | reply = self.networkAccess.get( request ) 124 | if reply is None: 125 | response = { 'isOk': False, 'message': "Netwok error", 'errorCode': -1 } 126 | self._connect( False ) 127 | self.finished.emit( response ) 128 | return 129 | 130 | self.reply = reply 131 | self._connectReply() 132 | 133 | def _errorCodeAttribute(self, code): 134 | msg = 'Error network' if not code in self.ErrorCodeAttribute.keys() else AccessSite.ErrorCodeAttribute[ code ] 135 | response = { 'isOk': False, 'message': msg, 'errorCode': code } 136 | self._clearConnect() 137 | self.finished.emit( response ) 138 | 139 | @QtCore.pyqtSlot(QtNetwork.QNetworkReply) 140 | def replyFinished(self, reply) : 141 | if self.isKilled: 142 | self._errorCodeAttribute(10) 143 | 144 | if reply.error() != QtNetwork.QNetworkReply.NoError : 145 | response = { 'isOk': False, 'message': reply.errorString(), 'errorCode': reply.error() } 146 | self._clearConnect() 147 | self.finished.emit( response ) 148 | return 149 | 150 | urlRedir = reply.attribute( QtNetwork.QNetworkRequest.RedirectionTargetAttribute ) 151 | if not urlRedir is None and urlRedir != reply.url(): 152 | self._redirectionReply( urlRedir ) 153 | return 154 | 155 | codeAttribute = reply.attribute( QtNetwork.QNetworkRequest.HttpStatusCodeAttribute ) 156 | if codeAttribute != 200: 157 | self._errorCodeAttribute( codeAttribute ) 158 | return 159 | 160 | statusRequest = { 161 | 'contentTypeHeader': reply.header( QtNetwork.QNetworkRequest.ContentTypeHeader ), 162 | 'lastModifiedHeader': reply.header( QtNetwork.QNetworkRequest.LastModifiedHeader ), 163 | 'contentLengthHeader': reply.header( QtNetwork.QNetworkRequest.ContentLengthHeader ), 164 | 'statusCodeAttribute': reply.attribute( QtNetwork.QNetworkRequest.HttpStatusCodeAttribute ), 165 | 'reasonPhraseAttribute': reply.attribute( QtNetwork.QNetworkRequest.HttpReasonPhraseAttribute ) 166 | } 167 | response = { 'isOk': True, 'statusRequest': statusRequest } 168 | if self.responseAllFinished: 169 | response[ 'data' ] = reply.readAll() 170 | else: 171 | response[ 'totalReady' ] = self.totalReady 172 | 173 | self._clearConnect() 174 | self.finished.emit( response ) 175 | 176 | @QtCore.pyqtSlot(QtNetwork.QNetworkReply, QtNetwork.QAuthenticator) 177 | def authenticationRequired (self, reply, authenticator): 178 | if not self.triedAuthentication: 179 | authenticator.setUser( self.credential['user'] ) 180 | authenticator.setPassword( self.credential['password'] ) 181 | self.triedAuthentication = True 182 | else: 183 | self._errorCodeAttribute( 401 ) 184 | 185 | @QtCore.pyqtSlot() 186 | def readyRead(self): 187 | if self.isKilled: 188 | self._errorCodeAttribute(10) 189 | return 190 | 191 | if self.responseAllFinished: 192 | return 193 | 194 | urlRedir = self.reply.attribute( QtNetwork.QNetworkRequest.RedirectionTargetAttribute ) 195 | if not urlRedir is None and urlRedir != self.reply.url(): 196 | self._redirectionReply( urlRedir ) 197 | return 198 | 199 | codeAttribute = self.reply.attribute( QtNetwork.QNetworkRequest.HttpStatusCodeAttribute ) 200 | if codeAttribute != 200: 201 | self._errorCodeAttribute( codeAttribute ) 202 | return 203 | 204 | data = self.reply.readAll() 205 | if data is None: 206 | return 207 | self.totalReady += len ( data ) 208 | self.send_data.emit( data ) 209 | 210 | @QtCore.pyqtSlot(int, int) 211 | def downloadProgress(self, bytesReceived, bytesTotal): 212 | if self.isKilled: 213 | self._errorCodeAttribute(10) 214 | else: 215 | self.status_download.emit( bytesReceived, bytesTotal ) 216 | 217 | @QtCore.pyqtSlot( list ) 218 | def sslErrors(self, errors): 219 | lstErros = map( lambda e: e.errorString(), errors ) 220 | self.status_erros.emit( lstErros ) 221 | self.reply.ignoreSslErrors() 222 | 223 | 224 | class API_PlanetLabs(QtCore.QObject): 225 | 226 | errorCodeLimitOK = (201, 207) # https://en.wikipedia.org/wiki/List_of_HTTP_status_codes (2107-09-30) 227 | errorCodeDownloads = { # Planet DOC (2107-09-30) 228 | 299: 'Download quota has been exceeded', 229 | 429: 'Request has been denied due to exceeding rate limits.' 230 | } 231 | validKey = None 232 | urlRoot = "https://api.planet.com" 233 | urlQuickSearch = "https://api.planet.com/data/v1/quick-search" 234 | urlThumbnail = "https://api.planet.com/data/v1/item-types/{item_type}/items/{item_id}/thumb" 235 | urlTMS = "https://tiles.planet.com/data/v1/{item_type}/{item_id}/{{z}}/{{x}}/{{y}}.png" 236 | urlAssets = "https://api.planet.com/data/v1/item-types/{item_type}/items/{item_id}/assets" 237 | 238 | def __init__(self): 239 | super( API_PlanetLabs, self ).__init__() 240 | self.access = AccessSite() 241 | self.currentUrl = None 242 | 243 | def _clearResponse(self, response): 244 | if response.has_key('data'): 245 | response['data'].clear() 246 | del response[ 'data' ] 247 | del response[ 'statusRequest' ] 248 | 249 | def kill(self): 250 | self.access.kill() 251 | 252 | def isRunning(self): 253 | return self.access.isRunning() 254 | 255 | def isHostLive(self, setFinished): 256 | @QtCore.pyqtSlot(dict) 257 | def finished( response): 258 | self.access.finished.disconnect( finished ) 259 | if response['isOk']: 260 | response[ 'isHostLive' ] = True 261 | self._clearResponse( response ) 262 | else: 263 | if response['errorCode'] == QtNetwork.QNetworkReply.HostNotFoundError: 264 | response[ 'isHostLive' ] = False 265 | response[ 'message' ] += "\nURL = %s" % API_PlanetLabs.urlRoot 266 | else: 267 | response[ 'isHostLive' ] = True 268 | 269 | setFinished( response ) 270 | 271 | self.currentUrl = API_PlanetLabs.urlRoot 272 | url = QtCore.QUrl( self.currentUrl ) 273 | self.access.finished.connect( finished ) 274 | credential = { 'user': '', 'password': ''} 275 | self.access.run( url, credential ) 276 | 277 | def setKey(self, key, setFinished): 278 | @QtCore.pyqtSlot(dict) 279 | def finished( response): 280 | self.access.finished.disconnect( finished ) 281 | if response['isOk']: 282 | API_PlanetLabs.validKey = key 283 | self._clearResponse( response ) 284 | 285 | setFinished( response ) 286 | 287 | self.currentUrl = API_PlanetLabs.urlRoot 288 | url = QtCore.QUrl( self.currentUrl ) 289 | self.access.finished.connect( finished ) 290 | credential = { 'user': key, 'password': ''} 291 | self.access.run( url, credential ) 292 | 293 | def getUrlScenes(self, json_request, setFinished): 294 | @QtCore.pyqtSlot(dict) 295 | def finished( response): 296 | self.access.finished.disconnect( finished ) 297 | if response[ 'isOk' ]: 298 | data = json.loads( str( response['data'] ) ) 299 | response[ 'url_scenes' ] = data['_links']['_self'] 300 | response['total'] = len( data['features'] ) 301 | 302 | data.clear() 303 | self._clearResponse( response ) 304 | 305 | setFinished( response ) 306 | 307 | self.currentUrl = API_PlanetLabs.urlQuickSearch 308 | url = QtCore.QUrl( self.currentUrl ) 309 | self.access.finished.connect( finished ) 310 | credential = { 'user': API_PlanetLabs.validKey, 'password': ''} 311 | self.access.run( url, credential, json_request=json_request ) 312 | 313 | def getScenes(self, url, setFinished): 314 | @QtCore.pyqtSlot(dict) 315 | def finished( response): 316 | self.access.finished.disconnect( finished ) 317 | if response[ 'isOk' ]: 318 | data = json.loads( str( response[ 'data' ] ) ) 319 | response[ 'url' ] = data[ '_links' ][ '_next' ] 320 | response[ 'scenes' ] = data[ 'features' ] 321 | self._clearResponse( response ) 322 | 323 | setFinished( response ) 324 | 325 | self.currentUrl = url 326 | url = QtCore.QUrl.fromEncoded( url ) 327 | self.access.finished.connect( finished ) 328 | credential = { 'user': API_PlanetLabs.validKey, 'password': '' } 329 | self.access.run( url, credential ) 330 | 331 | def getAssetsStatus(self, item_type, item_id, setFinished): 332 | @QtCore.pyqtSlot(dict) 333 | def finished( response): 334 | def setStatus(asset): 335 | def getDateTimeFormat(d): 336 | dt = datetime.datetime.strptime( d, "%Y-%m-%dT%H:%M:%S.%f") 337 | return dt.strftime( formatDateTime ) 338 | 339 | key = "a_{0}".format( asset ) 340 | response['assets_status'][ key ] = {} 341 | r = response['assets_status'][ key ] 342 | if not data.has_key( asset ): 343 | r['status'] = "*None*" 344 | return 345 | if data[ asset ].has_key('status'): 346 | r['status'] = data[ asset ]['status'] 347 | if data[ asset ].has_key('_permissions'): 348 | permissions = ",".join( data[ asset ]['_permissions']) 349 | r['permissions'] = permissions 350 | if data[ asset ].has_key('expires_at'): 351 | r['expires_at'] = getDateTimeFormat( data[ asset ]['expires_at'] ) 352 | if data[ asset ].has_key('_links'): 353 | if data[ asset ]['_links'].has_key('activate'): 354 | r['activate'] = data[ asset ]['_links']['activate'] 355 | if data[ asset ].has_key('location'): 356 | r['location'] = data[ asset ]['location'] 357 | 358 | self.access.finished.disconnect( finished ) 359 | if response[ 'isOk' ]: 360 | formatDateTime = '%Y-%m-%d %H:%M:%S' 361 | date_time = datetime.datetime.now().strftime( formatDateTime ) 362 | response['assets_status'] = { 363 | 'date_calculate': date_time, 364 | 'url': self.currentUrl 365 | } 366 | data = json.loads( str( response[ 'data' ] ) ) 367 | setStatus('analytic') 368 | setStatus('udm') 369 | self._clearResponse( response ) 370 | 371 | setFinished( response ) 372 | 373 | url = API_PlanetLabs.urlAssets.format(item_type=item_type, item_id=item_id) 374 | self.currentUrl = url 375 | url = QtCore.QUrl.fromEncoded( url ) 376 | 377 | self.access.finished.connect( finished ) 378 | credential = { 'user': API_PlanetLabs.validKey, 'password': ''} 379 | self.access.run( url, credential ) 380 | 381 | def getThumbnail(self, item_id, item_type, setFinished): 382 | @QtCore.pyqtSlot(dict) 383 | def finished( response ): 384 | self.access.finished.disconnect( finished ) 385 | if response['isOk']: 386 | pixmap = QtGui.QPixmap() 387 | pixmap.loadFromData( response[ 'data' ] ) 388 | response[ 'pixmap' ] = pixmap 389 | self._clearResponse( response ) 390 | 391 | setFinished( response ) 392 | 393 | url = API_PlanetLabs.urlThumbnail.format( item_type=item_type, item_id=item_id ) 394 | self.currentUrl = url 395 | url = QtCore.QUrl( url ) 396 | self.access.finished.connect( finished ) 397 | credential = { 'user': API_PlanetLabs.validKey, 'password': ''} 398 | self.access.run( url, credential ) 399 | 400 | def activeAsset(self, url, setFinished): 401 | @QtCore.pyqtSlot(dict) 402 | def finished( response ): 403 | self.access.finished.disconnect( finished ) 404 | if response['isOk']: 405 | self._clearResponse( response ) 406 | setFinished( response ) # response[ 'totalReady' ] 407 | 408 | url = QtCore.QUrl.fromEncoded( url ) 409 | url = QtCore.QUrl( url ) 410 | self.access.finished.connect( finished ) 411 | credential = { 'user': API_PlanetLabs.validKey, 'password': ''} 412 | self.access.run( url, credential ) 413 | 414 | def saveImage(self, url, setFinished, setSave, setProgress): 415 | @QtCore.pyqtSlot(dict) 416 | def finished( response ): 417 | self.access.finished.disconnect( finished ) 418 | if response['isOk']: 419 | self._clearResponse( response ) 420 | setFinished( response ) # response[ 'totalReady' ] 421 | 422 | url = QtCore.QUrl.fromEncoded( url ) 423 | self.access.finished.connect( finished ) 424 | self.access.send_data.connect( setSave ) 425 | self.access.status_download.connect( setProgress ) 426 | credential = { 'user': API_PlanetLabs.validKey, 'password': ''} 427 | self.access.run( url, credential, False ) 428 | 429 | @staticmethod 430 | def getUrlFilterScenesOrtho(filters): 431 | items = [] 432 | for item in filters.iteritems(): 433 | skey = str( item[0] ) 434 | svalue = str( item[1] ) 435 | items.append( ( skey, svalue ) ) 436 | 437 | url = QtCore.QUrl( API_PlanetLabs.urlScenesOrtho) # urlScenesRapideye 438 | url.setQueryItems( items ) 439 | 440 | return url.toEncoded() 441 | 442 | @staticmethod 443 | def getValue(jsonMetadataFeature, keys): 444 | dicMetadata = jsonMetadataFeature 445 | if not isinstance( jsonMetadataFeature, dict): 446 | dicMetadata = json.loads( jsonMetadataFeature ) 447 | msgError = None 448 | e_keys = map( lambda item: "'%s'" % item, keys ) 449 | try: 450 | value = reduce( lambda d, k: d[ k ], [ dicMetadata ] + keys ) 451 | except KeyError as e: 452 | msgError = "Catalog Planet: Have invalid key: %s" % ' -> '.join( e_keys) 453 | except TypeError as e: 454 | msgError = "Catalog Planet: The last key is invalid: %s" % ' -> '.join( e_keys) 455 | 456 | if msgError is None and isinstance( value, dict): 457 | msgError = "Catalog Planet: Missing key: %s" % ' -> '.join( e_keys) 458 | 459 | return ( True, value ) if msgError is None else ( False, msgError ) 460 | 461 | @staticmethod 462 | def getTextTreeMetadata( jsonMetadataFeature ): 463 | def fill_item(strLevel, value): 464 | if not isinstance( value, ( dict, list ) ): 465 | items[-1] += ": %s" % value 466 | return 467 | 468 | if isinstance( value, dict ): 469 | for key, val in sorted( value.iteritems() ): 470 | items.append( "%s%s" % ( strLevel, key ) ) 471 | strLevel += signalLevel 472 | fill_item( strLevel, val ) 473 | strLevel = strLevel[ : -1 * len( signalLevel ) ] 474 | return 475 | 476 | if isinstance( value, list ): 477 | for val in value: 478 | if not isinstance( value, ( dict, list ) ): 479 | items[-1] += ": %s" % value 480 | else: 481 | text = '[dict]' if isinstance( value, dict ) else '[list]' 482 | items.append( "%s%s" % ( strLevel, text ) ) 483 | strLevel += signalLevel 484 | fill_item( strLevel, val ) 485 | strLevel = strLevel[ : -1 * len( signalLevel ) ] 486 | 487 | signalLevel = "- " 488 | items = [] 489 | fill_item( '', json.loads( jsonMetadataFeature ) ) 490 | 491 | return '\n'.join( items ) 492 | 493 | @staticmethod 494 | def getHtmlTreeMetadata(value, html): 495 | if isinstance( value, dict ): 496 | html += "" 504 | return html 505 | return html 506 | 507 | @staticmethod 508 | def getTextValuesMetadata( dicMetadataFeature ): 509 | def fill_item(value): 510 | def addValue(_value): 511 | _text = "'%s' = %s" % (", ".join( keys ), _value ) 512 | items.append( _text ) 513 | 514 | if not isinstance( value, ( dict, list ) ): 515 | addValue( value ) 516 | return 517 | 518 | if isinstance( value, dict ): 519 | for key, val in sorted( value.iteritems() ): 520 | keys.append( '"%s"' % key ) 521 | fill_item( val ) 522 | del keys[ -1 ] 523 | return 524 | 525 | if isinstance( value, list ): 526 | for val in value: 527 | if not isinstance( val, ( dict, list ) ): 528 | addValue( val ) 529 | else: 530 | text = "[dict]" if isinstance( val, dict ) else "[list]" 531 | keys.append( '"%s"' % text ) 532 | fill_item( val ) 533 | del keys[ -1 ] 534 | 535 | keys = [] 536 | items = [] 537 | fill_item( dicMetadataFeature ) 538 | 539 | return '\n'.join( items ) 540 | 541 | @staticmethod 542 | def getQTreeWidgetMetadata( jsonMetadataFeature, parent=None ): 543 | def createTreeWidget(): 544 | tw = QTreeWidget(parent) 545 | tw.setColumnCount( 2 ) 546 | tw.header().hide() 547 | tw.clear() 548 | return tw 549 | 550 | def fill_item(item, value): 551 | item.setExpanded( True ) 552 | if not isinstance( value, ( dict, list ) ): 553 | item.setData( 1, QtCore.Qt.DisplayRole, value ) 554 | return 555 | 556 | if isinstance( value, dict ): 557 | for key, val in sorted( value.iteritems() ): 558 | child = QTreeWidgetItem() 559 | child.setText( 0, unicode(key) ) 560 | item.addChild( child ) 561 | fill_item( child, val ) 562 | return 563 | 564 | if isinstance( value, list ): 565 | for val in value: 566 | if not isinstance( val, ( dict, list ) ): 567 | item.setData( 1, QtCore.Qt.DisplayRole, val ) 568 | else: 569 | child = QTreeWidgetItem() 570 | item.addChild( child ) 571 | text = '[dict]' if isinstance( value, dict ) else '[list]' 572 | child.setText( 0, text ) 573 | fill_item( child , val ) 574 | 575 | child.setExpanded(True) 576 | 577 | tw = createTreeWidget() 578 | fill_item( tw.invisibleRootItem(), json.loads( jsonMetadataFeature ) ) 579 | tw.resizeColumnToContents( 0 ) 580 | tw.resizeColumnToContents( 1 ) 581 | 582 | return tw 583 | 584 | @staticmethod 585 | def getURL_TMS(feat, sbands): 586 | ( ok, item_type ) = API_PlanetLabs.getValue( feat['meta_json'], [ 'item_type' ] ) 587 | url = API_PlanetLabs.urlTMS.format( item_type=item_type, item_id=feat['id'] ) 588 | return url 589 | -------------------------------------------------------------------------------- /catalogpl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | Name : Catalog Planet Labs 5 | Description : Create catalog from Planet Labs 6 | Date : April, 2015 7 | copyright : (C) 2015 by Luiz Motta 8 | email : motta.luiz@gmail.com 9 | 10 | ***************************************************************************/ 11 | 12 | /*************************************************************************** 13 | * * 14 | * This program is free software; you can redistribute it and/or modify * 15 | * it under the terms of the GNU General Public License as published by * 16 | * the Free Software Foundation; either version 2 of the License, or * 17 | * (at your option) any later version. * 18 | * * 19 | ***************************************************************************/ 20 | """ 21 | 22 | import os, json 23 | 24 | from PyQt4 import QtCore, QtGui 25 | from qgis import core as QgsCore, gui as QgsGui, utils as QgsUtils 26 | 27 | from apiqtpl import API_PlanetLabs 28 | from legendlayerpl import ( DialogImageSettingPL, LegendCatalogLayer ) 29 | from legendlayer import LegendRasterGeom 30 | from managerloginkey import ManagerLoginKey 31 | from messagebarcancel import MessageBarCancel, MessageBarCancelProgress 32 | from workertms import WorkerCreateTMS_GDAL_WMS 33 | 34 | 35 | class CatalogPL(QtCore.QObject): 36 | 37 | pluginName = u'Catalog Planet Labs' 38 | styleFile = 'pl_scenes.qml' 39 | expressionFile = 'pl_expressions.py' 40 | expressionDir = 'expressions' 41 | 42 | enableRun = QtCore.pyqtSignal( bool ) 43 | 44 | def __init__(self, icon): 45 | def setLegendCatalogLayer(): 46 | # keys = LegendCatalogLayer.legendMenuIDs 47 | slots = { 48 | 'clear_key': self.clearKey, 49 | 'clipboard_key': self.clipboardKey, 50 | 'setting_images': self.settingImages, 51 | 'calculate_status_assets': self.calculateAssetStatus, 52 | 'activate_assets': self.activateAssets, 53 | 'create_tms': self.CreateTMS_GDAL_WMS, 54 | 'download_images': self.downloadImages, 55 | 'download_thumbnails': self.downloadThumbnails 56 | } 57 | arg = ( CatalogPL.pluginName, slots, self.getTotalAssets ) 58 | self.legendCatalogLayer = LegendCatalogLayer( *arg ) 59 | 60 | def setSearchSettings(): 61 | self.settings = DialogImageSettingPL.getSettings() 62 | 63 | # Next step add all informations (DialogImageSettingPL.getSettings) 64 | self.settings['current_asset'] = 'planet' 65 | self.settings['udm'] = False 66 | date2 = QtCore.QDate.currentDate() 67 | date1 = date2.addMonths( -1 ) 68 | self.settings['date1'] = date1 69 | self.settings['date2'] = date2 70 | 71 | super(CatalogPL, self).__init__() 72 | self.canvas = QgsUtils.iface.mapCanvas() 73 | self.msgBar = QgsUtils.iface.messageBar() 74 | self.logMessage = QgsCore.QgsMessageLog.instance().logMessage 75 | self.icon = icon 76 | self.mainWindow = QgsUtils.iface.mainWindow() 77 | 78 | self.apiPL = API_PlanetLabs() 79 | self.mngLogin = ManagerLoginKey('catalogpl_plugin') 80 | self.legendRasterGeom = LegendRasterGeom( CatalogPL.pluginName ) 81 | self.thread = self.worker = None # initThread 82 | self.mbcancel = None # Need for worker it is be class attribute 83 | self.isHostLive = False 84 | self.hasRegisterKey = False 85 | 86 | self.layer = self.layerTree = None 87 | self.hasCriticalMessage = None 88 | self.url_scenes = self.scenes = self.total_features_scenes = None 89 | self.pixmap = self.messagePL = self.isOkPL = None 90 | self.legendCatalogLayer = self.settings = None 91 | self.imageDownload = self.totalReady = None 92 | self.currentItem = None 93 | self.catalog = { 'ltg': None, 'satellite': None, 'typeImage': None } 94 | 95 | setLegendCatalogLayer() 96 | setSearchSettings() 97 | self._connect() 98 | self._initThread() 99 | 100 | def __del__(self): 101 | self._connect( False ) 102 | self._finishThread() 103 | del self.legendRasterGeom 104 | 105 | def _initThread(self): 106 | self.thread = QtCore.QThread( self ) 107 | self.thread.setObjectName( "QGIS_Plugin_Catalog_PlanetLabs" ) 108 | self.worker = WorkerCreateTMS_GDAL_WMS( self.logMessage, self.legendRasterGeom ) 109 | self.worker.moveToThread( self.thread ) 110 | self.thread.started.connect( self.worker.run ) 111 | 112 | def _finishThread(self): 113 | self.thread.started.disconnect( self.worker.run ) 114 | self.worker.deleteLater() 115 | self.thread.wait() 116 | self.thread.deleteLater() 117 | del self.worker 118 | self.thread = self.worker = None 119 | 120 | def _connect(self, isConnect = True): 121 | s = { 'signal': QgsCore.QgsMapLayerRegistry.instance().layerWillBeRemoved, 'slot': self.layerWillBeRemoved } 122 | if isConnect: 123 | s['signal'].connect( s['slot'] ) 124 | else: 125 | s['signal'].disconnect( s['slot'] ) 126 | 127 | def _startProcess(self, funcKill, hasProgressFile=False): 128 | def getFeatureIteratorTotal(): 129 | hasSelected = True 130 | iter = self.layer.selectedFeaturesIterator() 131 | total = self.layer.selectedFeatureCount() 132 | if total == 0: 133 | hasSelected = False 134 | iter = self.layer.getFeatures() 135 | total = self.layer.featureCount() 136 | 137 | return ( iter, total, hasSelected ) 138 | 139 | ( iterFeat, totalFeat, hasSelected ) = getFeatureIteratorTotal() 140 | if totalFeat == 0: 141 | msg = "Not have images for processing." 142 | arg = ( CatalogPL.pluginName, msg, QgsGui.QgsMessageBar.WARNING, 4 ) 143 | self.msgBar.pushMessage( *arg ) 144 | return { 'isOk': False } 145 | 146 | msg = "selected" if hasSelected else "all" 147 | msg = "Processing {0} images({1})...".format( totalFeat, msg ) 148 | arg = ( CatalogPL.pluginName, self.msgBar, msg, totalFeat, funcKill, hasProgressFile ) 149 | self.mbcancel = MessageBarCancelProgress( *arg ) 150 | self.enableRun.emit( False ) 151 | self.legendCatalogLayer.enabledProcessing( False ) 152 | return { 'isOk': True, 'iterFeat': iterFeat } 153 | 154 | def _endProcessing(self, nameProcessing, totalError): 155 | self.enableRun.emit( True ) 156 | if self.layerTree is None: 157 | self.msgBar.popWidget() 158 | return 159 | 160 | self.legendCatalogLayer.enabledProcessing() 161 | 162 | self.msgBar.popWidget() 163 | if not self.mbcancel.isCancel and totalError > 0: 164 | msg = "Has error in download (total = {0}) - See log messages".format( totalError ) 165 | arg = ( CatalogPL.pluginName, msg, QgsGui.QgsMessageBar.CRITICAL, 4 ) 166 | self.msgBar.pushMessage( *arg ) 167 | return 168 | 169 | if self.mbcancel.isCancel: 170 | f_msg = "Canceled '{0}' by user" 171 | typMessage = QgsGui.QgsMessageBar.WARNING 172 | else: 173 | f_msg = "Finished '{0}'" 174 | typMessage = QgsGui.QgsMessageBar.INFO 175 | 176 | msg = f_msg.format( nameProcessing ) 177 | self.msgBar.clearWidgets() 178 | self.msgBar.pushMessage( self.pluginName, msg, typMessage, 4 ) 179 | 180 | def _setGroupCatalog(self, typeImage): 181 | def existsGroupCatalog(): 182 | groups = [ n for n in root.children() if n.nodeType() == QgsCore.QgsLayerTreeNode.NodeGroup ] 183 | return self.catalog['ltg'] in groups 184 | 185 | def createGroupCatalog(): 186 | self.catalog['satellite'] = self.settings['current_asset'] 187 | self.catalog['typeImage'] = typeImage 188 | self.catalog['ltg'] = root.addGroup( 'Calculating...' ) 189 | 190 | root = QgsCore.QgsProject.instance().layerTreeRoot() 191 | if self.catalog['ltg'] is None: 192 | createGroupCatalog() 193 | if not self.catalog['satellite'] == self.settings['current_asset']: 194 | createGroupCatalog() 195 | if not existsGroupCatalog(): 196 | createGroupCatalog() 197 | else: 198 | self.catalog['ltg'].removeAllChildren() 199 | 200 | def _getValuesAssets(self, assets_status): 201 | def getValues(asset): 202 | status = assets_status[asset]['status'] # active, inactive, *Need calculate*, *None* 203 | if status in ('*Need calculate*', '*None*'): 204 | return { 'isOk': False, 'status': status } 205 | r = { 'isOk': True, 'status': status } 206 | if assets_status[asset].has_key('activate'): 207 | r['activate'] = assets_status[asset]['activate'] 208 | if assets_status[asset].has_key('location'): 209 | r['location'] = assets_status[asset]['location'] 210 | return r 211 | 212 | return { 'analytic': getValues('a_analytic'), 'udm': getValues('a_udm') } 213 | 214 | def _calculateTotalAsset(self, name_asset, valuesAssets, totalAssets): 215 | asset = valuesAssets[ name_asset ] 216 | if not asset['isOk']: 217 | return 218 | if asset['status'] == 'inactive' and asset.has_key('activate'): 219 | totalAssets[ name_asset ]['activate'] += 1 220 | if asset.has_key('location'): 221 | totalAssets[ name_asset ]['images'] += 1 222 | 223 | def _hasLimiteErrorOK(self, response): 224 | err = response['errorCode'] 225 | l1 = API_PlanetLabs.errorCodeLimitOK[0]-1 226 | l2 = API_PlanetLabs.errorCodeLimitOK[1]+1 227 | return err > l1 and err < l2 228 | 229 | def _hasErrorDownloads(self, response): 230 | if response['errorCode'] in API_PlanetLabs.errorCodeDownloads.keys(): 231 | msg = API_PlanetLabs.errorCodeDownloads[ response['errorCode'] ] 232 | msg = "{0}(code {1})".format( msg, response['errorCode'] ) 233 | return { 'isOk': True, 'message': msg } 234 | return { 'isOk': False } 235 | 236 | def _sortNameGroupCatalog(self, reverse=True): 237 | def getLayers(): 238 | ltls = self.catalog['ltg'].findLayers() 239 | if len( ltls ) == 0: 240 | return 241 | # Sort layer 242 | d_name_layerd = {} 243 | for ltl in ltls: 244 | layer = ltl.layer() 245 | name = layer.name() 246 | d_name_layerd[ name ] = layer 247 | l_name_sorted = sorted( d_name_layerd.keys() ) 248 | # 249 | layers = [ d_name_layerd[ name] for name in l_name_sorted ] 250 | 251 | return layers 252 | 253 | def getGroupsDate(layers): 254 | groupDates = {} # 'date': layers } 255 | for l in layers: 256 | date = l.customProperty('date', '_errorT').split('T')[0] 257 | if not date in groupDates.keys(): 258 | groupDates[ date ] = [ l ] 259 | else: 260 | groupDates[ date ].append( l ) 261 | return groupDates 262 | 263 | def addGroupDates(groupDates): 264 | keys = sorted(groupDates.keys(), reverse=reverse ) 265 | for idx, key in enumerate( keys ): 266 | name = "{0} [{1}]".format( key, len( groupDates[ key ] ) ) 267 | ltg = self.catalog['ltg'].insertGroup( idx, name ) 268 | for l in groupDates[ key ]: 269 | ltg.addLayer( l ).setVisible( QtCore.Qt.Unchecked ) 270 | ltg.setExpanded(False) 271 | self.catalog['ltg'].removeChildren( len( keys), len( layers) ) 272 | 273 | def setNameGroupCatalog(total): 274 | arg = ( self.catalog['satellite'].title(), self.catalog['typeImage'], total ) 275 | name = "PL Catalog {} ({}) [{}]".format( *arg ) 276 | self.catalog['ltg'].setName( name ) 277 | 278 | layers = getLayers() 279 | groupDates = getGroupsDate( layers ) 280 | addGroupDates( groupDates ) 281 | setNameGroupCatalog( len( groupDates ) ) 282 | 283 | def hostLive(self): 284 | def setFinished(response): 285 | self.isOkPL = response[ 'isHostLive' ] 286 | if not self.isOkPL: 287 | self.messagePL = response[ 'message' ] 288 | loop.quit() 289 | 290 | loop = QtCore.QEventLoop() 291 | msg = "Checking server..." 292 | self.msgBar.pushMessage( self.pluginName, msg, QgsGui.QgsMessageBar.INFO ) 293 | self.enableRun.emit( False ) 294 | self.apiPL.isHostLive( setFinished ) 295 | loop.exec_() 296 | self.msgBar.popWidget() 297 | if not self.isOkPL: 298 | self.msgBar.pushMessage( self.pluginName, self.messagePL, QgsGui.QgsMessageBar.CRITICAL, 4 ) 299 | self.messagePL = None 300 | self.isHostLive = self.isOkPL 301 | self.enableRun.emit( True ) 302 | 303 | def registerKey(self): 304 | 305 | def dialogGetKey(): 306 | def setResult( isValid ): 307 | self.hasRegisterKey = isValid 308 | 309 | dataDlg = { 310 | 'parent': self.mainWindow, 311 | 'windowTitle': CatalogPL.pluginName, 312 | 'icon': self.icon 313 | } 314 | dataMsgBox = { 315 | 'title': "Planet Labs Register Key", 316 | 'msg': "Do you would like register your Planet Labs key (QGIS setting)?" 317 | } 318 | # Accept -> set Key in API_PlanetLabs 319 | self.mngLogin.dialogLogin( dataDlg, dataMsgBox, setResult ) 320 | 321 | def setFinished(response): 322 | self.isOkPL = response[ 'isOk' ] 323 | loop.quit() 324 | 325 | self.enableRun.emit( False ) 326 | 327 | key = self.mngLogin.getKeySetting() 328 | if key is None: 329 | dialogGetKey() 330 | return 331 | 332 | msg = "Checking register key..." 333 | self.msgBar.pushMessage( self.pluginName, msg, QgsGui.QgsMessageBar.INFO ) 334 | loop = QtCore.QEventLoop() 335 | self.apiPL.setKey( key, setFinished ) 336 | loop.exec_() 337 | self.msgBar.popWidget() 338 | if not self.isOkPL : 339 | msg = "Your registered Key not is valid. It will be removed in QgsCore.QGis setting! Please enter a new key." 340 | self.msgBar.pushMessage( CatalogPL.pluginName, msg, QgsGui.QgsMessageBar.CRITICAL, 4 ) 341 | self.mngLogin.removeKey() 342 | self.hasRegisterKey = self.isOkPL 343 | self.enableRun.emit( True ) 344 | 345 | def createLayerScenes(self): 346 | def createLayer(): 347 | atts = [ 348 | "id:string(25)", "acquired:string(35)", "thumbnail:string(2000)", 349 | "meta_html:string(2000)", "meta_json:string(2000)", 350 | "meta_jsize:integer" 351 | ] 352 | l_fields = map( lambda item: "field=%s" % item, atts ) 353 | l_fields.insert( 0, "Multipolygon?crs=epsg:4326" ) 354 | l_fields.append( "index=yes" ) 355 | uri = '&'.join( l_fields ) 356 | 357 | date1 = self.settings['date1'].toString( QtCore.Qt.ISODate ) 358 | date2 = self.settings['date2'].toString( QtCore.Qt.ISODate ) 359 | arg = ( self.settings['current_asset'].title(), date1, date2 ) 360 | name = "PL {}({} to {})".format( *arg ) 361 | vl = QgsCore.QgsVectorLayer( uri, name, "memory" ) 362 | # Add layer 363 | self.layer = QgsCore.QgsMapLayerRegistry.instance().addMapLayer( vl, addToLegend=False ) 364 | self.layerTree = QgsCore.QgsProject.instance().layerTreeRoot().insertLayer( 0, self.layer ) 365 | # Symbology 366 | ns = os.path.join( os.path.dirname( __file__ ), CatalogPL.styleFile ) 367 | self.layer.loadNamedStyle( ns ) 368 | QgsUtils.iface.legendInterface().refreshLayerSymbology( self.layer ) 369 | self.layerTree.setVisible( QtCore.Qt.Unchecked ) 370 | 371 | def removeFeatures(): 372 | prov = self.layer.dataProvider() 373 | if prov.featureCount() > 0: 374 | self.layer.startEditing() 375 | prov.deleteFeatures( self.layer.allFeatureIds() ) 376 | self.layer.commitChanges() 377 | self.layer.updateExtents() 378 | 379 | def populateLayer(): 380 | def processScenes(json_request): 381 | def setFinishedPL(response): 382 | self.isOkPL = response['isOk'] 383 | if self.isOkPL: 384 | self.total_features_scenes = response['total'] 385 | self.url_scenes = response['url_scenes'] 386 | else: 387 | self.messagePL = response[ 'message' ] 388 | 389 | loop.quit() 390 | 391 | loop = QtCore.QEventLoop() 392 | self.apiPL.getUrlScenes( json_request, setFinishedPL ) 393 | loop.exec_() 394 | if not self.isOkPL: 395 | self.hasCriticalMessage = True 396 | self.msgBar.popWidget() 397 | self.msgBar.pushMessage( CatalogPL.pluginName, self.messagePL, QgsGui.QgsMessageBar.CRITICAL, 4 ) 398 | self.messagePL = None 399 | self.total_features_scenes = None 400 | 401 | def addFeatures( ): 402 | def setFinishedPL(response): 403 | self.isOkPL = response['isOk'] 404 | if self.isOkPL: 405 | ( self.url_scenes, self.scenes ) = ( response['url'], response['scenes'] ) 406 | else: 407 | self.messagePL = response['message'] 408 | 409 | loop.quit() 410 | 411 | def setScenesResponse(): 412 | def getFeatures(): 413 | fields = [ 'id', 'acquired', 'thumbnail', 'meta_html', 'meta_json', 'meta_jsize' ] # See FIELDs order from createLayer 414 | features = [] 415 | for item in self.scenes: 416 | # Fields 417 | meta_json = item['properties'] 418 | vFields = { } 419 | vFields[ fields[0] ] = item['id'] 420 | vFields[ fields[1] ] = meta_json['acquired'] 421 | del meta_json['acquired'] 422 | vFields[ fields[2] ] = "Need download thumbnail" 423 | meta_json['assets_status'] = { 424 | 'a_analytic': { 'status': '*Need calculate*' }, 425 | 'a_udm': { 'status': '*Need calculate*' } 426 | } 427 | vFields[ fields[3] ] = API_PlanetLabs.getHtmlTreeMetadata( meta_json, '') 428 | vjson = json.dumps( meta_json ) 429 | vFields[ fields[4] ] = vjson 430 | vFields[ fields[5] ] = len( vjson) 431 | # Geom 432 | geomItem = item['geometry'] 433 | geomCoords = geomItem['coordinates'] 434 | if geomItem['type'] == 'Polygon': 435 | qpolygon = map ( lambda polyline: map( lambda item: QgsCore.QgsPoint( item[0], item[1] ), polyline ), geomCoords ) 436 | geom = QgsCore.QgsGeometry.fromMultiPolygon( [ qpolygon ] ) 437 | elif geomItem['type'] == 'MultiPolygon': 438 | qmultipolygon = [] 439 | for polygon in geomCoords: 440 | qpolygon = map ( lambda polyline: map( lambda item: QgsCore.QgsPoint( item[0], item[1] ), polyline ), polygon ) 441 | qmultipolygon.append( qpolygon ) 442 | geom = QgsCore.QgsGeometry.fromMultiPolygon( qmultipolygon ) 443 | else: 444 | continue 445 | feat = QgsCore.QgsFeature() 446 | feat.setGeometry( geom ) 447 | 448 | atts = map( lambda item: vFields[ item ], fields ) 449 | feat.setAttributes( atts ) 450 | features.append( feat ) 451 | 452 | return features 453 | 454 | def commitFeatures(): 455 | if not self.layerTree is None and len( features ) > 0: 456 | self.layer.startEditing() 457 | prov.addFeatures( features ) 458 | self.layer.commitChanges() 459 | self.layer.updateExtents() 460 | 461 | if self.isOkPL: 462 | if len( self.scenes ) == 0: 463 | return 464 | features = getFeatures() 465 | del self.scenes[:] 466 | self.total_features_scenes += len( features ) 467 | commitFeatures() 468 | del features[:] 469 | else: 470 | self.hasCriticalMessage = True 471 | self.msgBar.popWidget() 472 | self.msgBar.pushMessage( CatalogPL.pluginName, self.messagePL, QgsGui.QgsMessageBar.CRITICAL, 4 ) 473 | self.messagePL = None 474 | self.url_scenes = None 475 | self.scenes = [] 476 | 477 | loop = QtCore.QEventLoop() 478 | self.apiPL.getScenes( self.url_scenes, setFinishedPL ) 479 | loop.exec_() 480 | setScenesResponse() 481 | 482 | def createRubberBand(): 483 | 484 | def canvasRect( ): 485 | # Adaption from "https://github.com/sourcepole/qgis-instantprint-plugin/blob/master/InstantPrintTool.py" 486 | mtp = self.canvas.mapSettings().mapToPixel() 487 | rect = self.canvas.extent().toRectF() 488 | p1 = mtp.transform( QgsCore.QgsPoint( rect.left(), rect.top() ) ) 489 | p2 = mtp.transform( QgsCore.QgsPoint( rect.right(), rect.bottom() ) ) 490 | return QtCore.QRect( p1.x(), p1.y(), p2.x() - p1.x(), p2.y() - p1.y() ) 491 | 492 | rb = QgsGui.QgsRubberBand( self.canvas, False ) 493 | rb.setBorderColor( QtGui.QColor( 0, 255 , 255 ) ) 494 | rb.setWidth( 2 ) 495 | rb.setToCanvasRectangle( canvasRect() ) 496 | return rb 497 | 498 | def extentFilter(): 499 | crsCanvas = self.canvas.mapSettings().destinationCrs() 500 | crsLayer = self.layer.crs() 501 | ct = QgsCore.QgsCoordinateTransform( crsCanvas, crsLayer ) 502 | extent = self.canvas.extent() if crsCanvas == crsLayer else ct.transform( self.canvas.extent() ) 503 | return json.loads( QgsCore.QgsGeometry.fromRect( extent ).exportToGeoJSON() ) 504 | 505 | def get_item_types(): 506 | # Same list of DialogImageSettingPL.nameAssets 507 | item_types = { 508 | 'planet': 'PSScene4Band', 509 | 'rapideye': 'REScene', 510 | 'landsat8': 'Landsat8L1G', 511 | 'sentinel2': 'Sentinel2L1C' 512 | } 513 | return [ item_types[ self.settings['current_asset'] ] ] 514 | 515 | def finished(): 516 | self.canvas.scene().removeItem( rb ) 517 | if not self.hasCriticalMessage: 518 | self.msgBar.popWidget() 519 | 520 | if self.layerTree is None: 521 | return 522 | self.layerTree.setCustomProperty ('showFeatureCount', True ) 523 | 524 | msg = "Finished the search of images. Found %d images" % self.total_features_scenes 525 | typeMessage = QgsGui.QgsMessageBar.INFO 526 | if self.mbcancel.isCancel: 527 | self.msgBar.popWidget() 528 | removeFeatures() 529 | typeMessage = QgsGui.QgsMessageBar.WARNING 530 | msg = "Canceled the search of images. Removed %d features" % self.total_features_scenes 531 | self.msgBar.pushMessage( CatalogPL.pluginName, msg, typeMessage, 4 ) 532 | 533 | date1 = self.settings['date1'] 534 | date2 = self.settings['date2'] 535 | days = date1.daysTo( date2) 536 | date1, date2 = date1.toString( QtCore.Qt.ISODate ), date2.toString( QtCore.Qt.ISODate ) 537 | sdate1 = "{0}T00:00:00.000000Z".format( date1 ) 538 | sdate2 = "{0}T00:00:00.000000Z".format( date2 ) 539 | 540 | self.msgBar.clearWidgets() 541 | msg = "Starting the search of images - %s(%d days)..." % ( date2, days ) 542 | self.msgBar.pushMessage( CatalogPL.pluginName, msg, QgsGui.QgsMessageBar.INFO ) 543 | rb = createRubberBand() # Show Rectangle of Query (coordinate in pixel) 544 | # JSon request 545 | geometry_filter = { 546 | 'type': 'GeometryFilter', 547 | 'field_name': 'geometry', 548 | 'config': extentFilter() 549 | } 550 | date_range_filter = { 551 | 'type': 'DateRangeFilter', 552 | 'field_name': 'acquired', 553 | 'config': { 'gte': sdate1, 'lte': sdate2 } 554 | } 555 | permission_filter = { 556 | 'type': 'PermissionFilter', 557 | 'config': ['assets.analytic:download'] 558 | } 559 | #config = [ geometry_filter, date_range_filter, permission_filter ] 560 | config = [ geometry_filter, date_range_filter ] 561 | json_request = { 562 | "item_types": get_item_types(), 563 | "filter": { "type": "AndFilter", "config": config } 564 | } 565 | processScenes( json_request ) 566 | if self.hasCriticalMessage: 567 | self.canvas.scene().removeItem( rb ) 568 | return 569 | if self.total_features_scenes == 0: 570 | self.canvas.scene().removeItem( rb ) 571 | msg = "Not found images" 572 | self.msgBar.popWidget() 573 | self.msgBar.pushMessage( CatalogPL.pluginName, msg, QgsGui.QgsMessageBar.WARNING, 2 ) 574 | return 575 | 576 | self.msgBar.popWidget() 577 | item_types = ",".join( json_request['item_types'] ) 578 | msg = "Item types: {2}".format( date2, days, item_types ) 579 | self.mbcancel = MessageBarCancel( CatalogPL.pluginName, self.msgBar, msg, self.apiPL.kill ) 580 | self.total_features_scenes = 0 581 | 582 | prov = self.layer.dataProvider() 583 | while self.url_scenes: 584 | if self.mbcancel.isCancel or self.layerTree is None : 585 | break 586 | addFeatures() 587 | msg = "Adding {0} features...".format( self.total_features_scenes ) 588 | self.mbcancel.message( msg ) 589 | 590 | finished() 591 | 592 | def checkLayerLegend(): 593 | # self.settings setting by __init__.setSearchSettings() 594 | if self.settings['isOk']: 595 | return True 596 | if not self.settings['has_path']: 597 | msg = "Please setting the directory for download in Planet Labs Catalog layer" 598 | else: 599 | msg = "The directory '{0}' not found, please setting directory in Planet Labs Catalog layer" 600 | msg = msg.format( self.settings['path'] ) 601 | self.msgBar.pushMessage( CatalogPL.pluginName, msg, QgsGui.QgsMessageBar.WARNING, 6 ) 602 | return False 603 | 604 | self.enableRun.emit( False ) 605 | 606 | # Setting Layer 607 | if not self.layer is None: 608 | QgsCore.QgsMapLayerRegistry.instance().removeMapLayer( self.layer.id() ) 609 | createLayer() 610 | self.layerTree.setVisible(QtCore.Qt.Unchecked) 611 | 612 | if not checkLayerLegend(): 613 | self.legendCatalogLayer.setLayer( self.layer ) 614 | self.enableRun.emit( True ) 615 | return 616 | 617 | self.hasCriticalMessage = False 618 | populateLayer() # addFeatures().commitFeatures() -> QtCore.QEventLoop() 619 | self.legendCatalogLayer.setLayer( self.layer ) 620 | self.enableRun.emit( True ) 621 | 622 | def getTotalAssets(self): 623 | iterFeat = self.layer.selectedFeaturesIterator() 624 | if self.layer.selectedFeatureCount() == 0: 625 | iterFeat = self.layer.getFeatures() 626 | 627 | totalAssets = { 628 | 'analytic': { 'images': 0, 'activate': 0 }, 629 | 'udm': { 'images': 0, 'activate': 0 } 630 | } 631 | for feat in iterFeat: 632 | meta_json = json.loads( feat['meta_json'] ) 633 | valuesAssets = self._getValuesAssets( meta_json['assets_status'] ) 634 | 635 | self._calculateTotalAsset('analytic', valuesAssets, totalAssets ) 636 | self._calculateTotalAsset('udm', valuesAssets, totalAssets ) 637 | 638 | return totalAssets 639 | 640 | @QtCore.pyqtSlot(str) 641 | def layerWillBeRemoved(self, id): 642 | if not self.layerTree is None and id == self.layer.id(): 643 | self.apiPL.kill() 644 | self.worker.kill() 645 | self.legendCatalogLayer.clean() 646 | self.layerTree = self.layer = None 647 | 648 | @QtCore.pyqtSlot() 649 | def clearKey(self): 650 | def dialogQuestion(): 651 | title = "Planet Labs" 652 | msg = "Are you sure want clear the register key?" 653 | msgBox = QtGui.QMessageBox( QtGui.QMessageBox.Question, title, msg, QtGui.QMessageBox.Yes | QtGui.QMessageBox.No, self.mainWindow ) 654 | msgBox.setDefaultButton( QtGui.QMessageBox.No ) 655 | return msgBox.exec_() 656 | 657 | if self.mngLogin.getKeySetting() is None: 658 | msg = "Already cleared the register key. Next run QGIS will need enter the key." 659 | self.msgBar.pushMessage( CatalogPL.pluginName, msg, QgsGui.QgsMessageBar.INFO, 4 ) 660 | return 661 | 662 | if QtGui.QMessageBox.Yes == dialogQuestion(): 663 | self.mngLogin.removeKey() 664 | msg = "Cleared the register key. Next run QGIS will need enter the key." 665 | self.msgBar.pushMessage( CatalogPL.pluginName, msg, QgsGui.QgsMessageBar.INFO, 4 ) 666 | 667 | @QtCore.pyqtSlot() 668 | def clipboardKey(self): 669 | cb = QtGui.QApplication.clipboard() 670 | key = self.mngLogin.getKeySetting() 671 | if key is None: 672 | key = "Don't have key registered" 673 | cb.setText( key, mode=cb.Clipboard ) 674 | 675 | @QtCore.pyqtSlot() 676 | def settingImages(self): 677 | settings = self.settings if self.settings['isOk'] else None 678 | dlg = DialogImageSettingPL( self.mainWindow, self.icon, settings ) 679 | if dlg.exec_() == QtGui.QDialog.Accepted: 680 | self.settings = dlg.getData() 681 | 682 | # Need Changed! Not used 2017-12-20 683 | @QtCore.pyqtSlot() 684 | def createTMS_ServerXYZ(self): 685 | @QtCore.pyqtSlot( dict ) 686 | def finished( message ): 687 | self.thread.quit() 688 | self.worker.finished.disconnect( finished ) 689 | self._endProcessing( "Create TMS", message['totalError'] ) 690 | 691 | self._setGroupCatalog('TMS') 692 | r = self._startProcess( self.worker.kill ) 693 | if not r['isOk']: 694 | return 695 | iterFeat = r['iterFeat'] 696 | 697 | path_tms = os.path.join( self.settings['path'], 'tms') 698 | if not os.path.exists( path_tms ): 699 | os.makedirs( path_tms ) 700 | 701 | self.worker.finished.connect( finished ) 702 | self.worker.setting( iterFeat, ltgRoot, self.catalog['ltg'] ) 703 | self.worker.stepProgress.connect( self.mbcancel.step ) 704 | self.thread.start() # Start Worker 705 | #self.worker.run() #DEBUGER 706 | 707 | @QtCore.pyqtSlot() 708 | def calculateAssetStatus(self): 709 | @QtCore.pyqtSlot( dict ) 710 | def finished( response ): 711 | if self.mbcancel.isCancel: 712 | self.messagePL = None 713 | else: 714 | self.messagePL = response['assets_status'] 715 | loop.quit() 716 | 717 | r = self._startProcess( self.apiPL.kill ) 718 | if not r['isOk']: 719 | return 720 | iterFeat = r['iterFeat'] 721 | 722 | id_meta_json = self.layer.fieldNameIndex('meta_json') 723 | id_meta_html = self.layer.fieldNameIndex('meta_html') 724 | isEditable = self.layer.isEditable() 725 | if not isEditable: 726 | self.layer.startEditing() 727 | totalAssets = { 728 | 'analytic': { 'images': 0, 'activate': 0 }, 729 | 'udm': { 'images': 0, 'activate': 0 } 730 | } 731 | loop = QtCore.QEventLoop() 732 | step = totalError = 0 733 | for feat in iterFeat: 734 | step += 1 735 | self.mbcancel.step( step ) 736 | meta_json = json.loads( feat['meta_json'] ) 737 | ( ok, item_type ) = API_PlanetLabs.getValue( meta_json, [ 'item_type' ] ) 738 | if not ok: 739 | totalError += 1 740 | continue 741 | self.apiPL.getAssetsStatus( item_type, feat['id'], finished ) 742 | loop.exec_() 743 | if self.mbcancel.isCancel or self.layerTree is None : 744 | step -= 1 745 | iterFeat.close() 746 | break 747 | meta_json['assets_status'] = self.messagePL 748 | valuesAssets = self._getValuesAssets( meta_json['assets_status'] ) 749 | self._calculateTotalAsset('analytic', valuesAssets, totalAssets ) 750 | self._calculateTotalAsset('udm', valuesAssets, totalAssets ) 751 | meta_html = API_PlanetLabs.getHtmlTreeMetadata( meta_json, '') 752 | vjson = json.dumps( meta_json ) 753 | self.messagePL.clear() 754 | self.messagePL = None 755 | if not self.layer.changeAttributeValue( feat.id(), id_meta_json, vjson ): 756 | totalError += 1 757 | if not self.layer.changeAttributeValue( feat.id(), id_meta_html, meta_html ): 758 | totalError += 1 759 | 760 | self.layer.commitChanges() 761 | if isEditable: 762 | self.layer.startEditing() 763 | 764 | self._endProcessing( "Calculate Asset Status", totalError ) 765 | self.legendCatalogLayer.setAssetImages( totalAssets ) 766 | 767 | ## Its is for API V0! Not update 768 | 769 | @QtCore.pyqtSlot() 770 | def activateAssets(self): 771 | def activeAsset(asset, activate, dataLocal): 772 | def setFinished( response ): 773 | if not response[ 'isOk' ] and not self._hasLimiteErrorOK(response ): 774 | r = self._hasErrorDownloads(response) 775 | if r['isOk']: 776 | arg = ( self.currentItem, r['message'] ) 777 | msg = "Error request for {0}: {1}".format( *arg ) 778 | else: 779 | arg = ( self.currentItem, response['message'], response[ 'errorCode' ] ) 780 | msg = "Error request for {0}: {1} (Code = {2})".format( *arg ) 781 | self.logMessage( msg, CatalogPL.pluginName, QgsCore.QgsMessageLog.CRITICAL ) 782 | self.currentItem = None 783 | self.isOkPL = False 784 | loop.quit() 785 | 786 | self.mbcancel.step( dataLocal['step'] ) 787 | if self.mbcancel.isCancel or self.layerTree is None : 788 | dataLocal['step'] -= 1 789 | iterFeat.close() 790 | return False 791 | self.currentItem = "'{0}({1})'".format( feat['id'], asset ) 792 | self.isOkPL = True 793 | self.apiPL.activeAsset( activate, setFinished ) 794 | loop.exec_() 795 | if not self.isOkPL: 796 | dataLocal['totalError'] += 1 797 | return True # Not cancel by user 798 | 799 | #msg = "Sorry! I am working this feature for API Planet V1" 800 | #self.msgBar.pushMessage( CatalogPL.pluginName, msg, QgsGui.QgsMessageBar.CRITICAL, 8 ) 801 | #return 802 | r = self._startProcess( self.apiPL.kill ) 803 | if not r['isOk']: 804 | return 805 | iterFeat = r['iterFeat'] 806 | 807 | dataLocal = { 'totalError': 0, 'step': 0 } 808 | loop = QtCore.QEventLoop() 809 | for feat in iterFeat: 810 | dataLocal['step'] += 1 811 | meta_json = json.loads( feat['meta_json'] ) 812 | valuesAssets = self._getValuesAssets( meta_json['assets_status'] ) 813 | asset = 'analytic' 814 | if valuesAssets[ asset ]['isOk'] and \ 815 | valuesAssets[ asset ]['status'] == 'inactive' and \ 816 | valuesAssets[ asset ].has_key('activate'): 817 | if not activeAsset( asset, valuesAssets[ asset ]['activate'], dataLocal ): 818 | break # Cancel by user 819 | asset = 'udm' 820 | if valuesAssets[ asset ]['isOk'] and \ 821 | valuesAssets[ asset ]['status'] == 'inactive' and \ 822 | valuesAssets[ asset ].has_key('activate'): 823 | if not activeAsset( asset, valuesAssets[ asset ]['activate'], dataLocal ): 824 | break # Cancel by user 825 | 826 | self._endProcessing( "Activate assets", dataLocal['totalError'] ) 827 | 828 | @QtCore.pyqtSlot() 829 | def CreateTMS_GDAL_WMS(self): 830 | @QtCore.pyqtSlot( dict ) 831 | def finished( message ): 832 | self.thread.quit() 833 | self.worker.finished.disconnect( finished ) 834 | self._sortNameGroupCatalog() 835 | self._endProcessing( "Create TMS", message['totalError'] ) 836 | 837 | self._setGroupCatalog('TMS') 838 | r = self._startProcess( self.worker.kill ) 839 | if not r['isOk']: 840 | return 841 | iterFeat = r['iterFeat'] 842 | 843 | path_tms = os.path.join( self.settings['path'], 'tms') 844 | if not os.path.exists( path_tms ): 845 | os.makedirs( path_tms ) 846 | cr3857 = QgsCore.QgsCoordinateReferenceSystem( 3857, QgsCore.QgsCoordinateReferenceSystem.EpsgCrsId ) 847 | ctTMS = QgsCore.QgsCoordinateTransform( self.layer.crs(), cr3857 ) 848 | 849 | self.worker.finished.connect( finished ) 850 | data = { 851 | 'pluginName': CatalogPL.pluginName, 852 | 'getURL': API_PlanetLabs.getURL_TMS, 853 | 'user_pwd': { 'user': API_PlanetLabs.validKey, 'pwd': '' }, 854 | 'path': path_tms, 855 | 'ltgCatalog': self.catalog['ltg'], 856 | 'id_layer': self.layer.id(), 857 | 'ctTMS': ctTMS, 858 | 'iterFeat': iterFeat # feat: 'id', 'acquired', 'meta_json' 859 | } 860 | self.worker.setting( data ) 861 | 862 | self.worker.stepProgress.connect( self.mbcancel.step ) 863 | self.thread.start() # Start Worker 864 | #self.worker.run() #DEBUGER 865 | 866 | @QtCore.pyqtSlot() 867 | def downloadThumbnails(self): 868 | def setFinished( response ): 869 | if response[ 'isOk' ]: 870 | self.pixmap = response[ 'pixmap' ] 871 | else: 872 | arg = ( self.currentItem, response['message'], response['errorCode'] ) 873 | msg = "Error request for {0}: {1} (Code = {2})".format( *arg ) 874 | self.logMessage( msg, CatalogPL.pluginName, QgsCore.QgsMessageLog.CRITICAL ) 875 | self.currentItem = None 876 | self.pixmap = None 877 | loop.quit() 878 | 879 | def createThumbnails(): 880 | self.currentItem = feat['id'] 881 | arg = ( feat['meta_json'], ['item_type'] ) 882 | ( ok, item_type ) = API_PlanetLabs.getValue( *arg ) 883 | if not ok: 884 | totalError += 1 885 | response = { 'isOk': False, 'errorCode': -1, 'message': item_type } 886 | setFinished( response ) 887 | else: 888 | self.apiPL.getThumbnail( feat['id'], item_type, setFinished ) 889 | 890 | r = self._startProcess( self.apiPL.kill ) 891 | if not r['isOk']: 892 | return 893 | iterFeat = r['iterFeat'] 894 | 895 | totalError = step = 0 896 | self.pixmap = None # Populate(self.apiPL.getThumbnail) and catch(setFinished) 897 | id_thumbnail = self.layer.fieldNameIndex('thumbnail') 898 | path_thumbnail = os.path.join( self.settings['path'], 'thumbnail') 899 | if not os.path.exists( path_thumbnail ): 900 | os.makedirs( path_thumbnail ) 901 | isEditable = self.layer.isEditable() 902 | if not isEditable: 903 | self.layer.startEditing() 904 | loop = QtCore.QEventLoop() 905 | for feat in iterFeat: 906 | step += 1 907 | self.mbcancel.step( step ) 908 | if self.mbcancel.isCancel or self.layerTree is None : 909 | iterFeat.close() 910 | step -= 1 911 | break 912 | arg = ( path_thumbnail, u"{0}_thumbnail.png".format( feat['id'] ) ) 913 | file_thumbnail = os.path.join( *arg ) 914 | if not QtCore.QFile.exists( file_thumbnail ): 915 | createThumbnails() 916 | loop.exec_() 917 | if not self.pixmap is None: 918 | isOk = self.pixmap.save( file_thumbnail, "PNG") 919 | del self.pixmap 920 | self.pixmap = None 921 | if not isOk: 922 | totalError += 1 923 | else: 924 | arg = ( feat.id(), id_thumbnail, file_thumbnail ) 925 | self.layer.changeAttributeValue( *arg ) 926 | else: 927 | totalError += 1 928 | else: 929 | arg = ( feat.id(), id_thumbnail, file_thumbnail ) 930 | self.layer.changeAttributeValue( *arg ) 931 | 932 | self.layer.commitChanges() 933 | if isEditable: 934 | self.layer.startEditing() 935 | 936 | self._endProcessing( "Download Thumbnails", totalError ) 937 | 938 | @QtCore.pyqtSlot() 939 | def downloadImages(self): 940 | def createImage(suffix, location, id_table, geom, v_crs, acquired, id_image, dataLocal, add_image=False): 941 | def setFinished( response ): 942 | self.imageDownload.flush() 943 | self.imageDownload.close() 944 | if not response[ 'isOk' ] and not self._hasLimiteErrorOK(response ): 945 | r = self._hasErrorDownloads(response) 946 | if r['isOk']: 947 | arg = ( self.currentItem, r['message'] ) 948 | msg = "Error request for {0}: {1}".format( *arg ) 949 | else: 950 | arg = ( self.currentItem, response['message'], response[ 'errorCode' ] ) 951 | msg = "Error request for {0}: {1} (Code = {2})".format( *arg ) 952 | self.logMessage( msg, CatalogPL.pluginName, QgsCore.QgsMessageLog.CRITICAL ) 953 | self.currentItem = None 954 | self.isOkPL = False 955 | self.totalReady = None 956 | self.imageDownload.remove() 957 | else: 958 | self.totalReady = response[ 'totalReady' ] 959 | fileName = self.imageDownload.fileName() 960 | self.imageDownload.rename( '.'.join( fileName.rsplit('.')[:-1] ) ) 961 | del self.imageDownload 962 | self.imageDownload = None 963 | loop.quit() 964 | 965 | def addImage(): 966 | layer = QgsCore.QgsRasterLayer( file_image, os.path.split( file_image )[-1] ) 967 | geomTransf = QgsCore.QgsGeometry( geom ) 968 | ct = QgsCore.QgsCoordinateTransform( v_crs, layer.crs() ) 969 | geomTransf.transform( ct ) 970 | wkt_geom = geomTransf.exportToWkt() 971 | layer.setCustomProperty( 'wkt_geom', wkt_geom ) 972 | layer.setCustomProperty( 'date', acquired ) 973 | layer.setCustomProperty( 'id_table', id_table ) 974 | layer.setCustomProperty( 'id_image', id_image ) 975 | QgsCore.QgsMapLayerRegistry.instance().addMapLayer( layer, addToLegend=False ) 976 | self.catalog['ltg'].addLayer( layer ) 977 | self.legendRasterGeom.setLayer( layer ) 978 | 979 | arg = ( path_img, u"{0}_{1}.tif".format( feat['id'], suffix ) ) 980 | file_image = os.path.join( *arg ) 981 | self.mbcancel.step( dataLocal['step'], file_image ) 982 | if self.mbcancel.isCancel or self.layerTree is None : 983 | dataLocal['step'] -= 1 984 | iterFeat.close() 985 | return False 986 | self.isOkPL = True 987 | if not QtCore.QFile.exists( file_image ): 988 | self.currentItem = "'{0}({1})'".format( feat['id'], suffix ) 989 | self.imageDownload = QtCore.QFile( "{0}.part".format( file_image ) ) 990 | self.imageDownload.open( QtCore.QIODevice.WriteOnly ) 991 | arg = ( location, setFinished, self.imageDownload.write, self.mbcancel.stepFile ) 992 | self.apiPL.saveImage( *arg ) 993 | loop.exec_() 994 | if not self.isOkPL: 995 | dataLocal['totalError'] += 1 996 | if add_image and self.isOkPL: 997 | files_in_map = map( lambda item: item.layer().source(), dataLocal['ltgRoot'].findLayers() ) 998 | if not file_image in files_in_map: 999 | addImage() 1000 | return True # Not cancel by user 1001 | 1002 | self._setGroupCatalog('TIF') 1003 | r = self._startProcess( self.worker.kill, True ) 1004 | if not r['isOk']: 1005 | return 1006 | iterFeat = r['iterFeat'] 1007 | 1008 | path_img = os.path.join( self.settings['path'], 'tif') 1009 | if not os.path.exists( path_img ): 1010 | os.makedirs( path_img ) 1011 | 1012 | crsLayer = self.layer.crs() 1013 | id_table = self.layer.id() 1014 | ltgRoot = QgsCore.QgsProject.instance().layerTreeRoot() 1015 | dataLocal = { 'totalError': 0, 'step': 0, 'ltgRoot': ltgRoot } 1016 | self.totalReady = None 1017 | loop = QtCore.QEventLoop() 1018 | for feat in iterFeat: 1019 | dataLocal['step'] += 1 1020 | meta_json = json.loads( feat['meta_json'] ) 1021 | valuesAssets = self._getValuesAssets( meta_json['assets_status'] ) 1022 | arg_core = [ id_table, feat.geometry(), crsLayer, feat['acquired'], feat['id'], dataLocal ] 1023 | asset = 'analytic' 1024 | if valuesAssets[ asset ]['isOk'] and valuesAssets[ asset ].has_key('location'): 1025 | arg_base = [ valuesAssets[ asset ]['location'] ] + arg_core 1026 | arg = [ asset ] + arg_base + [ True ] 1027 | if not createImage( *arg ): 1028 | break # Cancel by user 1029 | if self.settings['udm']: 1030 | asset = 'udm' 1031 | if valuesAssets[ asset ]['isOk'] and valuesAssets[ asset ].has_key('location'): 1032 | arg_base = [ valuesAssets[ asset ]['location'] ] + arg_core 1033 | arg = [ asset] + arg_base 1034 | if not createImage( *arg ): 1035 | break # Cancel by user 1036 | 1037 | self._sortNameGroupCatalog() 1038 | self._endProcessing( "Download Images", dataLocal['totalError'] ) 1039 | 1040 | @staticmethod 1041 | def copyExpression(): 1042 | dirname = os.path.dirname 1043 | fromExp = os.path.join( dirname( __file__ ), CatalogPL.expressionFile ) 1044 | dirExp = os.path.join( dirname( dirname( dirname( __file__ ) ) ), CatalogPL.expressionDir ) 1045 | toExp = os.path.join( dirExp , CatalogPL.expressionFile ) 1046 | if os.path.isdir( dirExp ): 1047 | if QtCore.QFile.exists( toExp ): 1048 | QtCore.QFile.remove( toExp ) 1049 | QtCore.QFile.copy( fromExp, toExp ) 1050 | -------------------------------------------------------------------------------- /catalogpl.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /comandos_git.txt: -------------------------------------------------------------------------------- 1 | Comandos do Github 2 | 3 | - Comitar local 4 | * Edite os arquivos - eles ficam no 'staged' 5 | * Os arquivos editados ficam no 'staged' 6 | * Adicionar os arquivos (add .) ou algum especifico (add nome_arquivo) do 'staged' 7 | git add . 8 | git commit -m "" 9 | OU 10 | git commit -a -m "" 11 | 12 | - Empurrar o diretório remoto(GitHub) - Publicar os commits locais 13 | git push 14 | * use o usr/pwd do Github: lmotta/lgithub17 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /create_zip_plugin.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | plugin_dir=$( basename $( pwd ) ) 3 | if [ -f "./$plugin_dir.zip" ]; then 4 | rm "./$plugin_dir.zip" 5 | fi 6 | mkdir "./$plugin_dir" 7 | cp *.py "./$plugin_dir" 8 | for item in metadata.txt README.md LICENSE catalogpl.svg pl_scenes.qml; do cp "./$item" "./$plugin_dir"; done 9 | zip -r $plugin_dir $plugin_dir 10 | rm -r $plugin_dir 11 | # 12 | kdialog --msgbox "Zip file created: "$plugin_dir".zip" 13 | -------------------------------------------------------------------------------- /legendlayer.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | Name : Legend Layer 5 | Description : Classes for add legend in layer 6 | Date : July, 2015 7 | copyright : (C) 2015 by Luiz Motta 8 | email : motta.luiz@gmail.com 9 | 10 | ***************************************************************************/ 11 | 12 | /*************************************************************************** 13 | * * 14 | * This program is free software; you can redistribute it and/or modify * 15 | * it under the terms of the GNU General Public License as published by * 16 | * the Free Software Foundation; either version 2 of the License, or * 17 | * (at your option) any later version. * 18 | * * 19 | ***************************************************************************/ 20 | """ 21 | 22 | from PyQt4 import QtCore, QtGui, QtXml 23 | 24 | from qgis import core as QgsCore, gui as QgsGui, utils as QgsUtils 25 | 26 | 27 | class PolygonEffectsCanvas(): 28 | def __init__(self): 29 | self.canvas = QgsUtils.iface.mapCanvas() 30 | self.crs = None # setCRS if need 31 | self.color = QtGui.QColor(255,0,0) 32 | 33 | def setCRS(self, crs): 34 | self.crs = crs 35 | 36 | def zoom(self, extent): 37 | extentTransform = extent 38 | crsCanvas = self.canvas.mapSettings().destinationCrs() 39 | if not self.crs == crsCanvas: 40 | ct = QgsCore.QgsCoordinateTransform( self.crs, crsCanvas ) 41 | extentTransform = ct.transform( extent ) 42 | self.canvas.setExtent( extentTransform ) 43 | self.canvas.zoomByFactor(1.05) 44 | self.canvas.refresh() 45 | 46 | def highlight(self, geom, seconds=2): 47 | def removeRB(): 48 | rb.reset( True ) 49 | self.canvas.scene().removeItem( rb ) 50 | 51 | geomTransform = geom 52 | crsCanvas = self.canvas.mapSettings().destinationCrs() 53 | if not self.crs == crsCanvas: 54 | ct = QgsCore.QgsCoordinateTransform( self.crs, crsCanvas ) 55 | geomTransform.transform( ct ) 56 | 57 | rb = QgsGui.QgsRubberBand( self.canvas, QgsCore.QGis.Polygon) 58 | rb.setBorderColor( self.color ) 59 | rb.setWidth(2) 60 | rb.setToGeometry( geomTransform, None ) 61 | QtCore.QTimer.singleShot( seconds*1000, removeRB ) 62 | 63 | class LegendRaster(object): 64 | def __init__(self, pluginName): 65 | def initLegendLayer(): 66 | self.legendLayer = [ 67 | { 68 | 'menu': u"Highlight", 69 | 'id': "idHighlight", 70 | 'slot': self.highlight, 71 | 'action': None 72 | }, 73 | { 74 | 'menu': u"Zoom to", 75 | 'id': "idZoom", 76 | 'slot': self.zoom, 77 | 'action': None 78 | }, 79 | { 80 | 'menu': u"Open form(table)", 81 | 'id': "idForm", 82 | 'slot': self.openForm, 83 | 'action': None 84 | } 85 | ] 86 | for item in self.legendLayer: 87 | item['action'] = QtGui.QAction( item['menu'], None ) 88 | item['action'].triggered.connect( item['slot'] ) 89 | self.legendInterface.addLegendLayerAction( item['action'], self.pluginName, item['id'], QgsCore.QgsMapLayer.RasterLayer, False ) 90 | 91 | self.pluginName = pluginName 92 | self.msgBar = QgsUtils.iface.messageBar() 93 | self.legendInterface = QgsUtils.iface.legendInterface() 94 | initLegendLayer() # Set self.legendLayer 95 | self.polygonEC = PolygonEffectsCanvas() 96 | 97 | def __del__(self): 98 | for item in self.legendLayer: 99 | self.legendInterface.removeLegendLayerAction( item['action'] ) 100 | 101 | def setLayer(self, layer): 102 | for item in self.legendLayer: 103 | self.legendInterface.addLegendLayerActionForLayer( item['action'], layer ) 104 | 105 | @QtCore.pyqtSlot() 106 | def zoom(self): 107 | layer = self.legendInterface.currentLayer() 108 | extent = layer.extent() 109 | self.polygonEC.setCRS( layer.crs() ) 110 | self.polygonEC.zoom( extent ) 111 | geom = QgsCore.QgsGeometry.fromRect( extent ) 112 | self.polygonEC.highlight( geom ) 113 | 114 | @QtCore.pyqtSlot() 115 | def highlight(self): 116 | layer = self.legendInterface.currentLayer() 117 | geom = QgsCore.QgsGeometry.fromRect( layer.extent() ) 118 | self.polygonEC.highlight( geom ) 119 | 120 | @QtCore.pyqtSlot() 121 | def openForm(self): 122 | pass 123 | 124 | class LegendTMSXml(LegendRaster): 125 | def __init__(self, pluginName): 126 | super(LegendRasterGeom, self).__init__( pluginName ) 127 | 128 | def _getExtent(self, layer): 129 | def getTargetWindow(): 130 | nodes = doc.elementsByTagName('TargetWindow') 131 | node = nodes.item( 0 ) 132 | targetWindow = { 'ulX': None, 'ulY': None, 'lrX': None, 'lrY': None } 133 | labels = { 'UpperLeftX': 'ulX', 'UpperLeftY': 'ulY', 'LowerRightX': 'lrX', 'LowerRightY': 'lrY' } 134 | for key, value in labels.iteritems(): 135 | text = node.firstChildElement( key ).text() 136 | if len( text ) == 0: 137 | continue 138 | targetWindow[ value ] = float( text ) 139 | return targetWindow 140 | 141 | doc = QtXml.QDomDocument() 142 | file = QtCore.QFile( layer.source() ) 143 | doc.setContent( file ) 144 | file.close() 145 | 146 | tw = getTargetWindow() 147 | return QgsCore.QgsRectangle( tw['ulX'], tw['lrY'], tw['lrX'], tw['ulY'] ) 148 | 149 | @QtCore.pyqtSlot() 150 | def zoom(self): 151 | layer = self.legendInterface.currentLayer() 152 | extent = self._getExtent( layer ) 153 | self.polygonEC.setCRS( layer.crs() ) 154 | self.polygonEC.zoom( extent ) 155 | geom = QgsCore.QgsGeometry.fromRect( extent ) 156 | self.polygonEC.highlight( geom ) 157 | 158 | @QtCore.pyqtSlot() 159 | def highlight(self): 160 | layer = self.legendInterface.currentLayer() 161 | extent = self.self._getExtent( layer ) 162 | geom = QgsCore.QgsGeometry.fromRect( extent ) 163 | self.polygonEC.setCRS( layer.crs() ) 164 | self.polygonEC.highlight( geom ) 165 | 166 | class LegendRasterGeom(LegendRaster): 167 | def __init__(self, pluginName): 168 | super(LegendRasterGeom, self).__init__( pluginName ) 169 | 170 | def _getGeometry(self, layer): 171 | wkt_geom = layer.customProperty('wkt_geom') 172 | return QgsCore.QgsGeometry.fromWkt( wkt_geom ) 173 | 174 | @QtCore.pyqtSlot() 175 | def zoom(self): 176 | layer = self.legendInterface.currentLayer() 177 | geom = self._getGeometry( layer ) 178 | self.polygonEC.setCRS( layer.crs() ) 179 | self.polygonEC.zoom( geom.boundingBox() ) 180 | self.polygonEC.highlight( geom ) 181 | 182 | @QtCore.pyqtSlot() 183 | def highlight(self): 184 | layer = self.legendInterface.currentLayer() 185 | geom = self._getGeometry( layer ) 186 | self.polygonEC.setCRS( layer.crs() ) 187 | self.polygonEC.highlight( geom ) 188 | 189 | @QtCore.pyqtSlot() 190 | def openForm(self): 191 | layer = self.legendInterface.currentLayer() 192 | id_table = layer.customProperty('id_table') 193 | layers_table = [ l for l in self.legendInterface.layers() if id_table == l.id() ] 194 | if len( layers_table ) == 0: 195 | msg = "Layer used for create this image not found." 196 | arg = ( self.pluginName, msg, QgsGui.QgsMessageBar.WARNING, 4 ) 197 | self.msgBar.pushMessage( *arg ) 198 | return 199 | table = layers_table[0] 200 | id_image = layer.customProperty('id_image') 201 | request = QgsCore.QgsFeatureRequest().setFilterExpression( u'"id" = \'{0}\''.format( id_image ) ) 202 | request.setFlags( QgsCore.QgsFeatureRequest.NoGeometry ) 203 | feats = [ f for f in table.getFeatures(request) ] 204 | if len( feats ) == 0: 205 | msg = "Image '{}' not found in '{}'.".format( id_image, table.name() ) 206 | arg = ( self.pluginName, msg, QgsGui.QgsMessageBar.WARNING, 4 ) 207 | self.msgBar.pushMessage( *arg ) 208 | return 209 | form = QgsUtils.iface.getFeatureForm( table, feats[0] ) 210 | form.show() 211 | -------------------------------------------------------------------------------- /legendlayerpl.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | Name : Legend Layer 5 | Description : Legend Layer for Planet Labs layer 6 | Date : June, 2015 7 | copyright : (C) 2015 by Luiz Motta 8 | email : motta.luiz@gmail.com 9 | 10 | ***************************************************************************/ 11 | 12 | /*************************************************************************** 13 | * * 14 | * This program is free software; you can redistribute it and/or modify * 15 | * it under the terms of the GNU General Public License as published by * 16 | * the Free Software Foundation; either version 2 of the License, or * 17 | * (at your option) any later version. * 18 | * * 19 | ***************************************************************************/ 20 | """ 21 | 22 | import os, shutil 23 | 24 | from PyQt4 import QtCore, QtGui, QtXml 25 | import qgis 26 | from qgis import core as QgsCore, gui as QgsGui, utils as QgsUtils 27 | 28 | class DialogImageSettingPL(QtGui.QDialog): 29 | 30 | localSetting = "catalogpl_plugin" # ~/.config/QGIS/QGIS2.conf 31 | 32 | def __init__(self, parent, icon=None, data=None): 33 | def initGui(): 34 | def setData(): 35 | def getSizeCacheTMS(): 36 | ( path, dirs ) = self._getDirsCacheTMS() 37 | if path is None: 38 | return 0 39 | total_size = 0 40 | # Files(XMLs) 41 | absdir = lambda d: os.path.join( path, d) 42 | for f in os.listdir( path ): 43 | if os.path.isfile( absdir( f ) ): 44 | total_size += os.path.getsize( absdir( f ) ) / 1024.0 # KB 45 | # Directories 46 | if len( dirs ) == 0: 47 | return total_size/1024.0 # in MB 48 | for path in dirs: 49 | for dirpath, dirnames, filenames in os.walk(path): 50 | total_size += os.path.getsize(dirpath) / 1024.0 # KB 51 | for f in filenames: 52 | fp = os.path.join( dirpath, f ) 53 | total_size += os.path.getsize(fp) / 1024.0 # KB 54 | return total_size/1024.0 # in MB 55 | 56 | w = self.findChild( QtGui.QRadioButton, self.data['current_asset'] ) 57 | w.setChecked(True) 58 | checkUdm.setChecked( self.data['udm'] ) 59 | buttonPath.setText( self.data['path'] ) 60 | total = getSizeCacheTMS() 61 | if total > 0: 62 | buttonClearCache.setText( self.titleClearCache.format( total ) ) 63 | buttonClearCache.setEnabled(True) 64 | d1 = self.data['date1'] 65 | d2 = self.data['date2'] 66 | date1.setDate( d1 ) 67 | date2.setDate( d2 ) 68 | date1.setMaximumDate( d2.addDays( -1 ) ) 69 | date2.setMinimumDate( d1.addDays( +1 ) ) 70 | spinDay.setValue( d1.daysTo( d2) ) 71 | 72 | def connect(): 73 | buttonOK.clicked.connect( self.onOK ) 74 | buttonPath.clicked.connect( self.onPath ) 75 | buttonClearCache.clicked.connect( self.onClearCache ) 76 | date1.dateChanged.connect( self.onDateChanged1 ) 77 | date2.dateChanged.connect( self.onDateChanged2 ) 78 | spinDay.valueChanged.connect( self.onValueChanged ) 79 | 80 | def createCheckBox(text, objName, group, layout=None): 81 | widget = QtGui.QCheckBox( text, group ) 82 | widget.setObjectName( objName ) 83 | if not layout is None: 84 | layout.addWidget( widget ) 85 | return widget 86 | 87 | def createDateEdit(labelName, objName, group, layout): 88 | label = QtGui.QLabel( labelName, group ) 89 | layout.addWidget( label ) 90 | widget = QtGui.QDateEdit( group ) 91 | widget.setObjectName( objName ) 92 | widget.setCalendarPopup( True ) 93 | format = widget.displayFormat().replace('yy', 'yyyy') 94 | widget.setDisplayFormat( format ) 95 | layout.addWidget( widget ) 96 | return widget 97 | 98 | def createRadioButton(text, objName, group, layout=None): 99 | widget = QtGui.QRadioButton( text, group ) 100 | widget.setObjectName( objName ) 101 | if not layout is None: 102 | layout.addWidget( widget ) 103 | 104 | windowTitle = "Setting Planet Labs" 105 | self.setWindowTitle( windowTitle ) 106 | self.setWindowIcon( icon ) 107 | 108 | grpImage = QtGui.QGroupBox('Images', self ) 109 | # https://www.planet.com/docs/reference/data-api/items-assets/#item-type 110 | lytAssets = QtGui.QHBoxLayout() 111 | for name in self.nameAssets: 112 | createRadioButton( name.capitalize(), name, grpImage, lytAssets ) 113 | 114 | checkUdm = createCheckBox( 'Save UDM(Unusable Data Mask)', 'udm', grpImage ) 115 | 116 | buttonPath = QtGui.QPushButton( self.titleSelectDirectory, grpImage ) 117 | buttonPath.setObjectName('path') 118 | 119 | buttonClearCache = QtGui.QPushButton( self.titleClearCache.format(0), grpImage ) 120 | buttonClearCache.setObjectName('clear_cache') 121 | buttonClearCache.setEnabled(False) 122 | 123 | lytImage = QtGui.QVBoxLayout( grpImage ) 124 | lytImage.addLayout( lytAssets ) 125 | lytImage.addWidget( checkUdm ) 126 | lytImage.addWidget( buttonPath ) 127 | lytImage.addWidget( buttonClearCache ) 128 | 129 | grpDateSearch = QtGui.QGroupBox('Dates for search', self ) 130 | lytDate = QtGui.QHBoxLayout( grpDateSearch ) 131 | date1 = createDateEdit('From', 'deDate1', grpDateSearch, lytDate ) 132 | date2 = createDateEdit('To', 'deDate2', grpDateSearch, lytDate ) 133 | spinDay = QtGui.QSpinBox( grpDateSearch ) 134 | spinDay.setObjectName('sbDay') 135 | spinDay.setSingleStep( 1 ) 136 | spinDay.setSuffix(' Days') 137 | spinDay.setRange( 1, 1000*360 ) 138 | lytDate.addWidget( spinDay ) 139 | 140 | buttonOK = QtGui.QPushButton('OK', self ) 141 | 142 | layout = QtGui.QVBoxLayout( self ) 143 | layout.addWidget( grpImage ) 144 | layout.addWidget( grpDateSearch ) 145 | layout.addWidget( buttonOK ) 146 | 147 | self.resize( 5 * len( windowTitle ) + 200 , 30 ) 148 | 149 | if not self.data is None: 150 | setData() 151 | else: 152 | w = self.findChild( QtGui.QRadioButton, 'planet' ) 153 | w.setChecked( True ) 154 | d2 = QtCore.QDate.currentDate() 155 | d1 = d2.addMonths( -1 ) 156 | date1.setDate( d1 ) 157 | date2.setDate( d2 ) 158 | date1.setMaximumDate( d2.addDays( -1 ) ) 159 | date2.setMinimumDate( d1.addDays( +1 ) ) 160 | spinDay.setValue( d1.daysTo( d2) ) 161 | 162 | connect() 163 | 164 | super( DialogImageSettingPL, self ).__init__( parent ) 165 | self.data = data 166 | self.titleSelectDirectory = "Select download directory" 167 | self.titleClearCache = "Clear TMS cache (total {:0.2f}MB)" 168 | self.nameAssets = ('planet', 'rapideye', 'landsat8', 'sentinel2') 169 | initGui() 170 | 171 | def getData(self): 172 | return self.data 173 | 174 | def _saveDataSetting(self): 175 | # Next step add all informations 176 | #See __init__.initGui 177 | #keys = ['path', 'planet', 'rapideye', 'landsat8', 'sentinel2', 'udm', 'date1', 'date2'] 178 | 179 | keys = ['path' ] 180 | values = {} 181 | for k in keys: 182 | values[ k ] = "{0}/{1}".format( DialogImageSettingPL.localSetting, k ) 183 | s = QtCore.QSettings() 184 | for k in values.keys(): 185 | s.setValue( values[ k ], self.data[ k ] ) 186 | 187 | def _setSpinDay(self, date1, date2 ): 188 | spinDay = self.findChild( QtGui.QSpinBox, "sbDay" ) 189 | spinDay.valueChanged.disconnect( self.onValueChanged ) 190 | spinDay.setValue( date1.daysTo( date2) ) 191 | spinDay.valueChanged.connect( self.onValueChanged ) 192 | 193 | def _getDirsCacheTMS(self): 194 | w = self.findChild( QtGui.QPushButton, 'path' ) 195 | tmsDir = os.path.join( w.text(), 'tms' ) 196 | if not os.path.isdir( tmsDir ): 197 | return ( None, [] ) 198 | dirs = [os.path.join(tmsDir, d) for d in os.listdir(tmsDir) if os.path.isdir(os.path.join(tmsDir, d))] 199 | return ( tmsDir, dirs ) 200 | 201 | @staticmethod 202 | def getSettings(): 203 | # Next step add all informations 204 | #See __init__.initGui 205 | #keys = ['path', 'planet', 'rapideye', 'landsat8', 'sentinel2', 'date1', 'date2'] 206 | 207 | keys = ['path'] 208 | values = {} 209 | for k in keys: 210 | values[ k ] = "{0}/{1}".format( DialogImageSettingPL.localSetting, k ) 211 | data = None 212 | s = QtCore.QSettings() 213 | path = s.value( values['path'], None ) 214 | if not path is None: 215 | # Next step add all informations 216 | # planet = s.value( values['planet'], None ) 217 | # planet = True if planet == "true" else False 218 | # rapideye = s.value( values['rapideye'], None ) 219 | # rapideye = True if rapideye == "true" else False 220 | # ... 221 | # if QtCore.QDir( path ).exists(): 222 | # data = { 'isOk': True, 'path': path, 'planet': planet, 'rapideye': rapideye,... } 223 | 224 | if QtCore.QDir( path ).exists(): 225 | data = { 'isOk': True, 'path': path } 226 | else: 227 | data = { 'isOk': False, 'has_path': True, 'path': path } 228 | s.remove( values['path'] ) 229 | else: 230 | data = { 'isOk': False, 'has_path': False } 231 | 232 | return data 233 | 234 | @QtCore.pyqtSlot() 235 | def onOK(self): 236 | def getCurrentNameAsset(): 237 | for name in self.nameAssets: 238 | w = self.findChild( QtGui.QRadioButton, name ) 239 | if w.isChecked(): 240 | return name 241 | 242 | pb = self.findChild( QtGui.QPushButton, 'path' ) 243 | path = pb.text() 244 | if path == self.titleSelectDirectory: 245 | msg = "Directory '{0}'not found".format( self.titleSelectDirectory ) 246 | QtGui.QMessageBox.information( self, "Missing directory for download", msg ) 247 | return 248 | udm = self.findChild( QtGui.QCheckBox, 'udm' ) 249 | date1 = self.findChild( QtGui.QDateEdit, "deDate1" ) 250 | date2 = self.findChild( QtGui.QDateEdit, "deDate2" ) 251 | self.data = { 252 | 'path': path, 253 | 'current_asset': getCurrentNameAsset(), 254 | 'udm': udm.isChecked(), 255 | 'date1': date1.date(), 256 | 'date2': date2.date() 257 | } 258 | self._saveDataSetting() 259 | self.data['isOk'] = True 260 | self.accept() 261 | 262 | @QtCore.pyqtSlot() 263 | def onPath(self): 264 | w = self.findChild( QtGui.QPushButton, 'path' ) 265 | path = w.text() 266 | if path == self.titleSelectDirectory: 267 | path = None 268 | sdir = QtGui.QFileDialog.getExistingDirectory(self, self.titleSelectDirectory, path ) 269 | if len(sdir) > 0: 270 | w.setText( sdir ) 271 | 272 | @QtCore.pyqtSlot() 273 | def onClearCache(self): 274 | # Remove Directories 275 | ( path, dirs ) = self._getDirsCacheTMS() 276 | if path is None: 277 | return 278 | # XML files 279 | absdir = lambda d: os.path.join( path, d) 280 | [ os.remove( absdir( f ) ) for f in os.listdir( path ) if os.path.isfile( absdir( f ) ) ] 281 | # TMS directories 282 | for d in dirs: 283 | shutil.rmtree(d) 284 | title = self.titleClearCache.format(0) 285 | w = self.findChild( QtGui.QPushButton, 'clear_cache' ) 286 | w.setText( title ) 287 | w.setEnabled(False) 288 | 289 | @QtCore.pyqtSlot(QtCore.QDate) 290 | def onDateChanged1(self, date ): 291 | date2 = self.findChild( QtGui.QDateEdit, "deDate2" ) 292 | date2.setMinimumDate( date.addDays( +1 ) ) 293 | self._setSpinDay( date, date2.date() ) 294 | 295 | @QtCore.pyqtSlot(QtCore.QDate) 296 | def onDateChanged2(self, date ): 297 | date1 = self.findChild( QtGui.QDateEdit, "deDate1" ) 298 | date1.setMaximumDate( date.addDays( -1 ) ) 299 | self._setSpinDay( date1.date(), date ) 300 | 301 | @QtCore.pyqtSlot( int ) 302 | def onValueChanged(self, days ): 303 | date1 = self.findChild( QtGui.QDateEdit, "deDate1" ) 304 | date2 = self.findChild( QtGui.QDateEdit, "deDate2" ) 305 | newDate = date2.date().addDays( -1 * days ) 306 | date1.dateChanged.disconnect( self.onDateChanged1 ) 307 | date1.setDate( newDate ) 308 | date2.setMinimumDate( newDate.addDays( +1 ) ) 309 | date1.dateChanged.connect( self.onDateChanged1 ) 310 | 311 | class LegendCatalogLayer(): 312 | def __init__(self, labelMenu, slots, getTotalAssets): 313 | self.labelMenu, self.slots, self.getTotalAssets = labelMenu, slots, getTotalAssets 314 | self.legendInterface = QgsUtils.iface.legendInterface() 315 | self.legendMenuIDs = { 316 | 'calculate_status_assets': 'idCalculateStatusAssets', 317 | 'activate_assets': 'idActivateAssets', 318 | 'create_tms': 'idCreateTMS', 319 | 'download_images': 'idDownloadImages', 320 | 'download_thumbnails': 'idDownloadThumbnails' 321 | } 322 | self.legendLayer = self.layer = None 323 | self.statusEnableAssetsImage = { 324 | 'activate_assets': False, 325 | 'download_images': False 326 | } 327 | 328 | def _getPrefixs(self, totalAssets): 329 | totalFeats = self.layer.selectedFeatureCount() 330 | isSelect = totalFeats > 0 331 | prefix = "selected" if isSelect else "total" 332 | if not isSelect: 333 | totalFeats = self.layer.featureCount() 334 | 335 | prefixTotal = "{0} - {1}".format( totalFeats, prefix ) 336 | arg = ( totalAssets['analytic']['images'], totalAssets['udm']['images'], prefix ) 337 | prefixImages = "{0} analytic - {1} udm - {2}".format( *arg ) 338 | arg = ( totalAssets['analytic']['activate'], totalAssets['udm']['activate'], prefix ) 339 | prefixAssets = "{0} analytic - {1} udm - {2}".format( *arg ) 340 | 341 | return { 342 | 'total': prefixTotal, 343 | 'images': prefixImages, 344 | 'assets': prefixAssets 345 | } 346 | 347 | def clean(self): 348 | for item in self.legendLayer: 349 | self.legendInterface.removeLegendLayerAction( item['action'] ) 350 | 351 | def setLayer(self, layer): 352 | def addActionLegendLayer(): 353 | self.legendLayer = [ 354 | { 355 | 'menu': u"Create TMS", 356 | 'id': self.legendMenuIDs['create_tms'], 357 | 'slot': self.slots['create_tms'], 358 | 'action': None 359 | }, 360 | { 361 | 'id': 'idSeparator', 362 | 'action': None 363 | }, 364 | { 365 | 'menu': u"Calculate status assets", 366 | 'id': self.legendMenuIDs['calculate_status_assets'], 367 | 'slot': self.slots['calculate_status_assets'], 368 | 'action': None 369 | }, 370 | { 371 | 'menu': u"Activate assets", 372 | 'id': self.legendMenuIDs['activate_assets'], 373 | 'slot': self.slots['activate_assets'], 374 | 'action': None 375 | }, 376 | { 377 | 'id': 'idSeparator', 378 | 'action': None 379 | }, 380 | { 381 | 'menu': u"Download images", 382 | 'id': self.legendMenuIDs['download_images'], 383 | 'slot': self.slots['download_images'], 384 | 'action': None 385 | }, 386 | { 387 | 'menu': u"Download thumbnails", 388 | 'id': self.legendMenuIDs['download_thumbnails'], 389 | 'slot': self.slots['download_thumbnails'], 390 | 'action': None 391 | } 392 | ] 393 | prefixs = { 394 | 'total': "{0} - total".format( self.layer.featureCount() ), 395 | 'images': "0 analytic - 0 udm - total", 396 | 'assets': "0 analytic - 0 udm - total" 397 | } 398 | idsTotal = ( 399 | self.legendMenuIDs['calculate_status_assets'], 400 | self.legendMenuIDs['create_tms'], 401 | self.legendMenuIDs['download_thumbnails'] 402 | ) 403 | for item in self.legendLayer: 404 | if item['id'] == 'idSeparator': 405 | item['action'] = QtGui.QAction(None) 406 | item['action'].setSeparator(True) 407 | else: 408 | item['action'] = QtGui.QAction( item['menu'], self.legendInterface ) 409 | item['action'].triggered.connect( item['slot'] ) 410 | if item['id'] in idsTotal: 411 | lblAction = "{0}({1})".format( item['menu'], prefixs['total'] ) 412 | item['action'].setText( lblAction ) 413 | if item['id'] == self.legendMenuIDs['download_images']: 414 | lblAction = "{0}({1})".format( item['menu'], prefixs['images'] ) 415 | item['action'].setText( lblAction ) 416 | item['action'].setEnabled( False ) 417 | if item['id'] == self.legendMenuIDs['activate_assets']: 418 | lblAction = "{0}({1})".format( item['menu'], prefixs['assets'] ) 419 | item['action'].setText( lblAction ) 420 | item['action'].setEnabled( False ) 421 | arg = ( item['action'], self.labelMenu, item['id'], QgsCore.QgsMapLayer.VectorLayer, False ) 422 | self.legendInterface.addLegendLayerAction( *arg ) 423 | self.legendInterface.addLegendLayerActionForLayer( item['action'], self.layer ) 424 | 425 | self.layer = layer 426 | self.layer.selectionChanged.connect( self.selectionChanged ) 427 | addActionLegendLayer() 428 | 429 | def enabledProcessing(self, enabled=True): 430 | for item in self.legendLayer: 431 | item['action'].setEnabled( enabled ) 432 | 433 | if enabled: 434 | ids = ( self.legendMenuIDs['download_images'], self.legendMenuIDs['activate_assets'] ) 435 | c_ids, total_ids = 0, len( ids ) 436 | for item in self.legendLayer: 437 | if item['id'] == self.legendMenuIDs['download_images']: 438 | item['action'].setEnabled( self.statusEnableAssetsImage['download_images'] ) 439 | c_ids += 1 440 | if item['id'] == self.legendMenuIDs['activate_assets']: 441 | item['action'].setEnabled( self.statusEnableAssetsImage['activate_assets'] ) 442 | c_ids += 1 443 | if c_ids == total_ids: 444 | break 445 | 446 | def setAssetImages(self, totalAssets): 447 | enable = not ( totalAssets['analytic']['images'] + totalAssets['udm']['images'] == 0 ) 448 | self.statusEnableAssetsImage['download_images'] = enable 449 | enable = not ( totalAssets['analytic']['activate'] + totalAssets['udm']['activate'] == 0 ) 450 | self.statusEnableAssetsImage['activate_assets'] = enable 451 | prefixs = self._getPrefixs( totalAssets ) 452 | ids = ( self.legendMenuIDs['download_images'], self.legendMenuIDs['activate_assets'] ) 453 | c_ids, total_ids = 0, len( ids ) 454 | for item in self.legendLayer: 455 | if item['id'] == self.legendMenuIDs['download_images']: 456 | lblAction = "{0} ({1})".format( item['menu'], prefixs['images'] ) 457 | item['action'].setText( lblAction ) 458 | item['action'].setEnabled( self.statusEnableAssetsImage['download_images'] ) 459 | c_ids += 1 460 | if item['id'] == self.legendMenuIDs['activate_assets']: 461 | lblAction = "{0} ({1})".format( item['menu'], prefixs['assets'] ) 462 | item['action'].setText( lblAction ) 463 | item['action'].setEnabled( self.statusEnableAssetsImage['activate_assets'] ) 464 | c_ids += 1 465 | if c_ids == total_ids: 466 | break 467 | 468 | @QtCore.pyqtSlot() 469 | def selectionChanged(self): 470 | totalAssets = self.getTotalAssets() 471 | enable = not ( totalAssets['analytic']['images'] + totalAssets['udm']['images'] == 0 ) 472 | self.statusEnableAssetsImage['download_images'] = enable 473 | enable = not ( totalAssets['analytic']['activate'] + totalAssets['udm']['activate'] == 0 ) 474 | self.statusEnableAssetsImage['activate_assets'] = enable 475 | prefixs = self._getPrefixs( totalAssets ) 476 | idsTotal = ( 477 | self.legendMenuIDs['calculate_status_assets'], 478 | self.legendMenuIDs['create_tms'], 479 | self.legendMenuIDs['download_thumbnails'] 480 | ) 481 | for item in self.legendLayer: 482 | if item['id'] in idsTotal: 483 | lblAction = "{0} ({1})".format( item['menu'], prefixs['total'] ) 484 | item['action'].setText( lblAction ) 485 | if item['id'] == self.legendMenuIDs['download_images']: 486 | lblAction = "{0} ({1})".format( item['menu'], prefixs['images'] ) 487 | item['action'].setText( lblAction ) 488 | item['action'].setEnabled( self.statusEnableAssetsImage['download_images'] ) 489 | if item['id'] == self.legendMenuIDs['activate_assets']: 490 | lblAction = "{0} ({1})".format( item['menu'], prefixs['assets'] ) 491 | item['action'].setText( lblAction ) 492 | item['action'].setEnabled( self.statusEnableAssetsImage['activate_assets'] ) 493 | 494 | -------------------------------------------------------------------------------- /managerloginkey.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | Name : Manager Login 5 | Description : Manager login in server 6 | Date : April, 2015 7 | copyright : (C) 2015 by Luiz Motta 8 | email : motta.luiz@gmail.com 9 | 10 | ***************************************************************************/ 11 | 12 | /*************************************************************************** 13 | * * 14 | * This program is free software; you can redistribute it and/or modify * 15 | * it under the terms of the GNU General Public License as published by * 16 | * the Free Software Foundation; either version 2 of the License, or * 17 | * (at your option) any later version. * 18 | * * 19 | ***************************************************************************/ 20 | """ 21 | 22 | from PyQt4 import QtCore, QtGui 23 | 24 | from apiqtpl import API_PlanetLabs 25 | 26 | class DialogLoginKey(QtGui.QDialog): 27 | 28 | def __init__(self, parent, windowTitle, icon=None): 29 | def initGui(): 30 | def connect(): 31 | buttonLogin.clicked.connect( self.onLogin ) 32 | self.textKey.textEdited.connect( self.onTextEdited ) 33 | # 34 | self.setWindowTitle( windowTitle ) 35 | if not icon is None: 36 | self.setWindowIcon( icon ) 37 | labelKey = QtGui.QLabel( "Key: ", self ) 38 | self.labelError = QtGui.QLabel( self ) 39 | self.labelError.hide() 40 | self.textKey = QtGui.QLineEdit( self ) 41 | self.textKey.setEchoMode( QtGui.QLineEdit.Password ) 42 | buttonLogin = QtGui.QPushButton( "Login", self ) 43 | connect() 44 | layout = QtGui.QVBoxLayout( self ) 45 | layout.addWidget( labelKey ) 46 | layout.addWidget( self.textKey ) 47 | layout.addWidget( buttonLogin ) 48 | layout.addWidget( self.labelError ) 49 | # 50 | self.resize( 4 * len( windowTitle ) + 200 , 30 ) 51 | # 52 | super( DialogLoginKey, self ).__init__( parent ) 53 | self.apiPL = API_PlanetLabs() 54 | self.responsePL = None 55 | initGui() 56 | 57 | @QtCore.pyqtSlot( bool ) 58 | def onLogin(self, checked): 59 | def setFinishedPL(response): 60 | self.responsePL = response 61 | loop.quit() 62 | 63 | def setKeyResponse(): 64 | if self.responsePL['isOk']: 65 | self.accept() 66 | else: 67 | self.labelError.setTextFormat( QtCore.Qt.RichText ) 68 | msg = "Invalid key! %s" % self.responsePL['message'] 69 | self.labelError.setText( msg ) 70 | self.labelError.show() 71 | 72 | key = self.textKey.text().encode('ascii', 'ignore') 73 | self.responsePL = None 74 | loop = QtCore.QEventLoop() 75 | self.apiPL.setKey( key, setFinishedPL ) 76 | loop.exec_() 77 | setKeyResponse() 78 | 79 | @QtCore.pyqtSlot( str ) 80 | def onTextEdited(self, text ): 81 | if self.labelError.isHidden(): 82 | return 83 | self.labelError.hide() 84 | 85 | 86 | class ManagerLoginKey(QtCore.QObject): 87 | 88 | def __init__(self, localSetting): 89 | super(ManagerLoginKey, self).__init__() 90 | self.localSettingKey = "%s/key" % localSetting # ~/.config/QGIS/QGIS2.conf 91 | 92 | def dialogLogin(self, dataDlg, dataMsgBox, setResult): 93 | 94 | def saveKeyDlg(): 95 | reply = QtGui.QMessageBox.question( dlg, dataMsgBox['title'], dataMsgBox['msg'], QtGui.QMessageBox.Yes | QtGui.QMessageBox.No) 96 | if reply == QtGui.QMessageBox.Yes: 97 | s = QtCore.QSettings() 98 | s.setValue( self.localSettingKey, API_PlanetLabs.validKey ) 99 | 100 | @QtCore.pyqtSlot( int ) 101 | def finished(result): 102 | isOk = result == QtGui.QDialog.Accepted 103 | if isOk: 104 | saveKeyDlg() 105 | setResult( isOk ) 106 | 107 | dlg = DialogLoginKey( dataDlg['parent'] , dataDlg['windowTitle'], dataDlg['icon'] ) 108 | dlg.finished.connect( finished ) 109 | dlg.exec_() 110 | 111 | def getKeySetting(self): 112 | s = QtCore.QSettings() 113 | return s.value( self.localSettingKey, None ) 114 | 115 | def removeKey(self): 116 | s = QtCore.QSettings() 117 | s.remove( self.localSettingKey ) 118 | -------------------------------------------------------------------------------- /messagebarcancel.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | Name : MessageBar Cancel 5 | Description : Use to add cancel in messagebar 6 | Date : November, 2017 7 | copyright : (C) 2017 by Luiz Motta 8 | email : motta.luiz@gmail.com 9 | 10 | ***************************************************************************/ 11 | 12 | /*************************************************************************** 13 | * * 14 | * This program is free software; you can redistribute it and/or modify * 15 | * it under the terms of the GNU General Public License as published by * 16 | * the Free Software Foundation; either version 2 of the License, or * 17 | * (at your option) any later version. * 18 | * * 19 | ***************************************************************************/ 20 | """ 21 | import os 22 | 23 | from PyQt4 import QtCore, QtGui 24 | from qgis import core as QgsCore, gui as QgsGui 25 | 26 | 27 | class MessageBarCancelProgress(QtCore.QObject): 28 | def __init__(self, pluginName, msgBar, msg, maximum, funcKill, hasProgressFile=False): 29 | def initGui(): 30 | self.pb = QtGui.QProgressBar( self.msgBar ) 31 | self.pb.setAlignment( QtCore.Qt.AlignLeft ) 32 | self.lb = QtGui.QLabel( self.msgBar ) 33 | self.tbCancel = QtGui.QToolButton( self.msgBar ) 34 | self.tbCancel.setIcon( QgsCore.QgsApplication.getThemeIcon( "/mActionCancelAllEdits.svg" ) ) 35 | self.tbCancel.setToolTip( "Cancel download") 36 | self.tbCancel.setText( "Cancel") 37 | self.tbCancel.setToolButtonStyle( QtCore.Qt.ToolButtonTextBesideIcon ) 38 | self.widget = self.msgBar.createMessage( pluginName, msg ) 39 | 40 | widgets = [ self.tbCancel, self.lb, self.pb ] 41 | lyt = self.widget.layout() 42 | for item in widgets: 43 | lyt.addWidget( item ) 44 | del widgets[:] 45 | 46 | if hasProgressFile: 47 | self.pbFile = QtGui.QProgressBar( self.msgBar ) 48 | self.pbFile.setAlignment( QtCore.Qt.AlignLeft ) 49 | self.pbFile.setValue( 1 ) 50 | lyt.addWidget( self.pbFile ) 51 | 52 | super(MessageBarCancelProgress, self).__init__() 53 | ( self.msgBar, self.maximum ) = ( msgBar, maximum ) 54 | self.pb = self.lb = self.widget = self.isCancel = self.pbFile = None 55 | initGui() 56 | self.tbCancel.clicked.connect( self.clickedCancel ) 57 | self.pb.destroyed.connect( self.destroyed) 58 | 59 | self.msgBar.pushWidget( self.widget, QgsGui.QgsMessageBar.INFO ) 60 | self.pb.setValue( 1 ) 61 | self.pb.setMaximum( maximum ) 62 | self.isCancel = False 63 | self.kill = funcKill 64 | 65 | def step(self, value, image=None): 66 | if self.pb is None: 67 | return 68 | 69 | self.pb.setValue( value ) 70 | self.lb.setText( "%d/%d" % ( value, self.maximum ) ) 71 | if not image is None: 72 | self.pbFile.setToolTip( image ) 73 | self.pbFile.setFormat( "%p% " + os.path.split( image )[-1] ) 74 | 75 | @QtCore.pyqtSlot(QtCore.QObject) 76 | def destroyed(self, obj): 77 | self.pb = None 78 | 79 | @QtCore.pyqtSlot(bool) 80 | def clickedCancel(self, checked): 81 | if self.pb is None: 82 | return 83 | self.kill() 84 | self.isCancel = True 85 | 86 | @QtCore.pyqtSlot(int, int) 87 | def stepFile(self, bytesReceived, bytesTotal): 88 | if self.pb is None: 89 | return 90 | 91 | self.pbFile.setMaximum( bytesTotal ) 92 | self.pbFile.setValue( bytesReceived ) 93 | 94 | 95 | class MessageBarCancel(QtCore.QObject): 96 | def __init__(self, pluginName, msgBar, msg, funcKill): 97 | def initGui(): 98 | self.tbCancel = QtGui.QToolButton( msgBar ) 99 | self.tbCancel.setIcon( QgsCore.QgsApplication.getThemeIcon( '/mActionCancelAllEdits.svg' ) ) 100 | self.tbCancel.setText( "Cancel") 101 | self.tbCancel.setToolButtonStyle( QtCore.Qt.ToolButtonTextBesideIcon ) 102 | self.widget = msgBar.createMessage( pluginName, msg ) 103 | 104 | lyt = self.widget.layout() 105 | lyt.addWidget( self.tbCancel ) 106 | 107 | super(MessageBarCancel, self).__init__() 108 | self.widget = self.isCancel = None 109 | initGui() 110 | self.tbCancel.clicked.connect( self.clickedCancel ) 111 | 112 | msgBar.pushWidget( self.widget, QgsGui.QgsMessageBar.INFO ) 113 | self.isCancel = False 114 | self.kill = funcKill 115 | 116 | def message(self, msg): 117 | if not self.isCancel: 118 | self.widget.setText( msg ) 119 | 120 | @QtCore.pyqtSlot(bool) 121 | def clickedCancel(self, checked): 122 | self.kill() 123 | self.isCancel = True 124 | -------------------------------------------------------------------------------- /metadata.txt: -------------------------------------------------------------------------------- 1 | [general] 2 | name=Catalog Planet Labs 3 | description=This plugin lets you get images from the Planet Labs API(Version 1). 4 | about=This plugin lets you get images from the Planet Labs API(Version 1), by performing searches for images that intersect with the extent of the map window. 5 | It is a product from Planet Explorers program (https://www.planet.com/explorers/), it is not an official Planet Labs's plugin. 6 | You need a key from Planet Labs in order to use this plugin. 7 | This plugin will be create polygon layer (Catalog of images) from intersect with the extent of the map window. 8 | With this plugin, you can download full images(Analytic, UDM, ...), thumbnail images or 9 | add TMS images. 10 | See presentation: https://www.slideshare.net/LuizMotta3/catalog-planet-labs-plugin-for-qgis-v1 11 | Tested with QGIS 2.18.13 12 | version=1.7.1 13 | qgisMinimumVersion=2.18 14 | category=Raster 15 | author=Luiz Motta 16 | email=motta.luiz@gmail.com 17 | changelog=1.7.1 18 | Fixed error ltgRoot in downloadImages(catalogpl.py). 19 | Thanks for Dr. Qiusheng Wu. 20 | 1.7 21 | Change menus, catalog group, scene layer, count total dates. Fixed use in Windows 22 | 1.6 23 | Add groups of images with same date, add menu Open Form in images, clear cache of TMS, and add action 'Add selection' to pl_scenes. 24 | 1.5 25 | Reverse images order in group catalog, add directories, 26 | tms, tif, thumbnail, inside download directory 27 | 1.4 28 | Fix cache_path when save TMS 29 | 1.3 30 | Change TMS of Tile Server XYZ to GDAL_WMS 31 | 1.2 32 | Fixed when the first use of plugin(not set the directory), not show the setting 33 | 1.1 34 | - Changed checkbox to radiobutton for images, only one type of image for searching. 35 | 1.0 36 | - Updated to API Planet V1 37 | 0.8 38 | - Fixed message when error download and add message log for errors 39 | 0.7 40 | - Add context menu remove key 41 | 0.6 42 | - Add context menu image full and TMS 43 | 0.5 44 | - Update the checkLayerLegend(), remove clean register 45 | 0.4 46 | - Add cancel for TMS for download 47 | 0.3 48 | - Add TMS for download 49 | 0.2 50 | - Add feature for download images and thumbnails 51 | - Add metadata in table and refactoring codes. 52 | 0.1 53 | - Start of plugin 54 | 55 | tags=catalog,raster, satellite, Planet Labs 56 | homepage=https://github.com/lmotta/catalogpl_plugin 57 | tracker=https://github.com/lmotta/catalogpl_plugin 58 | repository=https://github.com/lmotta/catalogpl_plugin 59 | icon=catalogpl.svg 60 | experimental=True 61 | deprecated=False 62 | -------------------------------------------------------------------------------- /pl_expressions.py: -------------------------------------------------------------------------------- 1 | """ 2 | /*************************************************************************** 3 | Name : PlanetLabs expressions 4 | Description : Set of expressions for QGIS ( 2.8 or above ) 5 | Date : April, 2015. 6 | copyright : (C) 2015 by Luiz Motta 7 | email : motta.luiz@gmail.com 8 | 9 | ***************************************************************************/ 10 | 11 | /*************************************************************************** 12 | * * 13 | * This program is free software; you can redistribute it and/or modify * 14 | * it under the terms of the GNU General Public License as published by * 15 | * the Free Software Foundation; either version 2 of the License, or * 16 | * (at your option) any later version. * 17 | * * 18 | ***************************************************************************/ 19 | """ 20 | 21 | from qgis.core import ( qgsfunction ) 22 | from catalogpl_plugin import API_PlanetLabs 23 | 24 | @qgsfunction(args=1, group='Planet Labs') 25 | def getValueFromMetadata(values, feature, parent): 26 | """ 27 |

Return

Get value of key of 'meta_json' field 28 |

Syntax

getValueFromMetadata('list_keys')

29 |

Argument

list_keys -> String with a sequence of keys names - '"key1","key2",...'

30 |

Example

getValueFromMetadata( '"item_type"' )

Return: Item type

31 | """ 32 | if values[0].count('"') % 2 != 0: 33 | raise Exception("Catalog Planet: Error! Key need double quotes: %s." % values[0] ) 34 | 35 | if len( values[0] ) < 1: 36 | raise Exception("Catalog Planet: Error! Field is empty." ) 37 | 38 | 39 | name_metadata_json = 'meta_json' 40 | id_metadata_json = feature.fieldNameIndex( name_metadata_json ) 41 | if id_metadata_json == -1: 42 | raise Exception("Catalog Planet: Error! Need have '%s' field." % name_metadata_json ) 43 | 44 | lstKey = map( lambda item: item.strip(), values[ 0 ].split(",") ) 45 | lstKey = map( lambda item: item.strip('"'), lstKey ) 46 | 47 | metadata_json = feature.attributes()[ id_metadata_json ] 48 | try: 49 | ( success, valueKey) = API_PlanetLabs.getValue( metadata_json, lstKey ) 50 | if not success: 51 | raise Exception( valueKey ) 52 | except Exception as e: 53 | raise Exception( e.message ) 54 | 55 | return valueKey 56 | 57 | @qgsfunction(args=0, group='Planet Labs') 58 | def getLocationAnalytic(values, feature, parent): 59 | """ 60 |

Return

Get value of location of 'meta_json' field 61 |

Syntax

getLocation()

62 |

Argument

None

63 |

Example

getLocation()

Return: Value of location

64 | """ 65 | 66 | name_metadata_json = 'meta_json' 67 | id_metadata_json = feature.fieldNameIndex( name_metadata_json ) 68 | if id_metadata_json == -1: 69 | raise Exception("Catalog Planet: Error! Need have '%s' field." % name_metadata_json ) 70 | 71 | metadata_json = feature.attributes()[ id_metadata_json ] 72 | lstKey = ['assets_status','a_analytic','location'] 73 | try: 74 | ( success, valueKey) = API_PlanetLabs.getValue( metadata_json, lstKey ) 75 | if not success: 76 | return "'location' not found" 77 | except Exception as e: 78 | pass 79 | 80 | return valueKey 81 | 82 | @qgsfunction(args=0, group='Planet Labs') 83 | def getLocationUDM(values, feature, parent): 84 | """ 85 |

Return

Get value of location of 'meta_json' field 86 |

Syntax

getLocation()

87 |

Argument

None

88 |

Example

getLocation()

Return: Value of location

89 | """ 90 | 91 | name_metadata_json = 'meta_json' 92 | id_metadata_json = feature.fieldNameIndex( name_metadata_json ) 93 | if id_metadata_json == -1: 94 | raise Exception("Catalog Planet: Error! Need have '%s' field." % name_metadata_json ) 95 | 96 | metadata_json = feature.attributes()[ id_metadata_json ] 97 | lstKey = ['assets_status','a_udm','location'] 98 | try: 99 | ( success, valueKey) = API_PlanetLabs.getValue( metadata_json, lstKey ) 100 | if not success: 101 | return "'location' not found" 102 | except Exception as e: 103 | pass 104 | 105 | return valueKey 106 | -------------------------------------------------------------------------------- /pl_scenes.qml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 0 212 | 0 213 | 70 214 | id 215 | 216 | 217 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | . 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 291 | 292 | . 293 | 294 | 0 295 | . 296 | 313 | 0 314 | tablayout 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | COALESCE( "id", '<NULL>' ) 339 | 2 340 | 341 | -------------------------------------------------------------------------------- /workertms.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | /*************************************************************************** 4 | Name : Worker TMS 5 | Description : Use to create TMS 6 | Date : November, 2017 7 | copyright : (C) 2017 by Luiz Motta 8 | email : motta.luiz@gmail.com 9 | 10 | ***************************************************************************/ 11 | 12 | /*************************************************************************** 13 | * * 14 | * This program is free software; you can redistribute it and/or modify * 15 | * it under the terms of the GNU General Public License as published by * 16 | * the Free Software Foundation; either version 2 of the License, or * 17 | * (at your option) any later version. * 18 | * * 19 | ***************************************************************************/ 20 | """ 21 | import os 22 | 23 | from PyQt4 import QtCore 24 | 25 | from qgis import core as QgsCore 26 | 27 | 28 | class WorkerCreateTMS_GDAL_WMS(QtCore.QObject): 29 | finished = QtCore.pyqtSignal( dict ) 30 | stepProgress = QtCore.pyqtSignal( int ) 31 | 32 | def __init__(self, logMessage, legendRasterGeom): 33 | super(WorkerCreateTMS_GDAL_WMS, self).__init__() 34 | self.logMessage, self.legendRasterGeom = logMessage, legendRasterGeom 35 | self.ltgRoot = QgsCore.QgsProject.instance().layerTreeRoot() 36 | self.isKilled = None # set in run 37 | self.ltgCatalog = None # setting 38 | 39 | def setting(self, data): 40 | self.id_table = data['id_layer'] 41 | self.path = data['path'] 42 | self.ct = data['ctTMS'] # QgsCoordinateTransform 43 | self.iterFeat = data['iterFeat'] # feat['id'], feat['acquired'], feat['meta_json'] 44 | self.ltgCatalog = data['ltgCatalog'] 45 | self.pluginName = data['pluginName'] 46 | key = 'user_pwd' # { 'user', 'pwd' } 47 | self.user_pwd = None if not data.has_key( key ) else data[ key ] 48 | key = 'rgb' # [ 'r', 'g', 'b' ] 49 | self.rgb = None if not data.has_key( key ) else data[ key ] 50 | self.getURL = data['getURL'] # ( feat, self.rgb ) 51 | 52 | @QtCore.pyqtSlot() 53 | def run(self): 54 | def saveTMS(feat, fileName): 55 | def contentTMS(): 56 | def contenTargetWindow(): 57 | r = self.ct.transform( feat.geometry().boundingBox() ) 58 | targetWindow = { 'ulX': r.xMinimum(), 'ulY': r.yMaximum(), 'lrX': r.xMaximum(), 'lrY': r.yMinimum() } 59 | l = [ 60 | ' ', 61 | ' {}', 62 | ' {}', 63 | ' {}', 64 | ' {}', 65 | ' ' 66 | ] 67 | arg = ( targetWindow['ulX'], targetWindow['ulY'], targetWindow['lrX'], targetWindow['lrY'] ) 68 | return '\n'.join(l).format( *arg ) 69 | 70 | def getCacheName(): 71 | dirname = os.path.dirname( fileName ) 72 | name = os.path.splitext( os.path.basename( fileName ) )[0] 73 | name = "cache_{}".format( name ) 74 | 75 | server_url = self.getURL( feat, self.rgb ) 76 | # Change for GDAL_WMS 77 | for c in ['{z}', '{x}', '{y}']: 78 | server_url = server_url.replace( c, "${}".format( c ) ) 79 | path, name = os.path.split(fileName) 80 | name = "cache_{}".format( os.path.splitext( name )[0] ) 81 | cache_path = os.path.join( path, name ) 82 | l = [ 83 | "", 84 | "".format( self.pluginName ), 85 | ' ', 86 | " {}".format( server_url ), 87 | " TRUE", 88 | " EPSG:3857", 89 | " image/png", 90 | " ", 91 | " ", 92 | " -20037508.34", 93 | " 20037508.34", 94 | " 20037508.34", 95 | " -20037508.34", 96 | " 15", 97 | " 1", 98 | " 1", 99 | " top", 100 | " ", 101 | "{}".format( contenTargetWindow() ), 102 | " EPSG:3857", 103 | " 256", 104 | " 256", 105 | " 4", 106 | " byte", 107 | " Mozilla/5.0", 108 | " true", 109 | " 204,303,400,404,500,501", 110 | " true", 111 | " 5", 112 | " ", 113 | " {}".format( cache_path ), 114 | " ", 115 | "" 116 | ] 117 | if not self.user_pwd is None: 118 | arg = ( self.user_pwd['user'], self.user_pwd['pwd'] ) 119 | user_pwd = " {}:{}".format( *arg ) 120 | l.insert( len(l)-1, user_pwd ) 121 | return '\n'.join( l ) 122 | 123 | fileDownload = QtCore.QFile( fileName ) 124 | fileDownload.open( QtCore.QIODevice.WriteOnly ) 125 | fileDownload.write( contentTMS() ) 126 | fileDownload.close() 127 | 128 | def addTMS(): 129 | geomTransf = QgsCore.QgsGeometry(feat.geometry() ) 130 | geomTransf.transform( self.ct ) 131 | wkt_geom = geomTransf.exportToWkt() 132 | layer = QgsCore.QgsRasterLayer( image, os.path.split( image )[-1] ) 133 | layer.setCustomProperty( 'wkt_geom', wkt_geom ) 134 | layer.setCustomProperty( 'date', feat['acquired'] ) 135 | layer.setCustomProperty( 'id_table', self.id_table ) 136 | layer.setCustomProperty( 'id_image', feat['id'] ) 137 | QgsCore.QgsMapLayerRegistry.instance().addMapLayer( layer, addToLegend=False ) 138 | self.ltgCatalog.addLayer( layer).setVisible( QtCore.Qt.Unchecked ) 139 | self.legendRasterGeom.setLayer( layer ) 140 | 141 | mlr = QgsCore.QgsMapLayerRegistry.instance() 142 | self.isKilled = False 143 | formatImage = u"{}_tms.xml" 144 | if not self.rgb is None: 145 | formatImage = u"{{}}_{}_tms.xml".format( '-'.join( self.rgb ) ) 146 | step = totalError = 0 147 | for feat in self.iterFeat: 148 | step += 1 149 | self.stepProgress.emit( step ) 150 | if self.isKilled: 151 | self.iterFeat.close() 152 | break 153 | image = os.path.join( self.path, formatImage.format( feat['id'] ) ) 154 | if not QtCore.QFile.exists( image ): 155 | saveTMS( feat, image ) 156 | addTMS() 157 | 158 | message = { 'totalError': totalError } 159 | self.finished.emit( message ) 160 | 161 | def kill(self): 162 | self.isKilled = True 163 | 164 | 165 | class WorkerCreateTMS_ServerXYZ(QtCore.QObject): # Obsolete, need changes! 166 | finished = QtCore.pyqtSignal( dict ) 167 | stepProgress = QtCore.pyqtSignal( int ) 168 | 169 | def __init__(self, logMessage, legendRasterGeom ): 170 | super(WorkerCreateTMS_ServerXYZ, self).__init__() 171 | self.logMessage, self.legendRasterGeom = logMessage, legendRasterGeom 172 | self.isKilled = None # set in run 173 | self.iterFeat = self.ltgRoot = self.ltgCatalog = self.msgDownload = None # setting 174 | 175 | def setting(self, iterFeat, ltgRoot, ltgCatalog): 176 | self.iterFeat, self.ltgRoot, self.ltgCatalog = iterFeat, ltgRoot, ltgCatalog 177 | 178 | @QtCore.pyqtSlot() 179 | def run(self): 180 | def addTMS(): 181 | server_url = API_PlanetLabs.urlTMS.format( item_type=item_type, item_id=item_id ) 182 | urlkey = "{0}?api_key={1}".format( server_url, user_pwd ) 183 | uri.setParam('url', urlkey ) 184 | lyr = QgsCore.QgsRasterLayer( str( uri.encodedUri() ), item_id , 'wms') 185 | if not lyr.isValid(): 186 | msg = "Error create TMS from {0}: Invalid layer".format( item_id ) 187 | self.logMessage( msg, CatalogPL.pluginName, QgsCore.QgsMessageLog.CRITICAL ) 188 | totalError += 1 189 | return 190 | if not lyr.source() in sources_catalog_group: 191 | lyr.setCustomProperty( 'wkt_geom', wkt_geom ) 192 | mlr.addMapLayer( lyr, addToLegend=False ) 193 | self.ltgCatalog.addLayer( lyr ).setVisible( QtCore.Qt.Unchecked ) 194 | self.legendRasterGeom.setLayer( lyr ) 195 | 196 | mlr = QgsCore.QgsMapLayerRegistry.instance() 197 | user_pwd = API_PlanetLabs.validKey 198 | uri = QgsCore.QgsDataSourceURI() 199 | uri.setParam('type', 'xyz' ) 200 | sources_catalog_group = map( lambda item: item.layer().source(), self.ltgRoot.findLayers() ) 201 | 202 | self.isKilled = False 203 | step = totalError = 0 204 | for feat in self.iterFeat: 205 | step += 1 206 | self.stepProgress.emit( step ) 207 | if self.isKilled: 208 | self.iterFeat.close() 209 | break 210 | item_id = feat['id'] 211 | ( ok, item_type ) = API_PlanetLabs.getValue( feat['meta_json'], [ 'item_type' ] ) 212 | if not ok: 213 | msg = "Error create TMS from {0}: {1}".format( item_id, item_type) 214 | self.logMessage( msg, CatalogPL.pluginName, QgsCore.QgsMessageLog.CRITICAL ) 215 | totalError += 1 216 | continue 217 | wkt_geom = feat.geometry().exportToWkt() 218 | addTMS() 219 | uri.removeParam('url') 220 | 221 | message = { 'totalError': totalError } 222 | self.finished.emit( message ) 223 | 224 | def kill(self): 225 | self.isKilled = True 226 | --------------------------------------------------------------------------------