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 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 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 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 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