class CheckboxesPanel(QWidget): def __init__(self, options, multiple, columns=2, parent=None): super(CheckboxesPanel, self).__init__(parent) self._options = [] for i, option in enumerate(options): if isinstance(option, str): self._options.append((i, option)) else: self.options.append(option) self._multiple = multiple self._buttons = [] rows = len(options) / columns self._buttonGroup = QButtonGroup() self._buttonGroup.setExclusive(not multiple) layout = QGridLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setMargin(0) for i, (v, t) in enumerate(self._options): if multiple: button = QCheckBox(t) else: button = QRadioButton(t) self._buttons.append((v, button)) self._buttonGroup.addButton(button, i) layout.addWidget(button, i % rows, i / rows) layout.addItem(QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Minimum), 0, columns) self.setLayout(layout) if multiple: self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.showPopupMenu) def showPopupMenu(self): popup_menu = QMenu() select_all_action = QAction(self.tr('Select All'), popup_menu) select_all_action.triggered.connect(self.selectAll) clear_all_action = QAction(self.tr('Clear Selection'), popup_menu) clear_all_action.triggered.connect(self.deselectAll) popup_menu.addAction(select_all_action) popup_menu.addAction(clear_all_action) popup_menu.exec_(QCursor.pos()) def selectAll(self): for (v, button) in self._buttons: button.setChecked(True) def deselectAll(self): for (v, button) in self._buttons: button.setChecked(False) def value(self): if self._multiple: value = [] for (v, checkbox) in self._buttons: if checkbox.isChecked(): value.append(v) return value else: return self._options[self._buttonGroup.checkedId()][0] def setValue(self, value): if self._multiple: for (v, button) in self._buttons: if v in value: button.setChecked(True) else: for v, button in self._buttons: if v == value: button.setChecked(True)
class PetaBencanaDialog(QDialog, FORM_CLASS): """Downloader for PetaBencana data. .. versionadded: 3.3 """ def __init__(self, parent=None, iface=None): """Constructor for import dialog. .. versionadded: 3.3 :param parent: Optional widget to use as parent. :type parent: QWidget :param iface: An instance of QgisInterface. :type iface: QgisInterface """ QDialog.__init__(self, parent) self.parent = parent self.setupUi(self) title = self.tr('PetaBencana Downloader') self.setWindowTitle(title) icon = resources_path('img', 'icons', 'add-petabencana-layer.svg') self.setWindowIcon(QtGui.QIcon(icon)) self.iface = iface self.source = None self.radio_button_group = QButtonGroup() self.radio_button_group.addButton(self.radio_button_production) self.radio_button_group.addButton(self.radio_button_development) self.radio_button_group.setExclusive(True) self.radio_button_production.setChecked(True) self.populate_combo_box() developer_mode = setting('developer_mode', False, bool) if not developer_mode: self.radio_button_widget.hide() self.source_label.hide() self.output_group.adjustSize() # signals self.radio_button_production.clicked.connect(self.populate_combo_box) self.radio_button_development.clicked.connect(self.populate_combo_box) # Set up things for context help self.help_button = self.button_box.button( QtWidgets.QDialogButtonBox.Help) # Allow toggling the help button self.help_button.setCheckable(True) self.help_button.toggled.connect(self.help_toggled) self.main_stacked_widget.setCurrentIndex(1) # set up the validator for the file name prefix expression = QRegExp('^[A-Za-z0-9-_]*$') validator = QtGui.QRegExpValidator(expression, self.filename_prefix) self.filename_prefix.setValidator(validator) self.time_stamp = None self.restore_state() @pyqtSlot(bool) # prevents actions being handled twice def help_toggled(self, flag): """Show or hide the help tab in the stacked widget. .. versionadded: 3.3 :param flag: Flag indicating whether help should be shown or hidden. :type flag: bool """ if flag: self.help_button.setText(self.tr('Hide Help')) self.show_help() else: self.help_button.setText(self.tr('Show Help')) self.hide_help() def hide_help(self): """Hide the usage info from the user. .. versionadded:: 3.3 """ self.main_stacked_widget.setCurrentIndex(1) def show_help(self): """Show usage info to the user. .. versionadded: 3.3 """ # Read the header and footer html snippets self.main_stacked_widget.setCurrentIndex(0) header = html_header() footer = html_footer() string = header message = peta_bencana_help() string += message.to_html() string += footer self.help_web_view.setHtml(string) def restore_state(self): """Read last state of GUI from configuration file. .. versionadded: 3.3 """ settings = QSettings() try: last_path = settings.value('directory', type=str) except TypeError: last_path = '' self.output_directory.setText(last_path) def save_state(self): """Store current state of GUI to configuration file. .. versionadded: 3.3 """ settings = QSettings() settings.setValue('directory', self.output_directory.text()) @pyqtSlot() # prevents actions being handled twice def on_directory_button_clicked(self): """Show a dialog to choose directory. .. versionadded: 3.3 """ # noinspection PyCallByClass,PyTypeChecker self.output_directory.setText(QFileDialog.getExistingDirectory( self, self.tr('Select download directory'))) def accept(self): """Do PetaBencana download and display it in QGIS. .. versionadded: 3.3 """ self.save_state() try: self.require_directory() except CanceledImportDialogError: return QgsApplication.instance().setOverrideCursor( QtGui.QCursor(QtCore.Qt.WaitCursor) ) source = self.define_url() # save the file as json first name = 'jakarta_flood.json' output_directory = self.output_directory.text() output_prefix = self.filename_prefix.text() overwrite = self.overwrite_flag.isChecked() date_stamp_flag = self.include_date_flag.isChecked() output_base_file_path = self.get_output_base_path( output_directory, output_prefix, date_stamp_flag, name, overwrite) title = self.tr("Can't access API") try: self.download(source, output_base_file_path) # Open downloaded file as QgsMapLayer options = QgsVectorLayer.LayerOptions(False) layer = QgsVectorLayer( output_base_file_path, 'flood', 'ogr', options) except Exception as e: disable_busy_cursor() QMessageBox.critical(self, title, str(e)) return self.time_stamp = time.strftime('%d-%b-%Y %H:%M:%S') # Now save as shp name = 'jakarta_flood.shp' output_base_file_path = self.get_output_base_path( output_directory, output_prefix, date_stamp_flag, name, overwrite) QgsVectorFileWriter.writeAsVectorFormat( layer, output_base_file_path, 'CP1250', QgsCoordinateTransform(), 'ESRI Shapefile') # Get rid of the GeoJSON layer and rather use local shp del layer self.copy_style(output_base_file_path) self.copy_keywords(output_base_file_path) layer = self.add_flooded_field(output_base_file_path) # check if the layer has feature or not if layer.featureCount() <= 0: city = self.city_combo_box.currentText() message = self.tr( 'There are no floods data available on {city} ' 'at this time.').format(city=city) display_warning_message_box( self, self.tr('No data'), message) disable_busy_cursor() else: # add the layer to the map project = QgsProject.instance() project.addMapLayer(layer) disable_busy_cursor() self.done(QDialog.Accepted) def add_flooded_field(self, shapefile_path): """Create the layer from the local shp adding the flooded field. .. versionadded:: 3.3 Use this method to add a calculated field to a shapefile. The shapefile should have a field called 'count' containing the number of flood reports for the field. The field values will be set to 0 if the count field is < 1, otherwise it will be set to 1. :param shapefile_path: Path to the shapefile that will have the flooded field added. :type shapefile_path: basestring :return: A vector layer with the flooded field added. :rtype: QgsVectorLayer """ layer = QgsVectorLayer( shapefile_path, self.tr('Jakarta Floods'), 'ogr') # Add a calculated field indicating if a poly is flooded or not # from qgis.PyQt.QtCore import QVariant layer.startEditing() # Add field with integer from 0 to 4 which represents the flood # class. Its the same as 'state' field except that is being treated # as a string. # This is used for cartography flood_class_field = QgsField('floodclass', QVariant.Int) layer.addAttribute(flood_class_field) layer.commitChanges() layer.startEditing() flood_class_idx = layer.fields().lookupField('floodclass') flood_class_expression = QgsExpression('to_int(state)') context = QgsExpressionContext() context.setFields(layer.fields()) flood_class_expression.prepare(context) # Add field with boolean flag to say if the area is flooded # This is used by the impact function flooded_field = QgsField('flooded', QVariant.Int) layer.dataProvider().addAttributes([flooded_field]) layer.commitChanges() layer.startEditing() flooded_idx = layer.fields().lookupField('flooded') flood_flag_expression = QgsExpression('state > 0') flood_flag_expression.prepare(context) for feature in layer.getFeatures(): context.setFeature(feature) feature[flood_class_idx] = flood_class_expression.evaluate(context) feature[flooded_idx] = flood_flag_expression.evaluate(context) layer.updateFeature(feature) layer.commitChanges() return layer def copy_keywords(self, shapefile_path): """Copy keywords from the OSM resource directory to the output path. .. versionadded: 3.3 In addition to copying the template, tokens within the template will be replaced with new values for the date token and title token. :param shapefile_path: Path to the shapefile that will have the flooded field added. :type shapefile_path: basestring """ source_xml_path = resources_path('petabencana', 'flood-keywords.xml') output_xml_path = shapefile_path.replace('shp', 'xml') LOGGER.info('Copying xml to: %s' % output_xml_path) title_token = '[TITLE]' new_title = self.tr('Jakarta Floods - %s' % self.time_stamp) date_token = '[DATE]' new_date = self.time_stamp with open(source_xml_path) as source_file, \ open(output_xml_path, 'w') as output_file: for line in source_file: line = line.replace(date_token, new_date) line = line.replace(title_token, new_title) output_file.write(line) @staticmethod def copy_style(shapefile_path): """Copy style from the OSM resource directory to the output path. .. versionadded: 3.3 :param shapefile_path: Path to the shapefile that should get the path added. :type shapefile_path: basestring """ source_qml_path = resources_path('petabencana', 'flood-style.qml') output_qml_path = shapefile_path.replace('shp', 'qml') LOGGER.info('Copying qml to: %s' % output_qml_path) copy(source_qml_path, output_qml_path) def get_output_base_path( self, output_directory, output_prefix, with_date_stamp, feature_type, overwrite): """Get a full base name path to save the shapefile. TODO: This is cut & paste from OSM - refactor to have one method :param output_directory: The directory where to put results. :type output_directory: str :param output_prefix: The prefix to add for the shapefile. :type output_prefix: str :param with_date_stamp: Whether to add a datestamp in between the file prefix and the feature_type for the shapefile name. :type output_prefix: str :param feature_type: What kind of data will be downloaded. Will be used for the shapefile name. :type feature_type: str :param overwrite: Boolean to know if we can overwrite existing files. :type overwrite: bool :return: The base path. :rtype: str """ if with_date_stamp and self.time_stamp is not None: time_stamp = self.time_stamp.replace(' ', '-') time_stamp = time_stamp.replace(':', '-') time_stamp += '-' feature_type = time_stamp + feature_type path = os.path.join( output_directory, '%s%s' % (output_prefix, feature_type)) if overwrite: # If a shapefile exists, we must remove it (only the .shp) shp = '%s.shp' % path if os.path.isfile(shp): os.remove(shp) else: separator = '-' suffix = self.get_unique_file_path_suffix( '%s.shp' % path, separator) if suffix: path = os.path.join(output_directory, '%s%s%s%s' % ( output_prefix, feature_type, separator, suffix)) return path @staticmethod def get_unique_file_path_suffix(file_path, separator='-', i=0): """Return the minimum number to suffix the file to not overwrite one. Example : /tmp/a.txt exists. - With file_path='/tmp/b.txt' will return 0. - With file_path='/tmp/a.txt' will return 1 (/tmp/a-1.txt) TODO: This is cut & paste from OSM - refactor to have one method :param file_path: The file to check. :type file_path: str :param separator: The separator to add before the prefix. :type separator: str :param i: The minimum prefix to check. :type i: int :return: The minimum prefix you should add to not overwrite a file. :rtype: int """ basename = os.path.splitext(file_path) if i != 0: file_path_test = os.path.join( '%s%s%s%s' % (basename[0], separator, i, basename[1])) else: file_path_test = file_path if os.path.isfile(file_path_test): return PetaBencanaDialog.get_unique_file_path_suffix( file_path, separator, i + 1) else: return i def require_directory(self): """Ensure directory path entered in dialog exist. When the path does not exist, this function will ask the user if he want to create it or not. TODO: This is cut & paste from OSM - refactor to have one method :raises: CanceledImportDialogError - when user choose 'No' in the question dialog for creating directory. """ path = self.output_directory.text() if os.path.exists(path): return title = self.tr('Directory %s not exist') % path question = self.tr( 'Directory %s not exist. Do you want to create it?') % path # noinspection PyCallByClass,PyTypeChecker answer = QMessageBox.question( self, title, question, QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.Yes: if len(path) != 0: os.makedirs(path) else: # noinspection PyCallByClass,PyTypeChecker,PyArgumentList display_warning_message_box( self, self.tr('InaSAFE error'), self.tr('Output directory can not be empty.')) raise CanceledImportDialogError() else: raise CanceledImportDialogError() def load_shapefile(self, feature_type, base_path): """Load downloaded shape file to QGIS Main Window. TODO: This is cut & paste from OSM - refactor to have one method :param feature_type: What kind of features should be downloaded. Currently 'buildings', 'building-points' or 'roads' are supported. :type feature_type: str :param base_path: The base path of the shape file (without extension). :type base_path: str :raises: FileMissingError - when buildings.shp not exist """ path = '%s.shp' % base_path if not os.path.exists(path): message = self.tr( '%s does not exist. The server does not have any data for ' 'this extent.' % path) raise FileMissingError(message) self.iface.addVectorLayer(path, feature_type, 'ogr') def reject(self): """Redefinition of the method. It will call the super method. """ super(PetaBencanaDialog, self).reject() def download(self, url, output_path): """Download file from API url and write to output path. :param url: URL of the API. :type url: str :param output_path: Path of output file, :type output_path: str """ request_failed_message = self.tr( "Can't access PetaBencana API: {source}").format( source=url) downloader = FileDownloader(url, output_path) result, message = downloader.download() if not result: display_warning_message_box( self, self.tr('Download error'), self.tr(request_failed_message + '\n' + message)) if result == QNetworkReply.OperationCanceledError: display_warning_message_box( self, self.tr('Download error'), self.tr(message)) # The function below might be usefull for future usage. # def get_available_area(self): # """Function to automatically get the available area on API. # *still cannot get string data from QByteArray* # """ # available_area = [] # network_manager = QgsNetworkAccessManager.instance() # api_url = QUrl('https://data.petabencana.id/cities') # api_request = QNetworkRequest(api_url) # api_response = network_manager.get(api_request) # data = api_response.readAll() # json_response = QScriptEngine().evaluate(data) # geometries = json_response.property('output').property('geometries') # iterator = QScriptValueIterator(geometries) # while iterator.hasNext(): # iterator.next() # geometry = iterator.value() # geometry_code = ( # geometry.property('properties').property('code').toString()) # available_area.append(geometry_code) def populate_combo_box(self): """Populate combobox for selecting city.""" if self.radio_button_production.isChecked(): self.source = production_api['url'] available_data = production_api['available_data'] else: self.source = development_api['url'] available_data = development_api['available_data'] self.city_combo_box.clear() for index, data in enumerate(available_data): self.city_combo_box.addItem(data['name']) self.city_combo_box.setItemData( index, data['code'], Qt.UserRole) def define_url(self): """Define API url based on which source is selected. :return: Valid url of selected source. :rtype: str """ current_index = self.city_combo_box.currentIndex() city_code = self.city_combo_box.itemData(current_index, Qt.UserRole) source = (self.source).format(city_code=city_code) return source
class SDEllipseDialog(QDialog, FORM_CLASS): def __init__(self, iface, parent=None): self.iface = iface self.plugin_dir = os.path.dirname(__file__) # Some constants self.SDELLIPSE = self.tr('SD Ellipse') self.CANCEL = self.tr('Cancel') self.HELP = self.tr('Help') self.CLOSE = self.tr('Close') self.OK = self.tr('OK') """Constructor.""" super(SDEllipseDialog, self).__init__(parent) # Set up the user interface from Designer. # After setupUI you can access any designer object by doing # self.<objectname>, and you can use autoconnect slots - see # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html # #widgets-and-dialogs-with-auto-connect self.setupUi(self) self.method_group = QButtonGroup() self.method_group.addButton(self.yuill_rb) self.method_group.addButton(self.crimestat_rb) self.method_group.buttonClicked[QAbstractButton].connect( self.methodChanged) self.yuill_rb.setChecked(True) self.method = 1 okButton = self.button_box.button(QDialogButtonBox.Ok) okButton.setText(self.OK) cancelButton = self.button_box.button(QDialogButtonBox.Cancel) cancelButton.setText(self.CANCEL) cancelButton.setEnabled(False) # helpButton = self.button_box.button(QDialogButtonBox.Help) helpButton = self.helpButton helpButton.setText(self.HELP) closeButton = self.button_box.button(QDialogButtonBox.Close) closeButton.setText(self.CLOSE) # Connect signals okButton.clicked.connect(self.startWorker) cancelButton.clicked.connect(self.killWorker) helpButton.clicked.connect(self.giveHelp) closeButton.clicked.connect(self.reject) self.cumulative = False inpIndexCh = self.InputLayer.currentIndexChanged['QString'] inpIndexCh.connect(self.layerchanged) # QObject.disconnect(self.button_box, SIGNAL("rejected()"), # self.reject) self.button_box.rejected.disconnect(self.reject) # Set instance variables self.worker = None self.inputlayerid = None self.layerlistchanging = False self.selectedFeatures_cb.setChecked(True) self.useWeights_cb.setChecked(False) self.result = None # end of __init__ def giveHelp(self): self.showInfo('Giving help') QDesktopServices.openUrl(QUrl.fromLocalFile( self.plugin_dir + "/help/html/index.html")) # showPluginHelp(None, "help/html/index") # end of giveHelp def startWorker(self): # self.showInfo('Ready to start worker') self.degfreedCorr = self.degfreedcorr_cb.isChecked() self.crimestatCorr = self.crimestatcorr_cb.isChecked() # Get the input layer layerindex = self.InputLayer.currentIndex() layerId = self.InputLayer.itemData(layerindex) inputlayer = QgsProject.instance().mapLayer(layerId) if inputlayer is None: self.showError(self.tr('No input layer defined')) return self.featureCount = 0 if self.selectedFeatures_cb.isChecked(): self.featureCount = inputlayer.selectedFeatureCount() if self.featureCount == 0: self.featureCount = inputlayer.featureCount() if self.featureCount < 2: self.showError(self.tr('Not enough features')) # self.scene.clear() return if (self.useWeights_cb.isChecked() and self.inputField.count() == 0): self.showError(self.tr('Missing numerical field')) return fieldindex = self.inputField.currentIndex() fieldname = self.inputField.itemData(fieldindex) # inpfield = inputlayer.fieldNameIndex(fieldindex) # minval = inputlayer.minimumValue(inpfield) if (not self.useWeights_cb.isChecked()): fieldname = None self.result = None self.SDLayer = inputlayer self.method = 1 if self.yuill_rb.isChecked(): self.method = 1 elif self.crimestat_rb.isChecked(): self.method = 2 if self.featureCount < 3 and (self.method == 2 or self.degfreedCorr): self.showError(self.tr('Not enough features')) return # create a new worker instance worker = Worker(inputlayer, self.selectedFeatures_cb.isChecked(), fieldname, self.method) # start the worker in a new thread thread = QThread(self) worker.moveToThread(thread) worker.finished.connect(self.workerFinished) worker.error.connect(self.workerError) worker.status.connect(self.workerInfo) worker.progress.connect(self.progressBar.setValue) # worker.progress.connect(self.aprogressBar.setValue) thread.started.connect(worker.run) thread.start() self.thread = thread self.worker = worker self.button_box.button(QDialogButtonBox.Ok).setEnabled(False) self.button_box.button(QDialogButtonBox.Close).setEnabled(False) self.button_box.button(QDialogButtonBox.Cancel).setEnabled(True) self.InputLayer.setEnabled(False) self.inputField.setEnabled(False) # end of startWorker def workerFinished(self, ok, ret): """Handles the output from the worker and cleans up after the worker has finished.""" # clean up the worker and thread self.worker.deleteLater() self.thread.quit() self.thread.wait() self.thread.deleteLater() # remove widget from the message bar (pop) # self.iface.messageBar().popWidget(self.messageBar) if ok and ret is not None: # self.showInfo("Result: " + str(ret)) self.result = ret # Draw the ellipse self.drawEllipse() else: # notify the user that something went wrong if not ok: self.showError(self.tr('Aborted') + '!') else: self.showError(self.tr('Not able to create ellipse') + '!') # Update the user interface self.progressBar.setValue(0.0) self.button_box.button(QDialogButtonBox.Ok).setEnabled(True) self.button_box.button(QDialogButtonBox.Close).setEnabled(True) self.button_box.button(QDialogButtonBox.Cancel).setEnabled(False) self.InputLayer.setEnabled(True) if self.method == 1 and self.inputField.count() > 0: self.inputField.setEnabled(True) # end of workerFinished(self, ok, ret) def workerError(self, exception_string): """Report an error from the worker.""" self.showError(self.tr('Worker failed - exception') + ': ' + exception_string) def workerInfo(self, message_string): """Report an info message from the worker.""" self.showInfo(self.tr('Worker') + ': ' + message_string) def killWorker(self): """Kill the worker thread.""" if self.worker is not None: QgsMessageLog.logMessage(self.tr('Killing worker'), self.SDELLIPSE, Qgis.Info) self.worker.kill() # Implement the reject method to have the possibility to avoid # exiting the dialog when cancelling def reject(self): """Reject override.""" # exit the dialog QDialog.reject(self) def drawEllipse(self): # self.showInfo('Result: ' + str(self.result)) meanx = self.result[0] meany = self.result[1] angle1 = self.result[2] angle2 = self.result[3] SD1 = self.result[4] SD2 = self.result[5] if self.method == 2: # CrimeStat SD1 = SD1 * (sqrt(2) * sqrt(self.featureCount) / sqrt(self.featureCount - 2)) SD2 = SD2 * (sqrt(2) * sqrt(self.featureCount) / sqrt(self.featureCount - 2)) if self.crimestatCorr and self.method != 2: SD1 = SD1 * sqrt(2) SD2 = SD2 * sqrt(2) if self.degfreedCorr and self.method != 2: SD1 = SD1 * sqrt(self.featureCount) / sqrt(self.featureCount - 2) SD2 = SD2 * sqrt(self.featureCount) / sqrt(self.featureCount - 2) # SD1 = SD1 * sqrt(self.featureCount) / sqrt(self.featureCount - 1) # SD2 = SD2 * sqrt(self.featureCount) / sqrt(self.featureCount - 1) # Find the major and minor axis majoraxisangle = angle1 minoraxisangle = angle2 majorSD = SD2 minorSD = SD1 if SD2 < SD1: majoraxisangle = angle2 minoraxisangle = angle1 majorSD = SD1 minorSD = SD2 # Calculate the "compass" direction angle (clockwise from north) direction = 90.0 - majoraxisangle * 180 / pi # Calculte the eccentricity eccentricity = sqrt(1 - pow(minorSD, 2) / pow(majorSD, 2)) # Create the memory layer for the ellipse sdefields = [] sdefields.append(QgsField("meanx", QVariant.Double)) sdefields.append(QgsField("meany", QVariant.Double)) sdefields.append(QgsField("majoranglerad", QVariant.Double)) # sdefields.append(QgsField("minoranglerad", QVariant.Double)) sdefields.append(QgsField("directiondeg", QVariant.Double)) sdefields.append(QgsField("majorsd", QVariant.Double)) sdefields.append(QgsField("minorsd", QVariant.Double)) sdefields.append(QgsField("eccentricity", QVariant.Double)) layeruri = 'Polygon?' layeruri = (layeruri + 'crs=' + str(self.SDLayer.dataProvider().crs().authid())) memSDlayer = QgsVectorLayer(layeruri, self.OutputLayerName.text(), "memory") # Set the CRS to the original CRS object memSDlayer.setCrs(self.SDLayer.dataProvider().crs()) memSDlayer.startEditing() # ? for field in sdefields: memSDlayer.dataProvider().addAttributes([field]) sdfeature = QgsFeature() theta1 = majoraxisangle points = [] step = pi / 180 # 360 points to draw the ellipse t = 0.0 while t < 2 * pi: p1 = QPointF(meanx + majorSD * cos(t) * cos(majoraxisangle) - minorSD * sin(t) * sin(majoraxisangle), meany + majorSD * cos(t) * sin(majoraxisangle) + minorSD * sin(t) * cos(majoraxisangle)) points.append(QgsPointXY(p1)) t = t + step # Close the polygon p1 = QPointF(meanx + majorSD * cos(majoraxisangle), meany + majorSD * sin(majoraxisangle)) points.append(QgsPointXY(p1)) sdfeature.setGeometry(QgsGeometry.fromPolygonXY([points])) attrs = [meanx, meany, majoraxisangle, direction, majorSD, minorSD, eccentricity] sdfeature.setAttributes(attrs) memSDlayer.dataProvider().addFeatures([sdfeature]) memSDlayer.commitChanges() # ? memSDlayer.updateExtents() QgsProject.instance().addMapLayers([memSDlayer]) # end of drawEllipse def layerchanged(self, number=0): """Do the necessary updates after a layer selection has been changed.""" self.layerselectionactive = True layerindex = self.InputLayer.currentIndex() layerId = self.InputLayer.itemData(layerindex) self.inputlayerid = layerId inputlayer = QgsProject.instance().mapLayer(layerId) while self.inputField.count() > 0: self.inputField.removeItem(0) self.useWeights_cb.setEnabled(False) self.inputField.setEnabled(False) self.layerselectionactive = False # Get the numerical fields if inputlayer is not None: provider = inputlayer.dataProvider() attribs = provider.fields() if str(type(attribs)) != "<type 'dict'>": atr = {} for i in range(attribs.count()): atr[i] = attribs.at(i) attrdict = atr for id, attrib in attrdict.items(): # Check for numeric attribute #if attrib.typeName().upper() in ('REAL', 'INTEGER', 'INT4', # 'INT8', 'FLOAT4'): if attrib.isNumeric(): self.inputField.addItem(attrib.name(), attrib.name()) if (self.inputField.count() > 0): if self.method == 1: self.useWeights_cb.setEnabled(True) self.inputField.setEnabled(True) else: self.useWeights_cb.setChecked(False) self.OutputLayerName.setText( "SDE_" + self.method_group.checkedButton().text().strip('"') + "_" + self.InputLayer.currentText()) # end of layerchanged def methodChanged(self, button): if self.InputLayer.currentText() is not None: self.OutputLayerName.setText("SDE_" + button.text().strip('"') + "_" + self.InputLayer.currentText()) # Disable all options self.crimestatcorr_cb.setEnabled(False) self.crimestatcorr_cb.setChecked(False) self.degfreedcorr_cb.setEnabled(False) self.degfreedcorr_cb.setChecked(False) self.useWeights_cb.setEnabled(False) self.useWeights_cb.setChecked(False) self.inputField.setEnabled(False) if button.text() == '"CrimeStat"': self.method = 2 elif button.text() == "Yuill": self.method = 1 self.crimestatcorr_cb.setEnabled(True) self.degfreedcorr_cb.setEnabled(True) if self.inputField.count() > 0: self.useWeights_cb.setEnabled(True) self.inputField.setEnabled(True) else: # Should not be reached yet if self.inputField.count() > 0: self.useWeights_cb.setEnabled(True) self.inputField.setEnabled(True) def showError(self, text): """Show an error.""" # self.iface.messageBar().pushMessage(self.tr('Error'), text, # level=QgsMessageBar.CRITICAL, # duration=3) QgsMessageLog.logMessage('Error: ' + text, self.SDELLIPSE, Qgis.Critical) def showWarning(self, text): """Show a warning.""" # self.iface.messageBar().pushMessage(self.tr('Warning'), text, # level=QgsMessageBar.WARNING, # duration=2) QgsMessageLog.logMessage('Warning: ' + text, self.SDELLIPSE, Qgis.Warning) def showInfo(self, text): """Show info.""" # self.iface.messageBar().pushMessage(self.tr('Info'), text, # level=QgsMessageBar.INFO, # duration=2) QgsMessageLog.logMessage('Info: ' + text, self.SDELLIPSE, Qgis.Info) # Implement the accept method to avoid exiting the dialog when # starting the work def accept(self): """Accept override.""" pass # Implement the reject method to have the possibility to avoid # exiting the dialog when cancelling def reject(self): """Reject override.""" # exit the dialog QDialog.reject(self) # Translation def tr(self, message): """Get the translation for a string using Qt translation API. :param message: String for translation. :type message: str, QString :returns: Translated version of message. :rtype: QString """ return QCoreApplication.translate('Dialog', message) # Overriding def resizeEvent(self, event): return # self.showInfo("resizeEvent") # Overriding def showEvent(self, event): return
class CheckboxesPanel(QWidget): def __init__(self, options, multiple, columns=2, parent=None): super(CheckboxesPanel, self).__init__(parent) self._options = [] for i, option in enumerate(options): if isinstance(option, str): self._options.append((i, option)) else: self.options.append(option) self._multiple = multiple self._buttons = [] rows = len(options) / columns self._buttonGroup = QButtonGroup() self._buttonGroup.setExclusive(not multiple) layout = QGridLayout() layout.setContentsMargins(0, 0, 0, 0) layout.setMargin(0) for i, (v, t) in enumerate(self._options): if multiple: button = QCheckBox(t) else: button = QRadioButton(t) self._buttons.append((v, button)) self._buttonGroup.addButton(button, i) layout.addWidget(button, i % rows, i / rows) layout.addItem( QSpacerItem(0, 0, QSizePolicy.Expanding, QSizePolicy.Minimum), 0, columns) self.setLayout(layout) if multiple: self.setContextMenuPolicy(Qt.CustomContextMenu) self.customContextMenuRequested.connect(self.showPopupMenu) def showPopupMenu(self): popup_menu = QMenu() select_all_action = QAction(self.tr('Select All'), popup_menu) select_all_action.triggered.connect(self.selectAll) clear_all_action = QAction(self.tr('Clear Selection'), popup_menu) clear_all_action.triggered.connect(self.deselectAll) popup_menu.addAction(select_all_action) popup_menu.addAction(clear_all_action) popup_menu.exec_(QCursor.pos()) def selectAll(self): for (v, button) in self._buttons: button.setChecked(True) def deselectAll(self): for (v, button) in self._buttons: button.setChecked(False) def value(self): if self._multiple: value = [] for (v, checkbox) in self._buttons: if checkbox.isChecked(): value.append(v) return value else: return self._options[self._buttonGroup.checkedId()][0] def setValue(self, value): if self._multiple: for (v, button) in self._buttons: button.setChecked(v in value) else: for v, button in self._buttons: button.setChecked(v == value)
class DefaultValueParameterWidget(GenericParameterWidget): """Widget class for Default Value Parameter.""" def __init__(self, parameter, parent=None): """Constructor. :param parameter: A DefaultValueParameter object. :type parameter: DefaultValueParameter """ super(DefaultValueParameterWidget, self).__init__(parameter, parent) self.radio_button_layout = QHBoxLayout() # Create radio button group self.input_button_group = QButtonGroup() for i in range(len(self._parameter.labels)): if '%s' in self._parameter.labels[i]: label = ( self._parameter.labels[i] % self._parameter.options[i]) else: label = self._parameter.labels[i] radio_button = QRadioButton(label) self.radio_button_layout.addWidget(radio_button) self.input_button_group.addButton(radio_button, i) if self._parameter.value == \ self._parameter.options[i]: radio_button.setChecked(True) # Create double spin box for custom value self.custom_value = QDoubleSpinBox() self.custom_value.setSingleStep(0.1) if self._parameter.options[-1]: self.custom_value.setValue(self._parameter.options[-1]) self.radio_button_layout.addWidget(self.custom_value) self.toggle_custom_value() self.inner_input_layout.addLayout(self.radio_button_layout) # Connect # noinspection PyUnresolvedReferences self.input_button_group.buttonClicked.connect( self.toggle_custom_value) def raise_invalid_type_exception(self): """Raise invalid type.""" message = 'Expecting element type of %s' % ( self._parameter.element_type.__name__) err = ValueError(message) return err def get_parameter(self): """Obtain list parameter object from the current widget state. :returns: A DefaultValueParameter from the current state of widget :rtype: DefaultValueParameter """ radio_button_checked_id = self.input_button_group.checkedId() # No radio button checked, then default value = None if radio_button_checked_id == -1: self._parameter.value = None # The last radio button (custom) is checked, get the value from the # line edit elif radio_button_checked_id == len(self._parameter.options) - 1: self._parameter.options[radio_button_checked_id] = \ self.custom_value.value() self._parameter.value = self.custom_value.value() else: self._parameter.value = self._parameter.options[ radio_button_checked_id] return self._parameter def set_value(self, value): """Set value by item's string. :param value: The value. :type value: str, int :returns: True if success, else False. :rtype: bool """ # Find index of choice try: value_index = self._parameter.options.index(value) self.input_button_group.button(value_index).setChecked(True) except ValueError: last_index = len(self._parameter.options) - 1 self.input_button_group.button(last_index).setChecked( True) self.custom_value.setValue(value) self.toggle_custom_value() def toggle_custom_value(self): """Enable or disable the custom value line edit.""" radio_button_checked_id = self.input_button_group.checkedId() if (radio_button_checked_id == len(self._parameter.options) - 1): self.custom_value.setDisabled(False) else: self.custom_value.setDisabled(True)
class GroupSelectParameterWidget(GenericParameterWidget): """Widget class for Group Select Parameter.""" def __init__(self, parameter, parent=None): """Constructor. :param parameter: A GroupSelectParameter object. :type parameter: GroupSelectParameter """ QWidget.__init__(self, parent) self._parameter = parameter # Store spin box self.spin_boxes = {} # Create elements # Label (name) self.label = QLabel(self._parameter.name) # Layouts self.main_layout = QVBoxLayout() self.input_layout = QVBoxLayout() # _inner_input_layout must be filled with widget in the child class self.inner_input_layout = QVBoxLayout() self.radio_button_layout = QGridLayout() # Create radio button group self.input_button_group = QButtonGroup() # List widget self.list_widget = QListWidget() self.list_widget.setSelectionMode(QAbstractItemView.ExtendedSelection) self.list_widget.setDragDropMode(QAbstractItemView.DragDrop) self.list_widget.setDefaultDropAction(Qt.MoveAction) self.list_widget.setEnabled(False) self.list_widget.setSizePolicy( QSizePolicy.Maximum, QSizePolicy.Expanding) for i, key in enumerate(self._parameter.options): value = self._parameter.options[key] radio_button = QRadioButton(value.get('label')) self.radio_button_layout.addWidget(radio_button, i, 0) if value.get('type') == SINGLE_DYNAMIC: percentage_spin_box = PercentageSpinBox(self) self.radio_button_layout.addWidget(percentage_spin_box, i, 1) percentage_spin_box.setValue(value.get('value', 0)) step = percentage_spin_box.singleStep() if step > 1: precision = 0 else: precision = len(str(step).split('.')[1]) if precision > 3: precision = 3 percentage_spin_box.setDecimals(precision) self.spin_boxes[key] = percentage_spin_box # Enable spin box depends on the selected option if self._parameter.selected == key: percentage_spin_box.setEnabled(True) else: percentage_spin_box.setEnabled(False) elif value.get('type') == STATIC: static_value = value.get('value', 0) if static_value is not None: self.radio_button_layout.addWidget( QLabel(str(static_value * 100) + ' %'), i, 1) elif value.get('type') == MULTIPLE_DYNAMIC: if self._parameter.selected == key: self.list_widget.setEnabled(True) else: self.list_widget.setEnabled(False) self.input_button_group.addButton(radio_button, i) if self._parameter.selected == key: radio_button.setChecked(True) # Help text self.help_label = QLabel(self._parameter.help_text) self.help_label.setSizePolicy( QSizePolicy.Maximum, QSizePolicy.Expanding) self.help_label.setWordWrap(True) self.help_label.setAlignment(Qt.AlignTop) self.inner_input_layout.addLayout(self.radio_button_layout) self.inner_input_layout.addWidget(self.list_widget) # Put elements into layouts self.input_layout.addWidget(self.label) self.input_layout.addLayout(self.inner_input_layout) self.help_layout = QVBoxLayout() self.help_layout.addWidget(self.help_label) self.main_layout.addLayout(self.input_layout) self.main_layout.addLayout(self.help_layout) self.setLayout(self.main_layout) self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Expanding) # Update list widget self.update_list_widget() # Connect signal # noinspection PyUnresolvedReferences self.input_button_group.buttonClicked.connect( self.radio_buttons_clicked) def get_parameter(self): """Obtain list parameter object from the current widget state. :returns: A DefaultValueParameter from the current state of widget :rtype: DefaultValueParameter """ # Set value for each key for key, value in list(self._parameter.options.items()): if value.get('type') == STATIC: continue elif value.get('type') == SINGLE_DYNAMIC: new_value = self.spin_boxes.get(key).value() self._parameter.set_value_for_key(key, new_value) elif value.get('type') == MULTIPLE_DYNAMIC: # Need to iterate through all items items = [] for index in range(self.list_widget.count()): items.append(self.list_widget.item(index)) new_value = [i.text() for i in items] self._parameter.set_value_for_key(key, new_value) # Get selected radio button radio_button_checked_id = self.input_button_group.checkedId() # No radio button checked, then default value = None if radio_button_checked_id == -1: self._parameter.selected = None else: self._parameter.selected = list(self._parameter.options.keys())[ radio_button_checked_id] return self._parameter def update_list_widget(self): """Update list widget when radio button is clicked.""" # Get selected radio button radio_button_checked_id = self.input_button_group.checkedId() # No radio button checked, then default value = None if radio_button_checked_id > -1: selected_dict = list(self._parameter.options.values())[ radio_button_checked_id] if selected_dict.get('type') == MULTIPLE_DYNAMIC: for field in selected_dict.get('value'): # Update list widget field_item = QListWidgetItem(self.list_widget) field_item.setFlags( Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled) field_item.setData(Qt.UserRole, field) field_item.setText(field) self.list_widget.addItem(field_item) def radio_buttons_clicked(self): """Handler when selected radio button changed.""" # Disable all spin boxes for spin_box in list(self.spin_boxes.values()): spin_box.setEnabled(False) # Disable list widget self.list_widget.setEnabled(False) # Get selected radio button radio_button_checked_id = self.input_button_group.checkedId() if radio_button_checked_id > -1: selected_value = list(self._parameter.options.values())[ radio_button_checked_id] if selected_value.get('type') == MULTIPLE_DYNAMIC: # Enable list widget self.list_widget.setEnabled(True) elif selected_value.get('type') == SINGLE_DYNAMIC: selected_key = list(self._parameter.options.keys())[ radio_button_checked_id] self.spin_boxes[selected_key].setEnabled(True) def select_radio_button(self, key): """Helper to select a radio button with key. :param key: The key of the radio button. :type key: str """ key_index = list(self._parameter.options.keys()).index(key) radio_button = self.input_button_group.button(key_index) radio_button.click()
class PetaBencanaDialog(QDialog, FORM_CLASS): """Downloader for PetaBencana data. .. versionadded: 3.3 """ def __init__(self, parent=None, iface=None): """Constructor for import dialog. .. versionadded: 3.3 :param parent: Optional widget to use as parent. :type parent: QWidget :param iface: An instance of QgisInterface. :type iface: QgisInterface """ QDialog.__init__(self, parent) self.parent = parent self.setupUi(self) title = self.tr('PetaBencana Downloader') self.setWindowTitle(title) icon = resources_path('img', 'icons', 'add-petabencana-layer.svg') self.setWindowIcon(QtGui.QIcon(icon)) self.iface = iface self.source = None self.radio_button_group = QButtonGroup() self.radio_button_group.addButton(self.radio_button_production) self.radio_button_group.addButton(self.radio_button_development) self.radio_button_group.setExclusive(True) self.radio_button_production.setChecked(True) self.populate_combo_box() developer_mode = setting('developer_mode', False, bool) if not developer_mode: self.radio_button_widget.hide() self.source_label.hide() self.output_group.adjustSize() # signals self.radio_button_production.clicked.connect(self.populate_combo_box) self.radio_button_development.clicked.connect(self.populate_combo_box) # Set up things for context help self.help_button = self.button_box.button( QtWidgets.QDialogButtonBox.Help) # Allow toggling the help button self.help_button.setCheckable(True) self.help_button.toggled.connect(self.help_toggled) self.main_stacked_widget.setCurrentIndex(1) # set up the validator for the file name prefix expression = QRegExp('^[A-Za-z0-9-_]*$') validator = QtGui.QRegExpValidator(expression, self.filename_prefix) self.filename_prefix.setValidator(validator) self.time_stamp = None self.restore_state() @pyqtSlot(bool) # prevents actions being handled twice def help_toggled(self, flag): """Show or hide the help tab in the stacked widget. .. versionadded: 3.3 :param flag: Flag indicating whether help should be shown or hidden. :type flag: bool """ if flag: self.help_button.setText(self.tr('Hide Help')) self.show_help() else: self.help_button.setText(self.tr('Show Help')) self.hide_help() def hide_help(self): """Hide the usage info from the user. .. versionadded:: 3.3 """ self.main_stacked_widget.setCurrentIndex(1) def show_help(self): """Show usage info to the user. .. versionadded: 3.3 """ # Read the header and footer html snippets self.main_stacked_widget.setCurrentIndex(0) header = html_header() footer = html_footer() string = header message = peta_bencana_help() string += message.to_html() string += footer self.help_web_view.setHtml(string) def restore_state(self): """Read last state of GUI from configuration file. .. versionadded: 3.3 """ settings = QSettings() try: last_path = settings.value('directory', type=str) except TypeError: last_path = '' self.output_directory.setText(last_path) def save_state(self): """Store current state of GUI to configuration file. .. versionadded: 3.3 """ settings = QSettings() settings.setValue('directory', self.output_directory.text()) @pyqtSlot() # prevents actions being handled twice def on_directory_button_clicked(self): """Show a dialog to choose directory. .. versionadded: 3.3 """ # noinspection PyCallByClass,PyTypeChecker self.output_directory.setText( QFileDialog.getExistingDirectory( self, self.tr('Select download directory'))) def accept(self): """Do PetaBencana download and display it in QGIS. .. versionadded: 3.3 """ self.save_state() try: self.require_directory() except CanceledImportDialogError: return QgsApplication.instance().setOverrideCursor( QtGui.QCursor(QtCore.Qt.WaitCursor)) source = self.define_url() # save the file as json first name = 'jakarta_flood.json' output_directory = self.output_directory.text() output_prefix = self.filename_prefix.text() overwrite = self.overwrite_flag.isChecked() date_stamp_flag = self.include_date_flag.isChecked() output_base_file_path = self.get_output_base_path( output_directory, output_prefix, date_stamp_flag, name, overwrite) title = self.tr("Can't access API") try: self.download(source, output_base_file_path) # Open downloaded file as QgsMapLayer options = QgsVectorLayer.LayerOptions(False) layer = QgsVectorLayer(output_base_file_path, 'flood', 'ogr', options) except Exception as e: disable_busy_cursor() QMessageBox.critical(self, title, str(e)) return self.time_stamp = time.strftime('%d-%b-%Y %H:%M:%S') # Now save as shp name = 'jakarta_flood.shp' output_base_file_path = self.get_output_base_path( output_directory, output_prefix, date_stamp_flag, name, overwrite) QgsVectorFileWriter.writeAsVectorFormat(layer, output_base_file_path, 'CP1250', QgsCoordinateTransform(), 'ESRI Shapefile') # Get rid of the GeoJSON layer and rather use local shp del layer self.copy_style(output_base_file_path) self.copy_keywords(output_base_file_path) layer = self.add_flooded_field(output_base_file_path) # check if the layer has feature or not if layer.featureCount() <= 0: city = self.city_combo_box.currentText() message = self.tr('There are no floods data available on {city} ' 'at this time.').format(city=city) display_warning_message_box(self, self.tr('No data'), message) disable_busy_cursor() else: # add the layer to the map project = QgsProject.instance() project.addMapLayer(layer) disable_busy_cursor() self.done(QDialog.Accepted) def add_flooded_field(self, shapefile_path): """Create the layer from the local shp adding the flooded field. .. versionadded:: 3.3 Use this method to add a calculated field to a shapefile. The shapefile should have a field called 'count' containing the number of flood reports for the field. The field values will be set to 0 if the count field is < 1, otherwise it will be set to 1. :param shapefile_path: Path to the shapefile that will have the flooded field added. :type shapefile_path: basestring :return: A vector layer with the flooded field added. :rtype: QgsVectorLayer """ layer = QgsVectorLayer(shapefile_path, self.tr('Jakarta Floods'), 'ogr') # Add a calculated field indicating if a poly is flooded or not # from qgis.PyQt.QtCore import QVariant layer.startEditing() # Add field with integer from 0 to 4 which represents the flood # class. Its the same as 'state' field except that is being treated # as a string. # This is used for cartography flood_class_field = QgsField('floodclass', QVariant.Int) layer.addAttribute(flood_class_field) layer.commitChanges() layer.startEditing() flood_class_idx = layer.fields().lookupField('floodclass') flood_class_expression = QgsExpression('to_int(state)') context = QgsExpressionContext() context.setFields(layer.fields()) flood_class_expression.prepare(context) # Add field with boolean flag to say if the area is flooded # This is used by the impact function flooded_field = QgsField('flooded', QVariant.Int) layer.dataProvider().addAttributes([flooded_field]) layer.commitChanges() layer.startEditing() flooded_idx = layer.fields().lookupField('flooded') flood_flag_expression = QgsExpression('state > 0') flood_flag_expression.prepare(context) for feature in layer.getFeatures(): context.setFeature(feature) feature[flood_class_idx] = flood_class_expression.evaluate(context) feature[flooded_idx] = flood_flag_expression.evaluate(context) layer.updateFeature(feature) layer.commitChanges() return layer def copy_keywords(self, shapefile_path): """Copy keywords from the OSM resource directory to the output path. .. versionadded: 3.3 In addition to copying the template, tokens within the template will be replaced with new values for the date token and title token. :param shapefile_path: Path to the shapefile that will have the flooded field added. :type shapefile_path: basestring """ source_xml_path = resources_path('petabencana', 'flood-keywords.xml') output_xml_path = shapefile_path.replace('shp', 'xml') LOGGER.info('Copying xml to: %s' % output_xml_path) title_token = '[TITLE]' new_title = self.tr('Jakarta Floods - %s' % self.time_stamp) date_token = '[DATE]' new_date = self.time_stamp with open(source_xml_path) as source_file, \ open(output_xml_path, 'w') as output_file: for line in source_file: line = line.replace(date_token, new_date) line = line.replace(title_token, new_title) output_file.write(line) @staticmethod def copy_style(shapefile_path): """Copy style from the OSM resource directory to the output path. .. versionadded: 3.3 :param shapefile_path: Path to the shapefile that should get the path added. :type shapefile_path: basestring """ source_qml_path = resources_path('petabencana', 'flood-style.qml') output_qml_path = shapefile_path.replace('shp', 'qml') LOGGER.info('Copying qml to: %s' % output_qml_path) copy(source_qml_path, output_qml_path) def get_output_base_path(self, output_directory, output_prefix, with_date_stamp, feature_type, overwrite): """Get a full base name path to save the shapefile. TODO: This is cut & paste from OSM - refactor to have one method :param output_directory: The directory where to put results. :type output_directory: str :param output_prefix: The prefix to add for the shapefile. :type output_prefix: str :param with_date_stamp: Whether to add a datestamp in between the file prefix and the feature_type for the shapefile name. :type output_prefix: str :param feature_type: What kind of data will be downloaded. Will be used for the shapefile name. :type feature_type: str :param overwrite: Boolean to know if we can overwrite existing files. :type overwrite: bool :return: The base path. :rtype: str """ if with_date_stamp and self.time_stamp is not None: time_stamp = self.time_stamp.replace(' ', '-') time_stamp = time_stamp.replace(':', '-') time_stamp += '-' feature_type = time_stamp + feature_type path = os.path.join(output_directory, '%s%s' % (output_prefix, feature_type)) if overwrite: # If a shapefile exists, we must remove it (only the .shp) shp = '%s.shp' % path if os.path.isfile(shp): os.remove(shp) else: separator = '-' suffix = self.get_unique_file_path_suffix('%s.shp' % path, separator) if suffix: path = os.path.join( output_directory, '%s%s%s%s' % (output_prefix, feature_type, separator, suffix)) return path @staticmethod def get_unique_file_path_suffix(file_path, separator='-', i=0): """Return the minimum number to suffix the file to not overwrite one. Example : /tmp/a.txt exists. - With file_path='/tmp/b.txt' will return 0. - With file_path='/tmp/a.txt' will return 1 (/tmp/a-1.txt) TODO: This is cut & paste from OSM - refactor to have one method :param file_path: The file to check. :type file_path: str :param separator: The separator to add before the prefix. :type separator: str :param i: The minimum prefix to check. :type i: int :return: The minimum prefix you should add to not overwrite a file. :rtype: int """ basename = os.path.splitext(file_path) if i != 0: file_path_test = os.path.join( '%s%s%s%s' % (basename[0], separator, i, basename[1])) else: file_path_test = file_path if os.path.isfile(file_path_test): return PetaBencanaDialog.get_unique_file_path_suffix( file_path, separator, i + 1) else: return i def require_directory(self): """Ensure directory path entered in dialog exist. When the path does not exist, this function will ask the user if he want to create it or not. TODO: This is cut & paste from OSM - refactor to have one method :raises: CanceledImportDialogError - when user choose 'No' in the question dialog for creating directory. """ path = self.output_directory.text() if os.path.exists(path): return title = self.tr('Directory %s not exist') % path question = self.tr( 'Directory %s not exist. Do you want to create it?') % path # noinspection PyCallByClass,PyTypeChecker answer = QMessageBox.question(self, title, question, QMessageBox.Yes | QMessageBox.No) if answer == QMessageBox.Yes: if len(path) != 0: os.makedirs(path) else: # noinspection PyCallByClass,PyTypeChecker,PyArgumentList display_warning_message_box( self, self.tr('InaSAFE error'), self.tr('Output directory can not be empty.')) raise CanceledImportDialogError() else: raise CanceledImportDialogError() def load_shapefile(self, feature_type, base_path): """Load downloaded shape file to QGIS Main Window. TODO: This is cut & paste from OSM - refactor to have one method :param feature_type: What kind of features should be downloaded. Currently 'buildings', 'building-points' or 'roads' are supported. :type feature_type: str :param base_path: The base path of the shape file (without extension). :type base_path: str :raises: FileMissingError - when buildings.shp not exist """ path = '%s.shp' % base_path if not os.path.exists(path): message = self.tr( '%s does not exist. The server does not have any data for ' 'this extent.' % path) raise FileMissingError(message) self.iface.addVectorLayer(path, feature_type, 'ogr') def reject(self): """Redefinition of the method. It will call the super method. """ super(PetaBencanaDialog, self).reject() def download(self, url, output_path): """Download file from API url and write to output path. :param url: URL of the API. :type url: str :param output_path: Path of output file, :type output_path: str """ request_failed_message = self.tr( "Can't access PetaBencana API: {source}").format(source=url) downloader = FileDownloader(url, output_path) result, message = downloader.download() if not result: display_warning_message_box( self, self.tr('Download error'), self.tr(request_failed_message + '\n' + message)) if result == QNetworkReply.OperationCanceledError: display_warning_message_box(self, self.tr('Download error'), self.tr(message)) # The function below might be usefull for future usage. # def get_available_area(self): # """Function to automatically get the available area on API. # *still cannot get string data from QByteArray* # """ # available_area = [] # network_manager = QgsNetworkAccessManager.instance() # api_url = QUrl('https://data.petabencana.id/cities') # api_request = QNetworkRequest(api_url) # api_response = network_manager.get(api_request) # data = api_response.readAll() # json_response = QScriptEngine().evaluate(data) # geometries = json_response.property('output').property('geometries') # iterator = QScriptValueIterator(geometries) # while iterator.hasNext(): # iterator.next() # geometry = iterator.value() # geometry_code = ( # geometry.property('properties').property('code').toString()) # available_area.append(geometry_code) def populate_combo_box(self): """Populate combobox for selecting city.""" if self.radio_button_production.isChecked(): self.source = production_api['url'] available_data = production_api['available_data'] else: self.source = development_api['url'] available_data = development_api['available_data'] self.city_combo_box.clear() for index, data in enumerate(available_data): self.city_combo_box.addItem(data['name']) self.city_combo_box.setItemData(index, data['code'], Qt.UserRole) def define_url(self): """Define API url based on which source is selected. :return: Valid url of selected source. :rtype: str """ current_index = self.city_combo_box.currentIndex() city_code = self.city_combo_box.itemData(current_index, Qt.UserRole) source = (self.source).format(city_code=city_code) return source
class DefaultValueParameterWidget(GenericParameterWidget): """Widget class for Default Value Parameter.""" def __init__(self, parameter, parent=None): """Constructor. :param parameter: A DefaultValueParameter object. :type parameter: DefaultValueParameter """ super(DefaultValueParameterWidget, self).__init__(parameter, parent) self.radio_button_layout = QHBoxLayout() # Create radio button group self.input_button_group = QButtonGroup() for i in range(len(self._parameter.labels)): if '%s' in self._parameter.labels[i]: label = (self._parameter.labels[i] % self._parameter.options[i]) else: label = self._parameter.labels[i] radio_button = QRadioButton(label) self.radio_button_layout.addWidget(radio_button) self.input_button_group.addButton(radio_button, i) if self._parameter.value == \ self._parameter.options[i]: radio_button.setChecked(True) # Create double spin box for custom value self.custom_value = QDoubleSpinBox() self.custom_value.setSingleStep(0.1) if self._parameter.options[-1]: self.custom_value.setValue(self._parameter.options[-1]) self.radio_button_layout.addWidget(self.custom_value) self.toggle_custom_value() self.inner_input_layout.addLayout(self.radio_button_layout) # Connect # noinspection PyUnresolvedReferences self.input_button_group.buttonClicked.connect(self.toggle_custom_value) def raise_invalid_type_exception(self): """Raise invalid type.""" message = 'Expecting element type of %s' % ( self._parameter.element_type.__name__) err = ValueError(message) return err def get_parameter(self): """Obtain list parameter object from the current widget state. :returns: A DefaultValueParameter from the current state of widget :rtype: DefaultValueParameter """ radio_button_checked_id = self.input_button_group.checkedId() # No radio button checked, then default value = None if radio_button_checked_id == -1: self._parameter.value = None # The last radio button (custom) is checked, get the value from the # line edit elif radio_button_checked_id == len(self._parameter.options) - 1: self._parameter.options[radio_button_checked_id] = \ self.custom_value.value() self._parameter.value = self.custom_value.value() else: self._parameter.value = self._parameter.options[ radio_button_checked_id] return self._parameter def set_value(self, value): """Set value by item's string. :param value: The value. :type value: str, int :returns: True if success, else False. :rtype: bool """ # Find index of choice try: value_index = self._parameter.options.index(value) self.input_button_group.button(value_index).setChecked(True) except ValueError: last_index = len(self._parameter.options) - 1 self.input_button_group.button(last_index).setChecked(True) self.custom_value.setValue(value) self.toggle_custom_value() def toggle_custom_value(self): """Enable or disable the custom value line edit.""" radio_button_checked_id = self.input_button_group.checkedId() if (radio_button_checked_id == len(self._parameter.options) - 1): self.custom_value.setDisabled(False) else: self.custom_value.setDisabled(True)
class DefaultSelectParameterWidget(SelectParameterWidget): """Widget class for Default Select Parameter.""" def __init__(self, parameter, parent=None): """Constructor :param parameter: A DefaultSelectParameter object. :type parameter: DefaultSelectParameter """ super(DefaultSelectParameterWidget, self).__init__(parameter, parent) self.default_layout = QHBoxLayout() self.radio_button_layout = QHBoxLayout() self.radio_button_widget = QWidget() self.default_label = QLabel(tr('Default')) # Create radio button group self.default_input_button_group = QButtonGroup() # Define string enabler for radio button self.radio_button_enabler = self.input.itemData(0, Qt.UserRole) for i in range(len(self._parameter.default_labels)): if '%s' in self._parameter.default_labels[i]: label = (self._parameter.default_labels[i] % self._parameter.default_values[i]) else: label = self._parameter.default_labels[i] radio_button = QRadioButton(label) self.radio_button_layout.addWidget(radio_button) self.default_input_button_group.addButton(radio_button, i) if self._parameter.default_value == \ self._parameter.default_values[i]: radio_button.setChecked(True) # Create double spin box for custom value self.custom_value = QDoubleSpinBox() if self._parameter.default_values[-1]: self.custom_value.setValue(self._parameter.default_values[-1]) has_min = False if self._parameter.minimum is not None: has_min = True self.custom_value.setMinimum(self._parameter.minimum) has_max = False if self._parameter.maximum is not None: has_max = True self.custom_value.setMaximum(self._parameter.maximum) if has_min and has_max: step = (self._parameter.maximum - self._parameter.minimum) / 100.0 self.custom_value.setSingleStep(step) self.radio_button_layout.addWidget(self.custom_value) self.toggle_custom_value() # Reset the layout self.input_layout.setParent(None) self.help_layout.setParent(None) self.label.setParent(None) self.inner_input_layout.setParent(None) self.input_layout = QGridLayout() self.input_layout.setSpacing(0) self.input_layout.addWidget(self.label, 0, 0) self.input_layout.addLayout(self.inner_input_layout, 0, 1) self.input_layout.addWidget(self.default_label, 1, 0) self.input_layout.addLayout(self.radio_button_layout, 1, 1) self.main_layout.addLayout(self.input_layout) self.main_layout.addLayout(self.help_layout) # check every added combobox, it could have been toggled by # the existing keyword self.toggle_input() # Connect # noinspection PyUnresolvedReferences self.input.currentIndexChanged.connect(self.toggle_input) self.default_input_button_group.buttonClicked.connect( self.toggle_custom_value) def raise_invalid_type_exception(self): message = 'Expecting element type of %s' % ( self._parameter.element_type.__name__) err = ValueError(message) return err def get_parameter(self): """Obtain list parameter object from the current widget state. :returns: A DefaultSelectParameter from the current state of widget. """ current_index = self.input.currentIndex() selected_value = self.input.itemData(current_index, Qt.UserRole) if hasattr(selected_value, 'toPyObject'): selected_value = selected_value.toPyObject() try: self._parameter.value = selected_value except ValueError: err = self.raise_invalid_type_exception() raise err radio_button_checked_id = self.default_input_button_group.checkedId() # No radio button checked, then default value = None if radio_button_checked_id == -1: self._parameter.default = None # The last radio button (custom) is checked, get the value from the # line edit elif (radio_button_checked_id == len(self._parameter.default_values) - 1): self._parameter.default_values[radio_button_checked_id] \ = self.custom_value.value() self._parameter.default = self.custom_value.value() else: self._parameter.default = self._parameter.default_values[ radio_button_checked_id] return self._parameter def set_default(self, default): """Set default value by item's string. :param default: The default. :type default: str, int :returns: True if success, else False. :rtype: bool """ # Find index of choice try: default_index = self._parameter.default_values.index(default) self.default_input_button_group.button(default_index).setChecked( True) except ValueError: last_index = len(self._parameter.default_values) - 1 self.default_input_button_group.button(last_index).setChecked(True) self.custom_value.setValue(default) self.toggle_custom_value() def toggle_custom_value(self): radio_button_checked_id = self.default_input_button_group.checkedId() if (radio_button_checked_id == len(self._parameter.default_values) - 1): self.custom_value.setDisabled(False) else: self.custom_value.setDisabled(True) def toggle_input(self): """Change behaviour of radio button based on input.""" current_index = self.input.currentIndex() # If current input is not a radio button enabler, disable radio button. if self.input.itemData(current_index, Qt.UserRole) != (self.radio_button_enabler): self.disable_radio_button() # Otherwise, enable radio button. else: self.enable_radio_button() def set_selected_radio_button(self): """Set selected radio button to 'Do not report'.""" dont_use_button = self.default_input_button_group.button( len(self._parameter.default_values) - 2) dont_use_button.setChecked(True) def disable_radio_button(self): """Disable radio button group and custom value input area.""" checked = self.default_input_button_group.checkedButton() if checked: self.default_input_button_group.setExclusive(False) checked.setChecked(False) self.default_input_button_group.setExclusive(True) for button in self.default_input_button_group.buttons(): button.setDisabled(True) self.custom_value.setDisabled(True) def enable_radio_button(self): """Enable radio button and custom value input area then set selected radio button to 'Do not report'. """ for button in self.default_input_button_group.buttons(): button.setEnabled(True) self.set_selected_radio_button() self.custom_value.setEnabled(True)
class DateTimePickerWidget(datepicker_widget, QWidget): ok = pyqtSignal() cancel = pyqtSignal() """ A custom date picker with a time and date picker """ def __init__(self, parent=None, mode="DateTime"): super(DateTimePickerWidget, self).__init__(parent) self.setupUi(self) self.mode = mode self.group = QButtonGroup() self.group.setExclusive(True) self.group.addButton(self.ambutton) self.group.addButton(self.pmbutton) self.ambutton.toggled.connect(self.isDirty) self.pmbutton.toggled.connect(self.isDirty) self.datepicker.selectionChanged.connect(self.isDirty) self.hourpicker.itemSelectionChanged.connect(self.isDirty) self.minutepicker.itemSelectionChanged.connect(self.isDirty) self.setasnowbutton.pressed.connect(self.setAsNow) self.setWindowFlags(Qt.Dialog | Qt.CustomizeWindowHint) self.okButton.pressed.connect(self.ok.emit) self.closebutton.pressed.connect(self.cancel.emit) def setMinValue(self, mindate): self.datepicker.setMinimumDate(mindate) def setmode(self, mode): if mode == "Date": self.timesection.hide() elif mode == "Time": self.datepicker.hide() def isDirty(self, *args): date = self.getSelectedDate() time = self.getSelectedTime() datetime = QDateTime(date, time) if self.mode == "Date": value = datetime.toString("ddd d MMM yyyy") elif self.mode == "Time": value = datetime.toString("hh:mm ap") else: value = datetime.toString("ddd d MMM yyyy 'at' hh:mm ap") self.label.setText("Set as: {}".format(value)) def setDateTime(self, datetime): """ Set the picker to datatime datetime - The QDateTime with the value to set. """ self.setTime(datetime.time()) self.setDate(datetime.date()) def setAsNow(self): """ Set the current date and time on the picker as now. """ now = QDateTime.currentDateTime() self.setDateTime(now) def setTime(self, time): """ Set just the time part of the picker """ hour = time.hour() if hour > 12: hour = hour - 12 if hour == 0: hour = hour + 12 minute = time.minute() minute = int(round(minute / 5.0) * 5.0) amap = time.toString("AP") utils.log("Hour %s Minute %s" % (hour, minute)) try: houritems = self.hourpicker.findItems(str(hour), Qt.MatchFixedString) self.hourpicker.setCurrentItem(houritems[0]) except IndexError: utils.log("Can't find hour") try: minuteitems = self.minutepicker.findItems(str(minute), Qt.MatchFixedString) self.minutepicker.setCurrentItem(minuteitems[0]) except IndexError: utils.log("Can't find minute") if amap == "PM": self.pmbutton.toggle() def setDate(self, date): """ Set just the date part of the picker """ self.datepicker.setSelectedDate(date) def getSelectedTime(self): """ Returns the currently selected data and time """ try: hour = self.hourpicker.currentItem().text() except AttributeError: hour = "" try: minute = self.minutepicker.currentItem().text() except AttributeError: minute = "" zone = self.ambutton.isChecked() and "AM" or "PM" return QTime.fromString("%s%s%s" % (hour.zfill(2), minute.zfill(2), zone), "hhmAP") def getSelectedDate(self): """ Returns just the date part of the picker """ return self.datepicker.selectedDate() @property def value(self): datetime = QDateTime() datetime.setDate(self.getSelectedDate()) datetime.setTime(self.getSelectedTime()) return datetime @value.setter def value(self, value): if value is None: self.setAsNow() return if isinstance(value, str): value = QDateTime.fromString(value, Qt.ISODate) self.setDate(value.date()) self.setTime(value.time())
class OptionWidget(EditorWidget): widgettype = 'Option Row' def __init__(self, *args, **kwargs): super(OptionWidget, self).__init__(*args, **kwargs) self._bindvalue = None self.group = QButtonGroup() self.group.buttonClicked.connect(self.emitvaluechanged) def createWidget(self, parent=None): widget = QWidget(parent) return widget def _buildfromlist(self, listconfig, multiselect): def chunks(l, n): """ Yield successive n-sized chunks from l. """ for i in range(0, len(l), n): yield l[i:i + n] items = listconfig['items'] wrap = self.config.get('wrap', 0) showcolor = self.config.get('always_color', False) if wrap > 0: rows = list(chunks(items, wrap)) else: rows = [items] for rowcount, row in enumerate(rows): for column, item in enumerate(row): parts = item.split(';') data = parts[0] try: desc = parts[1] except IndexError: desc = data button = QPushButton() button.setCheckable(multiselect) self.group.setExclusive(not multiselect) icon = QIcon() try: path = parts[2] if path.startswith("#"): # Colour the button with the hex value. # If show color is enabled we always show the color regardless of selection. if not showcolor: style = """ QPushButton::checked {{ border: 3px solid rgb(137, 175, 255); background-color: {colour}; }}""".format(colour=path) else: style = """ QPushButton::checked {{ border: 3px solid rgb(137, 175, 255); }} QPushButton {{ background-color: {colour}; }}""".format(colour=path) button.setStyleSheet(style) elif path.endswith("_icon"): icon = QIcon(":/icons/{}".format(path)) else: icon = QIcon(path) except: icon = QIcon() button.setCheckable(True) button.setText(desc) button.setProperty("value", data) button.setIcon(icon) button.setIconSize(QSize(24, 24)) if isinstance(self.widget.layout(), QBoxLayout): self.widget.layout().addWidget(button) else: self.widget.layout().addWidget(button, rowcount, column) self.group.addButton(button) def initWidget(self, widget, config): if not widget.layout(): widget.setLayout(QGridLayout()) widget.layout().setContentsMargins(0, 0, 0, 0) def updatefromconfig(self): super(OptionWidget, self).updatefromconfig() for button in self.group.buttons(): self.group.removeButton(button) self.widget.layout().removeWidget(button) button.deleteLater() button.setParent(None) listconfig = self.config['list'] multiselect = self.config.get('multi', False) self._buildfromlist(listconfig, multiselect) super(OptionWidget, self).endupdatefromconfig() def validate(self, *args): button = self.group.checkedButton() if button: return True return False @property def buttons(self): return self.group.buttons() @property def nullvalues(self): return ['NULL'] @property def multioption(self): return self.config.get('multi', False) def setvalue(self, value): def set_button(setvalue): for button in self.group.buttons(): buttonvalue = button.property("value") if (setvalue is None and buttonvalue in self.nullvalues) or buttonvalue == str(setvalue): button.setChecked(True) self.emitvaluechanged() return if value in self.nullvalues: value = None if self.multioption and value: values = value.split(';') else: values = [value] for value in values: set_button(value) def value(self): def _returnvalue(): if self.multioption: _values = [] checked = [button for button in self.group.buttons() if button.isChecked()] for button in checked: value = button.property("value") _values.append(value) if not _values: return None return ";".join(_values) else: checked = self.group.checkedButton() if not checked: return None value = checked.property("value") return value returnvalue = _returnvalue() if returnvalue in self.nullvalues: returnvalue = None return returnvalue def reset(self): for button in self.group.buttons(): if button.isChecked(): button.setChecked(False) self.emitvaluechanged() return def togglebutton(self, value): for button in self.group.buttons(): if button.property("value") == value: button.setChecked(not button.isChecked()) self.emitvaluechanged() return return
class GroupSelectParameterWidget(GenericParameterWidget): """Widget class for Group Select Parameter.""" def __init__(self, parameter, parent=None): """Constructor. :param parameter: A GroupSelectParameter object. :type parameter: GroupSelectParameter """ QWidget.__init__(self, parent) self._parameter = parameter # Store spin box self.spin_boxes = {} # Create elements # Label (name) self.label = QLabel(self._parameter.name) # Layouts self.main_layout = QVBoxLayout() self.input_layout = QVBoxLayout() # _inner_input_layout must be filled with widget in the child class self.inner_input_layout = QVBoxLayout() self.radio_button_layout = QGridLayout() # Create radio button group self.input_button_group = QButtonGroup() # List widget self.list_widget = QListWidget() self.list_widget.setSelectionMode(QAbstractItemView.ExtendedSelection) self.list_widget.setDragDropMode(QAbstractItemView.DragDrop) self.list_widget.setDefaultDropAction(Qt.MoveAction) self.list_widget.setEnabled(False) self.list_widget.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Expanding) for i, key in enumerate(self._parameter.options): value = self._parameter.options[key] radio_button = QRadioButton(value.get('label')) self.radio_button_layout.addWidget(radio_button, i, 0) if value.get('type') == SINGLE_DYNAMIC: percentage_spin_box = PercentageSpinBox(self) self.radio_button_layout.addWidget(percentage_spin_box, i, 1) percentage_spin_box.setValue(value.get('value', 0)) step = percentage_spin_box.singleStep() if step > 1: precision = 0 else: precision = len(str(step).split('.')[1]) if precision > 3: precision = 3 percentage_spin_box.setDecimals(precision) self.spin_boxes[key] = percentage_spin_box # Enable spin box depends on the selected option if self._parameter.selected == key: percentage_spin_box.setEnabled(True) else: percentage_spin_box.setEnabled(False) elif value.get('type') == STATIC: static_value = value.get('value', 0) if static_value is not None: self.radio_button_layout.addWidget( QLabel(str(static_value * 100) + ' %'), i, 1) elif value.get('type') == MULTIPLE_DYNAMIC: if self._parameter.selected == key: self.list_widget.setEnabled(True) else: self.list_widget.setEnabled(False) self.input_button_group.addButton(radio_button, i) if self._parameter.selected == key: radio_button.setChecked(True) # Help text self.help_label = QLabel(self._parameter.help_text) self.help_label.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Expanding) self.help_label.setWordWrap(True) self.help_label.setAlignment(Qt.AlignTop) self.inner_input_layout.addLayout(self.radio_button_layout) self.inner_input_layout.addWidget(self.list_widget) # Put elements into layouts self.input_layout.addWidget(self.label) self.input_layout.addLayout(self.inner_input_layout) self.help_layout = QVBoxLayout() self.help_layout.addWidget(self.help_label) self.main_layout.addLayout(self.input_layout) self.main_layout.addLayout(self.help_layout) self.setLayout(self.main_layout) self.setSizePolicy(QSizePolicy.Maximum, QSizePolicy.Expanding) # Update list widget self.update_list_widget() # Connect signal # noinspection PyUnresolvedReferences self.input_button_group.buttonClicked.connect( self.radio_buttons_clicked) def get_parameter(self): """Obtain list parameter object from the current widget state. :returns: A DefaultValueParameter from the current state of widget :rtype: DefaultValueParameter """ # Set value for each key for key, value in list(self._parameter.options.items()): if value.get('type') == STATIC: continue elif value.get('type') == SINGLE_DYNAMIC: new_value = self.spin_boxes.get(key).value() self._parameter.set_value_for_key(key, new_value) elif value.get('type') == MULTIPLE_DYNAMIC: # Need to iterate through all items items = [] for index in range(self.list_widget.count()): items.append(self.list_widget.item(index)) new_value = [i.text() for i in items] self._parameter.set_value_for_key(key, new_value) # Get selected radio button radio_button_checked_id = self.input_button_group.checkedId() # No radio button checked, then default value = None if radio_button_checked_id == -1: self._parameter.selected = None else: self._parameter.selected = list( self._parameter.options.keys())[radio_button_checked_id] return self._parameter def update_list_widget(self): """Update list widget when radio button is clicked.""" # Get selected radio button radio_button_checked_id = self.input_button_group.checkedId() # No radio button checked, then default value = None if radio_button_checked_id > -1: selected_dict = list( self._parameter.options.values())[radio_button_checked_id] if selected_dict.get('type') == MULTIPLE_DYNAMIC: for field in selected_dict.get('value'): # Update list widget field_item = QListWidgetItem(self.list_widget) field_item.setFlags(Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsDragEnabled) field_item.setData(Qt.UserRole, field) field_item.setText(field) self.list_widget.addItem(field_item) def radio_buttons_clicked(self): """Handler when selected radio button changed.""" # Disable all spin boxes for spin_box in list(self.spin_boxes.values()): spin_box.setEnabled(False) # Disable list widget self.list_widget.setEnabled(False) # Get selected radio button radio_button_checked_id = self.input_button_group.checkedId() if radio_button_checked_id > -1: selected_value = list( self._parameter.options.values())[radio_button_checked_id] if selected_value.get('type') == MULTIPLE_DYNAMIC: # Enable list widget self.list_widget.setEnabled(True) elif selected_value.get('type') == SINGLE_DYNAMIC: selected_key = list( self._parameter.options.keys())[radio_button_checked_id] self.spin_boxes[selected_key].setEnabled(True) def select_radio_button(self, key): """Helper to select a radio button with key. :param key: The key of the radio button. :type key: str """ key_index = list(self._parameter.options.keys()).index(key) radio_button = self.input_button_group.button(key_index) radio_button.click()