Beispiel #1
0
class FileGPSService(GPSService):
    def __init__(self, filename):
        super(FileGPSService, self).__init__()
        self.file = None
        self.timer = QTimer()
        self.timer.setInterval(100)
        self.timer.timeout.connect(self._read_from_file)
        self.filename = filename

    def connectGPS(self, portname):
        # Normally the portname is passed but we will take a filename
        # because it's a fake GPS service
        if not self.isConnected:
            self.file = open(self.filename, "r")
            self.timer.start()
            self.isConnected = True

    def _read_from_file(self):
        line = self.file.readline()
        if not line:
            self.file.seek(0)
            line = self.file.readline()
        self.parse_data(line)

    def disconnectGPS(self):
        if self.file:
            self.file.close()
        self.timer.stop()
        self.file = None
        self.gpsdisconnected.emit()
Beispiel #2
0
    def _load_layer(self, loader, conn_info, meta, **kw_start):
        lst_layer = list()
        batch = 100 # kw_start.get("limit",30000)
        for i in range(6):
            timer = QTimer()
            timer.timeout.connect(lambda: self.check_loader(loader,batch))
            if not i:
                loader.start(conn_info, meta, **kw_start)
            else:
                loader.restart(conn_info, meta, **kw_start)

            timer.start(10)
            try:
                self._wait_async()
            except AllErrorsDuringTest as e:
                lst_err = e.args[0]
                self.assertIsInstance(lst_err[0], ManualInterrupt)
            finally:
                timer.stop()
                lst_layer.append(loader.layer)
                
            # with self.assertRaises(AllErrorsDuringTest, msg="stopping loader should emit error") as cm: 
            #     self._wait_async()
            # lst_err = cm.exception.args[0]
            # self.assertIsInstance(lst_err[0], ManualInterrupt)
        
        return lst_layer
Beispiel #3
0
class ShowConsoleDialog(QDialog, FORM_CLASS):
    """
    Non-modal dialog to display the console log of a OQ-Engine calculation
    """
    def __init__(self, driver_dialog, calc_id):
        QDialog.__init__(self)
        # Set up the user interface from Designer.
        self.setupUi(self)
        self.driver_dialog = driver_dialog
        self.calc_id = calc_id
        # when re-opening the dialog for a calculation, display the log from
        # the beginning
        self.driver_dialog.calc_log_line[calc_id] = 0
        self.timer = QTimer()
        self.timer.timeout.connect(self.refresh_calc_log)
        self.timer.start(3000)  # refresh time in milliseconds
        # show the log before the first iteration of the timer
        self.refresh_calc_log()

    def refresh_calc_log(self):
        calc_status = self.driver_dialog.get_calc_status(self.calc_id)
        if calc_status is None:
            self.reject()
            return
        if calc_status['status'] in ('complete', 'failed'):
            self.timer.stop()
        calc_log = self.driver_dialog.get_calc_log(self.calc_id)
        if calc_log and not isinstance(calc_log, Exception):
            self.text_browser.append(calc_log)

    def reject(self):
        self.timer.stop()
        super().reject()
Beispiel #4
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
Beispiel #5
0
class ToolTipClass(QgsMapTool):
    def __init__(self, canvas, ms):  # msはミリ秒
        QgsMapTool.__init__(self, canvas)

        self.canvas = canvas

        self.ms = ms
        # canvasMoveEventで設定した秒数(msで設定)経過したら呼ばれるメソッドを設定
        self.timerMapTips = QTimer(canvas)
        self.timerMapTips.timeout.connect(self.showMapTip)

    def canvasMoveEvent(self, event):
        QToolTip.hideText()
        self.timerMapTips.start(self.ms)

    def showMapTip(self):
        self.timerMapTips.stop()

        # 表示する値を設定する。
        mappos = self.toMapCoordinates(self.canvas.mouseLastXY())
        value = mappos

        if value == None:
            return
        text = str(value)
        QToolTip.showText(self.canvas.mapToGlobal(self.canvas.mouseLastXY()),
                          text, self.canvas)

    def deactivate(self):
        self.timerMapTips.timeout.disconnect(self.showMapTip)
class  MapCanvasEffects():
    def __init__(self):
        self.project = QgsProject().instance()
        self.canvas = QgsUtils.iface.mapCanvas()
        self.timer = QTimer( self.canvas )
        self.flash = None

    def highlight(self, layer, geometry):
        def getFlash():
            h = QgsHighlight( self.canvas, geometry, layer )
            h.setColor(     QColor( 255, 0, 0, 255 ) )
            h.setFillColor( QColor( 255, 0, 0, 100 ) )
            h.setWidth( 2 )
            return h

        def finished():
            self.timer.stop()
            self.timer.timeout.disconnect( finished )
            del self.flash

        self.flash = getFlash()
        self.timer.timeout.connect( finished )
        self.timer.start( 500 ) # Milliseconds before finishing the flash

    def zoom(self, layer, geometry):
        def getBoudingBoxGeomCanvas():
            crsLayer = layer.crs()
            crsCanvas = self.project.crs()
            if not crsLayer == crsCanvas:
                ct = QgsCoordinateTransform( layer.crs(), self.project.crs(), self.project )
                bbox = ct.transform( geometry.boundingBox() )
            else:
                bbox = geometry.boundingBox()
            return bbox

        self.canvas.setExtent( getBoudingBoxGeomCanvas() )
        self.canvas.zoomByFactor( 1.05 )
        self.canvas.refresh()
        self.highlight( layer, geometry )

    def highlightFeature(self, layer, feature):
        if not feature.hasGeometry():
            return
        geom = feature.geometry()
        self.highlight( layer, geom )

    def zoomFeature(self, layer, feature):
        if not feature.hasGeometry():
            return
        geom = feature.geometry()
        self.zoom( layer, geom )
    def fetchFiles(self, urls):
        self.logT("TileLayer.fetchFiles() starts")
        # create a QEventLoop object that belongs to the current thread (if ver. > 2.1, it is render thread)
        eventLoop = QEventLoop()
        self.logT("Create event loop: " + str(eventLoop))  # DEBUG
        # QObject.connect(self, SIGNAL("allRepliesFinished()"), eventLoop.quit)
        self.allRepliesFinished.connect(eventLoop.quit)

        # create a timer to watch whether rendering is stopped
        watchTimer = QTimer()
        watchTimer.timeout.connect(eventLoop.quit)

        # send a fetch request to the main thread
        # self.emit(SIGNAL("fetchRequest(QStringList)"), urls)
        self.fetchRequest.emit(urls)

        # wait for the fetch to finish
        tick = 0
        interval = 500
        timeoutTick = self.downloadTimeout / interval
        watchTimer.start(interval)
        while tick < timeoutTick:
            # run event loop for 0.5 seconds at maximum
            eventLoop.exec_()

            if debug_mode:
                qDebug("watchTimerTick: %d" % tick)
                qDebug("unfinished downloads: %d" %
                       self.downloader.unfinishedCount())

            if self.downloader.unfinishedCount(
            ) == 0 or self.renderContext.renderingStopped():
                break
            tick += 1
        watchTimer.stop()

        if tick == timeoutTick and self.downloader.unfinishedCount() > 0:
            self.log("fetchFiles timeout")
            # self.emitShowBarMessage("fetchFiles timeout", duration=5)   #DEBUG
            self.downloader.abort()
            self.downloader.errorStatus = Downloader.TIMEOUT_ERROR
        files = self.downloader.fetchedFiles

        watchTimer.timeout.disconnect(eventLoop.quit)  #
        # QObject.disconnect(self, SIGNAL("allRepliesFinished()"), eventLoop.quit)
        self.allRepliesFinished.disconnect(eventLoop.quit)

        self.logT("TileLayer.fetchFiles() ends")
        return files
Beispiel #8
0
    def fetchFiles(self, urls):
        self.logT("TileLayer.fetchFiles() starts")
        # create a QEventLoop object that belongs to the current thread (if ver. > 2.1, it is render thread)
        eventLoop = QEventLoop()
        self.logT("Create event loop: " + str(eventLoop))  # DEBUG
        # QObject.connect(self, SIGNAL("allRepliesFinished()"), eventLoop.quit)
        self.allRepliesFinished.connect(eventLoop.quit)

        # create a timer to watch whether rendering is stopped
        watchTimer = QTimer()
        watchTimer.timeout.connect(eventLoop.quit)

        # send a fetch request to the main thread
        # self.emit(SIGNAL("fetchRequest(QStringList)"), urls)
        self.fetchRequest.emit(urls)

        # wait for the fetch to finish
        tick = 0
        interval = 500
        timeoutTick = self.downloadTimeout / interval
        watchTimer.start(interval)
        while tick < timeoutTick:
            # run event loop for 0.5 seconds at maximum
            eventLoop.exec_()

            if debug_mode:
                qDebug("watchTimerTick: %d" % tick)
                qDebug("unfinished downloads: %d" % self.downloader.unfinishedCount())

            if self.downloader.unfinishedCount() == 0 or self.renderContext.renderingStopped():
                break
            tick += 1
        watchTimer.stop()

        if tick == timeoutTick and self.downloader.unfinishedCount() > 0:
            self.log("fetchFiles timeout")
            # self.emitShowBarMessage("fetchFiles timeout", duration=5)   #DEBUG
            self.downloader.abort()
            self.downloader.errorStatus = Downloader.TIMEOUT_ERROR
        files = self.downloader.fetchedFiles

        watchTimer.timeout.disconnect(eventLoop.quit)  #
        # QObject.disconnect(self, SIGNAL("allRepliesFinished()"), eventLoop.quit)
        self.allRepliesFinished.disconnect(eventLoop.quit)

        self.logT("TileLayer.fetchFiles() ends")
        return files
Beispiel #9
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()
Beispiel #10
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
class ApiDimensioning(ApiParent):

    def __init__(self, iface, settings, controller, plugin_dir):
        """ Class constructor """

        ApiParent.__init__(self, iface, settings, controller, plugin_dir)
        self.iface = iface
        self.settings = settings
        self.controller = controller
        self.plugin_dir = plugin_dir

        self.canvas = self.iface.mapCanvas()
        self.emit_point = QgsMapToolEmitPoint(self.canvas)
        self.canvas.setMapTool(self.emit_point)

        # Snapper
        self.snapper_manager = SnappingConfigManager(self.iface)
        self.snapper = self.snapper_manager.get_snapper()


    def open_form(self, new_feature=None, layer=None, new_feature_id=None):
        self.dlg_dim = ApiDimensioningUi()
        self.load_settings(self.dlg_dim)

        # Set signals
        actionSnapping = self.dlg_dim.findChild(QAction, "actionSnapping")
        actionSnapping.triggered.connect(partial(self.snapping, actionSnapping))
        self.set_icon(actionSnapping, "103")

        actionOrientation = self.dlg_dim.findChild(QAction, "actionOrientation")
        actionOrientation.triggered.connect(partial(self.orientation, actionOrientation))
        self.set_icon(actionOrientation, "133")

        self.dlg_dim.btn_accept.clicked.connect(partial(self.save_dimensioning, new_feature, layer))
        self.dlg_dim.btn_cancel.clicked.connect(partial(self.cancel_dimensioning))
        self.dlg_dim.dlg_closed.connect(partial(self.cancel_dimensioning))
        self.dlg_dim.dlg_closed.connect(partial(self.save_settings, self.dlg_dim))

        # Set layers dimensions, node and connec
        self.layer_dimensions = self.controller.get_layer_by_tablename("v_edit_dimensions")
        self.layer_node = self.controller.get_layer_by_tablename("v_edit_node")
        self.layer_connec = self.controller.get_layer_by_tablename("v_edit_connec")

        self.create_map_tips()
        body = self.create_body()
        # Get layers under mouse clicked
        sql = f"SELECT gw_api_getdimensioning($${{{body}}}$$)::text"
        row = self.controller.get_row(sql, log_sql=True, commit=True)

        if row is None or row[0] is None:
            self.controller.show_message("NOT ROW FOR: " + sql, 2)
            return False
        # Parse string to order dict into List
        complet_result = [json.loads(row[0],  object_pairs_hook=OrderedDict)]

        layout_list = []
        for field in complet_result[0]['body']['data']['fields']:
            label, widget = self.set_widgets(self.dlg_dim, complet_result, field)

            if widget.objectName() == 'id':
                utils_giswater.setWidgetText(self.dlg_dim, widget, new_feature_id)
            layout = self.dlg_dim.findChild(QGridLayout, field['layoutname'])
           # Take the QGridLayout with the intention of adding a QSpacerItem later
            if layout not in layout_list and layout.objectName() not in ('top_layout', 'bot_layout_1', 'bot_layout_2'):
                layout_list.append(layout)

            # Add widgets into layout
            if field['layoutname'] in ('top_layout', 'bot_layout_1', 'bot_layout_2'):
                layout.addWidget(label, 0, field['layout_order'])
                layout.addWidget(widget, 1, field['layout_order'])
            else:
                self.put_widgets(self.dlg_dim, field, label, widget)

        # Add a QSpacerItem into each QGridLayout of the list
        for layout in layout_list:
            vertical_spacer1 = QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding)
            layout.addItem(vertical_spacer1)

        self.open_dialog(self.dlg_dim)
        return False, False


    def cancel_dimensioning(self):

        self.iface.actionRollbackEdits().trigger()
        self.close_dialog(self.dlg_dim)

    def save_dimensioning(self, new_feature, layer):

        # Insert new feature into db
        layer.updateFeature(new_feature)
        layer.commitChanges()

        # Create body
        fields = ''
        list_widgets = self.dlg_dim.findChildren(QLineEdit)
        for widget in list_widgets:
            widget_name = widget.objectName()
            widget_value = utils_giswater.getWidgetText(self.dlg_dim, widget)
            if widget_value == 'null':
                continue
            fields += f'"{widget_name}":"{widget_value}",'

        srid = self.controller.plugin_settings_value('srid')
        sql = f"SELECT ST_GeomFromText('{new_feature.geometry().asWkt()}', {srid})"
        the_geom = self.controller.get_row(sql, commit=True, log_sql=True)
        fields += f'"the_geom":"{the_geom[0]}"'

        feature = '"tableName":"v_edit_dimensions"'
        body = self.create_body(feature=feature, filter_fields=fields)

        # Execute query
        sql = f"SELECT gw_api_setdimensioning($${{{body}}}$$)::text"
        row = self.controller.get_row(sql, log_sql=True, commit=True)

        # Close dialog
        self.close_dialog(self.dlg_dim)


    def deactivate_signals(self, action):
        self.snapper_manager.remove_marker()
        try:
            self.canvas.xyCoordinates.disconnect()
        except TypeError as e:
            pass

        try:
            self.emit_point.canvasClicked.disconnect()
        except TypeError as e:
            pass

        if not action.isChecked():
            action.setChecked(False)
            return True
        return False


    def snapping(self, action):
        # Set active layer and set signals
        if self.deactivate_signals(action): return

        self.dlg_dim.actionOrientation.setChecked(False)
        self.iface.setActiveLayer(self.layer_node)
        self.canvas.xyCoordinates.connect(self.mouse_move)
        self.emit_point.canvasClicked.connect(partial(self.click_button_snapping, action))


    def mouse_move(self, point):

        # Hide marker and get coordinates
        self.snapper_manager.remove_marker()
        event_point = self.snapper_manager.get_event_point(point=point)

        # Snapping
        result = self.snapper_manager.snap_to_background_layers(event_point)
        if self.snapper_manager.result_is_valid():
            layer = self.snapper_manager.get_snapped_layer(result)
            # Check feature
            if layer == self.layer_node or layer == self.layer_connec:
                self.snapper_manager.add_marker(result)


    def click_button_snapping(self, action, point, btn):

        if not self.layer_dimensions:
            return

        if btn == Qt.RightButton:
            if btn == Qt.RightButton:
                action.setChecked(False)
                self.deactivate_signals(action)
                return

        layer = self.layer_dimensions
        self.iface.setActiveLayer(layer)
        layer.startEditing()

        # Get coordinates
        event_point = self.snapper_manager.get_event_point(point=point)

        # Snapping
        result = self.snapper_manager.snap_to_background_layers(event_point)
        if self.snapper_manager.result_is_valid():

            layer = self.snapper_manager.get_snapped_layer(result)
            # Check feature
            if layer == self.layer_node:
                feat_type = 'node'
            elif layer == self.layer_connec:
                feat_type = 'connec'
            else:
                return

            # Get the point
            snapped_feat = self.snapper_manager.get_snapped_feature(result)
            feature_id = self.snapper_manager.get_snapped_feature_id(result)
            element_id = snapped_feat.attribute(feat_type + '_id')

            # Leave selection
            layer.select([feature_id])

            # Get depth of the feature
            self.project_type = self.controller.get_project_type()
            if self.project_type == 'ws':
                fieldname = "depth"
            elif self.project_type == 'ud' and feat_type == 'node':
                fieldname = "ymax"
            elif self.project_type == 'ud' and feat_type == 'connec':
                fieldname = "connec_depth"

            sql = (f"SELECT {fieldname} "
                   f"FROM {feat_type} "
                   f"WHERE {feat_type}_id = '{element_id}'")
            row = self.controller.get_row(sql)

            if not row:
                return

            utils_giswater.setText(self.dlg_dim, "depth", row[0])
            utils_giswater.setText(self.dlg_dim, "feature_id", element_id)
            utils_giswater.setText(self.dlg_dim, "feature_type", feat_type.upper())


    def orientation(self, action):
        if self.deactivate_signals(action): return

        self.dlg_dim.actionSnapping.setChecked(False)
        self.emit_point.canvasClicked.connect(partial(self.click_button_orientation, action))


    def click_button_orientation(self, action, point, btn):

        if not self.layer_dimensions:
            return

        if btn == Qt.RightButton:
            action.setChecked(False)
            self.deactivate_signals(action)
            return

        self.x_symbol = self.dlg_dim.findChild(QLineEdit, "x_symbol")

        self.x_symbol.setText(str(int(point.x())))

        self.y_symbol = self.dlg_dim.findChild(QLineEdit, "y_symbol")
        self.y_symbol.setText(str(int(point.y())))


    def create_map_tips(self):
        """ Create MapTips on the map """

        row = self.controller.get_config('dim_tooltip')
        if not row or row[0].lower() != 'true':
            return

        self.timer_map_tips = QTimer(self.canvas)
        self.map_tip_node = QgsMapTip()
        self.map_tip_connec = QgsMapTip()

        self.canvas.xyCoordinates.connect(self.map_tip_changed)
        self.timer_map_tips.timeout.connect(self.show_map_tip)
        self.timer_map_tips_clear = QTimer(self.canvas)
        self.timer_map_tips_clear.timeout.connect(self.clear_map_tip)


    def map_tip_changed(self, point):
        """ SLOT. Initialize the Timer to show MapTips on the map """

        if self.canvas.underMouse():
            self.last_map_position = QgsPointXY(point.x(), point.y())
            self.map_tip_node.clear(self.canvas)
            self.map_tip_connec.clear(self.canvas)
            self.timer_map_tips.start(100)

    def show_map_tip(self):
        """ Show MapTips on the map """

        self.timer_map_tips.stop()
        if self.canvas.underMouse():
            point_qgs = self.last_map_position
            point_qt = self.canvas.mouseLastXY()
            if self.layer_node:
                self.map_tip_node.showMapTip(self.layer_node, point_qgs, point_qt, self.canvas)
            if self.layer_connec:
                self.map_tip_connec.showMapTip(self.layer_connec, point_qgs, point_qt, self.canvas)
            self.timer_map_tips_clear.start(1000)

    def clear_map_tip(self):
        """ Clear MapTips """

        self.timer_map_tips_clear.stop()
        self.map_tip_node.clear(self.canvas)
        self.map_tip_connec.clear(self.canvas)


    def set_widgets(self, dialog, complet_result, field):

        widget = None
        label = None
        if field['label']:
            label = QLabel()
            label.setObjectName('lbl_' + field['widgetname'])
            label.setText(field['label'].capitalize())
            if field['stylesheet'] is not None and 'label' in field['stylesheet']:
                label = self.set_setStyleSheet(field, label)
            if 'tooltip' in field:
                label.setToolTip(field['tooltip'])
            else:
                label.setToolTip(field['label'].capitalize())
        if field['widgettype'] == 'text' or field['widgettype'] == 'typeahead':
            completer = QCompleter()
            widget = self.add_lineedit(field)
            widget = self.set_widget_size(widget, field)
            widget = self.set_data_type(field, widget)
            if field['widgettype'] == 'typeahead':
                widget = self.manage_lineedit(field, dialog, widget, completer)
            # if widget.property('column_id') == self.field_id:
            #     self.feature_id = widget.text()
            #     # Get selected feature
            #     self.feature = self.get_feature_by_id(self.layer, self.feature_id, self.field_id)
        elif field['widgettype'] == 'combo':
            widget = self.add_combobox(field)
            widget = self.set_widget_size(widget, field)
        elif field['widgettype'] == 'check':
            widget = self.add_checkbox(field)
        elif field['widgettype'] == 'datepickertime':
            widget = self.add_calendar(dialog, field)
        elif field['widgettype'] == 'button':
            widget = self.add_button(dialog, field)
            widget = self.set_widget_size(widget, field)
        elif field['widgettype'] == 'hyperlink':
            widget = self.add_hyperlink(dialog, field)
            widget = self.set_widget_size(widget, field)
        elif field['widgettype'] == 'hspacer':
            widget = self.add_horizontal_spacer()
        elif field['widgettype'] == 'vspacer':
            widget = self.add_verical_spacer()
        elif field['widgettype'] == 'textarea':
            widget = self.add_textarea(field)
        elif field['widgettype'] in ('spinbox', 'doubleSpinbox'):
            widget = self.add_spinbox(field)
        elif field['widgettype'] == 'tableView':
            widget = self.add_tableview(complet_result, field)
            widget = self.set_headers(widget, field)
            widget = self.populate_table(widget, field)
            widget = self.set_columns_config(widget, field['widgetname'], sort_order=1, isQStandardItemModel=True)
            utils_giswater.set_qtv_config(widget)

        return label, widget
class TesterWidget(BASE, WIDGET):

    currentTestResult = None
    currentTest = 0
    currentTestStep = 0

    BLINKING_INTERVAL = 1000

    buttonColors = ["", 'QPushButton {color: yellow;}']

    testingFinished = pyqtSignal()


    def __init__(self):
        super(TesterWidget, self).__init__()
        self.setupUi(self)
        self.setObjectName("TesterPluginPanel")
        self.btnCancel.clicked.connect(self.cancelTesting)
        self.btnTestOk.clicked.connect(self.testPasses)
        self.btnTestFailed.clicked.connect(self.testFails)
        self.btnRestartTest.clicked.connect(self.restartTest)
        self.btnSkip.clicked.connect(self.skipTest)
        self.btnNextStep.clicked.connect(self.runNextStep)
        self.buttons = [self.btnTestOk, self.btnTestFailed, self.btnNextStep]

        self.blinkTimer = QTimer()
        self.blinkTimer.timeout.connect(self._blink)

    def startBlinking(self):
        self.currentBlinkingTime = 0
        self.blinkTimer.start(self.BLINKING_INTERVAL)

    def stopBlinking(self):
        self.blinkTimer.stop()
        for button in self.buttons:
            button.setStyleSheet(self.buttonColors[0])

    def _blink(self):
        self.currentBlinkingTime += 1
        color = self.buttonColors[self.currentBlinkingTime % 2]
        for button in self.buttons:
            if button.isEnabled():
                button.setStyleSheet(color)

    def setTests(self, tests):
        self.tests = tests

    def startTesting(self):
        self.currentTest = 0
        self.report = Report()
        self.runNextTest()

    def getReportDialog(self):
        """Wrapper for easy mocking"""
        self.reportDialog = ReportDialog(self.report)
        return self.reportDialog

    def restartTest(self):
        self.currentTestResult = None
        self.runNextTest()

    def runNextTest(self):
        if self.currentTestResult:
            self.report.addTestResult(self.currentTestResult)
        if self.currentTest < len(self.tests):
            test = self.tests[self.currentTest]
            self.labelCurrentTest.setText("Current test: %s-%s" % (test.group.upper(), test.name))
            self.currentTestResult = TestResult(test)
            self.currentTestStep = 0
            self.runNextStep()
        else:
            QApplication.restoreOverrideCursor()
            self.testingFinished.emit()
            self.setVisible(False)

    def runNextStep(self):
        self.stopBlinking()
        test = self.tests[self.currentTest]
        step = test.steps[self.currentTestStep]
        self.btnSkip.setEnabled(True)
        self.btnCancel.setEnabled(True)
        if os.path.exists(step.description):
            with open(step.description) as f:
                html = "".join(f.readlines())
            self.webView.setHtml(html)
        else:
            if step.function is not None:
                self.webView.setHtml(step.description + "<p><b>[This is an automated step. Please, wait until it has been completed]</b></p>")
            else:
                self.webView.setHtml(step.description + "<p><b>[Click on the right-hand side buttons once you have performed this step]</b></p>")
        QCoreApplication.processEvents()
        if self.currentTestStep == len(test.steps) - 1:
            if step.function is not None:
                self.btnTestOk.setEnabled(False)
                self.btnTestFailed.setEnabled(False)
                self.btnNextStep.setEnabled(False)
                self.btnSkip.setEnabled(False)
                self.btnCancel.setEnabled(False)
                self.webView.setEnabled(False)
                QCoreApplication.processEvents()
                try:
                    execute(step.function)
                    self.testPasses()
                except Exception as e:
                    if isinstance(e, AssertionError):
                        self.testFails("%s\n%s" % (str(e), traceback.format_exc()))
                    else:
                        self.testContainsError("%s\n%s" % (str(e), traceback.format_exc()))
            else:
                self.btnTestOk.setEnabled(True)
                self.btnTestOk.setText("Test passes")
                self.btnTestFailed.setEnabled(True)
                self.btnTestFailed.setText("Test fails")
                self.webView.setEnabled(True)
                self.btnNextStep.setEnabled(False)
                if step.prestep:
                    try:
                        execute(step.prestep)
                    except Exception as e:
                        if isinstance(e, AssertionError):
                            self.testFailsAtSetup("%s\n%s" % (str(e), traceback.format_exc()))
                        else:
                            self.testContainsError("%s\n%s" % (str(e), traceback.format_exc()))
        else:
            if step.function is not None:
                self.btnTestOk.setEnabled(False)
                self.btnTestFailed.setEnabled(False)
                self.btnNextStep.setEnabled(False)
                self.btnSkip.setEnabled(False)
                self.btnCancel.setEnabled(False)
                self.webView.setEnabled(False)
                QCoreApplication.processEvents()
                try:
                    execute(step.function)
                    self.currentTestStep += 1
                    self.runNextStep()
                except Exception as e:
                    if isinstance(e, AssertionError):
                        self.testFails("%s\n%s" % (str(e), traceback.format_exc()))
                    else:
                        self.testContainsError("%s\n%s" % (str(e), traceback.format_exc()))
            else:
                self.currentTestStep += 1
                self.webView.setEnabled(True)
                self.btnNextStep.setEnabled(not step.isVerifyStep)
                if step.isVerifyStep:
                    self.btnTestOk.setEnabled(True)
                    self.btnTestOk.setText("Step passes")
                    self.btnTestFailed.setEnabled(True)
                    self.btnTestFailed.setText("Step fails")
                else:
                    self.btnTestOk.setEnabled(False)
                    self.btnTestFailed.setEnabled(False)
                if step.prestep:
                    try:
                        execute(step.prestep)
                    except Exception as e:
                        if isinstance(e, AssertionError):
                            self.testFailsAtSetup("%s\n%s" % (str(e), traceback.format_exc()))
                        else:
                            self.testContainsError("%s\n%s" % (str(e), traceback.format_exc()))
        if step.function is None:
            self.startBlinking()

    def testPasses(self):
        test = self.tests[self.currentTest]
        if self.btnTestOk.isEnabled() and self.btnTestOk.text() == "Step passes":
            self.runNextStep()
        else:
            try:
                test = self.tests[self.currentTest]
                test.cleanup()
                self.currentTestResult.passed()
            except:
                self.currentTestResult.failed("Test cleanup", traceback.format_exc())

            self.currentTest +=1
            self.runNextTest()


    def testFails(self, msg = ""):
        test = self.tests[self.currentTest]
        if self.btnTestOk.isEnabled() and self.btnTestOk.text() == "Step passes":
            desc = test.steps[self.currentTestStep - 1].description
        else:
            desc = test.steps[self.currentTestStep].description
        self.currentTestResult.failed(desc, msg)
        try:
            test.cleanup()
        except:
            pass
        self.currentTest +=1
        self.runNextTest()

    def testFailsAtSetup(self, msg = ""):
        test = self.tests[self.currentTest]
        if self.btnTestOk.isEnabled() and self.btnTestOk.text() == "Step passes":
            desc = test.steps[self.currentTestStep - 1].description
        else:
            desc = test.steps[self.currentTestStep].description
        self.currentTestResult.setupFailed(desc, msg)
        try:
            test.cleanup()
        except:
            pass
        self.currentTest +=1
        self.runNextTest()

    def testContainsError(self, msg = ""):
        test = self.tests[self.currentTest]
        if self.btnTestOk.isEnabled() and self.btnTestOk.text() == "Step passes":
            desc = test.steps[self.currentTestStep - 1].description
        else:
            desc = test.steps[self.currentTestStep].description
        self.currentTestResult.containsError(desc, msg)
        try:
            test.cleanup()
        except:
            pass
        self.currentTest +=1
        self.runNextTest()

    def skipTest(self):
        try:
            test = self.tests[self.currentTest]
            test.cleanup()
        except:
            pass
        self.currentTest +=1
        self.currentTestResult.skipped()

        self.runNextTest()

    def cancelTesting(self):
        self.setVisible(False)
        self.testingFinished.emit()
class ApiDimensioning(ApiParent):
    def __init__(self, iface, settings, controller, plugin_dir):
        """ Class constructor """

        ApiParent.__init__(self, iface, settings, controller, plugin_dir)
        self.iface = iface
        self.settings = settings
        self.controller = controller
        self.plugin_dir = plugin_dir
        self.canvas = self.iface.mapCanvas()
        self.points = None

        # Snapper
        self.snapper_manager = SnappingConfigManager(self.iface)
        self.snapper_manager.set_controller(self.controller)
        self.snapper = self.snapper_manager.get_snapper()

    def open_form(self,
                  qgis_feature=None,
                  layer=None,
                  db_return=None,
                  fid=None):

        self.dlg_dim = DimensioningUi()
        self.load_settings(self.dlg_dim)

        # Set signals
        actionSnapping = self.dlg_dim.findChild(QAction, "actionSnapping")
        actionSnapping.triggered.connect(partial(self.snapping,
                                                 actionSnapping))
        self.set_icon(actionSnapping, "103")

        actionOrientation = self.dlg_dim.findChild(QAction,
                                                   "actionOrientation")
        actionOrientation.triggered.connect(
            partial(self.orientation, actionOrientation))
        self.set_icon(actionOrientation, "133")

        # Set layers dimensions, node and connec
        self.layer_dimensions = self.controller.get_layer_by_tablename(
            "v_edit_dimensions")
        self.layer_node = self.controller.get_layer_by_tablename("v_edit_node")
        self.layer_connec = self.controller.get_layer_by_tablename(
            "v_edit_connec")

        if qgis_feature is None:
            features = self.layer_dimensions.getFeatures()
            for feature in features:
                if feature['id'] == fid:
                    return feature
            qgis_feature = feature

        #qgis_feature = self.get_feature_by_id(self.layer_dimensions, fid, 'id')

        self.dlg_dim.btn_accept.clicked.connect(
            partial(self.save_dimensioning, qgis_feature, layer))
        self.dlg_dim.btn_cancel.clicked.connect(
            partial(self.cancel_dimensioning))
        self.dlg_dim.dlg_closed.connect(partial(self.cancel_dimensioning))
        self.dlg_dim.dlg_closed.connect(
            partial(self.save_settings, self.dlg_dim))
        self.dlg_dim.key_escape.connect(
            partial(self.close_dialog, self.dlg_dim))

        self.create_map_tips()
        # when funcion is called from new feature
        if db_return is None:
            extras = f'"coordinates":{{{self.points}}}'
            body = self.create_body(extras=extras)
            function_name = 'gw_fct_getdimensioning'
            json_result = self.controller.get_json(function_name, body)
            if json_result is None:
                return False
            db_return = [json_result]

        # get id from db response
        self.fid = db_return[0]['body']['feature']['id']

        layout_list = []
        for field in db_return[0]['body']['data']['fields']:
            if 'hidden' in field and field['hidden']:
                continue

            label, widget = self.set_widgets(self.dlg_dim, db_return, field)

            if widget.objectName() == 'id':
                utils_giswater.setWidgetText(self.dlg_dim, widget, self.fid)
            layout = self.dlg_dim.findChild(QGridLayout, field['layoutname'])

            # profilactic issue to prevent missed layouts againts db response and form
            if layout is not None:

                # Take the QGridLayout with the intention of adding a QSpacerItem later
                if layout not in layout_list and layout.objectName() not in (
                        'lyt_top_1', 'lyt_bot_1', 'lyt_bot_2'):
                    layout_list.append(layout)
                    # Add widgets into layout
                    layout.addWidget(label, 0, field['layoutorder'])
                    layout.addWidget(widget, 1, field['layoutorder'])

                # If field is on top or bottom layout the position is horitzontal no vertical
                if field['layoutname'] in ('lyt_top_1', 'lyt_bot_1',
                                           'lyt_bot_2'):
                    layout.addWidget(label, 0, field['layoutorder'])
                    layout.addWidget(widget, 1, field['layoutorder'])
                else:
                    self.put_widgets(self.dlg_dim, field, label, widget)

        # Add a QSpacerItem into each QGridLayout of the list
        for layout in layout_list:
            vertical_spacer1 = QSpacerItem(20, 40, QSizePolicy.Minimum,
                                           QSizePolicy.Expanding)
            layout.addItem(vertical_spacer1)

        title = f"DIMENSIONING - {self.fid}"
        self.open_dialog(self.dlg_dim, dlg_name='dimensioning', title=title)
        return False, False

    def cancel_dimensioning(self):

        self.iface.actionRollbackEdits().trigger()
        self.close_dialog(self.dlg_dim)

    def save_dimensioning(self, qgis_feature, layer):

        # Upsert feature into db
        layer.updateFeature(qgis_feature)
        layer.commitChanges()

        # Create body
        fields = ''
        list_widgets = self.dlg_dim.findChildren(QLineEdit)
        for widget in list_widgets:
            widget_name = widget.property('columnname')
            widget_value = utils_giswater.getWidgetText(self.dlg_dim, widget)
            if widget_value == 'null':
                continue
            fields += f'"{widget_name}":"{widget_value}", '

        list_widgets = self.dlg_dim.findChildren(QCheckBox)
        for widget in list_widgets:
            widget_name = widget.property('columnname')
            widget_value = f'"{utils_giswater.isChecked(self.dlg_dim, widget)}"'
            if widget_value == 'null':
                continue
            fields += f'"{widget_name}":{widget_value},'

        list_widgets = self.dlg_dim.findChildren(QComboBox)
        for widget in list_widgets:
            widget_name = widget.property('columnname')
            widget_value = f'"{utils_giswater.get_item_data(self.dlg_dim, widget)}"'
            if widget_value == 'null':
                continue
            fields += f'"{widget_name}":{widget_value},'

        # remove last character (,) from fields
        fields = fields[:-1]

        feature = '"tableName":"v_edit_dimensions", '
        feature += f'"id":"{self.fid}"'
        extras = f'"fields":{{{fields}}}'
        body = self.create_body(feature=feature, extras=extras)
        result = self.controller.get_json('gw_fct_setdimensioning', body)

        # Close dialog
        self.close_dialog(self.dlg_dim)

    def deactivate_signals(self, action):
        self.snapper_manager.remove_marker()
        try:
            self.canvas.xyCoordinates.disconnect()
        except TypeError:
            pass

        try:
            self.emit_point.canvasClicked.disconnect()
        except TypeError:
            pass

        if not action.isChecked():
            action.setChecked(False)
            return True

        return False

    def snapping(self, action):

        # Set active layer and set signals
        self.emit_point = QgsMapToolEmitPoint(self.canvas)
        self.canvas.setMapTool(self.emit_point)
        if self.deactivate_signals(action):
            return

        self.snapper_manager.set_snapping_layers()
        self.snapper_manager.remove_marker()
        self.snapper_manager.store_snapping_options()
        self.snapper_manager.enable_snapping()
        self.snapper_manager.snap_to_node()
        self.snapper_manager.snap_to_connec_gully()
        self.snapper_manager.set_snapping_mode()

        self.dlg_dim.actionOrientation.setChecked(False)
        self.iface.setActiveLayer(self.layer_node)
        self.canvas.xyCoordinates.connect(self.mouse_move)
        self.emit_point.canvasClicked.connect(
            partial(self.click_button_snapping, action))

    def mouse_move(self, point):

        # Hide marker and get coordinates
        self.snapper_manager.remove_marker()
        event_point = self.snapper_manager.get_event_point(point=point)

        # Snapping
        result = self.snapper_manager.snap_to_background_layers(event_point)
        if self.snapper_manager.result_is_valid():
            layer = self.snapper_manager.get_snapped_layer(result)
            # Check feature
            if layer == self.layer_node or layer == self.layer_connec:
                self.snapper_manager.add_marker(result)

    def click_button_snapping(self, action, point, btn):

        if not self.layer_dimensions:
            return

        if btn == Qt.RightButton:
            if btn == Qt.RightButton:
                action.setChecked(False)
                self.deactivate_signals(action)
                return

        layer = self.layer_dimensions
        self.iface.setActiveLayer(layer)
        layer.startEditing()

        # Get coordinates
        event_point = self.snapper_manager.get_event_point(point=point)

        # Snapping
        result = self.snapper_manager.snap_to_background_layers(event_point)
        if self.snapper_manager.result_is_valid():

            layer = self.snapper_manager.get_snapped_layer(result)
            # Check feature
            if layer == self.layer_node:
                feat_type = 'node'
            elif layer == self.layer_connec:
                feat_type = 'connec'
            else:
                return

            # Get the point
            snapped_feat = self.snapper_manager.get_snapped_feature(result)
            feature_id = self.snapper_manager.get_snapped_feature_id(result)
            element_id = snapped_feat.attribute(feat_type + '_id')

            # Leave selection
            layer.select([feature_id])

            # Get depth of the feature
            fieldname = None
            self.project_type = self.controller.get_project_type()
            if self.project_type == 'ws':
                fieldname = "depth"
            elif self.project_type == 'ud' and feat_type == 'node':
                fieldname = "ymax"
            elif self.project_type == 'ud' and feat_type == 'connec':
                fieldname = "connec_depth"

            if fieldname is None:
                return

            depth = snapped_feat.attribute(fieldname)
            if depth:
                utils_giswater.setText(self.dlg_dim, "depth", depth)
            utils_giswater.setText(self.dlg_dim, "feature_id", element_id)
            utils_giswater.setText(self.dlg_dim, "feature_type",
                                   feat_type.upper())

            self.snapper_manager.recover_snapping_options()
            self.deactivate_signals(action)
            action.setChecked(False)

    def orientation(self, action):

        self.emit_point = QgsMapToolEmitPoint(self.canvas)
        self.canvas.setMapTool(self.emit_point)
        if self.deactivate_signals(action):
            return

        self.snapper_manager.set_snapping_layers()
        self.snapper_manager.remove_marker()
        self.snapper_manager.store_snapping_options()
        self.snapper_manager.enable_snapping()
        self.snapper_manager.snap_to_node()
        self.snapper_manager.snap_to_connec_gully()
        self.snapper_manager.set_snapping_mode()

        self.dlg_dim.actionSnapping.setChecked(False)
        self.emit_point.canvasClicked.connect(
            partial(self.click_button_orientation, action))

    def click_button_orientation(self, action, point, btn):

        if not self.layer_dimensions:
            return

        if btn == Qt.RightButton:
            action.setChecked(False)
            self.deactivate_signals(action)
            return

        self.x_symbol = self.dlg_dim.findChild(QLineEdit, "x_symbol")

        self.x_symbol.setText(str(int(point.x())))

        self.y_symbol = self.dlg_dim.findChild(QLineEdit, "y_symbol")
        self.y_symbol.setText(str(int(point.y())))

        self.snapper_manager.recover_snapping_options()
        self.deactivate_signals(action)
        action.setChecked(False)

    def create_map_tips(self):
        """ Create MapTips on the map """

        row = self.controller.get_config('qgis_dim_tooltip')
        if not row or row[0].lower() != 'true':
            return

        self.timer_map_tips = QTimer(self.canvas)
        self.map_tip_node = QgsMapTip()
        self.map_tip_connec = QgsMapTip()

        self.canvas.xyCoordinates.connect(self.map_tip_changed)
        self.timer_map_tips.timeout.connect(self.show_map_tip)
        self.timer_map_tips_clear = QTimer(self.canvas)
        self.timer_map_tips_clear.timeout.connect(self.clear_map_tip)

    def map_tip_changed(self, point):
        """ SLOT. Initialize the Timer to show MapTips on the map """

        if self.canvas.underMouse():
            self.last_map_position = QgsPointXY(point.x(), point.y())
            self.map_tip_node.clear(self.canvas)
            self.map_tip_connec.clear(self.canvas)
            self.timer_map_tips.start(100)

    def show_map_tip(self):
        """ Show MapTips on the map """

        self.timer_map_tips.stop()
        if self.canvas.underMouse():
            point_qgs = self.last_map_position
            point_qt = self.canvas.mouseLastXY()
            if self.layer_node:
                self.map_tip_node.showMapTip(self.layer_node, point_qgs,
                                             point_qt, self.canvas)
            if self.layer_connec:
                self.map_tip_connec.showMapTip(self.layer_connec, point_qgs,
                                               point_qt, self.canvas)
            self.timer_map_tips_clear.start(1000)

    def clear_map_tip(self):
        """ Clear MapTips """

        self.timer_map_tips_clear.stop()
        self.map_tip_node.clear(self.canvas)
        self.map_tip_connec.clear(self.canvas)

    def set_widgets(self, dialog, db_return, field):

        widget = None
        label = None
        if field['label']:
            label = QLabel()
            label.setObjectName('lbl_' + field['widgetname'])
            label.setText(field['label'].capitalize())
            if field['stylesheet'] is not None and 'label' in field[
                    'stylesheet']:
                label = self.set_setStyleSheet(field, label)
            if 'tooltip' in field:
                label.setToolTip(field['tooltip'])
            else:
                label.setToolTip(field['label'].capitalize())
        if field['widgettype'] == 'text' or field['widgettype'] == 'typeahead':
            completer = QCompleter()
            widget = self.add_lineedit(field)
            widget = self.set_widget_size(widget, field)
            widget = self.set_data_type(field, widget)
            if field['widgettype'] == 'typeahead':
                widget = self.manage_lineedit(field, dialog, widget, completer)
        elif field['widgettype'] == 'combo':
            widget = self.add_combobox(field)
            widget = self.set_widget_size(widget, field)
        elif field['widgettype'] == 'check':
            widget = self.add_checkbox(field)
        elif field['widgettype'] == 'datetime':
            widget = self.add_calendar(dialog, field)
        elif field['widgettype'] == 'button':
            widget = self.add_button(dialog, field)
            widget = self.set_widget_size(widget, field)
        elif field['widgettype'] == 'hyperlink':
            widget = self.add_hyperlink(field)
            widget = self.set_widget_size(widget, field)
        elif field['widgettype'] == 'hspacer':
            widget = self.add_horizontal_spacer()
        elif field['widgettype'] == 'vspacer':
            widget = self.add_verical_spacer()
        elif field['widgettype'] == 'textarea':
            widget = self.add_textarea(field)
        elif field['widgettype'] in ('spinbox'):
            widget = self.add_spinbox(field)
        elif field['widgettype'] == 'tableview':
            widget = self.add_tableview(db_return, field)
            widget = self.set_headers(widget, field)
            widget = self.populate_table(widget, field)
            widget = self.set_columns_config(widget,
                                             field['widgetname'],
                                             sort_order=1,
                                             isQStandardItemModel=True)
            utils_giswater.set_qtv_config(widget)
        widget.setObjectName(widget.property('columnname'))

        return label, widget
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()
Beispiel #15
0
class Recorder(QObject):
    '''
    classdocs
    '''

    recordingStarted = pyqtSignal(str, bool)

    def __init__(self, path, interval=1000, maxLines=10000, parent=None):
        '''
        Constructor
        '''
        super(Recorder, self).__init__(parent)
        self.path = path
        self.filePrefix = ''
        self.mobiles = None
        self.interval = 1000
        self.timer = QTimer()
        self.timer.timeout.connect(self.takeSnapshot)
        self.fileName = ''
        self.file = None
        self.lineCount = 0
        self.maxLines = 10000

    def setMobiles(self, mobiles):
        self.stopRecording()
        self.mobiles = mobiles

    def openFile(self):
        dt = datetime.utcnow()
        s = self.filePrefix + dt.strftime('%Y%m%d-%H%M%S') + '.csv'
        self.fileName = os.path.join(self.path, self.filePrefix, s)
        try:
            self.file = open(self.fileName, 'w')
            self.file.write(self.fileHeader())
            self.lineCount = 0
            self.recordingStarted.emit(self.fileName, True)
        except (IOError, FileNotFoundError):
            self.file = None
            self.recordingStarted.emit(self.fileName, False)

    @pyqtSlot()
    def startRecording(self):
        self.openFile()
        self.timer.start(self.interval)

    @pyqtSlot()
    def stopRecording(self):
        self.timer.stop()
        if self.file is not None:
            self.file.close()
            self.file = None

    @pyqtSlot()
    def takeSnapshot(self):
        if self.file is None:
            return
        dt = datetime.utcnow()
        line = dt.strftime('%d.%m.%Y\t%H:%M:%S')
        for v in self.mobiles.values():
            lat, lon, depth, heading, altitude = v.reportPosition()
            line += '\t{:.9f}\t{:.9f}\t{:.1f}\t{:.1f}\t{:.1f}'.format(
                lat, lon, depth, heading, altitude)
        line += '\n'
        try:
            self.file.write(line)
            self.lineCount += 1
            if self.lineCount > self.maxLines:
                self.file.close()
                self.openFile()
        except (IOError, ValueError):
            pass

    def fileHeader(self):
        header = 'Date\tTime'
        for k in self.mobiles:
            header = header + '\t' + k + ' Lat\t' + k + ' Lon\t' + k + ' Depth\t' + k + ' Heading\t' + k + ' Altitude'
        header += '\n'
        return header
class DriveOqEngineServerDialog(QDialog, FORM_CLASS):
    """
    Non-modal dialog to drive the OpenQuake Engine Server's API. Through this,
    it is possible to run calculations, delete them, list them, visualize
    their outputs and loading them as vector layers.
    """
    def __init__(self, iface, viewer_dock):
        self.iface = iface
        self.viewer_dock = viewer_dock  # needed to change the output_type
        QDialog.__init__(self)
        # Set up the user interface from Designer.
        self.setupUi(self)
        self.params_dlg = None
        self.console_dlg = None
        self.full_report_dlg = None
        # keep track of the log lines acquired for each calculation
        self.calc_log_line = {}
        self.session = None
        self.hostname = None
        self.current_calc_id = None  # list of outputs refers to this calc_id
        self.pointed_calc_id = None  # we will scroll to it
        self.is_logged_in = False
        self.timer = None
        # Keep retrieving the list of calculations (especially important to
        # update the status of the calculation)
        # NOTE: start_polling() is called from outside, in order to reset
        #       the timer whenever the button to open the dialog is pressed
        self.finished.connect(self.stop_polling)

        self.message_bar = QgsMessageBar(self)
        self.layout().insertWidget(0, self.message_bar)

        self.engine_version = None
        self.attempt_login()

    def attempt_login(self):
        try:
            self.login()
        except HANDLED_EXCEPTIONS as exc:
            self._handle_exception(exc)
        else:
            if self.is_logged_in:
                self.refresh_calc_list()
                self.check_engine_compatibility()
                self.setWindowTitle(
                    'Drive the OpenQuake Engine v%s (%s)' % (
                        self.engine_version, self.hostname))

    def check_engine_compatibility(self):
        engine_version = self.get_engine_version()
        assert engine_version is not None
        self.engine_version = engine_version.split('-')[0]
        engine_version = tuple(int(x) for x in self.engine_version.split('.'))
        irmt_version = get_irmt_version()
        irmt_version = tuple(int(x) for x in irmt_version.split('.'))
        irmt_major_minor = irmt_version[:2]
        engine_major_minor = engine_version[:2]
        if irmt_major_minor != engine_major_minor:
            msg = ('The plugin is optimized to work with OpenQuake Engine '
                   ' version %s.%s. You are currently connecting with '
                   ' OpenQuake Engine version %s.%s. This could cause some '
                   ' malfunctioning.' % (irmt_major_minor[0],
                                         irmt_major_minor[1],
                                         engine_major_minor[0],
                                         engine_major_minor[1]))
            log_msg(msg, level='W', message_bar=self.message_bar)

    def login(self):
        self.session = Session()
        self.hostname, username, password = get_credentials('engine')
        # try without authentication (if authentication is disabled server
        # side)
        # NOTE: check_is_lockdown() can raise exceptions,
        #       to be caught from outside
        is_lockdown = check_is_lockdown(self.hostname, self.session)
        if not is_lockdown:
            self.is_logged_in = True
            return
        with WaitCursorManager('Logging in...', self.message_bar):
            # it can raise exceptions, caught by self.attempt_login
            engine_login(self.hostname, username, password, self.session)
            # if no exception occurred
            self.is_logged_in = True
            return
        self.is_logged_in = False

    def get_engine_version(self):
        engine_version_url = "%s/v1/engine_version" % self.hostname
        with WaitCursorManager():
            try:
                # FIXME: enable the user to set verify=True
                resp = self.session.get(
                    engine_version_url, timeout=10, verify=False,
                    allow_redirects=False)
                if resp.status_code == 302:
                    raise RedirectionError(
                        "Error %s loading %s: please check the url" % (
                            resp.status_code, resp.url))
                if not resp.ok:
                    raise ServerError(
                        "Error %s loading %s: %s" % (
                            resp.status_code, resp.url, resp.reason))
            except HANDLED_EXCEPTIONS as exc:
                self._handle_exception(exc)
                return
            return resp.text

    def refresh_calc_list(self):
        # returns True if the list is correctly retrieved
        calc_list_url = "%s/v1/calc/list?relevant=true" % self.hostname
        with WaitCursorManager():
            try:
                # FIXME: enable the user to set verify=True
                resp = self.session.get(
                    calc_list_url, timeout=10, verify=False,
                    allow_redirects=False)
                if resp.status_code == 302:
                    raise RedirectionError(
                        "Error %s loading %s: please check the url" % (
                            resp.status_code, resp.url))
                if not resp.ok:
                    raise ServerError(
                        "Error %s loading %s: %s" % (
                            resp.status_code, resp.url, resp.reason))
            except HANDLED_EXCEPTIONS as exc:
                self._handle_exception(exc)
                return False
            calc_list = json.loads(resp.text)
        selected_keys = [
            'description', 'id', 'calculation_mode', 'owner', 'status']
        col_names = [
            'Description', 'Job ID', 'Calculation Mode', 'Owner', 'Status']
        col_widths = [340, 60, 135, 70, 80]
        if not calc_list:
            if self.calc_list_tbl.rowCount() > 0:
                self.calc_list_tbl.clearContents()
                self.calc_list_tbl.setRowCount(0)
            else:
                self.calc_list_tbl.setRowCount(0)
                self.calc_list_tbl.setColumnCount(len(col_names))
                self.calc_list_tbl.setHorizontalHeaderLabels(col_names)
                self.calc_list_tbl.horizontalHeader().setStyleSheet(
                    "font-weight: bold;")
                self.set_calc_list_widths(col_widths)
            return False
        actions = [
            {'label': 'Console', 'bg_color': '#3cb3c5', 'txt_color': 'white'},
            {'label': 'Remove', 'bg_color': '#d9534f', 'txt_color': 'white'},
            {'label': 'Outputs', 'bg_color': '#3cb3c5', 'txt_color': 'white'},
            {'label': 'Continue', 'bg_color': 'white', 'txt_color': 'black'}
        ]
        self.calc_list_tbl.clearContents()
        self.calc_list_tbl.setRowCount(len(calc_list))
        self.calc_list_tbl.setColumnCount(len(selected_keys) + len(actions))
        for row, calc in enumerate(calc_list):
            for col, key in enumerate(selected_keys):
                item = QTableWidgetItem()
                try:
                    value = calc_list[row][key]
                except KeyError:
                    # from engine2.5 to engine2.6, job_type was changed into
                    # calculation_mode. This check prevents the plugin to break
                    # wnen using an old version of the engine.
                    if key == 'calculation_mode':
                        value = 'unknown'
                    else:
                        # if any other unexpected keys are used, it is safer to
                        # raise a KeyError
                        raise
                item.setData(Qt.DisplayRole, value)
                # set default colors
                row_bg_color = Qt.white
                row_txt_color = Qt.black
                if calc['status'] == 'failed':
                    row_bg_color = QColor('#f2dede')
                elif calc['status'] == 'complete':
                    row_bg_color = QColor('#dff0d8')
                item.setBackgroundColor(row_bg_color)
                item.setTextColor(row_txt_color)
                self.calc_list_tbl.setItem(row, col, item)
            for col, action in enumerate(actions, len(selected_keys)):
                # display the Continue and Output buttons only if the
                # computation is completed
                if (calc['status'] != 'complete' and
                        action['label'] in ('Continue', 'Outputs')):
                    continue
                button = QPushButton()
                button.setText(action['label'])
                style = 'background-color: %s; color: %s' % (
                    action['bg_color'], action['txt_color'])
                button.setStyleSheet(style)
                QObject.connect(
                    button, SIGNAL("clicked()"),
                    lambda calc_id=calc['id'], action=action['label']: (
                        self.on_calc_action_btn_clicked(calc_id, action)))
                self.calc_list_tbl.setCellWidget(row, col, button)
                self.calc_list_tbl.setColumnWidth(col, BUTTON_WIDTH)
        empty_col_names = [''] * len(actions)
        headers = col_names + empty_col_names
        self.calc_list_tbl.setHorizontalHeaderLabels(headers)
        self.calc_list_tbl.horizontalHeader().setStyleSheet(
            "font-weight: bold;")
        self.set_calc_list_widths(col_widths)
        if self.pointed_calc_id:
            self.highlight_and_scroll_to_calc_id(self.pointed_calc_id)
        # if a running calculation is selected, the corresponding outputs will
        # be displayed (once) automatically at completion
        if (self.pointed_calc_id and
                self.output_list_tbl.rowCount() == 0):
            self.update_output_list(self.pointed_calc_id)
        return True

    def get_row_by_calc_id(self, calc_id):
        # find QTableItem corresponding to that calc_id
        calc_id_col_idx = 1
        for row in range(self.calc_list_tbl.rowCount()):
            item_calc_id = self.calc_list_tbl.item(row, calc_id_col_idx)
            if int(item_calc_id.text()) == calc_id:
                return row
        return None

    def highlight_and_scroll_to_calc_id(self, calc_id):
        row = self.get_row_by_calc_id(calc_id)
        if row is not None:
            self.calc_list_tbl.selectRow(row)
            calc_id_col_idx = 1
            item_calc_id = self.calc_list_tbl.item(row, calc_id_col_idx)
            self.calc_list_tbl.scrollToItem(
                item_calc_id, QAbstractItemView.PositionAtCenter)
        else:
            self.pointed_calc_id = None
            self.calc_list_tbl.clearSelection()

    def set_calc_list_widths(self, widths):
        for i, width in enumerate(widths):
            self.calc_list_tbl.setColumnWidth(i, width)
        self.calc_list_tbl.resizeRowsToContents()

    def clear_output_list(self):
        self.output_list_tbl.clearContents()
        self.output_list_tbl.setRowCount(0)
        self.output_list_tbl.setColumnCount(0)
        self.list_of_outputs_lbl.setText('List of outputs')
        self.download_datastore_btn.setEnabled(False)
        self.download_datastore_btn.setText(
            'Download HDF5 datastore')
        self.show_calc_params_btn.setEnabled(False)
        self.show_calc_params_btn.setText(
            'Show calculation parameters')

    def update_output_list(self, calc_id):
        calc_status = self.get_calc_status(calc_id)
        self.clear_output_list()
        if calc_status['status'] != 'complete':
            return
        output_list = self.get_output_list(calc_id)
        self.list_of_outputs_lbl.setText(
            'List of outputs for calculation %s' % calc_id)
        # from engine2.5 to engine2.6, job_type was changed into
        # calculation_mode. This check prevents the plugin to break wnen
        # using an old version of the engine.
        self.show_output_list(
            output_list, calc_status.get('calculation_mode', 'unknown'))
        self.download_datastore_btn.setEnabled(True)
        self.download_datastore_btn.setText(
            'Download HDF5 datastore for calculation %s'
            % calc_id)
        self.show_calc_params_btn.setEnabled(True)
        self.show_calc_params_btn.setText(
            'Show parameters for calculation %s' % calc_id)

    def on_calc_action_btn_clicked(self, calc_id, action):
        # NOTE: while scrolling through the list of calculations, the tool
        # keeps polling and refreshing the list, without losing the current
        # scrolling.  But if you click on any button, at the next refresh, the
        # view is scrolled to the top. Therefore we need to keep track of which
        # line was selected, in order to scroll to that line.
        self.current_calc_id = self.pointed_calc_id = calc_id
        self._set_show_calc_params_btn()
        self.highlight_and_scroll_to_calc_id(calc_id)
        if action == 'Console':
            self.update_output_list(calc_id)
            self.console_dlg = ShowConsoleDialog(self, calc_id)
            self.console_dlg.setWindowTitle(
                'Console log of calculation %s' % calc_id)
            self.console_dlg.show()
        elif action == 'Remove':
            confirmed = QMessageBox.question(
                self,
                'Remove calculation?',
                'The calculation will be removed permanently. Are you sure?',
                QMessageBox.Yes | QMessageBox.No)
            if confirmed == QMessageBox.Yes:
                self.remove_calc(calc_id)
                if self.current_calc_id == calc_id:
                    self.clear_output_list()
        elif action == 'Outputs':
            self.update_output_list(calc_id)
        elif action == 'Continue':
            self.update_output_list(calc_id)
            self.run_calc(calc_id)
        else:
            raise NotImplementedError(action)

    def get_calc_log(self, calc_id):
        if calc_id not in self.calc_log_line:
            self.calc_log_line[calc_id] = 0
        start = self.calc_log_line[calc_id]
        stop = ''  # get until the end
        calc_log_url = "%s/v1/calc/%s/log/%s:%s" % (
            self.hostname, calc_id, start, stop)
        with WaitCursorManager():
            try:
                # FIXME: enable the user to set verify=True
                resp = self.session.get(calc_log_url, timeout=10, verify=False)
            except HANDLED_EXCEPTIONS as exc:
                self._handle_exception(exc)
                return
            calc_log = json.loads(resp.text)
            self.calc_log_line[calc_id] = start + len(calc_log)
            return '\n'.join([','.join(row) for row in calc_log])

    def get_calc_status(self, calc_id):
        calc_status_url = "%s/v1/calc/%s/status" % (self.hostname, calc_id)
        with WaitCursorManager():
            try:
                # FIXME: enable the user to set verify=True
                resp = self.session.get(
                    calc_status_url, timeout=10, verify=False)
            except HANDLED_EXCEPTIONS as exc:
                self._handle_exception(exc)
                return
            calc_status = json.loads(resp.text)
            return calc_status

    def remove_calc(self, calc_id):
        calc_remove_url = "%s/v1/calc/%s/remove" % (self.hostname, calc_id)
        with WaitCursorManager('Removing calculation...', self.message_bar):
            try:
                resp = self.session.post(calc_remove_url, timeout=10)
            except HANDLED_EXCEPTIONS as exc:
                self._handle_exception(exc)
                return
        if resp.ok:
            msg = 'Calculation %s successfully removed' % calc_id
            log_msg(msg, level='I', message_bar=self.message_bar)
            if self.current_calc_id == calc_id:
                self.current_calc_id = None
                self.clear_output_list()
            if self.pointed_calc_id == calc_id:
                self.pointed_calc_id = None
                self.clear_output_list()
            self.refresh_calc_list()
        else:
            msg = 'Unable to remove calculation %s' % calc_id
            log_msg(msg, level='C', message_bar=self.message_bar)
        return

    def run_calc(self, calc_id=None, file_names=None, directory=None):
        """
        Run a calculation. If `calc_id` is given, it means we want to run
        a calculation re-using the output of the given calculation
        """
        text = self.tr('Select the files needed to run the calculation,'
                       ' or the zip archive containing those files.')
        if directory is None:
            default_dir = QSettings().value('irmt/run_oqengine_calc_dir',
                                            QDir.homePath())
        else:
            default_dir = directory
        if not file_names:
            file_names = QFileDialog.getOpenFileNames(self, text, default_dir)
        if not file_names:
            return
        if directory is None:
            selected_dir = QFileInfo(file_names[0]).dir().path()
            QSettings().setValue('irmt/run_oqengine_calc_dir', selected_dir)
        else:
            file_names = [os.path.join(directory, os.path.basename(file_name))
                          for file_name in file_names]
        if len(file_names) == 1:
            file_full_path = file_names[0]
            _, file_ext = os.path.splitext(file_full_path)
            if file_ext == '.zip':
                zipped_file_name = file_full_path
            else:
                # NOTE: an alternative solution could be to check if the single
                # file is .ini, to look for all the files specified in the .ini
                # and to build a zip archive with all them
                msg = "Please select all the files needed, or a zip archive"
                log_msg(msg, level='C', message_bar=self.message_bar)
                return
        else:
            _, zipped_file_name = tempfile.mkstemp()
            with zipfile.ZipFile(zipped_file_name, 'w') as zipped_file:
                for file_name in file_names:
                    zipped_file.write(file_name)
        run_calc_url = "%s/v1/calc/run" % self.hostname
        with WaitCursorManager('Starting calculation...', self.message_bar):
            if calc_id is not None:
                # FIXME: currently the web api is expecting a hazard_job_id
                # although it could be any kind of job_id. This will have to be
                # changed as soon as the web api is updated.
                data = {'hazard_job_id': calc_id}
            else:
                data = {}
            files = {'archive': open(zipped_file_name, 'rb')}
            try:
                resp = self.session.post(
                    run_calc_url, files=files, data=data, timeout=20)
            except HANDLED_EXCEPTIONS as exc:
                self._handle_exception(exc)
                return
        if resp.ok:
            self.refresh_calc_list()
            return resp.json()
        else:
            log_msg(resp.text, level='C', message_bar=self.message_bar)

    def on_same_fs(self, checksum_file_path, ipt_checksum):
        on_same_fs_url = "%s/v1/on_same_fs" % self.hostname
        data = {'filename': checksum_file_path, 'checksum': str(ipt_checksum)}
        try:
            resp = self.session.post(on_same_fs_url, data=data, timeout=20)
        except HANDLED_EXCEPTIONS as exc:
            self._handle_exception(exc)
            return False
        try:
            result = json.loads(resp.text)['success']
        except Exception as exc:
            log_msg(str(exc), level='C', message_bar=self.iface.messageBar())
            return False
        else:
            return result

    @pyqtSlot(int, int)
    def on_calc_list_tbl_cellClicked(self, row, column):
        self.calc_list_tbl.selectRow(row)
        # find QTableItem corresponding to that calc_id
        calc_id_col_idx = 1
        item_calc_id = self.calc_list_tbl.item(row, calc_id_col_idx)
        calc_id = int(item_calc_id.text())
        if self.pointed_calc_id == calc_id:
            # if you click again on the row that was selected, it unselects it
            self.pointed_calc_id = None
            self.calc_list_tbl.clearSelection()
        else:
            self.pointed_calc_id = calc_id
            self._set_show_calc_params_btn()
        self.update_output_list(calc_id)
        self._set_show_calc_params_btn()

    def _set_show_calc_params_btn(self):
        self.show_calc_params_btn.setEnabled(
            self.current_calc_id is not None)
        if self.current_calc_id is not None:
            self.show_calc_params_btn.setText(
                'Show parameters for calculation %s'
                % self.current_calc_id)
        else:
            self.show_calc_params_btn.setText('Show calculation parameters')

    @pyqtSlot()
    def on_download_datastore_btn_clicked(self):
        dest_folder = ask_for_download_destination_folder(self)
        if not dest_folder:
            return
        datastore_url = "%s/v1/calc/%s/datastore" % (
            self.hostname, self.current_calc_id)
        with WaitCursorManager('Getting HDF5 datastore...',
                               self.message_bar):
            try:
                # FIXME: enable the user to set verify=True
                resp = self.session.get(datastore_url, timeout=10,
                                        verify=False)
            except HANDLED_EXCEPTIONS as exc:
                self._handle_exception(exc)
                return
            filename = resp.headers['content-disposition'].split(
                'filename=')[1]
            filepath = os.path.join(dest_folder, os.path.basename(filename))
            open(filepath, "wb").write(resp.content)
            log_msg('The datastore has been saved as %s' % filepath,
                    level='I', message_bar=self.message_bar)

    @pyqtSlot()
    def on_show_calc_params_btn_clicked(self):
        self.params_dlg = ShowParamsDialog()
        self.params_dlg.setWindowTitle(
            'Parameters of calculation %s' % self.current_calc_id)
        get_calc_params_url = "%s/v1/calc/%s/oqparam" % (
            self.hostname, self.current_calc_id)
        with WaitCursorManager('Getting calculation parameters...',
                               self.message_bar):
            try:
                # FIXME: enable the user to set verify=True
                resp = self.session.get(get_calc_params_url, timeout=10,
                                        verify=False)
            except HANDLED_EXCEPTIONS as exc:
                self._handle_exception(exc)
                return
            json_params = json.loads(resp.text)
            indented_params = json.dumps(json_params, indent=4)
            self.params_dlg.text_browser.setText(indented_params)
        self.params_dlg.show()

    def get_output_list(self, calc_id):
        output_list_url = "%s/v1/calc/%s/results" % (self.hostname, calc_id)
        with WaitCursorManager():
            try:
                # FIXME: enable the user to set verify=True
                resp = self.session.get(output_list_url, timeout=10,
                                        verify=False)
            except HANDLED_EXCEPTIONS as exc:
                self._handle_exception(exc)
                return
        if resp.ok:
            output_list = json.loads(resp.text)
            self.current_calc_id = calc_id
            return output_list
        else:
            return []

    def show_output_list(self, output_list, calculation_mode):
        if not output_list:
            self.clear_output_list()
            self.download_datastore_btn.setEnabled(False)
            self.download_datastore_btn.setText('Download HDF5 datastore')
            return
        exclude = ['url', 'outtypes', 'type']
        selected_keys = [key for key in sorted(output_list[0].keys())
                         if key not in exclude]
        max_actions = 0
        for row in output_list:
            num_actions = len(row['outtypes'])
            if row['type'] in (OQ_TO_LAYER_TYPES |
                               OQ_RST_TYPES |
                               OQ_EXTRACT_TO_VIEW_TYPES):
                # TODO: remove check when gmf_data will be loadable also for
                #       event_based
                if not (row['type'] == 'gmf_data'
                        and 'event_based' in calculation_mode):
                    num_actions += 1  # needs additional column for loader btn
            if "%s_aggr" % row['type'] in OQ_EXTRACT_TO_VIEW_TYPES:
                num_actions += 1
            max_actions = max(max_actions, num_actions)

        self.output_list_tbl.setRowCount(len(output_list))
        self.output_list_tbl.setColumnCount(
            len(selected_keys) + max_actions)
        for row, output in enumerate(output_list):
            for col, key in enumerate(selected_keys):
                item = QTableWidgetItem()
                value = output_list[row][key]
                item.setData(Qt.DisplayRole, value)
                self.output_list_tbl.setItem(row, col, item)
            outtypes = output_list[row]['outtypes']
            for col, outtype in enumerate(outtypes, len(selected_keys)):
                action = 'Download'
                button = QPushButton()
                self.connect_button_to_action(button, action, output, outtype)
                self.output_list_tbl.setCellWidget(row, col, button)
                self.calc_list_tbl.setColumnWidth(col, BUTTON_WIDTH)
                if output['type'] in (OQ_TO_LAYER_TYPES |
                                      OQ_RST_TYPES |
                                      OQ_EXTRACT_TO_VIEW_TYPES):
                    if output['type'] in (OQ_RST_TYPES |
                                          OQ_EXTRACT_TO_VIEW_TYPES):
                        action = 'Show'
                    else:
                        action = 'Load as layer'
                    # TODO: remove check when gmf_data will be loadable also
                    #       for event_based
                    if (output['type'] == 'gmf_data'
                            and calculation_mode == 'event_based'):
                        continue
                    button = QPushButton()
                    self.connect_button_to_action(
                        button, action, output, outtype)
                    self.output_list_tbl.setCellWidget(row, col + 1, button)
                if "%s_aggr" % output['type'] in OQ_EXTRACT_TO_VIEW_TYPES:
                    mod_output = copy.deepcopy(output)
                    mod_output['type'] = "%s_aggr" % output['type']
                    button = QPushButton()
                    self.connect_button_to_action(
                        button, 'Aggregate', mod_output, outtype)
                    self.output_list_tbl.setCellWidget(row, col + 2, button)
        col_names = [key.capitalize() for key in selected_keys]
        empty_col_names = ['' for outtype in range(max_actions)]
        headers = col_names + empty_col_names
        self.output_list_tbl.setHorizontalHeaderLabels(headers)
        self.output_list_tbl.horizontalHeader().setStyleSheet(
            "font-weight: bold;")
        self.output_list_tbl.resizeColumnsToContents()
        self.output_list_tbl.resizeRowsToContents()

    def connect_button_to_action(self, button, action, output, outtype):
        if action in ('Load as layer', 'Show', 'Aggregate'):
            style = 'background-color: blue; color: white;'
            if action == 'Load as layer':
                button.setText("Load layer")
            elif action == 'Aggregate':
                button.setText("Aggregate")
            else:
                button.setText("Show")
        else:
            style = 'background-color: #3cb3c5; color: white;'
            button.setText("%s %s" % (action, outtype))
        button.setStyleSheet(style)
        QObject.connect(
            button, SIGNAL("clicked()"),
            lambda output=output, action=action, outtype=outtype: (
                self.on_output_action_btn_clicked(output, action, outtype))
        )

    def on_output_action_btn_clicked(self, output, action, outtype):
        output_id = output['id']
        output_type = output['type']
        if action in ['Show', 'Aggregate']:
            dest_folder = tempfile.gettempdir()
            if output_type in OQ_EXTRACT_TO_VIEW_TYPES:
                self.viewer_dock.load_no_map_output(
                    self.current_calc_id, self.session,
                    self.hostname, output_type, self.engine_version)
            elif outtype == 'rst':
                filepath = self.download_output(
                    output_id, outtype, dest_folder)
                if not filepath:
                    return
                # NOTE: it might be created here directly instead, but this way
                # we can use the qt-designer
                self.full_report_dlg = ShowFullReportDialog(filepath)
                self.full_report_dlg.setWindowTitle(
                    'Full report of calculation %s' %
                    self.current_calc_id)
                self.full_report_dlg.show()
            else:
                raise NotImplementedError("%s %s" % (action, outtype))
        elif action == 'Load as layer':
            filepath = None
            if output_type not in OUTPUT_TYPE_LOADERS:
                raise NotImplementedError(output_type)
            if outtype == 'csv':
                dest_folder = tempfile.gettempdir()
                filepath = self.download_output(
                    output_id, outtype, dest_folder)
                if not filepath:
                    return
            if outtype in ('npz', 'csv'):
                dlg = OUTPUT_TYPE_LOADERS[output_type](
                    self.iface, self.viewer_dock,
                    self.session, self.hostname, self.current_calc_id,
                    output_type, path=filepath,
                    engine_version=self.engine_version)
                dlg.exec_()
            else:
                raise NotImplementedError("%s %s" % (action, outtype))
        elif action == 'Download':
            filepath = self.download_output(output_id, outtype)
            if not filepath:
                return
            msg = 'Calculation %s was saved as %s' % (output_id, filepath)
            log_msg(msg, level='I', message_bar=self.message_bar)
        else:
            raise NotImplementedError(action)

    def download_output(self, output_id, outtype, dest_folder=None):
        if not dest_folder:
            dest_folder = ask_for_download_destination_folder(self)
            if not dest_folder:
                return
        output_download_url = (
            "%s/v1/calc/result/%s?export_type=%s&dload=true" % (self.hostname,
                                                                output_id,
                                                                outtype))
        with WaitCursorManager('Downloading output...',
                               self.message_bar):
            try:
                # FIXME: enable the user to set verify=True
                resp = self.session.get(output_download_url, verify=False)
            except HANDLED_EXCEPTIONS as exc:
                self._handle_exception(exc)
                return
            if not resp.ok:
                err_msg = (
                    'Unable to download the output.\n%s: %s.\n%s'
                    % (resp.status_code, resp.reason, resp.text))
                log_msg(err_msg, level='C',
                        message_bar=self.message_bar)
                return
            filename = resp.headers['content-disposition'].split(
                'filename=')[1]
            filepath = os.path.join(dest_folder, filename)
            open(filepath, "wb").write(resp.content)
        return filepath

    def start_polling(self):
        if not self.is_logged_in:
            try:
                self.login()
            except HANDLED_EXCEPTIONS as exc:
                self._handle_exception(exc)
        if not self.is_logged_in:
            return
        self.refresh_calc_list()
        self.timer = QTimer()
        QObject.connect(
            self.timer, SIGNAL('timeout()'), self.refresh_calc_list)
        self.timer.start(5000)  # refresh calc list time in milliseconds

    def stop_polling(self):
        # NOTE: perhaps we should disconnect the timeout signal here?
        if hasattr(self, 'timer') and self.timer is not None:
            self.timer.stop()
        # QObject.disconnect(self.timer, SIGNAL('timeout()'))

    @pyqtSlot()
    def on_run_calc_btn_clicked(self):
        self.run_calc()

    def _handle_exception(self, exc):
        if isinstance(exc, SSLError):
            err_msg = '; '.join(exc.message.message.strerror.message[0])
            err_msg += ' (you could try prepending http:// or https://)'
            log_msg(err_msg, level='C', message_bar=self.iface.messageBar())
        elif isinstance(exc, (ConnectionError,
                              InvalidSchema,
                              MissingSchema,
                              ReadTimeout,
                              LocationParseError,
                              ServerError,
                              RedirectionError,
                              SvNetworkError)):
            err_msg = str(exc)
            if isinstance(exc, InvalidSchema):
                err_msg += ' (you could try prepending http:// or https://)'
            elif isinstance(exc, ConnectionError):
                err_msg += (
                    ' (please make sure the OpenQuake Engine WebUI'
                    ' is running)')
            elif isinstance(exc, (SvNetworkError, ServerError)):
                err_msg += (
                    ' (please make sure the username and password are'
                    ' spelled correctly)')
            elif isinstance(exc, RedirectionError):
                pass  # err_msg should already be enough
            else:
                err_msg += (
                    ' (please make sure the username and password are'
                    ' spelled correctly and that you are using the right'
                    ' url and port in the host setting)')
            log_msg(err_msg, level='C', message_bar=self.iface.messageBar())
        else:
            # sanity check (it should never occur)
            raise TypeError(
                'Unable to handle exception of type %s' % type(exc))
        self.is_logged_in = False
        self.reject()
        SettingsDialog(self.iface).exec_()

    def reject(self):
        self.stop_polling()
        super(DriveOqEngineServerDialog, self).reject()
class Dimensions(ParentDialog):
    
    def __init__(self, dialog, layer, feature):
        """ Constructor class """
        
        self.id = None        
        super(Dimensions, self).__init__(dialog, layer, feature)      
        self.init_config_form()
        if dialog.parent():        
            dialog.parent().setFixedSize(320, 410)
        
        
    def init_config_form(self):
        """ Custom form initial configuration """

        self.canvas = self.iface.mapCanvas()
        self.emit_point = QgsMapToolEmitPoint(self.canvas)
        self.canvas.setMapTool(self.emit_point)

        # Snapper
        self.snapper_manager = SnappingConfigManager(self.iface)
        self.snapper = self.snapper_manager.get_snapper()

        # Configure button signals
        btn_orientation = self.dialog.findChild(QPushButton, "btn_orientation")
        btn_orientation.clicked.connect(self.orientation)
        self.set_icon(btn_orientation, "133")
        btn_snapping = self.dialog.findChild(QPushButton, "btn_snapping")
        btn_snapping.clicked.connect(self.snapping)
        self.set_icon(btn_snapping, "129")
        
        # Set layers dimensions, node and connec
        self.layer_dimensions = self.controller.get_layer_by_tablename("v_edit_dimensions")
        self.layer_node = self.controller.get_layer_by_tablename("v_edit_node")
        self.layer_connec = self.controller.get_layer_by_tablename("v_edit_connec")

        self.create_map_tips()
        
        
    def orientation(self):
        
        self.emit_point.canvasClicked.connect(self.click_button_orientation)


    def snapping(self):  
                   
        # Set active layer and set signals
        self.iface.setActiveLayer(self.layer_node)
        self.canvas.xyCoordinates.connect(self.mouse_move)
        self.emit_point.canvasClicked.connect(self.click_button_snapping)


    def mouse_move(self, point):

        # Hide marker and get coordinates
        self.snapper_manager.remove_marker()
        event_point = self.snapper_manager.get_event_point(point=point)

        # Snapping
        result = self.snapper_manager.snap_to_background_layers(event_point)
        if self.snapper_manager.result_is_valid():
            layer = self.snapper_manager.get_snapped_layer(result)
            # Check feature
            if layer == self.layer_node or layer == self.layer_connec:
                self.snapper_manager.add_marker(result)

        
    def click_button_orientation(self, point):  # @UnusedVariable

        if not self.layer_dimensions:
            return   

        self.x_symbol = self.dialog.findChild(QLineEdit, "x_symbol")
        self.x_symbol.setText(str(int(point.x())))
        self.y_symbol = self.dialog.findChild(QLineEdit, "y_symbol")
        self.y_symbol.setText(str(int(point.y())))
        

    def click_button_snapping(self, point, btn):  # @UnusedVariable

        if not self.layer_dimensions:
            return   
             
        layer = self.layer_dimensions
        self.iface.setActiveLayer(layer)
        layer.startEditing()

        # Get coordinates
        event_point = self.snapper_manager.get_event_point(point=point)
                     
        # Snapping
        result = self.snapper_manager.snap_to_background_layers(event_point)
        if self.snapper_manager.result_is_valid():

            layer = self.snapper_manager.get_snapped_layer(result)
            # Check feature
            if layer == self.layer_node:
                feat_type = 'node'
            elif layer == self.layer_connec:
                feat_type = 'connec'
            else:
                return

            # Get the point
            snapped_feat = self.snapper_manager.get_snapped_feature(result)
            feature_id = self.snapper_manager.get_snapped_feature_id(result)
            element_id = snapped_feat.attribute(feat_type + '_id')

            # Leave selection
            layer.select([feature_id])

            # Get depth of the feature
            if self.project_type == 'ws':
                fieldname = "depth"
            elif self.project_type == 'ud' and feat_type == 'node':
                fieldname = "ymax"
            elif self.project_type == 'ud' and feat_type == 'connec':
                fieldname = "connec_depth"

            sql = ("SELECT " + fieldname + " "
                   "FROM " + feat_type + " "
                   "WHERE " + feat_type + "_id = '" + element_id + "'")
            row = self.controller.get_row(sql)
            if not row:
                return

            utils_giswater.setText(self.dialog, "depth", row[0])
            utils_giswater.setText(self.dialog, "feature_id", element_id)
            utils_giswater.setText(self.dialog, "feature_type", feat_type.upper())

    
    def create_map_tips(self):
        """ Create MapTips on the map """

        row = self.controller.get_config('dim_tooltip')
        if row and row[0].lower() != 'true':
            return

        self.timer_map_tips = QTimer(self.canvas)
        self.map_tip_node = QgsMapTip()
        self.map_tip_connec = QgsMapTip()

        self.canvas.xyCoordinates.connect(self.map_tip_changed)
        self.timer_map_tips.timeout.connect(self.show_map_tip)
        self.timer_map_tips_clear = QTimer(self.canvas)
        self.timer_map_tips_clear.timeout.connect(self.clear_map_tip)


    def map_tip_changed(self, point):
        """ SLOT. Initialize the Timer to show MapTips on the map """
        
        if self.canvas.underMouse(): 
            self.last_map_position = QgsPointXY(point.x(), point.y())
            self.map_tip_node.clear(self.canvas)
            self.map_tip_connec.clear(self.canvas)
            self.timer_map_tips.start(100)

            
    def show_map_tip(self):
        """ Show MapTips on the map """
        
        self.timer_map_tips.stop()
        if self.canvas.underMouse(): 
            point_qgs = self.last_map_position
            point_qt = self.canvas.mouseLastXY()
            if self.layer_node:
                self.map_tip_node.showMapTip(self.layer_node, point_qgs, point_qt, self.canvas)
            if self.layer_connec:
                self.map_tip_connec.showMapTip(self.layer_connec, point_qgs, point_qt, self.canvas)
            self.timer_map_tips_clear.start(1000)
                                            
            
    def clear_map_tip(self):
        """ Clear MapTips """
        
        self.timer_map_tips_clear.stop()
        self.map_tip_node.clear(self.canvas)         
        self.map_tip_connec.clear(self.canvas)    
            
        
    def reject_dialog(self):
        """ Reject dialog without saving """ 
        
        self.set_action_identify()
        try: 
            self.canvas.xyCoordinates.disconnect()                                            
            self.canvas.timeout.disconnect()           
        except Exception:
            pass            
                 
                 
    def save(self):
        """ Save feature """
        
        # General save
        self.dialog.save()     
        self.iface.actionSaveEdits().trigger()    
        self.reject_dialog()                                  
            
Beispiel #18
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
class AnalysisTool(QObject):
    editDatastoreSettings = pyqtSignal()

    def __init__(self, iface, settings, project):
        QObject.__init__(self)

        self.iface = iface
        self.settings = settings
        self.project = project
        self.legend = QgsProject.instance().mapLayers()
        self.analysis_engine = DepthmapNetEngine(self.iface)
        self.running_analysis = ''

    def load(self):
        # initialise UI
        self.dlg = AnalysisDialog(self.iface.mainWindow())

        # initialise axial analysis classes
        self.verificationThread = None

        # connect signal/slots with main program
        self.project.settingsUpdated.connect(self.setDatastore)
        self.editDatastoreSettings.connect(self.project.showDialog)

        # set up GUI signals
        self.dlg.visibilityChanged.connect(self.onShow)
        self.dlg.analysisDataButton.clicked.connect(self.changeDatastore)
        self.dlg.updateDatastore.connect(self.updateDatastore)
        self.dlg.axialVerifyButton.clicked.connect(self.runAxialVerification)
        self.dlg.axialUpdateButton.clicked.connect(self.runAxialUpdate)
        self.dlg.axialVerifyCancelButton.clicked.connect(
            self.cancelAxialVerification)
        self.dlg.axialReportList.itemSelectionChanged.connect(
            self.zoomAxialProblem)
        self.dlg.axialDepthmapCalculateButton.clicked.connect(
            self.run_analysis)
        self.dlg.axialDepthmapCancelButton.clicked.connect(
            self.cancel_analysis)

        # initialise internal globals
        self.isVisible = False
        self.datastore = dict()
        self.running_analysis = ""
        self.start_time = None
        self.end_time = None
        self.analysis_nodes = 0
        self.axial_id = ""
        self.all_ids = []
        self.current_layer = None

        # timer to check for analysis result
        self.timer = QTimer()
        self.timer.timeout.connect(self.check_analysis_progress)

        # define analysis data structures
        self.analysis_layers = {'map': "", 'unlinks': "", 'map_type': 0}
        self.axial_analysis_settings = {
            'type': 0,
            'distance': 0,
            'radius': 0,
            'rvalues': "n",
            'output': "",
            'fullset': 0,
            'betweenness': 1,
            'newnorm': 1,
            'weight': 0,
            'weightBy': "",
            'stubs': 40,
            'id': ""
        }
        self.user_ids = {'map': "", 'unlinks': ""}
        self.analysis_output = ""
        self.getProjectSettings()

    def unload(self):
        if self.isVisible:
            # Disconnect signals from main program
            QgsProject.instance().layersAdded.disconnect(self.updateLayers)
            QgsProject.instance().layersRemoved.disconnect(self.updateLayers)
            self.iface.projectRead.disconnect(self.updateLayers)
            self.iface.projectRead.disconnect(self.getProjectSettings)
            self.iface.newProjectCreated.disconnect(self.updateLayers)
        self.isVisible = False

    def onShow(self):
        if self.dlg.isVisible():
            # Connect signals to QGIS interface
            QgsProject.instance().layersAdded.connect(self.updateLayers)
            QgsProject.instance().layersRemoved.connect(self.updateLayers)
            self.iface.projectRead.connect(self.updateLayers)
            self.iface.projectRead.connect(self.getProjectSettings)
            self.iface.newProjectCreated.connect(self.updateLayers)
            self.updateLayers()
            self.setDatastore()
            self.isVisible = True
        else:
            if self.isVisible:
                # Disconnect signals to QGIS interface
                QgsProject.instance().layersAdded.disconnect(self.updateLayers)
                QgsProject.instance().layersRemoved.disconnect(
                    self.updateLayers)
                self.iface.projectRead.disconnect(self.updateLayers)
                self.iface.projectRead.disconnect(self.getProjectSettings)
                self.iface.newProjectCreated.disconnect(self.updateLayers)
            self.isVisible = False

    ##
    ## manage project and tool settings
    ##
    def getProjectSettings(self):
        # pull relevant settings from project manager
        self.project.readSettings(self.analysis_layers, "analysis")
        self.project.readSettings(self.axial_analysis_settings, "depthmap")
        # update UI
        self.dlg.clearAxialProblems(0)
        self.dlg.clearAxialProblems(1)
        # update graph analysis
        self.dlg.set_axial_depthmap_tab(self.axial_analysis_settings)

    def updateProjectSettings(self):
        self.project.writeSettings(self.analysis_layers, "analysis")
        self.project.writeSettings(self.axial_analysis_settings, "depthmap")

    def changeDatastore(self):
        # signal from UI if data store button is clicked
        self.editDatastoreSettings.emit()

    def updateDatastore(self, name):
        new_datastore = {
            'name': '',
            'path': '',
            'type': -1,
            'schema': '',
            'crs': ''
        }
        layer = lfh.getLegendLayerByName(self.iface, name)
        if layer:
            new_datastore['crs'] = layer.crs().postgisSrid()
            if 'SpatiaLite' in layer.storageType():
                new_datastore['type'] = 1
                path = lfh.getLayerPath(layer)
                dbname = os.path.basename(path)
                new_datastore['path'] = path
                new_datastore['name'] = dbname
                # create a new connection if not exists
                conn = dbh.listSpatialiteConnections()
                if path not in conn['path']:
                    dbh.createSpatialiteConnection(dbname, path)
            elif 'PostGIS' in layer.storageType():
                new_datastore['type'] = 2
                layerinfo = dbh.getPostgisLayerInfo(layer)
                if layerinfo['service']:
                    path = layerinfo['service']
                else:
                    path = layerinfo['database']
                new_datastore['path'] = path
                new_datastore['schema'] = layerinfo['schema']
                if 'connection' in layerinfo:
                    new_datastore['name'] = layerinfo['connection']
                else:
                    # create a new connection if not exists
                    dbh.createPostgisConnectionSetting(
                        path, dbh.getPostgisConnectionInfo(layer))
                    new_datastore['name'] = path
            elif 'memory?' not in layer.storageType():  # 'Shapefile'
                new_datastore['type'] = 0
                new_datastore['path'] = lfh.getLayerPath(layer)
                new_datastore['name'] = os.path.basename(new_datastore['path'])
            if new_datastore['type'] in (0, 1, 2):
                self.project.writeSettings(new_datastore, 'datastore')
                self.setDatastore()
            else:
                return
        else:
            return

    def clearDatastore(self):
        new_datastore = {
            'name': '',
            'path': '',
            'type': -1,
            'schema': '',
            'crs': ''
        }
        self.project.writeSettings(new_datastore, 'datastore')
        self.setDatastore()

    def setDatastore(self):
        self.datastore = self.project.getGroupSettings('datastore')
        if 'type' in self.datastore:
            self.datastore['type'] = int(self.datastore['type'])
        else:
            self.datastore['type'] = -1
        # update UI
        txt = ""
        path = ""
        if 'name' in self.datastore and self.datastore['name'] != "":
            # get elements for string to identify data store for user
            # shape file data store
            if self.datastore['type'] == 0 and os.path.exists(
                    self.datastore['path']):
                txt = 'SF: %s' % self.datastore['name']
                path = self.datastore['path']
            # spatialite data store
            elif self.datastore['type'] == 1 and os.path.exists(
                    self.datastore['path']):
                sl_connections = dbh.listSpatialiteConnections()
                if len(sl_connections) > 0:
                    if self.datastore['name'] in sl_connections['name'] and self.datastore['path'] == \
                            sl_connections['path'][sl_connections['name'].index(self.datastore['name'])]:
                        txt = 'SL: %s' % self.datastore['name']
                        path = self.datastore['path']
                else:
                    dbh.createSpatialiteConnection(self.datastore['name'],
                                                   self.datastore['path'])
                    txt = 'SL: %s' % self.datastore['name']
                    path = self.datastore['path']
            # postgis data store
            elif self.datastore['type'] == 2 and len(
                    dbh.listPostgisConnectionNames()) > 0:
                if self.datastore['name'] in dbh.listPostgisConnectionNames():
                    txt = 'PG: %s (%s)' % (self.datastore['name'],
                                           self.datastore['schema'])
                    path = """dbname='%s' schema='%s'""" % (
                        self.datastore['path'], self.datastore['schema'])
        self.dlg.setDatastore(txt, path)

    def isDatastoreSet(self):
        is_set = False
        if self.datastore:
            name = self.datastore['name']
            path = self.datastore['path']
            schema = self.datastore['schema']
            if name == "":
                self.clearDatastore()
                self.iface.messageBar().pushMessage(
                    "Info",
                    "Select a 'Data store' to save analysis results.",
                    level=0,
                    duration=5)
            elif self.datastore['type'] == 0 and not os.path.exists(path):
                is_set = False
            elif self.datastore['type'] == 1 and (
                    name not in dbh.listSpatialiteConnections()['name']
                    or not os.path.exists(path)):
                is_set = False
            elif self.datastore['type'] == 2 and (
                    name not in dbh.listPostgisConnectionNames()
                    or schema not in dbh.listPostgisSchemas(
                        dbh.getPostgisConnection(name))):
                is_set = False
            else:
                is_set = True
            # clear whatever data store settings are saved
            if not is_set:
                self.clearDatastore()
                self.iface.messageBar().pushMessage(
                    "Info",
                    "The selected data store cannot be found.",
                    level=0,
                    duration=5)
        else:
            self.clearDatastore()
            self.iface.messageBar().pushMessage(
                "Info",
                "Select a 'Data store' to save analysis results.",
                level=0,
                duration=5)
        return is_set

    ##
    ## Manage layers
    ##
    def updateLayers(self):
        if self.iface.actionMapTips().isChecked():
            self.iface.actionMapTips().trigger()
        # layer names by geometry type
        map_list = []
        unlinks_list = []
        # default selection
        analysis_map = -1
        analysis_unlinks = -1
        map_type = 0
        layers = lfh.getLegendLayers(self.iface, 'all', 'all')
        if layers:
            for layer in layers:
                # checks if the layer is projected. Geographic coordinates are not supported
                if layer.isSpatial() and lfh.isLayerProjected(layer):
                    unlinks_list.append(layer.name())
                    if layer.geometryType() == 1:  # line geometry
                        map_list.append(layer.name())
            # settings preference
            if self.analysis_layers['map'] in map_list:
                analysis_map = map_list.index(self.analysis_layers['map'])
                map_type = self.analysis_layers['map_type']
            if self.analysis_layers['unlinks'] in unlinks_list:
                analysis_unlinks = unlinks_list.index(
                    self.analysis_layers['unlinks'])
            # current selection
            selected_layers = self.dlg.getAnalysisLayers()
            if selected_layers['map'] != '' and selected_layers[
                    'map'] in map_list:
                analysis_map = map_list.index(selected_layers['map'])
                map_type = selected_layers['map_type']
            if selected_layers['unlinks'] != '' and selected_layers[
                    'unlinks'] in unlinks_list:
                analysis_unlinks = unlinks_list.index(
                    selected_layers['unlinks'])
        else:
            self.dlg.clear_analysis_tab()

        # update UI
        self.dlg.set_map_layers(map_list, analysis_map, map_type)
        self.dlg.set_unlinks_layers(unlinks_list, analysis_unlinks)
        self.dlg.update_analysis_tabs()
        self.dlg.update_analysis_tab()

    ##
    ## Layer verification functions
    ##
    def runAxialVerification(self):
        self.edit_mode = self.dlg.getLayerTab()
        self.analysis_layers = self.dlg.getAnalysisLayers()
        axial = lfh.getLegendLayerByName(self.iface,
                                         self.analysis_layers['map'])
        unlinks = lfh.getLegendLayerByName(self.iface,
                                           self.analysis_layers['unlinks'])
        settings = self.dlg.getAxialEditSettings()
        caps = None
        self.axial_id = lfh.getIdField(axial)
        if self.axial_id == '':
            self.iface.messageBar().pushMessage(
                "Info",
                "The axial layer has invalid values in the ID column. Using feature ids.",
                level=0,
                duration=3)
        # verify axial map
        if self.edit_mode == 0:
            # get ids (to match the object ids in the map)
            self.user_ids['map'] = "%s" % self.axial_id
            if axial.geometryType() == QgsWkbTypes.LineGeometry:
                caps = axial.dataProvider().capabilities()
                self.verificationThread = AxialVerification(
                    self.iface.mainWindow(), self, settings, axial,
                    self.user_ids['map'], unlinks)
            else:
                self.iface.messageBar().pushMessage(
                    "Info",
                    "Select an axial lines map layer.",
                    level=0,
                    duration=3)
                return False
        # verify unlinks
        elif self.edit_mode == 1:
            if unlinks and (axial.storageType() != unlinks.storageType()):
                self.iface.messageBar().pushMessage(
                    "Warning",
                    "All layers must be in the same file format.",
                    level=1,
                    duration=3)
                return False
            caps = unlinks.dataProvider().capabilities()
            self.user_ids['unlinks'] = lfh.getIdField(unlinks)
            if self.user_ids['unlinks'] == '':
                self.iface.messageBar().pushMessage(
                    "Info",
                    "The unlinks layer has invalid values in the ID column. Using feature ids.",
                    level=0,
                    duration=3)
            if unlinks.dataProvider().fieldNameIndex("line1") == -1 or \
                    unlinks.dataProvider().fieldNameIndex("line2") == -1:
                self.iface.messageBar().pushMessage(
                    "Warning",
                    "Line ID columns missing in unlinks layer, please 'Update IDs'.",
                    level=1,
                    duration=3)
                return False
            else:
                self.verificationThread = UnlinksVerification(
                    self.iface.mainWindow(), self, settings, axial,
                    self.axial_id, unlinks, self.user_ids['unlinks'])
        if not caps & QgsVectorDataProvider.AddFeatures:
            self.iface.messageBar().pushMessage(
                "Info",
                "To edit the selected layer, change to another file format.",
                level=0,
                duration=3)
        # prepare dialog
        self.dlg.lockLayerTab(True)
        self.dlg.setAxialVerifyProgressbar(0, 100)
        self.dlg.lockAxialEditTab(True)
        self.dlg.clearAxialVerifyReport()
        self.dlg.clearAxialProblems()
        if self.verificationThread:
            self.verificationThread.verificationFinished.connect(
                self.processAxialVerificationResults)
            self.verificationThread.verificationProgress.connect(
                self.dlg.updateAxialVerifyProgressbar)
            self.verificationThread.verificationError.connect(
                self.cancelAxialVerification)
            self.verificationThread.start()
        return

    def runAxialUpdate(self):
        self.edit_mode = self.dlg.getLayerTab()
        self.analysis_layers = self.dlg.getAnalysisLayers()
        axial = lfh.getLegendLayerByName(self.iface,
                                         self.analysis_layers['map'])
        unlinks = lfh.getLegendLayerByName(self.iface,
                                           self.analysis_layers['unlinks'])
        settings = self.dlg.getAxialEditSettings()
        self.axial_id = lfh.getIdField(axial)
        if self.axial_id == '':
            self.iface.messageBar().pushMessage(
                "Info",
                "The axial layer has invalid or duplicate values in the id column. Using feature ids instead.",
                level=0,
                duration=5)
        # update axial id
        if self.edit_mode == 0:
            self.user_ids['map'] = "%s" % self.axial_id
            # todo: update axial ids when layer is shapefile
        # update unlink line ids
        elif self.edit_mode == 1:
            if unlinks and (axial.storageType() != unlinks.storageType()):
                self.iface.messageBar().pushMessage(
                    "Error",
                    "The selected layers must be in the same file format.",
                    level=1,
                    duration=5)
                return False
            caps = unlinks.dataProvider().capabilities()
            if caps & QgsVectorDataProvider.ChangeAttributeValues:
                self.dlg.lockAxialEditTab(True)
                self.dlg.clearAxialProblems()
                ids = lfh.getIdFieldNames(unlinks)
                if ids:
                    self.user_ids['unlinks'] = ids[0]
                self.verificationThread = UnlinksIdUpdate(
                    self.iface.mainWindow(), self, unlinks,
                    self.user_ids['unlinks'], axial, self.axial_id,
                    settings['unlink_dist'])
        # prepare dialog
        self.dlg.lockLayerTab(True)
        self.dlg.setAxialVerifyProgressbar(0, 100)
        self.dlg.lockAxialEditTab(True)
        self.dlg.clearAxialVerifyReport()
        self.dlg.clearAxialProblems()
        if self.verificationThread:
            self.verificationThread.verificationFinished.connect(
                self.processAxialIdUpdateResults)
            self.verificationThread.verificationProgress.connect(
                self.dlg.updateAxialVerifyProgressbar)
            self.verificationThread.verificationError.connect(
                self.cancelAxialIdUpdate)
            self.verificationThread.start()
        return

    def cancelAxialIdUpdate(self, txt=""):
        self.verificationThread.stop()
        if txt:
            self.iface.messageBar().pushMessage("Error",
                                                txt,
                                                level=1,
                                                duration=5)
        try:
            self.verificationThread.verificationFinished.disconnect(
                self.processAxialIdUpdateResults)
            self.verificationThread.verificationProgress.disconnect(
                self.dlg.updateAxialVerifyProgressbar)
            self.verificationThread.verificationError.disconnect(
                self.cancelAxialIdUpdate)
        except:
            pass
        # self.verificationThread = None
        self.dlg.updateAxialVerifyProgressbar(0)
        self.dlg.lockLayerTab(False)
        self.dlg.lockAxialEditTab(False)

    def processAxialIdUpdateResults(self):
        # stop thread
        self.verificationThread.stop()
        try:
            self.verificationThread.verificationFinished.disconnect(
                self.processAxialIdUpdateResults)
            self.verificationThread.verificationProgress.disconnect(
                self.dlg.updateAxialVerifyProgressbar)
        except:
            pass
        self.verificationThread = None
        # reload the layer if columns were added with the ID update
        if self.datastore['type'] in (1, 2):
            if self.edit_mode == 0:
                layer = lfh.getLegendLayerByName(self.iface,
                                                 self.analysis_layers['map'])
            elif self.edit_mode == 1:
                layer = lfh.getLegendLayerByName(
                    self.iface, self.analysis_layers['unlinks'])
            connection = dbh.getDBLayerConnection(layer)
            if self.datastore['type'] == 1:
                cols = dbh.listSpatialiteColumns(connection, layer.name())
            else:
                info = dbh.getPostgisLayerInfo(layer)
                schema = info['schema']
                name = info['table']
                cols = dbh.listPostgisColumns(connection, schema, name)
            connection.close()
            # columns-1 to account for the geometry column that is not a field in QGIS
            if len(layer.dataProvider().fields()) == len(cols) - 1:
                layer.dataProvider().reloadData()
            else:
                lfh.reloadLayer(layer)
        self.dlg.setAxialProblemsFilter(["Layer IDs updated"])
        self.dlg.lockLayerTab(False)
        self.dlg.lockAxialEditTab(False)
        return True

    def cancelAxialVerification(self, txt=""):
        self.verificationThread.stop()
        if txt:
            self.iface.messageBar().pushMessage("Error",
                                                txt,
                                                level=1,
                                                duration=5)
        try:
            self.verificationThread.verificationFinished.disconnect(
                self.processAxialVerificationResults)
            self.verificationThread.verificationProgress.disconnect(
                self.dlg.updateAxialVerifyProgressbar)
            self.verificationThread.verificationError.disconnect(
                self.cancelAxialVerification)
        except:
            pass
        # self.verificationThread = None
        self.dlg.updateAxialVerifyProgressbar(0)
        self.dlg.lockLayerTab(False)
        self.dlg.lockAxialEditTab(False)

    def processAxialVerificationResults(self, results, nodes):
        # stop thread
        self.verificationThread.stop()
        try:
            self.verificationThread.verificationFinished.disconnect(
                self.processAxialVerificationResults)
            self.verificationThread.verificationProgress.disconnect(
                self.dlg.updateAxialVerifyProgressbar)
        except:
            pass
        self.verificationThread = None
        self.dlg.setAxialProblems(results, nodes)
        # build summary for filter Combo
        self.dlg.lockAxialEditTab(False)
        if len(nodes) > 0:
            # nodes_list = sorted(set(nodes))
            summary = ["All problems (%s)" % len(nodes)]
            for k, v in results.items():
                if len(v) > 0:
                    summary.append("%s (%s)" % (k.capitalize(), len(v)))
        else:
            summary = ["No problems found!"]
        self.dlg.setAxialProblemsFilter(summary)
        self.dlg.lockLayerTab(False)
        return True

    def zoomAxialProblem(self):
        # get relevant layer
        idx = self.dlg.getLayerTab()
        layers = self.dlg.getAnalysisLayers()
        layer = None
        name = None
        user_id = ''
        if idx == 0:
            name = layers['map']
            user_id = self.user_ids['map']
        elif idx == 1:
            name = layers['unlinks']
            user_id = self.user_ids['unlinks']
        if name:
            layer = lfh.getLegendLayerByName(self.iface, name)
        if layer:
            # get layer ids
            if user_id == '':
                self.all_ids = layer.allFeatureIds()
            else:
                self.all_ids, ids = lfh.getFieldValues(layer, user_id)
                layer.setDisplayExpression(
                    '"field_name" = {0}'.format(user_id))
            # set display field for axial map (always)
            if idx != 0:
                axial_layer = lfh.getLegendLayerByName(self.iface,
                                                       layers['map'])
                if self.axial_id != '':
                    axial_layer.setDisplayExpression(
                        '"field_name" = {0}'.format(self.axial_id))
            if not self.iface.actionMapTips().isChecked():
                self.iface.actionMapTips().trigger()
            # prepare features to check
            features = []
            items = self.dlg.getAxialVerifyProblems()
            for id in items:
                if type(id) == list:
                    for i in id:
                        if int(i) in self.all_ids:
                            features.append(int(i))
                else:
                    if int(id) in self.all_ids:
                        features.append(int(id))
            # select features and zoom
            if features:
                if user_id == '':
                    layer.selectByIds(features)
                else:
                    ids = lfh.getFeaturesListValues(layer, user_id, features)
                    layer.selectByIds(list(ids.keys()))
            else:
                layer.selectByIds([])
            if layer.selectedFeatureCount() > 0:
                self.iface.mapCanvas().setCurrentLayer(layer)
                layerNode = QgsProject.instance().layerTreeRoot().findLayer(
                    layer.id())
                if not layerNode.isVisible():
                    layerNode.setItemVisibilityChecked(True)
                self.iface.mapCanvas().zoomToSelected()
                if layer.geometryType() in (QgsWkbTypes.Polygon,
                                            QgsWkbTypes.Point):
                    self.iface.mapCanvas().zoomOut()

    def run_analysis(self):
        # check if there's a datastore defined
        if not self.isDatastoreSet():
            # self.iface.messageBar().pushMessage("Warning","Please select a 'Data store' to save the analysis results.", level=1, duration=4)
            return
        # try to connect to the analysis engine
        if self.analysis_engine.ready():
            self.dlg.clear_analysis_report()
            # get selected layers
            self.analysis_layers = self.dlg.getAnalysisLayers()
            # get analysis type based on map and axial/segment choice
            if self.dlg.get_analysis_type() == 0:
                self.axial_analysis_settings['type'] = 0
            else:
                if self.dlg.getSegmentedMode() == 0:
                    self.axial_analysis_settings['type'] = 1
                else:
                    self.axial_analysis_settings['type'] = 2
            # get the basic analysis settings
            analysis_layer = lfh.getLegendLayerByName(
                self.iface, self.analysis_layers['map'])
            self.axial_analysis_settings['id'] = lfh.getIdField(analysis_layer)
            self.axial_analysis_settings[
                'weight'] = self.dlg.get_analysis_weighted()
            self.axial_analysis_settings[
                'weightBy'] = self.dlg.get_analysis_weight_attribute()
            txt = DepthmapEngine.parse_radii(
                self.dlg.get_analysis_radius_text())
            if txt == '':
                self.dlg.write_analysis_report(
                    "Please verify the radius values.")
                return
            else:
                self.axial_analysis_settings['rvalues'] = txt
            self.axial_analysis_settings[
                'output'] = self.dlg.get_analysis_output_table()
            self.analysis_output = self.axial_analysis_settings['output']
            # get the advanced analysis settings
            self.axial_analysis_settings[
                'distance'] = self.dlg.get_analysis_distance_type()
            self.axial_analysis_settings[
                'radius'] = self.dlg.get_analysis_radius_type()
            self.axial_analysis_settings[
                'fullset'] = self.dlg.get_analysis_fullset()
            self.axial_analysis_settings[
                'betweenness'] = self.dlg.get_analysis_choice()
            self.axial_analysis_settings[
                'newnorm'] = self.dlg.get_analysis_normalised()
            self.axial_analysis_settings[
                'stubs'] = self.dlg.get_analysis_stubs()

            # check if output file/table already exists
            table_exists = False
            if self.datastore['type'] == 0:
                table_exists = shph.testShapeFileExists(
                    self.datastore['path'],
                    self.axial_analysis_settings['output'])
            elif self.datastore['type'] == 1:
                connection = dbh.getSpatialiteConnection(
                    self.datastore['path'])
                if connection:
                    table_exists = dbh.testSpatialiteTableExists(
                        connection, self.axial_analysis_settings['output'])
                connection.close()
            elif self.datastore['type'] == 2:
                connection = dbh.getPostgisConnection(self.datastore['name'])
                if connection:
                    table_exists = dbh.testPostgisTableExists(
                        connection, self.datastore['schema'],
                        self.axial_analysis_settings['output'])
                connection.close()
            if table_exists:
                action = QMessageBox.question(
                    None, "Overwrite table",
                    "The output table already exists in:\n %s.\nOverwrite?" %
                    self.datastore['path'],
                    QMessageBox.Ok | QMessageBox.Cancel)
                if action == QMessageBox.Ok:  # Yes
                    pass
                elif action == QMessageBox.Cancel:  # No
                    return
                else:
                    return
            # run the analysis
            analysis_ready = self.analysis_engine.setup_analysis(
                self.analysis_layers, self.axial_analysis_settings)
            if analysis_ready:
                self.updateProjectSettings()
                self.start_time = datetime.datetime.now()
                # write a short analysis summary
                message = self.compile_analysis_summary()
                # print message in results window
                self.dlg.write_analysis_report(message)
                self.dlg.lock_analysis_tab(True)
                self.iface.messageBar().pushMessage(
                    "Info",
                    "Do not close QGIS or depthmapXnet while the analysis is running!",
                    level=0,
                    duration=5)
                self.analysis_engine.start_analysis()
                # timer to check if results are ready, in milliseconds
                self.timer.start(1000)
                self.running_analysis = 'axial'
            else:
                self.dlg.write_analysis_report(
                    "Unable to run this analysis. Please check the input layer and analysis settings."
                )

    def compile_analysis_summary(self):
        message = u"Running analysis for map layer '%s':" % self.analysis_layers[
            'map']
        if self.analysis_layers['unlinks']:
            message += u"\n   unlinks layer - '%s'" % self.analysis_layers[
                'unlinks']
        if self.axial_analysis_settings['type'] == 0:
            txt = "axial"
        elif self.axial_analysis_settings['type'] == 1:
            txt = "segment"
        elif self.axial_analysis_settings['type'] == 2:
            txt = "segment input"
        message += u"\n   analysis type - %s" % txt
        if self.axial_analysis_settings['type'] == 1:
            message += u"\n   stubs removal - %s" % self.axial_analysis_settings[
                'stubs']
        if self.axial_analysis_settings['distance'] == 0:
            txt = "topological"
        elif self.axial_analysis_settings['distance'] == 1:
            txt = "angular"
        elif self.axial_analysis_settings['distance'] == 2:
            txt = "metric"
        message += u"\n   distance - %s" % txt
        if self.axial_analysis_settings['weight'] == 1:
            message += u"\n   weighted by - %s" % self.axial_analysis_settings[
                'weightBy']
        if self.axial_analysis_settings['radius'] == 0:
            txt = "topological"
        elif self.axial_analysis_settings['radius'] == 1:
            txt = "angular"
        elif self.axial_analysis_settings['radius'] == 2:
            txt = "metric"
        message += u"\n   %s radius - %s" % (
            txt, self.axial_analysis_settings['rvalues'])
        if self.axial_analysis_settings['betweenness'] == 1:
            message += u"\n   calculate choice"
        if self.axial_analysis_settings['fullset'] == 1:
            message += u"\n   include advanced measures"
        if self.axial_analysis_settings['type'] in (
                1, 2) and self.axial_analysis_settings['newnorm'] == 1:
            message += u"\n   calculate NACH and NAIN"
        message += u"\n\nStart: %s\n..." % self.start_time.strftime(
            "%d/%m/%Y %H:%M:%S")
        return message

    def cancel_analysis(self):
        if self.running_analysis == 'axial':
            self.dlg.set_analysis_progressbar(0, 100)
            self.dlg.lock_analysis_tab(False)
            self.dlg.write_analysis_report("Analysis canceled by user.")
        self.timer.stop()
        self.analysis_engine.cleanup()
        self.running_analysis = ''

    def check_analysis_progress(self):
        try:
            step, progress = self.analysis_engine.get_progress(
                self.axial_analysis_settings, self.datastore)
            if not progress:
                # no progress, just wait...
                return
            elif progress == 100:
                self.timer.stop()
                # update calculation time
                dt = datetime.datetime.now()
                feedback = u"Finish: %s" % dt.strftime("%d/%m/%Y %H:%M:%S")
                self.dlg.write_analysis_report(feedback)
                # process the output in the analysis
                self.process_analysis_results(
                    self.analysis_engine.analysis_results)
                self.running_analysis = ''
            else:
                if self.running_analysis == 'axial':
                    self.dlg.update_analysis_progressbar(progress)
                    if step > 0:
                        self.timer.start(step)
        except AnalysisEngine.AnalysisEngineError as engine_error:
            if self.running_analysis == 'axial':
                self.dlg.set_analysis_progressbar(0, 100)
                self.dlg.lock_analysis_tab(False)
                self.dlg.write_analysis_report("Analysis error: " +
                                               str(engine_error))
            self.timer.stop()
            self.analysis_engine.cleanup()
            self.running_analysis = ''

    def process_analysis_results(
            self, analysis_results: AnalysisEngine.AnalysisResults):
        new_layer = None
        if self.running_analysis == 'axial':
            self.dlg.set_analysis_progressbar(100, 100)
            if analysis_results.attributes:
                dt = datetime.datetime.now()
                message = u"Post-processing start: %s\n..." % dt.strftime(
                    "%d/%m/%Y %H:%M:%S")
                self.dlg.write_analysis_report(message)
                new_layer = self.save_analysis_results(
                    analysis_results.attributes, analysis_results.types,
                    analysis_results.values, analysis_results.coords)
                # update processing time
                dt = datetime.datetime.now()
                message = u"Post-processing finish: %s" % dt.strftime(
                    "%d/%m/%Y %H:%M:%S")
                self.dlg.write_analysis_report(message)
            else:
                self.iface.messageBar().pushMessage(
                    "Info",
                    "Failed to import the analysis results.",
                    level=1,
                    duration=5)
                self.dlg.write_analysis_report(u"Post-processing: Failed!")
            self.end_time = datetime.datetime.now()
            elapsed = self.end_time - self.start_time
            message = u"Total running time: %s" % elapsed
            self.dlg.write_analysis_report(message)
            self.dlg.lock_analysis_tab(False)
            self.dlg.set_analysis_progressbar(0, 100)
        if new_layer:
            existing_names = [
                layer.name() for layer in lfh.getLegendLayers(self.iface)
            ]
            if new_layer.name() in existing_names:
                old_layer = lfh.getLegendLayerByName(self.iface,
                                                     new_layer.name())
                if lfh.getLayerPath(new_layer) == lfh.getLayerPath(old_layer):
                    QgsProject.instance().removeMapLayer(old_layer.id())
            QgsProject.instance().addMapLayer(new_layer)
            new_layer.updateExtents()

    def save_analysis_results(self, attributes, types, values, coords):
        # Save results to output
        res = False
        analysis_layer = lfh.getLegendLayerByName(self.iface,
                                                  self.analysis_layers['map'])
        srid = analysis_layer.crs()
        path = self.datastore['path']
        table = self.analysis_output
        id = self.axial_analysis_settings['id']
        # if it's an axial analysis try to update the existing layer
        new_layer = None
        # must check if data store is still there
        if not self.isDatastoreSet():
            self.iface.messageBar().pushMessage(
                "Warning",
                "The analysis results will be saved in a memory layer.",
                level=0,
                duration=5)
        # save output based on data store format and type of analysis
        provider = analysis_layer.storageType()
        create_table = False
        # if it's a segment analysis always create a new layer
        # also if one of these is different: output table name, file type, data store location, number of records
        # this last one is a weak check for changes to the table. making a match of results by id would take ages.
        if analysis_layer.name() != table or self.axial_analysis_settings[
                'type'] == 1 or len(values) != analysis_layer.featureCount():
            create_table = True
        # shapefile data store
        if self.datastore['type'] == 0:
            existing_layer_path = lfh.getLayerPath(
                analysis_layer) + "/" + analysis_layer.name() + ".shp"
            new_layer_path = path + "/" + table + ".shp"
            original_table_name = table

            if len(values) != analysis_layer.featureCount(
            ) and existing_layer_path == new_layer_path:
                # we can't overwrite the file anymore because the number of lines is not the same,
                # force a new file, by appending a number at the end
                overwrite_counter = 1
                while os.path.isfile(new_layer_path):
                    # repeat until no such path exists
                    table = original_table_name + "_" + str(overwrite_counter)
                    new_layer_path = path + "/" + table + ".shp"
                    overwrite_counter = overwrite_counter + 1
                    if overwrite_counter > 1000:
                        self.iface.messageBar().pushMessage(
                            "Error",
                            "Existing file and newly suggested file have different "
                            "number of lines, but can not create new file as too many "
                            "existing duplicates",
                            level=Qgis.Critical,
                            duration=5)

            if original_table_name != table:
                self.iface.messageBar().pushMessage(
                    "Warning",
                    "Existing file and newly suggested file have different "
                    "number of lines, new file created with different name",
                    level=Qgis.Warning,
                    duration=5)
            if original_table_name == table and \
                    (lfh.getLayerPath(analysis_layer) != path or analysis_layer.name() != table):
                create_table = True

            # convert type of choice columns to float
            for attr in attributes:
                if 'CH' in attr:
                    idx = attributes.index(attr)
                    types[idx] = QVariant.Double
            # write a new file
            if 'shapefile' not in provider.lower() or create_table:
                new_layer = shph.create_shapefile_full_layer_data_provider(
                    path, table, srid, attributes, types, values, coords)
                if new_layer:
                    res = True
                else:
                    res = False
            # or append to an existing file
            else:
                res = shph.addShapeFileAttributes(analysis_layer, attributes,
                                                  types, values)
        # spatialite data store
        elif self.datastore['type'] == 1:
            connection = dbh.getSpatialiteConnection(path)
            if not dbh.testSpatialiteTableExists(
                    connection, self.axial_analysis_settings['output']):
                create_table = True
            if 'spatialite' not in provider.lower() or create_table:
                res = dbh.createSpatialiteTable(connection, path, table,
                                                srid.postgisSrid(), attributes,
                                                types, 'MULTILINESTRING')
                if res:
                    res = dbh.insertSpatialiteValues(connection, table,
                                                     attributes, values,
                                                     coords)
                    if res:
                        new_layer = dbh.getSpatialiteLayer(
                            connection, path, table)
                        if new_layer:
                            res = True
                        else:
                            res = False
            else:
                res = dbh.addSpatialiteAttributes(connection, table, id,
                                                  attributes, types, values)
                # the spatialite layer needs to be removed and re-inserted to display changes
                if res:
                    QgsProject.instance().removeMapLayer(analysis_layer.id())
                    new_layer = dbh.getSpatialiteLayer(connection, path, table)
                    if new_layer:
                        res = True
                    else:
                        res = False
            connection.close()
        # postgis data store
        elif self.datastore['type'] == 2:
            schema = self.datastore['schema']
            connection = dbh.getPostgisConnection(self.datastore['name'])
            if not dbh.testPostgisTableExists(
                    connection, self.datastore['schema'],
                    self.axial_analysis_settings['output']):
                create_table = True
            if 'postgresql' not in provider.lower() or create_table:
                res = dbh.createPostgisTable(connection, schema, table,
                                             srid.postgisSrid(), attributes,
                                             types, 'MULTILINESTRING')
                if res:
                    res = dbh.insertPostgisValues(connection, schema, table,
                                                  attributes, values, coords)
                    if res:
                        new_layer = dbh.getPostgisLayer(
                            connection, self.datastore['name'], schema, table)
                        if new_layer:
                            res = True
                        else:
                            res = False
            else:
                res = dbh.addPostgisAttributes(connection, schema, table, id,
                                               attributes, types, values)
                # the postgis layer needs to be removed and re-inserted to display changes
                if res:
                    QgsProject.instance().removeMapLayer(analysis_layer.id())
                    new_layer = dbh.getPostgisLayer(connection,
                                                    self.datastore['name'],
                                                    schema, table)
                    if new_layer:
                        res = True
                    else:
                        res = False
            connection.close()
        # memory layer data store
        if self.datastore['type'] == -1 or not res:
            # create a memory layer with the results
            # the coords indicates the results columns with x1, y1, x2, y2
            new_layer = lfh.createTempLayer(table, srid.postgisSrid(),
                                            attributes, types, values, coords)

        return new_layer
Beispiel #20
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()
Beispiel #21
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
Beispiel #22
0
class TesterWidget(BASE, WIDGET):

    currentTestResult = None
    currentTest = 0
    currentTestStep = 0

    BLINKING_INTERVAL = 1000

    buttonColors = ["", 'QPushButton {color: yellow;}']

    testingFinished = pyqtSignal()

    def __init__(self):
        super(TesterWidget, self).__init__()
        self.setupUi(self)
        self.setObjectName("TesterPluginPanel")
        self.btnCancel.clicked.connect(self.cancelTesting)
        self.btnTestOk.clicked.connect(self.testPasses)
        self.btnTestFailed.clicked.connect(self.testFails)
        self.btnRestartTest.clicked.connect(self.restartTest)
        self.btnSkip.clicked.connect(self.skipTest)
        self.btnNextStep.clicked.connect(self.runNextStep)
        self.buttons = [self.btnTestOk, self.btnTestFailed, self.btnNextStep]

        self.blinkTimer = QTimer()
        self.blinkTimer.timeout.connect(self._blink)

    def startBlinking(self):
        self.currentBlinkingTime = 0
        self.blinkTimer.start(self.BLINKING_INTERVAL)

    def stopBlinking(self):
        self.blinkTimer.stop()
        for button in self.buttons:
            button.setStyleSheet(self.buttonColors[0])

    def _blink(self):
        self.currentBlinkingTime += 1
        color = self.buttonColors[self.currentBlinkingTime % 2]
        for button in self.buttons:
            if button.isEnabled():
                button.setStyleSheet(color)

    def setTests(self, tests):
        self.tests = tests

    def startTesting(self):
        self.currentTest = 0
        self.report = Report()
        self.runNextTest()

    def getReportDialog(self):
        """Wrapper for easy mocking"""
        self.reportDialog = ReportDialog(self.report)
        return self.reportDialog

    def restartTest(self):
        self.currentTestResult = None
        self.runNextTest()

    def runNextTest(self):
        if self.currentTestResult:
            self.report.addTestResult(self.currentTestResult)
        if self.currentTest < len(self.tests):
            test = self.tests[self.currentTest]
            self.labelCurrentTest.setText("Current test: %s-%s" %
                                          (test.group.upper(), test.name))
            self.currentTestResult = TestResult(test)
            self.currentTestStep = 0
            self.runNextStep()
        else:
            QApplication.restoreOverrideCursor()
            self.testingFinished.emit()
            self.setVisible(False)

    def runNextStep(self):
        self.stopBlinking()
        test = self.tests[self.currentTest]
        step = test.steps[self.currentTestStep]
        self.btnSkip.setEnabled(True)
        self.btnCancel.setEnabled(True)
        if os.path.exists(step.description):
            with open(step.description) as f:
                html = "".join(f.readlines())
            self.webView.setHtml(html)
        else:
            if step.function is not None:
                self.webView.setHtml(
                    step.description +
                    "<p><b>[This is an automated step. Please, wait until it has been completed]</b></p>"
                )
            else:
                self.webView.setHtml(
                    step.description +
                    "<p><b>[Click on the right-hand side buttons once you have performed this step]</b></p>"
                )
        QCoreApplication.processEvents()
        if self.currentTestStep == len(test.steps) - 1:
            if step.function is not None:
                self.btnTestOk.setEnabled(False)
                self.btnTestFailed.setEnabled(False)
                self.btnNextStep.setEnabled(False)
                self.btnSkip.setEnabled(False)
                self.btnCancel.setEnabled(False)
                self.webView.setEnabled(False)
                QCoreApplication.processEvents()
                try:
                    execute(step.function)
                    self.testPasses()
                except Exception as e:
                    if isinstance(e, AssertionError):
                        self.testFails("%s\n%s" %
                                       (str(e), traceback.format_exc()))
                    else:
                        self.testContainsError(
                            "%s\n%s" % (str(e), traceback.format_exc()))
            else:
                self.btnTestOk.setEnabled(True)
                self.btnTestOk.setText("Test passes")
                self.btnTestFailed.setEnabled(True)
                self.btnTestFailed.setText("Test fails")
                self.webView.setEnabled(True)
                self.btnNextStep.setEnabled(False)
                if step.prestep:
                    try:
                        execute(step.prestep)
                    except Exception as e:
                        if isinstance(e, AssertionError):
                            self.testFailsAtSetup(
                                "%s\n%s" % (str(e), traceback.format_exc()))
                        else:
                            self.testContainsError(
                                "%s\n%s" % (str(e), traceback.format_exc()))
        else:
            if step.function is not None:
                self.btnTestOk.setEnabled(False)
                self.btnTestFailed.setEnabled(False)
                self.btnNextStep.setEnabled(False)
                self.btnSkip.setEnabled(False)
                self.btnCancel.setEnabled(False)
                self.webView.setEnabled(False)
                QCoreApplication.processEvents()
                try:
                    execute(step.function)
                    self.currentTestStep += 1
                    self.runNextStep()
                except Exception as e:
                    if isinstance(e, AssertionError):
                        self.testFails("%s\n%s" %
                                       (str(e), traceback.format_exc()))
                    else:
                        self.testContainsError(
                            "%s\n%s" % (str(e), traceback.format_exc()))
            else:
                self.currentTestStep += 1
                self.webView.setEnabled(True)
                self.btnNextStep.setEnabled(not step.isVerifyStep)
                if step.isVerifyStep:
                    self.btnTestOk.setEnabled(True)
                    self.btnTestOk.setText("Step passes")
                    self.btnTestFailed.setEnabled(True)
                    self.btnTestFailed.setText("Step fails")
                else:
                    self.btnTestOk.setEnabled(False)
                    self.btnTestFailed.setEnabled(False)
                if step.prestep:
                    try:
                        execute(step.prestep)
                    except Exception as e:
                        if isinstance(e, AssertionError):
                            self.testFailsAtSetup(
                                "%s\n%s" % (str(e), traceback.format_exc()))
                        else:
                            self.testContainsError(
                                "%s\n%s" % (str(e), traceback.format_exc()))
        if step.function is None:
            self.startBlinking()

    def testPasses(self):
        test = self.tests[self.currentTest]
        if self.btnTestOk.isEnabled() and self.btnTestOk.text(
        ) == "Step passes":
            self.runNextStep()
        else:
            try:
                test = self.tests[self.currentTest]
                test.cleanup()
                self.currentTestResult.passed()
            except:
                self.currentTestResult.failed("Test cleanup",
                                              traceback.format_exc())

            self.currentTest += 1
            self.runNextTest()

    def testFails(self, msg=""):
        test = self.tests[self.currentTest]
        if self.btnTestOk.isEnabled() and self.btnTestOk.text(
        ) == "Step passes":
            desc = test.steps[self.currentTestStep - 1].description
        else:
            desc = test.steps[self.currentTestStep].description
        self.currentTestResult.failed(desc, msg)
        try:
            test.cleanup()
        except:
            pass
        self.currentTest += 1
        self.runNextTest()

    def testFailsAtSetup(self, msg=""):
        test = self.tests[self.currentTest]
        if self.btnTestOk.isEnabled() and self.btnTestOk.text(
        ) == "Step passes":
            desc = test.steps[self.currentTestStep - 1].description
        else:
            desc = test.steps[self.currentTestStep].description
        self.currentTestResult.setupFailed(desc, msg)
        try:
            test.cleanup()
        except:
            pass
        self.currentTest += 1
        self.runNextTest()

    def testContainsError(self, msg=""):
        test = self.tests[self.currentTest]
        if self.btnTestOk.isEnabled() and self.btnTestOk.text(
        ) == "Step passes":
            desc = test.steps[self.currentTestStep - 1].description
        else:
            desc = test.steps[self.currentTestStep].description
        self.currentTestResult.containsError(desc, msg)
        try:
            test.cleanup()
        except:
            pass
        self.currentTest += 1
        self.runNextTest()

    def skipTest(self):
        try:
            test = self.tests[self.currentTest]
            test.cleanup()
        except:
            pass
        self.currentTest += 1
        self.currentTestResult.skipped()

        self.runNextTest()

    def cancelTesting(self):
        self.setVisible(False)
        self.testingFinished.emit()
Beispiel #23
0
class QRealTime:
    """QGIS Plugin Implementation."""
    def __init__(self, iface):
        """Constructor.

        :param iface: An interface instance that will be passed to this class
            which provides the hook by which you can manipulate the QGIS
            application at run time.
        :type iface: QgsInterface
        """
        # Save reference to the QGIS interface
        self.iface = iface
        # initialize plugin directory
        self.plugin_dir = os.path.dirname(__file__)
        # initialize locale
        try:
            if QSettings().value('locale//overrideFlag'):
                locale = QSettings().value('locale//globalLocale')[0:2]
            else:
                locale = QSettings().value('locale//userLocale')[0:2]
        except:
            locale = 'en'
        locale_path = os.path.join(self.plugin_dir, 'i18n',
                                   'QRealTime_{}.qm'.format(locale))
        print(locale_path)
        if os.path.exists(locale_path):
            self.translator = QTranslator()
            self.translator.load(locale_path)

            if qVersion() > '4.3.3':
                QCoreApplication.installTranslator(self.translator)

        # Declare instance attributes
        self.actions = []
        self.menu = 'QRealTime'
        # TODO: We are going to let the user set this up in a future iteration
        self.toolbar = self.iface.addToolBar('QRealTime')
        self.toolbar.setObjectName('QRealTime')

    # noinspection PyMethodMayBeStatic
    def tr(self, message):
        """Get the translation for a string using Qt translation API.

        We implement this ourselves since we do not inherit QObject.

        :param message: String for translation.
        :type message: str, QString

        :returns: Translated version of message.
        :rtype: QString
        """
        # noinspection PyTypeChecker,PyArgumentList,PyCallByClass
        return QCoreApplication.translate('QRealTime', message)

    def add_action(self,
                   icon_path,
                   text,
                   callback,
                   enabled_flag=True,
                   add_to_menu=True,
                   add_to_toolbar=True,
                   status_tip=None,
                   whats_this=None,
                   parent=None):
        """Add a toolbar icon to the toolbar.

        :param icon_path: Path to the icon for this action. Can be a resource
            path (e.g. ':/plugins/foo/bar.png') or a normal file system path.
        :type icon_path: str

        :param text: Text that should be shown in menu items for this action.
        :type text: str

        :param callback: Function to be called when the action is triggered.
        :type callback: function

        :param enabled_flag: A flag indicating if the action should be enabled
            by default. Defaults to True.
        :type enabled_flag: bool

        :param add_to_menu: Flag indicating whether the action should also
            be added to the menu. Defaults to True.
        :type add_to_menu: bool

        :param add_to_toolbar: Flag indicating whether the action should also
            be added to the toolbar. Defaults to True.
        :type add_to_toolbar: bool

        :param status_tip: Optional text to show in a popup when mouse pointer
            hovers over the action.
        :type status_tip: str

        :param parent: Parent widget for the new action. Defaults None.
        :type parent: QWidget

        :param whats_this: Optional text to show in the status bar when the
            mouse pointer hovers over the action.

        :returns: The action that was created. Note that the action is also
            added to self.actions list.
        :rtype: QAction
        """

        # Create the dialog (after translation) and keep reference
        self.dlg = QRealTimeDialog(self)

        icon = QIcon(icon_path)
        action = QAction(icon, text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)

        if status_tip is not None:
            action.setStatusTip(status_tip)

        if whats_this is not None:
            action.setWhatsThis(whats_this)

        if add_to_toolbar:
            self.toolbar.addAction(action)

        if add_to_menu:
            self.iface.addPluginToMenu(self.menu, action)
        self.actions.append(action)
        return action

    def add_layer_action(self,
                         icon_path,
                         text,
                         callback,
                         icon_enabled=True,
                         add_to_vLayer=True,
                         enabled_flag=True,
                         parent=None):
        icon = QIcon(icon_path)
        if icon_enabled:
            action = QAction(icon, text, parent)
        else:
            action = QAction(text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)
        if add_to_vLayer:
            self.iface.addCustomActionForLayerType(action, 'QRealTime',
                                                   QgsMapLayer.VectorLayer,
                                                   True)
        self.actions.append(action)

        return action

    def initGui(self):
        """Create the menu entries and toolbar icons inside the QGIS GUI."""

        icon_path = os.path.join(self.plugin_dir, 'icon.png')
        self.add_action(icon_path,
                        text=self.tr(u'QRealTime Setting'),
                        callback=self.run,
                        parent=self.iface.mainWindow())
        """add sync action"""
        self.sync = self.add_layer_action(icon_path, self.tr(u'sync'),
                                          self.download, False)
        # make sync action checkable and default to unchecked
        self.sync.setCheckable(True)
        self.sync.setChecked(False)
        """add import action """
        self.Import = self.add_layer_action(icon_path, self.tr(u'import'),
                                            self.importData)
        """add makeonline action """
        self.makeOnline = self.add_layer_action(icon_path,
                                                self.tr(u'Make Online'),
                                                self.sendForm)
        service = self.dlg.getCurrentService()
        self.service = service
        self.topElement = None
        self.version = ''
        try:
            self.time = 86400
            self.time = int(service.getValue(self.tr('sync time')))
        except:
            print('can not read time')
        self.timer = QTimer()

        def timeEvent():
            print('calling collect data')
            self.service.importData(self.layer, self.formID, False)

        self.timer.timeout.connect(timeEvent)

    def unload(self):
        """Removes the plugin menu item and icon from QGIS GUI."""
        for action in self.actions:
            self.iface.removePluginMenu('QRealTime', action)
            self.iface.removeCustomActionForLayerType(action)
            self.iface.removeToolBarIcon(action)
        # remove the toolbar
        del self.toolbar

    def run(self):
        """Run method that performs all the real work"""
        # show the dialog
        self.dlg.show()
        # Run the dialog event loop
        result = self.dlg.exec_()
        # See if OK was pressed
        if result:
            # Do something useful here - delete the line containing pass and
            # substitute with your code.
            self.dlg.getCurrentService().setup()

    def importData(self):
        service = self.dlg.getCurrentService()
        layer = self.getLayer()
        forms, response = service.getFormList()
        if response:
            if response.status_code == 200:
                self.ImportData = ImportData()
                for name, key in forms.items():
                    self.ImportData.comboBox.addItem(name, key)
                self.ImportData.show()
                result = self.ImportData.exec_()
                if result:
                    selectedForm = self.ImportData.comboBox.currentData()
                    service.importData(layer, selectedForm, True)

    def getLayer(self):
        return self.iface.activeLayer()

    def sendForm(self):
        #        get the fields model like name , widget type, options etc.
        layer = self.getLayer()
        service = self.dlg.getCurrentService()
        service.prepareSendForm(layer)

    def download(self, checked=False):
        if checked == True:
            self.layer = self.getLayer()
            self.service = self.dlg.getCurrentService()
            forms, response = self.service.getFormList()
            if response:
                self.formID = forms[self.layer.name()]
                try:
                    self.time = int(self.service.getValue(
                        self.tr('sync time')))
                except:
                    self.time = 3600
                print('starting timer every' + str(self.time) + 'second')
                self.timer.start(1000 * self.time)
        elif checked == False:
            self.timer.stop()
            print("timer stoped")
Beispiel #24
0
class QRealTime:
    """QGIS Plugin Implementation."""
    def __init__(self, iface):
        """Constructor.

        :param iface: An interface instance that will be passed to this class
            which provides the hook by which you can manipulate the QGIS
            application at run time.
        :type iface: QgsInterface
        """
        # Save reference to the QGIS interface
        self.iface = iface
        # initialize plugin directory
        self.plugin_dir = os.path.dirname(__file__)
        # initialize locale
        locale = QSettings().value('locale//userLocale')[0:2]
        locale_path = os.path.join(self.plugin_dir, 'i18n',
                                   'QRealTime_{}.qm'.format(locale))

        if os.path.exists(locale_path):
            self.translator = QTranslator()
            self.translator.load(locale_path)

            if qVersion() > '4.3.3':
                QCoreApplication.installTranslator(self.translator)

        # Declare instance attributes
        self.actions = []
        self.menu = self.tr(u'&QRealTime')
        # TODO: We are going to let the user set this up in a future iteration
        self.toolbar = self.iface.addToolBar(u'QRealTime')
        self.toolbar.setObjectName(u'QRealTime')

    # noinspection PyMethodMayBeStatic
    def tr(self, message):
        """Get the translation for a string using Qt translation API.

        We implement this ourselves since we do not inherit QObject.

        :param message: String for translation.
        :type message: str, QString

        :returns: Translated version of message.
        :rtype: QString
        """
        # noinspection PyTypeChecker,PyArgumentList,PyCallByClass
        return QCoreApplication.translate('QRealTime', message)

    def add_action(self,
                   icon_path,
                   text,
                   callback,
                   enabled_flag=True,
                   add_to_menu=True,
                   add_to_toolbar=True,
                   status_tip=None,
                   whats_this=None,
                   parent=None):
        """Add a toolbar icon to the toolbar.

        :param icon_path: Path to the icon for this action. Can be a resource
            path (e.g. ':/plugins/foo/bar.png') or a normal file system path.
        :type icon_path: str

        :param text: Text that should be shown in menu items for this action.
        :type text: str

        :param callback: Function to be called when the action is triggered.
        :type callback: function

        :param enabled_flag: A flag indicating if the action should be enabled
            by default. Defaults to True.
        :type enabled_flag: bool

        :param add_to_menu: Flag indicating whether the action should also
            be added to the menu. Defaults to True.
        :type add_to_menu: bool

        :param add_to_toolbar: Flag indicating whether the action should also
            be added to the toolbar. Defaults to True.
        :type add_to_toolbar: bool

        :param status_tip: Optional text to show in a popup when mouse pointer
            hovers over the action.
        :type status_tip: str

        :param parent: Parent widget for the new action. Defaults None.
        :type parent: QWidget

        :param whats_this: Optional text to show in the status bar when the
            mouse pointer hovers over the action.

        :returns: The action that was created. Note that the action is also
            added to self.actions list.
        :rtype: QAction
        """

        # Create the dialog (after translation) and keep reference
        self.dlg = QRealTimeDialog(self)

        icon = QIcon(icon_path)
        action = QAction(icon, text, parent)
        action.triggered.connect(callback)
        action.setEnabled(enabled_flag)

        if status_tip is not None:
            action.setStatusTip(status_tip)

        if whats_this is not None:
            action.setWhatsThis(whats_this)

        if add_to_toolbar:
            self.toolbar.addAction(action)

        if add_to_menu:
            self.iface.addPluginToMenu(self.menu, action)

        self.actions.append(action)

        return action

    def initGui(self):
        """Create the menu entries and toolbar icons inside the QGIS GUI."""

        icon_path = os.path.join(self.plugin_dir, 'icon.png')
        self.add_action(icon_path,
                        text=self.tr(u'QRealTime Setting'),
                        callback=self.run,
                        parent=self.iface.mainWindow())
        self.ODKMenu = QMenu('QRealTime')
        icon = QIcon(icon_path)
        self.sync = QAction(self.tr(u'sync'), self.ODKMenu)
        self.sync.setCheckable(True)
        self.sync.setChecked(False)
        self.sync.triggered.connect(self.download)
        self.sync.setChecked(False)
        self.iface.addCustomActionForLayerType(self.sync, 'QRealTime',
                                               QgsMapLayer.VectorLayer, True)

        self.Import = QAction(icon, self.tr(u'import'), self.ODKMenu)
        self.Import.triggered.connect(self.importData)
        self.iface.addCustomActionForLayerType(self.Import, 'QRealTime',
                                               QgsMapLayer.VectorLayer, True)
        self.makeOnline = QAction(icon, self.tr(u'Make Online'), self.ODKMenu)
        self.makeOnline.triggered.connect(self.sendForm)
        self.iface.addCustomActionForLayerType(self.makeOnline, 'QRealTime',
                                               QgsMapLayer.VectorLayer, True)
        service = self.dlg.getCurrentService()
        self.service = service
        self.topElement = None
        self.version = ''
        try:
            self.time = 1
            self.time = int(service.getValue('sync time'))
        except:
            print('can not read time')
        self.timer = QTimer()

        def timeEvent():
            print('calling collect data')
            layer = self.getLayer()
            print(layer)
            if (not self.topElement):
                self.topElement = layer.name()
            service.collectData(layer, layer.name(), False, self.topElement,
                                self.version)

        self.timer.timeout.connect(timeEvent)

    def unload(self):
        """Removes the plugin menu item and icon from QGIS GUI."""
        for action in self.actions:
            self.iface.removePluginMenu(self.tr(u'QRealTime'), action)
            self.iface.removeToolBarIcon(action)
        # remove the toolbar
        self.iface.removeCustomActionForLayerType(self.sync)
        self.iface.removeCustomActionForLayerType(self.makeOnline)
        self.iface.removeCustomActionForLayerType(self.Import)
        del self.toolbar

    def run(self):
        """Run method that performs all the real work"""
        # show the dialog
        self.dlg.show()
        # Run the dialog event loop
        result = self.dlg.exec_()
        # See if OK was pressed
        if result:
            # Do something useful here - delete the line containing pass and
            # substitute with your code.
            self.dlg.getCurrentService().setup()

    def importData(self):
        service = self.dlg.getCurrentService()
        layer = self.getLayer()
        forms, response = service.getFormList()
        if response.status_code == 200:
            self.ImportData = ImportData()
            self.ImportData.comboBox.addItems(forms)
            self.ImportData.show()
            result = self.ImportData.exec_()
            if result:
                selectedForm = self.ImportData.comboBox.currentText()
                url = service.getValue(
                    'url') + '//formXml?formId=' + selectedForm
                response = requests.request('GET',
                                            url,
                                            proxies=getProxiesConf(),
                                            verify=False)
                if response.status_code == 200:
                    # with open('importForm.xml','w') as importForm:
                    #     importForm.write(response.content)
                    self.formKey, self.topElement, self.version, self.geoField = self.updateLayer(
                        layer, response.content)
                    layer.setName(self.formKey)
                    service.collectData(layer, self.formKey, True,
                                        self.topElement, self.version,
                                        self.geoField)

    def updateLayer(self, layer, xml):
        ns = '{http://www.w3.org/2002/xforms}'
        root = ET.fromstring(xml)
        #key= root[0][1][0][0].attrib['id']
        instance = root[0][1].find(ns + 'instance')
        key = instance[0].attrib['id']
        #topElement=root[0][1][0][0].tag.split('}')[1]
        topElement = instance[0].tag.split('}')[1]
        try:
            version = instance[0].attrib['version']
        except:
            version = 'null'
        print('key captured' + key)
        print(root[0][1].findall(ns + 'bind'))
        for bind in root[0][1].findall(ns + 'bind'):
            attrib = bind.attrib
            print(attrib)
            fieldName = attrib['nodeset'].split('/')[-1]
            fieldType = attrib['type']
            print('attrib type is', attrib['type'])
            qgstype, config = qtype(attrib['type'])
            print('first attribute' + fieldName)
            inputs = root[1].findall('.//*[@ref]')
            if fieldType[:3] != 'geo':
                print('creating new field:' + fieldName)
                isHidden = True
                for input in inputs:
                    if fieldName == input.attrib['ref'].split('/')[-1]:
                        isHidden = False
                        break
                if isHidden:
                    print('Reached Hidden')
                    config['type'] = 'Hidden'
                self.dlg.getCurrentService().updateFields(
                    layer, fieldName, qgstype, config)
            else:
                geoField = fieldName
        return key, topElement, version, geoField

    def getLayer(self):
        return self.iface.activeLayer()

    def sendForm(self):
        #        get the fields model like name , widget type, options etc.
        version = str(datetime.date.today())
        print('version is' + version)
        layer = self.getLayer()
        self.dlg.getCurrentService().updateFields(layer)
        fieldDict = self.getFieldsModel(layer)
        print('fieldDict', fieldDict)
        surveyDict = {
            "name": layer.name(),
            "title": layer.name(),
            'VERSION': version,
            "instance_name": 'uuid()',
            "submission_url": '',
            "default_language": 'default',
            'id_string': layer.name(),
            'type': 'survey',
            'children': fieldDict
        }
        survey = create_survey_element_from_dict(surveyDict)
        xml = survey.to_xml(validate=None, warnings=warnings)
        os.chdir(os.path.expanduser('~'))
        with open('Xform.xml', 'w') as xForm:
            xForm.write(xml)
        self.dlg.getCurrentService().sendForm(layer.name(), 'Xform.xml')

    def download(self, checked=False):
        if checked == True:
            self.layer = self.getLayer()
            self.time = int(self.service.getValue('sync time'))
            print('starting timer every' + str(self.time) + 'second')
            self.timer.start(1000 * self.time)
        elif checked == False:
            self.timer.stop()

    def getFieldsModel(self, currentLayer):
        fieldsModel = []
        g_type = currentLayer.geometryType()
        fieldDef = {
            'name': 'GEOMETRY',
            'type': 'geopoint',
            'bind': {
                'required': 'true()'
            }
        }
        fieldDef['Appearance'] = 'maps'
        if g_type == 0:
            fieldDef['label'] = 'add point location'
        elif g_type == 1:
            fieldDef['label'] = 'Draw Line'
            fieldDef['type'] = 'geotrace'
        else:
            fieldDef['label'] = 'Draw Area'
            fieldDef['type'] = 'geoshape'
        fieldsModel.append(fieldDef)
        i = 0
        for field in currentLayer.fields():
            widget = currentLayer.editorWidgetSetup(i)
            fwidget = widget.type()
            if (fwidget == 'Hidden'):
                i += 1
                continue

            fieldDef = {}
            fieldDef['name'] = field.name()
            fieldDef['map'] = field.name()
            fieldDef['label'] = field.alias() or field.name()
            fieldDef['hint'] = ''
            fieldDef['type'] = QVariantToODKtype(field.type())
            fieldDef['bind'] = {}
            #            fieldDef['fieldWidget'] = currentFormConfig.widgetType(i)
            fieldDef['fieldWidget'] = widget.type()
            print('getFieldModel', fieldDef['fieldWidget'])
            if fieldDef['fieldWidget'] in ('ValueMap', 'CheckBox', 'Photo',
                                           'ExternalResource'):
                if fieldDef['fieldWidget'] == 'ValueMap':
                    fieldDef['type'] = 'select one'
                    valueMap = widget.config()['map']
                    config = {}
                    for value in valueMap:
                        for k, v in value.items():
                            config[v] = k
                    print('configuration is ', config)
                    choicesList = [{
                        'name': name,
                        'label': label
                    } for name, label in config.items()]
                    fieldDef["choices"] = choicesList
                elif fieldDef['fieldWidget'] == 'Photo' or fieldDef[
                        'fieldWidget'] == 'ExternalResource':
                    fieldDef['type'] = 'image'
                    print('got an image type field')

#                fieldDef['choices'] = config
            else:
                fieldDef['choices'] = {}
            if fieldDef['name'] == 'ODKUUID':
                fieldDef["bind"] = {
                    "readonly": "true()",
                    "calculate": "concat('uuid:', uuid())"
                }
            fieldsModel.append(fieldDef)
            i += 1
        return fieldsModel
Beispiel #25
0
class ApiRequestQueue(QObject):
    """
    A managed queue for ongoing API network requests
    """

    result_fetched = pyqtSignal(dict)
    error = pyqtSignal(BoundaryRequest, str)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.boundary_change_queue = []
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.process_queue)
        self.set_frequency(QgsSettings().value('redistrict/check_every', '30',
                                               int, QgsSettings.Plugins))

    def set_frequency(self, frequency: int):
        """
        Sets the frequency to check for completed results
        :param frequency: seconds between checks
        """
        self.timer.stop()
        self.timer.start(frequency * 1000)

    def append_request(self, connector: NzElectoralApi,
                       request: BoundaryRequest):
        """
        Appends a new BoundaryRequest to the queue.
        :param connector: API connector
        :param request: Boundary request object
        """
        result = connector.boundaryChanges(request)
        if isinstance(result, str):
            # no need to wait - we already have a result (i.e. blocking request)
            self.boundary_change_queue.append((connector, request, result))
            self.process_queue()
        else:
            result.reply.finished.connect(
                partial(self.finished_boundary_request, connector, result,
                        request))

    def clear(self):
        """
        Clears all requests from the queue
        """
        self.boundary_change_queue = []

    def finished_boundary_request(self, connector: NzElectoralApi,
                                  request: NetworkAccessManager,
                                  boundary_request: BoundaryRequest):
        """
        Triggered when a non-blocking boundary request is finished
        :param connector: API connector
        :param request: completed request
        """
        response = connector.parse_async(request)
        if response['status'] not in (200, 202):
            self.error.emit(
                boundary_request,
                str(response['status']) + ':' + response['reason'] + ' ' +
                response['content'])
            return
        request_id = response['content']
        self.boundary_change_queue.append(
            (connector, boundary_request, request_id))

    def process_queue(self):
        """
        Processes the outstanding queue, checking if any requests have finished calculation
        """
        for (connector, boundary_request,
             request_id) in self.boundary_change_queue:
            self.check_for_result(connector, boundary_request, request_id)

    def remove_from_queue(self, request_id):
        """
        Removes a request from the queue
        :param request_id: id of request
        """
        self.boundary_change_queue = [
            c for c in self.boundary_change_queue if c[2] != request_id
        ]

    def check_for_result(self, connector: NzElectoralApi,
                         boundary_request: BoundaryRequest, request_id: str):
        """
        Checks if a boundary request has finished calculating
        :param connector: API connector
        :param boundary_request: original boundary request
        :param request_id: ID of request
        """
        request = connector.boundaryChangesResults(request_id)
        if isinstance(request, dict):
            # no need to wait - we already have a result (i.e. blocking request)
            self.check_boundary_result_reply(request_id, request)
        else:
            request.reply.finished.connect(
                partial(self.finished_boundary_result_request, connector,
                        boundary_request, request_id, request))

    def finished_boundary_result_request(self, connector: NzElectoralApi,
                                         boundary_request: BoundaryRequest,
                                         request_id: str,
                                         request: NetworkAccessManager):
        """
        Triggered when a boundary change result network request has finished
        :param connector: API connector
        :param boundary_request: original boundary request
        :param request_id: ID of request
        :param request: completed request
        """
        results = connector.parse_async(request)
        if results['status'] not in (200, 202):
            self.remove_from_queue(request_id)
            self.error.emit(
                boundary_request,
                str(results['status']) + ":" + results['reason'] + ' ' +
                str(results['content']))
            return
        self.check_boundary_result_reply(request_id, results['content'])

    def check_boundary_result_reply(self, request_id: str, reply: Union[dict,
                                                                        str]):
        """
        Checks whether the result of a boundary request is a completed result
        :param request_id: ID of request
        :param reply: reply to check
        """
        if isinstance(reply,
                      str) and reply.startswith('Calculation in progress'):
            # not finished...
            return

        self.remove_from_queue(request_id)
        self.result_fetched.emit(reply)
Beispiel #26
0
class ProgressDialog(Dialog):
    '''
    Dialog showing progress in textfield and a progress bar after starting a
    certain task with run(). Contains a log section and a timer

    Attributes
    ----------
    success : bool
        indicates if the task was run successfully without errors
    error : bool
        indicates if an error occured while running the task
    '''
    ui_file = 'progress.ui'

    def __init__(self, worker: Worker, parent: QObject = None,
                 auto_close: bool = False, auto_run: bool = True,
                 on_success: object = None, on_close: object = None):
        '''
        Parameters
        ----------
        worker : Worker
            Worker object holding the task to do
        parent : QObject, optional
            parent ui element of the dialog, defaults to no parent
        auto_close : bool, optional
            close dialog automatically after task is done, defaults to automatic
            close
        auto_run : bool, optional
            start task automatically when showing the dialog, otherwise the user
            has to start it by pressing the start-button, defaults to automatic
            start
        on_success : object, optional
            function to call on successful run of task, function has to expect
            the result of the task as an argument, defaults to no callback on
            success
        on_close : object, optional
            function to call when closing the dialog, defaults to no callback on
            closing
        '''
        # parent = parent or iface.mainWindow()
        super().__init__(self.ui_file, modal=True, parent=parent)
        self.parent = parent
        self.setupUi()
        self.setAttribute(Qt.WA_DeleteOnClose)
        self.progress_bar.setValue(0)
        self.stop_button.setVisible(False)
        self.close_button.setVisible(False)
        self.auto_close_check.setChecked(auto_close)
        self.auto_run = auto_run
        # ToDo: use signals instead of callbacks
        self.on_success = on_success
        self.on_close = on_close
        self.success = False
        self.error = False

        self.worker = worker
        if self.worker:
            self.worker.finished.connect(self._success)
            self.worker.error.connect(self.on_error)
            self.worker.message.connect(self.show_status)
            self.worker.progress.connect(self.progress)

        self.start_button.clicked.connect(self.run)
        self.stop_button.clicked.connect(self.stop)
        self.close_button.clicked.connect(self.close)

        self.timer = QTimer(self)
        self.timer.timeout.connect(self._update_timer)

    def show(self):
        '''
        show the dialog
        '''
        QDialog.show(self)
        if self.auto_run:
            self.run()

    def _success(self, result: object = None):
        '''
        handle successful run
        '''
        self.progress(100)
        self.show_status('<br><b>fertig</b>')
        if not self.error:
            self.success = True
            if self.on_success:
                self.on_success(result)
        self._finished()

    def _finished(self):
        '''
        handle finished run
        '''
        #self.worker.deleteLater()
        self.timer.stop()
        self.close_button.setVisible(True)
        self.close_button.setEnabled(True)
        self.stop_button.setVisible(False)
        if self.auto_close_check.isChecked() and not self.error:
            self.close()

    def close(self):
        '''
        close the dialog
        '''
        super().close()
        if self.on_close:
            self.on_close()

    def on_error(self, message: str):
        '''
        call this if error occurs while running task

        Parameters
        ----------
        message : str
            error message to show
        '''
        self.show_status( f'<span style="color:red;">Fehler: {message}</span>')
        self.progress_bar.setStyleSheet(
            'QProgressBar::chunk { background-color: red; }')
        self.error = True
        self._finished()

    def show_status(self, text: str):
        '''
        write message into the log section

        Parameters
        ----------
        text : str
            message to show
        '''
        self.log_edit.appendHtml(text)
        #self.log_edit.moveCursor(QTextCursor.Down)
        scrollbar = self.log_edit.verticalScrollBar()
        scrollbar.setValue(scrollbar.maximum());

    def progress(self, progress: Union[int, QVariant]):
        '''
        set progress of task

        Parameters
        ----------
        progress : int or QVariant
            progress in percent [0..100]
        '''
        if isinstance(progress, QVariant):
            progress = progress.toInt()[0]
        self.progress_bar.setValue(progress)

    def start_timer(self):
        '''
        start the timer
        '''
        self.start_time = datetime.datetime.now()
        self.timer.start(1000)

    def run(self):
        '''
        run the task
        '''
        self.error = False
        self.start_timer()
        self.stop_button.setVisible(True)
        self.start_button.setVisible(False)
        self.close_button.setVisible(True)
        self.close_button.setEnabled(False)
        if self.worker:
            self.worker.start()

    def stop(self):
        '''
        cancel the task
        '''
        self.timer.stop()
        if self.worker:
            self.worker.terminate()
        self.log_edit.appendHtml('<b> Vorgang abgebrochen </b> <br>')
        self.log_edit.moveCursor(QTextCursor.End)
        self._finished()

    def _update_timer(self):
        '''
        update the timer
        '''
        delta = datetime.datetime.now() - self.start_time
        h, remainder = divmod(delta.seconds, 3600)
        m, s = divmod(remainder, 60)
        timer_text = '{:02d}:{:02d}:{:02d}'.format(h, m, s)
        self.elapsed_time_label.setText(timer_text)
Beispiel #27
0
class MainWidget(QDockWidget):
    '''
    the dockable main widget

    Attributes
    ----------
    closingWidget : pyqtSignal
        emitted when widget is closed in any way
    '''
    ui_file = 'main_dockwidget.ui'
    closingWidget = pyqtSignal()

    def __init__(self, parent: QWidget = None):
        super(MainWidget, self).__init__(parent)
        # currently selected output layer
        self.output = None
        # stores which layers are marked as output layers
        self.output_layer_ids = []

        self.input = None
        self.valid_bkg_key = False
        self.label_field_name = None
        # cache all results for a layer, (layer-id, feature-id) as keys,
        # geojson features as values
        self.result_cache = {}
        # cache field-map settings for layers, layer-ids as keys,
        # FieldMaps as values
        self.field_map_cache = {}
        # cache label fields, layer-ids as keys, field name as values
        self.label_cache = {}
        self.field_map = None

        add_fields = [
            ResField('n_results',
                     'int2',
                     alias='Anzahl der Ergebnisse',
                     prefix='gc'),
            ResField('i', 'int2', alias='Ergebnisindex', prefix='gc'),
            ResField('manuell_bearbeitet', 'bool', alias='Manuell bearbeitet')
        ]
        add_fields += BKG_RESULT_FIELDS

        # additional fields for storing results,
        # non-optional fields are active by default, others will be set by
        # user input
        self.result_fields = {
            f.name: (f, True if not f.optional else False)
            for f in add_fields
        }

        self.inspect_dialog = None
        self.reverse_dialog = None
        self.geocoding = None

        self.iface = utils.iface
        self.canvas = self.iface.mapCanvas()
        ui_file = self.ui_file if os.path.exists(self.ui_file) \
            else os.path.join(UI_PATH, self.ui_file)
        uic.loadUi(ui_file, self)
        self.setAllowedAreas(Qt.RightDockWidgetArea | Qt.LeftDockWidgetArea)
        self.setupUi()
        self.setup_config()

    def setupUi(self):
        '''
        set up the ui, fill it with dynamic content and connect all interactive
        ui elements with actions
        '''
        # connect buttons
        self.import_csv_button.clicked.connect(self.import_csv)
        self.export_csv_button.clicked.connect(self.export_csv)
        self.attribute_table_button.clicked.connect(self.show_attribute_table)
        self.request_start_button.clicked.connect(self.bkg_geocode)
        self.request_stop_button.clicked.connect(lambda: self.geocoding.kill())
        self.request_stop_button.setVisible(False)

        self.help_button.clicked.connect(self.show_help)
        self.about_button.clicked.connect(self.show_about)

        # only vector layers as input
        self.layer_combo.setFilters(QgsMapLayerProxyModel.VectorLayer)
        self.layer_combo.layerChanged.connect(self.change_layer)

        # input layer encodings
        for encoding in QgsVectorDataProvider.availableEncodings():
            self.encoding_combo.addItem(encoding)
        self.encoding_combo.currentTextChanged.connect(self.set_encoding)

        self.setup_crs()

        # initially set the first layer in the combobox as input
        self.change_layer(self.layer_combo.currentLayer())

        # "Regionalschlüssel" filter
        self.rs_combo.addItem('Eingabehilfe Bundesländer')
        self.rs_combo.model().item(0).setEnabled(False)
        for name, rs in RS_PRESETS:
            self.rs_combo.addItem(name, rs)
        self.rs_combo.currentIndexChanged.connect(
            lambda: self.rs_edit.setText(self.rs_combo.currentData()))

        def rs_finished():
            self.rs_combo.blockSignals(True)
            self.rs_combo.setCurrentIndex(0)
            self.rs_combo.blockSignals(False)

        self.rs_edit.editingFinished.connect(rs_finished)

        def set_rs(rs):
            valid = self.check_rs(rs)
            self.rs_error_label.setVisible(not valid
                                           and self.use_rs_check.isChecked())

        self.rs_edit.textChanged.connect(set_rs)
        self.use_rs_check.toggled.connect(lambda: set_rs(self.rs_edit.text()))

        # spatial filter
        # only polygons can be used as a spatial filter
        self.spatial_filter_combo.setFilters(
            QgsMapLayerProxyModel.PolygonLayer)

        # connect map tools
        self.inspect_picker = FeaturePicker(self.inspect_picker_button,
                                            canvas=self.canvas)
        self.inspect_picker.feature_picked.connect(self.inspect_results)
        self.reverse_picker = FeatureDragger(self.reverse_picker_button,
                                             canvas=self.canvas)
        self.reverse_picker.feature_dragged.connect(self.inspect_neighbours)

        self.reverse_picker_button.setEnabled(False)
        self.inspect_picker_button.setEnabled(False)
        self.export_csv_button.setEnabled(False)
        self.attribute_table_button.setEnabled(False)

        # initialize the timer running when geocoding
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.update_timer)
        self._dragged_feature = None

        # unregister layers from plugin when they are removed from QGIS
        # avoids errors when user is messing around in the QGIS UI
        QgsProject.instance().layersRemoved.connect(self.unregister_layers)

    def setup_config(self):
        '''
        apply all settings from the config file to the ui, connect ui elements
        of the config section to storing changes in this file
        '''
        # search options ('expert mode')
        self.search_and_check.setChecked(config.logic_link == 'AND')
        self.search_and_check.toggled.connect(
            lambda: setattr(config, 'logic_link', 'AND'))
        self.search_or_check.toggled.connect(
            lambda: setattr(config, 'logic_link', 'OR'))
        self.fuzzy_check.setChecked(config.fuzzy)
        self.fuzzy_check.toggled.connect(
            lambda checked: setattr(config, 'fuzzy', checked))

        self.background_check.setChecked(config.load_background)
        self.background_check.toggled.connect(
            lambda checked: setattr(config, 'load_background', checked))

        # API key and url
        self.api_key_edit.setText(config.api_key)
        self.api_url_edit.setText(config.api_url)

        def api_key_edited():
            api_key = self.api_key_edit.text()
            setattr(config, 'api_key', api_key)
            url = BKGGeocoder.get_url(api_key)
            self.api_url_edit.setText(url)
            setattr(config, 'api_url', url)
            self.setup_crs()

        self.api_key_edit.editingFinished.connect(api_key_edited)
        self.api_url_edit.editingFinished.connect(
            lambda: setattr(config, 'api_url', self.api_url_edit.text()))
        self.api_url_edit.editingFinished.connect(self.setup_crs)
        self.reload_UUID_button.clicked.connect(self.setup_crs)

        if config.use_api_url:
            self.api_url_check.setChecked(True)
        else:
            self.api_key_check.setChecked(True)
        self.api_key_check.toggled.connect(
            lambda checked: setattr(config, 'use_api_url', not checked))

        def toggle_encoding(enabled):
            config.show_encoding = enabled  # lazy
            self.encoding_label.setVisible(enabled)
            self.encoding_combo.setVisible(enabled)

        self.encoding_check.setChecked(config.show_encoding)
        self.encoding_check.toggled.connect(toggle_encoding)
        toggle_encoding(config.show_encoding)

        # crs config
        idx = self.output_projection_combo.findData(config.projection)
        self.output_projection_combo.setCurrentIndex(idx)
        self.output_projection_combo.currentIndexChanged.connect(
            lambda: setattr(config, 'projection',
                            self.output_projection_combo.currentData()))

        # filters ("Regionalschlüssel" and spatial filter)
        self.selected_features_only_check.setChecked(
            config.selected_features_only)
        self.selected_features_only_check.toggled.connect(
            lambda checked: setattr(config, 'selected_features_only', checked))
        self.rs_edit.setText(config.rs)
        self.rs_edit.textChanged.connect(
            lambda text: setattr(config, 'rs', text))
        self.use_rs_check.setChecked(config.use_rs)
        self.use_rs_check.toggled.connect(
            lambda checked: setattr(config, 'use_rs', checked))
        self.debug_check.setChecked(config.debug)
        self.debug_check.toggled.connect(
            lambda checked: setattr(config, 'debug', checked))

        # output layer style
        # workaround: version change included name changes of predefined styles
        if not os.path.exists(config.output_style):
            config.output_style = DEFAULT_STYLE
        self.layer_style_edit.setText(config.output_style)
        self.layer_style_edit.editingFinished.connect(
            lambda path: setattr(config, 'output_style', path))
        self.layer_style_edit.editingFinished.connect(self.apply_output_style)

        def browse_file():
            path, sf = QFileDialog.getOpenFileName(
                self,
                'Layerstil wählen',
                filter="QGIS-Layerstildatei(*.qml)",
                directory=STYLE_PATH)
            if path:
                self.layer_style_edit.setText(path)
                config.output_style = path
                self.apply_output_style()

        self.style_browse_button.clicked.connect(browse_file)

        # label field
        self.label_field_combo.currentIndexChanged.connect(self.apply_label)

        # additional result fields
        grid = self.output_fields_group.layout()
        i = 0

        def toggle_result_field(field, checked):
            self.result_fields[field.name] = field, checked
            result_fields = config.result_fields
            if checked:
                if field.name not in config.result_fields:
                    result_fields.append(field.name)
            elif field.name in config.result_fields:
                result_fields.remove(field.name)
            # set to trigger auto-write
            config.result_fields = result_fields

        # selectable optional result fields
        for field, active in self.result_fields.values():
            if field.optional:
                label = field.alias.replace(' laut Dienst', '')
                check = QCheckBox(label)
                checked = field.name in config.result_fields
                check.setChecked(checked)
                self.result_fields[field.name] = field, checked
                check.toggled.connect(
                    lambda state, f=field: toggle_result_field(f, state))
                grid.addWidget(check, i // 2, i % 2)
                i += 1

    def apply_output_style(self):
        '''
        apply currently set style file to current output layer
        '''
        layer = self.output.layer if self.output else None
        if not layer:
            return
        self.canvas.refresh()
        layer.loadNamedStyle(config.output_style)
        for field, active in self.result_fields.values():
            idx = field.idx(layer)
            layer.setFieldAlias(idx, field.alias)
        if self.label_field_name:
            self.apply_label()

    def apply_label(self):
        '''
        apply the label of the currently selected label field to the output
        layer
        '''
        layer = self.input.layer if self.input else None
        if not layer:
            return
        self.label_field_name = self.label_field_combo.currentData()
        self.label_cache[layer.id] = self.label_field_name

        layer = self.output.layer if self.output else None
        if not layer:
            return
        if not self.label_field_name:
            layer.setLabelsEnabled(False)
            layer.reload()
            return

        layer.setLabelsEnabled(True)
        labeling = layer.labeling()
        if not labeling:
            settings = QgsPalLayerSettings()
            settings.enabled = True
            buffer = QgsTextBufferSettings()
            buffer.setEnabled(True)
            buffer.setSize(0.8)
            text_format = QgsTextFormat()
            text_format.setBuffer(buffer)
            text_format.setSize(8)
            settings.setFormat(text_format)
            labeling = QgsVectorLayerSimpleLabeling(settings)
        settings = labeling.settings()
        settings.fieldName = self.label_field_name
        labeling.setSettings(settings)
        layer.setLabeling(labeling)
        layer.reload()

    def toggle_start_button(self):
        '''
        enable start button if all inputs are made and valid, else disable
        '''
        enable = (self.input is not None and self.valid_bkg_key
                  and self.field_map is not None
                  and self.field_map.count_active() > 0)
        self.request_start_button.setEnabled(enable)

    def check_rs(self, rs: str) -> bool:
        '''
        validate the given "Regionalschlüssel"

        Parameters
        ----------
        rs: str
            "Regionalschlüssel" to validate

        Returns
        -------
        bool
            True if valid, False if not valid
        '''
        if not rs:
            return False
        regex = '^[01]\d{0,11}\*?$'
        return re.match(regex, rs) is not None

    def setup_crs(self):
        '''
        request service-url for available crs and populate crs-combobox with
        retrieved values
        '''
        self.uuid_group.setEnabled(False)
        self.request_start_button.setEnabled(False)
        current_crs = self.output_projection_combo.currentData()
        self.output_projection_combo.clear()
        # fill crs combo
        url = config.api_url if config.use_api_url else None
        success, msg, available_crs = BKGGeocoder.get_crs(key=config.api_key,
                                                          url=url)
        self.key_error_label.setText(msg)
        self.key_error_label.setVisible(not success)
        self.valid_bkg_key = success
        self.toggle_start_button()
        for code, name in available_crs:
            self.output_projection_combo.addItem(f'{name} ({code})', code)
        if current_crs:
            idx = self.output_projection_combo.findData(current_crs)
            self.output_projection_combo.setCurrentIndex(idx)
        self.uuid_group.setEnabled(True)

    def inspect_results(self, feature_id: int):
        '''
        open inspect dialog with results listed for feature with given id of
        current output-layer

        Parameters
        ----------
        feature_id : int
            id of the feature to inspect the results of
        '''
        layer = self.output.layer if self.output else None
        if not layer:
            return
        # get the results for given feature id from the cache
        results = self.result_cache.get((layer.id(), feature_id), None)
        # ToDo: warning dialog or pass it to results diag and show warning there
        if not results:
            return
        # close dialog if there is already one opened
        if self.inspect_dialog:
            self.inspect_dialog.close()
        feature = layer.getFeature(feature_id)
        # fields and their values that were active during search are shown
        # in dialog
        review_fields = [
            f for f in self.field_map.fields() if self.field_map.active(f)
        ]
        label = (feature.attribute(self.label_field_name)
                 if self.label_field_name else '')

        self.inspect_dialog = InspectResultsDialog(
            feature,
            results,
            self.canvas,
            preselect=feature.attribute(self.result_fields['i'][0].field_name),
            parent=self,
            crs=layer.crs().authid(),
            review_fields=review_fields,
            label=label,
            text_field=self.result_fields['text'][0].field_name)
        accepted = self.inspect_dialog.show()
        # set picked result when user accepted
        if accepted:
            self.set_bkg_result(feature,
                                self.inspect_dialog.result,
                                i=self.inspect_dialog.i,
                                set_edited=True)
        self.canvas.refresh()
        self.inspect_dialog = None

    def inspect_neighbours(self, feature_id: int, point: QgsPointXY):
        '''
        reverse geocode given point, open dialog to pick result from and apply
        user choice to given dragged feature

        Parameters
        ----------
        feature_id : int
            id of the feature to set the reverse geocoding results to,
            was most likely dragged by user to new position
        point : QgsPointXY
            the position the feature was dragged to
        '''

        layer = self.output.layer if self.output else None
        if not layer:
            return
        layer.startEditing()

        dragged_feature = layer.getFeature(feature_id)
        prev_dragged_id = (self._dragged_feature.id()
                           if self._dragged_feature else None)
        if feature_id != prev_dragged_id:
            if self.reverse_dialog:
                self.reverse_dialog.close()
            # reset geometry of previously dragged feature
            if prev_dragged_id is not None:
                layer.changeGeometry(prev_dragged_id, self._init_drag_geom)
            # remember initial geometry because geometry of dragged feature
            # will be changed in place
            self._init_drag_geom = dragged_feature.geometry()
            self._dragged_feature = dragged_feature

        crs = layer.crs().authid()
        url = config.api_url if config.use_api_url else None

        output_crs = layer.crs()
        # point geometry originates from clicking on canvas ->
        # transform into crs of feature
        transform = QgsCoordinateTransform(
            self.canvas.mapSettings().destinationCrs(), output_crs,
            QgsProject.instance())
        current_geom = QgsGeometry.fromPointXY(transform.transform(point))

        # apply the geometry to the feature
        layer.changeGeometry(feature_id, current_geom)

        # request feature again, otherwise geometry remains be unchanged
        dragged_feature = layer.getFeature(feature_id)
        bkg_geocoder = BKGGeocoder(key=config.api_key,
                                   crs=crs,
                                   url=url,
                                   logic_link=config.logic_link)
        rev_geocoding = ReverseGeocoding(bkg_geocoder, [dragged_feature],
                                         parent=self)

        def error(msg, level):
            self.log(msg, debug_only=True, level=level)
            QMessageBox.information(self, 'Fehler', msg)

        rev_geocoding.error.connect(lambda msg: error(msg, Qgis.Critical))
        rev_geocoding.warning.connect(lambda msg: error(msg, Qgis.Warning))
        rev_geocoding.message.connect(
            lambda msg: self.log(msg, debug_only=True))

        def done(feature, r):
            '''open dialog / set results when reverse geocoding is done'''
            results = r.json()['features']
            # only one opened dialog at a time
            if not self.reverse_dialog:
                review_fields = [
                    f for f in self.field_map.fields()
                    if self.field_map.active(f)
                ]
                # remember the initial geometry
                self._init_rev_geom = feature.geometry()
                label = (feature.attribute(self.label_field_name)
                         if self.label_field_name else '')
                self.reverse_dialog = ReverseResultsDialog(
                    feature,
                    results,
                    self.canvas,
                    review_fields=review_fields,
                    parent=self,
                    crs=output_crs.authid(),
                    label=label,
                    text_field=self.result_fields['text'][0].field_name)
                accepted = self.reverse_dialog.show()
                if accepted:
                    result = self.reverse_dialog.result
                    # apply the geometry of the selected result
                    # (no result is selected -> geometry of dragged point is
                    # kept)
                    if result:
                        result['properties']['score'] = 1
                        self.set_bkg_result(
                            feature,
                            result,
                            i=-1,
                            set_edited=True,
                            geom_only=self.reverse_dialog.geom_only
                            #,apply_adress=not self.reverse_dialog.geom_only
                        )
                    self.result_fields['manuell_bearbeitet'][0].set_value(
                        layer, feature_id, True)
                else:
                    # reset the geometry if rejected
                    try:
                        layer.changeGeometry(self._dragged_feature.id(),
                                             self._init_drag_geom)
                    # catch error when quitting QGIS with dialog opened
                    # (layer is already deleted at this point)
                    except RuntimeError:
                        pass
                self._dragged_feature = None
                self.canvas.refresh()
                self.reverse_dialog = None
                self.reverse_picker.reset()
            else:
                # workaround for updating the feature position inside
                # the dialog
                self.reverse_dialog.feature.setGeometry(current_geom)
                # update the result options in the dialog
                self.reverse_dialog.update_results(results)

        # do the actual reverse geocoding
        rev_geocoding.feature_done.connect(done)
        rev_geocoding.start()

    def show_attribute_table(self):
        '''
        open the QGIS attribute table for current output layer
        '''
        layer = self.output.layer
        if not self.output.layer:
            return
        self.iface.showAttributeTable(layer)

    def unregister_layers(self, layer_ids: List[str]):
        '''
        removes all cached relations to given layers and resets output or
        input if they are part of the given layer list

        Parameters
        ----------
        layer_ids : list
             list of ids of layers to unregister
        '''
        io_removed = False
        for layer_id in layer_ids:
            self.field_map_cache.pop(layer_id, None)
            self.label_cache.pop(layer_id, None)
            # remove results if layer was output layer
            if layer_id in self.output_layer_ids:
                self.output_layer_ids.remove(layer_id)
                remove_keys = [
                    k for k in self.result_cache.keys() if k[0] == layer_id
                ]
                for k in remove_keys:
                    self.result_cache.pop(k)
            # current output layer removed -> reset ui
            if self.output and layer_id == self.output.id:
                self.reset_output()
                io_removed = True
                self.log(
                    'Ergebnisse wurden zurückgesetzt, da der '
                    'Ergebnislayer entfernt wurde.',
                    level=Qgis.Warning)
            if self.input and layer_id == self.input.id:
                self.input = None
                io_removed = True
                self.field_map = None
                clear_layout(self.parameter_grid)
        if io_removed and self.geocoding:
            self.geocoding.kill()
            self.log(
                'Eingabe-/Ausgabelayer wurden während des '
                'Geocodings gelöscht. Breche ab...',
                level=Qgis.Critical)

    def reset_output(self):
        '''
        resets the current output to none and disables UI elements connected
        to the results
        '''
        self.output = None
        self.reverse_picker_button.setEnabled(False)
        self.inspect_picker_button.setEnabled(False)
        self.export_csv_button.setEnabled(False)
        self.attribute_table_button.setEnabled(False)
        self.reverse_picker.set_active(False)
        self.inspect_picker.set_active(False)
        if self.inspect_dialog:
            self.inspect_dialog.close()
        if self.reverse_dialog:
            self.reverse_dialog.close()

    def export_csv(self):
        '''
        open the QGIS export dialog
        '''
        layer = self.output.layer if self.output else None
        if not layer:
            return
        self.iface.setActiveLayer(layer)
        actions = self.iface.layerMenu().actions()
        for action in actions:
            if action.objectName() == 'mActionLayerSaveAs':
                break
        action.trigger()

    def import_csv(self):
        '''
        open the QGIS import dialog with CSV preselected
        '''
        actions = self.iface.addLayerMenu().actions()
        for action in actions:
            if action.objectName() == 'mActionAddDelimitedText':
                break
        action.trigger()

    def unload(self):
        pass

    def closeEvent(self, event):
        '''
        override, emit closing signal
        '''
        self.reverse_picker.set_active(False)
        self.inspect_picker.set_active(False)
        self.iface.removeDockWidget(self)
        self.closingWidget.emit()
        event.accept()

    def show(self):
        '''
        open this widget
        '''
        # dock widget has to start docked
        self.iface.addDockWidget(Qt.LeftDockWidgetArea, self)
        # undock it immediately and resize to content
        self.setFloating(True)

        # switch to config tab to update min size of dock widget
        # otherwise widget tends to be have a very high height (as if all
        # config groups are expanded)
        self.tab_widget.setCurrentIndex(1)
        height = self.config_request_output_tab.layout().minimumSize().height()
        # set a fixed position, otherwise it is floating in a weird position
        self.setGeometry(500, 500, self.sizeHint().width(), height + 100)
        # switch back to input tab
        self.tab_widget.setCurrentIndex(0)
        # for some reason the height is ignored when setting geometry when
        # calling show() the first time
        self.resize(self.sizeHint().width(), max(height + 100, 500))

    def add_background(self):
        # load background maps on opening plugin
        bg_grey = TopPlusOpen(groupname='Hintergrundkarten',
                              greyscale=True,
                              crs='EPSG:25832')  #config.projection)
        bg_grey.draw('TopPlusOpen Graustufen (bkg.bund.de)', checked=True)
        bg_osm = TopPlusOpen(groupname='Hintergrundkarten',
                             crs='EPSG:25832')  #crs=config.projection)
        bg_osm.draw('TopPlusOpen (bkg.bund.de)', checked=False)
        for layer in [bg_osm, bg_grey]:
            layer.layer.setTitle(
                '© Bundesamt für Kartographie und Geodäsie 2020, '
                'Datenquellen: https://sg.geodatenzentrum.de/web_public/'
                'Datenquellen_TopPlus_Open.pdf')

    def log(self, text: str, level: int = Qgis.Info, debug_only=False):
        '''
        display given text in the log section

        Parameters
        ----------
        text : str
            the text to display in the log
        color : int, optional
            the qgis message level, defaults to Info
        '''
        color = 'black' if level == Qgis.Info else 'red' \
            if level == Qgis.Critical else 'orange'
        # don't show debug messages in log section
        if not debug_only:
            self.log_edit.moveCursor(QTextCursor.End)
            self.log_edit.insertHtml(
                f'<span style="color: {color}">{text}</span><br>')
            scrollbar = self.log_edit.verticalScrollBar()
            scrollbar.setValue(scrollbar.maximum())
        # always show critical messages in debug log, others only in debug mode
        if level == Qgis.Critical or config.debug:
            QgsMessageLog.logMessage(text, 'BKG Geocoder', level=level)

    def change_layer(self, layer: QgsVectorLayer):
        '''
        sets given layer to being the input of the geocoding,
        add field checks depending on given layer to UI and preset layer-related
        UI elements

        Parameters
        ----------
        layer : QgsVectorLayer
            the layer to change the UI to
        '''
        self.request_start_button.setEnabled(False)
        if not layer:
            return
        # set layer combo to given layer if it is not set to it
        if self.layer_combo.currentLayer().id() != layer.id():
            idx = -1
            for idx in range(len(self.layer_combo)):
                if self.layer_combo.layer(idx).id() == layer.id():
                    break
            self.layer_combo.setCurrentIndex(idx)

        self.input = LayerWrapper(layer)

        # layer can only be updated in place if it has a point geometry
        if layer.wkbType() != QgsWkbTypes.Point:
            self.update_input_layer_check.setChecked(False)
            self.update_input_layer_check.setEnabled(False)
        else:
            self.update_input_layer_check.setEnabled(True)
            # by default store results in selected layer if it is an output
            # layer. otherwise use create a new output layer when geocoding
            # (can be overridden by user)
            self.update_input_layer_check.setChecked(
                layer.id() in self.output_layer_ids)

        # set selected encoding in combobox to encoding of layer
        encoding = layer.dataProvider().encoding()
        self.encoding_combo.blockSignals(True)
        self.encoding_combo.setCurrentText(encoding)
        self.encoding_combo.blockSignals(False)

        # get field map with previous settings if layer was already used as
        # input before
        self.field_map = self.field_map_cache.get(layer.id(), None)
        if not self.field_map or not self.field_map.valid(layer):
            # if no field map was set yet, create it with the known BKG
            # keywords
            # ignore result fields (can't be mapped)
            bkg_f = [
                f[0].field_name_comp(layer)
                for f in self.result_fields.values()
            ]
            self.field_map = FieldMap(layer,
                                      ignore=bkg_f,
                                      keywords=BKGGeocoder.keywords)
            self.field_map_cache[layer.id()] = self.field_map
        # remove old widgets
        clear_layout(self.parameter_grid)

        # create a list of checkable items out of the fields of the layer
        for i, field_name in enumerate(self.field_map.fields()):
            checkbox = QCheckBox()
            checkbox.setText(field_name)
            # combobox for user-selection of a API-keyword matching the field
            combo = QComboBox()
            combo.addItem('Volltextsuche', None)
            for key, (text, regex) in BKGGeocoder.keywords.items():
                combo.addItem(text, key)

            def checkbox_changed(state: bool, combo: QComboBox,
                                 field_name: str):
                checked = state != 0
                self.field_map.set_active(field_name, checked)
                combo.setEnabled(checked)
                self.toggle_start_button()

            # apply changes to field map and combobox on check-state change
            checkbox.stateChanged.connect(
                lambda s, c=combo, f=field_name: checkbox_changed(s, c, f))
            # set initial check state
            checkbox_changed(self.field_map.active(field_name), combo,
                             field_name)

            def combo_changed(idx: int, combo: QComboBox, field_name: str):
                self.field_map.set_keyword(field_name, combo.itemData(idx))

            # apply changes field map when selecting different keyword
            combo.currentIndexChanged.connect(
                lambda i, c=combo, f=field_name: combo_changed(i, c, f))
            # set initial combo index
            cur_idx = combo.findData(self.field_map.keyword(field_name))
            combo_changed(cur_idx, combo, field_name)

            self.parameter_grid.addWidget(checkbox, i, 0)
            self.parameter_grid.addWidget(combo, i, 1)

            # initial state
            checked = self.field_map.active(field_name)
            keyword = self.field_map.keyword(field_name)
            checkbox.setChecked(checked)
            if keyword is not None:
                combo_idx = combo.findData(keyword)
                combo.setCurrentIndex(combo_idx)
                combo.setEnabled(checked)

        # label selection
        self.label_field_combo.blockSignals(True)
        self.label_field_combo.clear()
        self.label_field_combo.addItem('kein Label')
        aliases = {
            f[0].field_name_comp(layer): f[0].alias
            for f in self.result_fields.values()
        }
        for field in layer.fields():
            field_name = field.name()
            alias = aliases.get(field_name)
            self.label_field_combo.addItem(alias or field_name, field_name)
        self.label_field_combo.blockSignals(False)

        # try to set prev. selected field
        label_field = self.label_cache.get(layer.id())
        if label_field is None and self.output and self.output.layer:
            label_field = self.label_cache.get(self.output.id)
        idx = self.label_field_combo.findData(label_field)
        self.label_field_combo.setCurrentIndex(max(idx, 0))

        self.toggle_start_button()

    def set_encoding(self, encoding: str):
        '''
        set encoding of input layer and redraw the parameter section

        Parameters
        ----------
        encoding : str
            the name of the encoding e.g. 'utf-8'
        '''
        layer = self.input.layer if self.input else None
        if not layer:
            return
        layer.dataProvider().setEncoding(encoding)
        layer.updateFields()
        # repopulate fields
        self.change_layer(layer)

    def bkg_geocode(self):
        '''
        start geocoding of input layer with current settings
        '''
        layer = self.input.layer if self.input else None
        if not layer:
            return
        self.progress_bar.setStyleSheet('')
        self.reverse_picker_button.setEnabled(False)
        self.inspect_picker_button.setEnabled(False)
        self.export_csv_button.setEnabled(False)
        self.attribute_table_button.setEnabled(False)

        active_count = self.field_map.count_active()
        if active_count == 0:
            QMessageBox.information(
                self, 'Fehler', (u'Es sind keine Adressfelder ausgewählt.\n\n'
                                 u'Start abgebrochen...'))
            return

        rs = None
        if self.use_rs_check.isChecked():
            valid = self.check_rs(config.rs)
            if not valid:
                self.log(
                    'Der Regionalschlüssel ist ungültig und wird '
                    'ignoriert.',
                    level=Qgis.Warning)
            else:
                rs = config.rs

        if config.selected_features_only:
            features = layer.selectedFeatures()
        # if no features are selected (or all should be taken in first place)
        # -> take all features
        if not config.selected_features_only or len(features) == 0:
            features = list(layer.getFeatures())

        # input layer is flagged as output layer
        if self.update_input_layer_check.isChecked():
            if layer.wkbType() != QgsWkbTypes.Point:
                QMessageBox.information(
                    self, 'Fehler',
                    (u'Der Layer enthält keine Punktgeometrie. Daher können '
                     u'die Ergebnisse nicht direkt dem Layer hinzugefügt '
                     u'werden.\n'
                     u'Fügen Sie dem Layer eine Punktgeometrie hinzu oder '
                     u'deaktivieren Sie die Checkbox '
                     u'"Ausgangslayer aktualisieren".\n\n'
                     u'Start abgebrochen...'))
                return
            self.output = LayerWrapper(layer)
            self.output.layer.setCrs(
                QgsCoordinateReferenceSystem(config.projection))
        # create output layer as a clone of input layer
        else:
            self.output = LayerWrapper(
                clone_layer(layer,
                            name=f'{layer.name()}_ergebnisse',
                            crs=config.projection,
                            features=features))
            QgsProject.instance().addMapLayer(self.output.layer, False)
            # add output to same group as input layer
            tree_layer = QgsProject.instance().layerTreeRoot().findLayer(layer)
            group = tree_layer.parent()
            group.insertLayer(0, self.output.layer)
            self.output_layer_ids.append(self.output.id)
            # cloned layer gets same mapping, it has the same fields
            cloned_field_map = self.field_map.copy(layer=self.output.layer)
            self.field_map_cache[self.output.id] = cloned_field_map
            self.label_cache[self.output.id] =\
                self.label_cache.get(layer.id())
            # take features of output layer as input to match the ids of the
            # geocoding
            features = list(self.output.layer.getFeatures())

        self.success_count = 0
        self.feat_count = len(features)

        self.apply_label()

        layer.setReadOnly(True)
        self.output.layer.setReadOnly(True)

        area_wkt = None
        if self.use_spatial_filter_check.isChecked():
            spatial_layer = self.spatial_filter_combo.currentLayer()
            if spatial_layer:
                selected_only = self.spatial_selected_only_check.isChecked()
                geometries = get_geometries(spatial_layer,
                                            selected=selected_only,
                                            crs=config.projection)
                union = None
                for geom in geometries:
                    union = geom if not union else union.combine(geom)
                area_wkt = union.asWkt()

        url = config.api_url if config.use_api_url else None

        bkg_geocoder = BKGGeocoder(key=config.api_key,
                                   crs=config.projection,
                                   url=url,
                                   logic_link=config.logic_link,
                                   rs=rs,
                                   area_wkt=area_wkt,
                                   fuzzy=config.fuzzy)
        self.geocoding = Geocoding(bkg_geocoder,
                                   self.field_map,
                                   features=features,
                                   parent=self)

        self.geocoding.message.connect(
            lambda msg: self.log(msg, debug_only=True))

        def feature_done(f, r):
            label = f.attribute(self.label_field_name) \
                if (self.label_field_name) else f'Feature {f.id()}'
            results = r.json()['features']
            message = (f'{label} -> <b>{len(results)} </b> Ergebnis(se)')
            if len(results) > 0:
                self.success_count += 1
            self.log(message,
                     level=Qgis.Info if len(results) > 0 else Qgis.Warning)
            self.output.layer.setReadOnly(False)
            self.store_bkg_results(f, results)
            self.output.layer.setReadOnly(True)

        self.geocoding.progress.connect(self.progress_bar.setValue)
        self.geocoding.feature_done.connect(feature_done)
        self.geocoding.error.connect(
            lambda msg: self.log(msg, level=Qgis.Critical))
        self.geocoding.warning.connect(
            lambda msg: self.log(msg, level=Qgis.Warning))
        self.geocoding.finished.connect(self.geocoding_done)

        self.inspect_picker.set_layer(self.output.layer)
        self.reverse_picker.set_layer(self.output.layer)

        self.tab_widget.setCurrentIndex(2)

        # add active result fields if they are not already part of the layer
        add_fields = [
            f[0].to_qgs_field() for f in self.result_fields.values()
            if f[1] and f[0].idx(layer) < 0
        ]
        self.output.layer.dataProvider().addAttributes(add_fields)
        self.output.layer.updateFields()
        self.apply_output_style()

        self.request_start_button.setVisible(False)
        self.request_stop_button.setVisible(True)
        self.log(f'<br>Starte Geokodierung <b>{layer.name()}</b>')
        self.start_time = datetime.datetime.now()
        self.timer.start(1000)

        if config.load_background:
            self.add_background()
        self.geocoding.start()

    def update_timer(self):
        '''
        update the timer counting the time since starting the geocoding
        '''
        delta = datetime.datetime.now() - self.start_time
        h, remainder = divmod(delta.seconds, 3600)
        m, s = divmod(remainder, 60)
        timer_text = '{:02d}:{:02d}:{:02d}'.format(h, m, s)
        self.elapsed_time_label.setText(timer_text)

    def geocoding_done(self, success: bool):
        '''
        update UI when geocoding is done

        Parameters
        ----------
        success : bool
            whether the geocoding was run successfully without errors or not
        '''
        self.geocoding = None
        if not self.input or not self.output:
            return
        self.input.layer.setReadOnly(False)
        self.output.layer.setReadOnly(False)
        if success:
            self.log(f'Geokodierung von {self.feat_count} '
                     'Feature(s) abgeschlossen.')
            fail_count = self.feat_count - self.success_count
            if fail_count:
                self.log(f'{fail_count} Feature(s) lieferten keine Ergebnisse',
                         level=Qgis.Warning
                         if fail_count < self.feat_count else Qgis.Critical)
        else:
            self.progress_bar.setStyleSheet(
                'QProgressBar::chunk {background-color: red;}')
        # select output layer as current layer
        self.layer_combo.setLayer(self.output.layer)
        # zoom to extent of results
        extent = self.output.layer.extent()
        if not extent.isEmpty():
            transform = QgsCoordinateTransform(
                self.output.layer.crs(),
                self.canvas.mapSettings().destinationCrs(),
                QgsProject.instance())
            self.canvas.setExtent(transform.transform(extent))
            self.canvas.zoomByFactor(1.2)
        self.canvas.refresh()
        self.output.layer.reload()
        self.timer.stop()

        # update the states of the buttons
        self.request_start_button.setVisible(True)
        self.request_stop_button.setVisible(False)
        self.reverse_picker_button.setEnabled(True)
        self.inspect_picker_button.setEnabled(True)
        self.export_csv_button.setEnabled(True)
        self.attribute_table_button.setEnabled(True)

    def store_bkg_results(self, feature: QgsFeature, results: List[dict]):
        '''
        store the results (geojson features) per feature in the result cache

        Parameters
        ----------
        feature : QgsFeature
            the feature to store the results for
        results : list
            the geojson feature list of all matches returned by the BKG geocoder
        '''
        if not self.output:
            return
        if results:
            results.sort(key=lambda x: x['properties']['score'], reverse=True)
            best = results[0]
        else:
            best = None
        self.result_cache[self.output.id, feature.id()] = results
        self.set_bkg_result(feature, best, i=0, n_results=len(results))

    def set_bkg_result(self,
                       feature: QgsFeature,
                       result: dict,
                       i: int = -1,
                       n_results: int = None,
                       geom_only: bool = False,
                       set_edited: bool = False):  #, apply_adress=False):
        '''
        set result of BKG geocoding to given feature of current output layer (
        including properties 'typ', 'text', 'score', 'treffer' and the geometry)

        Parameters
        ----------
        feature : QgsFeature
            the feature to set the result to
        result : dict
            the geojson response of the BKG geocoder whose attributes to apply
            to the feature
        i : int, optional
            the index of the result in the list, -1 to indicate it is not in the
            list, defaults to not in results list
        n_results : int, optional
            number of results returned by the BKG geocoder in total,
            defaults to None
        geom_only : bool, optional
            only apply the geometry of the result to the feature, defaults to
            applying all atributes
        set_edited : bool, optional
            mark feature as manually edited, defaults to mark as not edited
        '''
        layer = self.output.layer if self.output else None
        if not layer:
            return
        if not layer.isEditable():
            layer.startEditing()

        feat_id = feature.id()
        if result:
            coords = result['geometry']['coordinates']
            geom = QgsGeometry.fromPointXY(QgsPointXY(coords[0], coords[1]))
            properties = result['properties']
            layer.changeGeometry(feat_id, geom)
            if not geom_only:
                # apply all result properties if corresponding field is
                # available and active
                for rf, active in self.result_fields.values():
                    if not active:
                        continue
                    value = properties.get(rf.name, None)
                    if value is not None:
                        rf.set_value(layer, feat_id, value)
                if n_results:
                    self.result_fields['n_results'][0].set_value(
                        layer, feat_id, n_results)
            self.result_fields['i'][0].set_value(layer, feat_id, i)
        else:
            for rf, active in self.result_fields.values():
                if active:
                    rf.set_value(layer, feat_id, None)
            layer.changeGeometry(feat_id, QgsGeometry())
        self.result_fields['manuell_bearbeitet'][0].set_value(
            layer, feat_id, set_edited)
        layer.commitChanges()

    def show_help(self, tag: str = ''):
        '''
        open help website in browser

        Parameters
        ----------
        tag : str
            anchor of help page to jump to on opening the help website,
            defaults to open first page
        '''
        url = HELP_URL
        if tag:
            url += f'#{tag}'
        webbrowser.open(url, new=0)

    def show_about(self):
        '''
        show information about plugin in dialog
        '''
        about = Dialog(ui_file='about.ui', parent=self)
        about.version_label.setText(str(VERSION))
        about.show()
Beispiel #28
0
class QtWaitingSpinner(QWidget):
    mColor = QColor(Qt.black)
    mRoundness = 120.0
    mMinimumTrailOpacity = 25.0
    mTrailFadePercentage = 60.0
    mRevolutionsPerSecond = 1.0
    mNumberOfLines = 20
    mLineLength = 20
    mLineWidth = 2
    mInnerRadius = 60
    mCurrentCounter = 0
    mIsSpinning = False

    def __init__(self,
                 parent=None,
                 centerOnParent=True,
                 disableParentWhenSpinning=True,
                 *args,
                 **kwargs):
        QWidget.__init__(self, parent=parent, *args, **kwargs)
        self.mCenterOnParent = centerOnParent
        self.mDisableParentWhenSpinning = disableParentWhenSpinning
        self.initialize()

    def initialize(self):
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.rotate)
        self.updateSize()
        self.updateTimer()
        self.hide()

    @pyqtSlot()
    def rotate(self):
        self.mCurrentCounter += 1
        if self.mCurrentCounter > self.numberOfLines():
            self.mCurrentCounter = 0
        self.update()

    def updateSize(self):
        size = (self.mInnerRadius + self.mLineLength) * 2
        self.setFixedSize(size, size)

    def updateTimer(self):
        self.timer.setInterval(
            1000 / (self.mNumberOfLines * self.mRevolutionsPerSecond))
        seconds = 1000 / (self.mNumberOfLines * self.mRevolutionsPerSecond)

    def updatePosition(self):
        if self.parentWidget() and self.mCenterOnParent:
            self.move(self.parentWidget().width() / 2 - self.width() / 2,
                      self.parentWidget().height() / 2 - self.height() / 2)

    def lineCountDistanceFromPrimary(self, current, primary, totalNrOfLines):
        distance = primary - current
        if distance < 0:
            distance += totalNrOfLines
        return distance

    def currentLineColor(self, countDistance, totalNrOfLines, trailFadePerc,
                         minOpacity, color):
        if countDistance == 0:
            return color

        minAlphaF = minOpacity / 100.0

        distanceThreshold = ceil((totalNrOfLines - 1) * trailFadePerc / 100.0)
        if countDistance > distanceThreshold:
            color.setAlphaF(minAlphaF)

        else:
            alphaDiff = self.mColor.alphaF() - minAlphaF
            gradient = alphaDiff / distanceThreshold + 1.0
            resultAlpha = color.alphaF() - gradient * countDistance
            resultAlpha = min(1.0, max(0.0, resultAlpha))
            color.setAlphaF(resultAlpha)
        return color

    def paintEvent(self, event):
        self.updatePosition()
        painter = QPainter(self)
        painter.fillRect(self.rect(), Qt.transparent)
        painter.setRenderHint(QPainter.Antialiasing, True)
        if self.mCurrentCounter > self.mNumberOfLines:
            self.mCurrentCounter = 0
        painter.setPen(Qt.NoPen)

        for i in range(self.mNumberOfLines):
            painter.save()
            painter.translate(self.mInnerRadius + self.mLineLength,
                              self.mInnerRadius + self.mLineLength)
            rotateAngle = 360.0 * i / self.mNumberOfLines
            painter.rotate(rotateAngle)
            painter.translate(self.mInnerRadius, 0)
            distance = self.lineCountDistanceFromPrimary(
                i, self.mCurrentCounter, self.mNumberOfLines)
            color = self.currentLineColor(distance, self.mNumberOfLines,
                                          self.mTrailFadePercentage,
                                          self.mMinimumTrailOpacity,
                                          self.mColor)
            painter.setBrush(color)
            painter.drawRoundedRect(
                QRect(0, -self.mLineWidth // 2, self.mLineLength,
                      self.mLineLength), self.mRoundness, Qt.RelativeSize)
            painter.restore()

    def start(self):
        self.updatePosition()
        self.mIsSpinning = True
        self.show()

        if self.parentWidget() and self.mDisableParentWhenSpinning:
            self.parentWidget().setEnabled(False)

        if not self.timer.isActive():
            self.timer.start()
            self.mCurrentCounter = 0

    def stop(self):
        self.mIsSpinning = False
        self.hide()

        if self.parentWidget() and self.mDisableParentWhenSpinning:
            self.parentWidget().setEnabled(True)

        if self.timer.isActive():
            self.timer.stop()
            self.mCurrentCounter = 0

    def setNumberOfLines(self, lines):
        self.mNumberOfLines = lines
        self.updateTimer()

    def setLineLength(self, length):
        self.mLineLength = length
        self.updateSize()

    def setLineWidth(self, width):
        self.mLineWidth = width
        self.updateSize()

    def setInnerRadius(self, radius):
        self.mInnerRadius = radius
        self.updateSize()

    def color(self):
        return self.mColor

    def roundness(self):
        return self.mRoundness

    def minimumTrailOpacity(self):
        return self.mMinimumTrailOpacity

    def trailFadePercentage(self):
        return self.mTrailFadePercentage

    def revolutionsPersSecond(self):
        return self.mRevolutionsPerSecond

    def numberOfLines(self):
        return self.mNumberOfLines

    def lineLength(self):
        return self.mLineLength

    def lineWidth(self):
        return self.mLineWidth

    def innerRadius(self):
        return self.mInnerRadius

    def isSpinning(self):
        return self.mIsSpinning

    def setRoundness(self, roundness):
        self.mRoundness = min(0.0, max(100, roundness))

    def setColor(self, color):
        self.mColor = color

    def setRevolutionsPerSecond(self, revolutionsPerSecond):
        self.mRevolutionsPerSecond = revolutionsPerSecond
        self.updateTimer()

    def setTrailFadePercentage(self, trail):
        self.mTrailFadePercentage = trail

    def setMinimumTrailOpacity(self, minimumTrailOpacity):
        self.mMinimumTrailOpacity = minimumTrailOpacity
Beispiel #29
0
class AdjustmentDialog(QDialog, Ui_AdjustmentDialogUI):
    """
    Dialog window that is shown after the optimization has successfully run
    through. Users can change the calculated cable layout by changing pole
    position, height, angle and the properties of the cable line. The cable
    line is then recalculated and the new layout is shown in a plot.
    """
    def __init__(self, interface, confHandler):
        """
        :type confHandler: configHandler.ConfigHandler
        """
        QDialog.__init__(self, interface.mainWindow())
        self.iface = interface

        # Management of Parameters and settings
        self.confHandler = confHandler
        self.confHandler.setDialog(self)
        self.profile = self.confHandler.project.profile
        self.poles = self.confHandler.project.poles
        # Max distance the anchors can move away from initial position
        self.anchorBuffer = self.confHandler.project.heightSource.buffer

        # Load data
        self.originalData = {}
        self.result = {}
        self.cableline = {}
        self.status = None
        self.doReRun = False

        # Setup GUI from UI-file
        self.setupUi(self)

        self.drawTool = MapMarkerTool(self.iface.mapCanvas())

        # Create plot
        self.plot = AdjustmentPlot(self)
        # Pan/Zoom tools for plot, pan already active
        tbar = MyNavigationToolbar(self.plot, self)
        tbar.pan()
        self.plot.setToolbar(tbar)
        self.plotLayout.addWidget(self.plot)
        self.plotLayout.addWidget(tbar,
                                  alignment=Qt.AlignHCenter | Qt.AlignTop)

        # Fill tab widget with data
        self.poleLayout = CustomPoleWidget(self.tabPoles, self.poleVGrid,
                                           self.poles)
        # self.poleLayout.sig_zoomIn.connect(self.zoomToPole)
        # self.poleLayout.sig_zoomOut.connect(self.zoomOut)
        self.poleLayout.sig_createPole.connect(self.addPole)
        self.poleLayout.sig_updatePole.connect(self.updatePole)
        self.poleLayout.sig_deletePole.connect(self.deletePole)

        # Threshold (thd) tab
        thdTblSize = [5, 6]
        self.thdLayout = AdjustmentDialogThresholds(self, thdTblSize)
        self.thdLayout.sig_clickedRow.connect(self.showThresholdInPlot)
        self.selectedThdRow = None
        self.thdUpdater = ThresholdUpdater(self.thdLayout, thdTblSize,
                                           self.showThresholdInPlot)

        self.paramLayout = AdjustmentDialogParams(self,
                                                  self.confHandler.params)

        # Project header
        self.prHeaderFields = {
            'PrVerf': self.fieldPrVerf,
            'PrNr': self.fieldPrNr,
            'PrGmd': self.fieldPrGmd,
            'PrWald': self.fieldPrWald,
            'PrBemerkung': self.fieldPrBemerkung,
        }

        # Thread for instant recalculation when poles or parameters are changed
        self.timer = QTimer()
        self.configurationHasChanged = False
        self.isRecalculating = False
        self.unsavedChanges = True

        # Save dialog
        self.saveDialog = DialogOutputOptions(self, self.confHandler)

        # Connect signals
        self.btnClose.clicked.connect(self.onClose)
        self.btnSave.clicked.connect(self.onSave)
        self.btnBackToStart.clicked.connect(self.onReturnToStart)
        for field in self.prHeaderFields.values():
            field.textChanged.connect(self.onPrHeaderChanged)
        self.infoQ.clicked.connect(self.onShowInfoFieldQ)

    # noinspection PyMethodMayBeStatic
    def tr(self, message, **kwargs):
        """Get the translation for a string using Qt translation API.
        We implement this ourselves since we do not inherit QObject.

        :param message: String for translation.
        :type message: str, QString

        :returns: Translated version of message.
        :rtype: QString

        Parameters
        ----------
        **kwargs
        """
        # noinspection PyTypeChecker,PyArgumentList,PyCallByClass
        return QCoreApplication.translate(type(self).__name__, message)

    def loadData(self, pickleFile):
        """ Is used to load testdata from pickl object in debug mode """
        f = open(pickleFile, 'rb')
        dump = pickle.load(f)
        f.close()

        self.poles.poles = dump['poles']
        self.initData(dump, 'optiSuccess')

    def initData(self, result, status):
        if not result:
            self.close()
        # Save original data from optimization
        self.originalData = result
        # result properties: cable line, optSTA, force, optLen, optLen_arr,
        #  duration
        self.result = result
        # Algorithm was skipped, no optimized solution
        if status in ['jumpedOver', 'savedFile']:
            try:
                params = self.confHandler.params.getSimpleParameterDict()
                cableline, force, \
                seil_possible = preciseCable(params, self.poles,
                                             self.result['optSTA'])
                self.result['cableline'] = cableline
                self.result['force'] = force

            except Exception as e:
                QMessageBox.critical(
                    self,
                    self.tr('Unerwarteter Fehler '
                            'bei Berechnung der Seillinie'), str(e),
                    QMessageBox.Ok)
                return

        self.cableline = self.result['cableline']
        self.profile.updateProfileAnalysis(self.cableline)

        self.updateRecalcStatus(status)

        # Draw profile in diagram
        self.plot.initData(self.profile.di_disp, self.profile.zi_disp,
                           self.profile.peakLoc_x, self.profile.peakLoc_z,
                           self.profile.surveyPnts)
        self.plot.updatePlot(self.poles.getAsArray(), self.cableline)

        # Create layout to modify poles
        lowerDistRange = floor(-1 * self.anchorBuffer[0])
        upperDistRange = floor(self.profile.profileLength +
                               self.anchorBuffer[1])
        self.poleLayout.setInitialGui([lowerDistRange, upperDistRange])

        # Fill in cable parameters
        self.paramLayout.fillInParams()

        # Fill in Threshold data
        self.thdUpdater.update(
            [
                self.cableline['groundclear_rel'],  # Distance cable - terrain
                self.result['force']['MaxSeilzugkraft']
                [0],  # Max force on cable
                self.result['force']['Sattelkraft_Total']
                [0],  # Max force on pole
                self.result['force']
                ['Lastseilknickwinkel'],  # Cable angle on pole
                self.result['force']['Leerseilknickwinkel']
            ],  # Cable angle on pole
            self.confHandler.params,
            self.poles,
            (self.status in ['jumpedOver', 'savedFile']))

        # Mark profile line and poles on map
        self.updateLineOnMap()
        self.addMarkerToMap()

        # Fill in project header data
        self.fillInPrHeaderData()

        # Start Thread to recalculate cable line every 300 milliseconds
        self.timer.timeout.connect(self.recalculate)
        self.timer.start(300)

        self.plot.zoomOut()

    def zoomToPole(self, idx):
        self.plot.zoomTo(self.poles.poles[idx])
        self.plot.updatePlot(self.poles.getAsArray(), self.cableline)

    def zoomOut(self):
        self.plot.zoomOut()
        self.plot.updatePlot(self.poles.getAsArray(), self.cableline)

    def updatePole(self, idx, property_name, newVal):
        prevAnchorA = self.poles.hasAnchorA is True
        prevAnchorE = self.poles.hasAnchorE is True
        self.poles.update(idx, property_name, newVal)
        # Update markers on map
        for i, pole in enumerate(self.poles.poles):
            if pole['active']:
                self.updateMarkerOnMap(i)
        self.updateLineOnMap()
        # Update anchors
        self.updateAnchorState(prevAnchorA, prevAnchorE)
        # self.plot.zoomTo(self.poles.poles[idx])
        self.poleLayout.changeRow(idx, property_name, newVal, prevAnchorA,
                                  prevAnchorE)
        if property_name == 'name':
            # No redraw when user only changes name
            return
        self.plot.updatePlot(self.poles.getAsArray(), self.cableline)
        self.configurationHasChanged = True

    def addPole(self, idx):
        newPoleIdx = idx + 1
        self.poles.add(newPoleIdx, None, manually=True)
        self.poleLayout.addRow(newPoleIdx)
        self.addMarkerToMap(newPoleIdx)
        # self.plot.zoomOut()
        self.plot.updatePlot(self.poles.getAsArray(), self.cableline)
        self.configurationHasChanged = True

    def deletePole(self, idx):
        self.poles.delete(idx)
        self.poleLayout.deleteRow(idx)
        self.drawTool.removeMarker(idx)
        # self.plot.zoomOut()
        self.plot.updatePlot(self.poles.getAsArray(), self.cableline)
        self.configurationHasChanged = True

    def updateAnchorState(self, prevAnchorA, prevAnchorE):
        """Update anchor markers on map: depending on nature of pole change,
        anchors can be activated or deactivated in self.poles.update."""
        if prevAnchorA is not self.poles.hasAnchorA:
            idxA = 0
            if self.poles.hasAnchorA:
                # Anchor A was activated
                point = [
                    self.poles.poles[0]['coordx'],
                    self.poles.poles[0]['coordy']
                ]
                self.drawTool.showMarker(point, idxA, 'anchor')
            else:
                # Anchor A was deactivated
                self.drawTool.hideMarker(idxA)

        if prevAnchorE is not self.poles.hasAnchorE:
            idxE = len(self.poles.poles) - 1
            if self.poles.hasAnchorE:
                # Anchor E was activated
                point = [
                    self.poles.poles[-1]['coordx'],
                    self.poles.poles[-1]['coordy']
                ]
                self.drawTool.showMarker(point, idxE, 'anchor')
            else:
                # Anchor E was deactivated
                self.drawTool.hideMarker(idxE)

    def updateLineOnMap(self):
        self.drawTool.updateLine(
            [[self.poles.firstPole['coordx'], self.poles.firstPole['coordy']],
             [self.poles.lastPole['coordx'], self.poles.lastPole['coordy']]],
            drawMarker=False)

    def addMarkerToMap(self, idx=-1):
        # Mark all poles except anchors on map
        if idx == -1:
            for idx, pole in enumerate(self.poles.poles):
                self.drawTool.drawMarker([pole['coordx'], pole['coordy']],
                                         idx,
                                         pointType=pole['poleType'],
                                         firstPoint=(idx == self.poles.idxA))
                if not pole['active']:
                    self.drawTool.hideMarker(idx)
        else:
            # Add a new pole to the map
            pole = self.poles.poles[idx]
            self.drawTool.drawMarker([pole['coordx'], pole['coordy']],
                                     idx,
                                     pointType=pole['poleType'])

    def updateMarkerOnMap(self, idx):
        point = [
            self.poles.poles[idx]['coordx'], self.poles.poles[idx]['coordy']
        ]
        self.drawTool.updateMarker(point, idx)

    def updateOptSTA(self, newVal):
        # Save new value to config Handler
        self.confHandler.params.setOptSTA(newVal)
        return str(self.confHandler.params.optSTA)

    def onShowInfoFieldQ(self):
        msg = self.tr('Erklaerung Gesamtlast')
        QMessageBox.information(self, self.tr("Gesamtlast"), msg,
                                QMessageBox.Ok)

    def updateCableParam(self):
        self.configurationHasChanged = True

    def onPrHeaderChanged(self):
        self.unsavedChanges = True

    def fillInPrHeaderData(self):
        for key, val in self.confHandler.project.prHeader.items():
            field = self.prHeaderFields[key]
            if isinstance(field, QTextEdit):
                field.setPlainText(val)
            else:
                field.setText(val)

    def readoutPrHeaderData(self):
        prHeader = {}
        for key, field in self.prHeaderFields.items():
            if isinstance(field, QTextEdit):
                prHeader[key] = field.toPlainText()
            else:
                prHeader[key] = field.text()
        self.confHandler.project.setPrHeader(prHeader)

    def updateRecalcStatus(self, status):
        self.status = status
        color = None
        green = '#b6ddb5'
        yellow = '#f4e27a'
        red = '#e8c4ca'
        ico_path = os.path.join(os.path.dirname(__file__), 'icons')
        if status == 'optiSuccess':
            self.recalcStatus_txt.setText(
                self.tr('Optimierung erfolgreich abgeschlossen'))
            self.recalcStatus_ico.setPixmap(
                QPixmap(os.path.join(ico_path, 'icon_green.png')))
        elif status == 'liftsOff':
            self.recalcStatus_txt.setText(
                self.tr('Tragseil hebt bei mindestens einer Stuetze ab'))
            self.recalcStatus_ico.setPixmap(
                QPixmap(os.path.join(ico_path, 'icon_yellow.png')))
            color = yellow
        elif status == 'notComplete':
            self.recalcStatus_txt.setText(
                self.tr('Nicht genuegend Stuetzenstandorte bestimmbar'))
            self.recalcStatus_ico.setPixmap(
                QPixmap(os.path.join(ico_path, 'icon_yellow.png')))
            color = yellow
        elif status == 'jumpedOver':
            self.recalcStatus_txt.setText(
                self.tr('Stuetzen manuell platzieren'))
            self.recalcStatus_ico.setPixmap(
                QPixmap(os.path.join(ico_path, 'icon_green.png')))
            color = yellow
        elif status == 'savedFile':
            self.recalcStatus_txt.setText(
                self.tr('Stuetzen aus Projektdatei geladen'))
            self.recalcStatus_ico.setPixmap(
                QPixmap(os.path.join(ico_path, 'icon_green.png')))
            color = yellow
        elif status == 'cableSuccess':
            self.recalcStatus_txt.setText(self.tr('Seillinie neu berechnet.'))
            self.recalcStatus_ico.setPixmap(
                QPixmap(os.path.join(ico_path, 'icon_green.png')))
        elif status == 'cableError':
            self.recalcStatus_txt.setText(self.tr('Fehler aufgetreten'))
            self.recalcStatus_ico.setPixmap(
                QPixmap(os.path.join(ico_path, 'icon_yellow.png')))
            color = red
        elif status == 'saveDone':
            self.recalcStatus_txt.setText(self.tr('Ergebnisse gespeichert'))
            self.recalcStatus_ico.setPixmap(
                QPixmap(os.path.join(ico_path, 'icon_save.png')))
            color = green
        stylesheet = ''
        if color:
            stylesheet = f"background-color:{color};"
        self.recalcStatus_txt.setStyleSheet(stylesheet)

    def recalculate(self):
        if not self.configurationHasChanged or self.isRecalculating:
            return
        self.isRecalculating = True

        try:
            params = self.confHandler.params.getSimpleParameterDict()
            cableline, force, seil_possible = preciseCable(
                params, self.poles, self.confHandler.params.optSTA)
        except Exception as e:
            self.updateRecalcStatus('cableError')
            self.isRecalculating = False
            self.configurationHasChanged = False
            # TODO: Error handling when shape mismach
            # QMessageBox.critical(self, 'Unerwarteter Fehler bei Neuberechnung '
            #     'der Seillinie', str(e), QMessageBox.Ok)
            return

        self.cableline = cableline
        self.result['force'] = force

        # Ground clearance
        self.profile.updateProfileAnalysis(self.cableline)

        # Update Plot
        self.plot.updatePlot(self.poles.getAsArray(), self.cableline)

        # Update Threshold data
        self.thdUpdater.update(
            [
                self.cableline['groundclear_rel'],  # Distance cable - terrain
                self.result['force']['MaxSeilzugkraft']
                [0],  # Max force on cable
                self.result['force']['Sattelkraft_Total']
                [0],  # Max force on pole
                self.result['force']
                ['Lastseilknickwinkel'],  # Cable angle on pole
                self.result['force']['Leerseilknickwinkel']
            ],  # Cable angle on pole
            self.confHandler.params,
            self.poles,
            (self.status in ['jumpedOver', 'savedFile']))

        # cable line lifts off of pole
        if not seil_possible:
            self.updateRecalcStatus('liftsOff')
        else:
            self.updateRecalcStatus('cableSuccess')
        self.configurationHasChanged = False
        self.isRecalculating = False
        self.unsavedChanges = True

    def showThresholdInPlot(self, row=None):
        """This function is ether called by the Threshold updater when
         the cable has been recalculated or when user clicks on a table row."""

        # Click on row was emitted but row is already selected -> deselect
        if row is not None and row == self.selectedThdRow:
            # Remove markers from plot
            self.plot.removeMarkers()
            self.selectedThdRow = None
            return
        # There was no new selection but a redraw of the table was done, so
        #  current selection has to be added to the plot again
        if row is None:
            if self.selectedThdRow is not None:
                row = self.selectedThdRow
            # Nothing is selected at the moment
            else:
                return

        location = self.thdUpdater.rows[row][5]['loc']
        color = self.thdUpdater.rows[row][5]['color']
        plotLabels = self.thdUpdater.plotLabels[row]
        arrIdx = []
        # Get index of horizontal distance so we know which height value to
        #  chose
        for loc in location:
            arrIdx.append(np.argwhere(self.profile.di_disp == loc)[0][0])
        z = self.profile.zi_disp[arrIdx]

        self.plot.showMarkers(location, z, plotLabels, color)
        self.selectedThdRow = row

    def onClose(self):
        self.close()

    def onReturnToStart(self):
        self.readoutPrHeaderData()
        self.doReRun = True
        self.close()

    def onSave(self):
        self.saveDialog.doSave = False
        self.saveDialog.exec()
        if self.saveDialog.doSave:
            self.readoutPrHeaderData()
            self.confHandler.updateUserSettings()
            self.createOutput()
            self.unsavedChanges = False

    def createOutput(self):
        outputFolder = self.confHandler.getCurrentPath()
        project = self.confHandler.project
        projName = project.getProjectName()
        outputLoc = createOutputFolder(os.path.join(outputFolder, projName))
        updateWithCableCoordinates(self.cableline, project.points['A'],
                                   project.azimut)
        # Save project file
        self.confHandler.saveSettings(
            os.path.join(outputLoc,
                         self.tr('Projekteinstellungen') + '.json'))

        # Create short report
        if self.confHandler.getOutputOption('shortReport'):
            generateShortReport(self.confHandler, self.result, projName,
                                outputLoc)

        # Create technical report
        if self.confHandler.getOutputOption('report'):
            reportText = generateReportText(self.confHandler, self.result,
                                            projName)
            generateReport(reportText, outputLoc)

        # Create plot
        if self.confHandler.getOutputOption('plot'):
            plotSavePath = os.path.join(outputLoc, self.tr('Diagramm.pdf'))
            printPlot = AdjustmentPlot(self)
            printPlot.initData(self.profile.di_disp, self.profile.zi_disp,
                               self.profile.peakLoc_x, self.profile.peakLoc_z,
                               self.profile.surveyPnts)
            printPlot.updatePlot(self.poles.getAsArray(), self.cableline, True)
            printPlot.printToPdf(plotSavePath, projName, self.poles.poles)

        # Generate geo data
        if self.confHandler.getOutputOption('geodata') \
                or self.confHandler.getOutputOption('kml'):
            # Put geo data in separate sub folder
            savePath = os.path.join(outputLoc, 'geodata')
            os.makedirs(savePath)
            epsg = project.heightSource.spatialRef
            geodata = organizeDataForExport(self.poles.poles, self.cableline)

            if self.confHandler.getOutputOption('geodata'):
                shapeFiles = exportToShape(geodata, epsg, savePath)
                addToMap(shapeFiles, projName)
            if self.confHandler.getOutputOption('kml'):
                exportToKML(geodata, epsg, savePath)

        # Generate coordinate tables
        if self.confHandler.getOutputOption('coords'):
            generateCoordTable(self.cableline, self.profile, self.poles.poles,
                               outputLoc)

        self.updateRecalcStatus('saveDone')

    def closeEvent(self, event):
        if self.isRecalculating or self.configurationHasChanged:
            return
        if self.unsavedChanges:
            msgBox = QMessageBox()
            msgBox.setIcon(QMessageBox.Information)
            msgBox.setWindowTitle(self.tr('Nicht gespeicherte Aenderungen'))
            msgBox.setText(self.tr('Moechten Sie die Ergebnisse speichern?'))
            msgBox.setStandardButtons(QMessageBox.Cancel | QMessageBox.No
                                      | QMessageBox.Yes)
            cancelBtn = msgBox.button(QMessageBox.Cancel)
            cancelBtn.setText(self.tr("Abbrechen"))
            noBtn = msgBox.button(QMessageBox.No)
            noBtn.setText(self.tr("Nein"))
            yesBtn = msgBox.button(QMessageBox.Yes)
            yesBtn.setText(self.tr("Ja"))
            msgBox.show()
            msgBox.exec()

            if msgBox.clickedButton() == yesBtn:
                self.onSave()
                self.drawTool.reset()
            elif msgBox.clickedButton() == cancelBtn:
                event.ignore()
                return
            elif msgBox.clickedButton() == noBtn:
                self.drawTool.reset()
        else:
            self.drawTool.reset()

        self.timer.stop()
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()
Beispiel #31
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
Beispiel #32
0
class HuntRegister:
    """QGIS Plugin Implementation."""
    def __init__(self, iface):
        """Constructor.

        :param iface: An interface instance that will be passed to this class
            which provides the hook by which you can manipulate the QGIS
            application at run time.
        :type iface: QgsInterface
        """
        # Save reference to the QGIS interface
        self.iface = iface
        # initialize plugin directory
        self.plugin_dir = os.path.dirname(__file__)

        # Check if plugin was started the first time in current QGIS session
        # Must be set in initGui() to survive plugin reloads
        self.first_start = None

        # This plugin depends on alkisplugin
        self.alkisplugin = None

        # Members
        self.alkisToolBar = None  # the toolbar instance where the alkisplugin and huntplugin QAction symbols are placed

        self.alkisSelectAreaLayer = None  # QgsVectorLayer selected parcels of alkisplugin
        self.huntSelectAreaLayer = None  # QgsVectorLayer selected parcels of huntplugin

        # help web view
        self.helpiew = None  # webview containing manuals

        # All actions are assigned to alter self.huntSelectAreaLayer
        self.hswapAction = None  # single QAction copy selected parcels from alkisplugin to huntplugin
        self.hAddMarkAction = None  # checkable QAction select and unselect parcels
        self.hlistAction = None  # single QAction select parcels by parcel attributes
        self.hclearAction = None  # single QAction unselect all selected parcels
        self.hownerAction = None  # single QAction get parcel certificates for all selected parcels
        self.hhuntAction = None  # single QAction create a hunt register
        self.hinfoAction = None  # single QAction get a basic summary of all selected parcels
        self.helpAction = None  # single QAction open help files webview browser window
        # self.testAction = None                                                                # single QAction used for testing program fragments

        self.hAddMarkTool = None  # click recognizing map tool for self.hAddMarkAction
        self.core = None  # function core for this plugin
        self.initTimer = QTimer(
            self.iface
        )  # timer used to init self.huntSelectAreaLayer dynamically when alkis layers are added
        self.initTimer.setInterval(1000)  # 1 sec interval
        self.initTimer.timeout.connect(self.initLayers)
        self.init()  # init main instances

    def init(self):
        """init main instances"""

        if (alkisAvailable):
            try:
                self.alkisplugin = Alkis.alkisplugin.alkisplugin(
                    self.iface)  # create alkisplugin object
                self.alkisplugin.queryOwnerAction = QAction(
                    None
                )  # this is used in akisplugin "opendb" and must therefore be set to prevent runtime errors
            except AttributeError:
                QMessageBox.critical(
                    None, "Fehler",
                    "norGIS ALKIS-Einbindung zuerst aktivieren und \"JagdKataster\" erneut aktivieren"
                )
                raise AttributeError("alkisplugin not active")
        else:
            QMessageBox.critical(
                None, "Fehler",
                "norGIS ALKIS-Einbindung installieren und zuerst aktivieren. Dann \"JagdKataster\" erneut aktivieren"
            )
            raise AttributeError("alkisplugin not installed")

        self.core = HuntCore(self)  # function core for this plugin

    def initGui(self):
        """Create the menu entries and toolbar icons inside the QGIS GUI."""

        # will be set False in run()
        self.first_start = True

        self.helpview = QWebView()
        self.helpview.resize(1280, 850)
        self.helpview.setWindowTitle("JagdKataster Anleitungen")
        help_dir = os.path.join(self.plugin_dir, "help", "build", "html",
                                "index.html")
        self.helpview.load(QUrl.fromLocalFile(help_dir))

        # the toolbar entries of this plugin should be placed alongside the alkisplugin entries
        # therefore the alkisplugin toolbar is derived
        tBars = self.iface.mainWindow().findChildren(
            QToolBar)  # get all toolbars from main window
        self.alkisToolBar = next(
            (n
             for n in tBars if n.objectName() == "norGIS_ALKIS_Toolbar"), None
        )  # find the instance of alkisplugin toolbar by its static name

        if self.alkisToolBar is None:  # in case the toolbar is not yet loaded create the instance with its static name
            # create alkis toolbar in case it is not yet loaded
            self.alkisToolBar = self.iface.addToolBar(u"norGIS: ALKIS")
            self.alkisToolBar.setObjectName("norGIS_ALKIS_Toolbar")

        #create toolbar items
        self.hswapAction = QAction(QIcon("hunt:mark_transfer.svg"),
                                   "Flächenmarkierung übertragen",
                                   self.iface.mainWindow())
        self.hswapAction.setWhatsThis(
            "Flächenmarkierung (gelb) nach Jagd-Flächenmarkierung (blau) übertragen"
        )
        self.hswapAction.setStatusTip(
            "Flächenmarkierung (gelb) nach Jagd-Flächenmarkierung (blau) übertragen"
        )
        self.hswapAction.triggered.connect(lambda: self.core.swapAlkisToHunt())
        self.alkisToolBar.addAction(self.hswapAction)

        self.hAddMarkAction = QAction(QIcon("hunt:mark_add.svg"),
                                      u"Flurstück (de)selektieren",
                                      self.iface.mainWindow())
        self.hAddMarkAction.setWhatsThis(
            "Flurstück in Jagd-Flächenmarkierung selektieren oder deselektieren"
        )
        self.hAddMarkAction.setStatusTip(
            "Flurstück in Jagd-Flächenmarkierung selektieren oder deselektieren"
        )
        self.hAddMarkAction.setCheckable(True)
        self.hAddMarkAction.triggered.connect(
            lambda: self.iface.mapCanvas().setMapTool(self.hAddMarkTool))
        self.alkisToolBar.addAction(self.hAddMarkAction)
        self.hAddMarkTool = HAdd(self)
        self.hAddMarkTool.setAction(self.hAddMarkAction)

        self.hlistAction = QAction(QIcon("hunt:mark_list.svg"),
                                   "Selektieren nach Flurstückseigenschaft",
                                   self.iface.mainWindow())
        self.hlistAction.setWhatsThis(
            "Selektierung der Flurstücke in Jagd-Flächenmarkierung anhand Flurstückseigenschaften"
        )
        self.hlistAction.setStatusTip(
            "Selektierung der Flurstücke in Jagd-Flächenmarkierung anhand Flurstückseigenschaften"
        )
        self.hlistAction.triggered.connect(
            lambda: self.core.showListSelection())
        self.alkisToolBar.addAction(self.hlistAction)

        self.hclearAction = QAction(QIcon("hunt:mark_clear.svg"),
                                    "Alle deselektieren",
                                    self.iface.mainWindow())
        self.hclearAction.setWhatsThis(
            "Alle Flurstücke in Jagd-Flächenmarkierung deselektieren")
        self.hclearAction.setStatusTip(
            "Alle Flurstücke in Jagd-Flächenmarkierung deselektieren")
        self.hclearAction.triggered.connect(lambda: self.core.clearHighlight())
        self.alkisToolBar.addAction(self.hclearAction)

        self.hownerAction = QAction(QIcon("hunt:mark_own.svg"),
                                    "Flurstücksnachweise anzeigen",
                                    self.iface.mainWindow())
        self.hownerAction.setWhatsThis(
            "Flurstücksnachweise für selektierte Flurstücke in Jagd-Flächenmarkierung anzeigen"
        )
        self.hownerAction.setStatusTip(
            "Flurstücksnachweise für selektierte Flurstücke in Jagd-Flächenmarkierung anzeigen"
        )
        self.hownerAction.triggered.connect(
            lambda: self.core.showParcelCerts())
        self.alkisToolBar.addAction(self.hownerAction)

        self.hhuntAction = QAction(QIcon("hunt:mark_hunt.svg"),
                                   "Jagdkataster erstellen",
                                   self.iface.mainWindow())
        self.hhuntAction.setWhatsThis(
            "Jagdkataster für selektierte Flurstücke in Jagd-Flächenmarkierung erstellen"
        )
        self.hhuntAction.setStatusTip(
            "Jagdkataster für selektierte Flurstücke in Jagd-Flächenmarkierung erstellen"
        )
        self.hhuntAction.triggered.connect(lambda: self.core.showHuntReg())
        self.alkisToolBar.addAction(self.hhuntAction)

        self.hinfoAction = QAction(QIcon("hunt:mark_info.svg"),
                                   "Flurstückszusammenfassung anzeigen",
                                   self.iface.mainWindow())
        self.hinfoAction.setWhatsThis(
            "Flurstückszusammenfassung für selektierte Flurstücke in Jagd-Flächenmarkierung anzeigen"
        )
        self.hinfoAction.setStatusTip(
            "Flurstückszusammenfassung für selektierte Flurstücke in Jagd-Flächenmarkierung anzeigen"
        )
        self.hinfoAction.triggered.connect(
            lambda: self.core.showSummaryDialog())
        self.alkisToolBar.addAction(self.hinfoAction)

        self.helpAction = QAction(QIcon("hunt:logo.svg"), "Anleitungen",
                                  self.iface.mainWindow())
        self.helpAction.setWhatsThis("JagdKataster-Anleitungen")
        self.helpAction.setStatusTip("JagdKataster-Anleitungen")
        self.helpAction.triggered.connect(lambda: self.helpview.show())

        # self.testAction = QAction(QIcon("hunt:test.svg"), "Test", self.iface.mainWindow())
        # self.testAction.setWhatsThis("Test action")
        # self.testAction.setStatusTip("Test action")
        # self.testAction.triggered.connect(lambda: self.core.test(self.huntSelectAreaLayer))
        # self.alkisToolBar.addAction(self.testAction)

        self.iface.addPluginToDatabaseMenu("&ALKIS", self.helpAction)

        QgsProject.instance().layersAdded.connect(
            self.initTimer.start
        )  # react to changes in the layer tree. Maybe the alkis layers were added
        QgsProject.instance().layersWillBeRemoved.connect(
            self.layersRemoved
        )  # remove entries in case this plugin layers are to be removed

    def initLayers(self):
        """init self.huntSelectAreaLayer in case the alkis layers from alkisplugin are loaded"""
        self.initTimer.stop(
        )  # this methode may be called by a timer started when layers are added => stop the timer after first timeout event
        if self.alkisSelectAreaLayer is None:  # are alkisplugin layers loaded ad readable from entry?
            (layerId,
             res) = QgsProject.instance().readEntry("alkis",
                                                    "/areaMarkerLayer")
            if res and layerId:
                self.alkisSelectAreaLayer = QgsProject.instance().mapLayer(
                    layerId)
        if self.huntSelectAreaLayer is None:  # is the huntplugin layer already loaded?
            (layerId,
             res) = QgsProject.instance().readEntry("hunt", "/areaMarkerLayer")
            if res and layerId:
                self.huntSelectAreaLayer = QgsProject.instance().mapLayer(
                    layerId)
        if self.huntSelectAreaLayer is None and self.alkisSelectAreaLayer is not None:  # alkisplugin layers are loaded but huntplugin layer is not
            self.createLayer()  # create huntplugin layer

    def layersRemoved(self, layersIds):
        """remove entries and references in case this plugin layers are to be removed"""
        if self.alkisSelectAreaLayer is not None and self.alkisSelectAreaLayer.id(
        ) in layersIds:
            self.alkisSelectAreaLayer = None
        if self.huntSelectAreaLayer is not None and self.huntSelectAreaLayer.id(
        ) in layersIds:
            QgsProject.instance().removeEntry("hunt", "/areaMarkerLayer")
            self.core.hlayer = None
            self.huntSelectAreaLayer = None

    def createLayer(self):
        """create and add huntplugin layer to the layer tree"""
        if (self.alkisSelectAreaLayer is not None):
            parent = QgsProject.instance().layerTreeRoot().findLayer(
                self.alkisSelectAreaLayer).parent()
            layeropts = QgsVectorLayer.LayerOptions(False, False)

            self.init(
            )  # reinit main instances because alkis instance conninfo might have changed
            (db, conninfo) = self.core.openDB()
            if db is None:
                return

            self.huntSelectAreaLayer = QgsVectorLayer(
                u"%s estimatedmetadata=true checkPrimaryKeyUnicity=0 key='ogc_fid' type=MULTIPOLYGON srid=%d table=%s.po_polygons (polygon) sql=false"
                % (conninfo, self.alkisplugin.epsg,
                   self.alkisplugin.quotedschema()), u"Jagd-Flächenmarkierung",
                "postgres", layeropts)

            sym = QgsSymbol.defaultSymbol(QgsWkbTypes.PolygonGeometry)
            sym.setColor(Qt.blue)
            sym.setOpacity(0.3)

            self.huntSelectAreaLayer.setRenderer(QgsSingleSymbolRenderer(sym))
            QgsProject.instance().addMapLayer(self.huntSelectAreaLayer, False)
            parent.insertLayer(0, self.huntSelectAreaLayer)

            self.core.hlayer = None
            QgsProject.instance().writeEntry("hunt", "/areaMarkerLayer",
                                             self.huntSelectAreaLayer.id())

    def unload(self):
        """Removes the plugin menu item and icon from QGIS GUI."""
        if self.hswapAction:
            self.hswapAction.deleteLater()
            self.hswapAction = None
        if self.hAddMarkAction:
            self.hAddMarkAction.deleteLater()
            self.hAddMarkAction = None
        if self.hlistAction:
            self.hlistAction.deleteLater()
            self.hlistAction = None
        if self.hclearAction:
            self.hclearAction.deleteLater()
            self.hclearAction = None
        if self.hownerAction:
            self.hownerAction.deleteLater()
            self.hownerAction = None
        if self.hhuntAction:
            self.hhuntAction.deleteLater()
            self.hhuntAction = None
        if self.hinfoAction:
            self.hinfoAction.deleteLater()
            self.hinfoAction = None
        if self.helpAction:
            self.helpAction.deleteLater()
            self.helpAction = None
        # if self.testAction:
        #     self.testAction.deleteLater()
        #     self.testAction = None

        QgsProject.instance().layersAdded.disconnect(self.initTimer.start)
        QgsProject.instance().layersWillBeRemoved.disconnect(
            self.layersRemoved)

    def run(self):
        """Run method"""
        if self.first_start:
            self.first_start = False
class GwDimensioning:
    def __init__(self):

        self.iface = global_vars.iface
        self.settings = global_vars.giswater_settings
        self.plugin_dir = global_vars.plugin_dir
        self.canvas = global_vars.canvas
        self.points = None
        self.snapper_manager = GwSnapManager(self.iface)
        self.vertex_marker = self.snapper_manager.vertex_marker
        self.vertex_marker.setIconType(QgsVertexMarker.ICON_CIRCLE)

    def open_dimensioning_form(self,
                               qgis_feature=None,
                               layer=None,
                               db_return=None,
                               fid=None,
                               rubber_band=None):

        self.dlg_dim = GwDimensioningUi()
        tools_gw.load_settings(self.dlg_dim)

        self.user_current_layer = self.iface.activeLayer()
        # Set layers dimensions, node and connec
        self.layer_dimensions = tools_qgis.get_layer_by_tablename(
            "v_edit_dimensions")
        self.layer_node = tools_qgis.get_layer_by_tablename("v_edit_node")
        self.layer_connec = tools_qgis.get_layer_by_tablename("v_edit_connec")

        feature = None
        if qgis_feature is None:
            features = self.layer_dimensions.getFeatures()
            for feature in features:
                if feature['id'] == fid:
                    return feature
            qgis_feature = feature

        # qgis_feature = self.get_feature_by_id(self.layer_dimensions, fid, 'id')

        # when funcion is called from new feature
        if db_return is None:
            rubber_band = tools_gw.create_rubberband(self.canvas, 0)
            extras = f'"coordinates":{{{self.points}}}'
            body = tools_gw.create_body(extras=extras)
            json_result = tools_gw.execute_procedure('gw_fct_getdimensioning',
                                                     body)
            if json_result is None or json_result['status'] == 'Failed':
                return False
            db_return = json_result

        # get id from db response
        self.fid = db_return['body']['feature']['id']

        # ACTION SIGNALS
        action_snapping = self.dlg_dim.findChild(QAction, "actionSnapping")
        action_snapping.triggered.connect(
            partial(self._snapping, action_snapping))
        tools_gw.add_icon(action_snapping, "103")

        action_orientation = self.dlg_dim.findChild(QAction,
                                                    "actionOrientation")
        action_orientation.triggered.connect(
            partial(self._orientation, action_orientation))
        tools_gw.add_icon(action_orientation, "133")

        # LAYER SIGNALS
        tools_gw.connect_signal(
            self.layer_dimensions.editingStarted,
            partial(tools_gw.enable_all, self.dlg_dim,
                    db_return['body']['data']), 'dimensioning',
            'open_dimensioning_form_layer_dimensions_editingStarted_enable_all'
        )
        tools_gw.connect_signal(
            self.layer_dimensions.editingStopped,
            partial(tools_gw.enable_widgets, self.dlg_dim,
                    db_return['body']['data'], False), 'dimensioning',
            'open_dimensioning_form_layer_dimensions_editingStopped_enable_widgets'
        )

        # WIDGETS SIGNALS
        self.dlg_dim.btn_accept.clicked.connect(
            partial(self._save_dimensioning, qgis_feature, layer))
        self.dlg_dim.btn_cancel.clicked.connect(
            partial(self._cancel_dimensioning, action_snapping,
                    action_orientation))
        self.dlg_dim.key_escape.connect(
            partial(tools_gw.close_dialog, self.dlg_dim))
        self.dlg_dim.dlg_closed.connect(
            partial(self._cancel_dimensioning, action_snapping,
                    action_orientation))
        self.dlg_dim.dlg_closed.connect(
            partial(tools_gw.save_settings, self.dlg_dim))
        self.dlg_dim.dlg_closed.connect(rubber_band.reset)
        self.dlg_dim.dlg_closed.connect(self.layer_node.removeSelection)
        self.dlg_dim.dlg_closed.connect(self.layer_connec.removeSelection)

        self._create_map_tips()

        layout_list = []
        for field in db_return['body']['data']['fields']:
            if 'hidden' in field and field['hidden']:
                continue

            label, widget = self._set_widgets(self.dlg_dim, db_return, field)

            if widget.objectName() == 'id':
                tools_qt.set_widget_text(self.dlg_dim, widget, self.fid)
            layout = self.dlg_dim.findChild(QGridLayout, field['layoutname'])

            # Profilactic issue to prevent missed layouts againts db response and form
            if layout is not None:
                # Take the QGridLayout with the intention of adding a QSpacerItem later
                if layout not in layout_list and layout.objectName() not in (
                        'lyt_top_1', 'lyt_bot_1', 'lyt_bot_2'):
                    layout_list.append(layout)
                    # Add widgets into layout
                    layout.addWidget(label, 0, field['layoutorder'])
                    layout.addWidget(widget, 1, field['layoutorder'])

                # If field is on top or bottom layout the position is horitzontal no vertical
                if field['layoutname'] in ('lyt_top_1', 'lyt_bot_1',
                                           'lyt_bot_2'):
                    layout.addWidget(label, 0, field['layoutorder'])
                    layout.addWidget(widget, 1, field['layoutorder'])
                else:
                    tools_gw.add_widget(self.dlg_dim, field, label, widget)

        # Add a QSpacerItem into each QGridLayout of the list
        for layout in layout_list:
            vertical_spacer1 = QSpacerItem(20, 40, QSizePolicy.Minimum,
                                           QSizePolicy.Expanding)
            layout.addItem(vertical_spacer1)

        if self.layer_dimensions:
            self.iface.setActiveLayer(self.layer_dimensions)
            if self.layer_dimensions.isEditable():
                tools_gw.enable_all(self.dlg_dim, db_return['body']['data'])
            else:
                tools_gw.enable_widgets(self.dlg_dim,
                                        db_return['body']['data'], False)

        tools_qt.hide_void_groupbox(self.dlg_dim)

        title = f"DIMENSIONING - {self.fid}"
        tools_gw.open_dialog(self.dlg_dim,
                             dlg_name='dimensioning',
                             title=title)
        return False, False

    # region private functions

    def _cancel_dimensioning(self, action_snapping, action_orientation):

        self.iface.actionRollbackEdits().trigger()
        if action_snapping.isChecked():
            action_snapping.trigger()
        if action_orientation.isChecked():
            action_orientation.trigger()
        tools_qgis.restore_user_layer('v_edit_node', self.user_current_layer)
        tools_gw.disconnect_signal('dimensioning')
        tools_gw.close_dialog(self.dlg_dim)

    def _save_dimensioning(self, qgis_feature, layer):

        # Upsert feature into db
        layer.updateFeature(qgis_feature)
        layer.commitChanges()

        # Create body
        fields = ''
        list_widgets = self.dlg_dim.findChildren(QLineEdit)
        for widget in list_widgets:
            widget_name = widget.property('columnname')
            widget_value = tools_qt.get_text(self.dlg_dim, widget)
            if widget_value == 'null':
                continue
            fields += f'"{widget_name}":"{widget_value}", '

        list_widgets = self.dlg_dim.findChildren(QCheckBox)
        for widget in list_widgets:
            widget_name = widget.property('columnname')
            widget_value = f'"{tools_qt.is_checked(self.dlg_dim, widget)}"'
            if widget_value == 'null':
                continue
            fields += f'"{widget_name}":{widget_value},'

        list_widgets = self.dlg_dim.findChildren(QComboBox)
        for widget in list_widgets:
            widget_name = widget.property('columnname')
            widget_value = f'"{tools_qt.get_combo_value(self.dlg_dim, widget)}"'
            if widget_value == 'null':
                continue
            fields += f'"{widget_name}":{widget_value},'

        # remove last character (,) from fields
        fields = fields[:-1]

        feature = '"tableName":"v_edit_dimensions", '
        feature += f'"id":"{self.fid}"'
        extras = f'"fields":{{{fields}}}'
        body = tools_gw.create_body(feature=feature, extras=extras)
        tools_gw.execute_procedure('gw_fct_setdimensioning', body)

        # Close dialog
        tools_gw.close_dialog(self.dlg_dim)

    def _deactivate_signals(self, action, emit_point=None):

        self.vertex_marker.hide()
        try:
            self.canvas.xyCoordinates.disconnect()
        except TypeError:
            pass

        try:
            emit_point.canvasClicked.disconnect()
        except TypeError:
            pass

        if not action.isChecked():
            action.setChecked(False)
            return True

        return False

    def _snapping(self, action):

        # Set active layer and set signals
        tools_gw.disconnect_signal(
            'dimensioning', 'snapping_ep_canvasClicked_click_button_snapping')
        tools_gw.disconnect_signal('dimensioning',
                                   'snapping_xyCoordinates_mouse_move')
        emit_point = QgsMapToolEmitPoint(self.canvas)
        self.canvas.setMapTool(emit_point)
        if self._deactivate_signals(action, emit_point):
            return

        self.snapper_manager.remove_marker(self.vertex_marker)
        self.previous_snapping = self.snapper_manager.get_snapping_options()
        self.snapper_manager.set_snapping_status()
        self.snapper_manager.set_snapping_layers()

        self.snapper_manager.config_snap_to_node()
        self.snapper_manager.config_snap_to_connec()
        self.snapper_manager.config_snap_to_gully()
        self.snapper_manager.set_snap_mode()

        self.dlg_dim.actionOrientation.setChecked(False)
        self.iface.setActiveLayer(self.layer_node)
        tools_gw.connect_signal(self.canvas.xyCoordinates, self._mouse_move,
                                'dimensioning',
                                'snapping_xyCoordinates_mouse_move')
        tools_gw.connect_signal(
            emit_point.canvasClicked,
            partial(self._click_button_snapping, action, emit_point),
            'dimensioning', 'snapping_ep_canvasClicked_click_button_snapping')

    def _mouse_move(self, point):

        # Hide marker and get coordinates
        self.vertex_marker.hide()
        event_point = self.snapper_manager.get_event_point(point=point)

        # Snapping
        result = self.snapper_manager.snap_to_project_config_layers(
            event_point)
        if result.isValid():
            layer = self.snapper_manager.get_snapped_layer(result)
            # Check feature
            if layer == self.layer_node or layer == self.layer_connec:
                self.snapper_manager.add_marker(result, self.vertex_marker)

    def _click_button_snapping(self, action, emit_point, point, btn):

        if not self.layer_dimensions:
            return

        if btn == Qt.RightButton:
            if btn == Qt.RightButton:
                action.setChecked(False)
                tools_gw.disconnect_signal(
                    'dimensioning',
                    'snapping_ep_canvasClicked_click_button_snapping')
                tools_gw.disconnect_signal(
                    'dimensioning', 'snapping_xyCoordinates_mouse_move')
                self._deactivate_signals(action, emit_point)
                return

        layer = self.layer_dimensions
        self.iface.setActiveLayer(layer)

        # Get coordinates
        event_point = self.snapper_manager.get_event_point(point=point)

        # Snapping
        result = self.snapper_manager.snap_to_project_config_layers(
            event_point)
        if result.isValid():

            layer = self.snapper_manager.get_snapped_layer(result)
            # Check feature
            if layer == self.layer_node:
                feat_type = 'node'
            elif layer == self.layer_connec:
                feat_type = 'connec'
            else:
                return

            # Get the point
            snapped_feat = self.snapper_manager.get_snapped_feature(result)
            feature_id = self.snapper_manager.get_snapped_feature_id(result)
            element_id = snapped_feat.attribute(feat_type + '_id')

            # Leave selection
            layer.select([feature_id])

            # Get depth of the feature
            fieldname = None
            self.project_type = tools_gw.get_project_type()
            if self.project_type == 'ws':
                fieldname = "depth"
            elif self.project_type == 'ud' and feat_type == 'node':
                fieldname = "ymax"
            elif self.project_type == 'ud' and feat_type == 'connec':
                fieldname = "connec_depth"

            if fieldname is None:
                return

            depth = snapped_feat.attribute(fieldname)
            if depth in ('', None, 0, '0', 'NULL'):
                tools_qt.set_widget_text(self.dlg_dim, "depth", None)
            else:
                tools_qt.set_widget_text(self.dlg_dim, "depth", depth)
            tools_qt.set_widget_text(self.dlg_dim, "feature_id", element_id)
            tools_qt.set_widget_text(self.dlg_dim, "feature_type",
                                     feat_type.upper())

            self.snapper_manager.restore_snap_options(self.previous_snapping)
            tools_gw.disconnect_signal(
                'dimensioning',
                'snapping_ep_canvasClicked_click_button_snapping')
            tools_gw.disconnect_signal('dimensioning',
                                       'snapping_xyCoordinates_mouse_move')
            self._deactivate_signals(action, emit_point)
            action.setChecked(False)

    def _orientation(self, action):

        tools_gw.disconnect_signal(
            'dimensioning',
            'orientation_ep_canvasClicked_click_button_orientation')
        tools_gw.disconnect_signal(
            'dimensioning', 'orientation_xyCoordinates_canvas_move_event')
        emit_point = QgsMapToolEmitPoint(self.canvas)
        self.canvas.setMapTool(emit_point)
        if self._deactivate_signals(action, emit_point):
            return

        self.snapper_manager.remove_marker(self.vertex_marker)
        self.previous_snapping = self.snapper_manager.get_snapping_options()
        self.snapper_manager.set_snapping_status()
        self.snapper_manager.set_snapping_layers()

        self.snapper_manager.config_snap_to_node()
        self.snapper_manager.config_snap_to_connec()
        self.snapper_manager.config_snap_to_gully()
        self.snapper_manager.set_snap_mode()

        self.dlg_dim.actionSnapping.setChecked(False)
        tools_gw.connect_signal(self.canvas.xyCoordinates,
                                self._canvas_move_event, 'dimensioning',
                                'orientation_xyCoordinates_canvas_move_event')
        tools_gw.connect_signal(
            emit_point.canvasClicked,
            partial(self._click_button_orientation, action,
                    emit_point), 'dimensioning',
            'orientation_ep_canvasClicked_click_button_orientation')

    def _canvas_move_event(self, point):

        # Get clicked point
        self.vertex_marker.hide()
        event_point = self.snapper_manager.get_event_point(point=point)
        result = self.snapper_manager.snap_to_project_config_layers(
            event_point)
        if self.snapper_manager.result_is_valid():
            self.snapper_manager.add_marker(result, self.vertex_marker)

    def _click_button_orientation(self, action, emit_point, point, btn):

        if not self.layer_dimensions:
            return

        if btn == Qt.RightButton:
            action.setChecked(False)
            tools_gw.disconnect_signal(
                'dimensioning',
                'orientation_ep_canvasClicked_click_button_orientation')
            tools_gw.disconnect_signal(
                'dimensioning', 'orientation_xyCoordinates_canvas_move_event')
            self._deactivate_signals(action, emit_point)
            return

        self.x_symbol = self.dlg_dim.findChild(QLineEdit, "x_symbol")
        self.x_symbol.setText(str(int(point.x())))
        self.y_symbol = self.dlg_dim.findChild(QLineEdit, "y_symbol")
        self.y_symbol.setText(str(int(point.y())))

        self.snapper_manager.restore_snap_options(self.previous_snapping)
        tools_gw.disconnect_signal(
            'dimensioning',
            'orientation_ep_canvasClicked_click_button_orientation')
        tools_gw.disconnect_signal(
            'dimensioning', 'orientation_xyCoordinates_canvas_move_event')
        self._deactivate_signals(action, emit_point)
        action.setChecked(False)

    def _create_map_tips(self):
        """ Create MapTips on the map """

        row = tools_gw.get_config_value('qgis_dim_tooltip')
        if not row or row[0].lower() != 'true':
            return

        tools_gw.disconnect_signal(
            'dimensioning',
            'create_map_tips_timer_map_tips_timeout_show_map_tip')
        self.timer_map_tips = QTimer(self.canvas)
        self.map_tip_node = QgsMapTip()
        self.map_tip_connec = QgsMapTip()

        tools_gw.connect_signal(
            self.canvas.xyCoordinates, self._map_tip_changed, 'dimensioning',
            'create_map_tips_xyCoordinates_map_tip_changed')
        tools_gw.connect_signal(
            self.timer_map_tips.timeout, self._show_map_tip, 'dimensioning',
            'create_map_tips_timer_map_tips_timeout_show_map_tip')

        tools_gw.disconnect_signal(
            'dimensioning',
            'create_map_tips_timer_map_tips_clear_timeout_clear_map_tip')
        self.timer_map_tips_clear = QTimer(self.canvas)
        tools_gw.connect_signal(
            self.timer_map_tips_clear.timeout, self._clear_map_tip,
            'dimensioning',
            'create_map_tips_timer_map_tips_clear_timeout_clear_map_tip')

    def _map_tip_changed(self, point):
        """ SLOT. Initialize the Timer to show MapTips on the map """

        if self.canvas.underMouse():
            self.last_map_position = QgsPointXY(point.x(), point.y())
            self.map_tip_node.clear(self.canvas)
            self.map_tip_connec.clear(self.canvas)
            self.timer_map_tips.start(100)

    def _show_map_tip(self):
        """ Show MapTips on the map """

        self.timer_map_tips.stop()
        if self.canvas.underMouse():
            point_qgs = self.last_map_position
            point_qt = self.canvas.mouseLastXY()
            if self.layer_node:
                self.map_tip_node.showMapTip(self.layer_node, point_qgs,
                                             point_qt, self.canvas)
            if self.layer_connec:
                self.map_tip_connec.showMapTip(self.layer_connec, point_qgs,
                                               point_qt, self.canvas)
            self.timer_map_tips_clear.start(1000)

    def _clear_map_tip(self):
        """ Clear MapTips """

        self.timer_map_tips_clear.stop()
        self.map_tip_node.clear(self.canvas)
        self.map_tip_connec.clear(self.canvas)

    def _set_widgets(self, dialog, db_return, field):

        widget = None
        label = None
        if field['label']:
            label = QLabel()
            label.setObjectName('lbl_' + field['widgetname'])
            label.setText(field['label'].capitalize())
            if 'stylesheet' in field and field[
                    'stylesheet'] is not None and 'label' in field[
                        'stylesheet']:
                label = tools_gw.set_stylesheet(field, label)
            if 'tooltip' in field:
                label.setToolTip(field['tooltip'])
            else:
                label.setToolTip(field['label'].capitalize())
        if field['widgettype'] == 'text' or field['widgettype'] == 'typeahead':
            completer = QCompleter()
            widget = tools_gw.add_lineedit(field)
            widget = tools_gw.set_widget_size(widget, field)
            widget = tools_gw.set_data_type(field, widget)
            if field['widgettype'] == 'typeahead':
                widget = tools_gw.set_typeahead(field, dialog, widget,
                                                completer)
        elif field['widgettype'] == 'combo':
            widget = tools_gw.add_combo(field)
            widget = tools_gw.set_widget_size(widget, field)
        elif field['widgettype'] == 'check':
            widget = tools_gw.add_checkbox(field)
        elif field['widgettype'] == 'datetime':
            widget = tools_gw.add_calendar(dialog, field)
        elif field['widgettype'] == 'button':
            widget = tools_gw.add_button(dialog, field)
            widget = tools_gw.set_widget_size(widget, field)
        elif field['widgettype'] == 'hyperlink':
            widget = tools_gw.add_hyperlink(field)
            widget = tools_gw.set_widget_size(widget, field)
        elif field['widgettype'] == 'hspacer':
            widget = tools_qt.add_horizontal_spacer()
        elif field['widgettype'] == 'vspacer':
            widget = tools_qt.add_verticalspacer()
        elif field['widgettype'] == 'textarea':
            widget = tools_gw.add_textarea(field)
        elif field['widgettype'] in 'spinbox':
            widget = tools_gw.add_spinbox(field)
        elif field['widgettype'] == 'tableview':
            widget = tools_gw.add_tableview(db_return, field)
            widget = tools_gw.add_tableview_header(widget, field)
            widget = tools_gw.fill_tableview_rows(widget, field)
            widget = tools_gw.set_tablemodel_config(dialog,
                                                    widget,
                                                    field['widgetname'],
                                                    sort_order=1,
                                                    isQStandardItemModel=True)
            tools_qt.set_tableview_config(widget)
        widget.setObjectName(widget.property('columnname'))

        return label, widget
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()
Beispiel #35
0
class MobileItem(QObject):
    '''
    A Mobile Item that reveives its position from a dataprovider
    and is displayed on the canvas
    Could be everything liek vehicles or simple beacons
    '''

    mobileItemCount = 0

    newPosition = pyqtSignal(float, QgsPointXY, float, float)
    newAttitude = pyqtSignal(float, float, float)  # heading, pitch, roll
    timeout = pyqtSignal()

    def __init__(self, iface, params={}, parent=None):
        '''
        Constructor
        :param iface: An interface instance that will be passed to this class
            which provides the hook by which you can manipulate the QGIS
            application at run time.
        :type iface: QgsInterface
        :param params: A dictionary defining all the properties of the item
        :type params: dictionary
        :param parent: Parent object for the new item. Defaults None.
        :type parent: QObject
        '''
        super(MobileItem, self).__init__(parent)

        self.iface = iface
        self.canvas = iface.mapCanvas()
        MobileItem.mobileItemCount += 1
        self.name = params.setdefault(
            'Name', 'MobileItem_' + str(MobileItem.mobileItemCount))
        self.marker = PositionMarker(self.canvas, params)
        self.marker.setToolTip(self.name)
        self.dataProvider = params.get('provider', dict())
        self.messageFilter = dict()
        self.extData = dict()
        self.coordinates = None
        self.position = None
        self.heading = 0.0
        self.depth = 0.0
        self.altitude = 0.0
        self.lastFix = 0.0
        self.crsXform = QgsCoordinateTransform()
        self.crsXform.setSourceCrs(QgsCoordinateReferenceSystem('EPSG:4326'))
        self.onCrsChange()
        self.canvas.destinationCrsChanged.connect(self.onCrsChange)
        if hasattr(self.canvas, 'magnificationChanged'):
            self.canvas.magnificationChanged.connect(
                self.onMagnificationChanged)
        self.timer = QTimer(self)
        self.timer.timeout.connect(self.timeout)
        self.notifyCount = int(params.get('nofixNotify', 0))
        self.fadeOut = bool(params.get('fadeOut', False))
        if self.notifyCount or self.fadeOut:
            self.timer.timeout.connect(self.notifyTimeout)
        self.timeoutCount = 0
        self.timeoutTime = int(params.get('timeout', 3000))
        self.notifyDuration = int(params.get('NotifyDuration', 0))
        self.timedOut = False
        self.enabled = True

    def removeFromCanvas(self):
        '''
        Remove the item and its track from the canvas
        '''
        self.marker.removeFromCanvas()

    def properties(self):
        '''
        Return the items properties as dictionary
        :returns: Items properties
        :rtype: dict
        '''
        d = {
            'Name': self.name,
            'timeout': self.timeoutTime,
            'nofixNotify': self.notifyCount,
            'fadeOut': self.fadeOut,
            'enabled': self.enabled,
            'provider': self.dataProvider
        }
        d.update(self.marker.properties())
        return d

    def subscribePositionProvider(self, provider, filterId=None):
        '''
        Subscribe the provider for this item
        by connecting to the providers signals
        :param provider: Provider to connect to
        :type provider: DataProvider
        :param filterId: Filter Id for this item
        :type filterId:
        '''
        provider.newDataReceived.connect(self.processNewData)
        try:
            if filterId['id'] not in (None, 'None') or filterId['flags']:
                self.messageFilter[provider.name] = filterId
            elif provider.name in self.messageFilter:
                self.messageFilter.pop(provider.name, None)
        except (KeyError, TypeError):
            self.messageFilter.pop(provider.name, None)

    def unsubscribePositionProvider(self, provider):
        '''
        Unsubscribe provider by disconnecting the providers signals
        :param provider: Provider to diconnect from
        :type provider: DataProvider
        '''
        try:
            provider.newDataReceived.disconnect(self.processData)
            self.messageFilter.pop(provider.name, None)
        except KeyError:
            pass

    @pyqtSlot(dict)
    def processNewData(self, data):
        '''
        Process incoming data from the data provider
        :param data: Positon or attitude data
        :type data: dict
        '''
        if not self.enabled:
            return

        flags = list()
        try:
            pname = data['name']
            flags = self.messageFilter[pname]['flags']
            if not self.messageFilter[pname]['id'] in (None, 'None'):
                if not data['id'] in (self.messageFilter[pname]['id'],
                                      str(self.messageFilter[pname]['id'])):
                    return
        except Exception:
            pass

        self.extData.update(data)

        if '-pos' not in flags:
            if 'lat' in data and 'lon' in data:
                if self.fadeOut and self.timedOut:
                    self.marker.setVisible(True)
                    self.timedOut = False
                self.position = QgsPointXY(data['lon'], data['lat'])
                self.heading = data.get('heading', -9999.9)
                self.depth = data.get('depth', -9999.9)
                self.altitude = data.get('altitude', -9999.9)
                try:
                    self.coordinates = self.crsXform.transform(self.position)
                    self.marker.setMapPosition(self.coordinates)
                    if 'time' in data:
                        self.lastFix = data['time']
                        self.newPosition.emit(
                            self.lastFix, self.position,
                            self.extData.get('depth', -9999.9),
                            self.extData.get('altitude', -9999.9))
                        self.timer.start(self.timeoutTime)
                        self.timeoutCount = 0
                except QgsCsException:
                    pass

            elif self.position is not None:
                if 'depth' in data or 'altitude' in data:
                    self.newPosition.emit(
                        self.lastFix, self.position,
                        self.extData.get('depth', -9999.9),
                        self.extData.get('altitude', -9999.9))

        if 'heading' in data and '-head' not in flags:
            self.newAttitude.emit(data['heading'], data.get('pitch', 0.0),
                                  data.get('roll', 0.0))
            self.marker.newHeading(data['heading'])
            self.heading = data['heading']
        elif 'course' in data and '+course' in flags:
            self.newAttitude.emit(data['course'], data.get('pitch', 0.0),
                                  data.get('roll', 0.0))
            self.marker.newHeading(data['course'])
            self.heading = data['course']

    @pyqtSlot(float)
    def onScaleChange(self, ):
        '''
        Slot called when the map is zoomed
        :param scale: New scale
        :type scale: float
        '''
        self.marker.updatePosition()

    @pyqtSlot()
    def onCrsChange(self):
        '''
        SLot called when the mapcanvas CRS is changed
        '''
        crsDst = self.canvas.mapSettings().destinationCrs()
        self.crsXform.setDestinationCrs(crsDst)
        self.marker.updatePosition()

    @pyqtSlot(float)
    def onMagnificationChanged(self, ):
        '''
        Slot called when the map magnification has changed
        :param scale: New scale
        :type scale: float
        '''
        self.marker.updateMapMagnification()

    @pyqtSlot(bool)
    def setEnabled(self, enabled):
        '''
        Hide or display the item and its track on the map
        :param enabled: what to do
        :type enabled: bool
        '''
        self.enabled = enabled
        self.marker.setVisible(self.enabled)
        self.marker.resetPosition()
        self.extData.clear()
        if self.enabled:
            self.timer.start(self.timeoutTime)
            self.timeoutCount = 0
        else:
            self.timer.stop()

    @pyqtSlot()
    def deleteTrack(self):
        '''
        Delete the track all points
        '''
        self.marker.deleteTrack()

    @pyqtSlot()
    def centerOnMap(self):
        '''
        Center the item on the map
        '''
        if self.coordinates is not None:
            self.canvas.setCenter(self.coordinates)
            self.canvas.refresh()

    def reportPosition(self):
        '''
        Report the position of the item. Used for logging
        :returns: geographic postion, depth and altitude
        :rtype: float, float, float, float, float
        '''
        if self.position is None:
            return -9999.9, -9999.9, -9999.9, 0.0, -9999.9
        return self.position.y(), self.position.x(
        ), self.depth, self.heading, self.altitude

    @pyqtSlot()
    def notifyTimeout(self):
        if self.fadeOut and not self.timedOut:
            self.marker.setVisible(False)
            self.timedOut = True
        if self.notifyCount:
            self.timeoutCount += 1
            if self.timeoutCount == self.notifyCount:
                msg = self.tr(u'No fix for %s since more than %d seconds!') % (
                    self.name, self.timeoutTime * self.timeoutCount / 1000)
                w = self.iface.messageBar().createMessage(
                    self.tr(u'PosiView Attention'), msg)
                label = QLabel(w)
                m = QMovie(':/plugins/PosiView/hand.gif')
                m.setSpeed(75)
                label.setMovie(m)
                m.setParent(label)
                m.start()
                w.layout().addWidget(label)
                self.iface.messageBar().pushWidget(
                    w, level=Qgis.Critical, duration=self.notifyDuration)

    def getTrack(self):
        tr = [e[1] for e in self.marker.track]
        return tr

    def applyTrack(self, track):
        self.marker.setTrack(track)