예제 #1
0
    def test_link_feature(self):
        """
        Check if an existing feature can be linked
        """
        wrapper = self.createWrapper(self.vl_a, '"name"=\'Douglas Adams\'')  # NOQA

        f = QgsFeature(self.vl_b.fields())
        f.setAttributes([self.vl_b.dataProvider().defaultValueClause(0), 'The Hitchhiker\'s Guide to the Galaxy'])
        self.vl_b.addFeature(f)

        def choose_linked_feature():
            dlg = QApplication.activeModalWidget()
            dlg.setSelectedFeatures([f.id()])
            dlg.accept()

        btn = self.widget.findChild(QToolButton, 'mLinkFeatureButton')

        timer = QTimer()
        timer.setSingleShot(True)
        timer.setInterval(0)  # will run in the event loop as soon as it's processed when the dialog is opened
        timer.timeout.connect(choose_linked_feature)
        timer.start()

        btn.click()
        # magically the above code selects the feature here...

        link_feature = next(self.vl_link.getFeatures(QgsFeatureRequest().setFilterExpression('"fk_book"={}'.format(f[0]))))
        self.assertIsNotNone(link_feature[0])

        self.assertEqual(self.table_view.model().rowCount(), 1)
    def test_link_feature(self):
        """
        Check if an existing feature can be linked
        """
        wrapper = self.createWrapper(self.vl_a, '"name"=\'Douglas Adams\'')  # NOQA

        f = QgsFeature(self.vl_b.fields())
        f.setAttributes([self.vl_b.dataProvider().defaultValueClause(0), 'The Hitchhiker\'s Guide to the Galaxy'])
        self.vl_b.addFeature(f)

        def choose_linked_feature():
            dlg = QApplication.activeModalWidget()
            dlg.setSelectedFeatures([f.id()])
            dlg.accept()

        btn = self.widget.findChild(QToolButton, 'mLinkFeatureButton')

        timer = QTimer()
        timer.setSingleShot(True)
        timer.setInterval(0)  # will run in the event loop as soon as it's processed when the dialog is opened
        timer.timeout.connect(choose_linked_feature)
        timer.start()

        btn.click()
        # magically the above code selects the feature here...

        link_feature = next(self.vl_link.getFeatures(QgsFeatureRequest().setFilterExpression('"fk_book"={}'.format(f[0]))))
        self.assertIsNotNone(link_feature[0])

        self.assertEqual(self.table_view.model().rowCount(), 1)
예제 #3
0
    def _get_sync(self, qurl: QUrl, timeout: int = 20000) -> Reply:
        '''
        synchronous GET-request
        '''
        request = QNetworkRequest(qurl)
        ## newer versions of QGIS (3.6+) support synchronous requests
        #if hasattr(self._manager, 'blockingGet'):
        #reply = self._manager.blockingGet(request, forceRefresh=True)
        ## use blocking event loop for older versions
        #else:
        loop = QEventLoop()
        timer = QTimer()
        timer.setSingleShot(True)
        # reply or timeout break event loop, whoever comes first
        timer.timeout.connect(loop.quit)
        reply = self._manager.get(request)
        reply.finished.connect(loop.quit)

        timer.start(timeout)

        # start blocking loop
        loop.exec()
        loop.deleteLater()
        if not timer.isActive():
            reply.deleteLater()
            raise ConnectionError('Timeout')

        timer.stop()
        #if reply.error():
        #self.error.emit(reply.errorString())
        #raise ConnectionError(reply.errorString())
        res = Reply(reply)
        self.finished.emit(res)
        return res
예제 #4
0
    def _check_path_exists(self, path, text_box):
        # Validates if the specified folder exists
        dir = QDir()

        if not dir.exists(path):
            msg = self.tr("'{0}' directory does not exist.".format(path))
            self.notif_bar.insertErrorNotification(msg)

            # Highlight textbox control
            text_box.setStyleSheet(INVALIDATESTYLESHEET)

            timer = QTimer(self)
            # Sync interval with that of the notification bar
            timer.setInterval(self.notif_bar.interval)
            timer.setSingleShot(True)

            # Remove previous connected slots (if any)
            receivers = timer.receivers(QTimer.timeout)
            if receivers > 0:
                self._timer.timeout.disconnect()

            timer.start()
            timer.timeout.connect(lambda: self._restore_stylesheet(
                text_box)
                                  )

            return False

        return True
예제 #5
0
 def update_referecedata_cache_model(self, filter_models, type):
     # updates the model and waits for the end
     loop = QEventLoop()
     self.ilireferencedatacache.model_refreshed.connect(lambda: loop.quit())
     timer = QTimer()
     timer.setSingleShot(True)
     timer.timeout.connect(lambda: loop.quit())
     timer.start(10000)
     self.refresh_referencedata_cache(filter_models, type)
     loop.exec()
     return self.ilireferencedatacache.model
예제 #6
0
class QvBafarada(QLabel):
    '''
    Mostra un text simulant una bafarada (globus) de còmic en una posició relativa al widget pare (TOPLEFT, TOPRIGHT, BOTTOMLEFT o BOTTOMRIGHT)
    Si no s'especifica pare, es mostrarà on li sembli, així que seria convenient fer-li un move a la posició que es vulgui
    La bafarada es mostra fent-li un show, i s'oculta per si sola, sense necessitat de fer res. 
    '''
    TOPLEFT=0
    TOPRIGHT=1
    BOTTOMLEFT=2
    BOTTOMRIGHT=3
    def __init__(self,text,parent=None, pos=BOTTOMRIGHT, temps=5):
        super().__init__(text)
        self.parent=parent
        self.setFont(QvConstants.FONTTEXT)
        self.setWordWrap(True)
        self.pos=pos
        self.temps=temps
        self.setStyleSheet('''
            background: %s;
            color: %s;
            padding: 2px;
            border: 2px solid %s;
            border-radius: 10px;
            margin: 0px;
        '''%(QvConstants.COLORBLANCHTML,QvConstants.COLORFOSCHTML, QvConstants.COLORDESTACATHTML))
        self.setWindowFlags(Qt.WindowStaysOnTopHint | Qt.FramelessWindowHint)
        # self.setWindowFlags(Qt.Window | Qt.WA_TranslucentBackground | Qt.FramelessWindowHint)
    def paintEvent(self,event):
        super().paintEvent(event)
    def show(self):
        super().show()
        self.timer=QTimer(self)
        self.timer.setSingleShot(True)
        self.timer.timeout.connect(lambda: self.hide())
        self.timer.start(self.temps*1000)
        # self.show()
        if self.parent is not None:
            #Ja que python no té switch-case, ho fem amb una llista
            pos=[(10,10),(self.parent.width()-self.width()-10,10),(10,self.parent.height()-self.height()-10),(self.parent.width()-self.width()-10,self.parent.height()-self.height()-10)][self.pos]
            self.move(*pos)
    def oculta(self):
        self.hide()
        self.timer.stop()
예제 #7
0
    def _post_sync(self,
                   qurl: QUrl,
                   timeout: int = 20000,
                   data: bytes = b'',
                   content_type=None):
        '''
        synchronous POST-request
        '''
        request = QNetworkRequest(qurl)
        if content_type:
            request.setHeader(QNetworkRequest.ContentTypeHeader, content_type)
        # newer versions of QGIS (3.6+) support synchronous requests
        if hasattr(self._manager, 'blockingPost'):
            reply = self._manager.blockingPost(request,
                                               data,
                                               forceRefresh=True)
        # use blocking event loop for older versions
        else:
            loop = QEventLoop()
            timer = QTimer()
            timer.setSingleShot(True)
            # reply or timeout break event loop, whoever comes first
            timer.timeout.connect(loop.quit)
            reply = self._manager.post(request, data)
            reply.finished.connect(loop.quit)

            timer.start(timeout)

            # start blocking loop
            loop.exec()
            loop.deleteLater()
            if not timer.isActive():
                reply.deleteLater()
                raise ConnectionError('Timeout')

            timer.stop()
        if reply.error():
            self.error.emit(reply.errorString())
            raise ConnectionError(reply.errorString())
        res = Reply(reply)
        self.finished.emit(res)
        return res
예제 #8
0
    def get_topping_file_model(self, id_list):
        topping_file_cache = IliToppingFileCache(
            self.import_schema_configuration.base_configuration, id_list)

        # we wait for the download or we timeout after 30 seconds and we apply what we have
        loop = QEventLoop()
        topping_file_cache.download_finished.connect(lambda: loop.quit())
        timer = QTimer()
        timer.setSingleShot(True)
        timer.timeout.connect(lambda: loop.quit())
        timer.start(30000)

        topping_file_cache.refresh()
        self.log_panel.print_info(self.tr("- - Downloading…"),
                                  LogColor.COLOR_TOPPING)

        if len(topping_file_cache.downloaded_files) != len(id_list):
            loop.exec()

        if len(topping_file_cache.downloaded_files) == len(id_list):
            self.log_panel.print_info(
                self.tr("- - All topping files successfully downloaded"),
                LogColor.COLOR_TOPPING,
            )
        else:
            missing_file_ids = id_list
            for downloaded_file_id in topping_file_cache.downloaded_files:
                if downloaded_file_id in missing_file_ids:
                    missing_file_ids.remove(downloaded_file_id)
            self.log_panel.print_info(
                self.
                tr("- - Some topping files where not successfully downloaded: {}"
                   ).format(" ".join(missing_file_ids)),
                LogColor.COLOR_TOPPING,
            )

        return topping_file_cache.model
예제 #9
0
class VisualizerDock(QDockWidget, FORM_CLASS):
    # Evento para quando o plugin é fechado
    closingPlugin = pyqtSignal()

    def __init__(self, iface):
        super(VisualizerDock, self).__init__()

        self.iface = iface
        self.request_error = False

        self.timer = QTimer()
        self.timer.setSingleShot(True)
        self.timer.setInterval(7000)
        self.timer.timeout.connect(lambda: self.lb_status.setText('Pronto'))

        self.setupUi(self)

        self.list_resource = ResourceTreeWidgetDecorator(self.list_resource)

        self.load_resources_from_model()

        # Eventos
        self.bt_add_resource.clicked.connect(self._bt_add_resource_clicked)
        self.bt_remove_resource.clicked.connect(self._bt_remove_resource_clicked)

        self.list_resource.setContextMenuPolicy(Qt.CustomContextMenu)
        self.list_resource.customContextMenuRequested.connect(self.open_context_menu)
        self.list_resource.leaf_resource_double_clicked.connect(self._list_resource_doubleClicked)

        #self.tx_quick_resource.returnPressed.connect(self._tx_quick_resource_pressed)

    def _tx_quick_resource_pressed(self):
        def extract_name(url):
            return url.strip(' /').split('/')[-1]

        url = self.tx_quick_resource.text()
        self.tx_quick_resource.clear()

        if url:
            name = extract_name(url)
            self.add_resource(name, url)


    def _list_resource_doubleClicked(self, item):
        name, iri = item.text(0), item.text(1)
        self.open_operations_editor(name, iri)

    def _bt_add_resource_clicked(self):
        self.open_add_resource_dialog()

    def _bt_remove_resource_clicked(self):
        selected_items = self.list_resource.selectedItems()

        if not selected_items:
            return

        confirm = MessageBox.question(u'Deseja realmente remover o recurso selecionado?', u'Remover Recurso')

        if confirm:
            memorized_urls = Config.get('memo_urls')

            item_name = selected_items[0].text(0)

            if item_name in memorized_urls:
                index = self.list_resource.indexOfTopLevelItem(selected_items[0])
                self.list_resource.takeTopLevelItem(index)

                memorized_urls.pop(item_name)
                Config.update_dict('memo_urls', memorized_urls)

    def load_resource(self, resource):
        parent_item = self.list_resource.add(resource)
        parent_item.set_color_user_resource()

    def add_resource(self, name, url):
        resource = ResourceManager.load(url, name)
        self.load_resource(resource)

        Config.set('memo_urls', {name: url})

    def load_resources_from_model(self):
        model = Config.get('memo_urls')
        if not model:
            return

        for name, iri in model.items():
            resource = ResourceManager.load(iri, name)
            self.load_resource(resource)

    def open_context_menu(self, position):
        item = self.list_resource.itemAt(position)
        if not item:
            return

        resource = ResourceManager.load(item.url(), item.name())
        is_leaf_node = item.childCount() == 0
        if is_leaf_node:
            self.show_context_menu(item, resource, position)
        else:
            self.show_entry_point_menu(item, resource, position)

    def show_context_menu(self, item, resource, where):
        menu = QMenu()

        # Load layers action
        action_open_layer = QAction(self.tr(u'Carregar camada'), None)
        action_open_layer.triggered.connect(lambda: self._load_layer_on_qgis(resource))
        menu.addAction(action_open_layer)

        # Load layers as...
        # action_open_layer_as = QAction(self.tr(u'Carregar camada como...'), None)
        # action_open_layer_as.triggered.connect(lambda: self._load_layer_from_url(item.text(0), item.text(1)))
        # menu.addAction(action_open_layer_as)

        menu.addSeparator()

        # Montar Operações
        action_open_editor = QAction(self.tr(u'Montar operações'), None)
        action_open_editor.triggered.connect(lambda: self.open_operations_editor(item.text(0), item.text(1)))
        menu.addAction(action_open_editor)

        menu.addSeparator()

        action_edit = QAction(self.tr(u'Editar'), None)
        action_edit.triggered.connect(lambda: self.open_edit_dialog(item))
        menu.addAction(action_edit)

        menu.exec_(self.list_resource.viewport().mapToGlobal(where))

    def show_entry_point_menu(self, item, resource, where):
        menu = QMenu()

        action_edit = QAction(self.tr(u'Editar'), None)
        action_edit.triggered.connect(lambda: self.open_edit_dialog(item))
        menu.addAction(action_edit)

        menu.exec_(self.list_resource.viewport().mapToGlobal(where))

    def open_edit_dialog(self, item):
        dialog_edit_resource = DialogEditResource(item)
        dialog_edit_resource.accepted.connect(self._resource_edited)
        dialog_edit_resource.exec_()

        return dialog_edit_resource

    def open_operations_editor(self, name, url):
        resource = ResourceManager.load(url, name)

        dialog_construct_url = DialogConstructUrl(resource)
        dialog_construct_url.load_url_command.connect(lambda n, i: self._load_layer_from_iri(dialog_construct_url, n, i))
        dialog_construct_url.exec_()

        return dialog_construct_url

    def open_add_resource_dialog(self):
        dialog_add_resource = DialogAddResource()
        dialog_add_resource.accepted.connect(self.add_resource)
        dialog_add_resource.exec_()

        return dialog_add_resource

    def _resource_edited(self, tree_item, new_name, new_url):
        memo = Config.get('memo_urls')

        old = memo.pop(tree_item.text(0))
        new = {new_name: new_url}

        memo.update(new)
        Config.update_dict('memo_urls', memo)

        tree_item.setText(0, new_name)
        tree_item.setText(1, new_url)

    def _load_layer_from_iri(self, widget, name, iri):
        try:
            dummy = ResourceManager.load(iri, name)
            self._load_layer_on_qgis(dummy)
            widget.close()

        except Exception as e:
            raise

    def _load_layer_on_qgis(self, resource):
        def request_failed(error):
            self.request_error = error
            self.update_status(u'Requisição retornou um erro')
            MessageBox.critical(error, u'Requisição retornou um erro')

        self.request_error = False

        self.timer.stop()
        resource.request_started.connect(self.start_request)
        resource.request_progress.connect(self.download_progress)
        resource.request_error.connect(request_failed)
        resource.request_finished.connect(self.trigger_reset_status)

        # Trigger download of data and events for user feedback
        resource.data()

        if not self.request_error:
            layer = Plugin.create_layer(resource)

            if layer:
                Layer.add(layer)

            if layer.featureCount() == 0:
                Logging.info(u'{} retornou conjunto vazio de dados'.format(resource.iri), u'IBGEVisualizer')
                MessageBox.info(u'URL retornou conjunto vazio')

        else:
            raise Exception(self.request_error)

    def start_request(self):
        self.request_error = False
        self.timer.stop()
        self.lb_status.setText(u'Enviando requisição e aguardando resposta...')

    def update_status(self, msg):
        self.lb_status.setText(msg)

    def download_progress(self, received, total):
        if not self.request_error:
            if received == total:
                msg = u'Concluído ' + received
            else:
                msg = u'Baixando recurso... ' + received + (' / ' + total if total != '-1.0' else '')

            self.update_status(msg)

    def trigger_reset_status(self):
        if not self.request_error:
            #self.request_error = False
            self.timer.start()

    def close_event(self, event):
        self.closingPlugin.emit()
        event.accept()

    def run(self):
        self.iface.addDockWidget(Qt.RightDockWidgetArea, self)

        self.show()
예제 #10
0
class OpenLayersOverviewWidget(QWidget, Ui_Form):

    def __init__(self, iface, dockwidget, olLayerTypeRegistry):
        QWidget.__init__(self)
        Ui_Form.__init__(self)
        self.setupUi(self)
        self.__canvas = iface.mapCanvas()
        self.__dockwidget = dockwidget
        self.__olLayerTypeRegistry = olLayerTypeRegistry
        self.__initLayerOL = False
        self.__fileNameImg = ''
        self.__srsOL = QgsCoordinateReferenceSystem(
            3857, QgsCoordinateReferenceSystem.EpsgCrsId)
        self.__marker = MarkerCursor(self.__canvas, self.__srsOL)
        self.__manager = None  # Need persist for PROXY
        bindogr.initOgr()
        self.__init()

    def __init(self):
        self.checkBoxHideCross.setEnabled(False)
        self.__populateTypeMapGUI()
        self.__populateButtonBox()
        self.__registerObjJS()
        self.lbStatusRead.setVisible(False)
        self.__setConnections()

        self.__timerMapReady = QTimer()
        self.__timerMapReady.setSingleShot(True)
        self.__timerMapReady.setInterval(20)
        self.__timerMapReady.timeout.connect(self.__checkMapReady)

    def __del__(self):
        self.__marker.reset()
    # Disconnect Canvas
    # Canvas
        QgsMapCanvas.extentsChanged.disconnect(self.__canvas)
    # Doc WidgetparentWidget
        QDockWidget.visibilityChanged.disconnect(self.__dockwidget)

    def __populateButtonBox(self):
        pathPlugin = "%s%s%%s" % (os.path.dirname(__file__), os.path.sep)
        self.pbRefresh.setIcon(QIcon(pathPlugin % "mActionDraw.png"))
        self.pbRefresh.setEnabled(False)
        self.pbAddRaster.setIcon(QIcon(pathPlugin %
                                       "mActionAddRasterLayer.png"))
        self.pbAddRaster.setEnabled(False)
        self.pbCopyKml.setIcon(QIcon(pathPlugin % "kml.png"))
        self.pbCopyKml.setEnabled(False)
        self.pbSaveImg.setIcon(QIcon(pathPlugin % "mActionSaveMapAsImage.png"))
        self.pbSaveImg.setEnabled(False)

    def __populateTypeMapGUI(self):
        pathPlugin = "%s%s%%s" % (os.path.dirname(__file__), os.path.sep)
        totalLayers = len(self.__olLayerTypeRegistry.types())
        for id in range(totalLayers):
            layer = self.__olLayerTypeRegistry.getById(id)
            name = str(layer.displayName)
            icon = QIcon(pathPlugin % layer.groupIcon)
            self.comboBoxTypeMap.addItem(icon, name, id)

    def __setConnections(self):
        # Check Box
        self.checkBoxEnableMap.stateChanged.connect(
            self.__signal_checkBoxEnableMap_stateChanged)
        self.checkBoxHideCross.stateChanged.connect(
            self.__signal_checkBoxHideCross_stateChanged)
        # comboBoxTypeMap
        self.comboBoxTypeMap.currentIndexChanged.connect(
            self.__signal_comboBoxTypeMap_currentIndexChanged)
        # Canvas
        self.__canvas.extentsChanged.connect(
            self.__signal_canvas_extentsChanged)
        # Doc WidgetparentWidget
        self.__dockwidget.visibilityChanged.connect(
            self.__signal_DocWidget_visibilityChanged)
        # WebView Map
        self.webViewMap.page().mainFrame().javaScriptWindowObjectCleared.connect(
            self.__registerObjJS)
        # Push Button
        self.pbRefresh.clicked.connect(
            self.__signal_pbRefresh_clicked)
        self.pbAddRaster.clicked.connect(
            self.__signal_pbAddRaster_clicked)
        self.pbCopyKml.clicked.connect(
            self.__signal_pbCopyKml_clicked)
        self.pbSaveImg.clicked.connect(
            self.__signal_pbSaveImg_clicked)

    def __registerObjJS(self):
        self.webViewMap.page().mainFrame().addToJavaScriptWindowObject(
            "MarkerCursorQGis", self.__marker)

    def __signal_checkBoxEnableMap_stateChanged(self, state):
        enable = False
        if state == Qt.Unchecked:
            self.__marker.reset()
        else:
            if self.__canvas.layerCount() == 0:
                QMessageBox.warning(self, QApplication.translate(
                    "OpenLayersOverviewWidget",
                    "OpenLayers Overview"), QApplication.translate(
                        "OpenLayersOverviewWidget",
                        "At least one layer in map canvas required"))
                self.checkBoxEnableMap.setCheckState(Qt.Unchecked)
            else:
                enable = True
                if not self.__initLayerOL:
                    self.__initLayerOL = True
                    self.__setWebViewMap(0)
                else:
                    self.__refreshMapOL()
        # GUI
        if enable:
            self.lbStatusRead.setVisible(False)
            self.webViewMap.setVisible(True)
        else:
            self.lbStatusRead.setText("")
            self.lbStatusRead.setVisible(True)
            self.webViewMap.setVisible(False)
        self.webViewMap.setEnabled(enable)
        self.comboBoxTypeMap.setEnabled(enable)
        self.pbRefresh.setEnabled(enable)
        self.pbAddRaster.setEnabled(enable)
        self.pbCopyKml.setEnabled(enable)
        self.pbSaveImg.setEnabled(enable)
        self.checkBoxHideCross.setEnabled(enable)

    def __signal_checkBoxHideCross_stateChanged(self, state):
        if state == Qt.Checked:
            self.__marker.reset()
            self.__marker.setVisible(False)
        else:
            self.__marker.setVisible(True)
            self.__refreshMapOL()

    def __signal_DocWidget_visibilityChanged(self, visible):
        if self.__canvas.layerCount() == 0:
            return
        self.checkBoxEnableMap.setCheckState(Qt.Unchecked)
        self.__signal_checkBoxEnableMap_stateChanged(Qt.Unchecked)

    def __signal_comboBoxTypeMap_currentIndexChanged(self, index):
        self.__setWebViewMap(index)

    def __signal_canvas_extentsChanged(self):
        if self.__canvas.layerCount() == 0 or not self.webViewMap.isVisible():
            return
        if self.checkBoxEnableMap.checkState() == Qt.Checked:
            self.__refreshMapOL()

    def __signal_pbRefresh_clicked(self, checked):
        index = self.comboBoxTypeMap.currentIndex()
        self.__setWebViewMap(index)

    def __signal_pbAddRaster_clicked(self, checked):
        index = self.comboBoxTypeMap.currentIndex()
        layer = self.__olLayerTypeRegistry.getById(index)
        QGuiApplication.setOverrideCursor(Qt.WaitCursor)
        layer.addLayer()
        QGuiApplication.restoreOverrideCursor()

    def __signal_pbCopyKml_clicked(self, cheked):
        # Extent Openlayers
        action = "map.getExtent().toGeometry().toString();"
        wkt = self.webViewMap.page().mainFrame().evaluateJavaScript(action)
        rect = QgsGeometry.fromWkt(wkt).boundingBox()
        srsGE = QgsCoordinateReferenceSystem(
            4326, QgsCoordinateReferenceSystem.EpsgCrsId)
        coodTrans = QgsCoordinateTransform(self.__srsOL, srsGE,
                                           QgsProject.instance())
        rect = coodTrans.transform(
            rect, QgsCoordinateTransform.ForwardTransform)
        line = QgsGeometry.fromRect(rect).asPolygon()[0]
        wkt = str(QgsGeometry.fromPolylineXY(line).asWkt())
        # Kml
        proj4 = str(srsGE.toProj4())
        kmlLine = bindogr.exportKml(wkt, proj4)
        kml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"\
              "<kml xmlns=\"http://www.opengis.net/kml/2.2\" " \
              "xmlns:gx=\"http://www.google.com/kml/ext/2.2\" " \
              "xmlns:kml=\"http://www.opengis.net/kml/2.2\" " \
              "xmlns:atom=\"http://www.w3.org/2005/Atom\">" \
              "<Placemark>" \
              "<name>KML from Plugin Openlayers Overview for QGIS</name>" \
              "<description>Extent of openlayers map from Plugin Openlayers \
              Overview for QGIS</description>"\
              "%s" \
              "</Placemark></kml>" % kmlLine
        clipBoard = QApplication.clipboard()
        clipBoard.setText(kml)

    def __signal_pbSaveImg_clicked(self, cheked):
        if type(self.__fileNameImg) == tuple:
            self.__fileNameImg = self.__fileNameImg[0]
        fileName = QFileDialog.getSaveFileName(self,
                                               QApplication.translate(
                                                   "OpenLayersOverviewWidget",
                                                   "Save image"),
                                               self.__fileNameImg,
                                               QApplication.translate(
                                                   "OpenLayersOverviewWidget",
                                                   "Image(*.jpg)"))
        if not fileName == '':
            self.__fileNameImg = fileName
        else:
            return
        img = QImage(self.webViewMap.page().mainFrame().contentsSize(),
                     QImage.Format_ARGB32_Premultiplied)
        imgPainter = QPainter()
        imgPainter.begin(img)
        self.webViewMap.page().mainFrame().render(imgPainter)
        imgPainter.end()
        img.save(fileName[0], "JPEG")

    def __signal_webViewMap_loadFinished(self, ok):
        if ok is False:
            QMessageBox.warning(self, QApplication.translate(
                "OpenLayersOverviewWidget", "OpenLayers Overview"),
                                QApplication.translate(
                                    "OpenLayersOverviewWidget",
                                    "Error loading page!"))
        else:
            # wait until OpenLayers map is ready
            self.__checkMapReady()
        self.lbStatusRead.setVisible(False)
        self.webViewMap.setVisible(True)
        self.webViewMap.page().mainFrame().loadFinished.disconnect(
            self.__signal_webViewMap_loadFinished)

    def __setWebViewMap(self, id):
        layer = self.__olLayerTypeRegistry.getById(id)
        self.lbStatusRead.setText("Loading " + layer.displayName + " ...")
        self.lbStatusRead.setVisible(True)
        self.webViewMap.setVisible(False)
        self.webViewMap.page().mainFrame().loadFinished.connect(
            self.__signal_webViewMap_loadFinished)
        url = layer.html_url()
        self.webViewMap.page().mainFrame().load(QUrl(url))

    def __checkMapReady(self):
        if self.webViewMap.page().mainFrame().evaluateJavaScript(
                "map != undefined"):
            # map ready
            self.__refreshMapOL()
        else:
            # wait for map
            self.__timerMapReady.start()

    def __refreshMapOL(self):
        # catch Exception where lat/long exceed limit of the loaded layer
        # the Exception name is unknown
        latlon = None
        try:
            latlon = self.__getCenterLongLat2OL()
        except Exception as e:
            QgsLogger().warning(e.args[0])

        if latlon:
            action = "map.setCenter(new OpenLayers.LonLat(%f, %f));" % (latlon)
            self.webViewMap.page().mainFrame().evaluateJavaScript(action)
            action = "map.zoomToScale(%f);" % self.__canvas.scale()
            self.webViewMap.page().mainFrame().evaluateJavaScript(action)
            self.webViewMap.page().mainFrame().evaluateJavaScript(
                "oloMarker.changeMarker();")

    def __getCenterLongLat2OL(self):
        pntCenter = self.__canvas.extent().center()
        crsCanvas = self.__canvas.mapSettings().destinationCrs()
        if crsCanvas != self.__srsOL:
            coodTrans = QgsCoordinateTransform(crsCanvas, self.__srsOL,
                                               QgsProject.instance())
            pntCenter = coodTrans.transform(
                pntCenter, QgsCoordinateTransform.ForwardTransform)
        return tuple([pntCenter.x(), pntCenter.y()])
class OpenlayersController(QObject):
    """
    Helper class that deals with QWebPage.
    The object lives in GUI thread, its request() slot is asynchronously called
    from worker thread.
    See https://github.com/wonder-sk/qgis-mtr-example-plugin for basic example
    1. Load Web Page with OpenLayers map
    2. Update OL map extend according to QGIS canvas extent
    """

    # signal that reports to the worker thread that the image is ready
    finished = pyqtSignal()

    def __init__(self, parent, context, webPage, layerType):
        QObject.__init__(self, parent)

        debug("OpenlayersController.__init__", 3)
        self.context = context
        self.layerType = layerType

        self.img = QImage()

        self.page = webPage
        self.page.loadFinished.connect(self.pageLoaded)
        # initial size for map
        self.page.setViewportSize(QSize(1, 1))

        self.timerMapReady = QTimer()
        self.timerMapReady.setSingleShot(True)
        self.timerMapReady.setInterval(20)
        self.timerMapReady.timeout.connect(self.checkMapReady)

        self.timer = QTimer()
        self.timer.setInterval(100)
        self.timer.timeout.connect(self.checkMapUpdate)

        self.timerMax = QTimer()
        self.timerMax.setSingleShot(True)
        # TODO: different timeouts for map types
        self.timerMax.setInterval(2500)
        self.timerMax.timeout.connect(self.mapTimeout)

    @pyqtSlot()
    def request(self):
        debug("[GUI THREAD] Processing request", 3)
        self.cancelled = False

        if not self.page.loaded:
            self.init_page()
        else:
            self.setup_map()

    def init_page(self):
        url = self.layerType.html_url()
        debug("page file: %s" % url)
        self.page.mainFrame().load(QUrl(url))
        # wait for page to finish loading
        debug("OpenlayersWorker waiting for page to load", 3)

    def pageLoaded(self):
        debug("[GUI THREAD] pageLoaded", 3)
        if self.cancelled:
            self.emitErrorImage()
            return

        # wait until OpenLayers map is ready
        self.checkMapReady()

    def checkMapReady(self):
        debug("[GUI THREAD] checkMapReady", 3)
        if self.page.mainFrame().evaluateJavaScript("map != undefined"):
            # map ready
            self.page.loaded = True
            self.setup_map()
        else:
            # wait for map
            self.timerMapReady.start()

    def setup_map(self):
        rendererContext = self.context

        # FIXME: self.mapSettings.outputDpi()
        self.outputDpi = rendererContext.painter().device().logicalDpiX()
        debug(" extent: %s" % rendererContext.extent().toString(), 3)
        debug(" center: %lf, %lf" % (rendererContext.extent().center().x(),
                                     rendererContext.extent().center().y()), 3)
        debug(" size: %d, %d" % (
            rendererContext.painter().viewport().size().width(),
              rendererContext.painter().viewport().size().height()), 3)
        debug(" logicalDpiX: %d" % rendererContext.painter().
              device().logicalDpiX(), 3)
        debug(" outputDpi: %lf" % self.outputDpi)
        debug(" mapUnitsPerPixel: %f" % rendererContext.mapToPixel().
              mapUnitsPerPixel(), 3)
        # debug(" rasterScaleFactor: %s" % str(rendererContext.
        #                                      rasterScaleFactor()), 3)
        # debug(" outputSize: %d, %d" % (self.iface.mapCanvas().mapRenderer().
        #                                outputSize().width(),
        #                                self.iface.mapCanvas().mapRenderer().
        #                                outputSize().height()), 3)
        # debug(" scale: %lf" % self.iface.mapCanvas().mapRenderer().scale(),
        #                                3)
        #
        # if (self.page.lastExtent == rendererContext.extent()
        #         and self.page.lastViewPortSize == rendererContext.painter().
        #         viewport().size()
        #         and self.page.lastLogicalDpi == rendererContext.painter().
        #         device().logicalDpiX()
        #         and self.page.lastOutputDpi == self.outputDpi
        #         and self.page.lastMapUnitsPerPixel == rendererContext.
        #         mapToPixel().mapUnitsPerPixel()):
        #     self.renderMap()
        #     self.finished.emit()
        #     return

        self.targetSize = rendererContext.painter().viewport().size()
        if rendererContext.painter().device().logicalDpiX() != int(self.outputDpi):
            # use screen dpi for printing
            sizeFact = self.outputDpi / 25.4 / rendererContext.mapToPixel().mapUnitsPerPixel()
            self.targetSize.setWidth(
                rendererContext.extent().width() * sizeFact)
            self.targetSize.setHeight(
                rendererContext.extent().height() * sizeFact)
        debug(" targetSize: %d, %d" % (
            self.targetSize.width(), self.targetSize.height()), 3)

        # find matching OL resolution
        qgisRes = rendererContext.extent().width() / self.targetSize.width()
        olRes = None
        for res in self.page.resolutions():
            if qgisRes >= res:
                olRes = res
                break
        if olRes is None:
            debug("No matching OL resolution found (QGIS resolution: %f)" %
                  qgisRes)
            self.emitErrorImage()
            return

        # adjust OpenLayers viewport to match QGIS extent
        olWidth = rendererContext.extent().width() / olRes
        olHeight = rendererContext.extent().height() / olRes
        debug("    adjust viewport: %f -> %f: %f x %f" % (qgisRes,
                                                          olRes, olWidth,
                                                          olHeight), 3)
        olSize = QSize(int(olWidth), int(olHeight))
        self.page.setViewportSize(olSize)
        self.page.mainFrame().evaluateJavaScript("map.updateSize();")
        self.img = QImage(olSize, QImage.Format_ARGB32_Premultiplied)

        self.page.extent = rendererContext.extent()
        debug("map.zoomToExtent (%f, %f, %f, %f)" % (
            self.page.extent.xMinimum(), self.page.extent.yMinimum(),
            self.page.extent.xMaximum(), self.page.extent.yMaximum()), 3)
        self.page.mainFrame().evaluateJavaScript(
            "map.zoomToExtent(new OpenLayers.Bounds(%f, %f, %f, %f), true);" %
            (self.page.extent.xMinimum(), self.page.extent.yMinimum(),
                self.page.extent.xMaximum(), self.page.extent.yMaximum()))
        olextent = self.page.mainFrame().evaluateJavaScript("map.getExtent();")
        debug("Resulting OL extent: %s" % olextent, 3)
        if olextent is None or not hasattr(olextent, '__getitem__'):
            debug("map.zoomToExtent failed")
            # map.setCenter and other operations throw "undefined[0]:
            # TypeError: 'null' is not an object" on first page load
            # We ignore that and render the initial map with wrong extents
            # self.emitErrorImage()
            # return
        else:
            reloffset = abs((self.page.extent.yMaximum()-olextent[
                "top"])/self.page.extent.xMinimum())
            debug("relative offset yMaximum %f" % reloffset, 3)
            if reloffset > 0.1:
                debug("OL extent shift failure: %s" % reloffset)
                self.emitErrorImage()
                return
        self.mapFinished = False
        self.timer.start()
        self.timerMax.start()

    def checkMapUpdate(self):
        if self.layerType.emitsLoadEnd:
            # wait for OpenLayers to finish loading
            loadEndOL = self.page.mainFrame().evaluateJavaScript("loadEnd")
            debug("waiting for loadEnd: renderingStopped=%r loadEndOL=%r" % (
                  self.context.renderingStopped(), loadEndOL), 4)
            if loadEndOL is not None:
                self.mapFinished = loadEndOL
            else:
                debug("OpenlayersLayer Warning: Could not get loadEnd")

        if self.mapFinished:
            self.timerMax.stop()
            self.timer.stop()

            self.renderMap()

            self.finished.emit()

    def renderMap(self):
        rendererContext = self.context
        if rendererContext.painter().device().logicalDpiX() != int(self.outputDpi):
            printScale = 25.4 / self.outputDpi  # OL DPI to printer pixels
            rendererContext.painter().scale(printScale, printScale)

        # render OpenLayers to image
        painter = QPainter(self.img)
        self.page.mainFrame().render(painter)
        painter.end()

        if self.img.size() != self.targetSize:
            targetWidth = self.targetSize.width()
            targetHeight = self.targetSize.height()
            # scale using QImage for better quality
            debug("    scale image: %i x %i -> %i x %i" % (
                self.img.width(), self.img.height(),
                  targetWidth, targetHeight), 3)
            self.img = self.img.scaled(targetWidth, targetHeight,
                                       Qt.KeepAspectRatio,
                                       Qt.SmoothTransformation)

        # save current state
        self.page.lastExtent = rendererContext.extent()
        self.page.lastViewPortSize = rendererContext.painter().viewport().size()
        self.page.lastLogicalDpi = rendererContext.painter().device().logicalDpiX()
        self.page.lastOutputDpi = self.outputDpi
        self.page.lastMapUnitsPerPixel = rendererContext.mapToPixel().mapUnitsPerPixel()

    def mapTimeout(self):
        debug("mapTimeout reached")
        self.timer.stop()
        # if not self.layerType.emitsLoadEnd:
        self.renderMap()
        self.finished.emit()

    def emitErrorImage(self):
        self.img = QImage()
        self.targetSize = self.img.size
        self.finished.emit()
class GeomapfishLocatorFilter(QgsLocatorFilter):

    USER_AGENT = b'Mozilla/5.0 QGIS GeoMapFish Locator Filter'

    changed = pyqtSignal()

    def __init__(self, service: Service, iface: QgisInterface = None):
        super().__init__()
        self.service = service.clone()
        self.rubberband = None
        self.iface = None
        self.map_canvas = None
        self.current_timer = None
        self.settings = Settings()
        self.crs = QgsCoordinateReferenceSystem(self.service.crs)

        # only get map_canvas on main thread, not when cloning
        if iface is not None:
            self.iface = iface
            self.map_canvas = iface.mapCanvas()

            self.rubberband = QgsRubberBand(self.map_canvas)
            self.reset_rubberband()

    def name(self) -> str:
        return slugify(self.displayName())

    def clone(self):
        return GeomapfishLocatorFilter(self.service)

    def displayName(self) -> str:
        return self.service.name

    def prefix(self) -> str:
        return 'gmf'

    def hasConfigWidget(self) -> bool:
        return True

    def openConfigWidget(self, parent=None):
        cfg = FilterConfigurationDialog(self.service, parent)
        if cfg.exec_():
            self.service = cfg.service.clone()
            self.crs = QgsCoordinateReferenceSystem(self.service.crs)
            self.changed.emit()

    def reset_rubberband(self):
        # this should happen on main thread only!
        self.rubberband.setColor(self.settings.value('point_color'))
        self.rubberband.setIcon(self.rubberband.ICON_CIRCLE)
        self.rubberband.setIconSize(self.settings.value('point_size'))
        self.rubberband.setWidth(self.settings.value('line_width'))
        self.rubberband.setBrushStyle(Qt.NoBrush)

    @staticmethod
    def url_with_param(url, params) -> str:
        url = QUrl(url)
        q = QUrlQuery(url)
        for key, value in params.items():
            q.addQueryItem(key, value)
        url.setQuery(q)
        return url

    def emit_bad_configuration(self, err=None):
        result = QgsLocatorResult()
        result.filter = self
        result.displayString = self.tr('Locator filter is not configured.')
        result.description = err if err else self.tr(
            'Double-click to configure it.')
        result.userData = FilterNotConfigured
        result.icon = QgsApplication.getThemeIcon('mIconWarning.svg')
        self.resultFetched.emit(result)
        return

    @pyqtSlot()
    def clear_results(self):
        if self.rubberband:
            self.rubberband.reset(QgsWkbTypes.PointGeometry)
        if self.current_timer is not None:
            self.current_timer.timeout.disconnect(self.clear_results)
            self.current_timer.stop()
            self.current_timer.deleteLater()
            self.current_timer = None

    def fetchResults(self, search, context, feedback):
        self.dbg_info("start GMF locator search...")
        url = self.service.url
        if not url:
            self.emit_bad_configuration()
            return
        if len(search) < 2:
            return

        params = {
            'query': search,
            'limit': str(self.service.total_limit),
            'partitionlimit': str(self.service.category_limit)
        }
        url = self.url_with_param(url, params)
        self.dbg_info(url.url())

        _request = QNetworkRequest(url)
        _request.setRawHeader(b'User-Agent', self.USER_AGENT)
        request = QgsBlockingNetworkRequest()
        if self.service.authid:
            request.setAuthCfg(self.service.authid)

        response = request.get(_request, False, feedback)
        if response == QgsBlockingNetworkRequest.NoError:
            self.handle_response(request.reply().content())
        else:
            error_msg = request.reply().errorString()
            self.emit_bad_configuration(error_msg)
            self.info(error_msg, Qgis.Critical)
        self.finished.emit()

    def handle_response(self, content: QByteArray):
        try:
            data = json.loads(str(content.data(), encoding='utf-8'))
            # self.dbg_info(data)

            features = data['features']
            for f in features:
                json_geom = json.dumps(f['geometry'])
                ogr_geom = ogr.CreateGeometryFromJson(json_geom)
                wkt = ogr_geom.ExportToWkt()
                geometry = QgsGeometry.fromWkt(wkt)
                self.dbg_info('---------')
                self.dbg_info(
                    QgsWkbTypes.geometryDisplayString(geometry.type()))
                self.dbg_info(f.keys())
                self.dbg_info('{} {}'.format(f['properties']['layer_name'],
                                             f['properties']['label']))
                self.dbg_info(f['bbox'])
                self.dbg_info(f['geometry'])
                if geometry is None:
                    continue
                result = QgsLocatorResult()
                result.filter = self
                result.displayString = f['properties']['label']
                result.group = self.beautify_group(
                    f['properties']['layer_name'])
                result.userData = geometry
                self.resultFetched.emit(result)

        except Exception as e:
            self.info(str(e), Qgis.Critical)
            # exc_type, exc_obj, exc_traceback = sys.exc_info()
            # #filename = os.path.split(exc_traceback.tb_frame.f_code.co_filename)[1]
            # #self.info('{} {} {}'.format(exc_type, filename, exc_traceback.tb_lineno), Qgis.Critical)
            # self.info(traceback.print_exception(exc_type, exc_obj, exc_traceback), Qgis.Critical)

    def triggerResult(self, result):
        self.clear_results()
        if result.userData == FilterNotConfigured:
            self.openConfigWidget()
            self.iface.invalidateLocatorResults()
            return

        # this should be run in the main thread, i.e. mapCanvas should not be None
        geometry = result.userData
        # geometry.transform(self.transform)
        dbg_info(str(geometry.asWkt()))
        dbg_info(geometry.type())

        try:
            rect = QgsReferencedRectangle(geometry.boundingBox(), self.crs)
            rect.scale(4)
            self.map_canvas.setReferencedExtent(rect)
        except AttributeError:
            # QGIS < 3.10 handling
            from qgis.core import QgsCoordinateTransform, QgsProject
            transform = QgsCoordinateTransform(
                self.crs,
                self.map_canvas.mapSettings().destinationCrs(),
                QgsProject.instance())
            geometry.transform(transform)
            rect = geometry.boundingBox()
            rect.scale(4)
            self.map_canvas.setExtent(rect)

        self.map_canvas.refresh()

        if geometry.type() == QgsWkbTypes.PolygonGeometry:
            nflash = 16
            color1: QColor = self.settings.value('polygon_color')
            color2 = color1
            color1.setAlpha(200)
            color2.setAlpha(100)
            self.map_canvas.flashGeometries(
                [geometry], self.crs, color1, color2, nflash,
                self.settings.value('highlight_duration') / nflash * 1000)
        else:
            self.rubberband.reset(geometry.type())
            self.rubberband.addGeometry(geometry, self.crs)

            self.current_timer = QTimer()
            self.current_timer.timeout.connect(self.clear_results)
            self.current_timer.setSingleShot(True)
            self.current_timer.start(
                self.settings.value('highlight_duration') * 1000)

    def beautify_group(self, group) -> str:
        if self.service.remove_leading_digits:
            group = re.sub('^[0-9]+', '', group)
        if self.service.replace_underscore:
            group = group.replace("_", " ")
        if self.service.break_camelcase:
            group = self.break_camelcase(group)
        return group

    def info(self, msg="", level=Qgis.Info):
        QgsMessageLog.logMessage('{} {}'.format(self.__class__.__name__, msg),
                                 'QgsLocatorFilter', level)

    def dbg_info(self, msg=""):
        if DEBUG:
            self.info(msg)

    @staticmethod
    def break_camelcase(identifier) -> str:
        matches = re.finditer(
            '.+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)',
            identifier)
        return ' '.join([m.group(0) for m in matches])
class GeoGigLiveLayerRefresher(object):

    nProgressBarsOpen = 0
    nfeaturesRead = 0
    lock = threading.RLock(
    )  # this might not be necessary - I think this will always be happening on the same ui thread

    def __init__(self,
                 connector,
                 geogiglayer,
                 fullDetail=False,
                 sm_factor=1.0,
                 sm_type="WithBBOX"):
        self.connector = connector
        self.geogiglayer = geogiglayer

        self.queryThread = QueryThread(self.connector)
        self.queryThread.started.connect(self.datasetStart)
        self.queryThread.finished.connect(self.datasetReceived)
        self.queryThread.progress_occurred.connect(self.featuresRead)

        self.refreshTimer = QTimer()
        self.refreshTimer.setSingleShot(True)
        self.refreshTimer.timeout.connect(self.makeQuery)

        self.lastExtent = None
        self.sm_factor = sm_factor
        self.sm_type = sm_type

        root = QgsProject.instance().layerTreeRoot()
        root.visibilityChanged.connect(
            self.visibilityChanged)  # track all layer visibility changes
        self.fullDetail = fullDetail
        #root.addedChildren.connect(self.layerTreeAddedTo) # track when layer is added to tree

    # called when layer is removed
    def cleanup(self):
        # don't track this anymore (it causes a problem because the c++ object is
        # deleted, but the python object isn't)
        root = QgsProject.instance().layerTreeRoot()
        root.visibilityChanged.disconnect(self.visibilityChanged)

    def isLayerVisible(self):
        if self.geogiglayer.layer is None:
            return None
        layerId = self.geogiglayer.layer.id()
        if self.geogiglayer.canvas is None:
            treelayer = QgsProject.instance().layerTreeRoot().findLayer(
                layerId)  # QgsLayerTreeLayer
            if treelayer is None:
                return False
            if not treelayer.isVisible():
                return False  # definitely not visible
        # likely visible, do a simple scale-range check
        return self.geogiglayer.layer.isInScaleRange(
            self.geogiglayer._canvas().scale())

    def visibilityChanged(self, qgsLayerTreeNode):
        if self.isLayerVisible():
            self.refresh(forceRefresh=False, tryToRepopulate=True)

    def openProgress(self):
        with self.lock:
            if self.nProgressBarsOpen == 0:
                self.nfeaturesRead = 0
                qgiscommons2.gui.startProgressBar(
                    "Transferring data from GeoGig", 0,
                    currentWindow().messageBar())
            self.nProgressBarsOpen += 1

    def closeProgress(self):
        with self.lock:
            self.nProgressBarsOpen -= 1
            if self.nProgressBarsOpen == 0:
                qgiscommons2.gui.closeProgressBar()

    # sometimes the progress bar can be closed by another thread/function
    #  this will re-open it if that happens.
    # ex. when you have a layers being populated() during a refresh()
    #     which can occur on project load
    def ensureProgressOpen(self):
        _progressActive = qgiscommons2.gui._progressActive
        if _progressActive:
            return  # nothing to do
        qgiscommons2.gui.startProgressBar("Transferring data from GeoGig", 0,
                                          currentWindow().messageBar())

    # called by backgrounding feature loader (self.queryThread)
    # this is for progress indication
    def featuresRead(self, nfeatsBatch):
        with self.lock:
            self.ensureProgressOpen()
            self.nfeaturesRead += nfeatsBatch
            try:
                qgiscommons2.gui.setProgressText(
                    "Read " + "{:,}".format(self.nfeaturesRead) +
                    " features...")
            except:
                pass  # could be a problem...

    # occurs when extents change, call this from geogiglayer
    def refresh(self, forceRefresh=True, tryToRepopulate=False):
        if tryToRepopulate and not self.geogiglayer.valid:
            try:
                self.geogiglayer.populate()
            except:
                item = QgsProject.instance().layerTreeRoot().findLayer(
                    self.geogiglayer.layer.id())
                item.setItemVisibilityCheckedRecursive(False)
                return
        if not forceRefresh:
            extentRect = self.geogiglayer.extentToLayerCrs(
                self.geogiglayer._canvas().extent())
            extent = [
                extentRect.xMinimum(),
                extentRect.yMinimum(),
                extentRect.xMaximum(),
                extentRect.yMaximum()
            ]
            if self.lastExtent == extent:
                return
        # set time -- will fire after 100ms and call makeQuery
        if self.refreshTimer.isActive():
            self.refreshTimer.setInterval(100)  # restart
        else:
            self.refreshTimer.start(100)

    #downloads the current extent at full detail. Called when entering or exiting the editing mode.
    def setFullDetail(self, fullDetail, refresh=True):
        self.fullDetail = fullDetail
        if refresh:
            self.makeQuery()

    def getFullDetail(self):
        return self.fullDetail

    # thread has started to do work
    def datasetStart(self, url, query):
        self.timeStart = time.perf_counter()
        QgsMessageLog.logMessage("loading dataset url={}, query={}".format(
            url, str(query)))

    # return true if you shouldn't draw this layer
    #   if its rules-based, and all the rules depend on scale, and all the rules are "out-of-scale"
    def doNotDrawScale(self, r, scale):
        if not isinstance(r, QgsRuleBasedRenderer):
            return False
        # any of them are NOT scale dependent, then need to draw
        if any([not r.dependsOnScale() for r in r.rootRule().children()]):
            return False
        return not any([r.isScaleOK(scale) for r in r.rootRule().children()])

    def ecqlFromLayerStyle(self):
        canvas = self.geogiglayer._canvas()
        ms = canvas.mapSettings()
        ctx = QgsRenderContext.fromMapSettings(ms)

        r = self.geogiglayer.layer.renderer().clone()
        try:
            r.startRender(ctx, self.geogiglayer.layer.fields())
            if self.doNotDrawScale(r, canvas.scale()):
                return "EXCLUDE"
            expression = r.filter()
            if expression == "" or expression == "TRUE":
                return None
            converter = ExpressionConverter(expression)
            return converter.asECQL()
        except:
            return None
        finally:
            r.stopRender(ctx)

    def makeQuery(self):
        self.queryThread.abort()
        self.queryThread.wait()  # wait for it to abort
        if not self.isLayerVisible():
            return  # don't do anything if the layer is invisible NOTE: layer likely has data in it
        self.openProgress()
        extent = self.geogiglayer.extentToLayerCrs(
            self.geogiglayer._canvas().extent())
        self.lastExtent = [
            extent.xMinimum(),
            extent.yMinimum(),
            extent.xMaximum(),
            extent.yMaximum()
        ]
        self.queryThread.createURL(self.geogiglayer.user,
                                   self.geogiglayer.repo,
                                   self.geogiglayer.layername)
        if self.fullDetail:
            self.queryThread.createQuery(self.geogiglayer.commitid,
                                         self.lastExtent,
                                         simplifyGeom=False,
                                         ecqlFilter=self.ecqlFromLayerStyle())
        else:
            self.queryThread.createQuery(self.geogiglayer.commitid,
                                         self.lastExtent,
                                         self.geogiglayer._canvas().width(),
                                         self.geogiglayer._canvas().height(),
                                         screenMap_factor=self.sm_factor,
                                         screenMap_type=self.sm_type,
                                         ecqlFilter=self.ecqlFromLayerStyle())
        self.queryThread.start()

    # called  by backgrounding feature loader (self.queryThread)
    # this is after the dataset has loaded.
    # None -> aborted
    def datasetReceived(self, memorydataset):
        if memorydataset is not None:
            end_time = time.perf_counter()
            QgsMessageLog.logMessage(
                "Dataset received ({}) - {:,} features in {}s".format(
                    self.geogiglayer.layername, len(memorydataset),
                    end_time - self.timeStart))
        self.closeProgress()
        if memorydataset is None:
            return
        try:
            self.geogiglayer.newDatasetReceived(memorydataset)
        except Exception as e:
            QgsMessageLog.logMessage("error - " + str(e))
class OpenLayersOverviewWidget(QWidget, Ui_Form):

    def __init__(self, iface, dockwidget, olLayerTypeRegistry):
        QWidget.__init__(self)
        Ui_Form.__init__(self)
        self.setupUi(self)
        self.__canvas = iface.mapCanvas()
        self.__dockwidget = dockwidget
        self.__olLayerTypeRegistry = olLayerTypeRegistry
        self.__initLayerOL = False
        self.__fileNameImg = ''
        self.__srsOL = QgsCoordinateReferenceSystem(
            3857, QgsCoordinateReferenceSystem.EpsgCrsId)
        self.__marker = MarkerCursor(self.__canvas, self.__srsOL)
        self.__manager = None  # Need persist for PROXY
        bindogr.initOgr()
        self.__init()

    def __init(self):
        self.checkBoxHideCross.setEnabled(False)
        self.__populateTypeMapGUI()
        self.__populateButtonBox()
        self.__registerObjJS()
        self.lbStatusRead.setVisible(False)
        self.__setConnections()

        self.__timerMapReady = QTimer()
        self.__timerMapReady.setSingleShot(True)
        self.__timerMapReady.setInterval(20)
        self.__timerMapReady.timeout.connect(self.__checkMapReady)

    def __del__(self):
        self.__marker.reset()
    # Disconnect Canvas
    # Canvas
        QgsMapCanvas.extentsChanged.disconnect(self.__canvas)
    # Doc WidgetparentWidget
        QDockWidget.visibilityChanged.disconnect(self.__dockwidget)

    def __populateButtonBox(self):
        pathPlugin = "%s%s%%s" % (os.path.dirname(__file__), os.path.sep)
        self.pbRefresh.setIcon(QIcon(pathPlugin % "mActionDraw.png"))
        self.pbRefresh.setEnabled(False)
        self.pbAddRaster.setIcon(QIcon(pathPlugin %
                                       "mActionAddRasterLayer.png"))
        self.pbAddRaster.setEnabled(False)
        self.pbCopyKml.setIcon(QIcon(pathPlugin % "kml.png"))
        self.pbCopyKml.setEnabled(False)
        self.pbSaveImg.setIcon(QIcon(pathPlugin % "mActionSaveMapAsImage.png"))
        self.pbSaveImg.setEnabled(False)

    def __populateTypeMapGUI(self):
        pathPlugin = "%s%s%%s" % (os.path.dirname(__file__), os.path.sep)
        totalLayers = len(self.__olLayerTypeRegistry.types())
        for id in range(totalLayers):
            layer = self.__olLayerTypeRegistry.getById(id)
            name = str(layer.displayName)
            icon = QIcon(pathPlugin % layer.groupIcon)
            self.comboBoxTypeMap.addItem(icon, name, id)

    def __setConnections(self):
        # Check Box
        self.checkBoxEnableMap.stateChanged.connect(
            self.__signal_checkBoxEnableMap_stateChanged)
        self.checkBoxHideCross.stateChanged.connect(
            self.__signal_checkBoxHideCross_stateChanged)
        # comboBoxTypeMap
        self.comboBoxTypeMap.currentIndexChanged.connect(
            self.__signal_comboBoxTypeMap_currentIndexChanged)
        # Canvas
        self.__canvas.extentsChanged.connect(
            self.__signal_canvas_extentsChanged)
        # Doc WidgetparentWidget
        self.__dockwidget.visibilityChanged.connect(
            self.__signal_DocWidget_visibilityChanged)
        # WebView Map
        self.webViewMap.page().mainFrame().javaScriptWindowObjectCleared.connect(
            self.__registerObjJS)
        # Push Button
        self.pbRefresh.clicked.connect(
            self.__signal_pbRefresh_clicked)
        self.pbAddRaster.clicked.connect(
            self.__signal_pbAddRaster_clicked)
        self.pbCopyKml.clicked.connect(
            self.__signal_pbCopyKml_clicked)
        self.pbSaveImg.clicked.connect(
            self.__signal_pbSaveImg_clicked)

    def __registerObjJS(self):
        self.webViewMap.page().mainFrame().addToJavaScriptWindowObject(
            "MarkerCursorQGis", self.__marker)

    def __signal_checkBoxEnableMap_stateChanged(self, state):
        enable = False
        if state == Qt.Unchecked:
            self.__marker.reset()
        else:
            if self.__canvas.layerCount() == 0:
                QMessageBox.warning(self, QApplication.translate(
                    "OpenLayersOverviewWidget",
                    "OpenLayers Overview"), QApplication.translate(
                        "OpenLayersOverviewWidget",
                        "At least one layer in map canvas required"))
                self.checkBoxEnableMap.setCheckState(Qt.Unchecked)
            else:
                enable = True
                if not self.__initLayerOL:
                    self.__initLayerOL = True
                    self.__setWebViewMap(0)
                else:
                    self.__refreshMapOL()
        # GUI
        if enable:
            self.lbStatusRead.setVisible(False)
            self.webViewMap.setVisible(True)
        else:
            self.lbStatusRead.setText("")
            self.lbStatusRead.setVisible(True)
            self.webViewMap.setVisible(False)
        self.webViewMap.setEnabled(enable)
        self.comboBoxTypeMap.setEnabled(enable)
        self.pbRefresh.setEnabled(enable)
        self.pbAddRaster.setEnabled(enable)
        self.pbCopyKml.setEnabled(enable)
        self.pbSaveImg.setEnabled(enable)
        self.checkBoxHideCross.setEnabled(enable)

    def __signal_checkBoxHideCross_stateChanged(self, state):
        if state == Qt.Checked:
            self.__marker.reset()
            self.__marker.setVisible(False)
        else:
            self.__marker.setVisible(True)
            self.__refreshMapOL()

    def __signal_DocWidget_visibilityChanged(self, visible):
        if self.__canvas.layerCount() == 0:
            return
        self.checkBoxEnableMap.setCheckState(Qt.Unchecked)
        self.__signal_checkBoxEnableMap_stateChanged(Qt.Unchecked)

    def __signal_comboBoxTypeMap_currentIndexChanged(self, index):
        self.__setWebViewMap(index)

    def __signal_canvas_extentsChanged(self):
        if self.__canvas.layerCount() == 0 or not self.webViewMap.isVisible():
            return
        if self.checkBoxEnableMap.checkState() == Qt.Checked:
            self.__refreshMapOL()

    def __signal_pbRefresh_clicked(self, checked):
        index = self.comboBoxTypeMap.currentIndex()
        self.__setWebViewMap(index)

    def __signal_pbAddRaster_clicked(self, checked):
        index = self.comboBoxTypeMap.currentIndex()
        layer = self.__olLayerTypeRegistry.getById(index)
        QGuiApplication.setOverrideCursor(Qt.WaitCursor)
        layer.addLayer()
        QGuiApplication.restoreOverrideCursor()

    def __signal_pbCopyKml_clicked(self, cheked):
        # Extent Openlayers
        action = "map.getExtent().toGeometry().toString();"
        wkt = self.webViewMap.page().mainFrame().evaluateJavaScript(action)
        rect = QgsGeometry.fromWkt(wkt).boundingBox()
        srsGE = QgsCoordinateReferenceSystem(
            4326, QgsCoordinateReferenceSystem.EpsgCrsId)
        coodTrans = QgsCoordinateTransform(self.__srsOL, srsGE,
                                           QgsProject.instance())
        rect = coodTrans.transform(
            rect, QgsCoordinateTransform.ForwardTransform)
        line = QgsGeometry.fromRect(rect).asPolygon()[0]
        wkt = str(QgsGeometry.fromPolylineXY(line).asWkt())
        # Kml
        proj4 = str(srsGE.toProj4())
        kmlLine = bindogr.exportKml(wkt, proj4)
        kml = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"\
              "<kml xmlns=\"http://www.opengis.net/kml/2.2\" " \
              "xmlns:gx=\"http://www.google.com/kml/ext/2.2\" " \
              "xmlns:kml=\"http://www.opengis.net/kml/2.2\" " \
              "xmlns:atom=\"http://www.w3.org/2005/Atom\">" \
              "<Placemark>" \
              "<name>KML from Plugin Openlayers Overview for QGIS</name>" \
              "<description>Extent of openlayers map from Plugin Openlayers \
              Overview for QGIS</description>"\
              "%s" \
              "</Placemark></kml>" % kmlLine
        clipBoard = QApplication.clipboard()
        clipBoard.setText(kml)

    def __signal_pbSaveImg_clicked(self, cheked):
        if type(self.__fileNameImg) == tuple:
            self.__fileNameImg = self.__fileNameImg[0]
        fileName = QFileDialog.getSaveFileName(self,
                                               QApplication.translate(
                                                   "OpenLayersOverviewWidget",
                                                   "Save image"),
                                               self.__fileNameImg,
                                               QApplication.translate(
                                                   "OpenLayersOverviewWidget",
                                                   "Image(*.jpg)"))
        if not fileName == '':
            self.__fileNameImg = fileName
        else:
            return
        img = QImage(self.webViewMap.page().mainFrame().contentsSize(),
                     QImage.Format_ARGB32_Premultiplied)
        imgPainter = QPainter()
        imgPainter.begin(img)
        self.webViewMap.page().mainFrame().render(imgPainter)
        imgPainter.end()
        img.save(fileName[0], "JPEG")

    def __signal_webViewMap_loadFinished(self, ok):
        if ok is False:
            QMessageBox.warning(self, QApplication.translate(
                "OpenLayersOverviewWidget", "OpenLayers Overview"),
                                QApplication.translate(
                                    "OpenLayersOverviewWidget",
                                    "Error loading page!"))
        else:
            # wait until OpenLayers map is ready
            self.__checkMapReady()
        self.lbStatusRead.setVisible(False)
        self.webViewMap.setVisible(True)
        self.webViewMap.page().mainFrame().loadFinished.disconnect(
            self.__signal_webViewMap_loadFinished)

    def __setWebViewMap(self, id):
        layer = self.__olLayerTypeRegistry.getById(id)
        self.lbStatusRead.setText("Loading " + layer.displayName + " ...")
        self.lbStatusRead.setVisible(True)
        self.webViewMap.setVisible(False)
        self.webViewMap.page().mainFrame().loadFinished.connect(
            self.__signal_webViewMap_loadFinished)
        url = layer.html_url()
        self.webViewMap.page().mainFrame().load(QUrl(url))

    def __checkMapReady(self):
        if self.webViewMap.page().mainFrame().evaluateJavaScript(
                "map != undefined"):
            # map ready
            self.__refreshMapOL()
        else:
            # wait for map
            self.__timerMapReady.start()

    def __refreshMapOL(self):
        # catch Exception where lat/long exceed limit of the loaded layer
        # the Exception name is unknown
        latlon = None
        try:
            latlon = self.__getCenterLongLat2OL()
        except Exception as e:
            QgsLogger().warning(e.args[0])

        if latlon:
            action = "map.setCenter(new OpenLayers.LonLat(%f, %f));" % (latlon)
            self.webViewMap.page().mainFrame().evaluateJavaScript(action)
            action = "map.zoomToScale(%f);" % self.__canvas.scale()
            self.webViewMap.page().mainFrame().evaluateJavaScript(action)
            self.webViewMap.page().mainFrame().evaluateJavaScript(
                "oloMarker.changeMarker();")

    def __getCenterLongLat2OL(self):
        pntCenter = self.__canvas.extent().center()
        crsCanvas = self.__canvas.mapSettings().destinationCrs()
        if crsCanvas != self.__srsOL:
            coodTrans = QgsCoordinateTransform(crsCanvas, self.__srsOL,
                                               QgsProject.instance())
            pntCenter = coodTrans.transform(
                pntCenter, QgsCoordinateTransform.ForwardTransform)
        return tuple([pntCenter.x(), pntCenter.y()])
예제 #15
0
class LessonsCreator(object):
    def __init__(self, iface):
        self.iface = iface

        # add tests to tester plugin
        try:
            from qgistester.tests import addTestModule
            from lessonscreator.test import testerplugin
            addTestModule(testerplugin, "LessonsCreator")
        except Exception as e:
            pass

        self.capturing = False

    def unload(self):
        self.iface.removePluginMenu(u"Lessons", self.action)
        del self.action
        del self.newStepAction

        QgsApplication.instance().focusChanged.disconnect(
            self.processFocusChanged)

        try:
            from qgistester.tests import removeTestModule
            from lessonscreator.test import testerplugin
            removeTestModule(testerplugin, "LessonsCreator")
        except Exception as e:
            pass

    def initGui(self):
        lessonIcon = QIcon(os.path.dirname(__file__) + '/edit.png')
        self.action = QAction(lessonIcon, "Capture lesson steps",
                              self.iface.mainWindow())
        self.action.triggered.connect(self.toggleCapture)
        self.iface.addPluginToMenu(u"Lessons", self.action)

        QgsApplication.instance().focusChanged.connect(
            self.processFocusChanged)

        self.newStepAction = QAction("New step", self.iface.mainWindow())

        self.newStepAction.setShortcut(QKeySequence(Qt.CTRL + Qt.Key_W))
        self.newStepAction.setShortcutContext(Qt.ApplicationShortcut)
        self.newStepAction.triggered.connect(self.startNewStep)
        self.iface.mainWindow().addAction(self.newStepAction)

    connections = []
    iScreenshot = 0
    iStep = 0
    outputHtmlFile = None
    outputPyFile = None

    def startNewStep(self):
        if self.outputHtmlFile:
            self.outputHtmlFile.close()
        self.iStep += 1
        path = os.path.join(self.folder, "step_%i.html" % self.iStep)
        self.outputHtmlFile = open(path, "w")
        self.outputPyFile.write(
            '''lesson.addStep("Step_%i", "step_%i.html", steptype=Step.MANUALSTEP)\n'''
            % (self.iStep, self.iStep))

    def toggleCapture(self):
        if self.capturing:
            self.action.setText("Capture lesson steps")
            self.capturing = False
            self.outputHtmlFile.close()
            self.outputPyFile.close()
            self.outputHtmlFile = None
            self.outputPyFile = None
        else:
            self.folder = QFileDialog.getExistingDirectory(
                self.iface.mainWindow(), "Select folder to store lesson")
            if not self.folder:
                return
            path = os.path.join(self.folder, "__init__.py")
            self.outputPyFile = open(path, "w")

            template = (
                "from lessons.lesson import Lesson, Step\n"
                "from lessons.utils import *\n"
                "lesson = Lesson('Lesson', 'Basic lessons', 'lesson.html')\n\n"
            )

            self.outputPyFile.write(template)

            self.iScreenshot = 0
            self.iStep = 0
            self.updateConnections()
            self.action.setText("Stop capturing lesson steps")
            self.capturing = True
            self.startNewStep()

    def processWidgetClick(self, obj):
        if self.capturing:
            try:
                text = "Click on '%s'" % obj.text()
            except Exception as e:
                # fix_print_with_import
                print(e)
                text = "Click on " + str(obj)
            self.outputHtmlFile.write("<p>%s</p>\n" % text)
            self.updateConnections()

    lastComboText = None

    def processComboNewSelection(self, combo):
        if self.capturing:
            text = "Select '%s' in '%s'" % (combo.currentText(),
                                            combo.objectName())
            if text == self.lastComboText:
                return
            self.lastComboText = text
            self.outputHtmlFile.write("<p>%s</p>\n" % text)
            self.createScreenshot(combo.parent(), combo.frameGeometry())
            self.updateConnections()

    def processCheckBoxChange(self, check):
        if self.capturing:
            if check.isChecked():
                text = "Check the '%s' checkbox" % (check.text())
            else:
                text = "Uncheck the '%s' checkbox" % (check.text())
            self.outputHtmlFile.write("<p>%s</p>\n" % text)
            self.createScreenshot(check.parent(), check.frameGeometry())

    def processMenuClick(self, action):
        if self.capturing and action.text() != "Stop capturing lesson steps":
            text = "Click on menu '%s'" % action.text()
            self.outputHtmlFile.write("<p>%s</p>\n" % text)

    def getParentWindow(self, obj):
        window = None
        try:
            parent = obj.parent()
            while parent is not None:
                if isinstance(parent, QDialog):
                    window = parent
                    break
                parent = parent.parent()
        except:
            window = None

        return window or QgsApplication.instance().desktop()

    def processFocusChanged(self, old, new):
        if self.capturing:
            self.updateConnections()
            if isinstance(old, QLineEdit) and old.text().strip():
                text = "Enter '%s' in textbox '%s'" % (old.text(),
                                                       old.objectName())
                self.outputHtmlFile.write("<p>%s</p>\n" % text)
                self.createScreenshot(old.parent(), old.frameGeometry())
            else:
                oldParent = self.getParentWindow(old)
                newParent = self.getParentWindow(new)
                # fix_print_with_import
                print(oldParent, newParent)
                if oldParent != newParent:
                    self.createScreenshot(newParent)
                elif isinstance(
                        new,
                    (QLineEdit, QTextEdit, QComboBox, QSpinBox, QRadioButton)):
                    text = "Select the '%s' widget" % (old.objectName()
                                                       or str(old))
                    self.outputHtmlFile.write("<p>%s</p>\n" % text)
                    self.createScreenshot(new.parent(), new.frameGeometry())

    timer = None

    def _createScreenshot(self, obj, rect):
        pixmap = QPixmap.grabWindow(obj.winId()).copy()
        if rect is not None:
            painter = QPainter()
            painter.begin(pixmap)
            painter.setPen(QPen(QBrush(Qt.red), 3, Qt.DashLine))
            painter.drawRect(rect)
            painter.end()

        pixmap.save(os.path.join(self.folder, '%i.jpg' % self.iScreenshot),
                    'jpg')

        self.outputHtmlFile.write("<img src='%i.jpg'/>\n" % self.iScreenshot)
        self.iScreenshot += 1
        self.timer = None

    def createScreenshot(self, obj, rect=None):
        if self.capturing and self.timer is None:
            self.timer = QTimer()
            self.timer.setInterval(1000)
            self.timer.setSingleShot(True)
            self.timer.timeout.connect(
                lambda: self._createScreenshot(obj, rect))
            self.timer.start()

    def updateConnections(self):
        widgets = QgsApplication.instance().allWidgets()
        for w in widgets:
            if w not in self.connections:
                if isinstance(w, (QPushButton, QToolButton)):
                    f = partial(self.processWidgetClick, w)
                    w.clicked.connect(f)
                    self.connections.append(w)
                elif isinstance(w, QComboBox):
                    f = partial(self.processComboNewSelection, w)
                    w.currentIndexChanged.connect(f)
                    self.connections.append(w)
                elif isinstance(w, QCheckBox):
                    f = partial(self.processCheckBoxChange, w)
                    w.stateChanged.connect(f)
                    self.connections.append(w)

        menuActions = []
        actions = self.iface.mainWindow().menuBar().actions()
        for action in actions:
            menuActions.extend(self.getActions(action, None))

        for action, menu in menuActions:
            if menu not in self.connections:
                menu.triggered.connect(self.processMenuClick)
                self.connections.append(menu)

    def getActions(self, action, menu):
        menuActions = []
        submenu = action.menu()
        if submenu is None:
            menuActions.append((action, menu))
            return menuActions
        else:
            actions = submenu.actions()
            for subaction in actions:
                if subaction.menu() is not None:
                    menuActions.extend(
                        self.getActions(subaction, subaction.menu()))
                elif not subaction.isSeparator():
                    menuActions.append((subaction, submenu))

        return menuActions
예제 #16
0
class AutoSuggest(QObject):

    def __init__(self, geturl_func, parseresult_func, parent = None):
        QObject.__init__(self, parent)
        self.geturl_func = geturl_func
        self.parseresult_func = parseresult_func
        
        self.editor = parent
        self.networkManager = QNetworkAccessManager()

        self.selectedObject = None
        self.isUnloaded = False

        self.popup = QTreeWidget(parent)
        #self.popup.setColumnCount(2)
        self.popup.setColumnCount(1)
        self.popup.setUniformRowHeights(True)
        self.popup.setRootIsDecorated(False)
        self.popup.setEditTriggers(QTreeWidget.NoEditTriggers)
        self.popup.setSelectionBehavior(QTreeWidget.SelectRows)
        self.popup.setFrameStyle(QFrame.Box | QFrame.Plain)
        self.popup.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

        self.popup.header().hide()
        self.popup.installEventFilter(self)
        self.popup.setMouseTracking(True)

        #self.connect(self.popup, SIGNAL("itemClicked(QTreeWidgetItem*, int)"),
        #             self.doneCompletion)
        self.popup.itemClicked.connect( self.doneCompletion )

        self.popup.setWindowFlags(Qt.Popup)
        self.popup.setFocusPolicy(Qt.NoFocus)
        self.popup.setFocusProxy(parent)

        self.timer = QTimer(self)
        self.timer.setSingleShot(True)
        self.timer.setInterval(500)
        #self.connect(self.timer, SIGNAL("timeout()"), self.autoSuggest)
        self.timer.timeout.connect( self.autoSuggest )
        
        #self.connect(self.editor, SIGNAL("textEdited(QString)"), self.timer, SLOT("start()"))
        #self.editor.textEdited.connect( self.timer.start )
        self.editor.textEdited.connect( self.timer.start )
        #self.editor.textChanged.connect( self.timer.start )

        #self.connect(self.networkManager, SIGNAL("finished(QNetworkReply*)"),
        #             self.handleNetworkData)
        self.networkManager.finished.connect( self.handleNetworkData )

    def eventFilter(self, obj, ev):
        if obj != self.popup:
            return False

        if ev.type() == QEvent.MouseButtonPress:
            self.popup.hide()
            self.editor.setFocus()
            return True

        if ev.type() == QEvent.KeyPress:
            consumed = False
            key = ev.key()
            if key == Qt.Key_Enter or key == Qt.Key_Return:
                self.doneCompletion()
                consumed = True

            elif key == Qt.Key_Escape:
                self.editor.setFocus()
                self.popup.hide()
                consumed = True

            elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_Home, Qt.Key_End,
                         Qt.Key_PageUp, Qt.Key_PageDown):
                pass

            else:
                self.editor.setFocus()
                self.editor.event(ev)
                self.popup.hide()

            return consumed

        return False

    def showCompletion(self, rows):
        # Rows is an iterable of tuples like [("text",object1),("text2", object2),...]
        pal = self.editor.palette()
        color = pal.color(QPalette.Disabled, QPalette.WindowText)

        self.popup.setUpdatesEnabled(False)
        self.popup.clear()
        if rows is None or len( rows ) < 1:
            return

        for row in rows:
            item = QTreeWidgetItem(self.popup)
            item.setText(0, row[0])
            #item.setText(1, hit['type'])
            item.setTextAlignment(1, Qt.AlignRight)
            item.setForeground(1, color)
            item.setData(2, Qt.UserRole, (row[1],)) # Try immutable py obj #http://stackoverflow.com/questions/9257422/how-to-get-the-original-python-data-from-qvariant

        self.popup.setCurrentItem(self.popup.topLevelItem(0))
        self.popup.resizeColumnToContents(0)
        #self.popup.resizeColumnToContents(1)
        self.popup.adjustSize()
        self.popup.setUpdatesEnabled(True)

        h = self.popup.sizeHintForRow(0) * min(15, len(rows)) + 3
        w = max(self.popup.width(), self.editor.width())
        self.popup.resize(w, h)

        self.popup.move(self.editor.mapToGlobal(QPoint(0, self.editor.height())))
        self.popup.setFocus()
        self.popup.show()

    def doneCompletion(self):
        self.timer.stop()
        self.popup.hide()
        self.editor.setFocus()
        item = self.popup.currentItem()
        if item:
            self.editor.setText(item.text(0) )
            obj =  item.data(2, Qt.UserRole) #.toPyObject()
            self.selectedObject = obj[0]
            e = QKeyEvent(QEvent.KeyPress, Qt.Key_Enter, Qt.NoModifier)
            QApplication.postEvent(self.editor, e)
            e = QKeyEvent(QEvent.KeyRelease, Qt.Key_Enter, Qt.NoModifier)
            QApplication.postEvent(self.editor, e)

    def preventSuggest(self):
        self.timer.stop()

    def autoSuggest(self):
        term = self.editor.text()
        if term:
            qurl = self.geturl_func( term )
            if qurl:
                # TODO: Cancel existing requests: http://qt-project.org/forums/viewthread/18073
                self.networkManager.get(QNetworkRequest( qurl ))      #QUrl(url)))


    def handleNetworkData(self, networkReply):
        url = networkReply.url()
        #print "received url:", url.toString()
        if not networkReply.error():
            response = networkReply.readAll()
            pystring = str(response, 'utf-8')
            #print "Response: ", response
            rows = self.parseresult_func( pystring )
            self.showCompletion( rows )

        networkReply.deleteLater()

    def unload( self ):
        # Avoid processing events after QGIS shutdown has begun
        self.popup.removeEventFilter(self)
        self.isUnloaded = True
예제 #17
0
class ExportDialog(QDialog, DIALOG_UI):
    ValidExtensions = ['xtf', 'itf', 'gml', 'xml']

    def __init__(self, base_config, parent=None):
        QDialog.__init__(self, parent)
        self.setupUi(self)
        QgsGui.instance().enableAutoGeometryRestore(self)
        self.buttonBox.accepted.disconnect()
        self.buttonBox.accepted.connect(self.accepted)
        self.buttonBox.clear()
        self.buttonBox.addButton(QDialogButtonBox.Cancel)
        self.buttonBox.addButton(self.tr('Export'),
                                 QDialogButtonBox.AcceptRole)
        self.buttonBox.addButton(QDialogButtonBox.Help)
        self.buttonBox.helpRequested.connect(self.help_requested)
        self.xtf_file_browse_button.clicked.connect(
            make_save_file_selector(
                self.xtf_file_line_edit,
                title=self.tr('Save in XTF Transfer File'),
                file_filter=self.
                tr('XTF Transfer File (*.xtf);;Interlis 1 Transfer File (*.itf);;XML (*.xml);;GML (*.gml)'
                   ),
                extension='.xtf',
                extensions=['.' + ext for ext in self.ValidExtensions]))
        self.xtf_file_browse_button.clicked.connect(
            self.xtf_browser_opened_to_true)
        self.xtf_browser_was_opened = False

        self.gpkg_file_browse_button.clicked.connect(
            make_file_selector(
                self.gpkg_file_line_edit,
                title=self.tr('Open GeoPackage database file'),
                file_filter=self.tr('GeoPackage Database (*.gpkg)')))
        self.type_combo_box.clear()
        self.type_combo_box.addItem(self.tr('PostGIS'), 'pg')
        self.type_combo_box.addItem(self.tr('GeoPackage'), 'gpkg')
        self.type_combo_box.currentIndexChanged.connect(self.type_changed)

        self.base_configuration = base_config
        self.restore_configuration()

        self.validators = Validators()
        nonEmptyValidator = NonEmptyStringValidator()
        fileValidator = FileValidator(
            pattern=['*.' + ext for ext in self.ValidExtensions],
            allow_non_existing=True)
        gpkgFileValidator = FileValidator(pattern='*.gpkg')

        self.pg_host_line_edit.setValidator(nonEmptyValidator)
        self.pg_database_line_edit.setValidator(nonEmptyValidator)
        self.pg_user_line_edit.setValidator(nonEmptyValidator)
        self.xtf_file_line_edit.setValidator(fileValidator)
        self.gpkg_file_line_edit.setValidator(gpkgFileValidator)

        self.pg_host_line_edit.textChanged.connect(
            self.validators.validate_line_edits)
        self.pg_host_line_edit.textChanged.emit(self.pg_host_line_edit.text())
        self.pg_database_line_edit.textChanged.connect(
            self.validators.validate_line_edits)
        self.pg_database_line_edit.textChanged.emit(
            self.pg_database_line_edit.text())
        self.pg_user_line_edit.textChanged.connect(
            self.validators.validate_line_edits)
        self.pg_user_line_edit.textChanged.emit(self.pg_user_line_edit.text())
        self.xtf_file_line_edit.textChanged.connect(
            self.validators.validate_line_edits)
        self.xtf_file_line_edit.textChanged.connect(
            self.xtf_browser_opened_to_false)
        self.xtf_file_line_edit.textChanged.emit(
            self.xtf_file_line_edit.text())
        self.gpkg_file_line_edit.textChanged.connect(
            self.validators.validate_line_edits)
        self.gpkg_file_line_edit.textChanged.emit(
            self.gpkg_file_line_edit.text())

        #refresh the models on changing values but avoid massive db connects by timer
        self.refreshTimer = QTimer()
        self.refreshTimer.setSingleShot(True)
        self.refreshTimer.timeout.connect(self.refresh_models)
        self.pg_host_line_edit.textChanged.connect(
            self.request_for_refresh_models)
        self.pg_port_line_edit.textChanged.connect(
            self.request_for_refresh_models)
        self.pg_database_line_edit.textChanged.connect(
            self.request_for_refresh_models)
        self.pg_schema_line_edit.textChanged.connect(
            self.request_for_refresh_models)
        self.pg_user_line_edit.textChanged.connect(
            self.request_for_refresh_models)
        self.pg_password_line_edit.textChanged.connect(
            self.request_for_refresh_models)
        self.gpkg_file_line_edit.textChanged.connect(
            self.request_for_refresh_models)

        self.export_models_model = ExportModels(None, None, None)
        self.refreshed_export_models_model()
        self.export_models_view.setModel(self.export_models_model)
        self.export_models_view.clicked.connect(self.export_models_model.check)
        self.export_models_view.space_pressed.connect(
            self.export_models_model.check)

    def request_for_refresh_models(self):
        # hold refresh back
        self.refreshTimer.start(500)

    def refresh_models(self):
        self.export_models_model = self.refreshed_export_models_model()
        self.export_models_view.setModel(self.export_models_model)
        self.export_models_view.clicked.connect(self.export_models_model.check)
        self.export_models_view.space_pressed.connect(
            self.export_models_model.check)

    def refreshed_export_models_model(self):
        tool_name = 'ili2pg' if self.type_combo_box.currentData(
        ) == 'pg' else 'ili2gpkg'
        uri = []
        if tool_name == 'ili2pg':
            uri += ['dbname={}'.format(self.updated_configuration().database)]
            uri += ['user={}'.format(self.updated_configuration().dbusr)]
            if self.updated_configuration().dbpwd:
                uri += [
                    'password={}'.format(self.updated_configuration().dbpwd)
                ]
            uri += ['host={}'.format(self.updated_configuration().dbhost)]
            if self.updated_configuration().dbport:
                uri += ['port={}'.format(self.updated_configuration().dbport)]
        elif tool_name == 'ili2gpkg':
            uri = [self.updated_configuration().dbfile]
        uri_string = ' '.join(uri)

        schema = self.updated_configuration().dbschema

        self.export_models_model = ExportModels(tool_name, uri_string, schema)

        return self.export_models_model

    def accepted(self):
        configuration = self.updated_configuration()

        if not self.xtf_file_line_edit.validator().validate(
                configuration.xtffile, 0)[0] == QValidator.Acceptable:
            self.txtStdout.setText(
                self.
                tr('Please set a valid INTERLIS XTF file before exporting data.'
                   ))
            self.xtf_file_line_edit.setFocus()
            return
        if not configuration.iliexportmodels:
            self.txtStdout.setText(
                self.tr('Please set a model before exporting data.'))
            self.export_models_view.setFocus()
            return

        if self.type_combo_box.currentData() == 'pg':
            if not configuration.dbhost:
                self.txtStdout.setText(
                    self.tr('Please set a host before exporting data.'))
                self.pg_host_line_edit.setFocus()
                return
            if not configuration.database:
                self.txtStdout.setText(
                    self.tr('Please set a database before exporting data.'))
                self.pg_database_line_edit.setFocus()
                return
            if not configuration.dbusr:
                self.txtStdout.setText(
                    self.tr(
                        'Please set a database user before exporting data.'))
                self.pg_user_line_edit.setFocus()
                return
        elif self.type_combo_box.currentData() == 'gpkg':
            if not configuration.dbfile or self.gpkg_file_line_edit.validator(
            ).validate(configuration.dbfile, 0)[0] != QValidator.Acceptable:
                self.txtStdout.setText(
                    self.
                    tr('Please set an existing database file before creating the project.'
                       ))
                self.gpkg_file_line_edit.setFocus()
                return

        # If xtf browser was opened and the file exists, the user already chose
        # to overwrite the file
        if os.path.isfile(self.xtf_file_line_edit.text().strip()
                          ) and not self.xtf_browser_was_opened:
            self.msg = QMessageBox()
            self.msg.setIcon(QMessageBox.Warning)
            self.msg.setText(
                self.tr(
                    "{filename} already exists.\nDo you want to replace it?").
                format(filename=os.path.basename(
                    self.xtf_file_line_edit.text().strip())))
            self.msg.setWindowTitle(self.tr("Save in XTF Transfer File"))
            self.msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
            msg_box = self.msg.exec_()
            if msg_box == QMessageBox.No:
                return

        with OverrideCursor(Qt.WaitCursor):
            self.progress_bar.show()
            self.progress_bar.setValue(0)

            self.disable()
            self.txtStdout.setTextColor(QColor('#000000'))
            self.txtStdout.clear()

            exporter = iliexporter.Exporter()

            tool_name = 'ili2pg' if self.type_combo_box.currentData(
            ) == 'pg' else 'ili2gpkg'
            exporter.tool_name = tool_name
            exporter.configuration = configuration

            self.save_configuration(configuration)

            exporter.stdout.connect(self.print_info)
            exporter.stderr.connect(self.on_stderr)
            exporter.process_started.connect(self.on_process_started)
            exporter.process_finished.connect(self.on_process_finished)

            self.progress_bar.setValue(25)

            try:
                if exporter.run() != iliexporter.Exporter.SUCCESS:
                    self.enable()
                    self.progress_bar.hide()
                    return
            except JavaNotFoundError:
                self.txtStdout.setTextColor(QColor('#000000'))
                self.txtStdout.clear()
                self.txtStdout.setText(
                    self.
                    tr('Java could not be found. Please <a href="https://java.com/en/download/">install Java</a> and or <a href="#configure">configure a custom java path</a>. We also support the JAVA_HOME environment variable in case you prefer this.'
                       ))
                self.enable()
                self.progress_bar.hide()
                return

            self.buttonBox.clear()
            self.buttonBox.setEnabled(True)
            self.buttonBox.addButton(QDialogButtonBox.Close)
            self.progress_bar.setValue(100)

    def print_info(self, text):
        self.txtStdout.setTextColor(QColor('#000000'))
        self.txtStdout.append(text)
        QCoreApplication.processEvents()

    def on_stderr(self, text):
        color_log_text(text, self.txtStdout)
        self.advance_progress_bar_by_text(text)
        QCoreApplication.processEvents()

    def on_process_started(self, command):
        self.disable()
        self.txtStdout.setTextColor(QColor('#000000'))
        self.txtStdout.clear()
        self.txtStdout.setText(command)
        QCoreApplication.processEvents()

    def on_process_finished(self, exit_code, result):
        color = '#004905' if exit_code == 0 else '#aa2222'
        self.txtStdout.setTextColor(QColor(color))
        self.txtStdout.append(self.tr('Finished ({})'.format(exit_code)))
        if result == iliexporter.Exporter.SUCCESS:
            self.buttonBox.clear()
            self.buttonBox.setEnabled(True)
            self.buttonBox.addButton(QDialogButtonBox.Close)
        else:
            self.enable()

    def updated_configuration(self):
        """
        Get the configuration that is updated with the user configuration changes on the dialog.
        :return: Configuration
        """
        configuration = ili2dbconfig.ExportConfiguration()

        if self.type_combo_box.currentData() == 'pg':
            # PostgreSQL specific options
            configuration.dbhost = self.pg_host_line_edit.text().strip()
            configuration.dbport = self.pg_port_line_edit.text().strip()
            configuration.dbusr = self.pg_user_line_edit.text().strip()
            configuration.database = self.pg_database_line_edit.text().strip()
            configuration.dbschema = self.pg_schema_line_edit.text().strip(
            ).lower()
            configuration.dbpwd = self.pg_password_line_edit.text()
        elif self.type_combo_box.currentData() == 'gpkg':
            configuration.dbfile = self.gpkg_file_line_edit.text().strip()

        configuration.xtffile = self.xtf_file_line_edit.text().strip()
        configuration.iliexportmodels = ';'.join(
            self.export_models_model.checked_models())
        configuration.ilimodels = ';'.join(
            self.export_models_model.stringList())
        configuration.base_configuration = self.base_configuration

        return configuration

    def save_configuration(self, configuration):
        settings = QSettings()
        settings.setValue('QgsProjectGenerator/ili2pg/xtffile_export',
                          configuration.xtffile)
        settings.setValue('QgsProjectGenerator/importtype',
                          self.type_combo_box.currentData())

        if self.type_combo_box.currentData() in ['ili2pg', 'pg']:
            # PostgreSQL specific options
            settings.setValue('QgsProjectGenerator/ili2pg/host',
                              configuration.dbhost)
            settings.setValue('QgsProjectGenerator/ili2pg/port',
                              configuration.dbport)
            settings.setValue('QgsProjectGenerator/ili2pg/user',
                              configuration.dbusr)
            settings.setValue('QgsProjectGenerator/ili2pg/database',
                              configuration.database)
            settings.setValue('QgsProjectGenerator/ili2pg/schema',
                              configuration.dbschema)
            settings.setValue('QgsProjectGenerator/ili2pg/password',
                              configuration.dbpwd)
        elif self.type_combo_box.currentData() in ['ili2gpkg', 'gpkg']:
            settings.setValue('QgsProjectGenerator/ili2gpkg/dbfile',
                              configuration.dbfile)

    def restore_configuration(self):
        settings = QSettings()

        self.xtf_file_line_edit.setText(
            settings.value('QgsProjectGenerator/ili2pg/xtffile_export'))
        self.pg_host_line_edit.setText(
            settings.value('QgsProjectGenerator/ili2pg/host', 'localhost'))
        self.pg_port_line_edit.setText(
            settings.value('QgsProjectGenerator/ili2pg/port'))
        self.pg_user_line_edit.setText(
            settings.value('QgsProjectGenerator/ili2pg/user'))
        self.pg_database_line_edit.setText(
            settings.value('QgsProjectGenerator/ili2pg/database'))
        self.pg_schema_line_edit.setText(
            settings.value('QgsProjectGenerator/ili2pg/schema'))
        self.pg_password_line_edit.setText(
            settings.value('QgsProjectGenerator/ili2pg/password'))
        self.gpkg_file_line_edit.setText(
            settings.value('QgsProjectGenerator/ili2gpkg/dbfile'))

        mode = settings.value('QgsProjectGenerator/importtype', 'pg')
        mode = 'pg' if mode == 'ili2pg' else mode
        mode = 'gpkg' if mode == 'ili2gpkg' else mode
        self.type_combo_box.setCurrentIndex(self.type_combo_box.findData(mode))
        self.type_changed()

    def disable(self):
        self.pg_config.setEnabled(False)
        self.ili_config.setEnabled(False)
        self.buttonBox.setEnabled(False)

    def enable(self):
        self.pg_config.setEnabled(True)
        self.ili_config.setEnabled(True)
        self.buttonBox.setEnabled(True)

    def type_changed(self):
        self.progress_bar.hide()
        if self.type_combo_box.currentData() == 'pg':
            self.pg_config.show()
            self.gpkg_config.hide()
        elif self.type_combo_box.currentData() == 'gpkg':
            self.pg_config.hide()
            self.gpkg_config.show()

    def link_activated(self, link):
        if link.url() == '#configure':
            cfg = OptionsDialog(self.base_configuration)
            if cfg.exec_():
                settings = QSettings()
                settings.beginGroup('QgsProjectGenerator/ili2db')
                self.base_configuration.save(settings)
        else:
            QDesktopServices.openUrl(link)

    def help_requested(self):
        os_language = QLocale(
            QSettings().value('locale/userLocale')).name()[:2]
        if os_language in ['es', 'de']:
            webbrowser.open(
                "https://opengisch.github.io/projectgenerator/docs/{}/user-guide.html#export-an-interlis-transfer-file-xtf"
                .format(os_language))
        else:
            webbrowser.open(
                "https://opengisch.github.io/projectgenerator/docs/user-guide.html#export-an-interlis-transfer-file-xtf"
            )

    def xtf_browser_opened_to_true(self):
        """
        Slot. Sets a flag to true to eventually avoid asking a user whether to overwrite a file.
        """
        self.xtf_browser_was_opened = True

    def xtf_browser_opened_to_false(self):
        """
        Slot. Sets a flag to false to eventually ask a user whether to overwrite a file.
        """
        self.xtf_browser_was_opened = False

    def advance_progress_bar_by_text(self, text):
        if text.strip() == 'Info: compile models…':
            self.progress_bar.setValue(50)
        elif text.strip() == 'Info: create table structure…':
            self.progress_bar.setValue(75)
예제 #18
0
class AutoSuggest(QObject):
    def __init__(self, geturl_func, parseresult_func, parent=None):
        QObject.__init__(self, parent)
        self.geturl_func = geturl_func
        self.parseresult_func = parseresult_func

        self.editor = parent
        self.networkManager = QNetworkAccessManager()

        self.selectedObject = None
        self.isUnloaded = False

        self.popup = QTreeWidget(parent)
        #self.popup.setColumnCount(2)
        self.popup.setColumnCount(1)
        self.popup.setUniformRowHeights(True)
        self.popup.setRootIsDecorated(False)
        self.popup.setEditTriggers(QTreeWidget.NoEditTriggers)
        self.popup.setSelectionBehavior(QTreeWidget.SelectRows)
        self.popup.setFrameStyle(QFrame.Box | QFrame.Plain)
        self.popup.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)

        self.popup.header().hide()
        self.popup.installEventFilter(self)
        self.popup.setMouseTracking(True)

        #self.connect(self.popup, SIGNAL("itemClicked(QTreeWidgetItem*, int)"),
        #             self.doneCompletion)
        self.popup.itemClicked.connect(self.doneCompletion)

        self.popup.setWindowFlags(Qt.Popup)
        self.popup.setFocusPolicy(Qt.NoFocus)
        self.popup.setFocusProxy(parent)

        self.timer = QTimer(self)
        self.timer.setSingleShot(True)
        self.timer.setInterval(500)
        #self.connect(self.timer, SIGNAL("timeout()"), self.autoSuggest)
        self.timer.timeout.connect(self.autoSuggest)

        #self.connect(self.editor, SIGNAL("textEdited(QString)"), self.timer, SLOT("start()"))
        #self.editor.textEdited.connect( self.timer.start )
        self.editor.textEdited.connect(self.timer.start)
        #self.editor.textChanged.connect( self.timer.start )

        #self.connect(self.networkManager, SIGNAL("finished(QNetworkReply*)"),
        #             self.handleNetworkData)
        self.networkManager.finished.connect(self.handleNetworkData)

    def eventFilter(self, obj, ev):
        if obj != self.popup:
            return False

        if ev.type() == QEvent.MouseButtonPress:
            self.popup.hide()
            self.editor.setFocus()
            return True

        if ev.type() == QEvent.KeyPress:
            consumed = False
            key = ev.key()
            if key == Qt.Key_Enter or key == Qt.Key_Return:
                self.doneCompletion()
                consumed = True

            elif key == Qt.Key_Escape:
                self.editor.setFocus()
                self.popup.hide()
                consumed = True

            elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_Home, Qt.Key_End,
                         Qt.Key_PageUp, Qt.Key_PageDown):
                pass

            else:
                self.editor.setFocus()
                self.editor.event(ev)
                self.popup.hide()

            return consumed

        return False

    def showCompletion(self, rows):
        # Rows is an iterable of tuples like [("text",object1),("text2", object2),...]
        pal = self.editor.palette()
        color = pal.color(QPalette.Disabled, QPalette.WindowText)

        self.popup.setUpdatesEnabled(False)
        self.popup.clear()
        if rows is None or len(rows) < 1:
            return

        for row in rows:
            item = QTreeWidgetItem(self.popup)
            item.setText(0, row[0])
            #item.setText(1, hit['type'])
            item.setTextAlignment(1, Qt.AlignRight)
            item.setForeground(1, color)
            item.setData(
                2, Qt.UserRole, (row[1], )
            )  # Try immutable py obj #http://stackoverflow.com/questions/9257422/how-to-get-the-original-python-data-from-qvariant

        self.popup.setCurrentItem(self.popup.topLevelItem(0))
        self.popup.resizeColumnToContents(0)
        #self.popup.resizeColumnToContents(1)
        self.popup.adjustSize()
        self.popup.setUpdatesEnabled(True)

        h = self.popup.sizeHintForRow(0) * min(15, len(rows)) + 3
        w = max(self.popup.width(), self.editor.width())
        self.popup.resize(w, h)

        self.popup.move(
            self.editor.mapToGlobal(QPoint(0, self.editor.height())))
        self.popup.setFocus()
        self.popup.show()

    def doneCompletion(self):
        self.timer.stop()
        self.popup.hide()
        self.editor.setFocus()
        item = self.popup.currentItem()
        if item:
            self.editor.setText(item.text(0))
            obj = item.data(2, Qt.UserRole)  #.toPyObject()
            self.selectedObject = obj[0]
            e = QKeyEvent(QEvent.KeyPress, Qt.Key_Enter, Qt.NoModifier)
            QApplication.postEvent(self.editor, e)
            e = QKeyEvent(QEvent.KeyRelease, Qt.Key_Enter, Qt.NoModifier)
            QApplication.postEvent(self.editor, e)

    def preventSuggest(self):
        self.timer.stop()

    def autoSuggest(self):
        term = self.editor.text()
        if term:
            qurl = self.geturl_func(term)
            if qurl:
                # TODO: Cancel existing requests: http://qt-project.org/forums/viewthread/18073
                self.networkManager.get(QNetworkRequest(qurl))  #QUrl(url)))

    def handleNetworkData(self, networkReply):
        url = networkReply.url()
        #print "received url:", url.toString()
        if not networkReply.error():
            response = networkReply.readAll()
            pystring = str(response, 'utf-8')
            #print "Response: ", response
            rows = self.parseresult_func(pystring)
            self.showCompletion(rows)

        networkReply.deleteLater()

    def unload(self):
        # Avoid processing events after QGIS shutdown has begun
        self.popup.removeEventFilter(self)
        self.isUnloaded = True
class SettingsDialog(QDialog, DIALOG_UI):

    db_connection_changed = pyqtSignal(DBConnector)
    fetcher_task = None

    def __init__(self, iface=None, parent=None, qgis_utils=None):
        QDialog.__init__(self, parent)
        self.setupUi(self)
        self.iface = iface
        self.log = QgsApplication.messageLog()
        self._db = None
        self.qgis_utils = qgis_utils
        self.connection_is_dirty = False

        self.cbo_db_source.clear()
        self.cbo_db_source.addItem(
            QCoreApplication.translate("SettingsDialog",
                                       'PostgreSQL / PostGIS'), 'pg')
        self.cbo_db_source.addItem(
            QCoreApplication.translate("SettingsDialog", 'GeoPackage'), 'gpkg')
        self.cbo_db_source.currentIndexChanged.connect(self.db_source_changed)

        self.online_models_radio_button.setChecked(True)
        self.online_models_radio_button.toggled.connect(
            self.model_provider_toggle)
        self.custom_model_directories_line_edit.setText("")
        self.custom_models_dir_button.clicked.connect(
            self.show_custom_model_dir)
        self.custom_model_directories_line_edit.setVisible(False)
        self.custom_models_dir_button.setVisible(False)

        # Set connections
        self.buttonBox.accepted.connect(self.accepted)
        self.buttonBox.helpRequested.connect(self.show_help)
        self.btn_test_connection.clicked.connect(self.test_connection)

        self.txt_pg_host.setPlaceholderText(
            QCoreApplication.translate(
                "SettingsDialog",
                "[Leave empty to use standard host: localhost]"))
        self.txt_pg_host.textEdited.connect(self.set_connection_dirty)

        self.txt_pg_port.setPlaceholderText(
            QCoreApplication.translate(
                "SettingsDialog", "[Leave empty to use standard port: 5432]"))
        self.txt_pg_port.textEdited.connect(self.set_connection_dirty)

        self.create_db_button.setToolTip(
            QCoreApplication.translate("SettingsDialog", "Create database"))
        self.create_db_button.clicked.connect(self.show_modal_create_db)
        self.selected_db_combobox.currentIndexChanged.connect(
            self.selected_database_changed)

        self.create_schema_button.setToolTip(
            QCoreApplication.translate("SettingsDialog", "Create schema"))
        self.create_schema_button.clicked.connect(
            self.show_modal_create_schema)

        self.txt_pg_user.setPlaceholderText(
            QCoreApplication.translate("SettingsDialog", "Database username"))
        self.txt_pg_user.textEdited.connect(self.set_connection_dirty)

        self.txt_pg_password.setPlaceholderText(
            QCoreApplication.translate("SettingsDialog",
                                       "[Leave empty to use system password]"))
        self.txt_pg_password.textEdited.connect(self.set_connection_dirty)
        self.txt_gpkg_file.textEdited.connect(self.set_connection_dirty)
        self.btn_test_service.clicked.connect(self.test_service)
        self.chk_use_roads.toggled.connect(self.update_images_state)

        # Trigger some default behaviours
        self.restore_settings()

        # Set a timer to avoid creating too many db connections while editing connection parameters
        self.refreshTimer = QTimer()
        self.refreshTimer.setSingleShot(True)
        self.refreshTimer.timeout.connect(self.refresh_connection)
        self.txt_pg_host.textChanged.connect(
            self.request_for_refresh_connection)
        self.txt_pg_port.textChanged.connect(
            self.request_for_refresh_connection)
        self.txt_pg_user.textChanged.connect(
            self.request_for_refresh_connection)
        self.txt_pg_password.textChanged.connect(
            self.request_for_refresh_connection)

        self.bar = QgsMessageBar()
        self.bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
        self.setLayout(QGridLayout())
        self.layout().addWidget(self.bar, 0, 0, Qt.AlignTop)

    def showEvent(self, event):
        self.update_db_names()
        # It is necessary to reload the variables
        # to load the database and schema name
        self.restore_settings()

        self.selected_schema_combobox.currentIndexChanged.connect(
            self.selected_schema_changed)
        print("Conectado...")

    def request_for_refresh_connection(self, text):
        # Wait half a second before refreshing connection
        self.refreshTimer.start(500)

    def refresh_connection(self):
        if not self.txt_pg_user.text().strip() \
                or not self.txt_pg_password.text().strip():
            self.selected_db_combobox.clear()
            self.selected_schema_combobox.clear()
        else:
            # Update database name list
            self.update_db_names()

    def selected_database_changed(self, index):
        self.update_db_schemas()

    def selected_schema_changed(self, index):
        if not self.connection_is_dirty:
            self.connection_is_dirty = True

    def update_db_names(self):
        if self.cbo_db_source.currentData() == 'pg':
            dict_conn = self.read_connection_parameters()
            tmp_db_conn = PGConnector('')
            uri = tmp_db_conn.get_connection_uri(dict_conn, 'pg', level=0)

            dbnames = tmp_db_conn.get_dbnames_list(uri)
            self.selected_db_combobox.clear()

            if dbnames[0]:
                self.selected_db_combobox.addItems(dbnames[1])
            else:
                # We won't show a message here to avoid bothering the user with potentially too much messages
                pass

    def update_db_schemas(self):
        if self.cbo_db_source.currentData() == 'pg':
            dict_conn = self.read_connection_parameters()
            tmp_db_conn = PGConnector('')
            uri = tmp_db_conn.get_connection_uri(dict_conn, 'pg')

            schemas_db = tmp_db_conn.get_dbname_schema_list(uri)
            self.selected_schema_combobox.clear()

            if schemas_db[0]:
                self.selected_schema_combobox.addItems(schemas_db[1])
            else:
                # We won't show a message here to avoid bothering the user with potentially too much messages
                pass

    def model_provider_toggle(self):
        if self.offline_models_radio_button.isChecked():
            self.custom_model_directories_line_edit.setVisible(True)
            self.custom_models_dir_button.setVisible(True)
        else:
            self.custom_model_directories_line_edit.setVisible(False)
            self.custom_models_dir_button.setVisible(False)
            self.custom_model_directories_line_edit.setText("")

    def get_db_connection(self, update_connection=True):
        if self._db is not None:
            self.log.logMessage("Returning existing db connection...",
                                PLUGIN_NAME, Qgis.Info)
            return self._db
        else:
            self.log.logMessage("Getting new db connection...", PLUGIN_NAME,
                                Qgis.Info)
            dict_conn = self.read_connection_parameters()
            if self.cbo_db_source.currentData() == 'pg':
                db = PGConnector(None, dict_conn['schema'], dict_conn)
            else:
                db = GPKGConnector(None, conn_dict=dict_conn)

            if update_connection:
                self._db = db

            return db

    def show_custom_model_dir(self):
        dlg = CustomModelDirDialog(
            self.custom_model_directories_line_edit.text(), self)
        dlg.exec_()

    def accepted(self):
        if self._db is not None:
            self._db.close_connection()

        self._db = None  # Reset db connection
        self._db = self.get_db_connection()

        # Schema combobox changes frequently, so control whether we listen to its changes to make the db conn dirty
        try:
            self.selected_schema_combobox.currentIndexChanged.disconnect(
                self.selected_schema_changed)
        except TypeError as e:
            pass

        if self.connection_is_dirty:
            self.connection_is_dirty = False

            res, msg = self._db.test_connection()
            if res:
                self.db_connection_changed.emit(self._db)
            else:
                self.show_message(msg, Qgis.Warning)
                return

        self.save_settings()

    def reject(self):
        self.restore_settings()
        self.connection_is_dirty = False

        # Schema combobox changes frequently, so control whether we listen to its changes to make the db conn dirty
        try:
            self.selected_schema_combobox.currentIndexChanged.disconnect(
                self.selected_schema_changed)
        except TypeError as e:
            pass

        self.done(0)

    def set_db_connection(self, mode, dict_conn):
        """
        To be used by external scripts and unit tests
        """
        self.cbo_db_source.setCurrentIndex(self.cbo_db_source.findData(mode))
        self.db_source_changed()

        if self.cbo_db_source.currentData() == 'pg':
            self.txt_pg_host.setText(dict_conn['host'])
            self.txt_pg_port.setText(dict_conn['port'])

            self.selected_db_combobox.clear()
            dbname_setting = dict_conn['database']
            self.selected_db_combobox.addItem(dbname_setting)

            self.selected_schema_combobox.clear()
            schema_setting = dict_conn['schema']
            self.selected_schema_combobox.addItem(schema_setting)

            self.txt_pg_user.setText(dict_conn['username'])
            self.txt_pg_password.setText(dict_conn['password'])
        else:
            self.txt_gpkg_file.setText(dict_conn['dbfile'])

        self.accepted()  # Create/update the db object

    def read_connection_parameters(self):
        """
        Convenient function to read connection parameters and apply default
        values if needed.
        """
        dict_conn = dict()
        dict_conn['host'] = self.txt_pg_host.text().strip() or 'localhost'
        dict_conn['port'] = self.txt_pg_port.text().strip() or '5432'
        dict_conn['database'] = "'{}'".format(
            self.selected_db_combobox.currentText().strip())
        dict_conn['schema'] = self.selected_schema_combobox.currentText(
        ).strip() or 'public'
        dict_conn['username'] = self.txt_pg_user.text().strip()
        dict_conn['password'] = self.txt_pg_password.text().strip()
        dict_conn['dbfile'] = self.txt_gpkg_file.text().strip()
        return dict_conn

    def save_settings(self):
        # Save QSettings
        dict_conn = self.read_connection_parameters()
        settings = QSettings()
        settings.setValue('Asistente-LADM_COL/db_connection_source',
                          self.cbo_db_source.currentData())
        settings.setValue('Asistente-LADM_COL/pg/host', dict_conn['host'])
        settings.setValue('Asistente-LADM_COL/pg/port', dict_conn['port'])
        settings.setValue('Asistente-LADM_COL/pg/database',
                          dict_conn['database'].strip("'"))
        settings.setValue('Asistente-LADM_COL/pg/schema', dict_conn['schema'])
        settings.setValue('Asistente-LADM_COL/pg/username',
                          dict_conn['username'])
        settings.setValue('Asistente-LADM_COL/pg/password',
                          dict_conn['password'])
        settings.setValue('Asistente-LADM_COL/gpkg/dbfile',
                          dict_conn['dbfile'])

        settings.setValue(
            'Asistente-LADM_COL/models/custom_model_directories_is_checked',
            self.offline_models_radio_button.isChecked())
        if self.offline_models_radio_button.isChecked():
            settings.setValue('Asistente-LADM_COL/models/custom_models',
                              self.custom_model_directories_line_edit.text())

        settings.setValue(
            'Asistente-LADM_COL/quality/too_long_tolerance',
            int(self.txt_too_long_tolerance.text())
            or DEFAULT_TOO_LONG_BOUNDARY_SEGMENTS_TOLERANCE)
        settings.setValue('Asistente-LADM_COL/quality/use_roads',
                          self.chk_use_roads.isChecked())

        settings.setValue(
            'Asistente-LADM_COL/automatic_values/automatic_values_in_batch_mode',
            self.chk_automatic_values_in_batch_mode.isChecked())
        settings.setValue('Asistente-LADM_COL/sources/document_repository',
                          self.connection_box.isChecked())

        endpoint = self.txt_service_endpoint.text().strip()
        settings.setValue(
            'Asistente-LADM_COL/sources/service_endpoint',
            (endpoint[:-1] if endpoint.endswith('/') else endpoint)
            or DEFAULT_ENDPOINT_SOURCE_SERVICE)

        # Changes in automatic namespace or local_id configuration?
        current_namespace_enabled = settings.value(
            'Asistente-LADM_COL/automatic_values/namespace_enabled', True,
            bool)
        current_namespace_prefix = settings.value(
            'Asistente-LADM_COL/automatic_values/namespace_prefix', "")
        current_local_id_enabled = settings.value(
            'Asistente-LADM_COL/automatic_values/local_id_enabled', True, bool)

        settings.setValue(
            'Asistente-LADM_COL/automatic_values/namespace_enabled',
            self.namespace_collapsible_group_box.isChecked())
        if self.namespace_collapsible_group_box.isChecked():
            settings.setValue(
                'Asistente-LADM_COL/automatic_values/namespace_prefix',
                self.txt_namespace.text())

        settings.setValue(
            'Asistente-LADM_COL/automatic_values/local_id_enabled',
            self.chk_local_id.isChecked())

        if current_namespace_enabled != self.namespace_collapsible_group_box.isChecked() or \
           current_namespace_prefix != self.txt_namespace.text() or \
           current_local_id_enabled != self.chk_local_id.isChecked():

            self.qgis_utils.automatic_namespace_local_id_configuration_changed(
                self._db)

    def restore_settings(self):
        # Restore QSettings
        settings = QSettings()
        self.cbo_db_source.setCurrentIndex(
            self.cbo_db_source.findData(
                settings.value('Asistente-LADM_COL/db_connection_source',
                               'pg')))
        self.db_source_changed()
        self.txt_pg_host.setText(settings.value('Asistente-LADM_COL/pg/host'))
        self.txt_pg_port.setText(settings.value('Asistente-LADM_COL/pg/port'))

        dbname_setting = settings.value('Asistente-LADM_COL/pg/database')
        if self.selected_db_combobox.count():
            index = self.selected_db_combobox.findText(dbname_setting,
                                                       Qt.MatchFixedString)
            if index >= 0:
                self.selected_db_combobox.setCurrentIndex(index)
        else:
            self.selected_db_combobox.addItem(dbname_setting)

        schema_setting = settings.value('Asistente-LADM_COL/pg/schema')
        if self.selected_schema_combobox.count():
            index = self.selected_schema_combobox.findText(
                schema_setting, Qt.MatchFixedString)
            if index >= 0:
                self.selected_schema_combobox.setCurrentIndex(index)
        else:
            self.selected_schema_combobox.addItem(schema_setting)

        self.txt_pg_user.setText(
            settings.value('Asistente-LADM_COL/pg/username'))
        self.txt_pg_password.setText(
            settings.value('Asistente-LADM_COL/pg/password'))
        self.txt_gpkg_file.setText(
            settings.value('Asistente-LADM_COL/gpkg/dbfile'))

        custom_model_directories_is_checked = settings.value(
            'Asistente-LADM_COL/models/custom_model_directories_is_checked',
            type=bool)
        if custom_model_directories_is_checked:
            self.offline_models_radio_button.setChecked(True)
            self.custom_model_directories_line_edit.setText(
                settings.value('Asistente-LADM_COL/models/custom_models'))
            self.custom_model_directories_line_edit.setVisible(True)
            self.custom_models_dir_button.setVisible(True)
        else:
            self.online_models_radio_button.setChecked(True)
            self.custom_model_directories_line_edit.setText("")
            self.custom_model_directories_line_edit.setVisible(False)
            self.custom_models_dir_button.setVisible(False)

        self.txt_too_long_tolerance.setText(
            str(
                settings.value('Asistente-LADM_COL/quality/too_long_tolerance',
                               DEFAULT_TOO_LONG_BOUNDARY_SEGMENTS_TOLERANCE)))
        use_roads = settings.value('Asistente-LADM_COL/quality/use_roads',
                                   True, bool)
        self.chk_use_roads.setChecked(use_roads)
        self.update_images_state(use_roads)

        self.chk_automatic_values_in_batch_mode.setChecked(
            settings.value(
                'Asistente-LADM_COL/automatic_values/automatic_values_in_batch_mode',
                True, bool))
        self.connection_box.setChecked(
            settings.value('Asistente-LADM_COL/sources/document_repository',
                           True, bool))
        self.namespace_collapsible_group_box.setChecked(
            settings.value(
                'Asistente-LADM_COL/automatic_values/namespace_enabled', True,
                bool))
        self.chk_local_id.setChecked(
            settings.value(
                'Asistente-LADM_COL/automatic_values/local_id_enabled', True,
                bool))
        self.txt_namespace.setText(
            str(
                settings.value(
                    'Asistente-LADM_COL/automatic_values/namespace_prefix',
                    "")))

        self.txt_service_endpoint.setText(
            settings.value('Asistente-LADM_COL/sources/service_endpoint',
                           DEFAULT_ENDPOINT_SOURCE_SERVICE))

    def db_source_changed(self):
        if self._db is not None:
            self._db.close_connection()

        self._db = None  # Reset db connection
        if self.cbo_db_source.currentData() == 'pg':
            self.gpkg_config.setVisible(False)
            self.pg_config.setVisible(True)
        else:
            self.pg_config.setVisible(False)
            self.gpkg_config.setVisible(True)

    def test_connection(self):
        if self._db is not None:
            self._db.close_connection()

        self._db = None  # Reset db connection
        db = self.get_db_connection(False)
        res, msg = db.test_connection()

        if db is not None:
            db.close_connection()

        self.show_message(msg, Qgis.Info if res else Qgis.Warning)
        self.log.logMessage("Test connection!", PLUGIN_NAME, Qgis.Info)

    def test_service(self):
        self.setEnabled(False)
        QCoreApplication.processEvents()
        res, msg = self.is_source_service_valid()
        self.setEnabled(True)
        self.show_message(msg['text'], msg['level'])

    def is_source_service_valid(self):
        res = False
        msg = {'text': '', 'level': Qgis.Warning}
        url = self.txt_service_endpoint.text().strip()
        if url:
            with OverrideCursor(Qt.WaitCursor):
                self.qgis_utils.status_bar_message_emitted.emit(
                    "Checking source service availability (this might take a while)...",
                    0)
                QCoreApplication.processEvents()
                if self.qgis_utils.is_connected(TEST_SERVER):

                    nam = QNetworkAccessManager()
                    request = QNetworkRequest(QUrl(url))
                    reply = nam.get(request)

                    loop = QEventLoop()
                    reply.finished.connect(loop.quit)
                    loop.exec_()

                    allData = reply.readAll()
                    response = QTextStream(allData, QIODevice.ReadOnly)
                    status = reply.attribute(
                        QNetworkRequest.HttpStatusCodeAttribute)
                    if status == 200:
                        try:
                            data = json.loads(response.readAll())
                            if 'id' in data and data[
                                    'id'] == SOURCE_SERVICE_EXPECTED_ID:
                                res = True
                                msg['text'] = QCoreApplication.translate(
                                    "SettingsDialog",
                                    "The tested service is valid to upload files!"
                                )
                                msg['level'] = Qgis.Info
                            else:
                                res = False
                                msg['text'] = QCoreApplication.translate(
                                    "SettingsDialog",
                                    "The tested upload service is not compatible: no valid 'id' found in response."
                                )
                        except json.decoder.JSONDecodeError as e:
                            res = False
                            msg['text'] = QCoreApplication.translate(
                                "SettingsDialog",
                                "Response from the tested service is not compatible: not valid JSON found."
                            )
                    else:
                        res = False
                        msg['text'] = QCoreApplication.translate(
                            "SettingsDialog",
                            "There was a problem connecting to the server. The server might be down or the service cannot be reached at the given URL."
                        )
                else:
                    res = False
                    msg['text'] = QCoreApplication.translate(
                        "SettingsDialog",
                        "There was a problem connecting to Internet.")

                self.qgis_utils.clear_status_bar_emitted.emit()
        else:
            res = False
            msg['text'] = QCoreApplication.translate(
                "SettingsDialog", "Not valid service URL to test!")

        return (res, msg)

    def show_message(self, message, level):
        self.bar.pushMessage(message, level, 10)

    def set_connection_dirty(self, text):
        if not self.connection_is_dirty:
            self.connection_is_dirty = True

    def update_images_state(self, checked):
        self.img_with_roads.setEnabled(checked)
        self.img_with_roads.setToolTip(
            QCoreApplication.translate(
                "SettingsDialog", "Missing roads will be marked as errors."
            ) if checked else '')
        self.img_without_roads.setEnabled(not checked)
        self.img_without_roads.setToolTip(
            '' if checked else QCoreApplication.translate(
                "SettingsDialog", "Missing roads will not be marked as errors."
            ))

    def show_help(self):
        self.qgis_utils.show_help("settings")

    def database_created(self, db_name):
        self.update_db_names()

        # select the database created by the user
        index = self.selected_db_combobox.findText(db_name,
                                                   Qt.MatchFixedString)
        if index >= 0:
            self.selected_db_combobox.setCurrentIndex(index)

    def schema_created(self, schema_name):
        self.update_db_schemas()

        # select the database created by the user
        index = self.selected_schema_combobox.findText(schema_name,
                                                       Qt.MatchFixedString)
        if index >= 0:
            self.selected_schema_combobox.setCurrentIndex(index)

    def show_modal_create_db(self):
        if self.cbo_db_source.currentData() == 'pg':
            tmp_db_conn = PGConnector('')
            dict_conn = self.read_connection_parameters()
            uri = tmp_db_conn.get_connection_uri(dict_conn, 'pg', level=0)
            test_conn = tmp_db_conn.test_connection(uri=uri, level=0)
            if test_conn[0]:
                create_db_dlg = DialogGetDBOrSchemaName(dict_conn,
                                                        'database',
                                                        parent=self)
                create_db_dlg.db_or_schema_created.connect(
                    self.database_created)
                create_db_dlg.setModal(True)
                create_db_dlg.exec_()
            else:
                self.show_message(
                    QCoreApplication.translate(
                        "SettingsDialog",
                        "First set the connection to the database before attempting to create a database."
                    ), Qgis.Warning)

    def show_modal_create_schema(self):
        if self.cbo_db_source.currentData() == 'pg':
            tmp_db_conn = PGConnector('')
            dict_conn = self.read_connection_parameters()
            uri = tmp_db_conn.get_connection_uri(dict_conn, 'pg', level=0)
            test_conn = tmp_db_conn.test_connection(uri=uri, level=0)

            if test_conn[0]:
                create_db_dlg = DialogGetDBOrSchemaName(
                    self.read_connection_parameters(), 'schema', parent=self)
                create_db_dlg.db_or_schema_created.connect(self.schema_created)
                create_db_dlg.setModal(True)
                create_db_dlg.exec_()
            else:
                self.show_message(
                    QCoreApplication.translate(
                        "SettingsDialog",
                        "First set the connection to the database before attempting to create a schema."
                    ), Qgis.Warning)
class QmsServiceToolbox(QDockWidget, FORM_CLASS):
    def __init__(self, iface):
        QDockWidget.__init__(self, iface.mainWindow())
        self.setupUi(self)
        self.newsFrame.setVisible(False)

        self.iface = iface
        self.search_threads = None  # []
        self.extent_renderer = RubberBandResultRenderer()

        self.cmbStatusFilter.addItem(self.tr('All'), STATUS_FILTER_ALL)
        self.cmbStatusFilter.addItem(self.tr('Valid'), STATUS_FILTER_ONLY_WORKS)
        self.cmbStatusFilter.currentIndexChanged.connect(self.start_search)

        if hasattr(self.txtSearch, 'setPlaceholderText'):
            self.txtSearch.setPlaceholderText(self.tr("Search string..."))

        self.delay_timer = QTimer(self)
        self.delay_timer.setSingleShot(True)
        self.delay_timer.setInterval(250)

        self.delay_timer.timeout.connect(self.start_search)
        self.txtSearch.textChanged.connect(self.delay_timer.start)
        self.btnFilterByExtent.toggled.connect(self.toggle_filter_button)
        self.one_process_work = QMutex()

        self.add_last_used_services()
        
        self.show_news()

    def show_news(self):
        client = Client()
        client.set_proxy(*QGISSettings.get_qgis_proxy())
        qms_news = client.get_news()

        if qms_news is None:
            self.newsFrame.setVisible(False)
            return

        news = News(qms_news)

        if news.is_time_to_show():
            self.newsLabel.setText(news.html)
            self.newsFrame.setVisible(True)
        else:
            self.newsFrame.setVisible(False)

    def toggle_filter_button(self, checked):
        self.txtSearch.setDisabled(checked)
        if checked:
            self.iface.mapCanvas().extentsChanged.connect(self.start_search)
            self.iface.mapCanvas().destinationCrsChanged.connect(self.start_search)
            self.start_search()
        else:
            self.iface.mapCanvas().extentsChanged.disconnect(self.start_search)
            self.iface.mapCanvas().destinationCrsChanged.disconnect(self.start_search)


    def start_search(self):
        search_text = None
        geom_filter = None

        # status filter
        status_filter = None
        sel_value = self.cmbStatusFilter.itemData(self.cmbStatusFilter.currentIndex())
        if sel_value != STATUS_FILTER_ALL:
            status_filter = sel_value

        if not self.btnFilterByExtent.isChecked():
            # text search
            search_text = unicode(self.txtSearch.text())
            if not search_text:
                self.lstSearchResult.clear()
                # self.clearSearchResult()
                self.add_last_used_services()
                return
        else:
            # extent filter
            extent = self.iface.mapCanvas().extent()
            map_crs = getCanvasDestinationCrs(self.iface)
            if map_crs.postgisSrid() != 4326:
                crsDest = QgsCoordinateReferenceSystem(4326)    # WGS 84
                xform = QgsCoordinateTransform(map_crs, crsDest)
                extent = xform.transform(extent)
            geom_filter = extent.asWktPolygon()

        if self.search_threads:
            self.search_threads.data_downloaded.disconnect()
            self.search_threads.search_finished.disconnect()
            self.search_threads.stop()
            self.search_threads.wait()
            
            self.lstSearchResult.clear()
            # self.clearSearchResult()

        searcher = SearchThread(search_text,
                                self.one_process_work,
                                parent=self.iface.mainWindow(),
                                geom_filter=geom_filter,
                                status_filter=status_filter)
        searcher.data_downloaded.connect(self.show_result)
        searcher.error_occurred.connect(self.show_error)
        searcher.search_started.connect(self.search_started_process)
        searcher.search_finished.connect(self.search_finished_progress)
        self.search_threads = searcher
        searcher.start()

    def add_last_used_services(self):
        services = CachedServices().get_cached_services()
        if len(services) == 0:
            return

        self.lstSearchResult.insertItem(0, self.tr("Last used:"))
        # l = QLabel(self.tr("Last used:"))
        # l.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
        # self.lSearchResult.addWidget(l)

        for attributes, image_qByteArray in services:
            custom_widget = QmsSearchResultItemWidget(
                attributes,
                image_qByteArray
            )
            new_item = QListWidgetItem(self.lstSearchResult)
            new_item.setSizeHint(custom_widget.sizeHint())
            self.lstSearchResult.addItem(new_item)
            self.lstSearchResult.setItemWidget(
                new_item,
                custom_widget
            )
            # self.lSearchResult.addWidget(custom_widget)

        # w = QWidget()
        # w.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        # self.lSearchResult.addWidget(w)

    def search_started_process(self):
        self.lstSearchResult.clear()
        self.lstSearchResult.insertItem(0, self.tr('Searching...'))


    def search_finished_progress(self):
        self.lstSearchResult.takeItem(0)
        if self.lstSearchResult.count() == 0:
            new_widget = QLabel()
            new_widget.setTextFormat(Qt.RichText)
            new_widget.setOpenExternalLinks(True)
            new_widget.setWordWrap(True)
            new_widget.setText(
                u"<div align='center'> <strong>{}</strong> </div><div align='center' style='margin-top: 3px'> {} </div>".format(
                    self.tr(u"No results."),
                    self.tr(u"You can add a service to become searchable. Start <a href='{}'>here</a>.").format(
                        u"https://qms.nextgis.com/create"
                    ),
                )
            )
            new_item = QListWidgetItem(self.lstSearchResult)
            new_item.setSizeHint(new_widget.sizeHint())
            self.lstSearchResult.addItem(new_item)
            self.lstSearchResult.setItemWidget(
                new_item,
                new_widget
            )


    def show_result(self, geoservice, image_ba):
        if geoservice:
            custom_widget = QmsSearchResultItemWidget(geoservice, image_ba, extent_renderer=self.extent_renderer)
            new_item = QListWidgetItem(self.lstSearchResult)
            new_item.setSizeHint(custom_widget.sizeHint())
            self.lstSearchResult.addItem(new_item)
            self.lstSearchResult.setItemWidget(
                new_item,
                custom_widget
            )

        else:
            new_item = QListWidgetItem()
            new_item.setText(self.tr('No results!'))
            new_item.setData(Qt.UserRole, None)
            self.lstSearchResult.addItem(new_item)
        self.lstSearchResult.update()


    def show_error(self, error_text):
        self.lstSearchResult.clear()
        new_widget = QLabel()
        new_widget.setTextFormat(Qt.RichText)
        new_widget.setOpenExternalLinks(True)
        new_widget.setWordWrap(True)
        new_widget.setText(
            u"<div align='center'> <strong>{}</strong> </div><div align='center' style='margin-top: 3px'> {} </div>".format(
                self.tr('Error'),
                error_text
            )
        )
        new_item = QListWidgetItem(self.lstSearchResult)
        new_item.setSizeHint(new_widget.sizeHint())
        self.lstSearchResult.addItem(new_item)
        self.lstSearchResult.setItemWidget(
            new_item,
            new_widget
        )
예제 #21
0
class QadMapTool(QgsMapTool):
    def __init__(self, plugIn):
        QgsMapTool.__init__(self, plugIn.iface.mapCanvas())
        self.plugIn = plugIn
        self.iface = self.plugIn.iface
        self.canvas = self.plugIn.iface.mapCanvas()
        self.cursor = QCursor(Qt.BlankCursor)
        self.__csrRubberBand = QadCursorRubberBand(
            self.canvas, QadCursorTypeEnum.BOX | QadCursorTypeEnum.CROSS)
        self.entitySet = QadEntitySet()
        self.entitySetGripPoints = QadEntitySetGripPoints(plugIn)

        self.gripPopupMenu = None
        self.timerForGripMenu = QTimer()
        self.timerForGripMenu.setSingleShot(True)

        self.startDateTimeForRightClick = 0

        # input dinamico
        self.dynamicCmdInput = QadDynamicCmdInput(plugIn)

    def __del__(self):
        self.removeItems()

    def removeItems(self):
        if self.__csrRubberBand is not None:
            self.__csrRubberBand.removeItems(
            )  # prima lo stacco dal canvas altrimenti non si rimuove perchè usato da canvas
            del self.__csrRubberBand
            __csrRubberBand = None
        self.entitySet.clear()
        self.entitySetGripPoints.removeItems()

        if self.dynamicCmdInput is not None:
            self.dynamicCmdInput.removeItems()
            del self.dynamicCmdInput
            self.dynamicCmdInput = None

    #============================================================================
    # getDynamicInput
    #============================================================================
    def getDynamicInput(self):
        return self.dynamicCmdInput

    #============================================================================
    # UpdatedVariablesEvent
    #============================================================================
    def UpdatedVariablesEvent(self):
        # aggiorna in base alle nuove impostazioni delle variabili
        self.removeItems()
        self.__csrRubberBand = QadCursorRubberBand(
            self.canvas, QadCursorTypeEnum.BOX | QadCursorTypeEnum.CROSS)
        if self.dynamicCmdInput is not None:
            del self.dynamicCmdInput
        self.dynamicCmdInput = QadDynamicCmdInput(self.plugIn)

    #============================================================================
    # clearEntitySet
    #============================================================================
    def clearEntitySet(self):
        self.entitySet.deselectOnLayer()
        self.entitySet.clear()

    #============================================================================
    # clearEntityGripPoints
    #============================================================================
    def clearEntityGripPoints(self):
        self.entitySetGripPoints.removeItems()  # svuoto la lista

    #============================================================================
    # refreshEntityGripPoints
    #============================================================================
    def refreshEntityGripPoints(self, entitySet=None):
        if entitySet is None:
            entitySet = self.entitySet

        gripObjLimit = QadVariables.get(
            QadMsg.translate("Environment variables", "GRIPOBJLIMIT"))
        if gripObjLimit != 0:  #  When set to 0, grips are always displayed.
            if entitySet.count() > gripObjLimit:
                # Suppresses the display of grips when the selection set includes more than the specified number of objects
                self.clearEntityGripPoints()
                return

        # cancello i grip delle entità che non sono in entitySet o che non sono in layer vettoriali modificabili
        i = self.entitySetGripPoints.count() - 1
        while i >= 0:
            entityGripPoint = self.entitySetGripPoints.entityGripPoints[i]
            if entitySet.containsEntity(entityGripPoint.entity) == False or \
               entityGripPoint.entity.layer.type() != QgsMapLayer.VectorLayer or entityGripPoint.entity.layer.isEditable() == False:
                self.entitySetGripPoints.entityGripPoints[i].removeItems(
                )  # lo stacco dal canvas
                del self.entitySetGripPoints.entityGripPoints[i]
            i = i - 1

        entity = QadEntity()
        for layerEntitySet in entitySet.layerEntitySetList:
            # considero solo i layer vettoriali che sono modificabili
            layer = layerEntitySet.layer
            if layer.type() == QgsMapLayer.VectorLayer and layer.isEditable():
                for featureId in layerEntitySet.featureIds:
                    entity.set(layer, featureId)
                    self.entitySetGripPoints.addEntity(
                        entity,
                        QadVariables.get(
                            QadMsg.translate("Environment variables",
                                             "GRIPS")))

    #============================================================================
    # INIZIO - eventi per il mouse
    #============================================================================

    #============================================================================
    # canvasPressEvent
    #============================================================================
    def canvasPressEvent(self, event):
        if event.button() == Qt.RightButton:
            self.startDateTimeForRightClick = datetime.datetime.now()
        elif event.button() == Qt.LeftButton:
            # verifico se tasto shift premuto
            shiftKey = True if event.modifiers() & Qt.ShiftModifier else False
            # posizione corrente del mouse
            point = self.toMapCoordinates(event.pos())
            # leggo il punto grip che si interseca alla posizione del mouse
            entityGripPoints, entityGripPoint = self.entitySetGripPoints.isIntersecting(
                point)
            if entityGripPoint is not None:
                if shiftKey == False:  # lancio il comando
                    selectedEntityGripPoints = self.entitySetGripPoints.getSelectedEntityGripPoints(
                    )
                    # se non ci sono già grip selezionati
                    if len(selectedEntityGripPoints) == 0:
                        # seleziono il corrente
                        if self.entitySetGripPoints.selectIntersectingGripPoints(
                                point) > 0:
                            selectedEntityGripPoints = self.entitySetGripPoints.getSelectedEntityGripPoints(
                            )

                    # lancio il comando
                    self.plugIn.runCommand("QadVirtualGripCommandsClass", [QadVirtualGripCommandsEnum.STRECTH, \
                                           self.entitySetGripPoints, entityGripPoint.getPoint()])
                else:  # shift premuto
                    # inverto lo stato ai grip che intersecano il punto
                    self.entitySetGripPoints.toggleSelectIntersectingGripPoints(
                        point)
            else:
                result = qad_utils.getEntSel(event.pos(), self, \
                                             QadVariables.get(QadMsg.translate("Environment variables", "PICKBOX")))
                if result is not None:
                    feature = result[0]
                    layer = result[1]
                    tmpEntity = QadEntity()
                    tmpEntity.set(layer, feature.id())
                    SSGetClass = QadSSGetClass(self.plugIn)
                    SSGetClass.entitySet.set(self.entitySet)
                    SSGetClass.elaborateEntity(tmpEntity, shiftKey)
                    self.plugIn.showMsg("\n", True)  # ripete il prompt
                    self.entitySet.set(SSGetClass.entitySet)
                    del SSGetClass  # che deseleziona gli oggetti
                    self.entitySet.selectOnLayer(False)
                    self.refreshEntityGripPoints(self.entitySet)
                else:
                    self.plugIn.runCommand("QadVirtualSelCommandClass", point)

    #============================================================================
    # canvasDoubleClickEvent
    #============================================================================
    def canvasDoubleClickEvent(self, event):
        pass

    #============================================================================
    # canvasMoveEvent
    #============================================================================
    def canvasMoveEvent(self, event):
        self.timerForGripMenu.stop()
        point = self.toMapCoordinates(event.pos())
        self.__csrRubberBand.moveEvent(point)

        if self.dynamicCmdInput.prevPart is not None or self.dynamicCmdInput.nextPart is not None:
            changedPart = True
        else:
            changedPart = False
        self.dynamicCmdInput.setPrevPart(None)
        self.dynamicCmdInput.setNextPart(None)

        # hover grip points
        if self.entitySetGripPoints.hoverIntersectingGripPoints(point) == 1:
            for entityGripPoint in self.entitySetGripPoints.entityGripPoints:
                for gripPoint in entityGripPoint.gripPoints:
                    if gripPoint.isIntersecting(point) and gripPoint.getStatus(
                    ) == QadGripStatusEnum.HOVER:
                        self.dynamicCmdInput.setPrevNextPart(
                            entityGripPoint.entity, gripPoint)
                        self.dynamicCmdInput.show(True,
                                                  self.canvas.mouseLastXY())

                        # Specifica i metodi di accesso per le opzioni dei grip multifunzionali.
                        # se > 1 devono essere mostrati i menu dinamici
                        if QadVariables.get(
                                QadMsg.translate("Environment variables",
                                                 "GRIPMULTIFUNCTIONAL")) > 1:
                            pos = QPoint(event.pos().x(), event.pos().y())
                            shot = lambda: self.displayPopupMenuOnGrip(
                                pos, entityGripPoint.entity, gripPoint)

                            del self.timerForGripMenu
                            self.timerForGripMenu = QTimer()
                            self.timerForGripMenu.setSingleShot(True)
                            self.timerForGripMenu.timeout.connect(shot)
                            self.timerForGripMenu.start(
                                1000)  # 1 sec tempo per il grip
                            return
        else:
            if changedPart == True:
                self.dynamicCmdInput.show(True, self.canvas.mouseLastXY())
            else:
                self.dynamicCmdInput.mouseMoveEvent(event.pos())

    #============================================================================
    # canvasReleaseEvent
    #============================================================================
    def canvasReleaseEvent(self, event):
        if event.button() == Qt.RightButton:
            shortCutMenu = QadVariables.get(
                QadMsg.translate("Environment variables", "SHORTCUTMENU"))
            if shortCutMenu == 0:
                # equivale a premere INVIO
                return self.plugIn.showEvaluateMsg(None)
            else:
                if self.entitySet.count(
                ) == 0:  # nessun oggetto selezionato (modalità Default)
                    # 16 = Enables the display of a shortcut menu when the right button on the pointing device is held down long enough
                    if shortCutMenu & 16:
                        now = datetime.datetime.now()
                        value = QadVariables.get(
                            QadMsg.translate("Environment variables",
                                             "SHORTCUTMENUDURATION"))
                        shortCutMenuDuration = datetime.timedelta(
                            0, 0, 0, value)
                        # se supera il numero di millisecondi impostato da SHORTCUTMENUDURATION
                        if now - self.startDateTimeForRightClick > shortCutMenuDuration:
                            return self.displayPopupMenuOnQuiescentState(
                                event.pos())
                        else:
                            # se click veloce equivale a premere INVIO
                            return self.plugIn.showEvaluateMsg(None)
                    else:
                        # 1 = Enables Default mode shortcut menus
                        if shortCutMenu & 1:
                            return self.displayPopupMenuOnQuiescentState(
                                event.pos())
                        else:
                            # equivale a premere INVIO
                            return self.plugIn.showEvaluateMsg(None)
                else:  # ci sono degli oggetti selezionati
                    # 2 = Enables Edit mode shortcut menus
                    if shortCutMenu & 2:
                        return self.displayPopupMenuOnQuiescentState(
                            event.pos())
                    else:
                        # equivale a premere INVIO
                        return self.plugIn.showEvaluateMsg(None)

    #============================================================================
    # FINE - eventi per il mouse
    # INIZIO - eventi per la tastiera
    #============================================================================

    #============================================================================
    # keyPressEvent
    #============================================================================
    def keyPressEvent(self, e):
        myEvent = e
        # ALTGR non si può usare perchè è usato per indicare le coordinate
        #       # if Key_AltGr is pressed, then perform the as return
        #       if e.key() == Qt.Key_AltGr:
        #          myEvent = QKeyEvent(QEvent.KeyPress, Qt.Key_Return, Qt.NoModifier)
        #       else:
        #          myEvent = e

        if self.plugIn.shortCutManagement(
                myEvent
        ):  # se è stata gestita una sequenza di tasti scorciatoia
            return

        if myEvent.text() != "" and self.dynamicCmdInput.show(
                True, self.canvas.mouseLastXY(),
                self.dynamicCmdInput.getPrompt()) == True:
            self.dynamicCmdInput.keyPressEvent(myEvent)
        else:
            self.plugIn.keyPressEvent(myEvent)

    #============================================================================
    # FINE - eventi per la tastiera
    # INIZIO - eventi per la rotella
    #============================================================================

    #============================================================================
    # wheelEvent
    #============================================================================
    def wheelEvent(self, event):
        QgsMapTool.wheelEvent(self, event)
        self.__csrRubberBand.moveEvent(self.toMapCoordinates(event.pos()))

    #============================================================================
    # FINE - eventi per la rotella
    #============================================================================

    #============================================================================
    # activate
    #============================================================================
    def activate(self):
        self.canvas.setToolTip("")
        self.canvas.setCursor(self.cursor)
        # posizione corrente del mouse
        self.__csrRubberBand.moveEvent(
            self.toMapCoordinates(self.canvas.mouseLastXY()))
        self.__csrRubberBand.show()
        self.entitySet.initByCurrentQgsSelectedFeatures(
            qad_utils.getVisibleVectorLayers(
                self.canvas))  # Tutti i layer vettoriali visibili
        self.refreshEntityGripPoints(self.entitySet)

        self.plugIn.QadCommands.continueCommandFromMapTool()
        #self.plugIn.enableShortcut()

        self.dynamicCmdInput.setPrevPart(None)
        self.dynamicCmdInput.setNextPart(None)
        self.dynamicCmdInput.show(True, self.canvas.mouseLastXY())

    #============================================================================
    # deactivate
    #============================================================================
    def deactivate(self):
        self.__csrRubberBand.hide()
        self.timerForGripMenu.stop()
        #self.plugIn.disableShortcut()

        self.dynamicCmdInput.show(False)

    #============================================================================
    # isTransient
    #============================================================================
    def isTransient(self):
        return False  # questo tool non fa zoom o pan

    #============================================================================
    # isEditTool
    #============================================================================
    def isEditTool(self):
        return False  # questo tool non fa editing

    #============================================================================
    # displayPopupMenuOnQuiescentState
    #============================================================================
    def displayPopupMenuOnQuiescentState(self, pos):
        popupMenu = QMenu(self.canvas)
        history = self.plugIn.cmdsHistory
        isLastCmdToInsert = True
        isRecentMenuToInsert = True

        historyLen = len(history)
        i = historyLen - 1
        cmdInputHistoryMax = QadVariables.get(
            QadMsg.translate("Environment variables", "CMDINPUTHISTORYMAX"))
        while i >= 0 and (historyLen - i) <= cmdInputHistoryMax:
            cmdName = history[i]
            i = i - 1
            cmd = self.plugIn.QadCommands.getCommandObj(cmdName)
            if cmd is not None:
                if isLastCmdToInsert:
                    isLastCmdToInsert = False
                    msg = QadMsg.translate("Popup_menu_graph_window",
                                           "Repeat ") + cmd.getName()
                    icon = cmd.getIcon()
                    if icon is None:
                        lastCmdAction = QAction(msg, popupMenu)
                    else:
                        lastCmdAction = QAction(icon, msg, popupMenu)
                    cmd.connectQAction(lastCmdAction)
                    popupMenu.addAction(lastCmdAction)
                else:
                    if isRecentMenuToInsert:
                        isRecentMenuToInsert = False
                        recentCmdsMenu = popupMenu.addMenu(
                            QadMsg.translate("Popup_menu_graph_window",
                                             "Recent commands"))

                    icon = cmd.getIcon()
                    if icon is None:
                        recentCmdAction = QAction(cmd.getName(),
                                                  recentCmdsMenu)
                    else:
                        recentCmdAction = QAction(icon, cmd.getName(),
                                                  recentCmdsMenu)
                    cmd.connectQAction(recentCmdAction)
                    recentCmdsMenu.addAction(recentCmdAction)

        if isLastCmdToInsert == False:  # menu non vuoto
            popupMenu.addSeparator()

        # aggiungo comando "OPTIONS"
        cmd = self.plugIn.QadCommands.getCommandObj(
            QadMsg.translate("Command_list", "OPTIONS"))
        icon = cmd.getIcon()
        if icon is None:
            optionsCmdAction = QAction(cmd.getName(), popupMenu)
        else:
            optionsCmdAction = QAction(icon, cmd.getName(), popupMenu)
        cmd.connectQAction(optionsCmdAction)
        popupMenu.addAction(optionsCmdAction)

        popupMenu.popup(self.canvas.mapToGlobal(pos))

    #============================================================================
    # runCmdFromPopupMenuOnGrip
    #============================================================================
    def runCmdFromPopupMenuOnGrip(self, virtualGripCommand, gripPoint):
        # seleziona il grip
        gripPoint.select()
        # lancio il comando
        self.plugIn.runCommand("QadVirtualGripCommandsClass", [
            virtualGripCommand, self.entitySetGripPoints,
            gripPoint.getPoint()
        ])

    #============================================================================
    # displayPopupMenuOnGrip
    #============================================================================
    def displayPopupMenuOnGrip(self, pos, entity, gripPoint):
        if self.gripPopupMenu is not None:
            self.gripPopupMenu.hide()
            del self.gripPopupMenu
            self.gripPopupMenu = None

        popupMenu = QadGripPopupMenu(self.canvas)

        found = False

        # verifico se l'entità appartiene ad uno stile di quotatura
        if QadDimStyles.isDimEntity(entity):
            pass
        else:
            qadGeom = entity.getQadGeom(gripPoint.atGeom, gripPoint.atSubGeom)
            qadGeomType = qadGeom.whatIs()
            if qadGeomType == "ARC":
                qadGeom = entity.getQadGeom(gripPoint.atGeom,
                                            gripPoint.atSubGeom)

                # se punti finali
                if gripPoint.isIntersecting(
                        qadGeom.getStartPt()) or gripPoint.isIntersecting(
                            qadGeom.getEndPt()):
                    found = True
                    msg = QadMsg.translate("Popup_menu_grip_window", "Stretch")
                    action = QAction(msg, popupMenu)
                    f = lambda: self.runCmdFromPopupMenuOnGrip(
                        QadVirtualGripCommandsEnum.STRECTH, gripPoint)
                    action.triggered.connect(f)
                    popupMenu.addAction(action)

                    msg = QadMsg.translate("Popup_menu_grip_window",
                                           "Lengthen")
                    action = QAction(msg, popupMenu)
                    f = lambda: self.runCmdFromPopupMenuOnGrip(
                        QadVirtualGripCommandsEnum.LENGTHEN, gripPoint)
                    action.triggered.connect(f)
                    popupMenu.addAction(action)
                # se punto medio
                elif gripPoint.isIntersecting(qadGeom.getMiddlePt()):
                    found = True
                    msg = QadMsg.translate("Popup_menu_grip_window", "Stretch")
                    action = QAction(msg, popupMenu)
                    f = lambda: self.runCmdFromPopupMenuOnGrip(
                        QadVirtualGripCommandsEnum.STRECTH, gripPoint)
                    action.triggered.connect(f)
                    popupMenu.addAction(action)

                    msg = QadMsg.translate("Popup_menu_grip_window", "Radius")
                    action = QAction(msg, popupMenu)
                    f = lambda: self.runCmdFromPopupMenuOnGrip(
                        QadVirtualGripCommandsEnum.CHANGE_RADIUS, gripPoint)
                    action.triggered.connect(f)
                    popupMenu.addAction(action)

                    msg = QadMsg.translate("Popup_menu_grip_window",
                                           "Convert to line")
                    action = QAction(msg, popupMenu)
                    f = lambda: self.runCmdFromPopupMenuOnGrip(
                        QadVirtualGripCommandsEnum.ARC_TO_LINE, gripPoint)
                    action.triggered.connect(f)
                    popupMenu.addAction(action)

            if qadGeomType == "LINE":
                qadGeom = entity.getQadGeom(gripPoint.atGeom,
                                            gripPoint.atSubGeom)

                # se punti finali
                if gripPoint.isIntersecting(
                        qadGeom.getStartPt()) or gripPoint.isIntersecting(
                            qadGeom.getEndPt()):
                    found = True
                    msg = QadMsg.translate("Popup_menu_grip_window", "Stretch")
                    action = QAction(msg, popupMenu)
                    f = lambda: self.runCmdFromPopupMenuOnGrip(
                        QadVirtualGripCommandsEnum.STRECTH, gripPoint)
                    action.triggered.connect(f)
                    popupMenu.addAction(action)

                    msg = QadMsg.translate("Popup_menu_grip_window",
                                           "Lengthen")
                    action = QAction(msg, popupMenu)
                    f = lambda: self.runCmdFromPopupMenuOnGrip(
                        QadVirtualGripCommandsEnum.LENGTHEN, gripPoint)
                    action.triggered.connect(f)
                    popupMenu.addAction(action)

            elif qadGeomType == "POLYLINE":
                isClosed = qadGeom.isClosed()
                nVertex = 0
                found = False
                while nVertex < qadGeom.qty():
                    linearObject = qadGeom.getLinearObjectAt(nVertex)

                    if gripPoint.isIntersecting(linearObject.getStartPt()):
                        found = True
                        msg = QadMsg.translate("Popup_menu_grip_window",
                                               "Stretch vertex")
                        action = QAction(msg, popupMenu)
                        f = lambda: self.runCmdFromPopupMenuOnGrip(
                            QadVirtualGripCommandsEnum.STRECTH, gripPoint)
                        action.triggered.connect(f)
                        popupMenu.addAction(action)

                        # punto iniziale
                        if isClosed == False and nVertex == 0:
                            msg = QadMsg.translate("Popup_menu_grip_window",
                                                   "Lengthen")
                            action = QAction(msg, popupMenu)
                            f = lambda: self.runCmdFromPopupMenuOnGrip(
                                QadVirtualGripCommandsEnum.LENGTHEN, gripPoint)
                            action.triggered.connect(f)
                            popupMenu.addAction(action)

                        msg = QadMsg.translate("Popup_menu_grip_window",
                                               "Add vertex")
                        action = QAction(msg, popupMenu)
                        f = lambda: self.runCmdFromPopupMenuOnGrip(
                            QadVirtualGripCommandsEnum.ADD_VERTEX, gripPoint)
                        action.triggered.connect(f)
                        popupMenu.addAction(action)

                        msg = QadMsg.translate("Popup_menu_grip_window",
                                               "Add vertex before")
                        action = QAction(msg, popupMenu)
                        f = lambda: self.runCmdFromPopupMenuOnGrip(
                            QadVirtualGripCommandsEnum.ADD_VERTEX_BEFORE,
                            gripPoint)
                        action.triggered.connect(f)
                        popupMenu.addAction(action)
                        break

                    # punto medio
                    if gripPoint.isIntersecting(linearObject.getMiddlePt()):
                        found = True
                        msg = QadMsg.translate("Popup_menu_grip_window",
                                               "Stretch")
                        action = QAction(msg, popupMenu)
                        f = lambda: self.runCmdFromPopupMenuOnGrip(
                            QadVirtualGripCommandsEnum.STRECTH, gripPoint)
                        action.triggered.connect(f)
                        popupMenu.addAction(action)

                        msg = QadMsg.translate("Popup_menu_grip_window",
                                               "Add vertex")
                        action = QAction(msg, popupMenu)
                        f = lambda: self.runCmdFromPopupMenuOnGrip(
                            QadVirtualGripCommandsEnum.ADD_VERTEX, gripPoint)
                        action.triggered.connect(f)
                        popupMenu.addAction(action)

                        msg = QadMsg.translate("Popup_menu_grip_window",
                                               "Add vertex before")
                        action = QAction(msg, popupMenu)
                        f = lambda: self.runCmdFromPopupMenuOnGrip(
                            QadVirtualGripCommandsEnum.ADD_VERTEX_BEFORE,
                            gripPoint)
                        action.triggered.connect(f)
                        popupMenu.addAction(action)

                        gType = linearObject.whatIs()
                        if gType == "LINE":
                            msg = QadMsg.translate("Popup_menu_grip_window",
                                                   "Convert to arc")
                            action = QAction(msg, popupMenu)
                            f = lambda: self.runCmdFromPopupMenuOnGrip(
                                QadVirtualGripCommandsEnum.LINE_TO_ARC,
                                gripPoint)
                        elif gType == "ARC":
                            msg = QadMsg.translate("Popup_menu_grip_window",
                                                   "Convert to line")
                            action = QAction(msg, popupMenu)
                            f = lambda: self.runCmdFromPopupMenuOnGrip(
                                QadVirtualGripCommandsEnum.ARC_TO_LINE,
                                gripPoint)
                        action.triggered.connect(f)
                        popupMenu.addAction(action)
                        break

                    nVertex = nVertex + 1

                linearObject = qadGeom.getLinearObjectAt(-1)  # ultima parte
                if not found and isClosed == False:
                    # punto finale
                    if gripPoint.isIntersecting(linearObject.getEndPt()):
                        found = True
                        msg = QadMsg.translate("Popup_menu_grip_window",
                                               "Stretch vertex")
                        action = QAction(msg, popupMenu)
                        f = lambda: self.runCmdFromPopupMenuOnGrip(
                            QadVirtualGripCommandsEnum.STRECTH, gripPoint)
                        action.triggered.connect(f)
                        popupMenu.addAction(action)

                        msg = QadMsg.translate("Popup_menu_grip_window",
                                               "Lengthen")
                        action = QAction(msg, popupMenu)
                        f = lambda: self.runCmdFromPopupMenuOnGrip(
                            QadVirtualGripCommandsEnum.LENGTHEN, gripPoint)
                        action.triggered.connect(f)
                        popupMenu.addAction(action)

                        msg = QadMsg.translate("Popup_menu_grip_window",
                                               "Add vertex")
                        action = QAction(msg, popupMenu)
                        f = lambda: self.runCmdFromPopupMenuOnGrip(
                            QadVirtualGripCommandsEnum.ADD_VERTEX, gripPoint)
                        action.triggered.connect(f)
                        popupMenu.addAction(action)

                        msg = QadMsg.translate("Popup_menu_grip_window",
                                               "Add vertex before")
                        action = QAction(msg, popupMenu)
                        f = lambda: self.runCmdFromPopupMenuOnGrip(
                            QadVirtualGripCommandsEnum.ADD_VERTEX_BEFORE,
                            gripPoint)
                        action.triggered.connect(f)
                        popupMenu.addAction(action)

                if isClosed == False:  # polyline
                    # ci devono essere almeno 2 parti
                    if qadGeom.qty() >= 2:
                        msg = QadMsg.translate("Popup_menu_grip_window",
                                               "Remove vertex")
                        action = QAction(msg, popupMenu)
                        f = lambda: self.runCmdFromPopupMenuOnGrip(
                            QadVirtualGripCommandsEnum.REMOVE_VERTEX, gripPoint
                        )
                        action.triggered.connect(f)
                        popupMenu.addAction(action)
                else:  # polygon
                    # ci devono essere almeno 4 parti
                    if qadGeom.qty() >= 4:
                        msg = QadMsg.translate("Popup_menu_grip_window",
                                               "Remove vertex")
                        action = QAction(msg, popupMenu)
                        f = lambda: self.runCmdFromPopupMenuOnGrip(
                            QadVirtualGripCommandsEnum.REMOVE_VERTEX, gripPoint
                        )
                        action.triggered.connect(f)
                        popupMenu.addAction(action)

        if found:  # menu non vuoto
            popupMenu.popup(self.canvas.mapToGlobal(pos))
            self.gripPopupMenu = popupMenu

        return None
예제 #22
0
class ValidatingLineEdit(QLineEdit):
    '''
    Custom QLineEdit control that validates user input against database
    values as the user types.
    '''
    # Signal raised when the user input is invalid.
    invalidatedInput = pyqtSignal()

    def __init__(self,
                 parent=None,
                 notificationbar=None,
                 dbmodel=None,
                 attrname=None):
        '''
        :param parent: Parent widget
        :param notificationbar: instance of stdm.ui.NotificationBar class.
        '''
        QLineEdit.__init__(self, parent)
        self._notifBar = notificationbar
        self._dbmodel = None
        self._attrName = None
        self._timer = QTimer(self)
        self._timer.setInterval(800)
        self._timer.setSingleShot(True)
        self._invalidMsg = ""
        self._filterOperator = "="
        self._defaultStyleSheet = self.styleSheet()
        self._isValid = True
        self._currInvalidMsg = ""

        # Connect signals
        self._timer.timeout.connect(self.validateInput)
        self.textChanged.connect(self.onTextChanged)

    def validateInput(self):
        '''
        Validate user input.
        '''
        if self._dbmodel:
            if callable(self._dbmodel):
                modelObj = self._dbmodel()

            # Then it is a class instance
            else:
                modelObj = self._dbmodel
                self._dbmodel = self._dbmodel.__class__

            objQueryProperty = getattr(self._dbmodel, self._attrName)
            modelRecord = modelObj.queryObject().filter(
                func.lower(objQueryProperty) == func.lower(
                    self.text())).first()

            if modelRecord != None:
                self.setStyleSheet(INVALIDATESTYLESHEET)
                self._currInvalidMsg = self._invalidMsg.format("'" +
                                                               self.text() +
                                                               "'")
                self._isValid = False

                if self._notifBar:
                    self._notifBar.insertErrorNotification(
                        self._currInvalidMsg)

    def setInvalidMessage(self, message):
        '''
        The message to be displayed when the user input is invalid.
        '''
        self._invalidMsg = message

    def invalidMessage(self):
        '''
        Returns the invalidation message for the control.
        '''
        return self._invalidMsg

    def setDatabaseModel(self, dbmodel):
        '''
        Set database model which should be callable.
        '''
        self._dbmodel = dbmodel

    def setAttributeName(self, attrname):
        '''
        Attribute name of the database model for validating against.
        '''
        self._attrName = attrname

    def setModelAttr(self, model, attributeName):
        '''
        Set a callable model class and attribute name.
        '''
        self._dbmodel = model
        self._attrName = attributeName

    def setNotificationBar(self, notifBar):
        '''
        Sets the notification bar.
        '''
        self._notifBar = notifBar

    def setQueryOperator(self, queryOp):
        '''
        Specify a string-based value for the filter operator that validates
        the user input.
        '''
        self._filterOperator = queryOp

    def queryOperator(self):
        '''
        Return the current query operator. Default is '=' operator.
        '''
        return self._filterOperator

    def validate(self):
        '''
        Convenience method that can be used to validate the current state of the control.
        '''
        if not self._isValid:
            if self._notifBar:
                self._notifBar.insertErrorNotification(self._currInvalidMsg)
            return False

        else:
            return True

    def onTextChanged(self, userText):
        '''
        Slot raised whenever the text changes in the control.
        '''
        self.setStyleSheet(self._defaultStyleSheet)
        self._isValid = True

        if self._notifBar != None:
            self._notifBar.clear()

        self._timer.start()
예제 #23
0
class ExportDialog(QDialog, DIALOG_UI):
    ValidExtensions = ['xtf', 'XTF', 'itf', 'ITF', 'gml', 'GML', 'xml', 'XML']

    def __init__(self, base_config, parent=None):
        QDialog.__init__(self, parent)
        self.setupUi(self)
        self.db_simple_factory = DbSimpleFactory()
        QgsGui.instance().enableAutoGeometryRestore(self)
        self.buttonBox.accepted.disconnect()
        self.buttonBox.clicked.connect(self.button_box_clicked)
        self.buttonBox.clear()
        self.buttonBox.addButton(QDialogButtonBox.Cancel)

        self.export_button_name = self.tr('Export')
        self.export_without_validate_button_name = self.tr(
            'Export without validation')

        self.buttonBox.addButton(self.export_button_name,
                                 QDialogButtonBox.AcceptRole)
        self.buttonBox.addButton(QDialogButtonBox.Help)
        self.buttonBox.helpRequested.connect(self.help_requested)
        self.xtf_file_browse_button.clicked.connect(
            make_save_file_selector(
                self.xtf_file_line_edit,
                title=self.tr('Save in XTF Transfer File'),
                file_filter=self.
                tr('XTF Transfer File (*.xtf *XTF);;Interlis 1 Transfer File (*.itf *ITF);;XML (*.xml *XML);;GML (*.gml *GML)'
                   ),
                extensions=['.' + ext for ext in self.ValidExtensions]))
        self.xtf_file_browse_button.clicked.connect(
            self.xtf_browser_opened_to_true)
        self.xtf_browser_was_opened = False

        self.type_combo_box.clear()
        self._lst_panel = dict()

        for db_id in self.db_simple_factory.get_db_list(False):
            self.type_combo_box.addItem(displayDbIliMode[db_id], db_id)
            db_factory = self.db_simple_factory.create_factory(db_id)
            item_panel = db_factory.get_config_panel(self, DbActionType.EXPORT)
            self._lst_panel[db_id] = item_panel
            self.db_layout.addWidget(item_panel)

        self.validators = Validators()

        fileValidator = FileValidator(
            pattern=['*.' + ext for ext in self.ValidExtensions],
            allow_non_existing=True)

        self.xtf_file_line_edit.setValidator(fileValidator)
        self.xtf_file_line_edit.textChanged.connect(
            self.validators.validate_line_edits)
        self.xtf_file_line_edit.textChanged.connect(
            self.xtf_browser_opened_to_false)
        self.xtf_file_line_edit.textChanged.emit(
            self.xtf_file_line_edit.text())

        # Remove export without validate button when xtf change
        self.xtf_file_line_edit.textChanged.connect(
            self.remove_export_without_validate_button)

        #refresh the models on changing values but avoid massive db connects by timer
        self.refreshTimer = QTimer()
        self.refreshTimer.setSingleShot(True)
        self.refreshTimer.timeout.connect(self.refresh_models)

        for key, value in self._lst_panel.items():
            value.notify_fields_modified.connect(
                self.request_for_refresh_models)

        self.validate_data = True  # validates exported data by default, We use --disableValidation when is False
        self.base_configuration = base_config
        self.restore_configuration()

        self.export_models_model = ExportModels()
        self.refresh_models()

        self.type_combo_box.currentIndexChanged.connect(self.type_changed)

    def request_for_refresh_models(self):
        # hold refresh back
        self.refreshTimer.start(500)

    def refresh_models(self):
        self.refreshed_export_models_model()
        self.export_models_view.setModel(self.export_models_model)
        self.export_models_view.clicked.connect(self.export_models_model.check)
        self.export_models_view.space_pressed.connect(
            self.export_models_model.check)

    def refreshed_export_models_model(self):
        tool = self.type_combo_box.currentData() & ~DbIliMode.ili

        configuration = self.updated_configuration()
        schema = configuration.dbschema

        db_factory = self.db_simple_factory.create_factory(tool)
        config_manager = db_factory.get_db_command_config_manager(
            configuration)
        uri_string = config_manager.get_uri()

        db_connector = None

        try:
            db_connector = db_factory.get_db_connector(uri_string, schema)
        except DBConnectorError:
            # when wrong connection parameters entered, there should just be returned an empty model - so let it pass
            pass

        self.export_models_model.refresh_models(db_connector)

    def button_box_clicked(self, button):
        if self.buttonBox.buttonRole(button) == QDialogButtonBox.AcceptRole:
            if button.text() == self.export_button_name:
                self.validate_data = True
            elif button.text() == self.export_without_validate_button_name:
                self.validate_data = False
            self.accepted()

    def accepted(self):
        configuration = self.updated_configuration()

        if not self.xtf_file_line_edit.validator().validate(
                configuration.xtffile, 0)[0] == QValidator.Acceptable:
            self.txtStdout.setText(
                self.
                tr('Please set a valid INTERLIS XTF file before exporting data.'
                   ))
            self.xtf_file_line_edit.setFocus()
            return
        if not configuration.iliexportmodels:
            self.txtStdout.setText(
                self.tr('Please set a model before exporting data.'))
            self.export_models_view.setFocus()
            return

        db_id = self.type_combo_box.currentData()
        res, message = self._lst_panel[db_id].is_valid()

        if not res:
            self.txtStdout.setText(message)
            return

        # If xtf browser was opened and the file exists, the user already chose
        # to overwrite the file
        if os.path.isfile(self.xtf_file_line_edit.text().strip()
                          ) and not self.xtf_browser_was_opened:
            self.msg = QMessageBox()
            self.msg.setIcon(QMessageBox.Warning)
            self.msg.setText(
                self.tr(
                    "{filename} already exists.\nDo you want to replace it?").
                format(filename=os.path.basename(
                    self.xtf_file_line_edit.text().strip())))
            self.msg.setWindowTitle(self.tr("Save in XTF Transfer File"))
            self.msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
            msg_box = self.msg.exec_()
            if msg_box == QMessageBox.No:
                return

        with OverrideCursor(Qt.WaitCursor):
            self.progress_bar.show()
            self.progress_bar.setValue(0)

            self.disable()
            self.txtStdout.setTextColor(QColor('#000000'))
            self.txtStdout.clear()

            exporter = iliexporter.Exporter()
            exporter.tool = self.type_combo_box.currentData()
            exporter.configuration = configuration

            self.save_configuration(configuration)

            exporter.stdout.connect(self.print_info)
            exporter.stderr.connect(self.on_stderr)
            exporter.process_started.connect(self.on_process_started)
            exporter.process_finished.connect(self.on_process_finished)

            self.progress_bar.setValue(25)

            try:
                if exporter.run() != iliexporter.Exporter.SUCCESS:
                    self.enable()
                    self.progress_bar.hide()
                    return
            except JavaNotFoundError as e:
                self.txtStdout.setTextColor(QColor('#000000'))
                self.txtStdout.clear()
                self.txtStdout.setText(e.error_string)
                self.enable()
                self.progress_bar.hide()
                return

            self.buttonBox.clear()
            self.buttonBox.setEnabled(True)
            self.buttonBox.addButton(QDialogButtonBox.Close)
            self.progress_bar.setValue(100)

    def print_info(self, text):
        self.txtStdout.setTextColor(QColor('#000000'))
        self.txtStdout.append(text)
        QCoreApplication.processEvents()

    def on_stderr(self, text):
        color_log_text(text, self.txtStdout)
        self.advance_progress_bar_by_text(text)
        QCoreApplication.processEvents()

    def on_process_started(self, command):
        self.disable()
        self.txtStdout.setTextColor(QColor('#000000'))
        self.txtStdout.clear()
        self.txtStdout.setText(command)
        QCoreApplication.processEvents()

    def on_process_finished(self, exit_code, result):
        color = '#004905' if exit_code == 0 else '#aa2222'
        self.txtStdout.setTextColor(QColor(color))
        self.txtStdout.append(self.tr('Finished ({})'.format(exit_code)))
        if result == iliexporter.Exporter.SUCCESS:
            self.buttonBox.clear()
            self.buttonBox.setEnabled(True)
            self.buttonBox.addButton(QDialogButtonBox.Close)
        else:
            if self.export_without_validate():

                # button is removed to define order in GUI
                for button in self.buttonBox.buttons():
                    if button.text() == self.export_button_name:
                        self.buttonBox.removeButton(button)
                # Check if button was previously added
                self.remove_export_without_validate_button()

                self.buttonBox.addButton(
                    self.export_without_validate_button_name,
                    QDialogButtonBox.AcceptRole).setStyleSheet(
                        "color: #aa2222;")
                self.buttonBox.addButton(self.export_button_name,
                                         QDialogButtonBox.AcceptRole)
            self.enable()

    def export_without_validate(self):
        """
        Valid if an error occurred that prevents executing the export without validations
        :return: True if you can execute the export without validations, False in other case
        """
        log = self.txtStdout.toPlainText().lower()
        if "permission denied" in log or "access is denied" in log:
            return False
        return True

    def remove_export_without_validate_button(self):
        for button in self.buttonBox.buttons():
            if button.text() == self.export_without_validate_button_name:
                self.buttonBox.removeButton(button)
                self.validate_data = True

    def updated_configuration(self):
        """
        Get the configuration that is updated with the user configuration changes on the dialog.
        :return: Configuration
        """
        configuration = ili2dbconfig.ExportConfiguration()

        mode = self.type_combo_box.currentData()
        self._lst_panel[mode].get_fields(configuration)

        configuration.tool = mode
        configuration.xtffile = self.xtf_file_line_edit.text().strip()
        configuration.iliexportmodels = ';'.join(
            self.export_models_model.checked_models())
        configuration.ilimodels = ';'.join(
            self.export_models_model.stringList())
        configuration.base_configuration = self.base_configuration

        if not self.validate_data:
            configuration.disable_validation = True
        return configuration

    def save_configuration(self, configuration):
        settings = QSettings()
        settings.setValue('QgisModelBaker/ili2pg/xtffile_export',
                          configuration.xtffile)
        settings.setValue('QgisModelBaker/importtype',
                          self.type_combo_box.currentData().name)

        mode = self.type_combo_box.currentData()
        db_factory = self.db_simple_factory.create_factory(mode)
        config_manager = db_factory.get_db_command_config_manager(
            configuration)
        config_manager.save_config_in_qsettings()

    def restore_configuration(self):
        settings = QSettings()

        for db_id in self.db_simple_factory.get_db_list(False):
            configuration = iliexporter.ExportConfiguration()
            db_factory = self.db_simple_factory.create_factory(db_id)
            config_manager = db_factory.get_db_command_config_manager(
                configuration)
            config_manager.load_config_from_qsettings()
            self._lst_panel[db_id].set_fields(configuration)

        mode = settings.value('QgisModelBaker/importtype')
        mode = DbIliMode[
            mode] if mode else self.db_simple_factory.default_database
        mode = mode & ~DbIliMode.ili

        self.type_combo_box.setCurrentIndex(self.type_combo_box.findData(mode))
        self.refresh_db_panel()

    def disable(self):
        self.type_combo_box.setEnabled(False)
        for key, value in self._lst_panel.items():
            value.setEnabled(False)
        self.ili_config.setEnabled(False)
        self.buttonBox.setEnabled(False)

    def enable(self):
        self.type_combo_box.setEnabled(True)
        for key, value in self._lst_panel.items():
            value.setEnabled(True)
        self.ili_config.setEnabled(True)
        self.buttonBox.setEnabled(True)

    def type_changed(self):
        self.txtStdout.clear()
        self.remove_export_without_validate_button()
        self.refresh_db_panel()
        self.refresh_models()
        self.txtStdout.clear()

    def refresh_db_panel(self):
        self.progress_bar.hide()

        db_id = self.type_combo_box.currentData()
        self.db_wrapper_group_box.setTitle(displayDbIliMode[db_id])

        # Refresh panels
        for key, value in self._lst_panel.items():
            value.interlis_mode = False
            is_current_panel_selected = db_id == key
            value.setVisible(is_current_panel_selected)
            if is_current_panel_selected:
                value._show_panel()

    def link_activated(self, link):
        if link.url() == '#configure':
            cfg = OptionsDialog(self.base_configuration)
            if cfg.exec_():
                settings = QSettings()
                settings.beginGroup('QgisModelBaker/ili2db')
                self.base_configuration.save(settings)
        else:
            QDesktopServices.openUrl(link)

    def help_requested(self):
        os_language = QLocale(
            QSettings().value('locale/userLocale')).name()[:2]
        if os_language in ['es', 'de']:
            webbrowser.open(
                "https://opengisch.github.io/QgisModelBaker/docs/{}/user-guide.html#export-an-interlis-transfer-file-xtf"
                .format(os_language))
        else:
            webbrowser.open(
                "https://opengisch.github.io/QgisModelBaker/docs/user-guide.html#export-an-interlis-transfer-file-xtf"
            )

    def xtf_browser_opened_to_true(self):
        """
        Slot. Sets a flag to true to eventually avoid asking a user whether to overwrite a file.
        """
        self.xtf_browser_was_opened = True

    def xtf_browser_opened_to_false(self):
        """
        Slot. Sets a flag to false to eventually ask a user whether to overwrite a file.
        """
        self.xtf_browser_was_opened = False

    def advance_progress_bar_by_text(self, text):
        if text.strip() == 'Info: compile models…':
            self.progress_bar.setValue(50)
        elif text.strip() == 'Info: create table structure…':
            self.progress_bar.setValue(75)
예제 #24
0
class ColorsDock(QgsDockWidget):
    """
    Custom dock widget for modfication of project colors
    """
    def __init__(self, iface):
        super().__init__()
        self.iface = iface

        stack = QgsPanelWidgetStack()

        self.main_panel = QgsPanelWidget()
        self.main_panel.setDockMode(True)
        layout = QVBoxLayout()
        layout.setContentsMargins(0, 0, 0, 0)
        layout.setMargin(0)  # pylint: disable=no-value-for-parameter
        self.main_panel.setLayout(layout)
        self.setWindowTitle(self.tr('Project Colors'))
        self.setObjectName('project_colors_dock')

        scheme = [
            s for s in QgsApplication.colorSchemeRegistry().schemes()
            if isinstance(s, QgsProjectColorScheme)
        ]
        self.color_list = QgsColorSchemeList(None, scheme[0])
        layout.addWidget(self.color_list)

        # defer updates for a short timeout -- prevents flooding with signals
        # when doing lots of color changes, improving app responsiveness
        self.timer = QTimer(self)
        self.timer.setSingleShot(True)
        self.timer.setInterval(100)

        self.block_updates = False

        def apply_colors():
            """
            Applies color changes to canvas and project
            """
            self.block_updates = True
            self.color_list.saveColorsToScheme()
            self.block_updates = False
            self.iface.mapCanvas().refreshAllLayers()

        self.timer.timeout.connect(apply_colors)

        def colors_changed():
            """
            Triggers a deferred update of the project colors
            """
            if self.timer.isActive():
                return
            self.timer.start()

        self.color_list.model().dataChanged.connect(colors_changed)
        stack.setMainPanel(self.main_panel)
        self.setWidget(stack)

        QgsProject.instance().projectColorsChanged.connect(
            self.repopulate_list)

    def repopulate_list(self):
        """
        Rebuild the colors list when project colors are changed
        """
        if self.block_updates:
            return
        scheme = [
            s for s in QgsApplication.colorSchemeRegistry().schemes()
            if isinstance(s, QgsProjectColorScheme)
        ][0]
        self.color_list.setScheme(scheme)
예제 #25
0
class RelationEditorFeatureSideWidget(QgsAbstractRelationEditorWidget,
                                      WidgetUi):
    class LastView(str, Enum):
        ListView = "ListView"
        IconView = "IconView"

    settingsLastView = QgsSettingsEntryString(
        'relationEditorFeatureSideLastView', PluginHelper.PLUGIN_SLUG,
        LastView.ListView,
        PluginHelper.tr(
            'Last view used in the relation editor document side widget'))

    signalCurrentViewChanged = pyqtSignal()

    def __init__(self, config, parent):
        super().__init__(config, parent)
        self._updateUiTimer = QTimer()
        self._updateUiTimer.setSingleShot(True)
        self._updateUiTimer.timeout.connect(self.updateUiTimeout)
        self.setupUi(self)

        print('DocumentRelationEditorFeatureSideWidget.__init__')

        self.documents_path = str()
        self.document_filename = str()

        self.model = DocumentModel()

        self._nmRelation = QgsRelation()
        self._layerInSameTransactionGroup = False

        self._currentDocumentId = None

        # Actions
        self.actionToggleEditing = QAction(
            QIcon(":/images/themes/default/mActionToggleEditing.svg"),
            self.tr("Toggle editing mode for child layers"))
        self.actionToggleEditing.setCheckable(True)
        self.actionSaveEdits = QAction(
            QIcon(":/images/themes/default/mActionSaveEdits.svg"),
            self.tr("Save child layer edits"))
        self.actionShowForm = QAction(
            QIcon(":/images/themes/default/mActionMultiEdit.svg"),
            self.tr("Show form"))
        self.actionAddFeature = QAction(
            QIcon(":/images/themes/default/symbologyAdd.svg"),
            self.tr("Add document"))
        self.actionDeleteFeature = QAction(
            QIcon(":/images/themes/default/mActionDeleteSelected.svg"),
            self.tr("Drop document"))
        self.actionLinkFeature = QAction(
            QIcon(":/images/themes/default/mActionLink.svg"),
            self.tr("Link document"))
        self.actionUnlinkFeature = QAction(
            QIcon(":/images/themes/default/mActionUnlink.svg"),
            self.tr("Unlink document"))

        # Tool buttons
        self.mToggleEditingToolButton.setDefaultAction(
            self.actionToggleEditing)
        self.mSaveEditsToolButton.setDefaultAction(self.actionSaveEdits)
        self.mShowFormToolButton.setDefaultAction(self.actionShowForm)
        self.mAddFeatureToolButton.setDefaultAction(self.actionAddFeature)
        self.mDeleteFeatureToolButton.setDefaultAction(
            self.actionDeleteFeature)
        self.mLinkFeatureToolButton.setDefaultAction(self.actionLinkFeature)
        self.mUnlinkFeatureToolButton.setDefaultAction(
            self.actionUnlinkFeature)

        self.mListViewToolButton.setIcon(
            QIcon(":/images/themes/default/mIconListView.svg"))
        self.mIconViewToolButton.setIcon(
            QIcon(":/images/themes/default/mActionIconView.svg"))
        self.mListViewToolButton.setChecked(self.currentView == str(
            RelationEditorFeatureSideWidget.LastView.ListView))
        self.mIconViewToolButton.setChecked(self.currentView == str(
            RelationEditorFeatureSideWidget.LastView.IconView))

        # Quick image providers
        self._previewImageProvider = PreviewImageProvider()
        self._fileTypeSmallIconProvider = FileTypeIconImageProvider(32)
        self._fileTypeBigIconProvider = FileTypeIconImageProvider(100)

        # Setup QML part
        self.view = QQuickWidget()
        self.view.rootContext().setContextProperty("documentModel", self.model)
        self.view.rootContext().setContextProperty("parentWidget", self)
        self.view.rootContext().setContextProperty(
            "CONST_LIST_VIEW",
            str(RelationEditorFeatureSideWidget.LastView.ListView))
        self.view.rootContext().setContextProperty(
            "CONST_ICON_VIEW",
            str(RelationEditorFeatureSideWidget.LastView.IconView))
        self.view.engine().addImageProvider("previewImageProvider",
                                            self._previewImageProvider)
        self.view.engine().addImageProvider("fileTypeSmallIconProvider",
                                            self._fileTypeSmallIconProvider)
        self.view.engine().addImageProvider("fileTypeBigIconProvider",
                                            self._fileTypeBigIconProvider)
        self.view.setSource(
            QUrl.fromLocalFile(
                os.path.join(os.path.dirname(__file__),
                             '../qml/DocumentList.qml')))
        self.view.setResizeMode(QQuickWidget.SizeRootObjectToView)
        self.layout().addWidget(self.view)

        # Set initial state for add / remove etc.buttons
        self.updateButtons()

        # Signal slots
        self.actionToggleEditing.triggered.connect(self.toggleEditing)
        self.actionSaveEdits.triggered.connect(self.saveChildLayerEdits)
        self.actionShowForm.triggered.connect(self.showDocumentForm)
        self.actionAddFeature.triggered.connect(self.addDocument)
        self.actionDeleteFeature.triggered.connect(self.dropDocument)
        self.actionLinkFeature.triggered.connect(self.linkDocument)
        self.actionUnlinkFeature.triggered.connect(self.unlinkDocument)
        self.mListViewToolButton.toggled.connect(
            self.listViewToolButtonToggled)
        self.mIconViewToolButton.toggled.connect(
            self.iconViewToolButtonToggled)

    @pyqtProperty(str)
    def currentView(self):
        return self.settingsLastView.value()

    def updateCurrentView(self):
        if self.mListViewToolButton.isChecked():
            self.settingsLastView.setValue(
                str(RelationEditorFeatureSideWidget.LastView.ListView))
        else:
            self.settingsLastView.setValue(
                str(RelationEditorFeatureSideWidget.LastView.IconView))

        self.signalCurrentViewChanged.emit()

    @pyqtProperty(QVariant)
    def currentDocumentId(self):
        return self._currentDocumentId

    @pyqtSlot(QVariant)
    def setCurrentDocumentId(self, value):
        self._currentDocumentId = value
        self.updateButtons()

    def nmRelation(self):
        return self._nmRelation

    def config(self):
        return {}

    def setConfig(self, config):
        self.documents_path = config['documents_path']
        self.document_filename = config['document_filename']

    def updateUi(self):
        self._updateUiTimer.start(200)

    def updateUiTimeout(self):
        self.model.init(self.relation(), self.nmRelation(), self.feature(),
                        self.documents_path, self.document_filename)

    def updateButtons(self):
        toggleEditingButtonEnabled = False
        editable = False
        linkable = False
        spatial = False
        selectionNotEmpty = self._currentDocumentId is not None

        if self.relation().isValid():
            toggleEditingButtonEnabled = self.relation().referencingLayer(
            ).supportsEditing()
            editable = self.relation().referencingLayer().isEditable()
            linkable = self.relation().referencingLayer().isEditable()
            spatial = self.relation().referencingLayer().isSpatial()

        if self.nmRelation().isValid():
            toggleEditingButtonEnabled |= self.nmRelation().referencedLayer(
            ).supportsEditing()
            editable = self.nmRelation().referencedLayer().isEditable()
            spatial = self.nmRelation().referencedLayer().isSpatial()

        self.mToggleEditingToolButton.setEnabled(toggleEditingButtonEnabled)
        self.mAddFeatureToolButton.setEnabled(editable)
        self.mLinkFeatureToolButton.setEnabled(linkable)
        self.mDeleteFeatureToolButton.setEnabled(editable
                                                 and selectionNotEmpty)
        self.mUnlinkFeatureToolButton.setEnabled(linkable
                                                 and selectionNotEmpty)
        self.mToggleEditingToolButton.setChecked(editable)
        self.mSaveEditsToolButton.setEnabled(editable or linkable)

        self.mShowFormToolButton.setEnabled(selectionNotEmpty)

        self.mToggleEditingToolButton.setVisible(
            self._layerInSameTransactionGroup is False)
        self.mSaveEditsToolButton.setVisible(
            self._layerInSameTransactionGroup is False)

    def afterSetRelations(self):
        self._nmRelation = QgsProject.instance().relationManager().relation(
            str(self.nmRelationId()))

        self._checkTransactionGroup()

        if self.relation().isValid():
            self.relation().referencingLayer().editingStopped.connect(
                self.updateButtons)
            self.relation().referencingLayer().editingStarted.connect(
                self.updateButtons)

        if self.nmRelation().isValid():
            self.nmRelation().referencedLayer().editingStarted.connect(
                self.updateButtons)
            self.nmRelation().referencedLayer().editingStopped.connect(
                self.updateButtons)

        self.updateButtons()

    def _checkTransactionGroup(self):

        self._layerInSameTransactionGroup = False
        connectionString = PluginHelper.connectionString(
            self.relation().referencedLayer().source())
        transactionGroup = QgsProject.instance().transactionGroup(
            self.relation().referencedLayer().providerType(), connectionString)

        if transactionGroup is None:
            return

        if self.nmRelation().isValid():
            if (self.relation().referencedLayer() in transactionGroup.layers()
                    and self.relation().referencingLayer()
                    in transactionGroup.layers()
                    and self.nmRelation().referencedLayer()
                    in transactionGroup.layers()):
                self._layerInSameTransactionGroup = True
        else:
            if (self.relation().referencedLayer() in transactionGroup.layers()
                    and self.relation().referencingLayer()
                    in transactionGroup.layers()):
                self._layerInSameTransactionGroup = True

    def parentFormValueChanged(self, attribute, newValue):
        pass

    def checkLayerEditingMode(self):

        if self.relation().referencingLayer().isEditable() is False:
            QMessageBox.critical(
                self, self.tr("Layer not editable"),
                self.tr("Layer '{0}' is not in editing mode.").format(
                    self.relation().referencingLayer().name()))
            return False

        if self.nmRelation().isValid():
            if self.nmRelation().referencedLayer().isEditable() is False:
                QMessageBox.critical(
                    self, self.tr("Layer not editable"),
                    self.tr("Layer '{0}' is not in editing mode.").format(
                        self.nmRelation().referencedLayer().name()))
                return False

        return True

    @pyqtSlot(bool)
    def toggleEditing(self, state):

        super().toggleEditing(state)
        self.updateButtons()

    @pyqtSlot()
    def saveChildLayerEdits(self):

        super().saveEdits()

    @pyqtSlot()
    def addDocument(self):

        # Workaround because of QGIS not resetting this property after linking features
        self.editorContext().vectorLayerTools().setForceSuppressFormPopup(
            False)
        self.addFeature()

    @pyqtSlot()
    def dropDocument(self):

        if self._currentDocumentId is None:
            return

        self.deleteFeature(self._currentDocumentId)

    @pyqtSlot(str)
    def addDroppedDocument(self, fileUrl):

        if self.checkLayerEditingMode() is False:
            return

        # Workaround because of QGIS not resetting this property after linking features
        self.editorContext().vectorLayerTools().setForceSuppressFormPopup(
            False)

        layer = self.relation().referencingLayer()
        if self.nmRelation().isValid():
            layer = self.nmRelation().referencedLayer()

        default_documents_path = str()
        if self.documents_path:
            exp = QgsExpression(self.documents_path)
            context = QgsExpressionContext()
            context.appendScopes(
                QgsExpressionContextUtils.globalProjectLayerScopes(layer))
            default_documents_path = str(exp.evaluate(context))

        filename = QUrl(fileUrl).toLocalFile()
        if default_documents_path:
            filename = QDir(default_documents_path).relativeFilePath(filename)

        keyAttrs = dict()

        # Fields of the linking table
        fields = self.relation().referencingLayer().fields()

        # For generated relations insert the referenced layer field
        if self.relation().type() == QgsRelation.Generated:
            polyRel = self.relation().polymorphicRelation()
            keyAttrs[fields.indexFromName(
                polyRel.referencedLayerField())] = polyRel.layerRepresentation(
                    self.relation().referencedLayer())

        if self.nmRelation().isValid():
            # only normal relations support m:n relation
            if self.nmRelation().type() != QgsRelation.Normal:
                QMessageBox.critical(
                    self, self.tr("Add document"),
                    self.
                    tr("Invalid relation, Only normal relations support m:n relation."
                       ))
                return

            # Pre fill inserting document filepath
            attributes = {
                self.nmRelation().referencedLayer().fields().indexFromName(self.document_filename):
                filename
            }

            # n:m Relation: first let the user create a new feature on the other table
            # and autocreate a new linking feature.
            ok, feature = self.editorContext().vectorLayerTools().addFeature(
                self.nmRelation().referencedLayer(), attributes, QgsGeometry())
            if not ok:
                QMessageBox.critical(
                    self, self.tr("Add document"),
                    self.tr("Could not add a new linking feature."))
                return

            for key in self.relation().fieldPairs():
                keyAttrs[fields.indexOf(key)] = self.feature().attribute(
                    self.relation().fieldPairs()[key])

            for key in self.nmRelation().fieldPairs():
                keyAttrs[fields.indexOf(key)] = feature.attribute(
                    self.nmRelation().fieldPairs()[key])

            linkFeature = QgsVectorLayerUtils.createFeature(
                self.relation().referencingLayer(), QgsGeometry(), keyAttrs,
                self.relation().referencingLayer().createExpressionContext())

            if not self.relation().referencingLayer().addFeature(linkFeature):
                QMessageBox.critical(self, self.tr("Add document"),
                                     self.tr("Could not add a new feature."))
                return

        else:
            for key in self.relation().fieldPairs():
                keyAttrs[fields.indexFromName(key)] = self.feature().attribute(
                    self.relation().fieldPairs()[key])

            # Pre fill inserting document filepath
            keyAttrs[fields] = filename

            ok, feature = self.editorContext().vectorLayerTools().addFeature(
                self.relation().referencingLayer(), keyAttrs, QgsGeometry())
            if not ok:
                QMessageBox.critical(self, self.tr("Add document"),
                                     self.tr("Could not add a new feature."))
                return

        self.updateUi()

    @pyqtSlot()
    def linkDocument(self):

        self.linkFeature()

    @pyqtSlot()
    def unlinkDocument(self):

        if self._currentDocumentId is None:
            return

        self.unlinkFeature(self._currentDocumentId)

    @pyqtSlot()
    def showDocumentForm(self):

        if self._currentDocumentId is None:
            return

        layer = self.relation().referencingLayer()
        if self.nmRelation().isValid():
            layer = self.nmRelation().referencedLayer()

        showDocumentFormDialog = QgsAttributeDialog(
            layer, layer.getFeature(self._currentDocumentId), False, self,
            True)
        showDocumentFormDialog.exec()
        self.updateUi()

    @pyqtSlot(bool)
    def listViewToolButtonToggled(self, checked):
        self.mIconViewToolButton.blockSignals(True)
        self.mIconViewToolButton.setChecked(checked is False)
        self.mIconViewToolButton.blockSignals(False)
        self.updateCurrentView()

    @pyqtSlot(bool)
    def iconViewToolButtonToggled(self, checked):
        self.mListViewToolButton.blockSignals(True)
        self.mListViewToolButton.setChecked(checked is False)
        self.mListViewToolButton.blockSignals(False)
        self.updateCurrentView()
class Downloader(QObject):

    NOT_FOUND = 0
    NO_ERROR = 0
    TIMEOUT_ERROR = 4
    UNKNOWN_ERROR = -1

    replyFinished = pyqtSignal(str, int, int)

    def __init__(self, parent=None):
        QObject.__init__(self, parent)
        self.queue = []
        self.redirected_urls = {}
        self.requestingUrls = []
        self.replies = []

        self.eventLoop = QEventLoop()
        self.sync = False
        self.fetchedFiles = {}
        self.clearCounts()

        self.timer = QTimer()
        self.timer.setSingleShot(True)
        self.timer.timeout.connect(self.fetchTimedOut)

        # network settings
        self.userAgent = "QuickMapServices tile layer (+https://github.com/nextgis/quickmapservices)"
        self.max_connection = 4
        self.default_cache_expiration = 24
        self.errorStatus = Downloader.NO_ERROR

    def clearCounts(self):
        self.fetchSuccesses = 0
        self.fetchErrors = 0
        self.cacheHits = 0

    def fetchTimedOut(self):
        self.log("Downloader.timeOut()")
        self.abort()
        self.errorStatus = Downloader.TIMEOUT_ERROR

    def abort(self):
        # clear queue and abort sent requests
        self.queue = []
        self.timer.stop()
        for reply in self.replies:
            reply.abort()
        self.errorStatus = Downloader.UNKNOWN_ERROR

    def replyFinishedSlot(self):
        reply = self.sender()
        url = reply.request().url().toString()
        self.log("replyFinishedSlot: %s" % url)
        if not url in self.fetchedFiles:
            self.fetchedFiles[url] = None
        self.requestingUrls.remove(url)
        self.replies.remove(reply)
        isFromCache = 0
        httpStatusCode = reply.attribute(QNetworkRequest.HttpStatusCodeAttribute)
        if reply.error() == QNetworkReply.NoError:
            if httpStatusCode == 301:
                new_url = str(reply.rawHeader("Location"))
                self.addToQueue(new_url, url)
            else:
                self.fetchSuccesses += 1
                if reply.attribute(QNetworkRequest.SourceIsFromCacheAttribute):
                    self.cacheHits += 1
                    isFromCache = 1
                elif not reply.hasRawHeader("Cache-Control"):
                    cache = QgsNetworkAccessManager.instance().cache()
                    if cache:
                        metadata = cache.metaData(reply.request().url())
                        # self.log("Expiration date: " + metadata.expirationDate().toString().encode("utf-8"))
                        if metadata.expirationDate().isNull():
                            metadata.setExpirationDate(
                                QDateTime.currentDateTime().addSecs(self.default_cache_expiration * 60 * 60))
                            cache.updateMetaData(metadata)
                            self.log(
                                "Default expiration date has been set: %s (%d h)" % (url, self.default_cache_expiration))

                if reply.isReadable():
                    data = reply.readAll()
                    if self.redirected_urls.has_key(url):
                        url = self.redirected_urls[url]

                    self.fetchedFiles[url] = data
                else:
                    qDebug("http status code: " + str(httpStatusCode))

                # self.emit(SIGNAL('replyFinished(QString, int, int)'), url, reply.error(), isFromCache)
                self.replyFinished.emit(url, reply.error(), isFromCache)
        else:
            if self.sync and httpStatusCode == 404:
                self.fetchedFiles[url] = self.NOT_FOUND
            self.fetchErrors += 1
            if self.errorStatus == self.NO_ERROR:
                self.errorStatus = self.UNKNOWN_ERROR

        reply.deleteLater()

        if debug_mode:
            qDebug("queue: %d, requesting: %d" % (len(self.queue), len(self.requestingUrls)))

        if len(self.queue) + len(self.requestingUrls) == 0:
            # all replies have been received
            if self.sync:
                self.logT("eventLoop.quit()")
                self.eventLoop.quit()
            else:
                self.timer.stop()
        elif len(self.queue) > 0:
            # start fetching the next file
            self.fetchNext()
        self.log("replyFinishedSlot End: %s" % url)

    def fetchNext(self):
        if len(self.queue) == 0:
            return
        url = self.queue.pop(0)
        self.log("fetchNext: %s" % url)

        request = QNetworkRequest(QUrl(url))
        request.setRawHeader("User-Agent", self.userAgent)
        reply = QgsNetworkAccessManager.instance().get(request)
        reply.finished.connect(self.replyFinishedSlot)
        self.requestingUrls.append(url)
        self.replies.append(reply)
        return reply

    def fetchFiles(self, urlList, timeout_ms=0):
        self.log("fetchFiles()")
        self.sync = True
        self.queue = []
        self.redirected_urls = {} 
        self.clearCounts()
        self.errorStatus = Downloader.NO_ERROR
        self.fetchedFiles = {}

        if len(urlList) == 0:
            return self.fetchedFiles

        for url in urlList:
            self.addToQueue(url)

        for i in range(self.max_connection):
            self.fetchNext()

        if timeout_ms > 0:
            self.timer.setInterval(timeout_ms)
            self.timer.start()

        self.logT("eventLoop.exec_(): " + str(self.eventLoop))
        self.eventLoop.exec_()
        self.log("fetchFiles() End: %d" % self.errorStatus)
        if timeout_ms > 0:
            self.timer.stop()
        return self.fetchedFiles

    def addToQueue(self, url, redirected_from=None):
        if url in self.queue:
            return False
        self.queue.append(url)
        if redirected_from is not None:
            self.redirected_urls[url] = redirected_from
        return True

    def queueCount(self):
        return len(self.queue)

    def finishedCount(self):
        return len(self.fetchedFiles)

    def unfinishedCount(self):
        return len(self.queue) + len(self.requestingUrls)

    def log(self, msg):
        if debug_mode:
            qDebug(msg)

    def logT(self, msg):
        if debug_mode:
            qDebug("%s: %s" % (str(threading.current_thread()), msg))

    def fetchFilesAsync(self, urlList, timeout_ms=0):
        self.log("fetchFilesAsync()")
        self.sync = False
        self.queue = []
        self.clearCounts()
        self.errorStatus = Downloader.NO_ERROR
        self.fetchedFiles = {}

        if len(urlList) == 0:
            return self.fetchedFiles

        for url in urlList:
            self.addToQueue(url)

        for i in range(self.max_connection):
            self.fetchNext()

        if timeout_ms > 0:
            self.timer.setInterval(timeout_ms)
            self.timer.start()
class TimeManagerGuiControl(QObject):
    """This class controls all plugin-related GUI elements. Emitted signals are defined here."""

    showOptions = pyqtSignal()
    signalExportVideo = pyqtSignal(str, int, bool, bool, bool)
    toggleTime = pyqtSignal()
    toggleArchaeology = pyqtSignal()
    back = pyqtSignal()
    forward = pyqtSignal()
    play = pyqtSignal()
    signalCurrentTimeUpdated = pyqtSignal(QDateTime)
    signalSliderTimeChanged = pyqtSignal(float)
    signalTimeFrameType = pyqtSignal(str)
    signalTimeFrameSize = pyqtSignal(int)
    signalSaveOptions = pyqtSignal()
    signalArchDigitsSpecified = pyqtSignal(int)
    signalArchCancelled = pyqtSignal()

    def __init__(self, iface, model):
        """Initialize the GUI control"""
        QObject.__init__(self)
        self.iface = iface
        self.model = model
        self.optionsDialog = None
        self.path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ui')

        # load the form
        self.dock = uic.loadUi(os.path.join(self.path, DOCK_WIDGET_FILE))
        self.iface.addDockWidget(Qt.BottomDockWidgetArea, self.dock)

        self.dock.pushButtonExportVideo.setEnabled(False)  # only enabled if there are managed layers
        self.dock.pushButtonOptions.clicked.connect(self.optionsClicked)
        self.dock.pushButtonExportVideo.clicked.connect(self.exportVideoClicked)
        self.dock.pushButtonToggleTime.clicked.connect(self.toggleTimeClicked)
        self.dock.pushButtonArchaeology.clicked.connect(self.archaeologyClicked)
        self.dock.pushButtonBack.clicked.connect(self.backClicked)
        self.dock.pushButtonForward.clicked.connect(self.forwardClicked)
        self.dock.pushButtonPlay.clicked.connect(self.playClicked)
        self.dock.dateTimeEditCurrentTime.dateTimeChanged.connect(self.currentTimeChangedDateText)
        # self.dock.horizontalTimeSlider.valueChanged.connect(self.currentTimeChangedSlider)

        self.sliderTimer = QTimer(self)
        self.sliderTimer.setInterval(250)
        self.sliderTimer.setSingleShot(True)
        self.sliderTimer.timeout.connect(self.currentTimeChangedSlider)
        self.dock.horizontalTimeSlider.valueChanged.connect(self.startTimer)

        self.dock.comboBoxTimeExtent.currentIndexChanged[str].connect(self.currentTimeFrameTypeChanged)
        self.dock.spinBoxTimeExtent.valueChanged.connect(self.currentTimeFrameSizeChanged)

        # this signal is responsible for rendering the label
        self.iface.mapCanvas().renderComplete.connect(self.renderLabel)

        # create shortcuts
        self.focusSC = QShortcut(QKeySequence("Ctrl+Space"), self.dock)
        self.focusSC.activated.connect(self.dock.horizontalTimeSlider.setFocus)

        # put default values
        self.dock.horizontalTimeSlider.setMinimum(conf.MIN_TIMESLIDER_DEFAULT)
        self.dock.horizontalTimeSlider.setMaximum(conf.MAX_TIMESLIDER_DEFAULT)
        self.dock.dateTimeEditCurrentTime.setMinimumDate(MIN_QDATE)
        self.showLabel = conf.DEFAULT_SHOW_LABEL
        self.exportEmpty = conf.DEFAULT_EXPORT_EMPTY
        self.labelOptions = TimestampLabelConfig(self.model)

        # placeholders for widgets that are added dynamically
        self.bcdateSpinBox = None

        # add to plugins toolbar
        try:
            self.action = QAction(QCoreApplication.translate("TimeManagerGuiControl", "Toggle visibility"), self.iface.mainWindow())
            self.action.triggered.connect(self.toggleDock)
            self.iface.addPluginToMenu(QCoreApplication.translate("TimeManagerGuiControl", "&TimeManager"), self.action)
        except Exception as e:
            pass  # OK for testing

    def startTimer(self):
        self.sliderTimer.start()

    def getLabelFormat(self):
        return self.labelOptions.fmt

    def getLabelFont(self):
        return self.labelOptions.font

    def getLabelSize(self):
        return self.labelOptions.size

    def getLabelColor(self):
        return self.labelOptions.color

    def getLabelBgColor(self):
        return self.labelOptions.bgcolor

    def getLabelPlacement(self):
        return self.labelOptions.placement

    def setLabelFormat(self, fmt):
        if not fmt:
            return
        self.labelOptions.fmt = fmt

    def setLabelFont(self, font):
        if not font:
            return
        self.labelOptions.font = font

    def setLabelSize(self, size):
        if not size:
            return
        self.labelOptions.size = size

    def setLabelColor(self, color):
        if not color:
            return
        self.labelOptions.color = color

    def setLabelBgColor(self, bgcolor):
        if not bgcolor:
            return
        self.labelOptions.bgcolor = bgcolor

    def setLabelPlacement(self, placement):
        if not placement:
            return
        self.labelOptions.placement = placement

    def toggleDock(self):
        self.dock.setVisible(not self.dock.isVisible())

    def getOptionsDialog(self):
        return self.optionsDialog

    def showAnimationOptions(self):
        self.animationDialog = uic.loadUi(os.path.join(self.path, ANIMATION_WIDGET_FILE))

        def selectFile():
            self.animationDialog.lineEdit.setText(QFileDialog.getOpenFileName())

        self.animationDialog.pushButton.clicked.connect(self.selectAnimationFolder)
        self.animationDialog.buttonBox.accepted.connect(self.sendAnimationOptions)
        self.animationDialog.show()

    def selectAnimationFolder(self):
        prev_directory = TimeManagerProjectHandler.plugin_setting(conf.LAST_ANIMATION_PATH_TAG)
        if prev_directory:
            self.animationDialog.lineEdit.setText(QFileDialog.getExistingDirectory(directory=prev_directory))
        else:
            self.animationDialog.lineEdit.setText(QFileDialog.getExistingDirectory())

    def sendAnimationOptions(self):
        path = self.animationDialog.lineEdit.text()
        if path == "":
            self.showAnimationOptions()
        TimeManagerProjectHandler.set_plugin_setting(conf.LAST_ANIMATION_PATH_TAG, path)
        delay_millis = self.animationDialog.spinBoxDelay.value()
        export_gif = self.animationDialog.radioAnimatedGif.isChecked()
        export_video = self.animationDialog.radioVideo.isChecked()
        do_clear = self.animationDialog.clearCheckBox.isChecked()
        self.signalExportVideo.emit(path, delay_millis, export_gif, export_video, do_clear)

    def showLabelOptions(self):
        options = self.labelOptions
        self.dialog = QDialog()

        lo = uic.loadUiType(os.path.join(self.path, LABEL_OPTIONS_WIDGET_FILE))[0]
        self.labelOptionsDialog = lo()
        self.labelOptionsDialog.setupUi(self.dialog)

        self.labelOptionsDialog.fontsize.setValue(options.size)
        self.labelOptionsDialog.time_format.setText(options.fmt)
        self.labelOptionsDialog.font.setCurrentFont(QFont(options.font))
        self.labelOptionsDialog.placement.addItems(TimestampLabelConfig.PLACEMENTS)
        self.labelOptionsDialog.placement.setCurrentIndex(TimestampLabelConfig.PLACEMENTS.index(options.placement))
        self.labelOptionsDialog.text_color.setColor(QColor(options.color))
        self.labelOptionsDialog.bg_color.setColor(QColor(options.bgcolor))
        self.labelOptionsDialog.buttonBox.accepted.connect(self.saveLabelOptions)
        self.dialog.show()

    def saveLabelOptions(self):
        self.labelOptions.font = self.labelOptionsDialog.font.currentFont().family()
        self.labelOptions.size = self.labelOptionsDialog.fontsize.value()
        self.labelOptions.bgcolor = self.labelOptionsDialog.bg_color.color().name()
        self.labelOptions.color = self.labelOptionsDialog.text_color.color().name()
        self.labelOptions.placement = self.labelOptionsDialog.placement.currentText()
        self.labelOptions.fmt = self.labelOptionsDialog.time_format.text()
        if self.labelOptionsDialog.radioButton_dt.isChecked():
            self.labelOptions.type = "dt"
        if self.labelOptionsDialog.radioButton_beginning.isChecked():
            self.labelOptions.type = "beginning"
        if self.labelOptionsDialog.radioButton_epoch.isChecked():
            self.labelOptions.type = "epoch"

    def enableArchaeologyTextBox(self):
        self.dock.dateTimeEditCurrentTime.dateTimeChanged.connect(self.currentTimeChangedDateText)
        if self.bcdateSpinBox is None:
            self.bcdateSpinBox = self.createBCWidget(self.dock)
            self.bcdateSpinBox.editingFinished.connect(self.currentBCYearChanged)
        self.replaceWidget(self.dock.horizontalLayout, self.dock.dateTimeEditCurrentTime, self.bcdateSpinBox, 5)

    def getTimeWidget(self):
        if time_util.is_archaelogical():
            return self.bcdateSpinBox
        else:
            return self.dock.dateTimeEditCurrentTime

    def currentBCYearChanged(self):
        val = self.bcdateSpinBox.text()
        try:
            bcdate_util.BCDate.from_str(val, strict_zeros=False)
            self.signalCurrentTimeUpdated.emit(val)
        except Exception as e:
            warn("Invalid bc date: {}".format(val))  # how to mark as such?
            return

    def disableArchaeologyTextBox(self):
        if self.bcdateSpinBox is None:
            return
        self.replaceWidget(self.dock.horizontalLayout, self.bcdateSpinBox, self.dock.dateTimeEditCurrentTime, 5)

    def createBCWidget(self, mainWidget):
        newWidget = QLineEdit(mainWidget)  # QtGui.QSpinBox(mainWidget)
        # newWidget.setMinimum(-1000000)
        # newWidget.setValue(-1)
        newWidget.setText("0001 BC")
        return newWidget

    def replaceWidget(self, layout, oldWidget, newWidget, idx):
        """
        Replaces oldWidget with newWidget at layout at index idx
        The way it is done, the widget is not destroyed and the connections to it remain
        """

        layout.removeWidget(oldWidget)
        oldWidget.close()  # I wonder if this has any memory leaks? </philosoraptor>
        layout.insertWidget(idx, newWidget)
        newWidget.show()
        layout.update()

    def optionsClicked(self):
        self.showOptions.emit()

    def exportVideoClicked(self):
        self.showAnimationOptions()

    def toggleTimeClicked(self):
        self.toggleTime.emit()

    def archaeologyClicked(self):
        self.toggleArchaeology.emit()

    def showArchOptions(self):
        self.archMenu = uic.loadUi(os.path.join(self.path, ARCH_WIDGET_FILE))
        self.archMenu.buttonBox.accepted.connect(self.saveArchOptions)
        self.archMenu.buttonBox.rejected.connect(self.cancelArch)
        self.archMenu.show()

    def saveArchOptions(self):
        self.signalArchDigitsSpecified.emit(self.archMenu.numDigits.value())

    def cancelArch(self):
        self.signalArchCancelled.emit()

    def backClicked(self):
        self.back.emit()

    def forwardClicked(self):
        self.forward.emit()

    def playClicked(self):
        if self.dock.pushButtonPlay.isChecked():
            self.dock.pushButtonPlay.setIcon(QIcon("TimeManager:images/pause.png"))
        else:
            self.dock.pushButtonPlay.setIcon(QIcon("TimeManager:images/play.png"))
        self.play.emit()

    def currentTimeChangedSlider(self):
        sliderVal = self.dock.horizontalTimeSlider.value()
        try:
            pct = (sliderVal - self.dock.horizontalTimeSlider.minimum()) * 1.0 / (
                self.dock.horizontalTimeSlider.maximum() - self.dock.horizontalTimeSlider.minimum())
        except Exception as e:
            # slider is not properly initialized yet
            return
        if self.model.getActiveDelimitedText() and qgs.getVersion() < conf.MIN_DTEXT_FIXED:
            time.sleep(0.1)  # hack to fix issue in qgis core with delimited text which was fixed in 2.9
        self.signalSliderTimeChanged.emit(pct)

    def currentTimeChangedDateText(self, qdate):
        # info("changed time via text")
        self.signalCurrentTimeUpdated.emit(qdate)

    def currentTimeFrameTypeChanged(self, frameType):
        self.signalTimeFrameType.emit(frameType)

    def currentTimeFrameSizeChanged(self, frameSize):
        if frameSize < 1:  # time frame size = 0  is meaningless
            self.dock.spinBoxTimeExtent.setValue(1)
            return
        self.signalTimeFrameSize.emit(frameSize)

    def unload(self):
        """Unload the plugin"""
        self.iface.removeDockWidget(self.dock)
        self.iface.removePluginMenu("TimeManager", self.action)

    def setWindowTitle(self, title):
        self.dock.setWindowTitle(title)

    def showOptionsDialog(self, layerList, animationFrameLength, playBackwards=False, loopAnimation=False):
        """Show the optionsDialog and populate it with settings from timeLayerManager"""

        # load the form
        self.optionsDialog = uic.loadUi(os.path.join(self.path, OPTIONS_WIDGET_FILE))

        # restore settings from layerList:
        for layer in layerList:
            settings = layer_settings.getSettingsFromLayer(layer)
            layer_settings.addSettingsToRow(settings, self.optionsDialog.tableWidget)
        self.optionsDialog.tableWidget.resizeColumnsToContents()

        # restore animation options
        self.optionsDialog.spinBoxFrameLength.setValue(animationFrameLength)
        self.optionsDialog.checkBoxBackwards.setChecked(playBackwards)
        self.optionsDialog.checkBoxLabel.setChecked(self.showLabel)
        self.optionsDialog.checkBoxDontExportEmpty.setChecked(not self.exportEmpty)
        self.optionsDialog.checkBoxLoop.setChecked(loopAnimation)
        self.optionsDialog.show_label_options_button.clicked.connect(self.showLabelOptions)
        self.optionsDialog.checkBoxLabel.stateChanged.connect(self.showOrHideLabelOptions)

        self.optionsDialog.textBrowser.setHtml(QCoreApplication.translate('TimeManager', """\
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html>
<head>
    <meta name="qrichtext" content="1"/>

    <style>
    li.mono {
    font-family: Consolas, Courier New, Courier, monospace;
}
    </style>
</head>
<body>

<h1>Time Manager</h1>

<p>Time Manager filters your layers and displays only layers and features that match the specified time frame. Time Manager supports vector layers and raster layers (including WMS-T).</p>

<p>Timestamps have to be in one of the following formats:</p>

<ul>
<li class="mono">%Y-%m-%d %H:%M:%S.%f</li>
<li class="mono">%Y-%m-%d %H:%M:%S</li>
<li class="mono">%Y-%m-%d %H:%M</li>
<li class="mono">%Y-%m-%dT%H:%M:%S</li>
<li class="mono">%Y-%m-%dT%H:%M:%SZ</li>
<li class="mono">%Y-%m-%dT%H:%M</li>
<li class="mono">%Y-%m-%dT%H:%MZ</li>
<li class="mono">%Y-%m-%d</li>
<li class="mono">%Y/%m/%d %H:%M:%S.%f</li>
<li class="mono">%Y/%m/%d %H:%M:%S</li>
<li class="mono">%Y/%m/%d %H:%M</li>
<li class="mono">%Y/%m/%d</li>
<li class="mono">%H:%M:%S</li>
<li class="mono">%H:%M:%S.%f</li>
<li class="mono">%Y.%m.%d %H:%M:%S.%f</li>
<li class="mono">%Y.%m.%d %H:%M:%S</li>
<li class="mono">%Y.%m.%d %H:%M</li>
<li class="mono">%Y.%m.%d</li>
<li class="mono">%Y%m%d%H%M%SED</li>
<li>Integer timestamp in seconds after or before the epoch (1970-1-1)</li>
</ul>


<p>The layer list contains all layers managed by Time Manager.
To add a vector layer, press [Add layer].
To add a raster layer, press [Add raster].
If you want to remove a layer from the list, select it and press [Remove layer].</p>


<p>Below the layer list, you'll find the following <b>animation options</b>:</p>

<p><b>Show frame for x milliseconds</b>...
allows you to adjust for how long a frame will be visible during the animation</p>

<p><b>Play animation backwards</b>...
if checked, the animation will run in reverse direction</p>

<p><b>Display frame start time on map</b>...
if checked, displays the start time of the visible frame in the lower right corner of the map</p>

<h2>Add Layer dialog</h2>

<p>Here, you are asked to select the layer that should be added and specify the columns containing
start and (optionally) end time.</p>

<p>The <b>offset</b> option allows you to further time the appearance of features. If you specify
an offset of -1, the features will appear one second later than they would by default.</p>

<h2>Dock widget</h2>

<p>The dock was designed to attach to the bottom of the QGIS main window. It offers the following tools:</p>

<ul>
<li><img src="images/power_on.png" alt="power"/> ... On/Off switch, allows you to turn Time Manager's functionality on/off with the click of only one button</li>
<li><span class="hidden">[Settings]</span><input type="button" value="Settings"/> ... opens the Settings dialog where you can manage your spatio-temporal layers</li>
<li><span class="hidden">[Export Video]</span><input type="button" value="Export Video"/> ... exports an image series based on current settings (This button is only enabled if there are layers registered in Time Manager "Settings")</li>
<li><b>Time frame start: <span class="hidden">[2000-01-01 00:00:00]</span></b><input type="text" value="2000-01-01 00:00:00"/> ... shows the start time of the currently active frame. Allows you to precisely specify your desired analysis time.</li>
<li><b>Time frame size: </b><input type="text" value="1"/><span class="hidden">[x]</span><select><option value="days">days</option></select> ... allow you to choose the size of the time frame</li>
<li><img src="images/back.png" alt="back"/> ... go to the previous time frame</li>
<li><img src="images/forward.png" alt="forward"/> ... go to the next time frame</li>
<li><b>Slider</b> ... shows the position of current frame relative to the whole dataset and allows you to seamlessly scroll through the dataset</li>
<li><img src="images/play.png" alt="play"/> ... start an automatic animation based on your current settings</li>
</ul>

</body>
</html>"""))

        # show dialog
        self.showOrHideLabelOptions()
        self.optionsDialog.show()

        # create raster and vector dialogs
        self.vectorDialog = VectorLayerDialog(self.iface, os.path.join(self.path, ADD_VECTOR_LAYER_WIDGET_FILE),
                                              self.optionsDialog.tableWidget)
        self.rasterDialog = RasterLayerDialog(self.iface, os.path.join(self.path, ADD_RASTER_LAYER_WIDGET_FILE),
                                              self.optionsDialog.tableWidget)
        # establish connections
        self.optionsDialog.pushButtonAddVector.clicked.connect(self.vectorDialog.show)
        self.optionsDialog.pushButtonAddRaster.clicked.connect(self.rasterDialog.show)
        self.optionsDialog.pushButtonRemove.clicked.connect(self.removeLayer)
        self.optionsDialog.buttonBox.accepted.connect(self.saveOptions)
        # self.optionsDialog.buttonBox.helpRequested.connect(self.showHelp)

    def showOrHideLabelOptions(self):
        self.optionsDialog.show_label_options_button.setEnabled(
            self.optionsDialog.checkBoxLabel.isChecked())

    def saveOptions(self):
        """Save the options from optionsDialog to timeLayerManager"""
        self.signalSaveOptions.emit()

    def removeLayer(self):
        """Remove currently selected layer (= row) from options"""
        currentRow = self.optionsDialog.tableWidget.currentRow()
        try:
            layerName = self.optionsDialog.tableWidget.item(currentRow, 0).text()
        except AttributeError:  # if no row is selected
            return
        if QMessageBox.question(self.optionsDialog,
                                QCoreApplication.translate("TimeManagerGuiControl", "Remove Layer"),
                                QCoreApplication.translate("TimeManagerGuiControl", "Do you really want to remove layer {}?").format(layerName),
                                QMessageBox.Ok, QMessageBox.Cancel) == QMessageBox.Ok:
            self.optionsDialog.tableWidget.removeRow(self.optionsDialog.tableWidget.currentRow())

    def disableAnimationExport(self):
        """Disable the animation export button"""
        self.dock.pushButtonExportVideo.setEnabled(False)

    def enableAnimationExport(self):
        """Enable animation export button"""
        self.dock.pushButtonExportVideo.setEnabled(True)

    def refreshMapCanvas(self, sender=None):
        """Refresh the map canvas"""
        # QMessageBox.information(self.iface.mainWindow(),'Test Output','Refresh!\n'+str(sender))
        self.iface.mapCanvas().refresh()

    def setTimeFrameSize(self, frameSize):
        """Set spinBoxTimeExtent to given frameSize"""
        self.dock.spinBoxTimeExtent.setValue(frameSize)

    def setTimeFrameType(self, frameType):
        """Set comboBoxTimeExtent to given frameType"""
        i = self.dock.comboBoxTimeExtent.findText(frameType)
        self.dock.comboBoxTimeExtent.setCurrentIndex(i)

    def setActive(self, isActive):
        """Toggle pushButtonToggleTime"""
        self.dock.pushButtonToggleTime.setChecked(isActive)

    def setArchaeologyPressed(self, isActive):
        """Toggle pushButtonArchaeology"""
        self.dock.pushButtonArchaeology.setChecked(isActive)

    def addActionShowSettings(self, action):
        """Add action to pushButttonOptions"""
        self.dock.pushButtonOptions.addAction(action)

    def turnPlayButtonOff(self):
        """Turn pushButtonPlay off"""
        if self.dock.pushButtonPlay.isChecked():
            self.dock.pushButtonPlay.toggle()
            self.dock.pushButtonPlay.setIcon(QIcon("TimeManager:images/play.png"))

    def renderLabel(self, painter):
        """Render the current timestamp on the map canvas"""
        if not self.showLabel or not self.model.hasLayers() or not self.dock.pushButtonToggleTime.isChecked():
            return

        dt = self.model.getCurrentTimePosition()
        if dt is None:
            return

        labelString = self.labelOptions.getLabel(dt)

        # Determine placement of label given cardinal directions
        flags = 0
        for direction, flag in ('N', Qt.AlignTop), ('S', Qt.AlignBottom), ('E', Qt.AlignRight), ('W', Qt.AlignLeft):
            if direction in self.labelOptions.placement:
                flags |= flag

        # Get canvas dimensions
        width = painter.device().width()
        height = painter.device().height()

        painter.setRenderHint(painter.Antialiasing, True)
        txt = QTextDocument()
        html = """<span style="background-color:%s; padding: 5px; font-size: %spx;">
                    <font face="%s" color="%s">%s</font>
                  </span> """\
               % (self.labelOptions.bgcolor, self.labelOptions.size, self.labelOptions.font,
                  self.labelOptions.color, labelString)
        txt.setHtml(html)
        layout = txt.documentLayout()
        size = layout.documentSize()

        if flags & Qt.AlignRight:
            x = width - 5 - size.width()
        elif flags & Qt.AlignLeft:
            x = 5
        else:
            x = width / 2 - size.width() / 2

        if flags & Qt.AlignBottom:
            y = height - 5 - size.height()
        elif flags & Qt.AlignTop:
            y = 5
        else:
            y = height / 2 - size.height() / 2

        painter.translate(x, y)
        layout.draw(painter, QAbstractTextDocumentLayout.PaintContext())
        painter.translate(-x, -y)  # translate back

    def repaintRasters(self):
        rasters = self.model.getActiveRasters()
        list([x.layer.triggerRepaint() for x in rasters])

    def repaintVectors(self):
        list([x.layer.triggerRepaint() for x in self.model.getActiveVectors()])

    def repaintJoined(self):
        layerIdsToRefresh = qgs.getAllJoinedLayers(set([x.layer.id() for x in self.model.getActiveVectors()]))
        # info("to refresh {}".format(layerIdsToRefresh))
        layersToRefresh = [qgs.getLayerFromId(x) for x in layerIdsToRefresh]
        list([x.triggerRepaint() for x in layersToRefresh])
예제 #28
0
class FeatureSelectorWidget(QWidget):
    feature_identified = pyqtSignal(QgsFeature)

    def __init__(self, parent):
        QWidget.__init__(self, parent)

        edit_layout = QHBoxLayout()
        edit_layout.setContentsMargins(0, 0, 0, 0)
        edit_layout.setSpacing(2)
        self.setLayout(edit_layout)

        self.line_edit = QLineEdit(self)
        self.line_edit.setReadOnly(True)
        edit_layout.addWidget(self.line_edit)

        self.highlight_feature_button = QToolButton(self)
        self.highlight_feature_button.setPopupMode(QToolButton.MenuButtonPopup)
        self.highlight_feature_action = QAction(
            QgsApplication.getThemeIcon("/mActionHighlightFeature.svg"),
            "Highlight feature", self)
        self.scale_highlight_feature_action = QAction(
            QgsApplication.getThemeIcon("/mActionScaleHighlightFeature.svg"),
            "Scale and highlight feature", self)
        self.pan_highlight_feature_action = QAction(
            QgsApplication.getThemeIcon("/mActionPanHighlightFeature.svg"),
            "Pan and highlight feature", self)
        self.highlight_feature_button.addAction(self.highlight_feature_action)
        self.highlight_feature_button.addAction(
            self.scale_highlight_feature_action)
        self.highlight_feature_button.addAction(
            self.pan_highlight_feature_action)
        self.highlight_feature_button.setDefaultAction(
            self.highlight_feature_action)
        edit_layout.addWidget(self.highlight_feature_button)

        self.map_identification_button = QToolButton(self)
        self.map_identification_button.setIcon(
            QgsApplication.getThemeIcon("/mActionMapIdentification.svg"))
        self.map_identification_button.setText("Select on map")
        self.map_identification_button.setCheckable(True)
        edit_layout.addWidget(self.map_identification_button)

        self.map_identification_button.clicked.connect(self.map_identification)
        self.highlight_feature_button.triggered.connect(
            self.highlight_action_triggered)

        self.layer = None
        self.map_tool = None
        self.canvas = None
        self.window_widget = None
        self.highlight = None
        self.feature = QgsFeature()

    def set_canvas(self, map_canvas):
        self.map_tool = QgsMapToolIdentifyFeature(map_canvas)
        self.map_tool.setButton(self.map_identification_button)
        self.canvas = map_canvas

    def set_layer(self, layer):
        self.layer = layer

    def set_feature(self, feature, canvas_extent=CanvasExtent.Fixed):
        self.line_edit.clear()
        self.feature = feature

        if self.feature is None or not self.feature.isValid(
        ) or self.layer is None:
            return

        expression = QgsExpression(self.layer.displayExpression())
        context = QgsExpressionContext()
        scope = QgsExpressionContextScope()
        context.appendScope(scope)
        scope.setFeature(feature)
        feature_title = expression.evaluate(context)
        if feature_title == "":
            feature_title = feature.id()
        self.line_edit.setText(str(feature_title))
        self.highlight_feature(canvas_extent)

    def clear(self):
        self.feature = QgsFeature()
        self.line_edit.clear()

    @pyqtSlot()
    def map_identification(self):
        if self.layer is None or self.map_tool is None or self.canvas is None:
            return

        self.map_tool.setLayer(self.layer)
        self.canvas.setMapTool(self.map_tool)

        self.window_widget = QWidget.window(self)
        self.canvas.window().raise_()
        self.canvas.activateWindow()
        self.canvas.setFocus()

        self.map_tool.featureIdentified.connect(
            self.map_tool_feature_identified)
        self.map_tool.deactivated.connect(self.map_tool_deactivated)

    def map_tool_feature_identified(self, feature):
        feature = QgsFeature(feature)
        self.feature_identified.emit(feature)
        self.unset_map_tool()
        self.set_feature(feature)

    def map_tool_deactivated(self):
        if self.window_widget is not None:
            self.window_widget.raise_()
            self.window_widget.activateWindow()

    def highlight_feature(self, canvas_extent=CanvasExtent.Fixed):
        if self.canvas is None or not self.feature.isValid():
            return

        geom = self.feature.geometry()

        if geom is None:
            return

        if canvas_extent == CanvasExtent.Scale:
            feature_bounding_box = geom.boundingBox()
            feature_bounding_box = self.canvas.mapSettings(
            ).layerToMapCoordinates(self.layer, feature_bounding_box)
            extent = self.canvas.extent()
            if not extent.contains(feature_bounding_box):
                extent.combineExtentWith(feature_bounding_box)
                extent.scale(1.1)
                self.canvas.setExtent(extent)
                self.canvas.refresh()

        elif canvas_extent == CanvasExtent.Pan:
            centroid = geom.centroid()
            center = centroid.asPoint()

            center = self.canvas.mapSettings().layerToMapCoordinates(
                self.layer, center)
            self.canvas.zoomByFactor(1.0,
                                     center)  # refresh is done in this method

        # highlight
        self.delete_highlight()
        self.highlight = QgsHighlight(self.canvas, geom, self.layer)

        settings = QSettings()
        color = QColor(
            settings.value("/Map/highlight/color",
                           Qgis.DEFAULT_HIGHLIGHT_COLOR.name()))
        alpha = int(
            settings.value("/Map/highlight/colorAlpha",
                           Qgis.DEFAULT_HIGHLIGHT_COLOR.alpha()))
        buffer = 2 * float(
            settings.value("/Map/highlight/buffer",
                           Qgis.DEFAULT_HIGHLIGHT_BUFFER_MM))
        min_width = 2 * float(
            settings.value("/Map/highlight/min_width",
                           Qgis.DEFAULT_HIGHLIGHT_MIN_WIDTH_MM))

        self.highlight.setColor(color)  # sets also fill with default alpha
        color.setAlpha(alpha)
        self.highlight.setFillColor(color)  # sets fill with alpha
        self.highlight.setBuffer(buffer)
        self.highlight.setMinWidth(min_width)
        self.highlight.setWidth(4.0)
        self.highlight.show()

        self.timer = QTimer(self)
        self.timer.setSingleShot(True)
        self.timer.timeout.connect(self.delete_highlight)
        self.timer.start(3000)

    def delete_highlight(self):
        if self.highlight is not None:
            self.highlight.hide()
            del self.highlight
            self.highlight = None

    def unset_map_tool(self):
        if self.canvas is not None and self.map_tool is not None:
            # this will call mapTool.deactivated
            self.canvas.unsetMapTool(self.map_tool)

    def highlight_action_triggered(self, action):
        self.highlight_feature_button.setDefaultAction(action)

        if action == self.highlight_feature_action:
            self.highlight_feature()

        elif action == self.scale_highlight_feature_action:
            self.highlight_feature(CanvasExtent.Scale)

        elif action == self.pan_highlight_feature_action:
            self.highlight_feature(CanvasExtent.Pan)
class OpenlayersController(QObject):
    """
    Helper class that deals with QWebPage.
    The object lives in GUI thread, its request() slot is asynchronously called
    from worker thread.
    See https://github.com/wonder-sk/qgis-mtr-example-plugin for basic example
    1. Load Web Page with OpenLayers map
    2. Update OL map extend according to QGIS canvas extent
    """

    # signal that reports to the worker thread that the image is ready
    finished = pyqtSignal()

    def __init__(self, parent, context, webPage, layerType):
        QObject.__init__(self, parent)

        debug("OpenlayersController.__init__", 3)
        self.context = context
        self.layerType = layerType

        self.img = QImage()

        self.page = webPage
        self.page.loadFinished.connect(self.pageLoaded)
        # initial size for map
        self.page.setViewportSize(QSize(1, 1))

        self.timerMapReady = QTimer()
        self.timerMapReady.setSingleShot(True)
        self.timerMapReady.setInterval(20)
        self.timerMapReady.timeout.connect(self.checkMapReady)

        self.timer = QTimer()
        self.timer.setInterval(100)
        self.timer.timeout.connect(self.checkMapUpdate)

        self.timerMax = QTimer()
        self.timerMax.setSingleShot(True)
        # TODO: different timeouts for map types
        self.timerMax.setInterval(2500)
        self.timerMax.timeout.connect(self.mapTimeout)

    @pyqtSlot()
    def request(self):
        debug("[GUI THREAD] Processing request", 3)
        self.cancelled = False

        if not self.page.loaded:
            self.init_page()
        else:
            self.setup_map()

    def init_page(self):
        url = self.layerType.html_url()
        debug("page file: %s" % url)
        self.page.mainFrame().load(QUrl(url))
        # wait for page to finish loading
        debug("OpenlayersWorker waiting for page to load", 3)

    def pageLoaded(self):
        debug("[GUI THREAD] pageLoaded", 3)
        if self.cancelled:
            self.emitErrorImage()
            return

        # wait until OpenLayers map is ready
        self.checkMapReady()

    def checkMapReady(self):
        debug("[GUI THREAD] checkMapReady", 3)
        if self.page.mainFrame().evaluateJavaScript("map != undefined"):
            # map ready
            self.page.loaded = True
            self.setup_map()
        else:
            # wait for map
            self.timerMapReady.start()

    def setup_map(self):
        rendererContext = self.context

        # FIXME: self.mapSettings.outputDpi()
        self.outputDpi = rendererContext.painter().device().logicalDpiX()
        debug(" extent: %s" % rendererContext.extent().toString(), 3)
        debug(
            " center: %lf, %lf" % (rendererContext.extent().center().x(),
                                   rendererContext.extent().center().y()), 3)
        debug(
            " size: %d, %d" %
            (rendererContext.painter().viewport().size().width(),
             rendererContext.painter().viewport().size().height()), 3)
        debug(
            " logicalDpiX: %d" %
            rendererContext.painter().device().logicalDpiX(), 3)
        debug(" outputDpi: %lf" % self.outputDpi)
        debug(
            " mapUnitsPerPixel: %f" %
            rendererContext.mapToPixel().mapUnitsPerPixel(), 3)
        # debug(" rasterScaleFactor: %s" % str(rendererContext.
        #                                      rasterScaleFactor()), 3)
        # debug(" outputSize: %d, %d" % (self.iface.mapCanvas().mapRenderer().
        #                                outputSize().width(),
        #                                self.iface.mapCanvas().mapRenderer().
        #                                outputSize().height()), 3)
        # debug(" scale: %lf" % self.iface.mapCanvas().mapRenderer().scale(),
        #                                3)
        #
        # if (self.page.lastExtent == rendererContext.extent()
        #         and self.page.lastViewPortSize == rendererContext.painter().
        #         viewport().size()
        #         and self.page.lastLogicalDpi == rendererContext.painter().
        #         device().logicalDpiX()
        #         and self.page.lastOutputDpi == self.outputDpi
        #         and self.page.lastMapUnitsPerPixel == rendererContext.
        #         mapToPixel().mapUnitsPerPixel()):
        #     self.renderMap()
        #     self.finished.emit()
        #     return

        self.targetSize = rendererContext.painter().viewport().size()
        if rendererContext.painter().device().logicalDpiX() != int(
                self.outputDpi):
            # use screen dpi for printing
            sizeFact = self.outputDpi / 25.4 / rendererContext.mapToPixel(
            ).mapUnitsPerPixel()
            self.targetSize.setWidth(rendererContext.extent().width() *
                                     sizeFact)
            self.targetSize.setHeight(rendererContext.extent().height() *
                                      sizeFact)
        debug(
            " targetSize: %d, %d" %
            (self.targetSize.width(), self.targetSize.height()), 3)

        # find matching OL resolution
        qgisRes = rendererContext.extent().width() / self.targetSize.width()
        olRes = None
        for res in self.page.resolutions():
            if qgisRes >= res:
                olRes = res
                break
        if olRes is None:
            debug("No matching OL resolution found (QGIS resolution: %f)" %
                  qgisRes)
            self.emitErrorImage()
            return

        # adjust OpenLayers viewport to match QGIS extent
        olWidth = rendererContext.extent().width() / olRes
        olHeight = rendererContext.extent().height() / olRes
        debug(
            "    adjust viewport: %f -> %f: %f x %f" %
            (qgisRes, olRes, olWidth, olHeight), 3)
        olSize = QSize(int(olWidth), int(olHeight))
        self.page.setViewportSize(olSize)
        self.page.mainFrame().evaluateJavaScript("map.updateSize();")
        self.img = QImage(olSize, QImage.Format_ARGB32_Premultiplied)

        self.page.extent = rendererContext.extent()
        debug(
            "map.zoomToExtent (%f, %f, %f, %f)" %
            (self.page.extent.xMinimum(), self.page.extent.yMinimum(),
             self.page.extent.xMaximum(), self.page.extent.yMaximum()), 3)
        self.page.mainFrame().evaluateJavaScript(
            "map.zoomToExtent(new OpenLayers.Bounds(%f, %f, %f, %f), true);" %
            (self.page.extent.xMinimum(), self.page.extent.yMinimum(),
             self.page.extent.xMaximum(), self.page.extent.yMaximum()))
        olextent = self.page.mainFrame().evaluateJavaScript("map.getExtent();")
        debug("Resulting OL extent: %s" % olextent, 3)
        if olextent is None or not hasattr(olextent, '__getitem__'):
            debug("map.zoomToExtent failed")
            # map.setCenter and other operations throw "undefined[0]:
            # TypeError: 'null' is not an object" on first page load
            # We ignore that and render the initial map with wrong extents
            # self.emitErrorImage()
            # return
        else:
            reloffset = abs((self.page.extent.yMaximum() - olextent["top"]) /
                            self.page.extent.xMinimum())
            debug("relative offset yMaximum %f" % reloffset, 3)
            if reloffset > 0.1:
                debug("OL extent shift failure: %s" % reloffset)
                self.emitErrorImage()
                return
        self.mapFinished = False
        self.timer.start()
        self.timerMax.start()

    def checkMapUpdate(self):
        if self.layerType.emitsLoadEnd:
            # wait for OpenLayers to finish loading
            loadEndOL = self.page.mainFrame().evaluateJavaScript("loadEnd")
            debug(
                "waiting for loadEnd: renderingStopped=%r loadEndOL=%r" %
                (self.context.renderingStopped(), loadEndOL), 4)
            if loadEndOL is not None:
                self.mapFinished = loadEndOL
            else:
                debug("OpenlayersLayer Warning: Could not get loadEnd")

        if self.mapFinished:
            self.timerMax.stop()
            self.timer.stop()

            self.renderMap()

            self.finished.emit()

    def renderMap(self):
        rendererContext = self.context
        if rendererContext.painter().device().logicalDpiX() != int(
                self.outputDpi):
            printScale = 25.4 / self.outputDpi  # OL DPI to printer pixels
            rendererContext.painter().scale(printScale, printScale)

        # render OpenLayers to image
        painter = QPainter(self.img)
        self.page.mainFrame().render(painter)
        painter.end()

        if self.img.size() != self.targetSize:
            targetWidth = self.targetSize.width()
            targetHeight = self.targetSize.height()
            # scale using QImage for better quality
            debug(
                "    scale image: %i x %i -> %i x %i" %
                (self.img.width(), self.img.height(), targetWidth,
                 targetHeight), 3)
            self.img = self.img.scaled(targetWidth, targetHeight,
                                       Qt.KeepAspectRatio,
                                       Qt.SmoothTransformation)

        # save current state
        self.page.lastExtent = rendererContext.extent()
        self.page.lastViewPortSize = rendererContext.painter().viewport().size(
        )
        self.page.lastLogicalDpi = rendererContext.painter().device(
        ).logicalDpiX()
        self.page.lastOutputDpi = self.outputDpi
        self.page.lastMapUnitsPerPixel = rendererContext.mapToPixel(
        ).mapUnitsPerPixel()

    def mapTimeout(self):
        debug("mapTimeout reached")
        self.timer.stop()
        # if not self.layerType.emitsLoadEnd:
        self.renderMap()
        self.finished.emit()

    def emitErrorImage(self):
        self.img = QImage()
        self.targetSize = self.img.size
        self.finished.emit()
예제 #30
0
class QmsServiceToolbox(QDockWidget, FORM_CLASS):
    def __init__(self, iface):
        QDockWidget.__init__(self, iface.mainWindow())
        self.setupUi(self)
        self.newsFrame.setVisible(False)

        self.iface = iface
        self.search_threads = None  # []
        self.extent_renderer = RubberBandResultRenderer()

        self.cmbStatusFilter.addItem(self.tr('All'), STATUS_FILTER_ALL)
        self.cmbStatusFilter.addItem(self.tr('Valid'),
                                     STATUS_FILTER_ONLY_WORKS)
        self.cmbStatusFilter.currentIndexChanged.connect(self.start_search)

        if hasattr(self.txtSearch, 'setPlaceholderText'):
            self.txtSearch.setPlaceholderText(self.tr("Search string..."))

        self.delay_timer = QTimer(self)
        self.delay_timer.setSingleShot(True)
        self.delay_timer.setInterval(250)

        self.delay_timer.timeout.connect(self.start_search)
        self.txtSearch.textChanged.connect(self.delay_timer.start)
        self.btnFilterByExtent.toggled.connect(self.toggle_filter_button)
        self.one_process_work = QMutex()

        self.add_last_used_services()

        self.show_news()

    def show_news(self):
        client = Client()
        client.set_proxy(*QGISSettings.get_qgis_proxy())
        qms_news = client.get_news()

        if qms_news is None:
            self.newsFrame.setVisible(False)
            return

        news = News(qms_news)

        if news.is_time_to_show():
            self.newsLabel.setText(news.html)
            self.newsFrame.setVisible(True)
        else:
            self.newsFrame.setVisible(False)

    def toggle_filter_button(self, checked):
        self.txtSearch.setDisabled(checked)
        if checked:
            self.iface.mapCanvas().extentsChanged.connect(self.start_search)
            self.iface.mapCanvas().destinationCrsChanged.connect(
                self.start_search)
            self.start_search()
        else:
            self.iface.mapCanvas().extentsChanged.disconnect(self.start_search)
            self.iface.mapCanvas().destinationCrsChanged.disconnect(
                self.start_search)

    def start_search(self):
        search_text = None
        geom_filter = None

        # status filter
        status_filter = None
        sel_value = self.cmbStatusFilter.itemData(
            self.cmbStatusFilter.currentIndex())
        if sel_value != STATUS_FILTER_ALL:
            status_filter = sel_value

        if not self.btnFilterByExtent.isChecked():
            # text search
            search_text = unicode(self.txtSearch.text())
            if not search_text:
                self.lstSearchResult.clear()
                # self.clearSearchResult()
                self.add_last_used_services()
                return
        else:
            # extent filter
            extent = self.iface.mapCanvas().extent()
            map_crs = getCanvasDestinationCrs(self.iface)
            if map_crs.postgisSrid() != 4326:
                crsDest = QgsCoordinateReferenceSystem(4326)  # WGS 84
                xform = QgsCoordinateTransform(map_crs, crsDest)
                extent = xform.transform(extent)
            geom_filter = extent.asWktPolygon()

        if self.search_threads:
            self.search_threads.data_downloaded.disconnect()
            self.search_threads.search_finished.disconnect()
            self.search_threads.stop()
            self.search_threads.wait()

            self.lstSearchResult.clear()
            # self.clearSearchResult()

        searcher = SearchThread(search_text,
                                self.one_process_work,
                                parent=self.iface.mainWindow(),
                                geom_filter=geom_filter,
                                status_filter=status_filter)
        searcher.data_downloaded.connect(self.show_result)
        searcher.error_occurred.connect(self.show_error)
        searcher.search_started.connect(self.search_started_process)
        searcher.search_finished.connect(self.search_finished_progress)
        self.search_threads = searcher
        searcher.start()

    def add_last_used_services(self):
        services = CachedServices().get_cached_services()
        if len(services) == 0:
            return

        self.lstSearchResult.insertItem(0, self.tr("Last used:"))
        # l = QLabel(self.tr("Last used:"))
        # l.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
        # self.lSearchResult.addWidget(l)

        for attributes, image_qByteArray in services:
            custom_widget = QmsSearchResultItemWidget(attributes,
                                                      image_qByteArray)
            new_item = QListWidgetItem(self.lstSearchResult)
            new_item.setSizeHint(custom_widget.sizeHint())
            self.lstSearchResult.addItem(new_item)
            self.lstSearchResult.setItemWidget(new_item, custom_widget)
            # self.lSearchResult.addWidget(custom_widget)

        # w = QWidget()
        # w.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        # self.lSearchResult.addWidget(w)

    def search_started_process(self):
        self.lstSearchResult.clear()
        self.lstSearchResult.insertItem(0, self.tr('Searching...'))

    def search_finished_progress(self):
        self.lstSearchResult.takeItem(0)
        if self.lstSearchResult.count() == 0:
            new_widget = QLabel()
            new_widget.setTextFormat(Qt.RichText)
            new_widget.setOpenExternalLinks(True)
            new_widget.setWordWrap(True)
            new_widget.setText(
                u"<div align='center'> <strong>{}</strong> </div><div align='center' style='margin-top: 3px'> {} </div>"
                .format(
                    self.tr(u"No results."),
                    self.
                    tr(u"You can add a service to become searchable. Start <a href='{}'>here</a>."
                       ).format(u"https://qms.nextgis.com/create"),
                ))
            new_item = QListWidgetItem(self.lstSearchResult)
            new_item.setSizeHint(new_widget.sizeHint())
            self.lstSearchResult.addItem(new_item)
            self.lstSearchResult.setItemWidget(new_item, new_widget)

    def show_result(self, geoservice, image_ba):
        if geoservice:
            custom_widget = QmsSearchResultItemWidget(
                geoservice, image_ba, extent_renderer=self.extent_renderer)
            new_item = QListWidgetItem(self.lstSearchResult)
            new_item.setSizeHint(custom_widget.sizeHint())
            self.lstSearchResult.addItem(new_item)
            self.lstSearchResult.setItemWidget(new_item, custom_widget)

        else:
            new_item = QListWidgetItem()
            new_item.setText(self.tr('No results!'))
            new_item.setData(Qt.UserRole, None)
            self.lstSearchResult.addItem(new_item)
        self.lstSearchResult.update()

    def show_error(self, error_text):
        self.lstSearchResult.clear()
        new_widget = QLabel()
        new_widget.setTextFormat(Qt.RichText)
        new_widget.setOpenExternalLinks(True)
        new_widget.setWordWrap(True)
        new_widget.setText(
            u"<div align='center'> <strong>{}</strong> </div><div align='center' style='margin-top: 3px'> {} </div>"
            .format(self.tr('Error'), error_text))
        new_item = QListWidgetItem(self.lstSearchResult)
        new_item.setSizeHint(new_widget.sizeHint())
        self.lstSearchResult.addItem(new_item)
        self.lstSearchResult.setItemWidget(new_item, new_widget)
예제 #31
0
class RelationEditorDocumentSideWidget(QgsAbstractRelationEditorWidget,
                                       WidgetUi):
    def __init__(self, config, parent):
        super().__init__(config, parent)
        self._updateUiTimer = QTimer()
        self._updateUiTimer.setSingleShot(True)
        self._updateUiTimer.timeout.connect(self.updateUiTimeout)
        self.setupUi(self)

        self.polymorphicRelationEnabled = False
        self.polymorphicRelationId = str()

        self.generatedRelationList = []

        self._nmRelation = QgsRelation()
        self._polymorphicRelation = QgsPolymorphicRelation()
        self._layerInSameTransactionGroup = False

        self.cardinality = Cardinality.ManyToOne

        # Actions
        self.actionToggleEditing = QAction(
            QIcon(":/images/themes/default/mActionToggleEditing.svg"),
            self.tr("Toggle editing mode for child layers"))
        self.actionToggleEditing.setCheckable(True)
        self.actionSaveEdits = QAction(
            QIcon(":/images/themes/default/mActionSaveEdits.svg"),
            self.tr("Save child layer edits"))
        self.actionShowForm = QAction(
            QIcon(":/images/themes/default/mActionMultiEdit.svg"),
            self.tr("Show form"))
        self.actionLinkFeature = QAction(
            QIcon(":/images/themes/default/mActionLink.svg"),
            self.tr("Link feature"))
        self.actionUnlinkFeature = QAction(
            QIcon(":/images/themes/default/mActionUnlink.svg"),
            self.tr("Unlink feature"))

        # Tool buttons
        self.mToggleEditingToolButton.setDefaultAction(
            self.actionToggleEditing)
        self.mSaveEditsToolButton.setDefaultAction(self.actionSaveEdits)
        self.mShowFormToolButton.setDefaultAction(self.actionShowForm)
        self.mShowFormToolButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
        self.mLinkFeaturesToolButton.setDefaultAction(self.actionLinkFeature)
        self.mLinkFeaturesToolButton.setToolButtonStyle(Qt.ToolButtonIconOnly)
        self.mUnlinkFeaturesToolButton.setDefaultAction(
            self.actionUnlinkFeature)
        self.mUnlinkFeaturesToolButton.setToolButtonStyle(
            Qt.ToolButtonIconOnly)

        # TreeWidgetItem menu
        self.mFeaturesTreeWidget.setContextMenuPolicy(Qt.ActionsContextMenu)
        self.mFeaturesTreeWidget.addAction(self.actionShowForm)
        self.mFeaturesTreeWidget.addAction(self.actionUnlinkFeature)

        # Set initial state for add / remove etc.buttons
        self.updateButtons()

        # Signal slots
        self.actionToggleEditing.triggered.connect(self.toggleEditing)
        self.actionSaveEdits.triggered.connect(self.saveChildLayerEdits)
        self.actionShowForm.triggered.connect(self.actionShowFormTriggered)
        self.actionLinkFeature.triggered.connect(
            self.actionLinkFeatureTriggered)
        self.actionUnlinkFeature.triggered.connect(
            self.actionUnlinkFeatureTriggered)
        self.mFeaturesTreeWidget.currentItemChanged.connect(self.updateButtons)

    def nmRelation(self):
        return self._nmRelation

    def config(self):
        return {}

    def setConfig(self, config):
        self.polymorphicRelationEnabled = config[
            'polymorphic_relation_enabled']
        self.polymorphicRelationId = config['polymorphic_relation_id']

        if self.polymorphicRelationEnabled is False:
            return

        self._polymorphicRelation = QgsProject.instance().relationManager(
        ).polymorphicRelation(self.polymorphicRelationId)

        self._setCardinality()

    def updateUi(self):
        self._updateUiTimer.start(200)

    def updateUiTimeout(self):
        print('DocumentRelationEditorDocumentSideWidget.updateUiTimeout')

        self.mFeaturesTreeWidget.clear()

        if self.relation().isValid() is False or self.feature().isValid(
        ) is False:
            return

        if self.cardinality == Cardinality.ManyToOne:
            self.updateUiManyToOne()

        if self.cardinality == Cardinality.ManyToMany:
            self.updateUiManyToMany()

        if self.cardinality == Cardinality.ManyToOnePolymorphic:
            self.updateUiManyToOnePolymorphic()

        if self.cardinality == Cardinality.ManyToManyPolymorphic:
            self.updateUiManyToManyPolymorphic()

    def updateUiManyToOne(self):
        layer = self.relation().referencingLayer()
        request = self.relation().getRelatedFeaturesRequest(self.feature())
        for feature in layer.getFeatures(request):
            treeWidgetItem = QTreeWidgetItem(
                self.mFeaturesTreeWidget,
                [QgsVectorLayerUtils.getFeatureDisplayString(layer, feature)])
            treeWidgetItem.setData(0, TreeWidgetItemRole.Type,
                                   TreeWidgetItemType.Feature)
            treeWidgetItem.setData(0, TreeWidgetItemRole.Layer, layer)
            treeWidgetItem.setData(0, TreeWidgetItemRole.Feature, feature)

    def updateUiManyToMany(self):
        layer = self.relation().referencingLayer()
        request = self.relation().getRelatedFeaturesRequest(self.feature())
        filters = []
        for feature in layer.getFeatures(request):
            referencedFeatureRequest = self.nmRelation(
            ).getReferencedFeatureRequest(feature)
            filterExpression = referencedFeatureRequest.filterExpression()
            filters.append("(" + filterExpression.expression() + ")")

            nmRequest = QgsFeatureRequest()
            nmRequest.setFilterExpression(" OR ".join(filters))

            finalLayer = self.nmRelation().referencedLayer()
            for finalFeature in finalLayer.getFeatures(nmRequest):
                treeWidgetItem = QTreeWidgetItem(self.mFeaturesTreeWidget, [
                    QgsVectorLayerUtils.getFeatureDisplayString(
                        finalLayer, finalFeature)
                ])
                treeWidgetItem.setData(0, TreeWidgetItemRole.Type,
                                       TreeWidgetItemType.Feature)
                treeWidgetItem.setData(0, TreeWidgetItemRole.Layer, finalLayer)
                treeWidgetItem.setData(0, TreeWidgetItemRole.Feature, feature)

    def updateUiManyToOnePolymorphic(self):
        layerFeature = dict()
        for relation in self._polymorphicRelation.generateRelations():
            layer = relation.referencingLayer()
            request = relation.getRelatedFeaturesRequest(self.feature())
            finalLayer = relation.referencedLayer()
            for feature in layer.getFeatures(request):
                if finalLayer in layerFeature:
                    layerFeature[finalLayer].append(feature)
                else:
                    layerFeature[finalLayer] = [feature]

        for layer in layerFeature:
            treeWidgetItemLayer = QTreeWidgetItem(self.mFeaturesTreeWidget,
                                                  [layer.name()])
            treeWidgetItemLayer.setData(0, TreeWidgetItemRole.Type,
                                        TreeWidgetItemType.Layer)
            treeWidgetItemLayer.setData(0, TreeWidgetItemRole.Layer, layer)
            treeWidgetItemLayer.setIcon(0, QgsIconUtils.iconForLayer(layer))
            for feature in layerFeature[layer]:
                treeWidgetItem = QTreeWidgetItem(treeWidgetItemLayer, [
                    QgsVectorLayerUtils.getFeatureDisplayString(
                        layerFeature, feature)
                ])
                treeWidgetItem.setData(0, TreeWidgetItemRole.Type,
                                       TreeWidgetItemType.Feature)
                treeWidgetItem.setData(0, TreeWidgetItemRole.Layer, layer)
                treeWidgetItem.setData(0, TreeWidgetItemRole.Feature, feature)
            treeWidgetItemLayer.setExpanded(True)

    def updateUiManyToManyPolymorphic(self):
        layer = self.relation().referencingLayer()
        request = self.relation().getRelatedFeaturesRequest(self.feature())
        layerFeature = dict()
        for linkFeature in layer.getFeatures(request):
            for relation in self._polymorphicRelation.generateRelations():
                referencedFeatureRequest = relation.getReferencedFeatureRequest(
                    linkFeature)
                filterExpression = referencedFeatureRequest.filterExpression()
                nmRequest = QgsFeatureRequest()
                nmRequest.setFilterExpression(filterExpression.expression())

                finalLayer = relation.referencedLayer()
                for finalFeature in finalLayer.getFeatures(nmRequest):
                    features = finalFeature, linkFeature
                    if finalLayer in layerFeature:
                        layerFeature[finalLayer].append(features)
                    else:
                        layerFeature[finalLayer] = [features]

        for layer in layerFeature:
            treeWidgetItemLayer = QTreeWidgetItem(self.mFeaturesTreeWidget,
                                                  [layer.name()])
            treeWidgetItemLayer.setData(0, TreeWidgetItemRole.Type,
                                        TreeWidgetItemType.Layer)
            treeWidgetItemLayer.setData(0, TreeWidgetItemRole.Layer, layer)
            treeWidgetItemLayer.setIcon(0, QgsIconUtils.iconForLayer(layer))
            for feature, linkFeature in layerFeature[layer]:
                treeWidgetItem = QTreeWidgetItem(treeWidgetItemLayer, [
                    QgsVectorLayerUtils.getFeatureDisplayString(
                        layer, feature)
                ])
                treeWidgetItem.setData(0, TreeWidgetItemRole.Type,
                                       TreeWidgetItemType.Feature)
                treeWidgetItem.setData(0, TreeWidgetItemRole.Layer, layer)
                treeWidgetItem.setData(0, TreeWidgetItemRole.Feature, feature)
                treeWidgetItem.setData(0, TreeWidgetItemRole.LinkFeature,
                                       linkFeature)
            treeWidgetItemLayer.setExpanded(True)

    def afterSetRelations(self):
        self._nmRelation = QgsProject.instance().relationManager().relation(
            str(self.nmRelationId()))

        self._setCardinality()

        self._checkTransactionGroup()

        if self.relation().isValid():
            self.relation().referencingLayer().editingStopped.connect(
                self.updateButtons)
            self.relation().referencingLayer().editingStarted.connect(
                self.updateButtons)

        if self.nmRelation().isValid():
            self.nmRelation().referencedLayer().editingStarted.connect(
                self.updateButtons)
            self.nmRelation().referencedLayer().editingStopped.connect(
                self.updateButtons)

        self.updateButtons()

    def parentFormValueChanged(self, attribute, newValue):
        self.updateUi()

    def checkLayerEditingMode(self):

        if self.relation().referencingLayer().isEditable() is False:
            QMessageBox.critical(
                self, self.tr("Layer not editable"),
                self.tr("Layer '{0}' is not in editing mode.").format(
                    self.relation().referencingLayer().name()))
            return False

        if self.nmRelation().isValid():
            if self.nmRelation().referencedLayer().isEditable() is False:
                QMessageBox.critical(
                    self, self.tr("Layer not editable"),
                    self.tr("Layer '{0}' is not in editing mode.").format(
                        self.nmRelation().referencedLayer().name()))
                return False

        return True

    def _setCardinality(self):

        if (self.nmRelation().isValid() is False
                and self.polymorphicRelationEnabled is False):
            self.cardinality = Cardinality.ManyToOne
            return

        elif (self.nmRelation().isValid()
              and self.polymorphicRelationEnabled is False):
            self.cardinality = Cardinality.ManyToMany
            return

        elif (self.polymorphicRelationEnabled
              and self._polymorphicRelation.referencingLayer().id()
              == self.relation().referencedLayer().id()):
            self.cardinality = Cardinality.ManyToOnePolymorphic
            return

        elif (self.polymorphicRelationEnabled
              and self._polymorphicRelation.referencingLayer().id()
              == self.relation().referencingLayer().id()):
            self.cardinality = Cardinality.ManyToManyPolymorphic
            return

        else:
            print("WARNING invalid cardinality set")

    @pyqtSlot(bool)
    def toggleEditing(self, state):

        super().toggleEditing(state)
        self.updateButtons()

    @pyqtSlot()
    def saveChildLayerEdits(self):

        super().saveEdits()

    def actionShowFormTriggered(self):

        if self.mFeaturesTreeWidget.currentItem() is None:
            QMessageBox.critical(self, self.tr("No feature selected"),
                                 self.tr("Please select a feature."))
            return

        if self.mFeaturesTreeWidget.currentItem().data(
                0, TreeWidgetItemRole.Type) != TreeWidgetItemType.Feature:
            QMessageBox.critical(self,
                                 self.tr("Selected item is not a feature"),
                                 self.tr("Please select a feature."))
            return

        showDocumentFormDialog = QgsAttributeDialog(
            self.mFeaturesTreeWidget.currentItem().data(
                0, TreeWidgetItemRole.Layer),
            self.mFeaturesTreeWidget.currentItem().data(
                0, TreeWidgetItemRole.Feature), False, self, True)
        showDocumentFormDialog.exec()
        self.updateUi()

    def actionLinkFeatureTriggered(self):

        if self.checkLayerEditingMode() is False:
            return

        if self.cardinality == Cardinality.ManyToOne:
            self.linkFeature()

        if self.cardinality == Cardinality.ManyToMany:
            self.linkFeature()

        if self.cardinality == Cardinality.ManyToOnePolymorphic:
            self.linkFeatureManyToOnePolymorphic()
            self.updateUi()

        if self.cardinality == Cardinality.ManyToManyPolymorphic:
            self.linkFeatureManyToManyPolymorphic()
            self.updateUi()

    def linkFeatureManyToOnePolymorphic(self):
        print("link ManyToOnePolymorphic")

    def linkFeatureManyToManyPolymorphic(self):

        nmRelations = dict()
        for relation in self._polymorphicRelation.generateRelations():
            nmRelations[relation.referencedLayer().name()] = relation

        layerName, ok = QInputDialog.getItem(self,
                                             self.tr("Please selct a layer"),
                                             self.tr("Layer:"),
                                             nmRelations.keys())

        if not ok:
            return

        nmRelation = nmRelations[layerName]
        selectionDlg = QgsFeatureSelectionDlg(nmRelation.referencedLayer(),
                                              self.editorContext(), self)
        selectionDlg.setWindowTitle(
            self.tr("Please select the features to link. Layer: {0}").format(
                layerName))
        if not selectionDlg.exec():
            return

        # Fields of the linking table
        fields = self.relation().referencingLayer().fields()

        linkAttributes = dict()
        linkAttributes[fields.indexFromName(
            self._polymorphicRelation.referencedLayerField(
            ))] = self._polymorphicRelation.layerRepresentation(
                nmRelation.referencedLayer())
        for key in self.relation().fieldPairs():
            linkAttributes[fields.indexOf(key)] = self.feature().attribute(
                self.relation().fieldPairs()[key])

        # Expression context for the linking table
        context = self.relation().referencingLayer().createExpressionContext()

        featureIterator = nmRelation.referencedLayer().getFeatures(
            QgsFeatureRequest().setFilterFids(
                selectionDlg.selectedFeatures()).setSubsetOfAttributes(
                    nmRelation.referencedFields()))
        relatedFeature = QgsFeature()
        newFeatures = []
        while featureIterator.nextFeature(relatedFeature):
            for key in nmRelation.fieldPairs():
                linkAttributes[fields.indexOf(key)] = relatedFeature.attribute(
                    nmRelation.fieldPairs()[key])

            linkFeature = QgsVectorLayerUtils.createFeature(
                self.relation().referencingLayer(), QgsGeometry(),
                linkAttributes, context)
            newFeatures.append(linkFeature)

        self.relation().referencingLayer().addFeatures(newFeatures)
        ids = []
        for feature in newFeatures:
            ids.append(feature.id())
        self.relation().referencingLayer().selectByIds(ids)

    def actionUnlinkFeatureTriggered(self):

        if self.checkLayerEditingMode() is False:
            return

        if self.mFeaturesTreeWidget.currentItem() is None:
            QMessageBox.critical(self, self.tr("No feature selected"),
                                 self.tr("Please select a feature to unlink."))
            return

        if self.mFeaturesTreeWidget.currentItem().data(
                0, TreeWidgetItemRole.Type) != TreeWidgetItemType.Feature:
            QMessageBox.critical(self,
                                 self.tr("Selected item is not a feature"),
                                 self.tr("Please select a feature."))
            return

        if self.cardinality == Cardinality.ManyToOne:
            self.unlinkFeature(self.mFeaturesTreeWidget.currentItem().data(
                0, TreeWidgetItemRole.Feature).id())

        if self.cardinality == Cardinality.ManyToMany:
            self.unlinkFeature(self.mFeaturesTreeWidget.currentItem().data(
                0, TreeWidgetItemRole.Feature).id())

        if self.cardinality == Cardinality.ManyToOnePolymorphic:
            print("unlink ManyToOnePolymorphic")

        if self.cardinality == Cardinality.ManyToManyPolymorphic:
            self.relation().referencingLayer().deleteFeature(
                self.mFeaturesTreeWidget.currentItem().data(
                    0, TreeWidgetItemRole.LinkFeature).id())
            self.updateUi()

    def updateButtons(self):

        toggleEditingButtonEnabled = False
        editable = False
        linkable = False
        spatial = False

        selectionNotEmpty = True
        if (self.mFeaturesTreeWidget.currentItem() is None
                or self.mFeaturesTreeWidget.currentItem().data(
                    0, TreeWidgetItemRole.Type) != TreeWidgetItemType.Feature):
            selectionNotEmpty = False

        if self.relation().isValid():
            toggleEditingButtonEnabled = self.relation().referencingLayer(
            ).supportsEditing()
            editable = self.relation().referencingLayer().isEditable()
            linkable = self.relation().referencingLayer().isEditable()
            spatial = self.relation().referencingLayer().isSpatial()

        if self.nmRelation().isValid():
            toggleEditingButtonEnabled |= self.nmRelation().referencedLayer(
            ).supportsEditing()
            editable = self.nmRelation().referencedLayer().isEditable()
            spatial = self.nmRelation().referencedLayer().isSpatial()

        self.actionToggleEditing.setEnabled(toggleEditingButtonEnabled)
        self.actionLinkFeature.setEnabled(linkable)
        self.actionUnlinkFeature.setEnabled(linkable and selectionNotEmpty)
        self.actionToggleEditing.setChecked(editable)
        self.actionSaveEdits.setEnabled(editable or linkable)

        self.actionShowForm.setEnabled(selectionNotEmpty)

        self.mToggleEditingToolButton.setVisible(
            self._layerInSameTransactionGroup is False)
        self.mSaveEditsToolButton.setVisible(
            self._layerInSameTransactionGroup is False)

    def _checkTransactionGroup(self):

        self._layerInSameTransactionGroup = False
        connectionString = PluginHelper.connectionString(
            self.relation().referencedLayer().source())
        transactionGroup = QgsProject.instance().transactionGroup(
            self.relation().referencedLayer().providerType(), connectionString)

        if transactionGroup is None:
            self.updateButtons()
            return

        if self.nmRelation().isValid():
            if (self.relation().referencedLayer() in transactionGroup.layers()
                    and self.relation().referencingLayer()
                    in transactionGroup.layers()
                    and self.nmRelation().referencedLayer()
                    in transactionGroup.layers()):
                self._layerInSameTransactionGroup = True
        else:
            if (self.relation().referencedLayer() in transactionGroup.layers()
                    and self.relation().referencingLayer()
                    in transactionGroup.layers()):
                self._layerInSameTransactionGroup = True
예제 #32
0
class TimeManagerGuiControl(QObject):
    """This class controls all plugin-related GUI elements. Emitted signals are defined here."""

    showOptions = pyqtSignal()
    signalExportVideo = pyqtSignal(str, int, bool, bool, bool)
    toggleTime = pyqtSignal()
    toggleArchaeology = pyqtSignal()
    back = pyqtSignal()
    forward = pyqtSignal()
    play = pyqtSignal()
    signalCurrentTimeUpdated = pyqtSignal(QDateTime)
    signalSliderTimeChanged = pyqtSignal(float)
    signalTimeFrameType = pyqtSignal(str)
    signalTimeFrameSize = pyqtSignal(int)
    signalSaveOptions = pyqtSignal()
    signalArchDigitsSpecified = pyqtSignal(int)
    signalArchCancelled = pyqtSignal()

    def __init__(self, iface, model):
        """Initialize the GUI control"""
        QObject.__init__(self)
        self.iface = iface
        self.model = model
        self.optionsDialog = None
        self.path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ui')

        # load the form
        self.dock = uic.loadUi(os.path.join(self.path, DOCK_WIDGET_FILE))
        self.iface.addDockWidget(Qt.BottomDockWidgetArea, self.dock)

        self.dock.pushButtonExportVideo.setEnabled(False)  # only enabled if there are managed layers
        self.dock.pushButtonOptions.clicked.connect(self.optionsClicked)
        self.dock.pushButtonExportVideo.clicked.connect(self.exportVideoClicked)
        self.dock.pushButtonToggleTime.clicked.connect(self.toggleTimeClicked)
        self.dock.pushButtonArchaeology.clicked.connect(self.archaeologyClicked)
        self.dock.pushButtonBack.clicked.connect(self.backClicked)
        self.dock.pushButtonForward.clicked.connect(self.forwardClicked)
        self.dock.pushButtonPlay.clicked.connect(self.playClicked)
        self.dock.dateTimeEditCurrentTime.dateTimeChanged.connect(self.currentTimeChangedDateText)
        # self.dock.horizontalTimeSlider.valueChanged.connect(self.currentTimeChangedSlider)

        self.sliderTimer = QTimer(self)
        self.sliderTimer.setInterval(250)
        self.sliderTimer.setSingleShot(True)
        self.sliderTimer.timeout.connect(self.currentTimeChangedSlider)
        self.dock.horizontalTimeSlider.valueChanged.connect(self.startTimer)

        self.dock.comboBoxTimeExtent.currentIndexChanged[str].connect(self.currentTimeFrameTypeChanged)
        self.dock.spinBoxTimeExtent.valueChanged.connect(self.currentTimeFrameSizeChanged)

        # this signal is responsible for rendering the label
        self.iface.mapCanvas().renderComplete.connect(self.renderLabel)

        # create shortcuts
        self.focusSC = QShortcut(QKeySequence("Ctrl+Space"), self.dock)
        self.focusSC.activated.connect(self.dock.horizontalTimeSlider.setFocus)

        # put default values
        self.dock.horizontalTimeSlider.setMinimum(conf.MIN_TIMESLIDER_DEFAULT)
        self.dock.horizontalTimeSlider.setMaximum(conf.MAX_TIMESLIDER_DEFAULT)
        self.dock.dateTimeEditCurrentTime.setMinimumDate(MIN_QDATE)
        self.showLabel = conf.DEFAULT_SHOW_LABEL
        self.exportEmpty = conf.DEFAULT_EXPORT_EMPTY
        self.labelOptions = TimestampLabelConfig(self.model)

        # placeholders for widgets that are added dynamically
        self.bcdateSpinBox = None

        # add to plugins toolbar
        try:
            self.action = QAction(QCoreApplication.translate("TimeManagerGuiControl", "Toggle visibility"), self.iface.mainWindow())
            self.action.triggered.connect(self.toggleDock)
            self.iface.addPluginToMenu(QCoreApplication.translate("TimeManagerGuiControl", "&TimeManager"), self.action)
        except Exception as e:
            pass  # OK for testing

    def startTimer(self):
        self.sliderTimer.start()

    def getLabelFormat(self):
        return self.labelOptions.fmt

    def getLabelFont(self):
        return self.labelOptions.font

    def getLabelSize(self):
        return self.labelOptions.size

    def getLabelColor(self):
        return self.labelOptions.color

    def getLabelBgColor(self):
        return self.labelOptions.bgcolor

    def getLabelPlacement(self):
        return self.labelOptions.placement

    def setLabelFormat(self, fmt):
        if not fmt:
            return
        self.labelOptions.fmt = fmt

    def setLabelFont(self, font):
        if not font:
            return
        self.labelOptions.font = font

    def setLabelSize(self, size):
        if not size:
            return
        self.labelOptions.size = size

    def setLabelColor(self, color):
        if not color:
            return
        self.labelOptions.color = color

    def setLabelBgColor(self, bgcolor):
        if not bgcolor:
            return
        self.labelOptions.bgcolor = bgcolor

    def setLabelPlacement(self, placement):
        if not placement:
            return
        self.labelOptions.placement = placement

    def toggleDock(self):
        self.dock.setVisible(not self.dock.isVisible())

    def getOptionsDialog(self):
        return self.optionsDialog

    def showAnimationOptions(self):
        self.animationDialog = uic.loadUi(os.path.join(self.path, ANIMATION_WIDGET_FILE))

        def selectFile():
            self.animationDialog.lineEdit.setText(QFileDialog.getOpenFileName())

        self.animationDialog.pushButton.clicked.connect(self.selectAnimationFolder)
        self.animationDialog.buttonBox.accepted.connect(self.sendAnimationOptions)
        self.animationDialog.show()

    def selectAnimationFolder(self):
        prev_directory = TimeManagerProjectHandler.plugin_setting(conf.LAST_ANIMATION_PATH_TAG)
        if prev_directory:
            self.animationDialog.lineEdit.setText(QFileDialog.getExistingDirectory(directory=prev_directory))
        else:
            self.animationDialog.lineEdit.setText(QFileDialog.getExistingDirectory())

    def sendAnimationOptions(self):
        path = self.animationDialog.lineEdit.text()
        if path == "":
            self.showAnimationOptions()
        TimeManagerProjectHandler.set_plugin_setting(conf.LAST_ANIMATION_PATH_TAG, path)
        delay_millis = self.animationDialog.spinBoxDelay.value()
        export_gif = self.animationDialog.radioAnimatedGif.isChecked()
        export_video = False  # self.animationDialog.radioVideo.isChecked()
        do_clear = self.animationDialog.clearCheckBox.isChecked()
        self.signalExportVideo.emit(path, delay_millis, export_gif, export_video, do_clear)

    def showLabelOptions(self):
        options = self.labelOptions
        self.dialog = QDialog()

        lo = uic.loadUiType(os.path.join(self.path, LABEL_OPTIONS_WIDGET_FILE))[0]
        self.labelOptionsDialog = lo()
        self.labelOptionsDialog.setupUi(self.dialog)

        self.labelOptionsDialog.fontsize.setValue(options.size)
        self.labelOptionsDialog.time_format.setText(options.fmt)
        self.labelOptionsDialog.font.setCurrentFont(QFont(options.font))
        self.labelOptionsDialog.placement.addItems(TimestampLabelConfig.PLACEMENTS)
        self.labelOptionsDialog.placement.setCurrentIndex(TimestampLabelConfig.PLACEMENTS.index(options.placement))
        self.labelOptionsDialog.text_color.setColor(QColor(options.color))
        self.labelOptionsDialog.bg_color.setColor(QColor(options.bgcolor))
        self.labelOptionsDialog.buttonBox.accepted.connect(self.saveLabelOptions)
        self.dialog.show()

    def saveLabelOptions(self):
        self.labelOptions.font = self.labelOptionsDialog.font.currentFont().family()
        self.labelOptions.size = self.labelOptionsDialog.fontsize.value()
        self.labelOptions.bgcolor = self.labelOptionsDialog.bg_color.color().name()
        self.labelOptions.color = self.labelOptionsDialog.text_color.color().name()
        self.labelOptions.placement = self.labelOptionsDialog.placement.currentText()
        self.labelOptions.fmt = self.labelOptionsDialog.time_format.text()
        if self.labelOptionsDialog.radioButton_dt.isChecked():
            self.labelOptions.type = "dt"
        if self.labelOptionsDialog.radioButton_beginning.isChecked():
            self.labelOptions.type = "beginning"
        if self.labelOptionsDialog.radioButton_epoch.isChecked():
            self.labelOptions.type = "epoch"

    def enableArchaeologyTextBox(self):
        self.dock.dateTimeEditCurrentTime.dateTimeChanged.connect(self.currentTimeChangedDateText)
        if self.bcdateSpinBox is None:
            self.bcdateSpinBox = self.createBCWidget(self.dock)
            self.bcdateSpinBox.editingFinished.connect(self.currentBCYearChanged)
        self.replaceWidget(self.dock.horizontalLayout, self.dock.dateTimeEditCurrentTime, self.bcdateSpinBox, 5)

    def getTimeWidget(self):
        if time_util.is_archaelogical():
            return self.bcdateSpinBox
        else:
            return self.dock.dateTimeEditCurrentTime

    def currentBCYearChanged(self):
        val = self.bcdateSpinBox.text()
        try:
            bcdate_util.BCDate.from_str(val, strict_zeros=False)
            self.signalCurrentTimeUpdated.emit(val)
        except Exception as e:
            warn("Invalid bc date: {}".format(val))  # how to mark as such?
            return

    def disableArchaeologyTextBox(self):
        if self.bcdateSpinBox is None:
            return
        self.replaceWidget(self.dock.horizontalLayout, self.bcdateSpinBox, self.dock.dateTimeEditCurrentTime, 5)

    def createBCWidget(self, mainWidget):
        newWidget = QLineEdit(mainWidget)  # QtGui.QSpinBox(mainWidget)
        # newWidget.setMinimum(-1000000)
        # newWidget.setValue(-1)
        newWidget.setText("0001 BC")
        return newWidget

    def replaceWidget(self, layout, oldWidget, newWidget, idx):
        """
        Replaces oldWidget with newWidget at layout at index idx
        The way it is done, the widget is not destroyed and the connections to it remain
        """

        layout.removeWidget(oldWidget)
        oldWidget.close()  # I wonder if this has any memory leaks? </philosoraptor>
        layout.insertWidget(idx, newWidget)
        newWidget.show()
        layout.update()

    def optionsClicked(self):
        self.showOptions.emit()

    def exportVideoClicked(self):
        self.showAnimationOptions()

    def toggleTimeClicked(self):
        self.toggleTime.emit()

    def archaeologyClicked(self):
        self.toggleArchaeology.emit()

    def showArchOptions(self):
        self.archMenu = uic.loadUi(os.path.join(self.path, ARCH_WIDGET_FILE))
        self.archMenu.buttonBox.accepted.connect(self.saveArchOptions)
        self.archMenu.buttonBox.rejected.connect(self.cancelArch)
        self.archMenu.show()

    def saveArchOptions(self):
        self.signalArchDigitsSpecified.emit(self.archMenu.numDigits.value())

    def cancelArch(self):
        self.signalArchCancelled.emit()

    def backClicked(self):
        self.back.emit()

    def forwardClicked(self):
        self.forward.emit()

    def playClicked(self):
        if self.dock.pushButtonPlay.isChecked():
            self.dock.pushButtonPlay.setIcon(QIcon("TimeManager:ui/images/pause.png"))
        else:
            self.dock.pushButtonPlay.setIcon(QIcon("TimeManager:ui/images/play.png"))
        self.play.emit()

    def currentTimeChangedSlider(self):
        sliderVal = self.dock.horizontalTimeSlider.value()
        try:
            pct = (sliderVal - self.dock.horizontalTimeSlider.minimum()) * 1.0 / (
                self.dock.horizontalTimeSlider.maximum() - self.dock.horizontalTimeSlider.minimum())
        except Exception as e:
            # slider is not properly initialized yet
            return
        if self.model.getActiveDelimitedText() and qgs.getVersion() < conf.MIN_DTEXT_FIXED:
            time.sleep(0.1)  # hack to fix issue in qgis core with delimited text which was fixed in 2.9
        self.signalSliderTimeChanged.emit(pct)

    def currentTimeChangedDateText(self, qdate):
        # info("changed time via text")
        self.signalCurrentTimeUpdated.emit(qdate)

    def currentTimeFrameTypeChanged(self, frameType):
        self.signalTimeFrameType.emit(frameType)

    def currentTimeFrameSizeChanged(self, frameSize):
        if frameSize < 1:  # time frame size = 0  is meaningless
            self.dock.spinBoxTimeExtent.setValue(1)
            return
        self.signalTimeFrameSize.emit(frameSize)

    def unload(self):
        """Unload the plugin"""
        self.iface.removeDockWidget(self.dock)
        self.iface.removePluginMenu("TimeManager", self.action)

    def setWindowTitle(self, title):
        self.dock.setWindowTitle(title)

    def showOptionsDialog(self, layerList, animationFrameLength, playBackwards=False, loopAnimation=False):
        """Show the optionsDialog and populate it with settings from timeLayerManager"""

        # load the form
        self.optionsDialog = uic.loadUi(os.path.join(self.path, OPTIONS_WIDGET_FILE))

        # restore settings from layerList:
        for layer in layerList:
            settings = layer_settings.getSettingsFromLayer(layer)
            layer_settings.addSettingsToRow(settings, self.optionsDialog.tableWidget)
        self.optionsDialog.tableWidget.resizeColumnsToContents()

        # restore animation options
        self.optionsDialog.spinBoxFrameLength.setValue(animationFrameLength)
        self.optionsDialog.checkBoxBackwards.setChecked(playBackwards)
        self.optionsDialog.checkBoxLabel.setChecked(self.showLabel)
        self.optionsDialog.checkBoxDontExportEmpty.setChecked(not self.exportEmpty)
        self.optionsDialog.checkBoxLoop.setChecked(loopAnimation)
        self.optionsDialog.show_label_options_button.clicked.connect(self.showLabelOptions)
        self.optionsDialog.checkBoxLabel.stateChanged.connect(self.showOrHideLabelOptions)

        self.optionsDialog.textBrowser.setHtml(QCoreApplication.translate('TimeManager', """\
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
<html>
<head>
    <meta name="qrichtext" content="1"/>

    <style>
    li.mono {
    font-family: Consolas, Courier New, Courier, monospace;
}
    </style>
</head>
<body>

<h1>Time Manager</h1>

<p>Time Manager filters your layers and displays only layers and features that match the specified time frame. Time Manager supports vector layers and raster layers (including WMS with TIME dimension).</p>

<p>Timestamps have to be in one of the following formats:</p>

<ul>
<li class="mono">%Y-%m-%d %H:%M:%S.%f</li>
<li class="mono">%Y-%m-%d %H:%M:%S</li>
<li class="mono">%Y-%m-%d %H:%M</li>
<li class="mono">%Y-%m-%dT%H:%M:%S</li>
<li class="mono">%Y-%m-%dT%H:%M:%SZ</li>
<li class="mono">%Y-%m-%dT%H:%M</li>
<li class="mono">%Y-%m-%dT%H:%MZ</li>
<li class="mono">%Y-%m-%d</li>
<li class="mono">%Y/%m/%d %H:%M:%S.%f</li>
<li class="mono">%Y/%m/%d %H:%M:%S</li>
<li class="mono">%Y/%m/%d %H:%M</li>
<li class="mono">%Y/%m/%d</li>
<li class="mono">%H:%M:%S</li>
<li class="mono">%H:%M:%S.%f</li>
<li class="mono">%Y.%m.%d %H:%M:%S.%f</li>
<li class="mono">%Y.%m.%d %H:%M:%S</li>
<li class="mono">%Y.%m.%d %H:%M</li>
<li class="mono">%Y.%m.%d</li>
<li class="mono">%Y%m%d%H%M%SED</li>
<li>Integer timestamp in seconds after or before the epoch (1970-1-1)</li>
</ul>


<p>The layer list contains all layers managed by Time Manager.
To add a vector layer, press [Add layer].
To add a raster layer, press [Add raster].
If you want to remove a layer from the list, select it and press [Remove layer].</p>


<p>Below the layer list, you'll find the following <b>animation options</b>:</p>

<p><b>Show frame for x milliseconds</b>...
allows you to adjust for how long a frame will be visible during the animation</p>

<p><b>Play animation backwards</b>...
if checked, the animation will run in reverse direction</p>

<p><b>Display frame start time on map</b>...
if checked, displays the start time of the visible frame in the lower right corner of the map</p>

<h2>Add Layer dialog</h2>

<p>Here, you are asked to select the layer that should be added and specify the columns containing
start and (optionally) end time.</p>

<p>The <b>offset</b> option allows you to further time the appearance of features. If you specify
an offset of -1, the features will appear one second later than they would by default.</p>

<h2>Dock widget</h2>

<p>The dock was designed to attach to the bottom of the QGIS main window. It offers the following tools:</p>

<ul>
<li><img src="images/power_on.png" alt="power"/> ... On/Off switch, allows you to turn Time Manager's functionality on/off with the click of only one button</li>
<li><span class="hidden">[Settings]</span><input type="button" value="Settings"/> ... opens the Settings dialog where you can manage your spatio-temporal layers</li>
<li><span class="hidden">[Export Video]</span><input type="button" value="Export Video"/> ... exports an image series based on current settings (This button is only enabled if there are layers registered in Time Manager "Settings")</li>
<li><b>Time frame start: <span class="hidden">[2000-01-01 00:00:00]</span></b><input type="text" value="2000-01-01 00:00:00"/> ... shows the start time of the currently active frame. Allows you to precisely specify your desired analysis time.</li>
<li><b>Time frame size: </b><input type="text" value="1"/><span class="hidden">[x]</span><select><option value="days">days</option></select> ... allow you to choose the size of the time frame</li>
<li><img src="images/back.png" alt="back"/> ... go to the previous time frame</li>
<li><img src="images/forward.png" alt="forward"/> ... go to the next time frame</li>
<li><b>Slider</b> ... shows the position of current frame relative to the whole dataset and allows you to seamlessly scroll through the dataset</li>
<li><img src="images/play.png" alt="play"/> ... start an automatic animation based on your current settings</li>
</ul>

</body>
</html>"""))

        # show dialog
        self.showOrHideLabelOptions()
        self.optionsDialog.show()

        # create raster and vector dialogs
        self.vectorDialog = VectorLayerDialog(self.iface, os.path.join(self.path, ADD_VECTOR_LAYER_WIDGET_FILE),
                                              self.optionsDialog.tableWidget)
        self.rasterDialog = RasterLayerDialog(self.iface, os.path.join(self.path, ADD_RASTER_LAYER_WIDGET_FILE),
                                              self.optionsDialog.tableWidget)
        # establish connections
        self.optionsDialog.pushButtonAddVector.clicked.connect(self.vectorDialog.show)
        self.optionsDialog.pushButtonAddRaster.clicked.connect(self.rasterDialog.show)
        self.optionsDialog.pushButtonRemove.clicked.connect(self.removeLayer)
        self.optionsDialog.buttonBox.accepted.connect(self.saveOptions)
        # self.optionsDialog.buttonBox.helpRequested.connect(self.showHelp)

    def showOrHideLabelOptions(self):
        self.optionsDialog.show_label_options_button.setEnabled(
            self.optionsDialog.checkBoxLabel.isChecked())

    def saveOptions(self):
        """Save the options from optionsDialog to timeLayerManager"""
        self.signalSaveOptions.emit()

    def removeLayer(self):
        """Remove currently selected layer (= row) from options"""
        currentRow = self.optionsDialog.tableWidget.currentRow()
        try:
            layerName = self.optionsDialog.tableWidget.item(currentRow, 0).text()
        except AttributeError:  # if no row is selected
            return
        if QMessageBox.question(self.optionsDialog,
                                QCoreApplication.translate("TimeManagerGuiControl", "Remove Layer"),
                                QCoreApplication.translate("TimeManagerGuiControl", "Do you really want to remove layer {}?").format(layerName),
                                QMessageBox.Ok, QMessageBox.Cancel) == QMessageBox.Ok:
            self.optionsDialog.tableWidget.removeRow(self.optionsDialog.tableWidget.currentRow())

    def disableAnimationExport(self):
        """Disable the animation export button"""
        self.dock.pushButtonExportVideo.setEnabled(False)

    def enableAnimationExport(self):
        """Enable animation export button"""
        self.dock.pushButtonExportVideo.setEnabled(True)

    def refreshMapCanvas(self, sender=None):
        """Refresh the map canvas"""
        # QMessageBox.information(self.iface.mainWindow(),'Test Output','Refresh!\n'+str(sender))
        self.iface.mapCanvas().refresh()

    def setTimeFrameSize(self, frameSize):
        """Set spinBoxTimeExtent to given frameSize"""
        self.dock.spinBoxTimeExtent.setValue(frameSize)

    def setTimeFrameType(self, frameType):
        """Set comboBoxTimeExtent to given frameType"""
        i = self.dock.comboBoxTimeExtent.findText(frameType)
        self.dock.comboBoxTimeExtent.setCurrentIndex(i)

    def setActive(self, isActive):
        """Toggle pushButtonToggleTime"""
        self.dock.pushButtonToggleTime.setChecked(isActive)

    def setArchaeologyPressed(self, isActive):
        """Toggle pushButtonArchaeology"""
        self.dock.pushButtonArchaeology.setChecked(isActive)

    def addActionShowSettings(self, action):
        """Add action to pushButttonOptions"""
        self.dock.pushButtonOptions.addAction(action)

    def turnPlayButtonOff(self):
        """Turn pushButtonPlay off"""
        if self.dock.pushButtonPlay.isChecked():
            self.dock.pushButtonPlay.toggle()
            self.dock.pushButtonPlay.setIcon(QIcon("TimeManager:ui/images/play.png"))

    def renderLabel(self, painter):
        """Render the current timestamp on the map canvas"""
        if not self.showLabel or not self.model.hasLayers() or not self.dock.pushButtonToggleTime.isChecked():
            return

        dt = self.model.getCurrentTimePosition()
        if dt is None:
            return

        labelString = self.labelOptions.getLabel(dt)

        # Determine placement of label given cardinal directions
        flags = 0
        for direction, flag in ('N', Qt.AlignTop), ('S', Qt.AlignBottom), ('E', Qt.AlignRight), ('W', Qt.AlignLeft):
            if direction in self.labelOptions.placement:
                flags |= flag

        # Get canvas dimensions
        pixelRatio = painter.device().devicePixelRatio()
        width = painter.device().width() / pixelRatio
        height = painter.device().height() / pixelRatio

        painter.setRenderHint(painter.Antialiasing, True)
        txt = QTextDocument()
        html = """<span style="background-color:%s; padding: 5px; font-size: %spx;">
                    <font face="%s" color="%s">&nbsp;%s</font>
                  </span> """\
               % (self.labelOptions.bgcolor, self.labelOptions.size, self.labelOptions.font,
                  self.labelOptions.color, labelString)
        txt.setHtml(html)
        layout = txt.documentLayout()
        size = layout.documentSize()

        if flags & Qt.AlignRight:
            x = width - 5 - size.width()
        elif flags & Qt.AlignLeft:
            x = 5
        else:
            x = width / 2 - size.width() / 2

        if flags & Qt.AlignBottom:
            y = height - 5 - size.height()
        elif flags & Qt.AlignTop:
            y = 5
        else:
            y = height / 2 - size.height() / 2

        painter.translate(x, y)
        layout.draw(painter, QAbstractTextDocumentLayout.PaintContext())
        painter.translate(-x, -y)  # translate back

    def repaintRasters(self):
        rasters = self.model.getActiveRasters()
        list([x.layer.triggerRepaint() for x in rasters])

    def repaintVectors(self):
        list([x.layer.triggerRepaint() for x in self.model.getActiveVectors()])

    def repaintJoined(self):
        layerIdsToRefresh = qgs.getAllJoinedLayers(set([x.layer.id() for x in self.model.getActiveVectors()]))
        # info("to refresh {}".format(layerIdsToRefresh))
        layersToRefresh = [qgs.getLayerFromId(x) for x in layerIdsToRefresh]
        list([x.triggerRepaint() for x in layersToRefresh])
class DatasetManagerDialog(QDialog, DIALOG_UI):
    def __init__(self, iface, parent=None, wizard_embedded=False):

        QDialog.__init__(self, parent)
        self.iface = iface
        self._close_editing()

        self.setupUi(self)
        self.buttonBox.accepted.connect(self._accepted)
        self.buttonBox.rejected.connect(self._rejected)

        self.type_combo_box.clear()
        self._lst_panel = dict()
        self.db_simple_factory = DbSimpleFactory()

        for db_id in self.db_simple_factory.get_db_list(False):
            self.type_combo_box.addItem(displayDbIliMode[db_id], db_id)
            db_factory = self.db_simple_factory.create_factory(db_id)
            item_panel = db_factory.get_config_panel(self, DbActionType.EXPORT)
            self._lst_panel[db_id] = item_panel
            self.db_layout.addWidget(item_panel)

        self.type_combo_box.currentIndexChanged.connect(self._type_changed)

        # when opened by the wizard it uses the current db connection settings and should not be changable
        self.db_frame.setHidden(wizard_embedded)

        self.dataset_model = DatasetModel()
        self.dataset_tableview.horizontalHeader().setSectionResizeMode(
            QHeaderView.Stretch)
        self.dataset_tableview.horizontalHeader().hide()
        self.dataset_tableview.verticalHeader().hide()
        self.dataset_tableview.setSelectionMode(QTableView.SingleSelection)
        self.dataset_tableview.setModel(self.dataset_model)

        self._restore_configuration()

        # refresh the models on changing values but avoid massive db connects by timer
        self.refreshTimer = QTimer()
        self.refreshTimer.setSingleShot(True)
        self.refreshTimer.timeout.connect(
            lambda: self._refresh_datasets(self._updated_configuration()))

        for key, value in self._lst_panel.items():
            value.notify_fields_modified.connect(
                self._request_for_refresh_datasets)

        self._refresh_datasets(self._updated_configuration())

        self.add_button.clicked.connect(self._add_dataset)
        self.edit_button.clicked.connect(self._edit_dataset)
        self.create_baskets_button.clicked.connect(self._create_baskets)
        self.dataset_tableview.selectionModel().selectionChanged.connect(
            lambda: self._enable_dataset_handling(True))

        self.add_button.setIcon(
            QgsApplication.getThemeIcon("/symbologyAdd.svg"))
        self.edit_button.setIcon(
            QgsApplication.getThemeIcon("/symbologyEdit.svg"))

    def _close_editing(self):
        editable_layers = []
        for layer in QgsProject.instance().mapLayers().values():
            if layer.type() == QgsMapLayer.VectorLayer:
                self.iface.vectorLayerTools().stopEditing(layer)
                if layer.isEditable():
                    editable_layers.append(layer)
        if editable_layers:
            warning_box = QMessageBox(self)
            warning_box.setIcon(QMessageBox.Warning)
            warning_title = self.tr("Layers still editable")
            warning_box.setWindowTitle(warning_title)
            warning_box.setText(
                self.
                tr("You still have layers in edit mode.\nIn case you modify datasets on the database of those layers, it could lead to database locks.\nEditable layers are:\n - {}"
                   ).format("\n - ".join(
                       [layer.name() for layer in editable_layers])))
            warning_box.exec_()

    def _valid_selection(self):
        """
        Returns if at least one dataset is selected
        """
        return bool(self.dataset_tableview.selectedIndexes())

    def _enable_dataset_handling(self, enable):
        self.dataset_tableview.setEnabled(enable)
        self.add_button.setEnabled(enable)
        self.edit_button.setEnabled(self._valid_selection())
        self.create_baskets_button.setEnabled(self._valid_selection())

    def _type_changed(self):
        ili_mode = self.type_combo_box.currentData()
        db_id = ili_mode & ~DbIliMode.ili

        self.db_wrapper_group_box.setTitle(displayDbIliMode[db_id])

        # Refresh panels
        for key, value in self._lst_panel.items():
            is_current_panel_selected = db_id == key
            value.setVisible(is_current_panel_selected)
            if is_current_panel_selected:
                value._show_panel()
        self._refresh_datasets(self._updated_configuration())

    def _request_for_refresh_datasets(self):
        # hold refresh back
        self.refreshTimer.start(500)

    def _refresh_datasets(self, configuration):
        db_connector = db_utils.get_db_connector(configuration)
        if db_connector and db_connector.get_basket_handling:
            self._enable_dataset_handling(True)
            return self.dataset_model.refresh_model(db_connector)
        else:
            self._enable_dataset_handling(False)
            return self.dataset_model.clear()

    def _add_dataset(self):
        db_connector = db_utils.get_db_connector(self._updated_configuration())
        if db_connector and db_connector.get_basket_handling:
            edit_dataset_dialog = EditDatasetDialog(self, db_connector)
            edit_dataset_dialog.exec_()
            self._refresh_datasets(self._updated_configuration())
            self._jump_to_entry(edit_dataset_dialog.dataset_line_edit.text())

    def _edit_dataset(self):
        if self._valid_selection():
            db_connector = db_utils.get_db_connector(
                self._updated_configuration())
            if db_connector and db_connector.get_basket_handling:
                dataset = (
                    self.dataset_tableview.selectedIndexes()[0].data(
                        int(DatasetModel.Roles.TID)),
                    self.dataset_tableview.selectedIndexes()[0].data(
                        int(DatasetModel.Roles.DATASETNAME)),
                )
                edit_dataset_dialog = EditDatasetDialog(
                    self, db_connector, dataset)
                edit_dataset_dialog.exec_()
                self._refresh_datasets(self._updated_configuration())
                self._jump_to_entry(
                    edit_dataset_dialog.dataset_line_edit.text())

    def _create_baskets(self):
        if self._valid_selection():
            db_connector = db_utils.get_db_connector(
                self._updated_configuration())
            if db_connector and db_connector.get_basket_handling:
                feedbacks = []
                for record in db_connector.get_topics_info():
                    dataset_tid = self.dataset_tableview.selectedIndexes(
                    )[0].data(int(DatasetModel.Roles.TID))
                    status, message = db_connector.create_basket(
                        dataset_tid,
                        ".".join([record["model"], record["topic"]]))
                    feedbacks.append((status, message))

                info_box = QMessageBox(self)
                info_box.setIcon(QMessageBox.Warning if len(
                    [feedback for feedback in feedbacks
                     if not feedback[0]]) else QMessageBox.Information)
                info_title = self.tr("Created baskets")
                info_box.setWindowTitle(info_title)
                info_box.setText("{}{}".format(
                    "\n".join([feedback[1] for feedback in feedbacks]),
                    "\n\nBe aware that the IDs of the baskets are created as UUIDs. To change that, edit the t_ili2db_basket table manually."
                    if len([feedback for feedback in feedbacks
                            if feedback[0]]) else "",
                ))
                info_box.exec_()

    def _jump_to_entry(self, datasetname):
        matches = self.dataset_model.match(
            self.dataset_model.index(0, 0),
            Qt.DisplayRole,
            datasetname,
            1,
            Qt.MatchExactly,
        )
        if matches:
            self.dataset_tableview.setCurrentIndex(matches[0])
            self.dataset_tableview.scrollTo(matches[0])

    def _restore_configuration(self):
        settings = QSettings()

        for db_id in self.db_simple_factory.get_db_list(False):
            configuration = Ili2DbCommandConfiguration()
            db_factory = self.db_simple_factory.create_factory(db_id)
            config_manager = db_factory.get_db_command_config_manager(
                configuration)
            config_manager.load_config_from_qsettings()
            self._lst_panel[db_id].set_fields(configuration)

        mode = settings.value("QgisModelBaker/importtype")
        mode = DbIliMode[
            mode] if mode else self.db_simple_factory.default_database
        mode = mode & ~DbIliMode.ili

        self.type_combo_box.setCurrentIndex(self.type_combo_box.findData(mode))
        self._type_changed()

    def _updated_configuration(self):
        configuration = Ili2DbCommandConfiguration()

        mode = self.type_combo_box.currentData()
        self._lst_panel[mode].get_fields(configuration)

        configuration.tool = mode
        configuration.db_ili_version = db_utils.db_ili_version(configuration)
        return configuration

    def _save_configuration(self, configuration):
        settings = QSettings()
        settings.setValue("QgisModelBaker/importtype",
                          self.type_combo_box.currentData().name)
        mode = self.type_combo_box.currentData()
        db_factory = self.db_simple_factory.create_factory(mode)
        config_manager = db_factory.get_db_command_config_manager(
            configuration)
        config_manager.save_config_in_qsettings()

    def _accepted(self):
        self._save_configuration(self._updated_configuration())
        self.close()

    def _rejected(self):
        self._restore_configuration()
        self.close()
예제 #34
0
class ExportDialog(QDialog, DIALOG_UI):
    ValidExtensions = ["xtf", "XTF", "itf", "ITF", "gml", "GML", "xml", "XML"]

    def __init__(self, base_config, parent=None):
        QDialog.__init__(self, parent)
        self.setupUi(self)
        self.db_simple_factory = DbSimpleFactory()
        QgsGui.instance().enableAutoGeometryRestore(self)
        self.buttonBox.accepted.disconnect()
        self.buttonBox.clear()
        self.buttonBox.addButton(QDialogButtonBox.Cancel)
        self.buttonBox.addButton(QDialogButtonBox.Help)
        self.buttonBox.helpRequested.connect(self.help_requested)

        self.export_text = self.tr("Export")
        self.set_button_to_export_action = QAction(self.export_text, None)
        self.set_button_to_export_action.triggered.connect(
            self.set_button_to_export)

        self.export_without_validation_text = self.tr(
            "Export without validation")
        self.set_button_to_export_without_validation_action = QAction(
            self.export_without_validation_text, None)
        self.set_button_to_export_without_validation_action.triggered.connect(
            self.set_button_to_export_without_validation)

        self.edit_command_action = QAction(self.tr("Edit ili2db command"),
                                           None)
        self.edit_command_action.triggered.connect(self.edit_command)

        self.export_tool_button.addAction(
            self.set_button_to_export_without_validation_action)
        self.export_tool_button.addAction(self.edit_command_action)
        self.export_tool_button.setText(self.export_text)
        self.export_tool_button.clicked.connect(self.accepted)

        self.xtf_file_browse_button.clicked.connect(
            make_save_file_selector(
                self.xtf_file_line_edit,
                title=self.tr("Save in XTF Transfer File"),
                file_filter=self.
                tr("XTF Transfer File (*.xtf *XTF);;Interlis 1 Transfer File (*.itf *ITF);;XML (*.xml *XML);;GML (*.gml *GML)"
                   ),
                extension=".xtf",
                extensions=["." + ext for ext in self.ValidExtensions],
            ))
        self.xtf_file_browse_button.clicked.connect(
            self.xtf_browser_opened_to_true)
        self.xtf_browser_was_opened = False

        self.type_combo_box.clear()
        self._lst_panel = dict()

        for db_id in self.db_simple_factory.get_db_list(False):
            self.type_combo_box.addItem(displayDbIliMode[db_id], db_id)
            db_factory = self.db_simple_factory.create_factory(db_id)
            item_panel = db_factory.get_config_panel(self, DbActionType.EXPORT)
            self._lst_panel[db_id] = item_panel
            self.db_layout.addWidget(item_panel)

        self.validators = Validators()

        fileValidator = FileValidator(
            pattern=["*." + ext for ext in self.ValidExtensions],
            allow_non_existing=True,
        )

        self.xtf_file_line_edit.setValidator(fileValidator)
        self.xtf_file_line_edit.textChanged.connect(
            self.validators.validate_line_edits)
        self.xtf_file_line_edit.textChanged.connect(
            self.xtf_browser_opened_to_false)
        self.xtf_file_line_edit.textChanged.emit(
            self.xtf_file_line_edit.text())

        # Reset to export as default text
        self.xtf_file_line_edit.textChanged.connect(self.set_button_to_export)

        # refresh the models on changing values but avoid massive db connects by timer
        self.refreshTimer = QTimer()
        self.refreshTimer.setSingleShot(True)
        self.refreshTimer.timeout.connect(self.refresh_models)

        for key, value in self._lst_panel.items():
            value.notify_fields_modified.connect(
                self.request_for_refresh_models)

        self.validate_data = True  # validates exported data by default, We use --disableValidation when is False
        self.base_configuration = base_config
        self.restore_configuration()

        self.export_models_model = ExportModels()
        self.export_models_view.setModel(self.export_models_model)
        self.export_models_view.clicked.connect(self.export_models_model.check)
        self.export_models_view.space_pressed.connect(
            self.export_models_model.check)
        self.request_for_refresh_models()

        self.type_combo_box.currentIndexChanged.connect(self.type_changed)

        self.bar = QgsMessageBar()
        self.bar.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Fixed)
        self.txtStdout.setLayout(QGridLayout())
        self.txtStdout.layout().setContentsMargins(0, 0, 0, 0)
        self.txtStdout.layout().addWidget(self.bar, 0, 0, Qt.AlignTop)

    def request_for_refresh_models(self):
        # hold refresh back
        self.refreshTimer.start(500)

    def refresh_models(self):
        self.refreshed_export_models_model()

    def refreshed_export_models_model(self):
        tool = self.type_combo_box.currentData() & ~DbIliMode.ili

        configuration = self.updated_configuration()
        schema = configuration.dbschema

        db_factory = self.db_simple_factory.create_factory(tool)
        config_manager = db_factory.get_db_command_config_manager(
            configuration)
        uri_string = config_manager.get_uri(configuration.db_use_super_login)

        db_connector = None

        try:
            db_connector = db_factory.get_db_connector(uri_string, schema)
        except (DBConnectorError, FileNotFoundError):
            # when wrong connection parameters entered, there should just be returned an empty model - so let it pass
            pass

        self.export_models_model.refresh_models(db_connector)

    def db_ili_version(self, configuration):
        """
        Returns the ili2db version the database has been created with or None if the database
        could not be detected as a ili2db database
        """
        schema = configuration.dbschema

        db_factory = self.db_simple_factory.create_factory(configuration.tool)
        config_manager = db_factory.get_db_command_config_manager(
            configuration)
        uri_string = config_manager.get_uri(configuration.db_use_super_login)

        db_connector = None

        try:
            db_connector = db_factory.get_db_connector(uri_string, schema)
            return db_connector.ili_version()
        except (DBConnectorError, FileNotFoundError):
            return None

    def set_button_to_export(self):
        """
        Changes the text of the button to export (with validation) and sets the validate_data to true.
        So on clicking the button the export will start with validation.
        The buttons actions are changed to be able to switch the without-validation mode.
        """
        self.validate_data = True
        self.export_tool_button.removeAction(self.set_button_to_export_action)
        self.export_tool_button.removeAction(self.edit_command_action)
        self.export_tool_button.addAction(
            self.set_button_to_export_without_validation_action)
        self.export_tool_button.addAction(self.edit_command_action)
        self.export_tool_button.setText(self.export_text)

    def set_button_to_export_without_validation(self):
        """
        Changes the text of the button to export without validation and sets the validate_data to false.
        So on clicking the button the export will start without validation.
        The buttons actions are changed to be able to switch the with-validation mode.
        """
        self.validate_data = False
        self.export_tool_button.removeAction(
            self.set_button_to_export_without_validation_action)
        self.export_tool_button.removeAction(self.edit_command_action)
        self.export_tool_button.addAction(self.set_button_to_export_action)
        self.export_tool_button.addAction(self.edit_command_action)
        self.export_tool_button.setText(self.export_without_validation_text)

    def edit_command(self):
        """
        A dialog opens giving the user the possibility to edit the ili2db command used for the export
        """
        exporter = iliexporter.Exporter()
        exporter.tool = self.type_combo_box.currentData()
        exporter.configuration = self.updated_configuration()
        command = exporter.command(True)
        edit_command_dialog = EditCommandDialog(self)
        edit_command_dialog.command_edit.setPlainText(command)
        if edit_command_dialog.exec_():
            edited_command = edit_command_dialog.command_edit.toPlainText()
            self.accepted(edited_command)

    def accepted(self, edited_command=None):
        db_id = self.type_combo_box.currentData()

        res, message = self._lst_panel[db_id].is_valid()

        if not res:
            self.txtStdout.setText(message)
            return

        configuration = self.updated_configuration()

        if not edited_command:
            if (not self.xtf_file_line_edit.validator().validate(
                    configuration.xtffile, 0)[0] == QValidator.Acceptable):
                self.txtStdout.setText(
                    self.
                    tr("Please set a valid INTERLIS XTF file before exporting data."
                       ))
                self.xtf_file_line_edit.setFocus()
                return
            if not configuration.ilimodels:
                self.txtStdout.setText(
                    self.tr("Please set a model before exporting data."))
                self.export_models_view.setFocus()
                return

        # If xtf browser was opened and the file exists, the user already chose
        # to overwrite the file
        if (os.path.isfile(self.xtf_file_line_edit.text().strip())
                and not self.xtf_browser_was_opened):
            self.msg = QMessageBox()
            self.msg.setIcon(QMessageBox.Warning)
            self.msg.setText(
                self.tr(
                    "{filename} already exists.\nDo you want to replace it?").
                format(filename=os.path.basename(
                    self.xtf_file_line_edit.text().strip())))
            self.msg.setWindowTitle(self.tr("Save in XTF Transfer File"))
            self.msg.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
            msg_box = self.msg.exec_()
            if msg_box == QMessageBox.No:
                return

        with OverrideCursor(Qt.WaitCursor):
            self.progress_bar.show()
            self.progress_bar.setValue(0)

            self.disable()
            self.txtStdout.setTextColor(QColor(LogColor.COLOR_INFO))
            self.txtStdout.clear()

            exporter = iliexporter.Exporter()
            exporter.tool = self.type_combo_box.currentData()
            exporter.configuration = configuration

            self.save_configuration(configuration)

            exporter.stdout.connect(self.print_info)
            exporter.stderr.connect(self.on_stderr)
            exporter.process_started.connect(self.on_process_started)
            exporter.process_finished.connect(self.on_process_finished)

            self.progress_bar.setValue(25)

            try:
                if exporter.run(
                        edited_command) != iliexporter.Exporter.SUCCESS:
                    if configuration.db_ili_version == 3:
                        # failed with a db created by ili2db version 3
                        if not edited_command:
                            # fallback because of issues with --export3 argument
                            self.show_message(
                                Qgis.Warning,
                                self.
                                tr("Tried export with ili2db version 3.x.x (fallback)"
                                   ),
                            )

                            exporter.version = 3
                            # ... and enforce the Exporter to use ili2db version 3.x.x
                            if exporter.run() != iliexporter.Exporter.SUCCESS:
                                self.enable()
                                self.progress_bar.hide()
                                return
                        else:
                            self.show_message(
                                Qgis.Warning,
                                self.
                                tr("Tried export with ili2db version 3.x.x (no fallback with editted command)"
                                   ),
                            )
                            return
                    else:
                        self.enable()
                        self.progress_bar.hide()
                        return
            except JavaNotFoundError as e:
                self.txtStdout.setTextColor(QColor(LogColor.COLOR_INFO))
                self.txtStdout.clear()
                self.txtStdout.setText(e.error_string)
                self.enable()
                self.progress_bar.hide()
                return

            self.buttonBox.clear()
            self.buttonBox.setEnabled(True)
            self.buttonBox.addButton(QDialogButtonBox.Close)
            self.progress_bar.setValue(100)

    def print_info(self, text):
        self.txtStdout.setTextColor(QColor(LogColor.COLOR_INFO))
        self.txtStdout.append(text)
        QCoreApplication.processEvents()

    def on_stderr(self, text):
        color_log_text(text, self.txtStdout)
        self.advance_progress_bar_by_text(text)
        QCoreApplication.processEvents()

    def show_message(self, level, message):
        if level == Qgis.Warning:
            self.bar.pushMessage(message, Qgis.Info, 10)
        elif level == Qgis.Critical:
            self.bar.pushMessage(message, Qgis.Warning, 10)

    def on_process_started(self, command):
        self.disable()
        self.txtStdout.setTextColor(QColor(LogColor.COLOR_INFO))
        self.txtStdout.clear()
        self.txtStdout.setText(command)
        QCoreApplication.processEvents()

    def on_process_finished(self, exit_code, result):
        color = "#004905" if exit_code == 0 else "#aa2222"
        self.txtStdout.setTextColor(QColor(color))
        self.txtStdout.append(self.tr("Finished ({})".format(exit_code)))
        if result == iliexporter.Exporter.SUCCESS:
            self.buttonBox.clear()
            self.buttonBox.setEnabled(True)
            self.buttonBox.addButton(QDialogButtonBox.Close)
        else:
            if self.export_without_validate():
                self.set_button_to_export_without_validation()
            self.enable()

    def export_without_validate(self):
        """
        Valid if an error occurred that prevents executing the export without validations
        :return: True if you can execute the export without validations, False in other case
        """
        log = self.txtStdout.toPlainText().lower()
        if "permission denied" in log or "access is denied" in log:
            return False
        return True

    def updated_configuration(self):
        """
        Get the configuration that is updated with the user configuration changes on the dialog.
        :return: Configuration
        """
        configuration = ili2dbconfig.ExportConfiguration()

        mode = self.type_combo_box.currentData()
        self._lst_panel[mode].get_fields(configuration)

        configuration.tool = mode
        configuration.xtffile = self.xtf_file_line_edit.text().strip()
        configuration.ilimodels = ";".join(
            self.export_models_model.checked_models())
        configuration.base_configuration = self.base_configuration
        configuration.db_ili_version = self.db_ili_version(configuration)

        if not self.validate_data:
            configuration.disable_validation = True
        return configuration

    def save_configuration(self, configuration):
        settings = QSettings()
        settings.setValue("QgisModelBaker/ili2pg/xtffile_export",
                          configuration.xtffile)
        settings.setValue("QgisModelBaker/importtype",
                          self.type_combo_box.currentData().name)

        mode = self.type_combo_box.currentData()
        db_factory = self.db_simple_factory.create_factory(mode)
        config_manager = db_factory.get_db_command_config_manager(
            configuration)
        config_manager.save_config_in_qsettings()

    def restore_configuration(self):
        settings = QSettings()

        for db_id in self.db_simple_factory.get_db_list(False):
            configuration = iliexporter.ExportConfiguration()
            db_factory = self.db_simple_factory.create_factory(db_id)
            config_manager = db_factory.get_db_command_config_manager(
                configuration)
            config_manager.load_config_from_qsettings()
            self._lst_panel[db_id].set_fields(configuration)

        mode = settings.value("QgisModelBaker/importtype")
        mode = DbIliMode[
            mode] if mode else self.db_simple_factory.default_database
        mode = mode & ~DbIliMode.ili

        self.type_combo_box.setCurrentIndex(self.type_combo_box.findData(mode))
        self.refresh_db_panel()

    def disable(self):
        self.type_combo_box.setEnabled(False)
        for key, value in self._lst_panel.items():
            value.setEnabled(False)
        self.ili_config.setEnabled(False)
        self.buttonBox.setEnabled(False)

    def enable(self):
        self.type_combo_box.setEnabled(True)
        for key, value in self._lst_panel.items():
            value.setEnabled(True)
        self.ili_config.setEnabled(True)
        self.buttonBox.setEnabled(True)

    def type_changed(self):
        self.txtStdout.clear()
        self.set_button_to_export()
        self.refresh_db_panel()
        self.refresh_models()
        self.txtStdout.clear()

    def refresh_db_panel(self):
        self.progress_bar.hide()

        db_id = self.type_combo_box.currentData()
        self.db_wrapper_group_box.setTitle(displayDbIliMode[db_id])

        # Refresh panels
        for key, value in self._lst_panel.items():
            value.interlis_mode = False
            is_current_panel_selected = db_id == key
            value.setVisible(is_current_panel_selected)
            if is_current_panel_selected:
                value._show_panel()

    def link_activated(self, link):
        if link.url() == "#configure":
            cfg = OptionsDialog(self.base_configuration)
            if cfg.exec_():
                settings = QSettings()
                settings.beginGroup("QgisModelBaker/ili2db")
                self.base_configuration.save(settings)
        else:
            QDesktopServices.openUrl(link)

    def help_requested(self):
        os_language = QLocale(
            QSettings().value("locale/userLocale")).name()[:2]
        if os_language in ["es", "de"]:
            webbrowser.open(
                "https://opengisch.github.io/QgisModelBaker/docs/{}/user-guide.html#export-an-interlis-transfer-file-xtf"
                .format(os_language))
        else:
            webbrowser.open(
                "https://opengisch.github.io/QgisModelBaker/docs/user-guide.html#export-an-interlis-transfer-file-xtf"
            )

    def xtf_browser_opened_to_true(self):
        """
        Slot. Sets a flag to true to eventually avoid asking a user whether to overwrite a file.
        """
        self.xtf_browser_was_opened = True

    def xtf_browser_opened_to_false(self):
        """
        Slot. Sets a flag to false to eventually ask a user whether to overwrite a file.
        """
        self.xtf_browser_was_opened = False

    def advance_progress_bar_by_text(self, text):
        if text.strip() == "Info: compile models…":
            self.progress_bar.setValue(50)
        elif text.strip() == "Info: create table structure…":
            self.progress_bar.setValue(75)
예제 #35
0
class TidalPredictionWidget(QtWidgets.QDockWidget, FORM_CLASS):
    TEMPORAL_HACK_SECS = 1
    AUTOLOAD_TIMER_MSECS = 300

    def __init__(self, parent, canvas):
        """Constructor."""
        super(TidalPredictionWidget, self).__init__(parent)
        self.canvas = canvas
        self.temporal = canvas.temporalController()
        self.setupUi(self)

        self.tableWidget.setColumnCount(3)
        self.tableWidget.setSortingEnabled(False)
        self.tableWidget.setHorizontalHeaderLabels(
            [tr('Time'), tr('Direction'),
             tr('Speed')])

        self.dateEdit.dateChanged.connect(self.updateDate)
        self.dateEdit.dateChanged.connect(self.loadStationPredictions)
        self.timeEdit.timeChanged.connect(self.updateTime)

        self.nextDay.clicked.connect(lambda: self.adjustDay(1))
        self.prevDay.clicked.connect(lambda: self.adjustDay(-1))
        self.nextStep.clicked.connect(lambda: self.adjustStep(1))
        self.prevStep.clicked.connect(lambda: self.adjustStep(-1))

        self.annotationButton.clicked.connect(self.annotatePredictions)

        self.predictionManager = None
        self.stationFeature = None
        self.stationZone = None
        self.stationHighlight = None

        self.active = False

        self.predictionCanvas = None

        self.includeCurrentsInTable = False

        self.autoLoadTimer = QTimer()
        self.autoLoadTimer.setSingleShot(True)

        self.progressBar.hide()

    def activate(self):
        if currentStationsLayer() is None or currentPredictionsLayer() is None:
            QMessageBox.critical(
                None, None,
                tr('You must add current station layers before this tool can be used.'
                   ))
            return

        self.show()

        if not self.active:
            self.active = True
            self.currentStationsLayer = currentStationsLayer()
            self.currentPredictionsLayer = currentPredictionsLayer()
            self.predictionManager = PredictionManager(
                self.currentStationsLayer, self.currentPredictionsLayer)
            self.predictionManager.progressChanged.connect(
                self.predictionProgress)
            self.setTemporalRange()
            self.loadMapExtentPredictions()

            self.autoLoadTimer.timeout.connect(self.loadMapExtentPredictions)
            self.canvas.extentsChanged.connect(self.triggerAutoLoad)
            QgsProject.instance().layerWillBeRemoved.connect(self.removalCheck)

    def deactivate(self):
        self.hide()

        if self.active:
            self.canvas.extentsChanged.disconnect(self.triggerAutoLoad)
            self.autoLoadTimer.timeout.disconnect(
                self.loadMapExtentPredictions)
            self.autoLoadTimer.stop()
            QgsProject.instance().layerWillBeRemoved.disconnect(
                self.removalCheck)

            self.tableWidget.clearContents()
            if self.predictionCanvas is not None:
                self.predictionCanvas.hide()
            if self.stationHighlight is not None:
                self.stationHighlight.hide()

            self.predictionManager.progressChanged.disconnect(
                self.predictionProgress)
            self.predictionManager = None

            self.stationFeature = None
            self.active = False

    def predictionProgress(self, progress):
        if progress == 100:
            self.progressBar.hide()
        else:
            self.progressBar.show()
            self.progressBar.setValue(progress)

    def maxAutoLoadCount(self):
        return 100
        # TODO: have a widget for this

    def removalCheck(self, layerId):
        if layerId == self.currentStationsLayer.id(
        ) or layerId == self.currentPredictionsLayer.id():
            self.deactivate()

    def triggerAutoLoad(self):
        self.autoLoadTimer.start(self.AUTOLOAD_TIMER_MSECS)

    def loadMapExtentPredictions(self):
        self.autoLoadTimer.stop()
        """ ensure all stations in visible extent of the map are loaded
        """
        if self.active and self.predictionManager is not None:
            xform = QgsCoordinateTransform(
                self.canvas.mapSettings().destinationCrs(), epsg4326,
                QgsProject.instance())
            rect = xform.transform(self.canvas.extent())
            mapFeatures = self.predictionManager.getExtentStations(rect)
            print('autoloading ', len(mapFeatures), ' in ', rect)
            if len(mapFeatures) <= self.maxAutoLoadCount():
                for f in mapFeatures:
                    self.predictionManager.getDataPromise(
                        f, self.dateEdit.date()).start()
            if self.stationZone is None and len(mapFeatures) > 0:
                self.stationZone = stationTimeZone(mapFeatures[0])

    def loadStationPredictions(self):
        """ load predictions for the selected station
        """
        if self.stationFeature is None:
            return

        self.stationData = self.predictionManager.getDataPromise(
            self.stationFeature, self.dateEdit.date())
        self.tableWidget.clearContents()
        self.stationData.resolved(self.predictionsResolved)
        self.stationData.start()

    def setTemporalRange(self):
        """ Set up the temporal range of either based on the current time, or on the temporal
            extents in the map canvas if those are defined.
        """
        if self.temporal.navigationMode(
        ) == QgsTemporalNavigationObject.NavigationMode.NavigationOff:
            startTime = QDateTime.currentDateTime().toUTC()
        else:
            startTime = self.temporal.temporalExtents().begin().addSecs(
                self.TEMPORAL_HACK_SECS)

        self.setDateTime(startTime)

    def setDateTime(self, datetime):
        """ Set our date and time choosers appropriately based on the given UTC time
            as interpreted for the current station if there is one, else base on local time.
        """
        if self.stationFeature:
            localtime = datetime.toTimeZone(
                stationTimeZone(self.stationFeature))
        else:
            localtime = datetime.toLocalTime()
        self.dateEdit.setDate(localtime.date())

        # round off the time to the nearest step
        displayTime = localtime.time()
        displayTime.setHMS(
            displayTime.hour(),
            PredictionManager.STEP_MINUTES *
            (displayTime.minute() // PredictionManager.STEP_MINUTES), 0)
        self.timeEdit.setTime(displayTime)

        self.updateTime()
        self.loadStationPredictions()

    def setCurrentStation(self, feature):
        """ set the panel's current prediction station to the one described by the given feature
        """
        self.stationFeature = feature
        self.stationZone = stationTimeZone(feature)
        self.stationLabel.setText(feature['name'])

        self.updateTime()
        self.updateStationLink()
        self.loadStationPredictions()

        if self.stationHighlight is not None:
            self.stationHighlight.hide()

        self.stationHighlight = QgsHighlight(self.canvas, self.stationFeature,
                                             self.currentStationsLayer)
        self.stationHighlight.setColor(QColor(Qt.red))
        self.stationHighlight.setFillColor(QColor(Qt.red))
        self.stationHighlight.show()

    def adjustDay(self, delta):
        self.dateEdit.setDate(self.dateEdit.date().addDays(delta))

    def adjustStep(self, delta):
        step = 60 * PredictionManager.STEP_MINUTES * delta
        curTime = self.timeEdit.time()
        if step < 0 and curTime.hour() == 0 and curTime.minute() == 0:
            self.adjustDay(-1)
        curTime = curTime.addSecs(step)
        self.timeEdit.setTime(curTime)
        if step > 0 and curTime.hour() == 0 and curTime.minute() == 0:
            self.adjustDay(1)

    def updateDate(self):
        self.loadMapExtentPredictions()
        self.updateStationLink()
        self.updateTime()

    def updateTime(self):
        self.temporal.setNavigationMode(
            QgsTemporalNavigationObject.NavigationMode.FixedRange)

        if self.stationZone is not None:
            self.datetime = QDateTime(self.dateEdit.date(),
                                      self.timeEdit.time(),
                                      self.stationZone).toUTC()
        else:
            self.datetime = QDateTime(self.dateEdit.date(),
                                      self.timeEdit.time()).toUTC()
        # Note: we hack around a memory provider range bug here by offsetting the window by 1 minute
        self.temporal.setTemporalExtents(
            QgsDateTimeRange(
                self.datetime.addSecs(-self.TEMPORAL_HACK_SECS),
                self.datetime.addSecs((60 * PredictionManager.STEP_MINUTES) -
                                      self.TEMPORAL_HACK_SECS), True, False))
        self.updatePlotXLine()

    def updateStationLink(self):
        if self.stationFeature is not None:
            linkUrl = 'https://tidesandcurrents.noaa.gov/noaacurrents/Predictions?id={}&d={}&r=1&tz=LST%2FLDT'
            linkUrl = linkUrl.format(
                self.stationFeature['station'],
                self.dateEdit.date().toString('yyyy-MM-dd'))
            self.linkLabel.setText('<a href="{}">{} Station Page</a>'.format(
                linkUrl, self.stationFeature['id']))
        else:
            self.linkLabel.setText('')

    def handlePlotClick(self, event):
        minutes = PredictionManager.STEP_MINUTES * (
            event.xdata * 60 // PredictionManager.STEP_MINUTES)
        self.timeEdit.setTime(QTime(minutes // 60, minutes % 60))

    def updatePlotXLine(self):
        if self.predictionCanvas is not None:
            x = QTime(0, 0).secsTo(self.timeEdit.time()) / 3600.0
            self.plotXLine.set_xdata([x, x])
            self.plotAxes.figure.canvas.draw()

    def predictionsResolved(self):
        # Check to see if the resolved signal is for data we currently care about.
        # if not, then just bail
        if self.stationData is None or self.stationData.state != PredictionPromise.ResolvedState:
            return
        """ when we have predictions for the current station, show them in the
            plot and table widget.
        """
        if self.predictionCanvas is not None:
            self.predictionCanvas.mpl_disconnect(self.plotCallbackId)
            self.plotLayout.removeWidget(self.predictionCanvas)
            self.predictionCanvas.hide()

        self.predictionCanvas = FigureCanvas(Figure(figsize=(5, 3)))
        self.plotLayout.addWidget(self.predictionCanvas)

        self.plotAxes = self.predictionCanvas.figure.subplots()
        # zero time in this plot = 00:00 local time on the date of interest
        t0 = QDateTime(self.dateEdit.date(), QTime(0, 0),
                       stationTimeZone(self.stationFeature)).toUTC()
        t = []
        val = []
        for f in self.stationData.predictions:
            if f['type'] == 'current':
                utcTime = f['time']
                utcTime.setTimeSpec(Qt.TimeSpec.UTC)
                t.append(t0.secsTo(utcTime) / 3600)
                val.append(f['value'])

        self.plotAxes.set_xlim(left=0, right=24)
        self.plotAxes.set_xticks([0, 3, 6, 9, 12, 15, 18, 21, 24])
        self.plotAxes.grid(linewidth=0.5)

        y0line = self.plotAxes.axhline(y=0)
        y0line.set_linestyle(':')
        y0line.set_linewidth(1)

        self.plotXLine = self.plotAxes.axvline(x=0)
        self.plotXLine.set_linestyle(':')
        self.plotXLine.set_linewidth(1)
        self.updatePlotXLine()

        self.plotAxes.plot(t, val)

        self.plotCallbackId = self.predictionCanvas.mpl_connect(
            'button_release_event', self.handlePlotClick)

        QgsProject.instance()._ax = self.plotAxes

        self.tableWidget.setRowCount(len(self.stationData.predictions))
        i = 0
        for p in self.stationData.predictions:
            dt = p['time']
            dt.setTimeSpec(Qt.TimeSpec.UTC)
            if self.includeCurrentsInTable and p[
                    'type'] == 'current' and p['dir'] != NULL:
                self.tableWidget.setItem(
                    i, 0,
                    QTableWidgetItem(
                        dt.toTimeZone(self.stationZone).toString('h:mm AP')))
                self.tableWidget.setItem(
                    i, 1, QTableWidgetItem(str(round(p['dir'])) + 'º'))
                self.tableWidget.setItem(
                    i, 2, QTableWidgetItem("{:.2f}".format(p['magnitude'])))
                i += 1
            elif p['type'] != 'current':
                self.tableWidget.setItem(
                    i, 0,
                    QTableWidgetItem(
                        dt.toTimeZone(self.stationZone).toString('h:mm AP')))
                self.tableWidget.setItem(i, 1, QTableWidgetItem(p['type']))
                self.tableWidget.setItem(
                    i, 2, QTableWidgetItem("{:.2f}".format(p['value'])))
                self.tableWidget.setRowHeight(i, 20)
                i += 1
        self.tableWidget.setRowCount(i)

    def annotatePredictions(self):
        if self.stationFeature is None:
            return

        a = QgsTextAnnotation()
        a.setMapLayer(self.predictionManager.stationsLayer)

        document = a.document()

        columnWidth = [80, 100, 60]
        columnAlign = ['left', 'left', 'right']

        html = '<font size="+2"><b>'
        html += self.stationFeature['name'] + '<br>' + self.dateEdit.date(
        ).toString() + '<br>'
        html += '</b></font>'
        html += '<font size="+1"><table cellpadding="0" cellspacing="0">'
        html += '<tr>'
        for j in range(0, self.tableWidget.columnCount()):
            html += '<td width="{}"><b>{}</b></td>'.format(
                columnWidth[j],
                self.tableWidget.horizontalHeaderItem(j).text())
        html += '</tr>'

        for i in range(0, self.tableWidget.rowCount()):
            html += '<tr bgcolor="{}">'.format('#FFFFFF' if i %
                                               2 else '#EEEEEE')
            for j in range(0, self.tableWidget.columnCount()):
                html += '<td align="{}" width="{}">{}</td>'.format(
                    columnAlign[j], columnWidth[j],
                    self.tableWidget.item(i, j).text())
            html += '</tr>'

        html += '</table></font>'
        document.setHtml(html)

        # TODO: this size and offset are wack. Can we dynamically calculate from the content somehow?
        a.setFrameSize(QSizeF(270, 300))
        a.setFrameOffsetFromReferencePoint(QPointF(-300, -200))
        a.setMapPosition(self.stationFeature.geometry().asPoint())
        a.setMapPositionCrs(
            QgsCoordinateReferenceSystem(
                self.predictionManager.stationsLayer.crs()))

        # disable its symbol
        for symbol in a.markerSymbol().symbolLayers():
            symbol.setEnabled(False)

        QgsProject.instance().annotationManager().addAnnotation(a)