testing.py 38.1 KB
Newer Older
Benjamin Jakimow's avatar
qps    
Benjamin Jakimow committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# -*- coding: utf-8 -*-
# noinspection PyPep8Naming
"""
***************************************************************************
    qps/testing.py

    A module to support unittesting in context of GDAL and QGIS
    ---------------------
    Beginning            : 2019-01-11
    Copyright            : (C) 2020 by Benjamin Jakimow
    Email                : benjamin.jakimow@geo.hu-berlin.de
***************************************************************************
    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 3 of the License, or
    (at your option) any later version.
                                                                                                                                                 *
    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this software. If not, see <http://www.gnu.org/licenses/>.
***************************************************************************
"""
import os
import sys
import re
import io
import importlib
import typing
import traceback
import sqlite3
import uuid
import warnings
import pathlib
import time
import site
import mock
import inspect
import types
import enum
Benjamin Jakimow's avatar
Benjamin Jakimow committed
44
import sip
Benjamin Jakimow's avatar
qps    
Benjamin Jakimow committed
45
import random
Benjamin Jakimow's avatar
Benjamin Jakimow committed
46
import unittest
Benjamin Jakimow's avatar
Benjamin Jakimow committed
47
from qgis.core import *
Benjamin Jakimow's avatar
Benjamin Jakimow committed
48
49
50
from qgis.core import QgsMapLayer, QgsRasterLayer, QgsVectorLayer, QgsWkbTypes, QgsProcessingContext, \
    QgsProcessingFeedback, QgsField, QgsFields, QgsApplication, QgsCoordinateReferenceSystem, QgsProject, \
    QgsProcessingParameterNumber, QgsProcessingAlgorithm, QgsProcessingProvider, QgsPythonRunner, \
Benjamin Jakimow's avatar
Benjamin Jakimow committed
51
    QgsFeatureStore, QgsProcessingParameterRasterDestination, QgsProcessingParameterRasterLayer, \
Benjamin Jakimow's avatar
Benjamin Jakimow committed
52
    QgsProviderRegistry, QgsLayerTree, QgsLayerTreeModel, QgsLayerTreeRegistryBridge
Benjamin Jakimow's avatar
Benjamin Jakimow committed
53
from qgis.gui import *
Benjamin Jakimow's avatar
Benjamin Jakimow committed
54
from qgis.gui import QgsPluginManagerInterface, QgsLayerTreeMapCanvasBridge, QgsLayerTreeView, QgsMessageBar, \
Benjamin Jakimow's avatar
Benjamin Jakimow committed
55
    QgsMapCanvas, QgsGui, QgisInterface
Benjamin Jakimow's avatar
Benjamin Jakimow committed
56
57
58
59
60
61
from qgis.PyQt.QtCore import *
from qgis.PyQt.QtGui import *
from qgis.PyQt.QtWidgets import *
import qgis.testing
import qgis.utils
import numpy as np
Benjamin Jakimow's avatar
Benjamin Jakimow committed
62
63
from osgeo import gdal, ogr, osr, gdal_array
from .resources import *
Benjamin Jakimow's avatar
Benjamin Jakimow committed
64
from .utils import UnitLookup
Benjamin Jakimow's avatar
Benjamin Jakimow committed
65

Benjamin Jakimow's avatar
Benjamin Jakimow committed
66
67
68
WMS_GMAPS = r'crs=EPSG:3857&format&type=xyz&url=https://mt1.google.com/vt/lyrs%3Ds%26x%3D%7Bx%7D%26y%3D%7By%7D%26z%3D%7Bz%7D&zmax=19&zmin=0'
WMS_OSM = r'referer=OpenStreetMap%20contributors,%20under%20ODbL&type=xyz&url=http://tiles.wmflabs.org/hikebike/%7Bz%7D/%7Bx%7D/%7By%7D.png&zmax=17&zmin=1'
WFS_Berlin = r'restrictToRequestBBOX=''1'' srsname=''EPSG:25833'' typename=''fis:re_postleit'' url=''http://fbinter.stadt-berlin.de/fb/wfs/geometry/senstadt/re_postleit'' version=''auto'''
Benjamin Jakimow's avatar
Benjamin Jakimow committed
69
70


Benjamin Jakimow's avatar
Benjamin Jakimow committed
71
def initQgisApplication(*args, qgisResourceDir: str = None,
Benjamin Jakimow's avatar
Benjamin Jakimow committed
72
73
74
75
                        loadProcessingFramework=True,
                        loadEditorWidgets=True,
                        loadPythonRunner=True,
                        minimal=False,
Benjamin Jakimow's avatar
Benjamin Jakimow committed
76
                        **kwds) -> QgsApplication:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
77
78
    """
    Initializes a QGIS Environment
Benjamin Jakimow's avatar
Benjamin Jakimow committed
79
80
81
82
83
84
85
86
    :param qgisResourceDir: path to folder with QGIS resource modules. default = None
    :param loadProcessingFramework:  True, loads the QgsProcessingFramework plugins
    :param loadEditorWidgets: True, load the Editor widgets
    :param loadPythonRunner:  True, initializes a Python Runner
    :param minimal: False, if set on True, will deactivate the `load*` and return only a basic QgsApplication
    :return:
    """
    """
Benjamin Jakimow's avatar
Benjamin Jakimow committed
87

Benjamin Jakimow's avatar
Benjamin Jakimow committed
88
89
    :return: QgsApplication instance of local QGIS installation
    """
Benjamin Jakimow's avatar
Benjamin Jakimow committed
90
    warnings.warn('Use qps.testing.start_app instead', DeprecationWarning)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
91
    return start_app(cleanup=True, options=StartOptions.All)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
92
93
94
95
96
97
98
99
100
101


@enum.unique
class StartOptions(enum.IntFlag):
    Minimized = 0
    EditorWidgets = 1
    ProcessingFramework = 2
    PythonRunner = 4
    PrintProviders = 8
    All = EditorWidgets | ProcessingFramework | PythonRunner | PrintProviders
Benjamin Jakimow's avatar
Benjamin Jakimow committed
102

Benjamin Jakimow's avatar
Benjamin Jakimow committed
103

Benjamin Jakimow's avatar
Benjamin Jakimow committed
104
def start_app(cleanup=True, options=StartOptions.Minimized, resources: list = []) -> QgsApplication:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
105
    if isinstance(QgsApplication.instance(), QgsApplication):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
106
        print('Found existing QgsApplication.instance()')
Benjamin Jakimow's avatar
Benjamin Jakimow committed
107
        qgsApp = QgsApplication.instance()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
108
    else:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
        # load resource files, e.g to make icons available
        for path in resources:
            initResourceFile(path)
        qgsApp = qgis.testing.start_app(cleanup=cleanup)

    # initialize things not done by qgis.test.start_app()...
    if not QgsProviderRegistry.instance().libraryDirectory().exists():
        libDir = pathlib.Path(QgsApplication.instance().pkgDataPath()) / 'plugins'
        QgsProviderRegistry.instance().setLibraryDirectory(QDir(libDir.as_posix()))

    # check for potentially missing qt plugin folders
    if not os.environ.get('QT_PLUGIN_PATH'):
        existing = [pathlib.Path(p).resolve() for p in qgsApp.libraryPaths()]

        prefixDir = pathlib.Path(qgsApp.pkgDataPath()).resolve()
        candidates = [prefixDir / 'qtplugins',
                      prefixDir / 'plugins',
                      prefixDir / 'bin']
        for candidate in candidates:
            if candidate.is_dir() and candidate not in existing:
                qgsApp.addLibraryPath(candidate.as_posix())

    assert QgsProviderRegistry.instance().libraryDirectory().exists(), \
Benjamin Jakimow's avatar
Benjamin Jakimow committed
132
133
        'Directory: {} does not exist. Please check if QGIS_PREFIX_PATH correct'.format(
            QgsProviderRegistry.instance().libraryDirectory().path())
Benjamin Jakimow's avatar
Benjamin Jakimow committed
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177

    # initiate a PythonRunner instance if None exists
    if StartOptions.PythonRunner in options and not QgsPythonRunner.isValid():
        r = QgsPythonRunnerMockup()
        QgsPythonRunner.setInstance(r)

    # init standard EditorWidgets
    if StartOptions.EditorWidgets in options and len(QgsGui.editorWidgetRegistry().factories()) == 0:
        QgsGui.editorWidgetRegistry().initEditors()

    # test SRS
    if True:
        assert os.path.isfile(QgsApplication.qgisUserDatabaseFilePath()), \
            'QgsApplication.qgisUserDatabaseFilePath() does not exists: {}'.format(
                QgsApplication.qgisUserDatabaseFilePath())

        con = sqlite3.connect(QgsApplication.qgisUserDatabaseFilePath())
        cursor = con.execute("SELECT name FROM sqlite_master WHERE type='table'")
        tables = [v[0] for v in cursor.fetchall() if v[0] != 'sqlite_sequence']
        if 'tbl_srs' not in tables:
            info = ['{} misses "tbl_srs"'.format(QgsApplication.qgisSettingsDirPath())]
            info.append(
                'Settings directory might be outdated: {}'.format(QgsApplication.instance().qgisSettingsDirPath()))
            print('\n'.join(info), file=sys.stderr)

    if not isinstance(qgis.utils.iface, QgisInterface):
        iface = QgisMockup()
        qgis.utils.initInterface(sip.unwrapinstance(iface))
        assert iface == qgis.utils.iface

    # set 'home_plugin_path', which is required from the QGIS Plugin manager
    qgis.utils.home_plugin_path = (pathlib.Path(QgsApplication.instance().qgisSettingsDirPath()) \
                                   / 'python' / 'plugins').as_posix()

    # initialize the QGIS processing framework
    if StartOptions.ProcessingFramework in options:

        pfProviderIds = [p.id() for p in QgsApplication.processingRegistry().providers()]
        if not 'native' in pfProviderIds:
            from qgis.analysis import QgsNativeAlgorithms
            QgsApplication.processingRegistry().addProvider(QgsNativeAlgorithms())

        qgisCorePythonPluginDir = pathlib.Path(QgsApplication.pkgDataPath()) \
                                  / 'python' / 'plugins'
Benjamin Jakimow's avatar
Benjamin Jakimow committed
178
179
        assert os.path.isdir(qgisCorePythonPluginDir)
        if not qgisCorePythonPluginDir in sys.path:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
180
            sys.path.append(qgisCorePythonPluginDir.as_posix())
Benjamin Jakimow's avatar
Benjamin Jakimow committed
181

Benjamin Jakimow's avatar
Benjamin Jakimow committed
182
183
184
        required = ['qgis', 'gdal']  # at least these should be available
        missing = [p for p in required if p not in pfProviderIds]
        if len(missing) > 0:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
185
186
            from processing.core.Processing import Processing
            Processing.initialize()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
187

Benjamin Jakimow's avatar
Benjamin Jakimow committed
188
    if StartOptions.PrintProviders in options:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
189
        providers = QgsProviderRegistry.instance().providerList()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
190
        print('Providers: {}'.format(', '.join(providers)))
Benjamin Jakimow's avatar
Benjamin Jakimow committed
191

Benjamin Jakimow's avatar
Benjamin Jakimow committed
192
    return qgsApp
Benjamin Jakimow's avatar
Benjamin Jakimow committed
193
194
195
196


class QgisMockup(QgisInterface):
    """
Benjamin Jakimow's avatar
Benjamin Jakimow committed
197
    A "fake" QGIS Desktop instance that should provide all the interfaces a plugin developer might need (and nothing more)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
198
    """
Benjamin Jakimow's avatar
Benjamin Jakimow committed
199

Benjamin Jakimow's avatar
Benjamin Jakimow committed
200
201
202
203
204
205
206
207
    def __init__(self, *args):
        super(QgisMockup, self).__init__()

        self.mCanvas = QgsMapCanvas()
        self.mCanvas.blockSignals(False)
        self.mCanvas.setCanvasColor(Qt.black)
        self.mLayerTreeView = QgsLayerTreeView()
        self.mRootNode = QgsLayerTree()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
208
        self.mLayerTreeRegistryBridge = QgsLayerTreeRegistryBridge(self.mRootNode, QgsProject.instance())
Benjamin Jakimow's avatar
Benjamin Jakimow committed
209
210
211
212
        self.mLayerTreeModel = QgsLayerTreeModel(self.mRootNode)
        self.mLayerTreeView.setModel(self.mLayerTreeModel)
        self.mLayerTreeMapCanvasBridge = QgsLayerTreeMapCanvasBridge(self.mRootNode, self.mCanvas)
        self.mLayerTreeMapCanvasBridge.setAutoSetupOnFirstLayer(True)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
213
        # QgsProject.instance().legendLayersAdded.connect(self.addLegendLayers)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
214
215
216
217
        self.mPluginManager = QgsPluginManagerMockup()

        self.ui = QMainWindow()

Benjamin Jakimow's avatar
Benjamin Jakimow committed
218
219
220
221
222
        self.mViewMenu = self.ui.menuBar().addMenu('View')
        self.mVectorMenu = self.ui.menuBar().addMenu('Vector')
        self.mRasterMenu = self.ui.menuBar().addMenu('Raster')
        self.mWindowMenu = self.ui.menuBar().addMenu('Window')

Benjamin Jakimow's avatar
Benjamin Jakimow committed
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
        self.mMessageBar = QgsMessageBar()
        mainFrame = QFrame()
        self.ui.setCentralWidget(mainFrame)
        self.ui.setWindowTitle('QGIS Mockup')
        l = QHBoxLayout()
        l.addWidget(self.mLayerTreeView)
        l.addWidget(self.mCanvas)
        v = QVBoxLayout()
        v.addWidget(self.mMessageBar)
        v.addLayout(l)
        mainFrame.setLayout(v)
        self.ui.setCentralWidget(mainFrame)
        self.lyrs = []
        self.createActions()
        self.mClipBoard = QgsClipboardMockup()

Benjamin Jakimow's avatar
Benjamin Jakimow committed
239
240
241
242
243
244
245
246
247
248
249
        # mock other functions
        excluded = QObject.__dict__.keys()
        self._mock = mock.Mock(spec=QgisInterface)
        for n in self._mock._mock_methods:
            assert isinstance(n, str)
            if not n.startswith('_') and n not in excluded:
                try:
                    inspect.getfullargspec(getattr(self, n))
                except:
                    setattr(self, n, getattr(self._mock, n))

Benjamin Jakimow's avatar
Benjamin Jakimow committed
250
251
252
    def addLegendLayers(self, mapLayers: typing.List[QgsMapLayer]):
        for l in mapLayers:
            self.mRootNode.addLayer(l)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
253
254
255
256

    def pluginManagerInterface(self) -> QgsPluginManagerInterface:
        return self.mPluginManager

Benjamin Jakimow's avatar
Benjamin Jakimow committed
257
    def activeLayer(self):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
258
259
        return self.mapCanvas().currentLayer()

Benjamin Jakimow's avatar
Benjamin Jakimow committed
260
    def setActiveLayer(self, mapLayer: QgsMapLayer):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
261
262
263
        if mapLayer in self.mapCanvas().layers():
            self.mapCanvas().setCurrentLayer(mapLayer)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
264
    def cutSelectionToClipboard(self, mapLayer: QgsMapLayer):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
265
266
267
268
269
270
        if isinstance(mapLayer, QgsVectorLayer):
            self.mClipBoard.replaceWithCopyOf(mapLayer)
            mapLayer.beginEditCommand('Features cut')
            mapLayer.deleteSelectedFeatures()
            mapLayer.endEditCommand()

Benjamin Jakimow's avatar
Benjamin Jakimow committed
271
    def copySelectionToClipboard(self, mapLayer: QgsMapLayer):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
272
273
274
        if isinstance(mapLayer, QgsVectorLayer):
            self.mClipBoard.replaceWithCopyOf(mapLayer)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
275
    def pasteFromClipboard(self, pasteVectorLayer: QgsMapLayer):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
276
277
278
279
280
281
282
283
284
285
286
287
        if not isinstance(pasteVectorLayer, QgsVectorLayer):
            return
        return
        # todo: implement
        pasteVectorLayer.beginEditCommand('Features pasted')
        features = self.mClipBoard.transformedCopyOf(pasteVectorLayer.crs(), pasteVectorLayer.fields())
        nTotalFeatures = features.count()
        context = pasteVectorLayer.createExpressionContext()
        compatibleFeatures = QgsVectorLayerUtils.makeFeatureCompatible(features, pasteVectorLayer)
        newFeatures

    def iconSize(self, dockedToolbar=False):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
288
        return QSize(30, 30)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
289
290
291
292
293
294
295
296
297
298

    def mainWindow(self):
        return self.ui

    def addToolBarIcon(self, action):
        assert isinstance(action, QAction)

    def removeToolBarIcon(self, action):
        assert isinstance(action, QAction)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
299
    def addVectorLayer(self, path, basename=None, providerkey: str = 'ogr'):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
300
301
302
303
304
305
306
307
308
309
310
311
        if basename is None:
            basename = os.path.basename(path)

        l = QgsVectorLayer(path, basename, providerkey)
        assert l.isValid()
        QgsProject.instance().addMapLayer(l, True)
        self.mRootNode.addLayer(l)
        self.mLayerTreeMapCanvasBridge.setCanvasLayers()

    def legendInterface(self):
        return None

Benjamin Jakimow's avatar
Benjamin Jakimow committed
312
    def layerTreeCanvasBridge(self) -> QgsLayerTreeMapCanvasBridge:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
313
314
        return self.mLayerTreeMapCanvasBridge

Benjamin Jakimow's avatar
Benjamin Jakimow committed
315
    def layerTreeView(self) -> QgsLayerTreeView:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
316
317
        return self.mLayerTreeView

Benjamin Jakimow's avatar
Benjamin Jakimow committed
318
    def addRasterLayer(self, path, baseName: str = '') -> QgsRasterLayer:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
319
320
321
322
        l = QgsRasterLayer(path, os.path.basename(path))
        self.lyrs.append(l)
        QgsProject.instance().addMapLayer(l, True)
        self.mRootNode.addLayer(l)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
323
        return l
Benjamin Jakimow's avatar
Benjamin Jakimow committed
324
325
326
327
328

    def createActions(self):
        m = self.ui.menuBar().addAction('Add Vector')
        m = self.ui.menuBar().addAction('Add Raster')

Benjamin Jakimow's avatar
Benjamin Jakimow committed
329
    def mapCanvas(self) -> QgsMapCanvas:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
330
331
        return self.mCanvas

Benjamin Jakimow's avatar
Benjamin Jakimow committed
332
333
334
    def mapCanvases(self) -> typing.List[QgsMapCanvas]:
        return [self.mCanvas]

Benjamin Jakimow's avatar
Benjamin Jakimow committed
335
336
    def mapNavToolToolBar(self) -> QToolBar:
        return self.mMapNavToolBar
Benjamin Jakimow's avatar
Benjamin Jakimow committed
337

Benjamin Jakimow's avatar
Benjamin Jakimow committed
338
    def messageBar(self, *args, **kwargs) -> QgsMessageBar:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
339
340
        return self.mMessageBar

Benjamin Jakimow's avatar
Benjamin Jakimow committed
341
    def rasterMenu(self) -> QMenu:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
342
        return self.mRasterMenu
Benjamin Jakimow's avatar
Benjamin Jakimow committed
343

Benjamin Jakimow's avatar
Benjamin Jakimow committed
344
    def vectorMenu(self) -> QMenu:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
345
        return self.mVectorMenu
Benjamin Jakimow's avatar
Benjamin Jakimow committed
346

Benjamin Jakimow's avatar
Benjamin Jakimow committed
347
348
    def viewMenu(self) -> QMenu:
        return self.mViewMenu
Benjamin Jakimow's avatar
Benjamin Jakimow committed
349

Benjamin Jakimow's avatar
Benjamin Jakimow committed
350
351
    def windowMenu(self) -> QMenu:
        return self.mWindowMenu
Benjamin Jakimow's avatar
Benjamin Jakimow committed
352
353

    def zoomFull(self, *args, **kwargs):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
354
355
356
357
358
359
360
361
        self.mCanvas.zoomToFullExtent()


class TestCase(qgis.testing.TestCase):

    @classmethod
    def setUpClass(cls, cleanup=True, options=StartOptions.All, resources=[]) -> None:

Benjamin Jakimow's avatar
Benjamin Jakimow committed
362
        # tryto find QGIS resource files
Benjamin Jakimow's avatar
Benjamin Jakimow committed
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
        for r in findQGISResourceFiles():
            if r not in resources:
                resources.append(r)

        cls.app = start_app(cleanup=cleanup, options=options, resources=resources)

        from osgeo import gdal
        gdal.AllRegister()

    @classmethod
    def tearDownClass(cls):
        if False and isinstance(QgsApplication.instance(), QgsApplication):
            QgsApplication.exitQgis()
            QApplication.quit()
            import gc
            gc.collect()

Benjamin Jakimow's avatar
Benjamin Jakimow committed
380
381
382
383
384
385
    # @unittest.skip("deprectated method")
    # def testOutputDirectory(self, *args, **kwds):
    #    warnings.warn('Use createTestOutputDirectory(...) instead', DeprecationWarning)
    #    self.createTestOutputDirectory(*args, **kwds)

    def createTestOutputDirectory(self, name: str = 'test-outputs') -> pathlib.Path:
Benjamin Jakimow's avatar
qps    
Benjamin Jakimow committed
386
387
388
389
390
391
392
393
394
395
        """
        Returns the path to a test output directory
        :return:
        """
        repo = findUpwardPath(__file__, '.git').parent

        testDir = repo / name
        os.makedirs(testDir, exist_ok=True)
        return testDir

Benjamin Jakimow's avatar
Benjamin Jakimow committed
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
    def createImageCopy(self, path, overwrite_existing: bool = True) -> str:
        """
        Creates a save image copy to manipulate metadata
        :param path: str, path to valid raster image
        :type path:
        :return:
        :rtype:
        """
        if isinstance(path, pathlib.Path):
            path = path.as_posix()

        ds: gdal.Dataset = gdal.Open(path)
        assert isinstance(ds, gdal.Dataset)
        drv: gdal.Driver = ds.GetDriver()

        testdir = self.createTestOutputDirectory() / 'images'
        os.makedirs(testdir, exist_ok=True)
        bn, ext = os.path.splitext(os.path.basename(path))

        newpath = testdir / f'{bn}{ext}'
        i = 0
        if overwrite_existing and newpath.is_file():
            drv.Delete(newpath.as_posix())
        else:
            while newpath.is_file():
                i += 1
                newpath = testdir / f'{bn}{i}{ext}'

        drv.CopyFiles(newpath.as_posix(), path)

        return newpath.as_posix()

Benjamin Jakimow's avatar
Benjamin Jakimow committed
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
    def setUp(self):

        print('\nSET UP {}'.format(self.id()))

    def tearDown(self):

        print('TEAR DOWN {}'.format(self.id()))

    def showGui(self, widgets=[]) -> bool:
        """
        Call this to show GUI(s) in case we do not run within a CI system
        """
        if str(os.environ.get('CI')).lower() not in ['', 'none', 'false', '0']:
            return False

        if not isinstance(widgets, list):
            widgets = [widgets]

        keepOpen = False

        for w in widgets:
            if isinstance(w, QWidget):
                w.show()
                keepOpen = True
            elif callable(w):
                w()

        app = QApplication.instance()
        if isinstance(app, QApplication) and keepOpen:
            app.exec_()

        return True

    def assertIconsEqual(self, icon1, icon2):
        self.assertIsInstance(icon1, QIcon)
        self.assertIsInstance(icon2, QIcon)
        size = QSize(256, 256)
        self.assertEqual(icon1.actualSize(size), icon2.actualSize(size))

        img1 = QImage(icon1.pixmap(size))
        img2 = QImage(icon2.pixmap(size))
        self.assertImagesEqual(img1, img2)

    def assertImagesEqual(self, image1: QImage, image2: QImage):
        if image1.size() != image2.size():
            return False
        if image1.format() != image2.format():
            return False

        for x in range(image1.width()):
            for y in range(image1.height()):
                s = image1.bits()
                if image1.pixel(x, y, ) != image2.pixel(x, y):
                    return False
        return True
Benjamin Jakimow's avatar
Benjamin Jakimow committed
483

Benjamin Jakimow's avatar
Benjamin Jakimow committed
484

Benjamin Jakimow's avatar
Benjamin Jakimow committed
485
486
487
488
489
class TestObjects():
    """
    Creates objects to be used for testing. It is preferred to generate objects in-memory.
    """

Benjamin Jakimow's avatar
Benjamin Jakimow committed
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
    _coreData = _coreDataWL = _coreDataWLU = _coreDataWkt = _coreDataGT = None

    @staticmethod
    def coreData() -> typing.Tuple[np.ndarray, typing.List[float], str]:
        if TestObjects._coreData is None:
            source_raster = pathlib.Path(__file__).parent / 'enmap.tif'
            assert source_raster.is_file()

            ds = gdal.Open(source_raster.as_posix())
            assert isinstance(ds, gdal.Dataset)
            TestObjects._coreData = ds.ReadAsArray()
            TestObjects._coreDataGT = ds.GetGeoTransform()
            TestObjects._coreDataWkt = ds.GetProjection()
            from .utils import parseWavelength
            TestObjects._coreDataWL, TestObjects._coreDataWLU = parseWavelength(ds)

        return TestObjects._coreData, TestObjects._coreDataWL, TestObjects._coreDataWLU, \
               TestObjects._coreDataGT, TestObjects._coreDataWkt

Benjamin Jakimow's avatar
Benjamin Jakimow committed
509
    @staticmethod
Benjamin Jakimow's avatar
Benjamin Jakimow committed
510
    def createDropEvent(mimeData: QMimeData) -> QDropEvent:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
511
512
513
514
        """Creates a QDropEvent containing the provided QMimeData ``mimeData``"""
        return QDropEvent(QPointF(0, 0), Qt.CopyAction, mimeData, Qt.LeftButton, Qt.NoModifier)

    @staticmethod
Benjamin Jakimow's avatar
qps    
Benjamin Jakimow committed
515
516
    def spectralProfileData(n: int = 10,
                            n_bands: typing.List[int] = [-1]):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
517
518
519
520
521
        """
        Returns n random spectral profiles from the test data
        :return: lost of (N,3) array of floats specifying point locations.
        """

Benjamin Jakimow's avatar
Benjamin Jakimow committed
522
        coredata, wl, wlu, gt, wkt = TestObjects.coreData()
Benjamin Jakimow's avatar
qps    
Benjamin Jakimow committed
523
        cnb, cnl, cns = coredata.shape
Benjamin Jakimow's avatar
Benjamin Jakimow committed
524
        assert n > 0
Benjamin Jakimow's avatar
Benjamin Jakimow committed
525
526
        if not isinstance(n_bands, list):
            n_bands = [n_bands]
Benjamin Jakimow's avatar
qps    
Benjamin Jakimow committed
527
        assert isinstance(n_bands, list)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
528
529
530
531
532
533
534
        for i in range(len(n_bands)):
            nb = n_bands[i]
            if nb == -1:
                n_bands[i] = cnb
            else:
                assert 0 < nb <= cnb, f'Number of bands need to be in range 0 < nb <= {cnb}.'

Benjamin Jakimow's avatar
qps    
Benjamin Jakimow committed
535
536
537
        n_bands = [nb if nb > 0 else cnb for nb in n_bands]

        for nb in n_bands:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
538
            band_indices = np.linspace(0, cnb - 1, num=nb, dtype=np.int16)
Benjamin Jakimow's avatar
qps    
Benjamin Jakimow committed
539
540
541
542
543
544
            i = 0
            while i < n:
                x = random.randint(0, coredata.shape[2] - 1)
                y = random.randint(0, coredata.shape[1] - 1)
                yield coredata[band_indices, y, x], wl[band_indices], wlu
                i += 1
Benjamin Jakimow's avatar
Benjamin Jakimow committed
545
546

    @staticmethod
Benjamin Jakimow's avatar
qps    
Benjamin Jakimow committed
547
548
    def spectralProfiles(n=10,
                         fields: QgsFields = None,
Benjamin Jakimow's avatar
Benjamin Jakimow committed
549
550
551
                         n_bands: typing.List[int] = [-1],
                         wlu: str = None):

Benjamin Jakimow's avatar
Benjamin Jakimow committed
552
        from .speclib.core import SpectralProfile
Benjamin Jakimow's avatar
Benjamin Jakimow committed
553

Benjamin Jakimow's avatar
qps    
Benjamin Jakimow committed
554
        i = 1
Benjamin Jakimow's avatar
Benjamin Jakimow committed
555
556
557
        for (data, wl, data_wlu) in TestObjects.spectralProfileData(n, n_bands=n_bands):
            if wlu is None:
                wlu = data_wlu
Benjamin Jakimow's avatar
Benjamin Jakimow committed
558
            elif wlu != data_wlu:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
559
560
                wl = UnitLookup.convertMetricUnit(wl, data_wlu, wlu)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
561
            profile = SpectralProfile(fields=fields)
Benjamin Jakimow's avatar
qps    
Benjamin Jakimow committed
562
            profile.setName(f'Profile {i}')
Benjamin Jakimow's avatar
Benjamin Jakimow committed
563
564
            profile.setValues(y=data, x=wl, xUnit=wlu)
            yield profile
Benjamin Jakimow's avatar
qps    
Benjamin Jakimow committed
565
            i += 1
Benjamin Jakimow's avatar
Benjamin Jakimow committed
566
567
568
569

    """
    Class with static routines to create test objects
    """
Benjamin Jakimow's avatar
Benjamin Jakimow committed
570

Benjamin Jakimow's avatar
Benjamin Jakimow committed
571
    @staticmethod
Benjamin Jakimow's avatar
qps    
Benjamin Jakimow committed
572
573
    def createSpectralLibrary(n: int = 10,
                              n_empty: int = 0,
Benjamin Jakimow's avatar
Benjamin Jakimow committed
574
575
                              n_bands: typing.List[int] = [-1],
                              wlu: str = None):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
576
577
        """
        Creates an Spectral Library
Benjamin Jakimow's avatar
Benjamin Jakimow committed
578
579
        :param n_bands:
        :type n_bands:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
580
581
        :param wlu:
        :type wlu:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
582
583
        :param n: total number of profiles
        :type n: int
Benjamin Jakimow's avatar
qps    
Benjamin Jakimow committed
584
585
        :param n_empty: number of empty profiles, SpectralProfiles with empty x/y values
        :type n_empty: int
Benjamin Jakimow's avatar
Benjamin Jakimow committed
586
587
588
589
        :return: SpectralLibrary
        :rtype: SpectralLibrary
        """
        assert n > 0
Benjamin Jakimow's avatar
qps    
Benjamin Jakimow committed
590
        assert n_empty >= 0 and n_empty <= n
Benjamin Jakimow's avatar
Benjamin Jakimow committed
591
592
        if not isinstance(n_bands, list):
            n_bands = [n_bands]
Benjamin Jakimow's avatar
Benjamin Jakimow committed
593
594
595
        from .speclib.core import SpectralLibrary
        slib = SpectralLibrary()
        assert slib.startEditing()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
596
        profiles = list(TestObjects.spectralProfiles(n, fields=slib.fields(), n_bands=n_bands, wlu=wlu))
Benjamin Jakimow's avatar
Benjamin Jakimow committed
597
598
        slib.addProfiles(profiles, addMissingFields=False)

Benjamin Jakimow's avatar
qps    
Benjamin Jakimow committed
599
        for i in range(n_empty):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
            p = slib[i]
            p.setValues([], [])
            assert slib.updateFeature(p)

        assert slib.commitChanges()
        return slib

    @staticmethod
    def inMemoryImage(*args, **kwds):

        warnings.warn(''.join(traceback.format_stack()) + '\nUse createRasterDataset instead')
        return TestObjects.createRasterDataset(*args, **kwds)

    @staticmethod
    def createRasterDataset(ns=10, nl=20, nb=1,
                            crs=None, gt=None,
Benjamin Jakimow's avatar
Benjamin Jakimow committed
616
617
                            eType: int = gdal.GDT_Int16,
                            nc: int = 0,
Benjamin Jakimow's avatar
Benjamin Jakimow committed
618
619
620
                            path: typing.Union[str, pathlib.Path] = None,
                            drv: typing.Union[str, gdal.Driver] = None,
                            wlu: str = None,
Benjamin Jakimow's avatar
Benjamin Jakimow committed
621
622
                            no_data_rectangle: int = 0,
                            no_data_value: typing.Union[int, float] = -9999) -> gdal.Dataset:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
623
624
625
        """
        Generates a gdal.Dataset of arbitrary size based on true data from a smaller EnMAP raster image
        """
Benjamin Jakimow's avatar
Benjamin Jakimow committed
626
627
628
629
630
        from .classification.classificationscheme import ClassificationScheme

        scheme = None
        if nc is None:
            nc = 0
Benjamin Jakimow's avatar
Benjamin Jakimow committed
631

Benjamin Jakimow's avatar
Benjamin Jakimow committed
632
633
634
635
636
        if nc > 0:
            eType = gdal.GDT_Byte if nc < 256 else gdal.GDT_Int16
            scheme = ClassificationScheme()
            scheme.createClasses(nc)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
637
638
639
640
        if isinstance(drv, str):
            drv = gdal.GetDriverByName(drv)
        elif drv is None:
            drv = gdal.GetDriverByName('GTiff')
Benjamin Jakimow's avatar
Benjamin Jakimow committed
641
642
        assert isinstance(drv, gdal.Driver)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
643
644
645
        if isinstance(path, pathlib.Path):
            path = path.as_posix()
        elif path is None:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
646
647
648
649
            if nc > 0:
                path = '/vsimem/testClassification.{}.tif'.format(str(uuid.uuid4()))
            else:
                path = '/vsimem/testImage.{}.tif'.format(str(uuid.uuid4()))
Benjamin Jakimow's avatar
Benjamin Jakimow committed
650
        assert isinstance(path, str)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
651

Benjamin Jakimow's avatar
Benjamin Jakimow committed
652
        ds: gdal.Driver = drv.Create(path, ns, nl, bands=nb, eType=eType)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
653
        assert isinstance(ds, gdal.Dataset)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
654
655
656
657
        if no_data_rectangle > 0:
            no_data_rectangle = min([no_data_rectangle, ns])
            no_data_rectangle = min([no_data_rectangle, nl])
            for b in range(ds.RasterCount):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
658
                band: gdal.Band = ds.GetRasterBand(b + 1)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
659
660
                band.SetNoDataValue(no_data_value)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
661
662
663
664
665
666
        coredata, core_wl, core_wlu, core_gt, core_wkt = TestObjects.coreData()

        dt_out = gdal_array.flip_code(eType)
        if isinstance(crs, str) or gt is not None:
            assert isinstance(gt, list) and len(gt) == 6
            assert isinstance(crs, str) and len(crs) > 0
Benjamin Jakimow's avatar
Benjamin Jakimow committed
667
668
            c = QgsCoordinateReferenceSystem(crs)
            ds.SetProjection(c.toWkt())
Benjamin Jakimow's avatar
Benjamin Jakimow committed
669
670
671
672
            ds.SetGeoTransform(gt)
        else:
            ds.SetProjection(core_wkt)
            ds.SetGeoTransform(core_gt)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
673

Benjamin Jakimow's avatar
Benjamin Jakimow committed
674
675
        if nc > 0:
            for b in range(nb):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
676
                band: gdal.Band = ds.GetRasterBand(b + 1)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
677
678
679
680
                assert isinstance(band, gdal.Band)

                array = np.empty((nl, ns), dtype=dt_out)
                assert isinstance(array, np.ndarray)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
681

Benjamin Jakimow's avatar
Benjamin Jakimow committed
682
                array.fill(0)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
683
684
685
686
687
688
689
690
691
692
                y0 = 0

                step = int(np.ceil(float(nl) / len(scheme)))

                for i, c in enumerate(scheme):
                    y1 = min(y0 + step, nl - 1)
                    array[y0:y1, :] = c.label()
                    y0 += y1 + 1
                band.SetCategoryNames(scheme.classNames())
                band.SetColorTable(scheme.gdalColorTable())
Benjamin Jakimow's avatar
Benjamin Jakimow committed
693
694
695

                if no_data_rectangle > 0:
                    array[0:no_data_rectangle, 0:no_data_rectangle] = no_data_value
Benjamin Jakimow's avatar
Benjamin Jakimow committed
696
697
698
699
700
701
702
703
                band.WriteArray(array)
        else:
            # fill with test data
            coredata = coredata.astype(dt_out)
            cb, cl, cs = coredata.shape
            if nb > coredata.shape[0]:
                coreddata2 = np.empty((nb, cl, cs), dtype=dt_out)
                coreddata2[0:cb, :, :] = coredata
Benjamin Jakimow's avatar
Benjamin Jakimow committed
704
                # todo: increase the number of output bands by linear interpolation instead just repeating the last band
Benjamin Jakimow's avatar
Benjamin Jakimow committed
705
706
707
708
709
710
711
712
713
714
715
716
717
718
                for b in range(cb, nb):
                    coreddata2[b, :, :] = coredata[-1, :, :]
                coredata = coreddata2

            xoff = 0
            while xoff < ns - 1:
                xsize = min(cs, ns - xoff)
                yoff = 0
                while yoff < nl - 1:
                    ysize = min(cl, nl - yoff)
                    ds.WriteRaster(xoff, yoff, xsize, ysize, coredata[:, 0:ysize, 0:xsize].tobytes())
                    yoff += ysize
                xoff += xsize

Benjamin Jakimow's avatar
Benjamin Jakimow committed
719
720
721
722
723
            if no_data_rectangle > 0:
                arr = np.empty((nb, no_data_rectangle, no_data_rectangle), dtype=coredata.dtype)
                arr.fill(no_data_value)
                ds.WriteRaster(0, 0, no_data_rectangle, no_data_rectangle, arr.tobytes())

Benjamin Jakimow's avatar
Benjamin Jakimow committed
724
725
726
727
728
            wl = []
            if nb > cb:
                wl.extend(core_wl.tolist())
                for b in range(cb, nb):
                    wl.append(core_wl[-1])
Benjamin Jakimow's avatar
Benjamin Jakimow committed
729
            else:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
730
731
                wl = core_wl[:nb].tolist()
            assert len(wl) == nb
Benjamin Jakimow's avatar
Benjamin Jakimow committed
732

Benjamin Jakimow's avatar
Benjamin Jakimow committed
733
734
735
            if wlu is None:
                wlu = core_wlu
            elif wlu != core_wlu:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
736
737
738
739
740
741
742
743
                wl = UnitLookup.convertMetricUnit(wl, core_wlu, wlu)

            domain = None
            if drv.ShortName == 'ENVI':
                domain = 'ENVI'

            ds.SetMetadataItem('wavelength units', wlu, domain)
            ds.SetMetadataItem('wavelength', ','.join([str(w) for w in wl]), domain)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
744

Benjamin Jakimow's avatar
Benjamin Jakimow committed
745
746
747
748
        ds.FlushCache()
        return ds

    @staticmethod
Benjamin Jakimow's avatar
Benjamin Jakimow committed
749
    def createRasterLayer(*args, **kwds) -> QgsRasterLayer:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
750
751
752
753
754
        """
        Creates an in-memory raster layer.
        See arguments & keyword for `inMemoryImage()`
        :return: QgsRasterLayer
        """
Benjamin Jakimow's avatar
Benjamin Jakimow committed
755
        ds = TestObjects.createRasterDataset(*args, **kwds)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
756
757
758
759
760
761
762
763
        assert isinstance(ds, gdal.Dataset)
        path = ds.GetDescription()

        lyr = QgsRasterLayer(path, os.path.basename(path), 'gdal')
        assert lyr.isValid()
        return lyr

    @staticmethod
Benjamin Jakimow's avatar
Benjamin Jakimow committed
764
    def createVectorDataSet(wkb=ogr.wkbPolygon) -> ogr.DataSource:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
765
766
767
768
        """
        Create an in-memory ogr.DataSource
        :return: ogr.DataSource
        """
Benjamin Jakimow's avatar
Benjamin Jakimow committed
769
        ogr.UseExceptions()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
770
771
        assert wkb in [ogr.wkbPoint, ogr.wkbPolygon, ogr.wkbLineString]

Benjamin Jakimow's avatar
Benjamin Jakimow committed
772
        # find the QGIS world_map.shp
773
        pkgPath = QgsApplication.instance().pkgDataPath()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
774
775
776
777
778
        assert os.path.isdir(pkgPath)

        pathSrc = pathlib.Path(__file__).parent / 'landcover_polygons.geojson'
        assert pathSrc.is_file(), 'Unable to find {}'.format(pathSrc)
        """
Benjamin Jakimow's avatar
Benjamin Jakimow committed
779
780
781
782
783
784
785
786
787
        potentialPathes = [
            os.path.join(os.path.dirname(__file__), 'testpolygons.geojson'),
            os.path.join(pkgPath, *['resources', 'data', 'world_map.shp']),
        ]
        for p in potentialPathes:
            if os.path.isfile(p):
                pathSrc = p
                break
        assert os.path.isfile(pathSrc), 'Unable to find QGIS "world_map.shp". QGIS Pkg path = {}'.format(pkgPath)
788

Benjamin Jakimow's avatar
Benjamin Jakimow committed
789
790
791
        """

        dsSrc = ogr.Open(pathSrc.as_posix())
Benjamin Jakimow's avatar
Benjamin Jakimow committed
792
        assert isinstance(dsSrc, ogr.DataSource)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
793
794
795
796
797
798
799
800
801
        lyrSrc = dsSrc.GetLayer(0)
        assert isinstance(lyrSrc, ogr.Layer)

        ldef = lyrSrc.GetLayerDefn()
        assert isinstance(ldef, ogr.FeatureDefn)

        srs = lyrSrc.GetSpatialRef()
        assert isinstance(srs, osr.SpatialReference)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
802
        drv = ogr.GetDriverByName('GPKG')
Benjamin Jakimow's avatar
Benjamin Jakimow committed
803
        assert isinstance(drv, ogr.Driver)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
804

Benjamin Jakimow's avatar
Benjamin Jakimow committed
805
        # set temp path
Benjamin Jakimow's avatar
Benjamin Jakimow committed
806
807
        if wkb == ogr.wkbPolygon:
            lname = 'polygons'
Benjamin Jakimow's avatar
Benjamin Jakimow committed
808
            pathDst = '/vsimem/tmp' + str(uuid.uuid4()) + '.test.polygons.gpkg'
Benjamin Jakimow's avatar
Benjamin Jakimow committed
809
810
        elif wkb == ogr.wkbPoint:
            lname = 'points'
Benjamin Jakimow's avatar
Benjamin Jakimow committed
811
            pathDst = '/vsimem/tmp' + str(uuid.uuid4()) + '.test.centroids.gpkg'
Benjamin Jakimow's avatar
Benjamin Jakimow committed
812
813
        elif wkb == ogr.wkbLineString:
            lname = 'lines'
Benjamin Jakimow's avatar
Benjamin Jakimow committed
814
            pathDst = '/vsimem/tmp' + str(uuid.uuid4()) + '.test.line.gpkg'
Benjamin Jakimow's avatar
Benjamin Jakimow committed
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
        else:
            raise NotImplementedError()

        if wkb == ogr.wkbPolygon:
            dsDst = drv.CopyDataSource(dsSrc, pathDst)
        else:
            dsDst = drv.CreateDataSource(pathDst)
            assert isinstance(dsDst, ogr.DataSource)
            lyrDst = dsDst.CreateLayer(lname, srs=srs, geom_type=wkb)
            assert isinstance(lyrDst, ogr.Layer)

            # copy field definitions
            for i in range(ldef.GetFieldCount()):
                fieldDefn = ldef.GetFieldDefn(i)
                assert isinstance(fieldDefn, ogr.FieldDefn)
                lyrDst.CreateField(fieldDefn)

            # copy features

            for fSrc in lyrSrc:
                assert isinstance(fSrc, ogr.Feature)
                g = fSrc.geometry()

                fDst = ogr.Feature(lyrDst.GetLayerDefn())
                assert isinstance(fDst, ogr.Feature)

                if isinstance(g, ogr.Geometry):
                    if wkb == ogr.wkbPoint:
                        g = g.Centroid()
                    elif wkb == ogr.wkbLineString:
                        g = g.GetBoundary()
                    else:
                        raise NotImplementedError()

                fDst.SetGeometry(g)

                for i in range(ldef.GetFieldCount()):
                    fDst.SetField(i, fSrc.GetField(i))

Benjamin Jakimow's avatar
Benjamin Jakimow committed
854
                assert lyrDst.CreateFeature(fDst) == ogr.OGRERR_NONE
Benjamin Jakimow's avatar
Benjamin Jakimow committed
855

Benjamin Jakimow's avatar
Benjamin Jakimow committed
856
857
858
859
860
        assert isinstance(dsDst, ogr.DataSource)
        dsDst.FlushCache()
        return dsDst

    @staticmethod
Benjamin Jakimow's avatar
Benjamin Jakimow committed
861
    def createVectorLayer(wkbType: QgsWkbTypes = QgsWkbTypes.Polygon) -> QgsVectorLayer:
Benjamin Jakimow's avatar
Benjamin Jakimow committed
862
863
864
865
866
        """
        Create a QgsVectorLayer
        :return: QgsVectorLayer
        """
        lyrOptions = QgsVectorLayer.LayerOptions(loadDefaultStyle=False, readExtentFromXml=False)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
867
868
869
870
871
872
873
874
875
876
877
878
879

        wkb = None

        if wkbType in [QgsWkbTypes.Point, QgsWkbTypes.PointGeometry]:
            wkb = ogr.wkbPoint
        elif wkbType in [QgsWkbTypes.LineString, QgsWkbTypes.LineGeometry]:
            wkb = ogr.wkbLineString
        elif wkbType in [QgsWkbTypes.Polygon, QgsWkbTypes.PolygonGeometry]:
            wkb = ogr.wkbPolygon

        assert wkb is not None
        dsSrc = TestObjects.createVectorDataSet(wkb)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
880
881
882
883
        assert isinstance(dsSrc, ogr.DataSource)
        lyr = dsSrc.GetLayer(0)
        assert isinstance(lyr, ogr.Layer)
        assert lyr.GetFeatureCount() > 0
Benjamin Jakimow's avatar
Benjamin Jakimow committed
884
885
        #uri = '{}|{}'.format(dsSrc.GetName(), lyr.GetName())
        uri = dsSrc.GetName()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
886
887
888
889
890
891
892
893
        # dsSrc = None
        vl = QgsVectorLayer(uri, 'testlayer', 'ogr', lyrOptions)
        assert isinstance(vl, QgsVectorLayer)
        assert vl.isValid()
        assert vl.featureCount() == lyr.GetFeatureCount()
        return vl

    @staticmethod
Benjamin Jakimow's avatar
Benjamin Jakimow committed
894
    def createDropEvent(mimeData: QMimeData):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
        """Creates a QDropEvent conaining the provided QMimeData"""
        return QDropEvent(QPointF(0, 0), Qt.CopyAction, mimeData, Qt.LeftButton, Qt.NoModifier)

    @staticmethod
    def processingAlgorithm():

        class TestProcessingAlgorithm(QgsProcessingAlgorithm):

            def __init__(self):
                super(TestProcessingAlgorithm, self).__init__()
                s = ""

            def createInstance(self):
                return TestProcessingAlgorithm()

            def name(self):
                return 'exmaplealg'

            def displayName(self):
                return 'Example Algorithm'

            def groupId(self):
                return 'exampleapp'

            def group(self):
                return 'TEST APPS'

            def initAlgorithm(self, configuration=None):
                self.addParameter(QgsProcessingParameterRasterLayer('pathInput', 'The Input Dataset'))
                self.addParameter(
                    QgsProcessingParameterNumber('value', 'The value', QgsProcessingParameterNumber.Double, 1, False,
                                                 0.00, 999999.99))
                self.addParameter(QgsProcessingParameterRasterDestination('pathOutput', 'The Output Dataset'))

            def processAlgorithm(self, parameters, context, feedback):
                assert isinstance(parameters, dict)
                assert isinstance(context, QgsProcessingContext)
                assert isinstance(feedback, QgsProcessingFeedback)

                outputs = {}
                return outputs

        return TestProcessingAlgorithm()


class QgsPluginManagerMockup(QgsPluginManagerInterface):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def addPluginMetadata(self, *args, **kwargs):
        super().addPluginMetadata(*args, **kwargs)

    def addToRepositoryList(self, *args, **kwargs):
        super().addToRepositoryList(*args, **kwargs)

    def childEvent(self, *args, **kwargs):
        super().childEvent(*args, **kwargs)

    def clearPythonPluginMetadata(self, *args, **kwargs):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
955
        # super().clearPythonPluginMetadata(*args, **kwargs)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
        pass

    def clearRepositoryList(self, *args, **kwargs):
        super().clearRepositoryList(*args, **kwargs)

    def connectNotify(self, *args, **kwargs):
        super().connectNotify(*args, **kwargs)

    def customEvent(self, *args, **kwargs):
        super().customEvent(*args, **kwargs)

    def disconnectNotify(self, *args, **kwargs):
        super().disconnectNotify(*args, **kwargs)

    def isSignalConnected(self, *args, **kwargs):
        return super().isSignalConnected(*args, **kwargs)

    def pluginMetadata(self, *args, **kwargs):
        super().pluginMetadata(*args, **kwargs)

    def pushMessage(self, *args, **kwargs):
        super().pushMessage(*args, **kwargs)

    def receivers(self, *args, **kwargs):
        return super().receivers(*args, **kwargs)

    def reloadModel(self, *args, **kwargs):
        super().reloadModel(*args, **kwargs)

    def sender(self, *args, **kwargs):
        return super().sender(*args, **kwargs)

    def senderSignalIndex(self, *args, **kwargs):
        return super().senderSignalIndex(*args, **kwargs)

    def showPluginManager(self, *args, **kwargs):
        super().showPluginManager(*args, **kwargs)

    def timerEvent(self, *args, **kwargs):
        super().timerEvent(*args, **kwargs)


class QgsClipboardMockup(QObject):
    changed = pyqtSignal()

    def __init__(self, parent=None):
        super(QgsClipboardMockup, self).__init__(parent)

        self.mFeatureFields = None
        self.mFeatureClipboard = None
        self.mCRS = None
        self.mSrcLayer = None
        self.mUseSystemClipboard = False
        QApplication.clipboard().dataChanged.connect(self.systemClipboardChanged)

    def replaceWithCopyOf(self, src):
        if isinstance(src, QgsVectorLayer):
            self.mFeatureFields = src.fields()
            self.mFeatureClipboard = src.selectedFeatures()
            self.mCRS = src.crs()
            self.mSrcLayer = src

            return

            self.setSystemClipBoard()
            self.mUseSystemClipboard = False
            self.changed.emit()

        elif isinstance(src, QgsFeatureStore):
            raise NotImplementedError()

    def setSystemClipBoard(self):

        raise NotImplementedError()
        cb = QApplication.clipboard()
        textCopy = self.generateClipboardText()

        m = QMimeData()
        m.setText(textCopy)

Benjamin Jakimow's avatar
Benjamin Jakimow committed
1036
        # todo: set HTML
Benjamin Jakimow's avatar
Benjamin Jakimow committed
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049

    def generateClipboardText(self):

        raise NotImplementedError()
        pass
        textFields = ['wkt_geom'] + [n for n in self.mFeatureFields]

        textLines = '\t'.join(textFields)
        textFields.clear()

    def systemClipboardChanged(self):
        pass

Benjamin Jakimow's avatar
Benjamin Jakimow committed
1050

Benjamin Jakimow's avatar
Benjamin Jakimow committed
1051
class QgsPythonRunnerMockup(QgsPythonRunner):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
1052
1053
1054
1055
1056
    """
    A Qgs PythonRunner implementation
    """

    def __init__(self):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
1057
        super(QgsPythonRunnerMockup, self).__init__()
Benjamin Jakimow's avatar
Benjamin Jakimow committed
1058

Benjamin Jakimow's avatar
Benjamin Jakimow committed
1059
    def evalCommand(self, cmd: str, result: str):
Benjamin Jakimow's avatar
Benjamin Jakimow committed
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
        try:
            o = compile(cmd)
        except Exception as ex:
            result = str(ex)
            return False
        return True

    def runCommand(self, command, messageOnError=''):
        try:
            o = compile(command, 'fakemodule', 'exec')
            exec(o)
        except Exception as ex:
            messageOnError = str(ex)
Benjamin Jakimow's avatar
Benjamin Jakimow committed
1073
            command = ['{}:{}'.format(i + 1, l) for i, l in enumerate(command.splitlines())]
Benjamin Jakimow's avatar
Benjamin Jakimow committed
1074
1075
1076
1077
            print('\n'.join(command), file=sys.stderr)
            raise ex
            return False
        return True