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()
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
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()
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
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
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
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()
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()
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()
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
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()
class AutoSuggest(QObject): def __init__(self, geturl_func, parseresult_func, parent=None): QObject.__init__(self, parent) self.geturl_func = geturl_func self.parseresult_func = parseresult_func self.editor = parent self.networkManager = QNetworkAccessManager() self.selectedObject = None self.isUnloaded = False self.popup = QTreeWidget(parent) #self.popup.setColumnCount(2) self.popup.setColumnCount(1) self.popup.setUniformRowHeights(True) self.popup.setRootIsDecorated(False) self.popup.setEditTriggers(QTreeWidget.NoEditTriggers) self.popup.setSelectionBehavior(QTreeWidget.SelectRows) self.popup.setFrameStyle(QFrame.Box | QFrame.Plain) self.popup.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.popup.header().hide() self.popup.installEventFilter(self) self.popup.setMouseTracking(True) #self.connect(self.popup, SIGNAL("itemClicked(QTreeWidgetItem*, int)"), # self.doneCompletion) self.popup.itemClicked.connect(self.doneCompletion) self.popup.setWindowFlags(Qt.Popup) self.popup.setFocusPolicy(Qt.NoFocus) self.popup.setFocusProxy(parent) self.timer = QTimer(self) self.timer.setSingleShot(True) self.timer.setInterval(500) #self.connect(self.timer, SIGNAL("timeout()"), self.autoSuggest) self.timer.timeout.connect(self.autoSuggest) #self.connect(self.editor, SIGNAL("textEdited(QString)"), self.timer, SLOT("start()")) #self.editor.textEdited.connect( self.timer.start ) self.editor.textEdited.connect(self.timer.start) #self.editor.textChanged.connect( self.timer.start ) #self.connect(self.networkManager, SIGNAL("finished(QNetworkReply*)"), # self.handleNetworkData) self.networkManager.finished.connect(self.handleNetworkData) def eventFilter(self, obj, ev): if obj != self.popup: return False if ev.type() == QEvent.MouseButtonPress: self.popup.hide() self.editor.setFocus() return True if ev.type() == QEvent.KeyPress: consumed = False key = ev.key() if key == Qt.Key_Enter or key == Qt.Key_Return: self.doneCompletion() consumed = True elif key == Qt.Key_Escape: self.editor.setFocus() self.popup.hide() consumed = True elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_Home, Qt.Key_End, Qt.Key_PageUp, Qt.Key_PageDown): pass else: self.editor.setFocus() self.editor.event(ev) self.popup.hide() return consumed return False def showCompletion(self, rows): # Rows is an iterable of tuples like [("text",object1),("text2", object2),...] pal = self.editor.palette() color = pal.color(QPalette.Disabled, QPalette.WindowText) self.popup.setUpdatesEnabled(False) self.popup.clear() if rows is None or len(rows) < 1: return for row in rows: item = QTreeWidgetItem(self.popup) item.setText(0, row[0]) #item.setText(1, hit['type']) item.setTextAlignment(1, Qt.AlignRight) item.setForeground(1, color) item.setData( 2, Qt.UserRole, (row[1], ) ) # Try immutable py obj #http://stackoverflow.com/questions/9257422/how-to-get-the-original-python-data-from-qvariant self.popup.setCurrentItem(self.popup.topLevelItem(0)) self.popup.resizeColumnToContents(0) #self.popup.resizeColumnToContents(1) self.popup.adjustSize() self.popup.setUpdatesEnabled(True) h = self.popup.sizeHintForRow(0) * min(15, len(rows)) + 3 w = max(self.popup.width(), self.editor.width()) self.popup.resize(w, h) self.popup.move( self.editor.mapToGlobal(QPoint(0, self.editor.height()))) self.popup.setFocus() self.popup.show() def doneCompletion(self): self.timer.stop() self.popup.hide() self.editor.setFocus() item = self.popup.currentItem() if item: self.editor.setText(item.text(0)) obj = item.data(2, Qt.UserRole) #.toPyObject() self.selectedObject = obj[0] e = QKeyEvent(QEvent.KeyPress, Qt.Key_Enter, Qt.NoModifier) QApplication.postEvent(self.editor, e) e = QKeyEvent(QEvent.KeyRelease, Qt.Key_Enter, Qt.NoModifier) QApplication.postEvent(self.editor, e) def preventSuggest(self): self.timer.stop() def autoSuggest(self): term = self.editor.text() if term: qurl = self.geturl_func(term) if qurl: # TODO: Cancel existing requests: http://qt-project.org/forums/viewthread/18073 self.networkManager.get(QNetworkRequest(qurl)) #QUrl(url))) def handleNetworkData(self, networkReply): url = networkReply.url() #print "received url:", url.toString() if not networkReply.error(): response = networkReply.readAll() pystring = str(response, 'utf-8') #print "Response: ", response rows = self.parseresult_func(pystring) self.showCompletion(rows) networkReply.deleteLater() def unload(self): # Avoid processing events after QGIS shutdown has begun self.popup.removeEventFilter(self) self.isUnloaded = True
class 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 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")
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
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)
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)
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()
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
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()
class AutoSuggest(QObject): def __init__(self, geturl_func, parseresult_func, parent = None): QObject.__init__(self, parent) self.geturl_func = geturl_func self.parseresult_func = parseresult_func self.editor = parent self.networkManager = QNetworkAccessManager() self.selectedObject = None self.isUnloaded = False self.popup = QTreeWidget(parent) #self.popup.setColumnCount(2) self.popup.setColumnCount(1) self.popup.setUniformRowHeights(True) self.popup.setRootIsDecorated(False) self.popup.setEditTriggers(QTreeWidget.NoEditTriggers) self.popup.setSelectionBehavior(QTreeWidget.SelectRows) self.popup.setFrameStyle(QFrame.Box | QFrame.Plain) self.popup.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) self.popup.header().hide() self.popup.installEventFilter(self) self.popup.setMouseTracking(True) #self.connect(self.popup, SIGNAL("itemClicked(QTreeWidgetItem*, int)"), # self.doneCompletion) self.popup.itemClicked.connect( self.doneCompletion ) self.popup.setWindowFlags(Qt.Popup) self.popup.setFocusPolicy(Qt.NoFocus) self.popup.setFocusProxy(parent) self.timer = QTimer(self) self.timer.setSingleShot(True) self.timer.setInterval(500) #self.connect(self.timer, SIGNAL("timeout()"), self.autoSuggest) self.timer.timeout.connect( self.autoSuggest ) #self.connect(self.editor, SIGNAL("textEdited(QString)"), self.timer, SLOT("start()")) #self.editor.textEdited.connect( self.timer.start ) self.editor.textEdited.connect( self.timer.start ) #self.editor.textChanged.connect( self.timer.start ) #self.connect(self.networkManager, SIGNAL("finished(QNetworkReply*)"), # self.handleNetworkData) self.networkManager.finished.connect( self.handleNetworkData ) def eventFilter(self, obj, ev): if obj != self.popup: return False if ev.type() == QEvent.MouseButtonPress: self.popup.hide() self.editor.setFocus() return True if ev.type() == QEvent.KeyPress: consumed = False key = ev.key() if key == Qt.Key_Enter or key == Qt.Key_Return: self.doneCompletion() consumed = True elif key == Qt.Key_Escape: self.editor.setFocus() self.popup.hide() consumed = True elif key in (Qt.Key_Up, Qt.Key_Down, Qt.Key_Home, Qt.Key_End, Qt.Key_PageUp, Qt.Key_PageDown): pass else: self.editor.setFocus() self.editor.event(ev) self.popup.hide() return consumed return False def showCompletion(self, rows): # Rows is an iterable of tuples like [("text",object1),("text2", object2),...] pal = self.editor.palette() color = pal.color(QPalette.Disabled, QPalette.WindowText) self.popup.setUpdatesEnabled(False) self.popup.clear() if rows is None or len( rows ) < 1: return for row in rows: item = QTreeWidgetItem(self.popup) item.setText(0, row[0]) #item.setText(1, hit['type']) item.setTextAlignment(1, Qt.AlignRight) item.setForeground(1, color) item.setData(2, Qt.UserRole, (row[1],)) # Try immutable py obj #http://stackoverflow.com/questions/9257422/how-to-get-the-original-python-data-from-qvariant self.popup.setCurrentItem(self.popup.topLevelItem(0)) self.popup.resizeColumnToContents(0) #self.popup.resizeColumnToContents(1) self.popup.adjustSize() self.popup.setUpdatesEnabled(True) h = self.popup.sizeHintForRow(0) * min(15, len(rows)) + 3 w = max(self.popup.width(), self.editor.width()) self.popup.resize(w, h) self.popup.move(self.editor.mapToGlobal(QPoint(0, self.editor.height()))) self.popup.setFocus() self.popup.show() def doneCompletion(self): self.timer.stop() self.popup.hide() self.editor.setFocus() item = self.popup.currentItem() if item: self.editor.setText(item.text(0) ) obj = item.data(2, Qt.UserRole) #.toPyObject() self.selectedObject = obj[0] e = QKeyEvent(QEvent.KeyPress, Qt.Key_Enter, Qt.NoModifier) QApplication.postEvent(self.editor, e) e = QKeyEvent(QEvent.KeyRelease, Qt.Key_Enter, Qt.NoModifier) QApplication.postEvent(self.editor, e) def preventSuggest(self): self.timer.stop() def autoSuggest(self): term = self.editor.text() if term: qurl = self.geturl_func( term ) if qurl: # TODO: Cancel existing requests: http://qt-project.org/forums/viewthread/18073 self.networkManager.get(QNetworkRequest( qurl )) #QUrl(url))) def handleNetworkData(self, networkReply): url = networkReply.url() #print "received url:", url.toString() if not networkReply.error(): response = networkReply.readAll() pystring = str(response, 'utf-8') #print "Response: ", response rows = self.parseresult_func( pystring ) self.showCompletion( rows ) networkReply.deleteLater() def unload( self ): # Avoid processing events after QGIS shutdown has begun self.popup.removeEventFilter(self) self.isUnloaded = True
class 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()
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)