├── .docker └── Dockerfile ├── .github └── workflows │ └── main.yml ├── .gitignore ├── LoadThemAll ├── LICENSE ├── LoadThemAll.py ├── Makefile ├── __init__.py ├── core │ ├── Enums.py │ ├── FileFormatConfiguration.py │ ├── Filter.py │ ├── LayerTypes.py │ ├── LoadConfiguration.py │ ├── LoadFiles.py │ ├── LoadThemAllResult.py │ ├── QGISLayerTree.py │ ├── Utils.py │ └── __init__.py ├── gui │ ├── BaseLoadThemAllDialog.py │ ├── LoadThemAllDialog.py │ └── __init__.py ├── i18n │ ├── loadthemall.pro │ ├── loadthemall_es.qm │ ├── loadthemall_es.ts │ ├── loadthemall_fr.qm │ └── loadthemall_fr.ts ├── metadata.txt ├── resources │ ├── __init__.py │ ├── dir.png │ ├── icon.png │ ├── logo_80x94.png │ ├── resources.qrc │ └── resources_rc.py └── ui │ ├── Ui_Base_LoadThemAll.py │ ├── Ui_Base_LoadThemAll.ui │ ├── Ui_DockWidget.py │ ├── Ui_DockWidget.ui │ └── __init__.py ├── README.md ├── changelog.txt ├── requirements.txt ├── run-docker-tests.sh └── tests ├── test_compressed_layers.py ├── test_core_utils.py ├── test_load_point_clouds.py ├── test_load_rasters.py ├── test_load_vectors.py ├── test_plugin_load.py └── utils.py /.docker/Dockerfile: -------------------------------------------------------------------------------- 1 | ARG QGIS_TEST_VERSION 2 | FROM qgis/qgis:${QGIS_TEST_VERSION} 3 | MAINTAINER Matthias Kuhn 4 | 5 | RUN apt-get update && \ 6 | apt-get -y install \ 7 | python3-pip \ 8 | && rm -rf /var/lib/apt/lists/* 9 | 10 | COPY ./requirements.txt /tmp/ 11 | RUN pip3 install -r /tmp/requirements.txt 12 | 13 | COPY ./ /usr/src/ 14 | 15 | RUN chmod a+x /usr/src/run-docker-tests.sh 16 | 17 | ENTRYPOINT ["/usr/src/run-docker-tests.sh"] 18 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Continuous integration 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | - release-** 8 | pull_request: 9 | branches: 10 | - "**" 11 | release: 12 | types: ["prereleased", "released"] 13 | 14 | jobs: 15 | # Run unit tests 16 | test: 17 | runs-on: ubuntu-22.04 18 | strategy: 19 | matrix: 20 | qgis_version: [final-3_28_13] # We could eventually add more, e.g., latest 21 | env: 22 | QGIS_TEST_VERSION: ${{ matrix.qgis_version }} 23 | steps: 24 | - name: Checkout 25 | uses: actions/checkout@v2 26 | with: 27 | submodules: recursive 28 | - name: Build docker image 29 | run: docker build . -f .docker/Dockerfile -t docker_qgis --build-arg QGIS_TEST_VERSION=${QGIS_TEST_VERSION} 30 | - name: Test on QGIS 31 | run: docker run --rm -v ${GITHUB_WORKSPACE}:/usr/src docker_qgis 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | i18n/loadthemall_es.qm 3 | i18n/loadthemall_fr.qm 4 | ui/Ui_DockWidget.py 5 | ui/Ui_Base_LoadThemAll.py 6 | resources/resources_rc.py 7 | __pycache__/ 8 | -------------------------------------------------------------------------------- /LoadThemAll/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 | -------------------------------------------------------------------------------- /LoadThemAll/LoadThemAll.py: -------------------------------------------------------------------------------- 1 | """ 2 | /*************************************************************************** 3 | LoadThemAll 4 | A QGIS plugin 5 | Loads files stored in a directory structure recursively, based on several filters 6 | ------------------- 7 | begin : 2010-10-03 8 | copyright : (C) 2010 by Germán Carrillo (GeoTux) 9 | email : gcarrillo@linuxmail.org 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 qgis.PyQt.QtGui import QIcon 24 | from qgis.PyQt.QtWidgets import QAction 25 | from qgis.core import (QgsApplication, 26 | Qgis) 27 | from qgis.PyQt.QtCore import (QCoreApplication, 28 | QFileInfo, 29 | QSettings, 30 | Qt, 31 | QTranslator) 32 | 33 | from .resources.resources_rc import * 34 | from .gui.LoadThemAllDialog import LoadThemAllDialog 35 | 36 | 37 | class LoadThemAll: 38 | def __init__(self, iface, with_gui=True): 39 | # Save reference to the QGIS interface 40 | self.iface = iface 41 | self.__with_gui = with_gui 42 | 43 | self.__default_action_location = True # Default location: Plugin menu and plugin toolbar 44 | self.installTranslator() 45 | 46 | def initGui(self): 47 | # Create action that will start plugin configuration 48 | self.action = QAction(QIcon(":/plugins/loadthemall/icon.png"), "Load them all...", self.iface.mainWindow()) 49 | # connect the action to the run method 50 | self.action.triggered.connect(self.run) 51 | 52 | # Add toolbar button and menu item 53 | dsm = self.iface.dataSourceManagerToolBar() 54 | if len(dsm.actions()) > 1: 55 | before_action = dsm.actions()[1] # Get the action after "Data source manager", which should be the 1st one 56 | dsm.insertAction(before_action, self.action) # Add to toolbar 57 | self.iface.addLayerMenu().addAction(self.action) # Add to menu (difficult to find) 58 | self.iface.addPluginToMenu("&Load them all", self.action) # Add to plugins menu (easy to find) 59 | 60 | self.__default_action_location = False 61 | else: # Fallback if somehow the data source manager toolbar is not able to get LTA's main action 62 | self.iface.addToolBarIcon(self.action) 63 | self.iface.addPluginToMenu("&Load them all", self.action) 64 | 65 | self.dockWidget = LoadThemAllDialog(self.iface.mainWindow(), self.iface) 66 | 67 | def unload(self): 68 | if self.__with_gui: 69 | # Remove the plugin menu item and icon 70 | if not self.__default_action_location: 71 | self.iface.dataSourceManagerToolBar().removeAction(self.action) 72 | self.iface.addLayerMenu().removeAction(self.action) 73 | self.iface.removePluginMenu("&Load them all", self.action) 74 | else: 75 | self.iface.removePluginMenu("&Load them all", self.action) 76 | self.iface.removeToolBarIcon(self.action) 77 | 78 | self.dockWidget.close() 79 | self.iface.removeDockWidget(self.dockWidget) 80 | 81 | # run method that performs all the real work 82 | def run(self): 83 | if Qgis.QGIS_VERSION_INT >= 31300: # Use native addTabifiedDockWidget 84 | self.iface.addTabifiedDockWidget(Qt.RightDockWidgetArea, self.dockWidget, raiseTab=True) 85 | else: 86 | self.iface.addDockWidget(Qt.RightDockWidgetArea, self.dockWidget) 87 | 88 | def installTranslator(self): 89 | userPluginPath = os.path.join(os.path.dirname(str(QgsApplication.qgisUserDatabaseFilePath())), "python/plugins/loadthemall/i18n") 90 | systemPluginPath = os.path.join(str(QgsApplication.prefixPath()), "python/plugins/loadthemall/i18n") 91 | translationPath = '' 92 | 93 | try: 94 | # Errors here could happen if the value cannot be converted to string or 95 | # if it is not subscriptable (see https://github.com/gacarrillor/loadthemall/issues/11) 96 | locale = QSettings().value("locale/userLocale", type=str) 97 | myLocale = str(locale[0:2]) 98 | except TypeError as e: 99 | myLocale = 'en' 100 | 101 | if os.path.exists(userPluginPath): 102 | translationPath = os.path.join(userPluginPath, "loadthemall_" + myLocale + ".qm") 103 | else: 104 | translationPath = os.path.join(systemPluginPath, "loadthemall_" + myLocale + ".qm") 105 | 106 | if QFileInfo(translationPath).exists(): 107 | self.translator = QTranslator() 108 | self.translator.load(translationPath) 109 | QCoreApplication.installTranslator(self.translator) 110 | -------------------------------------------------------------------------------- /LoadThemAll/Makefile: -------------------------------------------------------------------------------- 1 | 2 | all: ui/Ui_DockWidget.py ui/Ui_Base_LoadThemAll.py resources/resources_rc.py i18n/loadthemall_es.ts i18n/loadthemall_fr.ts i18n/loadthemall_es.qm i18n/loadthemall_fr.qm 3 | 4 | clean: 5 | rm -f ui/Ui_DockWidget.py ui/Ui_Base_LoadThemAll.py 6 | rm -f resources/resources_rc.py 7 | rm -f i18n/*.qm 8 | rm -f *.pyc *~ 9 | 10 | resources/resources_rc.py: resources/resources.qrc 11 | pyrcc5 -o resources/resources_rc.py resources/resources.qrc 12 | 13 | ui/Ui_DockWidget.py: ui/Ui_DockWidget.ui 14 | pyuic5 -o ui/Ui_DockWidget.py ui/Ui_DockWidget.ui 15 | 16 | ui/Ui_Base_LoadThemAll.py: ui/Ui_Base_LoadThemAll.ui 17 | pyuic5 -o ui/Ui_Base_LoadThemAll.py ui/Ui_Base_LoadThemAll.ui 18 | 19 | i18n/loadthemall_es.ts i18n/loadthemall_fr.ts: i18n/loadthemall.pro 20 | lupdate i18n/loadthemall.pro 21 | 22 | i18n/loadthemall_es.qm i18n/loadthemall_fr.qm: i18n/loadthemall.pro 23 | lrelease i18n/loadthemall.pro 24 | 25 | build: 26 | cd ..;zip -r /tmp/LoadThemAll.zip LoadThemAll -x LoadThemAll/.git/\* LoadThemAll/.idea/\* \ 27 | LoadThemAll/.gitignore LoadThemAll/README.md LoadThemAll/changelog.txt LoadThemAll/Makefile \ 28 | LoadThemAll/i18n/*.ts LoadThemAll/i18n/*.pro LoadThemAll/resources/resources.qrc \ 29 | LoadThemAll/ui/*.ui 30 | -------------------------------------------------------------------------------- /LoadThemAll/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | /*************************************************************************** 3 | LoadThemAll 4 | A QGIS plugin 5 | Loads files stored in a directory structure recursively, based on several filters 6 | ------------------- 7 | begin : 2010-10-03 8 | copyright : (C) 2010 by Germán Carrillo (GeoTux) 9 | email : gcarrillo@linuxmail.org 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 | def classFactory(iface): 22 | # load LoadThemAll class from file LoadThemAll 23 | from .LoadThemAll import LoadThemAll 24 | return LoadThemAll(iface) 25 | -------------------------------------------------------------------------------- /LoadThemAll/core/Enums.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | 4 | class EnumLoadThemAllResult(Enum): 5 | SUCCESS = 1 # Load Layers was run, although it could've loaded layers or not 6 | CANCELLED = 9 # The task was cancelled by the user 7 | -------------------------------------------------------------------------------- /LoadThemAll/core/FileFormatConfiguration.py: -------------------------------------------------------------------------------- 1 | 2 | # Note: 3 | # All file extensions should be written in lower case. 4 | # In the code we make sure to properly test against variations (e.g., uppercase). 5 | 6 | COMPRESSED_FILE_EXTENSIONS = [".zip", 7 | ".rar", 8 | ".tar", ".tar.gz", ".tgz", 9 | ".gz", 10 | ".7z"] 11 | 12 | VECTOR_FORMATS = [ 13 | ("Arc/Info ASCII Coverage (*.e00)", [".e00"]), 14 | ("Arc/Info Generate (*.gen)", [".gen"]), 15 | ("AutoCAD Driver (*.dwg)", [".dwg"]), 16 | ("AutoCAD DXF (*.dxf)", [".dxf"]), 17 | ("Bathymetry Attributed Grid (*.bag)", [".bag"]), 18 | ("Comma Separated Value (*.csv)", [".csv"]), 19 | ("Czech Cadastral Exchange Data Format (*.vfk)", [".vfk"]), 20 | ("ESRI Shapefile (*.shp *.shz *.shp.zip)", [".shp", ".shz", ".shp.zip"]), 21 | ("FlatGeobuf (*.fgb)", [".fgb"]), 22 | ("Flexible Image Transport System (*.fits)", [".fits"]), 23 | ("Geoconcept (*.gxt *.txt)", [".gxt", ".txt"]), 24 | ("GeoJSON (*.geojson)", [".geojson"]), 25 | ("Geography Markup Language [GML] (*.gml)", [".gml"]), 26 | ("Geomedia .mdb (*.mdb)", [".mdb"]), 27 | ("GeoPackage (*.gpkg)", [".gpkg"]), 28 | ("GeoRSS (*.xml)", [".xml"]), 29 | ("Geospatial PDF (*.pdf)", [".pdf"]), 30 | ("GMT ASCII Vectors (*.gmt)", [".gmt"]), 31 | ("GPS eXchange Format [GPX] (*.gpx)", [".gpx"]), 32 | ("GPSTrackMaker (*.gtm *.gtz)", [".gtm", ".gtz"]), 33 | ("INTERLIS 2 (*.xtf *.xml *.ili)", [".xtf", ".xml", ".ili"]), 34 | ("JSON (*.json)", [".json"]), 35 | ("Keyhole Markup Language [KML] (*.kml *.kmz)", [".kml", ".kmz"]), 36 | ("Mapbox Vector Tiles (*.mvt *.mvt.gz *.pbf)", [".mvt", ".mvt.gz", ".pbf"]), 37 | ("Mapinfo File (*.mif *.tab)", [".mif", ".tab"]), 38 | ("MBTiles (*.mbtiles)", [".mbtiles"]), 39 | ("Microstation DGN (*.dgn)", [".dgn"]), 40 | ("NDJSON Newline Delimited JSON", [".geojsonl", ".geojsons", ".nlgeojson"]), 41 | ("OpenJUMP JML (*.jml)", [".jml"]), 42 | ("OpenStreetMap (*.osm *.pbf)", [".osm", ".pbf"]), 43 | ("PCI Geomatics Database File (*.pix)", [".pix"]), 44 | ("Scalable Vector Graphics (*.svg)", [".svg"]), 45 | ("SQLite/SpatiaLite (*.sqlite(3) *.db(3) *.s3db *.sl3)", [".sqlite", ".db", ".sqlite3", ".db3", ".s3db", ".sl3"]), 46 | ("TopoJSON (*.json *.topojson)", [".json", ".topojson"]), 47 | ("Virtual Datasource [VRT] (*.vrt *.ovf)", [".vrt", ".ovf"]) 48 | ] 49 | 50 | RASTER_FORMATS = [ 51 | ("Virtual Raster (*.vrt)", [".vrt"]), 52 | ("GeoTIFF (*.tif *.tiff)", [".tif", ".tiff"]), 53 | ("Erdas Imagine Images (*.img)", [".img"]), 54 | ("Erdas Compressed Wavelets (*.ecw)", [".ecw"]), 55 | ("ERMapper .ers Labelled (*.ers)", [".ers"]), 56 | ("DTED Elevation Raster (*.dt)", [".dt2", ".dt3"]), 57 | ("Arc/Info ASCII Grid (*.asc)", [".asc"]), 58 | ("Portable Network Graphics (*.png)", [".png"]), 59 | ("JPEG JFIF (*.jpg *.jpeg)", [".jpg", ".jpeg"]), 60 | ("JPEG2000 (*.jp2)", [".jp2"]), 61 | ("Graphics Interchange Format (*.gif)", [".gif"]), 62 | ("X11 PixMap Format (*.xpm)", [".xpm"]), 63 | ("Bitmap image file (*.bmp)", [".bmp"]), 64 | ("PCIDSK Database File (*.pix)", [".pix"]), 65 | ("PCRaster Raster File (*.map)", [".map"]), 66 | ("ILWIS Raster Map (*.mpr *.mpl)", [".mpr", ".mpl"]), 67 | ("SRTMHGT File Format (*.hgt)", [".hgt"]), 68 | ("GMT NetCDF Grid Format (*.nc)", [".nc"]), 69 | ("GRIdded Binary (*.grb)", [".grb"]), 70 | ("Idrisi Raster A.1 (*.rst)", [".rst"]), 71 | ("Golden Software ASCII Grid (*.grd)", [".grd"]), 72 | ("R Object Data Store (*.rda)", [".rda"]), 73 | ("Vexcel MFF Raster (*.hdr)", [".hdr"]), 74 | ("USGS Optional ASCII DEM (*.dem)", [".dem"]), 75 | ("Magellan topo (*.blx)", [".blx"]), 76 | ("Rasterlite (*.sqlite)", [".sqlite"]), 77 | ("SAGA GIS Binary Grid (*.sdat)", [".sdat"]), 78 | ("Multi-resolution Seamless Image Database (*.sid)", [".sid"]), 79 | ("BSB Nautical Chart Format (*.kap)", [".kap"]), 80 | ] 81 | 82 | POINT_CLOUD_FORMATS = [ 83 | ("COPC Point Clouds (*.copc.laz)", [".copc.laz"]), 84 | ("Entwine Point Clouds (ept.json)", ["ept.json"]), 85 | ("PDAL Point Clouds (*.bpf *.e57 *.las *.laz)", [".bpf", ".e57", ".las", ".laz"]), 86 | ("Virtual Point Clouds (*.vpc)", [".vpc"]), 87 | ] 88 | -------------------------------------------------------------------------------- /LoadThemAll/core/Filter.py: -------------------------------------------------------------------------------- 1 | """ 2 | /*************************************************************************** 3 | LoadThemAll 4 | A QGIS plugin 5 | Loads files stored in a directory structure recursively, based on several filters 6 | ------------------- 7 | begin : 2010-10-03 8 | copyright : (C) 2010 by Germán Carrillo (GeoTux) 9 | email : gcarrillo@linuxmail.org 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, re 22 | import time 23 | from abc import (ABC, 24 | abstractmethod) 25 | 26 | from qgis.PyQt.QtCore import QDateTime 27 | from qgis.core import (QgsRectangle, 28 | QgsWkbTypes) 29 | 30 | from .LayerTypes import LayerType 31 | from .Utils import (get_raster_layer, 32 | get_vector_layer, 33 | get_point_cloud_layer) 34 | 35 | 36 | class Filter(ABC): 37 | """ Abstract class to encapsulate filters behavior """ 38 | def __init__(self): 39 | pass 40 | 41 | @abstractmethod 42 | def apply(self, layer_path, layer_dict): 43 | """ To be overwritten """ 44 | pass 45 | 46 | 47 | class NoFilter(Filter): 48 | """ Dummy filter """ 49 | def __init__(self): 50 | Filter.__init__(self) 51 | 52 | def apply(self, layer_path, layer_dict): 53 | """ There is no condition to be applied """ 54 | return True 55 | 56 | 57 | class AlphanumericFilter(Filter): 58 | """ Filter based on text using a regular expression """ 59 | def __init__(self, matchType, filterText, configuration): 60 | Filter.__init__(self) 61 | self.matchType = matchType 62 | self.filterText = filterText 63 | self.caseInsensitive = configuration.b_case_insensitive 64 | self.accentInsensitive = configuration.b_accent_insensitive 65 | self.bSearchParentLayer = configuration.b_search_parent_layer 66 | self.regExpPattern = None # Stores compiled RE pattern to reuse it afterwards 67 | 68 | def apply(self, layer_path, layer_dict): 69 | """ Apply an alphanumeric filter """ 70 | if not self.regExpPattern: # We build a RE pattern only once and then reuse it 71 | self.regExpPattern = self._getRegExpPattern() 72 | 73 | baseName = os.path.basename(layer_path) 74 | layerBaseName = self._get_layer_base_name(baseName) 75 | if '|layername=' in baseName and not baseName.endswith('|layername='): 76 | if self.bSearchParentLayer: 77 | layerBaseName = "".join([layerBaseName, " ", os.path.basename(layer_path).split('|layername=')[1]]) 78 | else: 79 | layerBaseName = baseName.split('|layername=')[1] 80 | 81 | layerBaseName = layerBaseName.lower() if self.caseInsensitive else layerBaseName 82 | if self.accentInsensitive: 83 | try: 84 | from unidecode import unidecode 85 | layerBaseName = unidecode(layerBaseName) 86 | except (ImportError, NameError) as e: 87 | pass # This error is handled in LoadThemAllDialog 88 | 89 | return True if self.regExpPattern.search(layerBaseName) else False 90 | 91 | def _get_layer_base_name(self, baseName): 92 | """ 93 | For a layer like points.geojson.gz, we should compare against points, 94 | instead of points.geojson. This method takes care of that. 95 | """ 96 | if baseName.endswith(".gz") and not baseName.endswith("tar.gz"): 97 | baseName = baseName[:-3] 98 | 99 | return os.path.splitext(baseName)[0] 100 | 101 | def _getRegExpPattern(self): 102 | regExpString = '' 103 | self.filterText = self._normalizeText(self.filterText) 104 | 105 | andList = self.filterText.split("&&") 106 | newAndList = [] 107 | for andTerm in andList: 108 | andTerm = self._normalizeText(andTerm) 109 | orList = andTerm.split("||") 110 | newOrList = [] 111 | 112 | for orTerm in orList: 113 | orTerm = self._normalizeText(orTerm) 114 | if self.matchType == 'StartsWith': 115 | newOrList.append(''.join(['^', orTerm])) 116 | elif self.matchType == 'EndsWith': 117 | newOrList.append(''.join([orTerm, '$'])) 118 | else: # "anyPosition" 119 | newOrList.append(orTerm) 120 | 121 | tmp = '|' if len(orList) > 1 else '' # If there's 1+ 'or' (||) operator... 122 | newAndList.append(tmp.join(newOrList)) 123 | 124 | if len(andList) > 1: # Is there at least an 'and' (&&) operator? 125 | newAndList = [''.join(['(', andElem, ')']) for andElem in newAndList] 126 | # .*?, not .*: https://docs.python.org/2/howto/regex.html#greedy-versus-non-greedy 127 | regExpString = '.*?'.join(newAndList) # .*? is our and RE operator 128 | else: 129 | regExpString = ''.join(newAndList) 130 | 131 | regExpString = regExpString.lower() if self.caseInsensitive else regExpString 132 | if self.accentInsensitive: 133 | try: 134 | from unidecode import unidecode 135 | regExpString = unidecode(regExpString) 136 | except ImportError as e: 137 | pass # This error is handled in LoadThemAllDialog 138 | 139 | return re.compile(regExpString) 140 | 141 | def _normalizeText(self, text): 142 | text = text.strip() 143 | if text.endswith("&&") or text.endswith("||"): text = text[:-2] 144 | if text.startswith("&&") or text.startswith("||"): text = text[2:] 145 | return text 146 | 147 | 148 | class InvertedAlphanumericFilter(Filter): 149 | """ Prepend a logic NOT to an Alphanumeric filter """ 150 | def __init__(self, matchType, filterText, configuration): 151 | Filter.__init__(self) 152 | self._filter = AlphanumericFilter(matchType, filterText, configuration) 153 | 154 | def apply(self, layer_path, layer_dict): 155 | """ Invert Alphanumeric filter result """ 156 | return not self._filter.apply(layer_path, layer_dict) 157 | 158 | 159 | class BoundingBoxFilter(Filter): 160 | """ Filter based on a bounding box """ 161 | def __init__(self, layerType: LayerType, boundingBox, method): 162 | """ 163 | :param layerType: LayerType enum 164 | :type string: 165 | :param boundingBox: The bounding box for selection 166 | :type QgsRectangle: 167 | :param method: The topological relation that should be used for selection, "contains" or "intersects" 168 | """ 169 | Filter.__init__(self) 170 | self.layerType = layerType 171 | self.boundingBox = boundingBox 172 | self.method = method 173 | 174 | def apply(self, layer_path, layer_dict): 175 | """ Apply the bounding box filter """ 176 | 177 | if self.layerType == LayerType.VECTOR: 178 | if layer_dict[layer_path] is None: 179 | layer_dict[layer_path] = get_vector_layer(layer_path, '', layer_dict) 180 | elif self.layerType == LayerType.RASTER: 181 | if layer_dict[layer_path] is None: 182 | layer_dict[layer_path] = get_raster_layer(layer_path, '', layer_dict) 183 | elif self.layerType == LayerType.POINTCLOUD: 184 | if layer_dict[layer_path] is None: 185 | layer_dict[layer_path] = get_point_cloud_layer(layer_path, '', layer_dict) 186 | 187 | bbox = layer_dict[layer_path].extent() 188 | 189 | if self.method == "contains": 190 | return self.boundingBox.contains(bbox) 191 | else: 192 | return self.boundingBox.intersects(bbox) 193 | 194 | 195 | class DateModifiedFilter(Filter): 196 | """ Filter based on 'date modified' from file metadata """ 197 | def __init__(self, comparison, datetime): 198 | Filter.__init__(self) 199 | self._comparison = comparison 200 | self._datetime = datetime 201 | 202 | def apply(self, layer_path, layer_dict): 203 | """ Apply date modifier filter """ 204 | dateModified = QDateTime().fromString(time.ctime(os.path.getmtime(layer_path.split('|layername=')[0]))) 205 | 206 | if self._comparison == 'before': 207 | return dateModified < self._datetime 208 | elif self._comparison == 'after': 209 | return dateModified > self._datetime 210 | else: # 'day' 211 | return dateModified.date() == self._datetime.date() 212 | 213 | 214 | class TypeFilter(Filter): 215 | """ Abstract class to define a filter based on an object's type """ 216 | def __init__(self, itemTypes): 217 | Filter.__init__(self) 218 | self.lstFilterItems = [] # Types to be considered as True 219 | 220 | @abstractmethod 221 | def getItemType(self, layer_path, layer_dict): 222 | """ To be overwritten """ 223 | pass 224 | 225 | def apply(self, layer_path, layer_dict): 226 | """ Apply a type filter """ 227 | itemType = self.getItemType(layer_path, layer_dict) 228 | return itemType in self.lstFilterItems 229 | 230 | 231 | class GeometryTypeFilter(TypeFilter): 232 | """ Filter based on the layer's geometry type """ 233 | def __init__(self, itemTypes): 234 | TypeFilter.__init__(self, itemTypes) 235 | if 'Point' in itemTypes: self.lstFilterItems.append(QgsWkbTypes.PointGeometry) 236 | if 'Line' in itemTypes: self.lstFilterItems.append(QgsWkbTypes.LineGeometry) 237 | if 'Polygon' in itemTypes: self.lstFilterItems.append(QgsWkbTypes.PolygonGeometry) 238 | 239 | if not self.lstFilterItems: 240 | # The user created a Geometry Filter but doesn't want points, lines nor polygons. 241 | # In conclusion, he/she wants geometryless layers. 242 | self.lstFilterItems.append(QgsWkbTypes.NullGeometry) # Alphanumeric tables 243 | 244 | def getItemType(self, layer_path, layer_dict): 245 | """ Get the layer's geometry type """ 246 | if layer_dict[layer_path] is None: 247 | layer_dict[layer_path] = get_vector_layer(layer_path, '', layer_dict) 248 | 249 | return layer_dict[layer_path].geometryType() 250 | 251 | 252 | class RasterTypeFilter(TypeFilter): 253 | """ Filter based on the layer's raster type """ 254 | def __init__(self, itemTypes): 255 | TypeFilter.__init__(self, itemTypes) 256 | if 'GrayOrUndefined' in itemTypes: self.lstFilterItems.append(0) 257 | if 'Palette' in itemTypes: self.lstFilterItems.append(1) 258 | if 'Multiband' in itemTypes: self.lstFilterItems.append(2) 259 | if 'ColorLayer' in itemTypes: self.lstFilterItems.append(3) 260 | 261 | def getItemType(self, layer_path, layer_dict): 262 | """ Get the layer's raster type """ 263 | if layer_dict[layer_path] is None: 264 | layer_dict[layer_path] = get_raster_layer(layer_path, '', layer_dict) 265 | 266 | return layer_dict[layer_path].rasterType() 267 | 268 | 269 | class FilterList(Filter): 270 | """ Manage a list of filters """ 271 | def __init__(self): 272 | Filter.__init__(self) 273 | self.reset() 274 | 275 | def reset(self): 276 | self.filterList = [] 277 | 278 | def addFilter(self, filter): 279 | self.filterList.append(filter) 280 | 281 | def apply(self, layer_path, layer_dict): 282 | if not self.filterList: 283 | return NoFilter().apply(layer_path, layer_dict) # No filter was specified 284 | 285 | for filter in self.filterList: 286 | check = filter.apply(layer_path, layer_dict) 287 | if check is False: 288 | return check 289 | return True 290 | -------------------------------------------------------------------------------- /LoadThemAll/core/LayerTypes.py: -------------------------------------------------------------------------------- 1 | import enum 2 | 3 | 4 | class LayerType(enum.Enum): 5 | VECTOR = enum.auto() 6 | RASTER = enum.auto() 7 | POINTCLOUD = enum.auto() 8 | -------------------------------------------------------------------------------- /LoadThemAll/core/LoadConfiguration.py: -------------------------------------------------------------------------------- 1 | """ 2 | /*************************************************************************** 3 | LoadThemAll 4 | A QGIS plugin 5 | Loads files stored in a directory structure recursively, based on several filters 6 | ------------------- 7 | begin : 2010-10-03 8 | copyright : (C) 2010 by Germán Carrillo (GeoTux) 9 | email : gcarrillo@linuxmail.org 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 | 23 | class LoadConfiguration: 24 | """ Load Them All configuration object """ 25 | def __init__(self): 26 | # Vector/Raster/PointCloud tabs 27 | self.base_dir = '' 28 | self.extension = [] 29 | self.with_gui = True # Switch to False for running LTA with no GUI 30 | 31 | # Configuration tab 32 | self.b_groups = False 33 | self.b_search_in_compressed_files = False 34 | self.b_layers_off = False 35 | self.b_not_empty = True 36 | self.b_sort = True 37 | self.b_reverse_sort = False 38 | self.b_case_insensitive = True 39 | self.b_accent_insensitive = False 40 | self.b_styles = False 41 | self.b_search_parent_layer = False 42 | self.b_add_parent_layer_name = True 43 | self.num_layers_to_confirm = 50 44 | -------------------------------------------------------------------------------- /LoadThemAll/core/LoadFiles.py: -------------------------------------------------------------------------------- 1 | """ 2 | /*************************************************************************** 3 | LoadThemAll 4 | A QGIS plugin 5 | Loads files stored in a directory structure recursively, based on several filters 6 | ------------------- 7 | begin : 2010-10-03 8 | copyright : (C) 2010 by Germán Carrillo (GeoTux) 9 | email : gcarrillo@linuxmail.org 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 | import locale 23 | from abc import (ABC, 24 | abstractmethod) 25 | import pathlib 26 | 27 | from qgis.PyQt.QtCore import (QCoreApplication, 28 | QObject, 29 | pyqtSignal, 30 | pyqtSlot) 31 | from qgis.PyQt.QtWidgets import (QApplication, 32 | QMessageBox) 33 | from qgis.core import (Qgis, 34 | QgsApplication, 35 | QgsVectorLayer, 36 | QgsMapLayer, 37 | QgsCoordinateReferenceSystem) 38 | 39 | if Qgis.versionInt() >= 31800: 40 | from qgis.core import QgsPointCloudLayer 41 | 42 | from .Enums import EnumLoadThemAllResult 43 | from .FileFormatConfiguration import COMPRESSED_FILE_EXTENSIONS 44 | from .Filter import FilterList 45 | from .LoadThemAllResult import LoadThemAllResult 46 | from .QGISLayerTree import QGISLayerTree 47 | from .Utils import (AbstractQObjectMeta, 48 | get_vector_layer, 49 | get_raster_layer, 50 | get_point_cloud_layer, 51 | get_compressed_files_to_load, 52 | get_file_extension, 53 | get_parent_folder) 54 | 55 | 56 | class LoadFiles(QObject, metaclass=AbstractQObjectMeta): 57 | """ Abstract Class to inherit common methods to Load classes 58 | like Vector, Raster and Point Clouds 59 | """ 60 | 61 | reset_progressbar_emitted = pyqtSignal() 62 | update_progress_value_emitted = pyqtSignal(int) # value 63 | update_progress_max_emitted = pyqtSignal(int) # value 64 | 65 | def __init__(self, iface, configuration): 66 | QObject.__init__(self) 67 | self.filterList = FilterList() 68 | self.iface = iface 69 | self.files_to_load = dict() # {'layer_path_1': layer_obj_1, ...} 70 | self.dataType = '' 71 | 72 | self.defaultCrs: QgsCoordinateReferenceSystem = None 73 | 74 | # Configuration parameters 75 | self.configuration = configuration 76 | self.tree = QGISLayerTree(configuration.base_dir, configuration.b_groups) 77 | 78 | self._cancel_process = False # Whether the process has been cancelled externally 79 | 80 | def loadLayers(self): 81 | if self._getFilesToLoad(): 82 | return self._loadLayers() 83 | 84 | return LoadThemAllResult(EnumLoadThemAllResult.CANCELLED) 85 | 86 | def _applyFilter(self, layer_path, layer_dict): 87 | """ Method to encapsulate the filter's application """ 88 | return self.filterList.apply(layer_path, layer_dict) 89 | 90 | def _getFilesToLoad(self): 91 | """ Go through directories to fill a list with layers ready to be loaded """ 92 | self.update_progress_max_emitted.emit(0) # ProgressBar in busy mode 93 | layer_dict = dict() # {'layer_path_1': layer_obj_1, ...} 94 | 95 | for root, dirs, files in os.walk(self.configuration.base_dir): 96 | # files = [self.decodeName(f) for f in files] 97 | for file_ in files: 98 | QApplication.processEvents() # TODO: Perhaps better by chunks? 99 | if self._process_cancelled(): 100 | return False 101 | 102 | extension = get_file_extension(file_) 103 | 104 | if extension in self.configuration.extension or ( 105 | extension in COMPRESSED_FILE_EXTENSIONS and self.configuration.b_search_in_compressed_files): 106 | # current_layer_path = os.path.join( self.decodeName( root ), file_ ) 107 | current_layer_path = os.path.join(root, file_) 108 | 109 | if extension in COMPRESSED_FILE_EXTENSIONS: 110 | layer_paths = get_compressed_files_to_load(current_layer_path, self.configuration.extension) 111 | else: 112 | layer_paths = [current_layer_path] 113 | 114 | for layer_path in layer_paths: 115 | if self.dataType == 'vector': 116 | # Since vectors might have sublayers, treat them specially here 117 | layer = QgsVectorLayer(layer_path, "", "ogr") 118 | if layer.isValid(): 119 | # Do we have sublayers? 120 | if len(layer.dataProvider().subLayers()) > 1: 121 | # Sample: ['0!!::!!line_intersection_collection!!::!!12!!::!!LineString!!::!!geometryProperty'] 122 | subLayers = dict() 123 | for subLayer in layer.dataProvider().subLayers(): 124 | parts = subLayer.split("!!::!!") # 1: name, 3: geometry type 125 | # Sublayers might share layer name, we need to get geometry types just in case 126 | if parts[1] in subLayers: 127 | subLayers[parts[1]].append(parts[3]) 128 | else: 129 | subLayers[parts[1]] = [parts[3]] 130 | 131 | for subLayerName, subLayerGeometries in subLayers.items(): 132 | if len(subLayerGeometries) > 1: 133 | for subLayerGeometry in subLayerGeometries: 134 | layer_dict["{}|layername={}|geometrytype={}".format(layer_path, 135 | subLayerName, 136 | subLayerGeometry)] = None 137 | else: 138 | layer_dict["{}|layername={}".format(layer_path, subLayerName)] = None 139 | else: 140 | layer_dict[layer_path] = layer 141 | else: 142 | layer_dict[layer_path] = layer # We'll take it into account for statistics anyways 143 | else: # 'raster' or 'point cloud' 144 | layer_dict[layer_path] = None 145 | 146 | for path in layer_dict: 147 | QApplication.processEvents() 148 | if self._process_cancelled(): 149 | return False 150 | 151 | if self._applyFilter(path, layer_dict): # The layer passes the filter? 152 | if self.configuration.b_not_empty: # Do not load empty layers 153 | if not self._isEmptyLayer(path, layer_dict): 154 | self.files_to_load[path] = layer_dict.get(path, None) 155 | else: 156 | self.files_to_load[path] = layer_dict.get(path, None) 157 | 158 | self.update_progress_max_emitted.emit(len(self.files_to_load)) 159 | return True 160 | 161 | def _loadLayers(self): 162 | """ Load the layer to the map """ 163 | if self._process_cancelled(): 164 | return LoadThemAllResult(EnumLoadThemAllResult.CANCELLED) 165 | 166 | numLayers = len(self.files_to_load) 167 | layersLoaded = 0 168 | 169 | if numLayers > 0: 170 | result = QMessageBox.Ok # Convenient variable to pass an upcoming condition 171 | 172 | if numLayers >= self.configuration.num_layers_to_confirm: 173 | if self.configuration.with_gui: 174 | result = QMessageBox.question(self.iface.mainWindow(), 175 | QCoreApplication.translate("Load Them All", "Load Them All"), 176 | QCoreApplication.translate("Load Them All", 177 | "There are {} layers to load.\n Do you want to continue?").format( 178 | numLayers), 179 | QMessageBox.Ok | QMessageBox.Cancel, QMessageBox.Ok) 180 | else: 181 | pass # We cannot ask the user if we're in a non-GUI session 182 | 183 | if result == QMessageBox.Ok: 184 | self.iface.mapCanvas().setRenderFlag(False) # Start the loading process 185 | step = 0 186 | 187 | if self.configuration.b_sort: 188 | self.files_to_load = self._sort_layers_to_load() 189 | 190 | for layer_path, layer in self.files_to_load.items(): 191 | QApplication.processEvents() 192 | if self._process_cancelled(): 193 | self.iface.mapCanvas().setRenderFlag(True) 194 | return LoadThemAllResult(EnumLoadThemAllResult.CANCELLED) 195 | 196 | # Finally add the layer and apply the options the user chose 197 | if self.configuration.b_groups: 198 | group = self.tree.add_group(get_parent_folder(layer_path)) 199 | 200 | baseName = os.path.basename(layer_path) 201 | layerName = os.path.splitext(baseName)[0] 202 | 203 | # Let's clear the layer name for sublayers 204 | if '|layername=' in baseName and not baseName.endswith('|layername='): 205 | subLayerName = baseName.split('|layername=')[1].split('|geometrytype=')[0] 206 | if self.configuration.b_add_parent_layer_name: 207 | layerName = "".join([layerName, " ", subLayerName]) 208 | else: 209 | layerName = subLayerName 210 | 211 | ml = self._createLayer(layer_path, layerName, self.files_to_load) 212 | if ml and ml.isValid(): 213 | layersLoaded += 1 214 | if self.configuration.b_groups: 215 | self.tree.add_layer_to_group(ml, group) 216 | else: 217 | self.tree.add_layer(ml, self.configuration.b_layers_off) 218 | 219 | # Look if there is a style to apply 220 | bStyleFound = False 221 | if self.configuration.b_styles: 222 | if self.configuration.b_groups: 223 | # Has the group a style to apply? 224 | aGroup = os.path.dirname(layer_path) 225 | aBaseGroup = os.path.basename(aGroup) 226 | styleFile = os.path.join(aGroup, aBaseGroup + ".qml") 227 | 228 | if os.path.exists(styleFile): 229 | ml.loadNamedStyle(styleFile) 230 | bStyleFound = True 231 | QgsApplication.messageLog().logMessage( 232 | "QML for group '{}' applied to layer '{}'".format( 233 | aBaseGroup, ml.name()), "Load Them All", Qgis.Info) 234 | 235 | if bStyleFound: 236 | self.iface.layerTreeView().refreshLayerSymbology(ml.id()) 237 | else: 238 | QgsApplication.messageLog().logMessage( 239 | "No style found for layer group '{}' or 'create groups' option is disabled!".format( 240 | aBaseGroup), "Load Them All", Qgis.Warning) 241 | # End Styles 242 | 243 | else: 244 | QgsApplication.messageLog().logMessage( 245 | "Layer '{}' couldn't be created properly and wasn't loaded into QGIS. Is the layer data valid? Is the corresponding provider properly installed?".format( 246 | layer_path), 247 | "Load Them All", Qgis.Warning) 248 | 249 | step += 1 250 | self.update_progress_value_emitted.emit(step) 251 | 252 | if self.configuration.b_groups and self.configuration.b_layers_off: # Parent group must be invisible 253 | self.tree.set_parent_invisible() 254 | 255 | self.iface.mapCanvas().setRenderFlag(True) # Finish the loading process 256 | 257 | postMsg = '' 258 | if layersLoaded < numLayers: 259 | postMsg = QCoreApplication.translate("Load Them All", 260 | " You can see a list of not loaded layers in the QGIS log (tab 'Load Them All').") 261 | 262 | if layersLoaded > 1 and numLayers > 1: 263 | doneMsg = QCoreApplication.translate("Load Them All", "layers were loaded successfully.") 264 | elif layersLoaded < 1: 265 | doneMsg = QCoreApplication.translate("Load Them All", "layers loaded successfully.") 266 | elif layersLoaded == 1 and numLayers > 1: 267 | doneMsg = QCoreApplication.translate("Load Them All", "layers was loaded successfully.") 268 | else: 269 | doneMsg = QCoreApplication.translate("Load Them All", "layer was loaded successfully.") 270 | 271 | self.iface.messageBar().pushMessage("Load Them All", 272 | "{} {} {} {}{}".format(layersLoaded, 273 | QCoreApplication.translate("Load Them All", 274 | " out of "), 275 | numLayers, 276 | doneMsg, 277 | postMsg), 278 | duration=20) 279 | 280 | if self.configuration.b_groups: 281 | self.tree.remove_empty_groups() 282 | 283 | if numLayers == 0: 284 | self.reset_progressbar_emitted.emit() 285 | 286 | if self.configuration.with_gui: 287 | if len(self.configuration.extension) == 1: 288 | msgExtensions = str(self.configuration.extension[0])[1:] 289 | else: 290 | msgExtensions = ", ".join(str(x)[1:] for x in self.configuration.extension[:-1]) + \ 291 | QCoreApplication.translate("Load Them All", " or ") + \ 292 | str(self.configuration.extension[len(self.configuration.extension) - 1])[1:] 293 | QMessageBox.information(self.iface.mainWindow(), "Load Them All", 294 | QCoreApplication.translate("Load Them All", "There are no ") + msgExtensions + 295 | QCoreApplication.translate("Load Them All", 296 | " files to load from the base directory with this filter.\n") + 297 | QCoreApplication.translate("Load Them All", 298 | "Change those parameters and try again."), 299 | QMessageBox.Ok) 300 | 301 | return LoadThemAllResult(EnumLoadThemAllResult.SUCCESS, numLayers, layersLoaded) 302 | 303 | @pyqtSlot(bool) 304 | def set_cancel_process(self, cancel): 305 | self._cancel_process = cancel 306 | 307 | def _process_cancelled(self): 308 | return self._cancel_process 309 | 310 | def _sort_layers_to_load(self): 311 | """ 312 | :return: Dict of layers to load sorted {'layer_path_1': layer_obj_1, ...} 313 | """ 314 | files_to_load = self.files_to_load.copy() 315 | layer_paths = list(files_to_load.keys()) 316 | self.files_to_load = None 317 | 318 | # Sort layer list 319 | if self.configuration.b_groups: 320 | layer_paths = sorted(layer_paths, key=locale.strxfrm, reverse=self.configuration.b_reverse_sort) 321 | else: 322 | # Get basenames and order them, otherwise folders will distort order 323 | tmp_dict = {path: os.path.splitext(os.path.basename(path))[0] for path in layer_paths} 324 | # We revert the order to load single layers from the back, so any 325 | # new layer will be added to the top of the layer tree 326 | layer_paths = [k for k, v in sorted(tmp_dict.items(), key=lambda item: locale.strxfrm(str(item[1])), 327 | reverse=not self.configuration.b_reverse_sort)] 328 | 329 | # Now use the sorted list to return a sorted dict 330 | sorted_files_to_load = dict() 331 | for layer_path in layer_paths: 332 | sorted_files_to_load[layer_path] = files_to_load.get(layer_path, None) 333 | 334 | return sorted_files_to_load 335 | 336 | @abstractmethod 337 | def _createLayer(self, layer_path, layer_base_name, files_to_load): 338 | """ To be overwritten by subclasses """ 339 | pass 340 | 341 | @abstractmethod 342 | def _isEmptyLayer(self, layer_path, layer_dict): 343 | """ To be overwritten by subclasses """ 344 | pass 345 | 346 | def set_default_crs(self, default_crs: QgsCoordinateReferenceSystem) -> None: 347 | self.defaultCrs = default_crs 348 | 349 | 350 | class LoadVectors(LoadFiles): 351 | """ Subclass to load vector layers """ 352 | 353 | def __init__(self, iface, configuration): 354 | LoadFiles.__init__(self, iface, configuration) 355 | 356 | self.dataType = 'vector' 357 | 358 | def _createLayer(self, layer_path, layer_base_name, files_to_load): 359 | """ Create a vector layer """ 360 | files_to_load[layer_path] = get_vector_layer(layer_path, layer_base_name, files_to_load, True) 361 | 362 | return files_to_load[layer_path] 363 | 364 | def _isEmptyLayer(self, layer_path, layer_dict): 365 | """ Check whether a vector layer has no features """ 366 | if layer_dict[layer_path] is None: 367 | layer_dict[layer_path] = get_vector_layer(layer_path, '', layer_dict) 368 | 369 | if layer_dict[layer_path].type() == QgsMapLayer.VectorLayer: 370 | if layer_dict[layer_path].featureCount() == 0: 371 | return True 372 | return False 373 | 374 | 375 | class LoadRasters(LoadFiles): 376 | """ Subclass to load raster layers """ 377 | 378 | def __init__(self, iface, configuration): 379 | LoadFiles.__init__(self, iface, configuration) 380 | 381 | self.dataType = 'raster' 382 | 383 | def _createLayer(self, layer_path, layer_base_name, files_to_load): 384 | """ Create a raster layer """ 385 | files_to_load[layer_path] = get_raster_layer(layer_path, layer_base_name, files_to_load, True) 386 | 387 | return files_to_load[layer_path] 388 | 389 | def _isEmptyLayer(self, layer_path, layer_dict): 390 | """ Do not check this on raster layers """ 391 | return False 392 | 393 | 394 | class LoadPointClouds(LoadFiles): 395 | """ Subclass to load point cloud layers """ 396 | 397 | def __init__(self, iface, configuration): 398 | LoadFiles.__init__(self, iface, configuration) 399 | 400 | self.dataType = 'point cloud' 401 | 402 | def _createLayer(self, layer_path, layer_base_name, files_to_load): 403 | """ Create a point cloud layer """ 404 | files_to_load[layer_path] = get_point_cloud_layer(layer_path, layer_base_name, files_to_load, True, self.defaultCrs) 405 | 406 | return files_to_load[layer_path] 407 | 408 | def _isEmptyLayer(self, layer_path, layer_dict): 409 | """ Check whether a point cloud layer has no points """ 410 | if Qgis.versionInt() < 31800: 411 | return False 412 | 413 | if layer_dict[layer_path] is None: 414 | layer_dict[layer_path] = get_point_cloud_layer(layer_path, '', layer_dict) 415 | 416 | if isinstance(layer_dict[layer_path], QgsPointCloudLayer): 417 | if layer_dict[layer_path].dataProvider().pointCount() == 0: 418 | return True 419 | return False 420 | -------------------------------------------------------------------------------- /LoadThemAll/core/LoadThemAllResult.py: -------------------------------------------------------------------------------- 1 | """ 2 | /*************************************************************************** 3 | LoadThemAll 4 | A QGIS plugin 5 | Loads files stored in a directory structure recursively, based on several filters 6 | ------------------- 7 | begin : 2024-03-01 8 | copyright : (C) 2024 by Germán Carrillo (GeoTux) 9 | email : gcarrillo@linuxmail.org 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 | from .Enums import EnumLoadThemAllResult 22 | 23 | 24 | class LoadThemAllResult: 25 | def __init__(self, result: EnumLoadThemAllResult, layers_found:int = 0, layers_loaded:int = 0): 26 | """ 27 | Stores the result of an LTA session. 28 | 29 | :param result: EnumLoadThemAllResult value. indicates the overall result of an LTA session. 30 | :param layers_found: Number of layers that matched the filters and extensions set. 31 | :param layers_loaded: Number of loaded layers. 32 | """ 33 | self.result = result 34 | self.layers_found = layers_found 35 | self.layers_loaded = layers_loaded 36 | -------------------------------------------------------------------------------- /LoadThemAll/core/QGISLayerTree.py: -------------------------------------------------------------------------------- 1 | """ 2 | /*************************************************************************** 3 | LoadThemAll 4 | A QGIS plugin 5 | Loads files stored in a directory structure recursively, based on several filters 6 | ------------------- 7 | begin : 2010-10-03 8 | copyright : (C) 2010 by Germán Carrillo (GeoTux) 9 | email : gcarrillo@linuxmail.org 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.path 22 | 23 | from qgis.core import QgsProject 24 | 25 | 26 | class QGISLayerTree: 27 | """ Class to manage QGIS layer tree calls """ 28 | 29 | def __init__(self, baseDir, createParentGroup=True): 30 | self.baseDir = baseDir 31 | 32 | self.root = QgsProject.instance().layerTreeRoot() 33 | self.createParentGroup = createParentGroup 34 | if createParentGroup: 35 | # Initialize root to match the base dir and build root group in ToC 36 | baseGroupName = os.path.split(baseDir)[1] 37 | group = self.root.findGroup(baseGroupName) 38 | if not group: 39 | group = self.root.insertGroup(0, baseGroupName) 40 | self.root = group 41 | 42 | def add_group(self, path): 43 | """ 44 | Add a group based on a layer's directory. 45 | If parent groups don't exist, it creates all of them until base dir. 46 | 47 | Warning: path must point to a folder! (i.e., path is not a full layer's path). 48 | """ 49 | if path != self.baseDir: 50 | previousPath = os.path.dirname(path) 51 | previousGroup = self.add_group(previousPath) 52 | 53 | lastDir = os.path.split(path)[1] # Get the last dir in the path 54 | group = previousGroup.findGroup(lastDir) 55 | if not group: 56 | group = previousGroup.addGroup(lastDir) 57 | return group 58 | 59 | else: 60 | return self.root 61 | 62 | def add_layer_to_group(self, layer, group): 63 | """ Add a layer to its corresponding group """ 64 | addedLayer = QgsProject.instance().addMapLayer(layer, False) 65 | group.addLayer(addedLayer) 66 | 67 | def add_layer(self, layer, notVisible): 68 | """ Add a layer to the root of the layer tree """ 69 | addedLayer = QgsProject.instance().addMapLayer(layer, False) 70 | addedLayerToRoot = self.root.insertLayer(0, addedLayer) 71 | if notVisible: 72 | addedLayerToRoot.setItemVisibilityChecked(0) 73 | 74 | def set_parent_invisible(self): 75 | self.root.setItemVisibilityChecked(0) 76 | 77 | def remove_empty_groups(self): 78 | """ 79 | Remove created groups if layers weren't added to them, e.g., if the 80 | layer is not valid. 81 | """ 82 | self.root.removeChildrenGroupWithoutLayers() 83 | if len(self.root.children()) == 0: 84 | QgsProject.instance().layerTreeRoot().removeChildNode(self.root) 85 | -------------------------------------------------------------------------------- /LoadThemAll/core/Utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | /*************************************************************************** 3 | LoadThemAll 4 | A QGIS plugin 5 | Loads files stored in a directory structure recursively, based on several filters 6 | ------------------- 7 | begin : 2010-10-03 8 | copyright : (C) 2010 by Germán Carrillo (GeoTux) 9 | email : gcarrillo@linuxmail.org 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.path 22 | import pathlib 23 | from abc import ABCMeta 24 | 25 | try: 26 | from qgis.PyQt.QtCore import pyqtWrapperType 27 | except ImportError: 28 | from sip import wrappertype as pyqtWrapperType 29 | 30 | from qgis.core import (QgsApplication, 31 | QgsRasterLayer, 32 | QgsVectorLayer, 33 | QgsCoordinateReferenceSystem, 34 | QgsProviderRegistry, 35 | Qgis, 36 | QgsMapLayerType) 37 | if Qgis.versionInt() >= 31800: 38 | from qgis.core import QgsPointCloudLayer 39 | 40 | from processing.algs.gdal.GdalUtils import GdalUtils 41 | 42 | from .FileFormatConfiguration import COMPRESSED_FILE_EXTENSIONS 43 | 44 | 45 | _gdal_version = None # Global variable, use get_gdal_version() instead 46 | 47 | 48 | class AbstractQObjectMeta(pyqtWrapperType, ABCMeta): 49 | """Abstract class implementing QObject""" 50 | pass 51 | 52 | 53 | def get_gdal_version(): 54 | global _gdal_version 55 | if _gdal_version is None: 56 | _gdal_version = GdalUtils.version() # e.g., 3040100 --> 3.4.1 57 | 58 | return _gdal_version 59 | 60 | 61 | def get_vector_layer(layer_path, layer_name, layer_dict, rename=False): 62 | res = layer_dict[layer_path] 63 | if res is None: 64 | res = QgsVectorLayer(layer_path, layer_name, 'ogr') 65 | elif rename: 66 | res.setName(layer_name) 67 | 68 | return res 69 | 70 | 71 | def get_raster_layer(layer_path, layer_name, layer_dict, rename=False): 72 | res = layer_dict[layer_path] 73 | if res is None: 74 | res = QgsRasterLayer(layer_path, layer_name) 75 | elif rename: 76 | res.setName(layer_name) 77 | 78 | return res 79 | 80 | 81 | def get_point_cloud_layer(layer_path, layer_name, layer_dict, rename=False, default_crs: QgsCoordinateReferenceSystem = None): 82 | if Qgis.versionInt() < 31800: 83 | return None 84 | 85 | res = layer_dict[layer_path] 86 | if res is None: 87 | provider = QgsProviderRegistry.instance().preferredProvidersForUri(layer_path) 88 | if not provider: 89 | QgsApplication.messageLog().logMessage( 90 | "No provider found for layer '{}'!".format(layer_path), "Load Them All", Qgis.Warning) 91 | return None 92 | res = QgsPointCloudLayer(layer_path, layer_name, provider[0].metadata().key()) 93 | elif rename: 94 | res.setName(layer_name) 95 | 96 | if default_crs: 97 | res.setCrs(default_crs) 98 | 99 | return res 100 | 101 | 102 | def get_compressed_files_to_load(path, extensions): 103 | """ 104 | Recursive function to get all the files inside a compressed file that match the expected extensions. 105 | 106 | :param path: Root compressed file 107 | :param extensions: List of chosen extensions 108 | :return: List of files found inside the compressed file 109 | """ 110 | extension = get_file_extension(path) 111 | files_to_load = [] 112 | 113 | if extension == ".zip": 114 | files_to_load += get_zip_files_to_load(path, extensions) 115 | elif extension == ".rar": 116 | files_to_load += get_rar_files_to_load(path, extensions) 117 | elif extension in [".tar", ".tar.gz", ".tgz"]: 118 | files_to_load += get_tar_files_to_load(path, extensions) 119 | elif extension == ".gz": # Order and 'elif' is important here to avoid getting .tar.gz files 120 | files_to_load += get_gzip_file_to_load(path, extensions) 121 | elif extension == ".7z": 122 | files_to_load += get_7zip_files_to_load(path, extensions) 123 | 124 | return files_to_load 125 | 126 | 127 | def get_zip_files_to_load(path: str, extensions: list[str]) -> list[str]: 128 | """ 129 | Recursive function to get all the files inside a ZIP file that match the expected extensions. 130 | 131 | :param path: Root ZIP file 132 | :param extensions: List of chosen extensions 133 | :return: List of files found inside the ZIP file 134 | """ 135 | import zipfile 136 | zip = zipfile.ZipFile(path) 137 | files_to_load = [] 138 | 139 | for file_ in zip.namelist(): 140 | extension = get_file_extension(file_) 141 | 142 | if extension in extensions: 143 | files_to_load.append('/vsizip/' + path + '/' + file_) 144 | elif extension in COMPRESSED_FILE_EXTENSIONS: 145 | files_to_load += get_compressed_files_to_load(file_, extensions) 146 | 147 | return files_to_load 148 | 149 | 150 | def get_rar_files_to_load(path, extensions): 151 | """ 152 | Recursive function to get all the files inside a RAR file that match the expected extensions. 153 | 154 | :param path: Root RAR file 155 | :param extensions: List of chosen extensions 156 | :return: List of files found inside the RAR file 157 | """ 158 | # Check GDAL >= v3.7 159 | if get_gdal_version() < 3070000: 160 | QgsApplication.messageLog().logMessage( 161 | "Unable to load layers from '{}'!".format(path), "Load Them All", Qgis.Warning) 162 | QgsApplication.messageLog().logMessage( 163 | "To load RAR files you need GDAL >= v3.7 (yours is v{})!".format(get_gdal_version()), "Load Them All", 164 | Qgis.Warning) 165 | return [] 166 | 167 | try: 168 | import rarfile 169 | except ModuleNotFoundError as e: 170 | QgsApplication.messageLog().logMessage( 171 | "Unable to load layers from '{}'!".format(path), "Load Them All", Qgis.Warning) 172 | QgsApplication.messageLog().logMessage( 173 | "To search inside RAR files you need to install the module 'rarfile' (e.g., pip install rarfile)!", 174 | "Load Them All", Qgis.Warning) 175 | return [] 176 | 177 | rf = rarfile.RarFile(path) 178 | files_to_load = [] 179 | 180 | for file_ in rf.namelist(): 181 | extension = get_file_extension(file_) 182 | 183 | if extension in extensions: 184 | files_to_load.append('/vsirar/' + path + '/' + file_) 185 | elif extension in COMPRESSED_FILE_EXTENSIONS: 186 | files_to_load += get_compressed_files_to_load(file_, extensions) 187 | 188 | return files_to_load 189 | 190 | 191 | def get_tar_files_to_load(path, extensions): 192 | """ 193 | Recursive function to get all the files inside a TAR file that match the expected extensions. 194 | 195 | :param path: Root TAR file 196 | :param extensions: List of chosen extensions 197 | :return: List of files found inside the TAR file 198 | """ 199 | import tarfile 200 | if get_file_extension(path) == ".tar": 201 | tar = tarfile.TarFile(path) 202 | elif get_file_extension(path) in [".tar.gz", ".tgz"]: 203 | tar = tarfile.open(path, "r:gz") 204 | 205 | files_to_load = [] 206 | 207 | for file_ in tar.getnames(): 208 | extension = get_file_extension(file_) 209 | 210 | if extension in extensions: 211 | files_to_load.append('/vsitar/' + path + '/' + file_) 212 | elif extension in COMPRESSED_FILE_EXTENSIONS: 213 | files_to_load += get_compressed_files_to_load(file_, extensions) 214 | 215 | tar.close() 216 | 217 | return files_to_load 218 | 219 | 220 | def get_gzip_file_to_load(path, extensions): 221 | """ 222 | Get a GDAL-ready url to the GZIP file that match the expected extensions. 223 | 224 | :param path: GZIP file path 225 | :param extensions: List of chosen extensions 226 | :return: List with the GZipped file's GDAL-ready url (if it matches the chosen extensions) 227 | """ 228 | files_to_load = [] 229 | extension = get_file_extension(path[:-3]) # Get rid of '.gz' to get the path to the real file 230 | 231 | if extension in extensions: 232 | files_to_load.append('/vsigzip/' + path) # This time path should include the .gz suffix 233 | elif extension in COMPRESSED_FILE_EXTENSIONS: 234 | files_to_load += get_compressed_files_to_load(file_, extensions) 235 | 236 | return files_to_load 237 | 238 | 239 | def get_7zip_files_to_load(path, extensions): 240 | """ 241 | Recursive function to get all the files inside a 7zip file that match the expected extensions. 242 | 243 | :param path: Root 7zip file 244 | :param extensions: List of chosen extensions 245 | :return: List of files found inside the 7zip file 246 | """ 247 | # Check GDAL >= v3.7 248 | if get_gdal_version() < 3070000: 249 | QgsApplication.messageLog().logMessage( 250 | "Unable to load layers from '{}'!".format(path), "Load Them All", Qgis.Warning) 251 | QgsApplication.messageLog().logMessage( 252 | "To load 7zip files you need GDAL >= v3.7 (yours is v{})!".format(get_gdal_version()), "Load Them All", 253 | Qgis.Warning) 254 | return [] 255 | 256 | try: 257 | import py7zr 258 | except ModuleNotFoundError as e: 259 | QgsApplication.messageLog().logMessage( 260 | "Unable to load layers from '{}'!".format(path), "Load Them All", Qgis.Warning) 261 | QgsApplication.messageLog().logMessage( 262 | "To search inside 7z files you need to install the module 'py7zr' (e.g., pip install py7zr)!", 263 | "Load Them All", Qgis.Warning) 264 | return [] 265 | 266 | zip = py7zr.SevenZipFile(path) 267 | files_to_load = [] 268 | 269 | for file_ in zip.getnames(): 270 | extension = get_file_extension(file_) 271 | 272 | if extension in extensions: 273 | files_to_load.append('/vsi7z/' + path + '/' + file_) 274 | elif extension in COMPRESSED_FILE_EXTENSIONS: 275 | files_to_load += get_compressed_files_to_load(file_, extensions) 276 | 277 | print(files_to_load) 278 | return files_to_load 279 | 280 | 281 | def get_parent_folder(layer_path): 282 | """ 283 | For ZIP files: 284 | path = '/vsizip//docs/Regional/ZIP_data.zip/AA_PreQuat_NAD27z12.TAB' 285 | QgsProviderRegistry.instance().decodeUri('ogr', path) --> 286 | {'layerId': None, 287 | 'layerName': NULL, 288 | 'path': '/docs/Regional/ZIP_data.zip', 289 | 'vsiPrefix': '/vsizip/', 290 | 'vsiSuffix': '/AA_PreQuat_NAD27z12.TAB'} 291 | 292 | For regular files: 293 | path = '/docs/geodata/Map_Database_ZIP/Geology/Regional/AA_PreQuat_NAD27z12.TAB' 294 | {'layerId': None, 295 | 'layerName': NULL, 296 | 'path': '/docs/geodata/Map_Database_ZIP/Geology/Regional/'} 297 | 298 | :param layer_path: Full layer path 299 | :return: Folder in which we can find the layer 300 | """ 301 | folder = '' 302 | if layer_path.startswith('/vsirar/'): # TODO: Verify if we need this with GDAL v3.7+ 303 | import re 304 | base = re.split("\\.rar", layer_path[8:], flags=re.IGNORECASE)[0] # Get rid of prefix & case-insensitive split 305 | folder = os.path.dirname(base) 306 | else: 307 | parts = QgsProviderRegistry.instance().decodeUri('ogr', layer_path) 308 | folder = os.path.dirname(parts['path']) 309 | 310 | return folder 311 | 312 | 313 | def has_point_cloud_provider() -> bool: 314 | if Qgis.versionInt() < 33000: 315 | point_cloud_providers = QgsProviderRegistry.instance().providersForLayerType(QgsMapLayerType.PointCloudLayer) 316 | else: 317 | point_cloud_providers = QgsProviderRegistry.instance().providersForLayerType(Qgis.LayerType.PointCloud) 318 | 319 | return bool(point_cloud_providers) 320 | 321 | 322 | def get_file_extension(file_path): 323 | """ 324 | Get the file extension considering some special cases. 325 | 326 | abc.zip --> .zip 327 | abc.shp --> .shp 328 | abc.shp.zip --> .shp.zip (special format handled by GDAL/OGR) 329 | a.b.c.shp.zip --> .shp.zip 330 | abc.gz --> .gz 331 | a.b.c.gz --> .gz 332 | abc.geojson.gz --> .gz 333 | a.b.c.geojson --> .geojson 334 | abc.tar.gz --> .tar.gz 335 | a.b.c.tar.gz --> .tar.gz 336 | abc.copc.laz --> .copc.laz 337 | a.b.c.copc.laz --> .copc.laz 338 | abc.laz --> .laz 339 | a.b.c.laz --> .laz 340 | abc.ept.json --> .ept.json 341 | a.b.c.ept.json --> .ept.json 342 | a.b.c.json --> .json 343 | 344 | :param file_path: String with the full file path 345 | :return: String of the file extension 346 | """ 347 | extension = None 348 | try: 349 | # Nasty file names like those created by malware should be caught and ignored 350 | suffixes = pathlib.Path(file_path).suffixes 351 | 352 | if not suffixes: 353 | return None 354 | 355 | if len(suffixes) == 1: 356 | # ept.json files get one suffix, let's see if we have 357 | # this special case and deal with it accordingly 358 | if suffixes[0].lower() == ".json" and file_path.lower().endswith("ept.json"): 359 | extension = "ept.json" 360 | else: 361 | extension = suffixes[0].lower() 362 | elif len(suffixes) >= 2: 363 | tmp_extension = "".join(suffixes[-2:]).lower() 364 | if tmp_extension in [".shp.zip", ".tar.gz", ".copc.laz"]: 365 | # These are well-known 'double' extensions that QGIS will handle 366 | extension = tmp_extension 367 | 368 | if extension is None: 369 | # Take the latest extension, chances are it's a filename with points in it 370 | extension = suffixes[-1].lower() 371 | 372 | except UnicodeEncodeError as e: 373 | extension = None 374 | 375 | return extension 376 | -------------------------------------------------------------------------------- /LoadThemAll/core/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gacarrillor/loadthemall/1c8d1421a2c0d3fe54cef50ad996f6996b152d39/LoadThemAll/core/__init__.py -------------------------------------------------------------------------------- /LoadThemAll/gui/BaseLoadThemAllDialog.py: -------------------------------------------------------------------------------- 1 | """ 2 | /*************************************************************************** 3 | LoadThemAll 4 | A QGIS plugin 5 | Loads files stored in a directory structure recursively, based on several filters 6 | ------------------- 7 | begin : 2010-10-03 8 | copyright : (C) 2010 by Germán Carrillo (GeoTux) 9 | email : gcarrillo@linuxmail.org 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 qgis.PyQt.QtCore import (Qt, 23 | QSettings) 24 | from qgis.PyQt.QtWidgets import (QApplication, 25 | QDialog, 26 | QFileDialog) 27 | 28 | from ..core.FileFormatConfiguration import VECTOR_FORMATS, RASTER_FORMATS, POINT_CLOUD_FORMATS 29 | from ..core.LayerTypes import LayerType 30 | from ..ui.Ui_Base_LoadThemAll import Ui_Base_LoadThemAll 31 | 32 | 33 | class BaseLoadThemAllDialog(QDialog, Ui_Base_LoadThemAll): 34 | """ A generic class to be reused in vector and raster dialogs """ 35 | 36 | def __init__(self, layerType: LayerType, iface): 37 | QDialog.__init__(self) 38 | self.setupUi(self) 39 | self.layerType = layerType 40 | self.loadFormats() 41 | self.loadDateComparisons() 42 | self.btnBaseDir.clicked.connect(self.selectDir) 43 | self.btnLoadExtent.clicked.connect(self.updateExtentFromCanvas) 44 | self.cboDateComparison.currentIndexChanged.connect(self.updateDateFormat) 45 | self.iface = iface 46 | 47 | def selectDir(self): 48 | """ Open a dialog for the user to choose a starting directory """ 49 | settings = QSettings() 50 | settings_name = "" 51 | 52 | if self.layerType == LayerType.RASTER: 53 | settings_name = "/Load_Them_All/raster/path" 54 | elif self.layerType == LayerType.VECTOR: 55 | settings_name = "/Load_Them_All/vector/path" 56 | elif self.layerType == LayerType.POINTCLOUD: 57 | settings_name = "/Load_Them_All/pointcloud/path" 58 | 59 | path = QFileDialog.getExistingDirectory(self, self.tr("Select a base directory"), 60 | settings.value(settings_name, "", 61 | type=str), 62 | QFileDialog.ShowDirsOnly) 63 | 64 | if path: 65 | self.txtBaseDir.setText(path) 66 | 67 | def updateExtentFromCanvas(self): 68 | canvas = self.iface.mapCanvas() 69 | boundBox = canvas.extent() 70 | self.txtXMin.setText(str(boundBox.xMinimum())) 71 | self.txtYMin.setText(str(boundBox.yMinimum())) 72 | self.txtXMax.setText(str(boundBox.xMaximum())) 73 | self.txtYMax.setText(str(boundBox.yMaximum())) 74 | 75 | def loadFormats(self): 76 | """ Fill the comboBox with file formats """ 77 | if self.layerType == LayerType.RASTER: 78 | allFormats = RASTER_FORMATS 79 | elif self.layerType == LayerType.VECTOR: 80 | allFormats = VECTOR_FORMATS 81 | elif self.layerType == LayerType.POINTCLOUD: 82 | allFormats = POINT_CLOUD_FORMATS 83 | allExtensions = [extension for format in allFormats for extension in format[1]] 84 | self.cboFormats.addItem("All listed formats (*.*)", allExtensions) 85 | for format in allFormats: 86 | self.cboFormats.addItem(*format) 87 | 88 | def loadDateComparisons(self): 89 | self.cboDateComparison.addItem(QApplication.translate("Base_LoadThemAllDialog", "Before"), "before") 90 | self.cboDateComparison.addItem(QApplication.translate("Base_LoadThemAllDialog", "Exact date"), "day") 91 | self.cboDateComparison.addItem(QApplication.translate("Base_LoadThemAllDialog", "After"), "after") 92 | 93 | def updateDateFormat(self, index): 94 | comparison = self.cboDateComparison.itemData(index) 95 | if comparison == 'day': 96 | self.dtDateTime.setDisplayFormat("ddd dd MMM yyyy") 97 | else: # 'before' or 'after' 98 | self.dtDateTime.setDisplayFormat("ddd dd MMM yyyy hh:mm AP") 99 | 100 | def keyPressEvent(self, e): 101 | """ Handle the ESC key to avoid only the base dialog being closed """ 102 | if e.key() == Qt.Key_Escape: 103 | e.ignore() 104 | -------------------------------------------------------------------------------- /LoadThemAll/gui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gacarrillor/loadthemall/1c8d1421a2c0d3fe54cef50ad996f6996b152d39/LoadThemAll/gui/__init__.py -------------------------------------------------------------------------------- /LoadThemAll/i18n/loadthemall.pro: -------------------------------------------------------------------------------- 1 | FORMS = ../ui/Ui_Base_LoadThemAll.ui \ 2 | ../ui/Ui_DockWidget.ui 3 | 4 | SOURCES = ../Base_LoadThemAllDialog.py \ 5 | ../Filter.py \ 6 | ../LoadFiles.py \ 7 | ../LoadThemAllDialog.py \ 8 | ../LoadThemAll.py \ 9 | ../ui/Ui_Base_LoadThemAll.py \ 10 | ../ui/Ui_DockWidget.py 11 | 12 | TRANSLATIONS = loadthemall_es.ts loadthemall_fr.ts 13 | -------------------------------------------------------------------------------- /LoadThemAll/i18n/loadthemall_es.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gacarrillor/loadthemall/1c8d1421a2c0d3fe54cef50ad996f6996b152d39/LoadThemAll/i18n/loadthemall_es.qm -------------------------------------------------------------------------------- /LoadThemAll/i18n/loadthemall_es.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Base_LoadThemAll 6 | 7 | 8 | Dialog 9 | Diálogo 10 | 11 | 12 | 13 | Select a base directory 14 | Selecciona un directorio base 15 | 16 | 17 | 18 | Format 19 | Formato 20 | 21 | 22 | 23 | Alphanumeric filter 24 | Filtro alfanumérico 25 | 26 | 27 | 28 | Invert filter (i.e., prepend a logic NOT) 29 | Invertir filtro (anteponer un NOT) 30 | 31 | 32 | 33 | Filter string 34 | Texto 35 | 36 | 37 | 38 | Starts with 39 | Inicia con 40 | 41 | 42 | 43 | In any position 44 | En cualquiera 45 | 46 | 47 | 48 | Ends with 49 | Termina con 50 | 51 | 52 | 53 | Bounding box filter 54 | Filtro por rectángulo delimitador 55 | 56 | 57 | 58 | North 59 | Norte 60 | 61 | 62 | 63 | West 64 | Oeste 65 | 66 | 67 | 68 | East 69 | Este 70 | 71 | 72 | 73 | South 74 | Sur 75 | 76 | 77 | 78 | Contains 79 | Contiene 80 | 81 | 82 | 83 | Intersects 84 | Intersecta 85 | 86 | 87 | 88 | Map extent 89 | Extensión del mapa 90 | 91 | 92 | 93 | Date modified filter 94 | Filtro por fecha de modificación 95 | 96 | 97 | 98 | ddd dd MMM yyyy hh:mm AP 99 | 100 | 101 | 102 | 103 | Base_LoadThemAllDialog 104 | 105 | Select a base directory 106 | Selecciona un directorio base 107 | 108 | 109 | Before 110 | Antes de 111 | 112 | 113 | Exact date 114 | Fecha exacta 115 | 116 | 117 | After 118 | Después de 119 | 120 | 121 | 122 | DockWidget 123 | 124 | 125 | Load Them All 126 | Load Them All 127 | 128 | 129 | 130 | Vector 131 | Vector 132 | 133 | 134 | 135 | Geometry type filter 136 | Filtro por tipo de geometría 137 | 138 | 139 | 140 | Polygon 141 | Polígono 142 | 143 | 144 | 145 | Point 146 | Punto 147 | 148 | 149 | 150 | Line 151 | Línea 152 | 153 | 154 | 155 | Raster 156 | Raster 157 | 158 | 159 | 160 | Raster type filter 161 | Filtro por tipo ráster 162 | 163 | 164 | 165 | Gray or Undefined 166 | Gris o indefinido 167 | 168 | 169 | 170 | Palette 171 | Paleta 172 | 173 | 174 | 175 | Multiband 176 | Multibanda 177 | 178 | 179 | 180 | Color Layer 181 | 182 | 183 | 184 | 185 | Point Cloud 186 | Nube de puntos 187 | 188 | 189 | 190 | Set CRS to all Point Cloud Layers 191 | 192 | 193 | 194 | 195 | Configuration 196 | Configuración 197 | 198 | 199 | 200 | Create groups based on directories' names 201 | Crear grupos con base en nombres de directorios 202 | 203 | 204 | 205 | Turn off the loaded layers 206 | Apagar las capas cargadas 207 | 208 | 209 | 210 | <html><head/><body><p>Supported compressed files:</p><p>ZIP, Gzip (.gz), 7zip (.7z), TAR, and RAR.</p><p><br/></p><p><span style=" font-style:italic;">For 7zip: Module 'py7zr' is required.</span></p><p><span style=" font-style:italic;">For RAR: Module 'rarfile' is required.</span></p></body></html> 211 | <html><head/><body><p>Archivos soportados:</p><p>ZIP, Gzip (.gz), 7zip (.7z), TAR y RAR.</p><p><br/></p><p><span style=" font-style:italic;">Para 7zip: Se requiere el módulo 'py7zr'.</span></p><p><span style=" font-style:italic;">Para RAR: Se requiere el módulo 'rarfile'.</span></p></body></html> 212 | 213 | 214 | 215 | Also search inside compressed files 216 | Buscar también dentro de archivos comprimidos 217 | 218 | 219 | 220 | Copyright (C) 2010-2023 Germán Carrillo 221 | Copyright (C) 2010-2023 Germán Carrillo 222 | 223 | 224 | 225 | <html><head/><body><p><span style=" font-style:italic;">Code contributors:</span><br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;David Bakeman (v2.1 and v2.4)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Soeren Gebbert (v2.3)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Jean Hemmi (v3.1 &amp; French transl.)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Guillaume Lostis (v3.3)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Jan Caha (v3.5)</p></body></html> 226 | <html><head/><body><p><span style=" font-style:italic;">Colaboradores de código:</span><br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;David Bakeman (v2.1 y v2.4)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Soeren Gebbert (v2.3)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Jean Hemmi (v3.1 &amp; trad. al francés)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Guillaume Lostis (v3.3)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Jan Caha (v3.5)</p></body></html> 227 | 228 | 229 | 230 | Do not load empty vector layers 231 | No cargar capas vacías 232 | 233 | 234 | 235 | Sort loaded layers by name 236 | Ordenar por nombre las capas cargadas 237 | 238 | 239 | 240 | Reverse sort order 241 | Ordenar de manera inversa 242 | 243 | 244 | 245 | Ignore case in the alphanumeric filter 246 | Ignorar mayúsculas/minúsculas en filtros 247 | 248 | 249 | 250 | This option requires the Python lib 'unidecode' 251 | Esta opción requiere la librería de Python 'unidecode' 252 | 253 | 254 | 255 | Ignore accents in the alphanumeric filter 256 | Ignorar caracteres especiales en filtros 257 | 258 | 259 | 260 | SubLayers 261 | Subcapas 262 | 263 | 264 | 265 | Include parent in search 266 | Incluir capa padre en búsquedas 267 | 268 | 269 | 270 | Include parent name in loaded sublayers 271 | Incluir nombre de capa padre en subcapas 272 | 273 | 274 | 275 | Number of layers to show you a confirmation dialog before the load 276 | Número de capas para mostrar un diálogo de confirmación antes del cargue 277 | 278 | 279 | 280 | Apply group style to layers 281 | Aplicar estilo de grupo a las capas 282 | 283 | 284 | Copyright (C) 2010-2021 Germán Carrillo 285 | Copyright (C) 2010-2021 Germán Carrillo 286 | 287 | 288 | <html><head/><body><p><span style=" font-style:italic;">Code contributors:</span><br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;David Bakeman (v2.1 and v2.4)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Soeren Gebbert (v2.3)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Jean Hemmi (v3.1 &amp; French transl.)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Guillaume Lostis (v3.3)</p></body></html> 289 | <html><head/><body><p><span style=" font-style:italic;">Colaboradores:</span><br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;David Bakeman (v2.1 y v2.4)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Soeren Gebbert (v2.3)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Jean Hemmi (v3.1 y trad. al francés)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Guillaume Lostis (v3.3)</p></body></html> 290 | 291 | 292 | 293 | 50 294 | 50 295 | 296 | 297 | 298 | When checked, if groups are checked and a QML is found inside a layer folder with the 299 | same folder name (e.g., my_group.qml), it will be applied to all layers inside that group. 300 | When checked, if a QML file shares a layer's name and path, it will be applied. If groups are checked and a QML has the group name, it will be applied to all layers inside that group. 301 | Cuando esté seleccionado, si los grupos están seleccionados y se encuentra un QML dentro de la carpeta de 302 | una capa con el nombre del grupo (ej. mi_grupo.qml), este será aplicado a todas las capas dentro de ese grupo. 303 | 304 | 305 | Apply layer style 306 | Aplicar estilos a las capas 307 | 308 | 309 | 310 | About 311 | Acerca de 312 | 313 | 314 | 315 | The <i>Load Them All</i> plugin allows you to load at the same time a number of layers stored in a directory structure, based on a variety of filters you may customize. 316 | El plugin <i>Load Them All</i> permite cargar de forma simultánea varias capas almacenadas en una estructura de directorios, con base en una serie de filtros que se pueden personalizar. 317 | 318 | 319 | 320 | Help 321 | Ayuda 322 | 323 | 324 | 325 | <html><head/><body><p>Feel free to report bugs, suggest improvements or say hello at gcarrillo@linuxmail.org or directly at the <a href="https://github.com/gacarrillor/loadthemall"><span style=" text-decoration: underline; color:#0000ff;">GitHub repository</span></a></p></body></html> 326 | <html><head/><body><p>Reporta bugs, sugiere mejoras o saluda :D en gcarrillo@linuxmail.org o directamente en el <a href="https://github.com/gacarrillor/loadthemall"><span style=" text-decoration: underline; color:#0000ff;">repositorio de GitHub</span></a></p></body></html> 327 | 328 | 329 | Copyright (C) 2010-2020 Germán Carrillo 330 | Copyright (C) 2010-2020 Germán Carrillo 331 | 332 | 333 | 334 | <i>Licensed under the terms of GNU GPL 2</i> 335 | <i>Licencia GNU GPL 2</i> 336 | 337 | 338 | <html><head/><body><p><span style=" font-style:italic;">Code contributors:</span><br/><br/> David Bakeman (v2.1 and v2.4)<br/> Soeren Gebbert (v2.3)<br/> Jean Hemmi (v3.1 &amp; French transl.)</p></body></html> 339 | <html><head/><body><p><span style=" font-style:italic;">Code contributors:</span><br/><br/> David Bakeman (v2.1 and v2.4)<br/> Soeren Gebbert (v2.3)<br/> Jean Hemmi (v3.1 &amp; French transl.)</p></body></html> 340 | 341 | 342 | 343 | Load layers 344 | Cargar capas 345 | 346 | 347 | 348 | Cancel 349 | Cancelar 350 | 351 | 352 | 353 | Load Them All 354 | 355 | Load Them All 356 | Load Them All 357 | 358 | 359 | There are {} layers to load. 360 | Do you want to continue? 361 | Hay {} capas para cargar. 362 | ¿Quieres continuar? 363 | 364 | 365 | You can see a list of not loaded layers in the QGIS log (tab 'Load Them All'). 366 | Puedes ver un listado de capas no cargadas en la pestaña 'Load Them All' del log de QGIS. 367 | 368 | 369 | layers were loaded successfully. 370 | capas fueron cargadas exitosamente. 371 | 372 | 373 | layers loaded successfully. 374 | capas cargadas exitosamente. 375 | 376 | 377 | layers was loaded successfully. 378 | capas fue cargada exitosamente. 379 | 380 | 381 | layer was loaded successfully. 382 | capa fue cargada exitosamente. 383 | 384 | 385 | out of 386 | de 387 | 388 | 389 | or 390 | o 391 | 392 | 393 | There are no <i> 394 | No hay archivos <i> 395 | 396 | 397 | </i> files to load from the base directory with this filter. 398 | 399 | </i> para cargar desde el directorio elegido y con el filtro especificado. 400 | 401 | 402 | 403 | Change those parameters and try again. 404 | Cambia esos parámetros e intenta de nuevo. 405 | 406 | 407 | 408 | LoadThemAllDialog 409 | 410 | The bounding box coordinates are not correct! 411 | 412 | ¡Las coordenadas del rectángulo delimitador (BBOX) son incorrectas! 413 | 414 | 415 | Please adjust the bounding box settings. 416 | Por favor ajústalas. 417 | 418 | 419 | Some bounding box coordinates are missing! 420 | 421 | ¡Hacen falta algunas coordenadas del rectángulo delimitador (BBOX)! 422 | 423 | 424 | Please set all bounding box coordinates. 425 | Por favor define todas las coordenadas. 426 | 427 | 428 | No layer will match the filter! 429 | 430 | ¡Ninguna capa coincidirá con el filtro! 431 | 432 | 433 | Select a geometry type or uncheck the Geometry type filter. 434 | Selecciona un tipo de geometría o no elijas el filtro por tipo de geometría. 435 | 436 | 437 | Select a raster type or uncheck the Raster type filter. 438 | Selecciona un tipo de ráster o no elijas el filtro por tipo de ráster. 439 | 440 | 441 | Accents were not ignored! 442 | ¡Los caracteres especiales no fueron ignorados! 443 | 444 | 445 | The specified directory could not be found! 446 | 447 | ¡El directorio especificado no se encontró! 448 | 449 | 450 | Please select an existing directory. 451 | Por favor selecciona un directorio existente. 452 | 453 | 454 | You have chosen to ignore accents in the alphanumeric filter, but first 455 | Has elegido ignorar caracteres especiales para el filtro alfanumérico, pero primero 456 | 457 | 458 | you need to install the Python library 'unidecode'. 459 | necesitas instalar la librería de Python 'unicode'. 460 | 461 | 462 | 463 | -------------------------------------------------------------------------------- /LoadThemAll/i18n/loadthemall_fr.qm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gacarrillor/loadthemall/1c8d1421a2c0d3fe54cef50ad996f6996b152d39/LoadThemAll/i18n/loadthemall_fr.qm -------------------------------------------------------------------------------- /LoadThemAll/i18n/loadthemall_fr.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Base_LoadThemAll 6 | 7 | 8 | Dialog 9 | Dialogue 10 | 11 | 12 | 13 | Select a base directory 14 | Choisir le répertoire base du chargement 15 | 16 | 17 | 18 | Format 19 | Format 20 | 21 | 22 | 23 | Alphanumeric filter 24 | Filtre alphanumérique 25 | 26 | 27 | 28 | Invert filter (i.e., prepend a logic NOT) 29 | Inverser le filtre (préfixer d'un NOT) 30 | 31 | 32 | 33 | Filter string 34 | Filtre textuel 35 | 36 | 37 | 38 | Starts with 39 | Commence par 40 | 41 | 42 | 43 | In any position 44 | N'importe où 45 | 46 | 47 | 48 | Ends with 49 | Se termine par 50 | 51 | 52 | 53 | Bounding box filter 54 | Filtre dans une boite rectangulaire (emprise) 55 | 56 | 57 | 58 | North 59 | Nord 60 | 61 | 62 | 63 | West 64 | Ouest 65 | 66 | 67 | 68 | East 69 | Est 70 | 71 | 72 | 73 | South 74 | Sud 75 | 76 | 77 | 78 | Contains 79 | Contient 80 | 81 | 82 | 83 | Intersects 84 | Intersecte 85 | 86 | 87 | 88 | Map extent 89 | Emprise de la carte 90 | 91 | 92 | 93 | Date modified filter 94 | Filtre par date de modification 95 | 96 | 97 | 98 | ddd dd MMM yyyy hh:mm AP 99 | 100 | 101 | 102 | 103 | Base_LoadThemAllDialog 104 | 105 | Select a base directory 106 | Choisir le répertoire base du chargement 107 | 108 | 109 | Before 110 | Avant 111 | 112 | 113 | Exact date 114 | Date exacte 115 | 116 | 117 | After 118 | Après 119 | 120 | 121 | 122 | DockWidget 123 | 124 | 125 | Load Them All 126 | Load Them All 127 | 128 | 129 | 130 | Vector 131 | Vecteur 132 | 133 | 134 | 135 | Geometry type filter 136 | Filtre par type de géométríe 137 | 138 | 139 | 140 | Polygon 141 | Polygone 142 | 143 | 144 | 145 | Point 146 | Point 147 | 148 | 149 | 150 | Line 151 | Lígne 152 | 153 | 154 | 155 | Raster 156 | Raster 157 | 158 | 159 | 160 | Raster type filter 161 | Filtre par type de raster 162 | 163 | 164 | 165 | Gray or Undefined 166 | Gris ou non défini 167 | 168 | 169 | 170 | Palette 171 | Palette de couleur 172 | 173 | 174 | 175 | Multiband 176 | Multibande 177 | 178 | 179 | 180 | Color Layer 181 | 182 | 183 | 184 | 185 | Point Cloud 186 | Nuage de points 187 | 188 | 189 | 190 | Set CRS to all Point Cloud Layers 191 | 192 | 193 | 194 | 195 | Configuration 196 | Configuration 197 | 198 | 199 | 200 | Create groups based on directories' names 201 | Créer les groupes sur la base des noms de répertoires 202 | 203 | 204 | 205 | Turn off the loaded layers 206 | Pas de chargement dynamique des couches 207 | 208 | 209 | 210 | <html><head/><body><p>Supported compressed files:</p><p>ZIP, Gzip (.gz), 7zip (.7z), TAR, and RAR.</p><p><br/></p><p><span style=" font-style:italic;">For 7zip: Module 'py7zr' is required.</span></p><p><span style=" font-style:italic;">For RAR: Module 'rarfile' is required.</span></p></body></html> 211 | <html><head/><body><p>Fichiers compressés supportés :</p><p>ZIP, Gzip (.gz), 7zip (.7z), TAR et RAR.</p><p><br/></p><p><span style=" font-style:italic;">Pour 7zip : Le module 'py7zr' est requis.</span></p><p><span style=" font-style:italic;">Pour RAR : Le module 'rarfile' est requis.</span></p></body></html> 212 | 213 | 214 | 215 | Also search inside compressed files 216 | Chercher aussi dans les fichiers compressés 217 | 218 | 219 | 220 | Copyright (C) 2010-2023 Germán Carrillo 221 | Copyright (C) 2010-2023 Germán Carrillo 222 | 223 | 224 | 225 | <html><head/><body><p><span style=" font-style:italic;">Code contributors:</span><br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;David Bakeman (v2.1 and v2.4)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Soeren Gebbert (v2.3)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Jean Hemmi (v3.1 &amp; French transl.)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Guillaume Lostis (v3.3)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Jan Caha (v3.5)</p></body></html> 226 | <html><head/><body><p><span style=" font-style:italic;">Collaborateurs :</span><br/><br/>&nbsp;&nbsp;&nbsp;&nbsp;David Bakeman (v2.1 et v2.4)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Soeren Gebbert (v2.3)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Jean Hemmi (v3.1 &amp; traduction FR)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Guillaume Lostis (v3.3)<br/>&nbsp;&nbsp;&nbsp;&nbsp;Jan Caha (v3.5)</p></body></html> 227 | 228 | 229 | 230 | Do not load empty vector layers 231 | Ne pas charger les vecteurs vides 232 | 233 | 234 | 235 | Sort loaded layers by name 236 | Trier les couches par nom 237 | 238 | 239 | 240 | Reverse sort order 241 | Inverser le tri 242 | 243 | 244 | 245 | Ignore case in the alphanumeric filter 246 | Ignorer majuscules/minuscules dans le filtre 247 | 248 | 249 | 250 | This option requires the Python lib 'unidecode' 251 | Cette opcion nécessite la librairie Python 'unidecode' 252 | 253 | 254 | 255 | Ignore accents in the alphanumeric filter 256 | Ignorer les caractères spéciaux dans le filtre 257 | 258 | 259 | 260 | SubLayers 261 | Sous-couches 262 | 263 | 264 | 265 | Include parent in search 266 | Inclure le parent dans la recherche 267 | 268 | 269 | 270 | Include parent name in loaded sublayers 271 | Inclure le nom du parent dans les sous-couches 272 | 273 | 274 | 275 | Number of layers to show you a confirmation dialog before the load 276 | Nombre de couches pour faire apparaitre la fenêtre de confirmation du chargement 277 | 278 | 279 | 280 | Apply group style to layers 281 | Appliquer vos styles 282 | 283 | 284 | Copyright (C) 2010-2021 Germán Carrillo 285 | Copyright (C) 2010-2020 Germán Carrillo {2010-2021 ?} 286 | 287 | 288 | 289 | 50 290 | 50 291 | 292 | 293 | 294 | When checked, if groups are checked and a QML is found inside a layer folder with the 295 | same folder name (e.g., my_group.qml), it will be applied to all layers inside that group. 296 | When checked, if a QML file shares a layer's name and path, it will be applied. If groups are checked and a QML has the group name, it will be applied to all layers inside that group. 297 | 298 | 299 | 300 | Apply layer style 301 | Appliquer vos styles 302 | 303 | 304 | 305 | About 306 | À propos 307 | 308 | 309 | 310 | The <i>Load Them All</i> plugin allows you to load at the same time a number of layers stored in a directory structure, based on a variety of filters you may customize. 311 | L'extension <i>Load Them All</i> permet de charger un lot de couches ainsi que la structure des répertoires. En option, on peut personnaliser le chargement par plusieurs filtres. 312 | 313 | 314 | 315 | Help 316 | Aide 317 | 318 | 319 | 320 | <html><head/><body><p>Feel free to report bugs, suggest improvements or say hello at gcarrillo@linuxmail.org or directly at the <a href="https://github.com/gacarrillor/loadthemall"><span style=" text-decoration: underline; color:#0000ff;">GitHub repository</span></a></p></body></html> 321 | <html><head/><body><p>Retour de bugs, proposition d'améliorations ou salutations à gcarrillo@linuxmail.org ou directement dans le <a href="https://github.com/gacarrillor/loadthemall"><span style=" text-decoration: underline; color:#0000ff;">dépôt GitHub</span></a></p></body></html> 322 | 323 | 324 | Copyright (C) 2010-2020 Germán Carrillo 325 | Copyright (C) 2010-2020 Germán Carrillo 326 | 327 | 328 | 329 | <i>Licensed under the terms of GNU GPL 2</i> 330 | <i>Licencia GNU GPL 2</i> 331 | 332 | 333 | <html><head/><body><p><span style=" font-style:italic;">Code contributors:</span><br/><br/> David Bakeman (v2.1 and v2.4)<br/> Soeren Gebbert (v2.3)<br/> Jean Hemmi (v3.1 &amp; French transl.)</p></body></html> 334 | <html><head/><body><p><span style=" font-style:italic;">Code contributors:</span><br/><br/> David Bakeman (v2.1 and v2.4)<br/> Soeren Gebbert (v2.3)<br/> Jean Hemmi (v3.1 &amp; French transl.)</p></body></html> 335 | 336 | 337 | 338 | Load layers 339 | Charger les couches 340 | 341 | 342 | 343 | Cancel 344 | Annuler 345 | 346 | 347 | Apply styles 348 | Appliquer vos styles 349 | 350 | 351 | 352 | Load Them All 353 | 354 | Load Them All 355 | Load Them All 356 | 357 | 358 | There are {} layers to load. 359 | Do you want to continue? 360 | {} couches sont à charger. 361 | Voulez-vous continuer? 362 | 363 | 364 | You can see a list of not loaded layers in the QGIS log (tab 'Load Them All'). 365 | Vous pouvez retrouver la liste des vouches non chargées dans la log de QGIS 'Load Them All'. 366 | 367 | 368 | layers were loaded successfully. 369 | couches déjà chargées. 370 | 371 | 372 | layers loaded successfully. 373 | couches déjà chargées. 374 | 375 | 376 | layers was loaded successfully. 377 | couche déjà chargée. 378 | 379 | 380 | layer was loaded successfully. 381 | couche déjà chargée. 382 | 383 | 384 | out of 385 | parmi 386 | 387 | 388 | or 389 | ou 390 | 391 | 392 | There are no <i> 393 | Aucune archive <i> 394 | 395 | 396 | </i> files to load from the base directory with this filter. 397 | 398 | </i> fichiers à charger dans le répertoire et répondant à vos filtres. 399 | 400 | 401 | 402 | Change those parameters and try again. 403 | Changer vos paramétrages et charger à nouveau. 404 | 405 | 406 | 407 | LoadThemAllDialog 408 | 409 | The bounding box coordinates are not correct! 410 | 411 | Les coordonées du rectangle de délimitation sont incorrectes! 412 | 413 | 414 | 415 | Please adjust the bounding box settings. 416 | Ajustez ces coordonées SVP. 417 | 418 | 419 | Some bounding box coordinates are missing! 420 | 421 | Il manque des coordonnées au rectangle de délimitation (emprise)! 422 | 423 | 424 | 425 | Please set all bounding box coordinates. 426 | Merci de définir toutes les coordonées. 427 | 428 | 429 | No layer will match the filter! 430 | 431 | Aucune couche ne correspond a votre filtre! 432 | 433 | 434 | 435 | Select a geometry type or uncheck the Geometry type filter. 436 | Choisir un type de géometríe ou décochez le choix par type de géometríe. 437 | 438 | 439 | Select a raster type or uncheck the Raster type filter. 440 | Choisir un type de raster ou décochez le choix par type de raster. 441 | 442 | 443 | Accents were not ignored! 444 | Les caractères speciaux ne sont pas ignorés! 445 | 446 | 447 | The specified directory could not be found! 448 | 449 | Le répertoire choisi ne peut être trouvé! 450 | 451 | 452 | 453 | Please select an existing directory. 454 | Merci de choisir un répertoire existant. 455 | 456 | 457 | You have chosen to ignore accents in the alphanumeric filter, but first 458 | Vous avez choisi d'ignorer les caractères speciaux dans le filtre alphanumérique, mais au préalable 459 | 460 | 461 | you need to install the Python library 'unidecode'. 462 | vous devez installer la librarie Python 'unicode'. 463 | 464 | 465 | 466 | -------------------------------------------------------------------------------- /LoadThemAll/metadata.txt: -------------------------------------------------------------------------------- 1 | [general] 2 | name=Load Them All 3 | description=Loads files stored in a directory structure recursively, based on several filters 4 | about=QGIS plugin that recursively loads vector, raster, and point cloud layers stored in a directory structure, based on several filters. 5 | version=3.5.0-beta-2403011 6 | qgisMinimumVersion=3.0 7 | category=Layers 8 | author=Germán Carrillo (GeoTux) 9 | email=gcarrillo@linuxmail.org 10 | tags=layers,vector,raster,point cloud,toc,legend,directory,recursively,filter,load,shapefile,layer tree,geopackage,style,qml,gpkg,automatic,batch,multiple 11 | homepage=https://geotux.tuxfamily.org/en/2010/10/18/load-them-all-a-qgis-plugin/ 12 | tracker=https://github.com/gacarrillor/loadthemall/issues 13 | repository=https://github.com/gacarrillor/loadthemall 14 | icon=resources/icon.png 15 | experimental=False 16 | deprecated=False 17 | 18 | 19 | changelog=3.5-beta (20240301): 20 | ...* Support for Point Cloud Layers. 21 | ...* Support for searching layers in compressed files (optional) 22 | ......[ZIP, Gzip, 7zip, TAR, and RAR]. 23 | ...* Support .gen, .dwg, .bag, .vfk, .fgb, .fits, .gxt, .txt, .geojsonl, .geojsons, 24 | .......nlgeojson, .mdb, .pdf, .gtm, .gtz, .xtf, .xml, .ili, .mvt, .mvt.gz, .pbf, 25 | .......mbtiles, .jml, .osm, .pix, .svg, and .topojson files. 26 | ...* Unit tests! 27 | 28 | 3.4 (20231005): 29 | ...* Support for .KAP files. 30 | ...* Plugin button can be found in: 31 | ......+ 'Data source manager' toolbar. 32 | ......+ 'Layer -> Add layer' menu. 33 | ......+ 'Plugins' menu. 34 | ...* Heavy code clean-up. 35 | 36 | 3.3 (20210621): 37 | ...* Support JP2, KMZ, ERS, and SID files. 38 | ...* Use addTabifiedDockWidget to show LoadThemAll panel on top of others. 39 | ...* Add support for loading only geometryless layers (using GeometryFilter). 40 | 41 | 3.2 (20200401): 42 | ...* Support JSON files. 43 | ...* Support sublayers that share name and have different geometry types. 44 | 45 | 3.1 (20190218): 46 | ...* Apply QML styles when loading layers 47 | ...* French translation 48 | 49 | 3.0.1 (20181015): 50 | ...* Fix issue (#11) with locale not yet set. 51 | 52 | 3.0 (20180910): 53 | ...* Migration to QGIS v3.x 54 | ...* Support for GeoPackage. 55 | ...* For sublayers: 56 | ......Include parent layer name in searches. 57 | ......Include parent layer name as prefix for each sublayer in TOC. 58 | ...* Remove empty groups 59 | 60 | 2.7 (20170713): 61 | ...* New 'Date modified' filter to load files according to their latest 62 | ......modification date. 63 | 64 | 2.6 (20170703): 65 | ...* New 'Invert Alphanumeric filter' option to prepend a logic NOT to the 66 | ......whole alphanumeric expression. 67 | 68 | 2.5 (20170128): 69 | ...* Added support for vector sublayers (e.g., from GML or GPX files). 70 | 71 | 2.4 (20161111): 72 | ...* New option to sort (or reverse sort) loaded layers. Thanks to David 73 | ......Bakeman for the idea and the code! 74 | ...* Load Them All is now a QGIS dock widget. 75 | ...* New Cancel button to allow users to cancel long loading processes at 76 | ......any time. 77 | ...* Now using QGIS Layer Tree classes to deal with groups/subgroups. 78 | ...* Fixed an issue with nasty file names (e.g., those generated by 79 | ......malware) while performing os.walk. 80 | 81 | 2.3 (20150118): 82 | ...* The alphanumeric filter supports boolean operators || (or) and && (and). 83 | ......Example: The expression "Ae || Ar || As || B[ers]" will load all map 84 | .........layer that have Ae, Ar, As, Be, Br or Bs in their file names. 85 | ......Note 1: && has higher priority, i.e., "a || b && c " becomes 86 | ........."(a || b) && c" 87 | ......Note 2: Boolean operators are combined with match modes, i.e., the 88 | .........expression "a || b" with a match mode 'Starts with' becomes 89 | ........."^a || ^b" and will find all layers whose file names START WITH 90 | ........."a" or "b". 91 | ...Case insensitive option for the alphanumeric filter (Configuration tab). 92 | ...Accent insensitive option for the alphanumeric filter. This option requires 93 | ......the Python lib 'unidecode' and can be activated in the Configuration tab. 94 | ...Performance improvement in alphanumeric filter. RE patterns are compiled 95 | ......and reused. 96 | ...* New bounding box based filter for raster and vector files. The bounding 97 | ......box can be load from the current extent or specified by hand. 98 | ......Raster and vector layers are checked if their spatial extent 99 | ......intersects with the specified bbox or if they lay inside of it. 100 | ......This operation (intersects/contains) can be specified in the dialog. 101 | ...* Modified class filter structure to easily support more filters. Soeren has 102 | ......removed the combined filter class and replaced it with a filter list, 103 | ......that can handle any number of filters. It is assured in the code that 104 | ......the bounding box filter is applied after the alphanumeric and the 105 | ......geometry/raster type filter, since the check of the raster and vector 106 | ......layers extent may take a while. 107 | ...Bug fix: Similar to the bug fixed in v.2.2 related to trailing slashes, the 108 | ......same bug was occurring on Windows (e.g., "c:\folder\"). 109 | ...(*: Contribution by Sören Gebbert) 110 | 111 | 2.2 (20150108): 112 | ...Load Them All window is no longer modal. Now you can interact with other QGIS 113 | ......tools while your Load Them All window is open. 114 | ...Programmatically switching "CRS for new layers" to "useProjection" to avoid 115 | ......opening dialogs for definig the CRS manually. After loading layers, 116 | ......the option is set back to its original status. 117 | ......(Taken from http://pyqgis.blogspot.com/) 118 | ...Messages in done dialogs were adjusted to account for different situations: 119 | ......0 layers loaded, 1 layer loaded, various layers loaded. Messages now 120 | ......also include how many layers were loaded successfully out of all 121 | ......possible layers to load, e.g. "5 out of 9 layers were loaded 122 | ......successfully," which means 4 were not valid layers. 123 | ...XML removed as vector format because it causes nasty bugs and, after all, 124 | ......it's rarely used as a vector format. I recommend using GML, KML or 125 | ......others. 126 | ...Bug fix: A bug was ocurring when a layer was not valid and attempted to load. 127 | ...Bug fix: A bug was ocurring when the base path contained a trailing slash, 128 | ......e.g., "/abc/def/" The bug was related to exceeding the number of 129 | ......recursions allowed (1000), since paths compared never matched: 130 | ......"/abc/def/" vs. "/abc/def" 131 | ...Bug fix: A bug was ocurring when running the plugin for the first time. It 132 | ......was related to a QSettings value that didn't exist yet: 133 | ......"/Load_Them_All/vector/path". Line 21 of Base_LoadThemAllDialog.py 134 | ......"Error: unable to convert a QVariant of type 0 to a QMetaType of type 135 | ......10" 136 | 137 | 2.1 (20141127): 138 | ...Groups are properly nested according to the directory 139 | ......structure (contribution by David Bakeman.) 140 | 141 | 2.0 (20131220): 142 | ...Migration to QGIS 2.0 143 | 144 | 1.5 (20120328): 145 | ...GPX format correctly handled by considering track, route 146 | ......and waypoints (problem reported by Arnaud LE 147 | ......BRETON.) 148 | 149 | 1.4 (20110311): 150 | ...New option: Do not load empty vector layers. 151 | ...Now it is possible to select "All listed formats" from 152 | ......the format filter. 153 | ...New vector and raster formats added. 154 | 155 | 1.3.1 (20110307): 156 | ...Use QSettings locale instead of QLocale system locale. 157 | ...Set the icon() method in __init__.py to show the plugin 158 | ......icon in the manage window. 159 | ...Disable "Add groups" option in QGis v.1.6 since there 160 | ......are some bugs caused by nested groups addition 161 | 162 | 1.3 (20101025): 163 | ...Avoided a legendInterface bug in QGIS 1.4 related to 164 | ......setLayerVisible(). 165 | 166 | 1.2 (20101021): 167 | ...Use of the setRenderFlag method to avoid refreshing the 168 | ......canvas unnecessarily. 169 | ...A new dialog included when there are no files to load. 170 | 171 | 1.1 (20101020): 172 | ...Reported by Milena Nowotarska: 173 | ......Handle of special characters in directories and 174 | ......alphanumeric filters. 175 | ......Save settings on tab switches. 176 | 177 | 1.0 (20101018): 178 | ...First version 179 | -------------------------------------------------------------------------------- /LoadThemAll/resources/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gacarrillor/loadthemall/1c8d1421a2c0d3fe54cef50ad996f6996b152d39/LoadThemAll/resources/__init__.py -------------------------------------------------------------------------------- /LoadThemAll/resources/dir.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gacarrillor/loadthemall/1c8d1421a2c0d3fe54cef50ad996f6996b152d39/LoadThemAll/resources/dir.png -------------------------------------------------------------------------------- /LoadThemAll/resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gacarrillor/loadthemall/1c8d1421a2c0d3fe54cef50ad996f6996b152d39/LoadThemAll/resources/icon.png -------------------------------------------------------------------------------- /LoadThemAll/resources/logo_80x94.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gacarrillor/loadthemall/1c8d1421a2c0d3fe54cef50ad996f6996b152d39/LoadThemAll/resources/logo_80x94.png -------------------------------------------------------------------------------- /LoadThemAll/resources/resources.qrc: -------------------------------------------------------------------------------- 1 | 2 | 3 | logo_80x94.png 4 | dir.png 5 | icon.png 6 | 7 | 8 | -------------------------------------------------------------------------------- /LoadThemAll/ui/Ui_Base_LoadThemAll.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # Form implementation generated from reading ui file 'ui/Ui_Base_LoadThemAll.ui' 4 | # 5 | # Created by: PyQt5 UI code generator 5.15.6 6 | # 7 | # WARNING: Any manual changes made to this file will be lost when pyuic5 is 8 | # run again. Do not edit this file unless you know what you are doing. 9 | 10 | 11 | from PyQt5 import QtCore, QtGui, QtWidgets 12 | 13 | 14 | class Ui_Base_LoadThemAll(object): 15 | def setupUi(self, Base_LoadThemAll): 16 | Base_LoadThemAll.setObjectName("Base_LoadThemAll") 17 | Base_LoadThemAll.resize(375, 488) 18 | self.gridLayout = QtWidgets.QGridLayout(Base_LoadThemAll) 19 | self.gridLayout.setObjectName("gridLayout") 20 | self.btnBaseDir = QtWidgets.QPushButton(Base_LoadThemAll) 21 | self.btnBaseDir.setText("") 22 | icon = QtGui.QIcon() 23 | icon.addPixmap(QtGui.QPixmap(":/plugins/loadthemall/dir.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) 24 | self.btnBaseDir.setIcon(icon) 25 | self.btnBaseDir.setIconSize(QtCore.QSize(22, 22)) 26 | self.btnBaseDir.setObjectName("btnBaseDir") 27 | self.gridLayout.addWidget(self.btnBaseDir, 0, 0, 1, 1) 28 | self.txtBaseDir = QtWidgets.QLineEdit(Base_LoadThemAll) 29 | self.txtBaseDir.setObjectName("txtBaseDir") 30 | self.gridLayout.addWidget(self.txtBaseDir, 0, 1, 1, 1) 31 | self.label_2 = QtWidgets.QLabel(Base_LoadThemAll) 32 | self.label_2.setObjectName("label_2") 33 | self.gridLayout.addWidget(self.label_2, 1, 0, 1, 1) 34 | self.cboFormats = QtWidgets.QComboBox(Base_LoadThemAll) 35 | self.cboFormats.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContentsOnFirstShow) 36 | self.cboFormats.setObjectName("cboFormats") 37 | self.gridLayout.addWidget(self.cboFormats, 1, 1, 1, 1) 38 | spacerItem = QtWidgets.QSpacerItem(20, 15, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) 39 | self.gridLayout.addItem(spacerItem, 2, 1, 1, 1) 40 | self.groupBoxAlphanumeric = QtWidgets.QGroupBox(Base_LoadThemAll) 41 | self.groupBoxAlphanumeric.setCheckable(True) 42 | self.groupBoxAlphanumeric.setChecked(False) 43 | self.groupBoxAlphanumeric.setObjectName("groupBoxAlphanumeric") 44 | self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.groupBoxAlphanumeric) 45 | self.verticalLayout_2.setObjectName("verticalLayout_2") 46 | self.chkInvertAlphanumeric = QtWidgets.QCheckBox(self.groupBoxAlphanumeric) 47 | self.chkInvertAlphanumeric.setObjectName("chkInvertAlphanumeric") 48 | self.verticalLayout_2.addWidget(self.chkInvertAlphanumeric) 49 | self.formLayout_2 = QtWidgets.QFormLayout() 50 | self.formLayout_2.setObjectName("formLayout_2") 51 | self.label_3 = QtWidgets.QLabel(self.groupBoxAlphanumeric) 52 | self.label_3.setObjectName("label_3") 53 | self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.label_3) 54 | self.txtFilter = QtWidgets.QLineEdit(self.groupBoxAlphanumeric) 55 | self.txtFilter.setObjectName("txtFilter") 56 | self.formLayout_2.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.txtFilter) 57 | self.verticalLayout_2.addLayout(self.formLayout_2) 58 | self.splitter = QtWidgets.QSplitter(self.groupBoxAlphanumeric) 59 | self.splitter.setOrientation(QtCore.Qt.Horizontal) 60 | self.splitter.setObjectName("splitter") 61 | self.radStarts = QtWidgets.QRadioButton(self.splitter) 62 | self.radStarts.setObjectName("radStarts") 63 | self.radAny = QtWidgets.QRadioButton(self.splitter) 64 | self.radAny.setChecked(True) 65 | self.radAny.setObjectName("radAny") 66 | self.radEnds = QtWidgets.QRadioButton(self.splitter) 67 | self.radEnds.setObjectName("radEnds") 68 | self.verticalLayout_2.addWidget(self.splitter) 69 | self.gridLayout.addWidget(self.groupBoxAlphanumeric, 3, 0, 1, 2) 70 | self.groupBoxBoundingBox = QtWidgets.QGroupBox(Base_LoadThemAll) 71 | self.groupBoxBoundingBox.setCheckable(True) 72 | self.groupBoxBoundingBox.setChecked(False) 73 | self.groupBoxBoundingBox.setObjectName("groupBoxBoundingBox") 74 | self.gridLayout_2 = QtWidgets.QGridLayout(self.groupBoxBoundingBox) 75 | self.gridLayout_2.setObjectName("gridLayout_2") 76 | self.verticalLayout = QtWidgets.QVBoxLayout() 77 | self.verticalLayout.setObjectName("verticalLayout") 78 | self.horizontalLayout_2 = QtWidgets.QHBoxLayout() 79 | self.horizontalLayout_2.setObjectName("horizontalLayout_2") 80 | spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) 81 | self.horizontalLayout_2.addItem(spacerItem1) 82 | self.txtYMax = QtWidgets.QLineEdit(self.groupBoxBoundingBox) 83 | self.txtYMax.setMinimumSize(QtCore.QSize(150, 0)) 84 | self.txtYMax.setMaximumSize(QtCore.QSize(16777215, 16777215)) 85 | font = QtGui.QFont() 86 | font.setPointSize(9) 87 | self.txtYMax.setFont(font) 88 | self.txtYMax.setObjectName("txtYMax") 89 | self.horizontalLayout_2.addWidget(self.txtYMax) 90 | spacerItem2 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) 91 | self.horizontalLayout_2.addItem(spacerItem2) 92 | self.verticalLayout.addLayout(self.horizontalLayout_2) 93 | self.horizontalLayout = QtWidgets.QHBoxLayout() 94 | self.horizontalLayout.setObjectName("horizontalLayout") 95 | self.txtXMin = QtWidgets.QLineEdit(self.groupBoxBoundingBox) 96 | self.txtXMin.setMinimumSize(QtCore.QSize(150, 0)) 97 | self.txtXMin.setMaximumSize(QtCore.QSize(150, 16777215)) 98 | font = QtGui.QFont() 99 | font.setPointSize(9) 100 | self.txtXMin.setFont(font) 101 | self.txtXMin.setObjectName("txtXMin") 102 | self.horizontalLayout.addWidget(self.txtXMin) 103 | self.txtXMax = QtWidgets.QLineEdit(self.groupBoxBoundingBox) 104 | self.txtXMax.setMinimumSize(QtCore.QSize(150, 0)) 105 | self.txtXMax.setMaximumSize(QtCore.QSize(150, 16777215)) 106 | font = QtGui.QFont() 107 | font.setPointSize(9) 108 | self.txtXMax.setFont(font) 109 | self.txtXMax.setObjectName("txtXMax") 110 | self.horizontalLayout.addWidget(self.txtXMax) 111 | self.verticalLayout.addLayout(self.horizontalLayout) 112 | self.horizontalLayout_3 = QtWidgets.QHBoxLayout() 113 | self.horizontalLayout_3.setObjectName("horizontalLayout_3") 114 | spacerItem3 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) 115 | self.horizontalLayout_3.addItem(spacerItem3) 116 | self.txtYMin = QtWidgets.QLineEdit(self.groupBoxBoundingBox) 117 | self.txtYMin.setMinimumSize(QtCore.QSize(150, 0)) 118 | self.txtYMin.setMaximumSize(QtCore.QSize(16777215, 16777215)) 119 | font = QtGui.QFont() 120 | font.setPointSize(9) 121 | self.txtYMin.setFont(font) 122 | self.txtYMin.setLayoutDirection(QtCore.Qt.LeftToRight) 123 | self.txtYMin.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) 124 | self.txtYMin.setObjectName("txtYMin") 125 | self.horizontalLayout_3.addWidget(self.txtYMin) 126 | spacerItem4 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum) 127 | self.horizontalLayout_3.addItem(spacerItem4) 128 | self.verticalLayout.addLayout(self.horizontalLayout_3) 129 | self.splitter_2 = QtWidgets.QSplitter(self.groupBoxBoundingBox) 130 | self.splitter_2.setOrientation(QtCore.Qt.Horizontal) 131 | self.splitter_2.setObjectName("splitter_2") 132 | self.radContains = QtWidgets.QRadioButton(self.splitter_2) 133 | self.radContains.setChecked(True) 134 | self.radContains.setObjectName("radContains") 135 | self.radIntersects = QtWidgets.QRadioButton(self.splitter_2) 136 | self.radIntersects.setObjectName("radIntersects") 137 | self.btnLoadExtent = QtWidgets.QPushButton(self.splitter_2) 138 | self.btnLoadExtent.setObjectName("btnLoadExtent") 139 | self.verticalLayout.addWidget(self.splitter_2) 140 | self.gridLayout_2.addLayout(self.verticalLayout, 0, 0, 1, 1) 141 | self.gridLayout.addWidget(self.groupBoxBoundingBox, 4, 0, 1, 2) 142 | self.groupBoxDateModified = QtWidgets.QGroupBox(Base_LoadThemAll) 143 | self.groupBoxDateModified.setCheckable(True) 144 | self.groupBoxDateModified.setChecked(False) 145 | self.groupBoxDateModified.setObjectName("groupBoxDateModified") 146 | self.horizontalLayout_4 = QtWidgets.QHBoxLayout(self.groupBoxDateModified) 147 | self.horizontalLayout_4.setObjectName("horizontalLayout_4") 148 | self.cboDateComparison = QtWidgets.QComboBox(self.groupBoxDateModified) 149 | self.cboDateComparison.setMaximumSize(QtCore.QSize(100, 16777215)) 150 | self.cboDateComparison.setObjectName("cboDateComparison") 151 | self.horizontalLayout_4.addWidget(self.cboDateComparison) 152 | self.dtDateTime = QtWidgets.QDateTimeEdit(self.groupBoxDateModified) 153 | self.dtDateTime.setButtonSymbols(QtWidgets.QAbstractSpinBox.UpDownArrows) 154 | self.dtDateTime.setAccelerated(True) 155 | self.dtDateTime.setDateTime(QtCore.QDateTime(QtCore.QDate(2017, 1, 1), QtCore.QTime(0, 0, 0))) 156 | self.dtDateTime.setCurrentSection(QtWidgets.QDateTimeEdit.DaySection) 157 | self.dtDateTime.setCalendarPopup(True) 158 | self.dtDateTime.setObjectName("dtDateTime") 159 | self.horizontalLayout_4.addWidget(self.dtDateTime) 160 | self.gridLayout.addWidget(self.groupBoxDateModified, 5, 0, 1, 2) 161 | 162 | self.retranslateUi(Base_LoadThemAll) 163 | QtCore.QMetaObject.connectSlotsByName(Base_LoadThemAll) 164 | 165 | def retranslateUi(self, Base_LoadThemAll): 166 | _translate = QtCore.QCoreApplication.translate 167 | Base_LoadThemAll.setWindowTitle(_translate("Base_LoadThemAll", "Dialog")) 168 | self.btnBaseDir.setToolTip(_translate("Base_LoadThemAll", "Select a base directory")) 169 | self.label_2.setText(_translate("Base_LoadThemAll", "Format ")) 170 | self.groupBoxAlphanumeric.setTitle(_translate("Base_LoadThemAll", "Alphanumeric filter")) 171 | self.chkInvertAlphanumeric.setText(_translate("Base_LoadThemAll", "Invert filter (i.e., prepend a logic NOT)")) 172 | self.label_3.setText(_translate("Base_LoadThemAll", "Filter string")) 173 | self.radStarts.setText(_translate("Base_LoadThemAll", "Starts with")) 174 | self.radAny.setText(_translate("Base_LoadThemAll", "In any position")) 175 | self.radEnds.setText(_translate("Base_LoadThemAll", "Ends with")) 176 | self.groupBoxBoundingBox.setTitle(_translate("Base_LoadThemAll", "Bounding box filter")) 177 | self.txtYMax.setPlaceholderText(_translate("Base_LoadThemAll", "North")) 178 | self.txtXMin.setPlaceholderText(_translate("Base_LoadThemAll", "West")) 179 | self.txtXMax.setPlaceholderText(_translate("Base_LoadThemAll", "East")) 180 | self.txtYMin.setPlaceholderText(_translate("Base_LoadThemAll", "South")) 181 | self.radContains.setText(_translate("Base_LoadThemAll", "Contains")) 182 | self.radIntersects.setText(_translate("Base_LoadThemAll", "Intersects")) 183 | self.btnLoadExtent.setText(_translate("Base_LoadThemAll", "Map extent")) 184 | self.groupBoxDateModified.setTitle(_translate("Base_LoadThemAll", "Date modified filter")) 185 | self.dtDateTime.setDisplayFormat(_translate("Base_LoadThemAll", "ddd dd MMM yyyy hh:mm AP")) 186 | -------------------------------------------------------------------------------- /LoadThemAll/ui/Ui_Base_LoadThemAll.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | Base_LoadThemAll 4 | 5 | 6 | 7 | 0 8 | 0 9 | 375 10 | 488 11 | 12 | 13 | 14 | Dialog 15 | 16 | 17 | 18 | 19 | 20 | Select a base directory 21 | 22 | 23 | 24 | 25 | 26 | 27 | :/plugins/loadthemall/dir.png:/plugins/loadthemall/dir.png 28 | 29 | 30 | 31 | 22 32 | 22 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | Format 44 | 45 | 46 | 47 | 48 | 49 | 50 | QComboBox::AdjustToContentsOnFirstShow 51 | 52 | 53 | 54 | 55 | 56 | 57 | Qt::Vertical 58 | 59 | 60 | QSizePolicy::Fixed 61 | 62 | 63 | 64 | 20 65 | 15 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | Alphanumeric filter 74 | 75 | 76 | true 77 | 78 | 79 | false 80 | 81 | 82 | 83 | 84 | 85 | Invert filter (i.e., prepend a logic NOT) 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | Filter string 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | Qt::Horizontal 107 | 108 | 109 | 110 | Starts with 111 | 112 | 113 | 114 | 115 | In any position 116 | 117 | 118 | true 119 | 120 | 121 | 122 | 123 | Ends with 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | Bounding box filter 135 | 136 | 137 | true 138 | 139 | 140 | false 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | Qt::Horizontal 151 | 152 | 153 | 154 | 40 155 | 20 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 150 165 | 0 166 | 167 | 168 | 169 | 170 | 16777215 171 | 16777215 172 | 173 | 174 | 175 | 176 | 9 177 | 178 | 179 | 180 | North 181 | 182 | 183 | 184 | 185 | 186 | 187 | Qt::Horizontal 188 | 189 | 190 | 191 | 40 192 | 20 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 150 206 | 0 207 | 208 | 209 | 210 | 211 | 150 212 | 16777215 213 | 214 | 215 | 216 | 217 | 9 218 | 219 | 220 | 221 | West 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 150 230 | 0 231 | 232 | 233 | 234 | 235 | 150 236 | 16777215 237 | 238 | 239 | 240 | 241 | 9 242 | 243 | 244 | 245 | East 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | Qt::Horizontal 257 | 258 | 259 | 260 | 40 261 | 20 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 150 271 | 0 272 | 273 | 274 | 275 | 276 | 16777215 277 | 16777215 278 | 279 | 280 | 281 | 282 | 9 283 | 284 | 285 | 286 | Qt::LeftToRight 287 | 288 | 289 | Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter 290 | 291 | 292 | South 293 | 294 | 295 | 296 | 297 | 298 | 299 | Qt::Horizontal 300 | 301 | 302 | 303 | 40 304 | 20 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | Qt::Horizontal 315 | 316 | 317 | 318 | Contains 319 | 320 | 321 | true 322 | 323 | 324 | 325 | 326 | Intersects 327 | 328 | 329 | 330 | 331 | Map extent 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | Date modified filter 345 | 346 | 347 | true 348 | 349 | 350 | false 351 | 352 | 353 | 354 | 355 | 356 | 357 | 100 358 | 16777215 359 | 360 | 361 | 362 | 363 | 364 | 365 | 366 | QAbstractSpinBox::UpDownArrows 367 | 368 | 369 | true 370 | 371 | 372 | 373 | 0 374 | 0 375 | 0 376 | 2017 377 | 1 378 | 1 379 | 380 | 381 | 382 | QDateTimeEdit::DaySection 383 | 384 | 385 | ddd dd MMM yyyy hh:mm AP 386 | 387 | 388 | true 389 | 390 | 391 | 392 | 393 | 394 | 395 | 396 | 397 | 398 | 399 | -------------------------------------------------------------------------------- /LoadThemAll/ui/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gacarrillor/loadthemall/1c8d1421a2c0d3fe54cef50ad996f6996b152d39/LoadThemAll/ui/__init__.py -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Unit tests](https://github.com/gacarrillor/loadthemall/actions/workflows/main.yml/badge.svg) 2 | [![Release](https://img.shields.io/github/v/release/gacarrillor/loadthemall.svg)](https://github.com/gacarrillor/loadthemall/releases) 3 | 4 | 5 | # Load Them All 6 | 7 | QGIS plugin that recursively loads **vector**, **raster**, and **point cloud** (🆕✨) layers stored in a directory structure, based on several filters. 8 | 9 | **License**: This plugin is distributed under the [GNU GPL v2 license](https://github.com/gacarrillor/loadthemall/blob/master/LICENSE). 10 | 11 | 📹 [Screencasts](#-screencasts)
12 | 🚦 [Available filters](#-available-filters)
13 | ⚙️ [Configuration](#%EF%B8%8F-configuration)
14 | 🗄️ [Supported file formats](#%EF%B8%8F-supported-flle-formats)
15 | 🔎 [Where to find the plugin in QGIS](#-where-to-find-the-plugin-in-qgis)
16 | 💻 [For developers](#-for-developers)
17 | 🙋‍♂️ [Code contributors](#%EF%B8%8F-code-contributors)
18 | ℹ️ [More info](#%E2%84%B9%EF%B8%8F-more-info) 19 | 20 | ----------- 21 | ### 📹 Screencasts 22 | 23 | ![Load Them All 3][2] 24 | 25 | ![Load Them All][1] 26 | 27 | ![Load Them All][3] 28 | 29 | 30 | 31 | ### 🚦 Available filters 32 | 33 | Load Them All offers you several filters to properly choose the layers to be loaded. These are: 34 | 35 | * **Alphanumeric**: 36 | 37 | Enter a filter text and choose among the modes `Start with`, `Any position`, or `Ends with` to find files by name. 38 | You can use the logical operators `||` (or) and `&&` (and), and even combine them. `&&` has more priority, so the expression `a || b && c` becomes `(a || b) && c`. 39 | 40 | Additionally, you can invert the Alphanumeric filter, i.e., prepend a logic NOT to it, allowing you to invert the original filter result. 41 | 42 | * **Bounding box**: 43 | 44 | Enter coordinates by hand or get the current map extent. Choose the spatial filter: `Contains` or `Intersects`. 45 | 46 | * **Date modified**: 47 | 48 | Filter files by their latest modification date, using comparisons like `before`, `after` and `exact date`. 49 | 50 | * **Geometry type**: 51 | 52 | Choose which geometry type you want to load: `Point`, `Line`, or `Polygon`. Uncheck all geometry types for loading only geometryless layers (i.e., alphanumeric tables). 53 | 54 | * **Raster type**: 55 | 56 | Choose which raster type you want to load: `Gray or undefined`, `Palette`, `Multiband`, or `Color Layer`. 57 | 58 | 59 | ### ⚙️ Configuration 60 | 61 | There are several options for you to configure how layers should be loaded to QGIS: 62 | 63 | * **Groups**: Whether or not to create groups based on directories' names. When groups are created, they reflect the directory structure, i.e., groups are nested if necessary. 64 | 65 | * **Also search inside compressed files** 📦: Enable searching layers inside compressed files (ZIP, Gzip, 7zip, TAR, and RAR). 🆕✨ 66 | 67 | * **Turn off layers**: Make loaded layers invisible (it improves performance). 68 | 69 | * **Do not load empty layers**. 70 | 71 | * **Sort (or reverse sort) loaded layers by name**. 72 | 73 | * **Ignore case in the alphanumeric filter**: Make the alphanumeric filter case insensitive. 74 | 75 | * **Ignore accents in the alphanumeric filter**: You need the `unidecode` module for this option to work (you probably need to install it because it doesn't come with the standard Python installation). If enabled, an alphanumeric filter like 'arbol' will also match 'árbol'. 76 | 77 | * **Sublayers**: 78 | The following two options can work independently from each other. 79 | * **Include parent in search**: Make alphanumeric filters work with the parent name prepended. If enabled, an alphanumeric filter like 'Starts with: rivers' won't match the sublayer rivers, because the parent layer name is taken into account (e.g., 'parent_layer_name rivers'). 80 | * **Include parent in loaded sublayers**: Prepend the parent layer name in all its sublayers. 81 | 82 | * **Apply group style to layers**: Whether or not to load QML style (group_name.qml) for all layers inside a group. The QML file must have the same name as the parent folder and must be found in the layer folder. 83 | 84 | 85 | ### 🗄️ Supported flle formats 86 | 87 | The plugin supports the following file extensions: 88 | 89 | * Vectors 90 | * Arc/Info ASCII Coverage (*.e00) 91 | * Arc/Info Generate (*.gen) 🆕 92 | * AutoCAD Driver (*.dwg) 🆕 93 | * AutoCAD DXF (*.dxf) 94 | * Bathymetry Attributed Grid (*.bag) 🆕 95 | * Comma Separated Value (*.csv) 96 | * Czech Cadastral Exchange Data Format (*.vfk) 🆕 97 | * ESRI Shapefile (*.shp *.shz 🆕 *.shp.zip 🆕) 98 | * FlatGeobuf (*.fgb) 🆕 99 | * Flexible Image Transport System (*.fits) 🆕 100 | * Geoconcept (*.gxt *.txt) 🆕 101 | * GeoJSON (*.geojson) 102 | * GeoJSON Newline Delimited JSON (*.geojsonl *.geojsons *.nlgeojson *.json) 🆕 103 | * Geography Markup Language [GML] (*.gml) 104 | * Geomedia .mdb (*.mdb) 🆕 105 | * GeoPackage (*.gpkg) 106 | * GeoRSS (*.xml) 🆕 107 | * Geospatial PDF (*.pdf) 🆕 108 | * GMT ASCII Vectors (*.gmt) 109 | * GPS eXchange Format [GPX] (*.gpx) 110 | * GPSTrackMaker (*.gtm *.gtz) 🆕 111 | * INTERLIS 2 (*.xtf *.xml *.ili) 🆕 112 | * JSON (*.json) 113 | * Keyhole Markup Language [KML] (*.kml *.kmz) 114 | * Mapbox Vector Tiles (*.mvt *.mvt.gz *.pbf) 🆕 115 | * Mapinfo File (*.mif *.tab) 116 | * MBTiles (*.mbtiles) 🆕 117 | * Microstation DGN (*.dgn) 118 | * OpenJUMP JML (*.jml) 🆕 119 | * OpenStreetMap (*.osm *.pbf) 🆕 120 | * PCI Geomatics Database File (*.pix) 🆕 121 | * Scalable Vector Graphics (*.svg) 🆕 122 | * SQLite/SpatiaLite (*.sqlite *.db 🆕 *.sqlite3 🆕 *.db3 🆕 *.s3db 🆕 *.sl3 🆕) 123 | * TopoJSON (*.json *.topojson) 🆕 124 | * Virtual Datasource [VRT] (*.vrt *.ovf 🆕) 125 | 126 | 127 | * Rasters 128 | * Virtual Raster (*.vrt) 129 | * GeoTIFF (*.tif, *.tiff) 130 | * Erdas Imagine Images (*.img) 131 | * Erdas Compressed Wavelets (*.ecw) 132 | * ERMapper .ers Labelled (*.ers) 133 | * DTED Elevation Raster (*.dt) 134 | * Arc/Info ASCII Grid (*.asc) 135 | * Portable Network Graphics (*.png) 136 | * JPEG JFIF (*.jpg, *.jpeg) 137 | * JPEG2000 (*.jp2) 138 | * Graphics Interchange Format (*.gif) 139 | * X11 PixMap Format (*.xpm) 140 | * Bitmap image file (*.bmp) 141 | * PCIDSK Database File (*.pix) 142 | * PCRaster Raster File (*.map) 143 | * ILWIS Raster Map (*.mpr, *.mpl) 144 | * SRTMHGT File Format (*.hgt) 145 | * GMT NetCDF Grid Format (*.nc) 146 | * GRIdded Binary (*.grb) 147 | * Idrisi Raster A.1 (*.rst) 148 | * Golden Software ASCII Grid (*.grd) 149 | * R Object Data Store (*.rda) 150 | * Vexcel MFF Raster (*.hdr) 151 | * USGS Optional ASCII DEM (*.dem) 152 | * Magellan topo (*.blx) 153 | * Rasterlite (*.sqlite) 154 | * SAGA GIS Binary Grid (*.sdat) 155 | * Multi-resolution Seamless Image Database (*.sid) 156 | 157 | 158 | * Point Clouds 🆕✨ 159 | * COPC Point Clouds (*.copc.laz) 160 | * Entwine Point Clouds (ept.json) 161 | * PDAL Point Clouds (*.bpf, *.e57, *.las, *.laz) 162 | * Virtual Point Clouds (*.vpc) 163 | 164 | 165 | ### 🔎 Where to find the plugin in QGIS 166 | 167 | After installation, Load Them All can be found in these places: 168 | 169 | + `Data source manager` toolbar: 170 | 171 | ![image](https://user-images.githubusercontent.com/652785/157999846-5e355b84-d4c7-4005-b347-393f2ac3a338.png) 172 | 173 | + `Layer -> Add layer` menu: 174 | 175 | ![image](https://user-images.githubusercontent.com/652785/157999893-31261a30-2c16-4c66-a7ea-de61ae79d6bd.png) 176 | 177 | + `Plugins` menu: 178 | 179 | ![image](https://user-images.githubusercontent.com/652785/157999941-b25b70ec-aa65-4631-8d87-91ecece7f460.png) 180 | 181 | 182 | ### 💻 For developers 183 | 184 | + To build the plugin run this command on the `loadthemall/LoadThemAll` directory: 185 | 186 | ```$ make clean;make;make build``` 187 | 188 | After that, you'll get the plugin's ZIP file in `/tmp/`. 189 | 190 | + To run Unit Tests locally, first install Docker, then set these 2 environment variables: 191 | 192 | export GITHUB_WORKSPACE=/path/to/loadthemall/ 193 | export QGIS_TEST_VERSION="final-3_28_13" 194 | 195 | Build the docker image (from plugin repo's root folder): 196 | 197 | docker build . -f .docker/Dockerfile -t docker_qgis --build-arg QGIS_TEST_VERSION=$QGIS_TEST_VERSION 198 | 199 | And finally, run the docker command: 200 | 201 | docker run --rm -v $GITHUB_WORKSPACE:/usr/src docker_qgis 202 | 203 | 204 | ### 🙋‍♂️ Code contributors 205 | 206 | * David Bakeman (v2.1 and v2.4) 207 | * Sören Gebbert (v2.3) 208 | * Jean Hemmi (v3.1 and French translation) 209 | * Guillaume Lostis (v3.3) 210 | * Jan Caha (v3.5) 211 | 212 | 213 | ### ℹ️ More info 214 | 215 | + More info about LoadThemAll at http://geotux.tuxfamily.org/index.php/en/geo-blogs/item/264-plugin-load-them-all-para-quantum-gis 216 | 217 | + See the changelog at https://github.com/gacarrillor/loadthemall/blob/master/changelog.txt 218 | 219 | 220 | [1]: http://downloads.tuxfamily.org/tuxgis/geoblogs/plugin_LoadThemAll/imgs/LoadThemAll_v2_4.png 221 | [2]: http://downloads.tuxfamily.org/tuxgis/geoblogs/plugin_LoadThemAll/imgs/load_them_all_v3_0.gif 222 | [3]: http://downloads.tuxfamily.org/tuxgis/geoblogs/plugin_LoadThemAll/imgs/LoadThemAll_ApplyGroupStyles.gif 223 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | Changes: 2 | 3 | 3.5-beta (20231026): 4 | * Support for Point Cloud Layers. 5 | * Support for searching layers in compressed files (optional) 6 | [ZIP, Gzip, 7zip, TAR, and RAR]. 7 | * Support .gen, .dwg, .bag, .vfk, .fgb, .fits, .gxt, .txt, .geojsonl, .geojsons, 8 | .nlgeojson, .mdb, .pdf, .gtm, .gtz, .xtf, .xml, .ili, .mvt, .mvt.gz, .pbf, 9 | .mbtiles, .jml, .osm, .pix, .svg, and .topojson files. 10 | 11 | 3.4 (20231005): 12 | * Support for .KAP files. 13 | * Plugin button can be found in: 14 | + 'Data source manager' toolbar. 15 | + 'Layer -> Add layer' menu. 16 | + 'Plugins' menu. 17 | * Heavy code clean-up. 18 | 19 | 3.3 (20210621): 20 | * Support JP2, KMZ, ERS, and SID files. 21 | * Use addTabifiedDockWidget to show LoadThemAll panel on top of others. 22 | * Add support for loading only geometryless layers (using GeometryFilter). 23 | 24 | 3.2 (20200401): 25 | * Support JSON files. 26 | * Support sublayers that share name and have different geometry types. 27 | 28 | 3.1 (20190218): 29 | * Apply QML styles when loading layers 30 | * French translation 31 | 32 | 3.0.1 (20181015): 33 | * Fix issue (#11) with locale not yet set. 34 | 35 | 3.0 (20180910): 36 | * Support QGIS v3.x 37 | * Support for GeoPackage. 38 | * For sublayers: 39 | Include parent layer name in searches. 40 | Include parent layer name as prefix for each sublayer in TOC. 41 | * Remove empty groups 42 | 43 | 2.7 (20170713): 44 | * New 'Date modified' filter to load files according to their latest 45 | modification date. 46 | 47 | 2.6 (20170703): 48 | * New 'Invert Alphanumeric filter' option to prepend a logic NOT to the whole 49 | alphanumeric expression. 50 | 51 | 2.5 (20170128): 52 | * Added support for vector sublayers (e.g., from GML or GPX files). 53 | 54 | 2.4 (20161111): 55 | * New option to sort (or reverse sort) loaded layers. Thanks to David Bakeman for 56 | the idea and the code! 57 | * Load Them All is now a QGIS dock widget. 58 | * New Cancel button to allow users to cancel long loading processes at any time. 59 | * Now using QGIS Layer Tree classes to deal with groups/subgroups. 60 | * Fixed an issue with nasty file names (e.g., those generated by malware) while 61 | performing os.walk. 62 | 63 | 2.3 (20150118): 64 | * The alphanumeric filter supports boolean operators || (or) and && (and). 65 | Example: The expression "Ae || Ar || As || B[ers]" will load all map layer 66 | that have Ae, Ar, As, Be, Br or Bs in their file names. 67 | Note 1: && has higher priority, i.e., "a || b && c " becomes "(a || b) && c" 68 | Note 2: Boolean operators are combined with match modes, i.e., the expression 69 | "a || b" with a match mode 'Starts with' becomes "^a || ^b" and will find 70 | all layers whose file names START WITH "a" or "b". 71 | Case insensitive option for the alphanumeric filter (Configuration tab). 72 | Accent insensitive option for the alphanumeric filter. This option requires the 73 | Python lib 'unidecode' and can be activated in the Configuration tab. 74 | Performance improvement in alphanumeric filter. RE patterns are compiled and reused. 75 | * New bounding box based filter for raster and vector files. The 76 | bounding box can be load from the current extent or specified by hand. 77 | Raster and vector layers are checked if their spatial extent intersects 78 | with the specified bounding box or if they lay inside of it. 79 | This operation (intersects/contains) can be specified in the dialog. 80 | * Modified class filter structure to easily support more filters. I (Soeren) have 81 | removed the combined filter class and replaced it with a filter list, 82 | that can handle any number of filters. It is assured in the code that 83 | the bounding box filter is applied after the alphanumeric and the 84 | geometry/raster type filter, since the check of the raster and vector 85 | layers extent may take a while. 86 | Bug fix: Similar to the bug fixed in v.2.2 related to trailing slashes, the same bug 87 | was occurring on Windows with backslashes (e.g., "c:\folder\"). 88 | (*: Contribution by Sören Gebbert) 89 | 90 | 2.2 (20150108): 91 | Load Them All window is no longer modal. Now you can interact with other QGIS tools 92 | while your Load Them All window is open. 93 | Programmatically switching "CRS for new layers" to "useProjection" to avoid opening 94 | dialogs for definig the CRS manually. After loading layers, the option is set 95 | back to its original status. (Taken from http://pyqgis.blogspot.com/) 96 | Messages in done dialogs were adjusted to account for different situations: 0 layers 97 | loaded, 1 layer loaded, various layers loaded. Messages now also include how many 98 | layers were loaded successfully out of all possible layers to load, e.g. 99 | "5 out of 9 layers were loaded successfully," which means 4 were not valid layers. 100 | XML removed as vector format because it causes nasty bugs and, after all, it's 101 | rarely used as a vector format (at least by me :D.) I recommend using GML, KML 102 | or others. 103 | Bug fix: A bug was ocurring when a layer was not valid and attempted to load. 104 | Bug fix: A bug was ocurring when the base path contained a trailing slash, e.g., 105 | "/abc/def/" The bug was related to exceeding the number of recursions allowed 106 | (1000), since paths compared never matched: "/abc/def/" vs. "/abc/def" 107 | Bug fix: A bug was ocurring when running the plugin for the first time. It was 108 | related to a QSettings value that didn't exist yet: 109 | "/Load_Them_All/vector/path". Line 21 of Base_LoadThemAllDialog.py 110 | "Error: unable to convert a QVariant of type 0 to a QMetaType of type 10" 111 | 112 | 2.1 (20141127): 113 | Groups are properly nested according to the directory structure (contribution by 114 | David Bakeman.) 115 | 116 | 2.0 (20131220): 117 | Migration to QGIS 2.0 118 | 119 | 1.5 (20120328): 120 | GPX format correctly handled by considering track, route and waypoints (problem 121 | reported by Arnaud LE BRETON). 122 | 123 | 1.4 (20110311): 124 | New option: Do not load empty vector layers. 125 | Now it is possible to select "All listed formats" from the format filter. 126 | New vector and raster formats added. 127 | 128 | 1.3.1 (20110307): 129 | Use QSettings locale instead of QLocale system locale. 130 | Set the icon() method in __init__.py to show the plugin icon in the manage window. 131 | Disable "Add groups" option in QGis v.1.6 since there are some bugs caused by 132 | nested groups addition 133 | 134 | 1.3 (20101025): 135 | Avoided a legendInterface bug in QGIS 1.4 related to setLayerVisible(). 136 | 137 | 1.2 (20101021): 138 | Use of the setRenderFlag method to avoid refreshing the canvas unnecessarily. 139 | A new dialog included when there are no files to load. 140 | 141 | 1.1 (20101020): 142 | Reported by Milena Nowotarska: 143 | Handle of special characters in directories and alphanumeric filters. 144 | Save settings on tab switches. 145 | 146 | 1.0 (20101018): 147 | First version 148 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | nose2 2 | -------------------------------------------------------------------------------- /run-docker-tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | pushd /usr/src/ 5 | xvfb-run nose2-3 6 | popd 7 | -------------------------------------------------------------------------------- /tests/test_compressed_layers.py: -------------------------------------------------------------------------------- 1 | import nose2 2 | 3 | from qgis.core import (QgsApplication, 4 | QgsProject) 5 | from qgis.testing import unittest, start_app 6 | from qgis.testing.mocked import get_iface 7 | 8 | from LoadThemAll.LoadThemAll import LoadThemAll 9 | from LoadThemAll.core.Enums import EnumLoadThemAllResult 10 | from LoadThemAll.core.Filter import (FilterList, 11 | AlphanumericFilter) 12 | from LoadThemAll.core.LoadFiles import (LoadVectors, 13 | LoadRasters) 14 | from tests.utils import get_configuration 15 | 16 | start_app() 17 | 18 | 19 | class TestCompressedLayers(unittest.TestCase): 20 | 21 | @classmethod 22 | def setUpClass(cls): 23 | print('\nINFO: Set up test_compressed_layers') 24 | cls.plugin = LoadThemAll(get_iface(), with_gui=False) 25 | cls.project = QgsProject.instance() 26 | 27 | def test_compressed_shp(self): 28 | configuration = get_configuration() 29 | configuration.base_dir = "/QGIS/tests/testdata/zip/" 30 | configuration.extension = [".shp"] 31 | configuration.b_search_in_compressed_files = True 32 | 33 | # Set filters for next session 34 | filter_list = FilterList() 35 | filter = AlphanumericFilter('EndsWith', 'points', configuration) 36 | filter_list.addFilter(filter) 37 | 38 | # Load layers 39 | loader = LoadVectors(self.plugin.iface, configuration) 40 | self.assertEqual(self.project.count(), 0) 41 | loader.filterList = filter_list 42 | res = loader.loadLayers() 43 | self.assertEqual(res.result, EnumLoadThemAllResult.SUCCESS) 44 | self.assertEqual(res.layers_found, 4) 45 | self.assertEqual(res.layers_loaded, 4) 46 | self.assertTrue(self.project.count() > 0) 47 | 48 | # Check loaded layer 49 | #print([l.source() for l in self.project.mapLayers().values()]) 50 | layers = self.project.mapLayersByName("points") 51 | self.assertEqual(len(layers), 4) 52 | self.assertTrue(layers[0].isValid()) 53 | 54 | def test_compressed_gz_geojson(self): 55 | configuration = get_configuration() 56 | configuration.base_dir = "/QGIS/tests/testdata/zip/" 57 | configuration.extension = [".geojson"] 58 | configuration.b_search_in_compressed_files = True 59 | 60 | # Set filters for next session 61 | filter_list = FilterList() 62 | filter = AlphanumericFilter('EndsWith', 'points3', configuration) 63 | filter_list.addFilter(filter) 64 | 65 | # Load 1 layer 66 | loader = LoadVectors(self.plugin.iface, configuration) 67 | self.assertEqual(self.project.count(), 0) 68 | loader.filterList = filter_list 69 | res = loader.loadLayers() 70 | self.assertEqual(res.result, EnumLoadThemAllResult.SUCCESS) 71 | self.assertEqual(res.layers_found, 1) 72 | self.assertEqual(res.layers_loaded, 1) 73 | self.assertEqual(self.project.count(), 1) 74 | 75 | # Check loaded layer 76 | #print([l.source() for l in self.project.mapLayers().values()]) 77 | layers = self.project.mapLayersByName("points3.geojson") 78 | self.assertTrue(bool(layers)) 79 | self.assertTrue(layers[0].isValid()) 80 | 81 | def test_compressed_gz_geojson_2(self): 82 | configuration = get_configuration() 83 | configuration.base_dir = "/QGIS/tests/testdata/zip/" 84 | configuration.extension = [".geojson"] 85 | configuration.b_search_in_compressed_files = True 86 | 87 | # Set filters for next session 88 | filter_list = FilterList() 89 | filter = AlphanumericFilter('EndsWith', 'points', configuration) 90 | filter_list.addFilter(filter) 91 | 92 | # Load 1 layer 93 | loader = LoadVectors(self.plugin.iface, configuration) 94 | self.assertEqual(self.project.count(), 0) 95 | loader.filterList = filter_list 96 | res = loader.loadLayers() 97 | self.assertEqual(res.result, EnumLoadThemAllResult.SUCCESS) 98 | self.assertEqual(res.layers_found, 2) 99 | self.assertEqual(res.layers_loaded, 2) 100 | self.assertEqual(self.project.count(), 2) 101 | 102 | # Check loaded layer 103 | #print([l.source() for l in self.project.mapLayers().values()]) 104 | layers = self.project.mapLayersByName("points") 105 | self.assertTrue(bool(layers)) 106 | self.assertTrue(layers[0].isValid()) 107 | 108 | 109 | def test_compressed_tif(self): 110 | configuration = get_configuration() 111 | configuration.base_dir = "/QGIS/tests/testdata/zip/" 112 | configuration.extension = [".tif"] 113 | configuration.b_search_in_compressed_files = True 114 | 115 | # Set filters for next session 116 | filter_list = FilterList() 117 | filter = AlphanumericFilter('StartsWith', 'landsat_b2', configuration) 118 | filter_list.addFilter(filter) 119 | 120 | # Load 2 layers 121 | loader = LoadRasters(self.plugin.iface, configuration) 122 | self.assertEqual(self.project.count(), 0) 123 | loader.filterList = filter_list 124 | res = loader.loadLayers() 125 | self.assertEqual(res.result, EnumLoadThemAllResult.SUCCESS) 126 | self.assertEqual(res.layers_found, 2) 127 | self.assertEqual(res.layers_loaded, 2) 128 | self.assertTrue(self.project.count() > 0) 129 | 130 | # Check loaded layers 131 | #print(self.project.mapLayers()) 132 | layers = self.project.mapLayersByName("landsat_b2") 133 | self.assertTrue(bool(layers)) 134 | self.assertTrue(layers[0].isValid()) 135 | 136 | def test_compressed_tif_2(self): 137 | configuration = get_configuration() 138 | configuration.base_dir = "/QGIS/tests/testdata/zip/" 139 | configuration.extension = [".tif"] 140 | configuration.b_search_in_compressed_files = True 141 | 142 | # Set filters for next session 143 | filter_list = FilterList() 144 | filter = AlphanumericFilter('StartsWith', 'landsat_b1', configuration) 145 | filter_list.addFilter(filter) 146 | 147 | # Load 2 layers 148 | loader = LoadRasters(self.plugin.iface, configuration) 149 | self.assertEqual(self.project.count(), 0) 150 | loader.filterList = filter_list 151 | res = loader.loadLayers() 152 | self.assertEqual(res.result, EnumLoadThemAllResult.SUCCESS) 153 | self.assertEqual(res.layers_found, 5) 154 | self.assertEqual(res.layers_loaded, 5) 155 | self.assertTrue(self.project.count() > 0) 156 | 157 | # Check loaded layers 158 | #print(self.project.mapLayers()) 159 | layers = self.project.mapLayersByName("landsat_b1") 160 | self.assertTrue(bool(layers)) 161 | self.assertTrue(layers[0].isValid()) 162 | 163 | def tearDown(self): 164 | print('INFO: Removing all layers after compressed layers test...') 165 | self.project.removeAllMapLayers() 166 | 167 | @classmethod 168 | def tearDownClass(cls): 169 | print('INFO: Tear down test_compressed_layers') 170 | cls.plugin.unload() 171 | 172 | 173 | if __name__ == '__main__': 174 | nose2.main() 175 | -------------------------------------------------------------------------------- /tests/test_core_utils.py: -------------------------------------------------------------------------------- 1 | import nose2 2 | import os.path 3 | 4 | from qgis.testing import unittest, start_app 5 | 6 | start_app() 7 | 8 | from LoadThemAll.core.Utils import (get_parent_folder, 9 | get_file_extension, 10 | get_gzip_file_to_load, 11 | get_tar_files_to_load, 12 | get_zip_files_to_load) 13 | 14 | 15 | class TestPluginUtils(unittest.TestCase): 16 | 17 | def test_get_parent_folder(self): 18 | print('INFO: Validating get parent folder...') 19 | file_path = "/vsizip//docs/Regional/ZIP_data.zip/AA_PreQuat_NAD27z12.TAB" 20 | folder_path = "/docs/Regional/" 21 | self.assertEqual(os.path.normpath(get_parent_folder(file_path)), os.path.normpath(folder_path)) 22 | 23 | file_path = "/docs/geodata/Map_Database_ZIP/Geology/Regional/AA_PreQuat_NAD27z12.TAB" 24 | folder_path = "/docs/geodata/Map_Database_ZIP/Geology/Regional" 25 | self.assertEqual(os.path.normpath(get_parent_folder(file_path)), os.path.normpath(folder_path)) 26 | 27 | # Windows paths 28 | file_path = "/vsizip/C:/Users/33769/Documents/germain/data/ZIP_data.zip/AA_PreQuat_NAD27z12.TAB" 29 | folder_path = "C:/Users/33769/Documents/germain/data" 30 | self.assertEqual(os.path.normpath(get_parent_folder(file_path)), os.path.normpath(folder_path)) 31 | 32 | # TODO: 33 | # test paths with layername=abc suffixes 34 | 35 | def test_get_file_extensions(self): 36 | print('INFO: Validating get file extensions...') 37 | self.assertEqual(get_file_extension("abc.zip"), ".zip") 38 | self.assertEqual(get_file_extension("ABC.ZIP"), ".zip") 39 | self.assertEqual(get_file_extension("abc.shp"), ".shp") 40 | self.assertEqual(get_file_extension("ABC.SHP"), ".shp") 41 | self.assertEqual(get_file_extension("abc.shp.zip"), ".shp.zip") 42 | self.assertEqual(get_file_extension("ABC.SHP.ZIP"), ".shp.zip") 43 | self.assertEqual(get_file_extension("a.b.c.shp.zip"), ".shp.zip") 44 | self.assertEqual(get_file_extension("A.B.C.SHP.ZIP"), ".shp.zip") 45 | self.assertEqual(get_file_extension("abc.gz"), ".gz") 46 | self.assertEqual(get_file_extension("ABC.GZ"), ".gz") 47 | self.assertEqual(get_file_extension("a.b.c.gz"), ".gz") 48 | self.assertEqual(get_file_extension("A.B.C.GZ"), ".gz") 49 | self.assertEqual(get_file_extension("abc.geojson.gz"), ".gz") 50 | self.assertEqual(get_file_extension("ABC.GEOJSON.GZ"), ".gz") 51 | self.assertEqual(get_file_extension("a.b.c.geojson"), ".geojson") 52 | self.assertEqual(get_file_extension("A.B.C.GEOJSON"), ".geojson") 53 | self.assertEqual(get_file_extension("abc.tar.gz"), ".tar.gz") 54 | self.assertEqual(get_file_extension("ABC.TAR.GZ"), ".tar.gz") 55 | self.assertEqual(get_file_extension("a.b.c.tar.gz"), ".tar.gz") 56 | self.assertEqual(get_file_extension("A.B.C.TAR.GZ"), ".tar.gz") 57 | self.assertEqual(get_file_extension("abc.copc.laz"), ".copc.laz") 58 | self.assertEqual(get_file_extension("ABC.COPC.LAZ"), ".copc.laz") 59 | self.assertEqual(get_file_extension("a.b.c.copc.laz"), ".copc.laz") 60 | self.assertEqual(get_file_extension("A.B.C.COPC.LAZ"), ".copc.laz") 61 | self.assertEqual(get_file_extension("abc.laz"), ".laz") 62 | self.assertEqual(get_file_extension("ABC.LAZ"), ".laz") 63 | self.assertEqual(get_file_extension("a.b.c.laz"), ".laz") 64 | self.assertEqual(get_file_extension("A.B.C.LAZ"), ".laz") 65 | self.assertEqual(get_file_extension("ept.json"), "ept.json") 66 | self.assertEqual(get_file_extension("EPT.JSON"), "ept.json") 67 | self.assertEqual(get_file_extension("/tmp/ept.json"), "ept.json") 68 | self.assertEqual(get_file_extension("/TMP/EPT.JSON"), "ept.json") 69 | self.assertEqual(get_file_extension("ept-build.json"), ".json") 70 | self.assertEqual(get_file_extension("EPT-BUILD.JSON"), ".json") 71 | self.assertEqual(get_file_extension("a.b.c.json"), ".json") 72 | self.assertEqual(get_file_extension("A.B.C.JSON"), ".json") 73 | 74 | def test_get_zip_files_to_load(self): 75 | self.assertEqual(get_zip_files_to_load("/QGIS/tests/testdata/zip/landsat_b1.zip", [".tif"]), 76 | ["/vsizip//QGIS/tests/testdata/zip/landsat_b1.zip/landsat_b1.tif"]) 77 | self.assertEqual(get_zip_files_to_load("/QGIS/tests/testdata/zip/points2.zip", [".shp"]), 78 | ["/vsizip//QGIS/tests/testdata/zip/points2.zip/points.shp"]) 79 | self.assertEqual(get_zip_files_to_load("/QGIS/tests/testdata/zip/points2.zip", [".dbf"]), 80 | ["/vsizip//QGIS/tests/testdata/zip/points2.zip/points.dbf"]) 81 | self.assertEqual(get_zip_files_to_load("/QGIS/tests/testdata/zip/testzip.zip", [".geojson"]), 82 | ["/vsizip//QGIS/tests/testdata/zip/testzip.zip/folder/points.geojson"]) 83 | self.assertEqual(get_zip_files_to_load("/QGIS/tests/testdata/zip/testzip.zip", [".tif"]), 84 | ["/vsizip//QGIS/tests/testdata/zip/testzip.zip/folder/folder2/landsat_b2.tif", 85 | "/vsizip//QGIS/tests/testdata/zip/testzip.zip/landsat_b1.tif"]) 86 | self.assertEqual(get_zip_files_to_load("/QGIS/tests/testdata/zip/testzip.zip", [".shp"]), 87 | ["/vsizip//QGIS/tests/testdata/zip/testzip.zip/points.shp"]) 88 | self.assertEqual(get_zip_files_to_load("/QGIS/tests/testdata/zip/landsat_b1.zip", [".png"]), 89 | []) 90 | 91 | def test_get_tar_files_to_load(self): 92 | self.assertEqual(get_tar_files_to_load("/QGIS/tests/testdata/zip/landsat_b1.tar", [".tif"]), 93 | ["/vsitar//QGIS/tests/testdata/zip/landsat_b1.tar/landsat_b1.tif"]) 94 | self.assertEqual(get_tar_files_to_load("/QGIS/tests/testdata/zip/points2.tar", [".shp"]), 95 | ["/vsitar//QGIS/tests/testdata/zip/points2.tar/points.shp"]) 96 | self.assertEqual(get_tar_files_to_load("/QGIS/tests/testdata/zip/testtar.tgz", [".tif"]), 97 | ["/vsitar//QGIS/tests/testdata/zip/testtar.tgz/folder/folder2/landsat_b2.tif", 98 | "/vsitar//QGIS/tests/testdata/zip/testtar.tgz/landsat_b1.tif"]) 99 | self.assertEqual(get_tar_files_to_load("/QGIS/tests/testdata/zip/testtar.tgz", [".geojson"]), 100 | ["/vsitar//QGIS/tests/testdata/zip/testtar.tgz/folder/points.geojson"]) 101 | self.assertEqual(get_tar_files_to_load("/QGIS/tests/testdata/zip/testtar.tgz", [".shp"]), 102 | ["/vsitar//QGIS/tests/testdata/zip/testtar.tgz/points.shp"]) 103 | self.assertEqual(get_tar_files_to_load("/QGIS/tests/testdata/zip/testtar.tgz", [".gpkg"]), 104 | []) 105 | 106 | def test_get_gzip_file_to_load(self): 107 | self.assertEqual(get_gzip_file_to_load("/QGIS/tests/testdata/zip/points3.geojson.gz", [".geojson"]), 108 | ["/vsigzip//QGIS/tests/testdata/zip/points3.geojson.gz"]) 109 | self.assertEqual(get_gzip_file_to_load("/QGIS/tests/testdata/zip/landsat_b1.tif.gz", [".tif"]), 110 | ["/vsigzip//QGIS/tests/testdata/zip/landsat_b1.tif.gz"]) 111 | self.assertEqual(get_gzip_file_to_load("/QGIS/tests/testdata/zip/landsat_b1.tif.gz", [".png"]), 112 | []) 113 | 114 | 115 | if __name__ == '__main__': 116 | nose2.main() 117 | -------------------------------------------------------------------------------- /tests/test_load_point_clouds.py: -------------------------------------------------------------------------------- 1 | import nose2 2 | 3 | from qgis.core import (QgsApplication, 4 | QgsProject) 5 | from qgis.testing import unittest, start_app 6 | from qgis.testing.mocked import get_iface 7 | 8 | from LoadThemAll.LoadThemAll import LoadThemAll 9 | from LoadThemAll.core.Enums import EnumLoadThemAllResult 10 | from LoadThemAll.core.LoadFiles import LoadPointClouds 11 | from tests.utils import get_configuration 12 | 13 | start_app() 14 | 15 | 16 | class TestLoadPointClouds(unittest.TestCase): 17 | 18 | @classmethod 19 | def setUpClass(cls): 20 | print('\nINFO: Set up test_load_point_clouds') 21 | cls.plugin = LoadThemAll(get_iface(), with_gui=False) 22 | cls.project = QgsProject.instance() 23 | 24 | def test_load_point_clouds_single_ept(self): 25 | configuration = get_configuration() 26 | configuration.base_dir = "/QGIS/tests/testdata/point_clouds/ept/rgb/" 27 | configuration.extension = ["ept.json"] 28 | 29 | # Load 1 layer 30 | loader = LoadPointClouds(self.plugin.iface, configuration) 31 | self.assertEqual(self.project.count(), 0) 32 | res = loader.loadLayers() 33 | self.assertEqual(res.result, EnumLoadThemAllResult.SUCCESS) 34 | self.assertEqual(res.layers_found, 1) 35 | self.assertEqual(res.layers_loaded, 1) 36 | self.assertTrue(self.project.count() > 0) 37 | 38 | # Check loaded layer 39 | layers = self.project.mapLayersByName("ept") 40 | self.assertTrue(bool(layers)) 41 | self.assertTrue(layers[0].isValid()) 42 | 43 | def test_load_point_clouds_ept_not_found(self): 44 | configuration = get_configuration() 45 | configuration.base_dir = "/QGIS/tests/testdata/point_clouds/ept/rgb/ept-sources/" 46 | configuration.extension = ["ept.json"] 47 | 48 | # Load 1 layer 49 | loader = LoadPointClouds(self.plugin.iface, configuration) 50 | self.assertEqual(self.project.count(), 0) 51 | res = loader.loadLayers() 52 | 53 | self.assertEqual(res.result, EnumLoadThemAllResult.SUCCESS) 54 | self.assertEqual(res.layers_found, 0) 55 | self.assertEqual(res.layers_loaded, 0) 56 | self.assertEqual(self.project.count(), 0) 57 | 58 | def tearDown(self): 59 | print('INFO: Removing all layers after load test...') 60 | self.project.removeAllMapLayers() 61 | 62 | @classmethod 63 | def tearDownClass(cls): 64 | print('INFO: Tear down test_load_point_clouds') 65 | cls.plugin.unload() 66 | 67 | 68 | if __name__ == '__main__': 69 | nose2.main() 70 | -------------------------------------------------------------------------------- /tests/test_load_rasters.py: -------------------------------------------------------------------------------- 1 | import nose2 2 | 3 | from qgis.core import (QgsApplication, 4 | QgsProject) 5 | from qgis.testing import unittest, start_app 6 | from qgis.testing.mocked import get_iface 7 | 8 | from LoadThemAll.LoadThemAll import LoadThemAll 9 | from LoadThemAll.core.Enums import EnumLoadThemAllResult 10 | from LoadThemAll.core.Filter import (FilterList, 11 | AlphanumericFilter) 12 | from LoadThemAll.core.LoadFiles import LoadRasters 13 | from tests.utils import get_configuration 14 | 15 | start_app() 16 | 17 | 18 | class TestLoadRasters(unittest.TestCase): 19 | 20 | @classmethod 21 | def setUpClass(cls): 22 | print('\nINFO: Set up test_load_rasters') 23 | cls.plugin = LoadThemAll(get_iface(), with_gui=False) 24 | cls.project = QgsProject.instance() 25 | 26 | def test_load_rasters(self): 27 | configuration = get_configuration() 28 | configuration.base_dir = "/QGIS/tests/testdata/raster/" 29 | configuration.extension = [".png"] 30 | 31 | # Set filters for next session 32 | filter_list = FilterList() 33 | filter = AlphanumericFilter('EndsWith', 'rgb', configuration) 34 | filter_list.addFilter(filter) 35 | 36 | # Load 1 layer 37 | loader = LoadRasters(self.plugin.iface, configuration) 38 | self.assertEqual(self.project.count(), 0) 39 | loader.filterList = filter_list 40 | res = loader.loadLayers() 41 | self.assertEqual(res.result, EnumLoadThemAllResult.SUCCESS) 42 | self.assertEqual(res.layers_found, 1) 43 | self.assertEqual(res.layers_loaded, 1) 44 | self.assertTrue(self.project.count() > 0) 45 | 46 | # Check loaded layer 47 | layers = self.project.mapLayersByName("rotated_rgb") 48 | self.assertTrue(bool(layers)) 49 | self.assertTrue(layers[0].isValid()) 50 | 51 | def tearDown(self): 52 | print('INFO: Removing all layers after load test...') 53 | self.project.removeAllMapLayers() 54 | 55 | @classmethod 56 | def tearDownClass(cls): 57 | print('INFO: Tear down test_load_rasters') 58 | cls.plugin.unload() 59 | 60 | 61 | if __name__ == '__main__': 62 | nose2.main() 63 | -------------------------------------------------------------------------------- /tests/test_load_vectors.py: -------------------------------------------------------------------------------- 1 | import nose2 2 | 3 | from qgis.core import (QgsApplication, 4 | QgsProject) 5 | from qgis.testing import unittest, start_app 6 | from qgis.testing.mocked import get_iface 7 | 8 | from LoadThemAll.LoadThemAll import LoadThemAll 9 | from LoadThemAll.core.Enums import EnumLoadThemAllResult 10 | from LoadThemAll.core.Filter import (FilterList, 11 | AlphanumericFilter) 12 | from LoadThemAll.core.LoadFiles import LoadVectors 13 | from tests.utils import get_configuration 14 | 15 | start_app() 16 | 17 | 18 | class TestLoadVectors(unittest.TestCase): 19 | 20 | @classmethod 21 | def setUpClass(cls): 22 | print('\nINFO: Set up test_load_vectors') 23 | cls.plugin = LoadThemAll(get_iface(), with_gui=False) 24 | cls.project = QgsProject.instance() 25 | 26 | def test_load_vectors(self): 27 | configuration = get_configuration() 28 | configuration.base_dir = "/QGIS/tests/testdata/provider/" 29 | configuration.extension = [".gpkg"] 30 | 31 | # Set filters for next session 32 | filter_list = FilterList() 33 | filter = AlphanumericFilter('EndsWith', 'geopackage', configuration) 34 | filter_list.addFilter(filter) 35 | 36 | # Load 1 layer 37 | loader = LoadVectors(self.plugin.iface, configuration) 38 | self.assertEqual(self.project.count(), 0) 39 | loader.filterList = filter_list 40 | res = loader.loadLayers() 41 | self.assertEqual(res.result, EnumLoadThemAllResult.SUCCESS) 42 | self.assertEqual(res.layers_found, 1) 43 | self.assertEqual(res.layers_loaded, 1) 44 | self.assertTrue(self.project.count() > 0) 45 | 46 | # Check loaded layer 47 | layers = self.project.mapLayersByName("geopackage") 48 | self.assertTrue(bool(layers)) 49 | self.assertTrue(layers[0].isValid()) 50 | 51 | def tearDown(self): 52 | print('INFO: Removing all layers after load test...') 53 | self.project.removeAllMapLayers() 54 | 55 | @classmethod 56 | def tearDownClass(cls): 57 | print('INFO: Tear down test_load_vectors') 58 | cls.plugin.unload() 59 | 60 | 61 | if __name__ == '__main__': 62 | nose2.main() 63 | -------------------------------------------------------------------------------- /tests/test_plugin_load.py: -------------------------------------------------------------------------------- 1 | import nose2 2 | 3 | from qgis.core import QgsApplication 4 | from qgis.testing import unittest, start_app 5 | from qgis.testing.mocked import get_iface 6 | 7 | start_app() 8 | 9 | 10 | class TestPluginLoad(unittest.TestCase): 11 | 12 | @classmethod 13 | def setUpClass(cls): 14 | print('\nINFO: Set up test_plugin_load') 15 | from LoadThemAll.LoadThemAll import LoadThemAll 16 | cls.plugin = LoadThemAll(get_iface(), with_gui=False) 17 | # cls.plugin.initGui() # No GUI for tests 18 | 19 | def test_plugin_load(self): 20 | print('INFO: Validating plugin load...') 21 | self.assertIsNotNone(self.plugin.iface) 22 | 23 | @classmethod 24 | def tearDownClass(cls): 25 | print('INFO: Tear down test_plugin_load') 26 | cls.plugin.unload() 27 | 28 | 29 | if __name__ == '__main__': 30 | nose2.main() 31 | -------------------------------------------------------------------------------- /tests/utils.py: -------------------------------------------------------------------------------- 1 | from LoadThemAll.core.LoadConfiguration import LoadConfiguration 2 | 3 | 4 | def get_configuration() -> LoadConfiguration: 5 | configuration = LoadConfiguration() 6 | configuration.base_dir = "" # Override 7 | configuration.extension = [] # Override 8 | configuration.with_gui = False # Suitable for tests 9 | 10 | configuration.b_groups = False 11 | configuration.b_search_in_compressed_files = False 12 | configuration.b_layers_off = False 13 | configuration.b_not_empty = True 14 | configuration.b_sort = True 15 | configuration.b_reverse_sort = False 16 | configuration.b_case_insensitive = True 17 | configuration.b_accent_insensitive = False 18 | configuration.b_styles = False 19 | configuration.b_search_parent_layer = False 20 | configuration.b_add_parent_layer_name = False 21 | configuration.num_layers_to_confirm = 50 22 | 23 | return configuration 24 | --------------------------------------------------------------------------------