def __init__(self, version): super(AboutDialog, self).__init__() dialog_layout = QVBoxLayout() htmlText = """ <h3>{} - release {}</h3> Created by M. Alberti ([email protected]). <br /><br /><a href="https://github.com/mauroalberti/qgSurf">https://github.com/mauroalberti/qgSurf</a> <br /><br />Processing of geological data. <br /><br />Licensed under the terms of GNU GPL 3. """.format(plugin_nm, version) aboutQTextBrowser = QTextBrowser(self) aboutQTextBrowser.insertHtml(htmlText) aboutQTextBrowser.setMinimumSize(400, 200) dialog_layout.addWidget(aboutQTextBrowser) self.setLayout(dialog_layout) self.setWindowTitle('{} about'.format(plugin_nm))
class PlanetExplorerDockWidget(BASE, WIDGET): BASE: QDockWidget _auth_man: QgsAuthManager msgBar: QgsMessageBar stckdWidgetViews: QStackedWidget stckdWidgetResourceType: QStackedWidget leUser: QLineEdit lePass: QLineEdit leMosaicName: QLineEdit chkBxSaveCreds: QCheckBox lblSignUp: QLabel lblTermsOfService: QLabel lblForgotPass: QLabel cmbBoxItemGroupType: QComboBox btnSearch: QToolButton grpBoxMainFilters: QgsCollapsibleGroupBox grpBoxSeries: QgsCollapsibleGroupBox _main_filters: PlanetMainFilters grpBoxFilters: QgsCollapsibleGroupBox stckdWidgetFilters: QStackedWidget frameResults: QFrame tabWidgetResults: QTabWidget btnOrder: QToolButton btnCog: QToolButton btnInfo: QToolButton btnUser: QToolButton _user_act: QWidgetAction _terms_browser: QTextBrowser closingPlugin = pyqtSignal() def __init__(self, parent=None, iface=None, visible=False): super(PlanetExplorerDockWidget, 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://doc.qt.io/qt-5/designer-using-a-ui-file.html # #widgets-and-dialogs-with-auto-connect self.setupUi(self) self.setVisible(visible) self._iface = iface # noinspection PyArgumentList self._auth_man = QgsApplication.authManager() self.p_client = None self.api_key = None self._save_creds = bool( pluginSetting(SAVE_CREDS_KEY, namespace=SETTINGS_NAMESPACE, typ='bool')) self.leUser.addAction( QIcon(':/plugins/planet_explorer/envelope-gray.svg'), QLineEdit.LeadingPosition) self.lblSignUp.linkActivated[str].connect( lambda: self._open_link_with_browser(SIGNUP_URL)) self.lblTermsOfService.linkActivated[str].connect( lambda: self._open_link_with_browser(TOS_URL)) self.lblForgotPass.linkActivated[str].connect( lambda: self._open_link_with_browser(FORGOT_PASS_URL)) self.btn_ok = self.buttonBoxLogin.button(QDialogButtonBox.Ok) self.btn_ok.setText('Sign In') self.btn_api_key = self.buttonBoxLogin.button(QDialogButtonBox.Abort) """:type: QPushButton""" self.btn_api_key.setText('Use API key') self.btn_api_key.hide() self.buttonBoxLogin.accepted.connect(self.login) self.buttonBoxLogin.rejected.connect(self.api_key_login) self._setup_group_type_selector() self.cmbBoxItemGroupType.currentIndexChanged[int].connect( self._item_group_changed) self._toggle_search_highlight(True) self.btnSearch.clicked[bool].connect(self.perform_search) # Collected sources/filters, upon search request self._sources = None self._sort_order = None self._filters = None self._request = None # Set up the mosaics widget self._setup_mosaics_panel() # Set up AOI, date-range and text filters self._setup_main_filters() # Non-main, per-item-group control widget filters self._filter_widget_registry = {} self._setup_filter_widgets() # Set default group type and filter widget self.cmbBoxItemGroupType.setCurrentIndex(0) self.stckdWidgetFilters.setCurrentIndex(0) # Set up tabbed widget self.tabWidgetResults.tabCloseRequested[int].connect( self.tab_close_requested) # Set up lower button bar self.btnOrder.clicked.connect(self.order_checked) self._setup_cog_button() # noinspection PyTypeChecker self._user_act = None # noinspection PyTypeChecker self._terms_browser = None self._setup_info_button() self.msg_log = QgsMessageLog() # Local QgsMessageBar self.msgBar.hide() self.frameResults.setStyleSheet(RESULTS_BKGRD_PE) self._checked_queue_set_count = 0 self._checked_queue_set = set() self._checked_item_type_nodes = {} # noinspection PyUnusedLocal def showEvent(self, event): if self.logged_in(): self.stckdWidgetViews.setCurrentIndex(1) else: self._setup_client() def _setup_client(self): # Init api client self.p_client = PlanetClient.getInstance() self.p_client.register_area_km_func(area_from_geojsons) self.p_client.loginChanged[bool].connect(self.login_changed) # Retrieve any login/key settings self.switch_to_login_panel() if not self.logged_in(): self.api_key = API_KEY_DEFAULT self._set_credential_fields() # self.btn_api_key.setEnabled(bool(self.api_key)) self.chkBxSaveCreds.stateChanged.connect( self.save_credentials_changed) self._setup_user_button() # Skip login panel if an API key was retrieved/accepted by client # self.login_changed() def logged_in(self): return self.p_client is not None and self.p_client.has_api_key() @pyqtSlot() def api_key_login(self): if self.api_key: self.login(api_key=self.api_key) # Now switch panels self.login_changed() @pyqtSlot() def login(self, api_key=None): if self.logged_in(): return # Do login, push any error to message bar try: # Don't switch panels just yet self.p_client.blockSignals(True) self.p_client.log_in(self.leUser.text(), self.lePass.text(), api_key=api_key) self.p_client.blockSignals(False) except LoginException as e: self.show_message('Login failed!', show_more=str(e.__cause__), level=Qgis.Warning) # Stay on login panel if error return # Login OK self.api_key = self.p_client.api_key() user = self.p_client.user() if is_segments_write_key_valid(): analytics.identify( user["email"], { "email": user["email"], "apiKey": user["api_key"], "organizationId": user["organization_id"], "programId": user["program_id"] }) analytics.track(user["email"], "Log in to Explorer") if is_sentry_dsn_valid(): with sentry_sdk.configure_scope() as scope: scope.user = {"email": user["email"]} # Store settings if self.chkBxSaveCreds.isChecked(): self._store_auth_creds() # For debugging specs = f'logged_in={self.logged_in()}\n\n' \ f'api_key = {self.p_client.api_key()}\n\n' \ f'user: {self.p_client.user()}\n\n' log.debug(f'Login successful:\n{specs}') # Now switch panels self.p_client.loginChanged.emit(self.p_client.has_api_key()) #self.login_changed() @pyqtSlot() def logout(self): if not self.logged_in(): return # Do logout (switches to Login panel) self.p_client.log_out() # self.btn_api_key.setEnabled(bool(self.api_key)) log.debug('User logged out') @pyqtSlot() def login_changed(self): user_name = 'User' if self.logged_in(): p_user = self.p_client.user() if p_user and 'user_name' in p_user: user_name = p_user['user_name'] self.lePass.setText("") self.leUser.setText("") self.switch_to_browse_panel() else: self._set_credential_fields() self.switch_to_login_panel() self._user_act.defaultWidget().setText(f"<b>{user_name}</b>") @pyqtSlot() def switch_to_login_panel(self): self.stckdWidgetViews.setCurrentIndex(0) @pyqtSlot() def switch_to_browse_panel(self): self.stckdWidgetViews.setCurrentIndex(1) def switch_to_daily_images_panel(self): self.stckdWidgetResourceType.setCurrentIndex(0) def switch_to_mosaic_series_panel(self): self.stckdWidgetResourceType.setCurrentIndex(1) def switch_to_single_mosaics_panel(self): self.stckdWidgetResourceType.setCurrentIndex(2) @pyqtSlot(int) def _item_group_changed(self, indx): self.stckdWidgetResourceType.setCurrentIndex(indx) resource_type = self.cmbBoxItemGroupType.currentData() self.btnSearch.setEnabled(resource_type != RESOURCE_MOSAIC_SERIES) self.btnOrder.setVisible(resource_type == RESOURCE_DAILY) self.btnCog.setVisible(resource_type == RESOURCE_DAILY) if resource_type == RESOURCE_MOSAIC_SERIES: self.treeMosaicSeries.populate() elif resource_type == RESOURCE_SINGLE_MOSAICS: self.listSingleMosaics.populate_with_first_page() @pyqtSlot(bool) def _toggle_search_highlight(self, on=True): if on: self.btnSearch.setStyleSheet(SEARCH_HIGHLIGHT) self.btnSearch.setIcon( QIcon(':/plugins/planet_explorer/search_p.svg')) else: self.btnSearch.setStyleSheet('') self.btnSearch.setIcon( QIcon(':/plugins/planet_explorer/search.svg')) @pyqtSlot() def _update_checked_queue_set(self): tab_queues = [] for i in range(self.tabWidgetResults.count()): # noinspection PyTypeChecker wdgt: PlanetSearchResultsWidget = self.tabWidgetResults.widget(i) tab_queues.append(wdgt.checked_queue()) # unique item_type:item_id key grouping set new_queue_set = set().union(*tab_queues) # When using with {'item_type': set(nodes)} # new_queue_set = {} # tab_keys = set().union(*tab_queues) # for tk in tab_keys: # new_queue_set[tk] = set() # # for tq in tab_queues: # for tk in tq.keys(): # new_queue_set[tk] = new_queue_set[tk].union(tq[tk]) self._checked_queue_set = new_queue_set self._update_checked_queue_set_count() @pyqtSlot() def _update_checked_queue_set_count(self): # self._checked_queue_set_count = \ # sum([len(n) for n in self._checked_queue_set.values()]) self._checked_queue_set_count = len(self._checked_queue_set) self.btnOrder.setText( f'Order ({self._checked_queue_set_count} unique)') # noinspection PyUnusedLocal @pyqtSlot() def _collect_sources_filters(self): main_filters = self._main_filters.filters() if not main_filters: main_filters = [] # main_filters_json = self._main_filters.filters_as_json() group_filters: PlanetFilterMixin = \ self._filter_widget(self.cmbBoxItemGroupType.currentIndex()) self._sources = group_filters.sources() self._sort_order = group_filters.sort_order() item_filters = group_filters.filters() if not item_filters: item_filters = [] # item_filters_json = item_filters.filters_as_json() all_filters = main_filters + item_filters # Merge main and item filters self._filters = and_filter(*all_filters) # TODO: Validate filters # noinspection PyUnusedLocal @pyqtSlot(bool) def perform_search(self, clicked=True): log.debug('Search initiated') # Remove highlight on search button self._toggle_search_highlight(False) resource_type = self.cmbBoxItemGroupType.currentData() if resource_type == RESOURCE_DAILY: self._collect_sources_filters() if not self._main_filters.leAOI.text(): self.show_message('No area of interest (AOI) defined', level=Qgis.Warning, duration=10) return # TODO: Also validate GeoJSON prior to performing search # TODO: replace hardcoded item type with dynamic item types search_request = build_search_request(self._filters, self._sources) self._request = search_request if is_segments_write_key_valid(): analytics.track(self.p_client.user()["email"], "Daily images search executed", {"query": search_request}) # self.msg_log.logMessage( # f"Request:\n" \ # f"<pre>{json.dumps(self._request, indent=2)}</pre>", # LOG_NAME) # Create new results tab, in results tab viewer, passing in request wdgt = PlanetSearchResultsWidget( parent=self.tabWidgetResults, iface=self._iface, api_key=self.api_key, request_type=resource_type, request=search_request, sort_order=self._sort_order, ) wdgt.checkedCountChanged.connect(self._update_checked_queue_set) wdgt.setAOIRequested.connect(self.set_aoi_from_request) wdgt.setSearchParamsRequested.connect( self.set_search_params_from_request) wdgt.zoomToAOIRequested.connect( self._prepare_for_zoom_to_search_aoi) self.frameResults.setStyleSheet(RESULTS_BKGRD_WHITE) self.tabWidgetResults.setUpdatesEnabled(False) self.tabWidgetResults.addTab(wdgt, 'Daily') self.tabWidgetResults.setUpdatesEnabled(True) self.tabWidgetResults.setCurrentWidget(wdgt) # search_results = self.p_client.quick_search(search_request) if resource_type == RESOURCE_SINGLE_MOSAICS: search_text = self.leMosaicName.text() if is_segments_write_key_valid(): analytics.track(self.p_client.user()["email"], "Mosaics search executed", {"text": search_text}) self.listSingleMosaics.populate(search_text) @pyqtSlot(dict, tuple) def set_search_params_from_request(self, request, sort_order): for filter_widget in self._filter_widget_registry.values(): filter_widget.set_from_request(request) filter_widget.set_sort_order(sort_order) self._main_filters.set_from_request(request) @pyqtSlot(dict) def set_aoi_from_request(self, request): self._main_filters.set_from_request(request) @pyqtSlot() def _prepare_for_zoom_to_search_aoi(self): active = self.tabWidgetResults.currentIndex() for i in range(self.tabWidgetResults.count()): if i != active: wdgt = self.tabWidgetResults.widget(i) wdgt.clear_aoi_box() wdgt = self.tabWidgetResults.widget(active) self._main_filters.hide_aoi_if_matches_geom(wdgt.aoi_geom()) @pyqtSlot() def _prepare_for_zoom_to_main_aoi(self): wdgt = self.tabWidgetResults.widget( self.tabWidgetResults.currentIndex()) if wdgt: wdgt.hide_aoi_if_matches_geom(self._main_filters.aoi_geom()) @pyqtSlot(int) def tab_close_requested(self, indx): wdgt: Optional[PlanetSearchResultsWidget] = \ self.tabWidgetResults.widget(indx) if wdgt and hasattr(wdgt, 'clean_up'): wdgt.clean_up() self.tabWidgetResults.removeTab(indx) self._update_checked_queue_set() if self.tabWidgetResults.count() == 0: self.frameResults.setStyleSheet(RESULTS_BKGRD_PE) def _collect_checked_nodes(self): tab_queues = [] for i in range(self.tabWidgetResults.count()): # noinspection PyTypeChecker wdgt: PlanetSearchResultsWidget = self.tabWidgetResults.widget(i) tab_queues.append(wdgt.checked_queue()) # Per-tab checked_queue() are a 1-to-1 dict of {'item_type:id': node} # An item_type:id may be checked in trees across multiple tabs. # Use a ChainMap view to resolve dupes and for faster iteration; # because, although the nodes in each tree's model may be different # (e.g. index), the actual metadata and thumbnail are the same. # This means the earliest found item_type:id will be used regardless # of the which tabs it is checked in. tab_queue_chainmap = ChainMap(*tab_queues) # _checked_queue_set is a set of unique, checked item_type:item_id keys # A dict of {'item_type': [nodes]} self._checked_item_type_nodes.clear() for chkd_it_id in sorted(self._checked_queue_set, reverse=True): if ':' not in chkd_it_id: log.debug(f'Item type:id is not valid') continue it_type = chkd_it_id.split(':')[0] if it_type not in self._checked_item_type_nodes: self._checked_item_type_nodes[it_type] = [] if chkd_it_id in tab_queue_chainmap: self._checked_item_type_nodes[it_type].append( tab_queue_chainmap[chkd_it_id]) else: # This should not happen log.debug('Item type:id in checked queue, but NOT in any tab') # Now sort each item_type's node list by date acquired, descending, # even though tabs may have been sorted acquired|published, asc|desc for item_type in self._checked_item_type_nodes: self._checked_item_type_nodes[item_type].sort( key=attrgetter('_acquired'), reverse=True) # When using with {'item_type': set(nodes)} if LOG_VERBOSE: for it_id in self._checked_item_type_nodes: # Possibly super verbose output... nl = '\n' i_types = \ [n.item_id() for n in self._checked_item_type_nodes[it_id]] log.debug(f'\n - {it_id}: ' f'{len(self._checked_item_type_nodes[it_id])}\n' f' - {"{0} - ".format(nl).join(i_types)}') @pyqtSlot() def show_orders_monitor_dialog(self): dlg = PlanetOrdersMonitorDialog(p_client=self.p_client, parent=self) #dlg.setMinimumWidth(700) dlg.setMinimumHeight(750) dlg.exec_() @pyqtSlot() def order_checked(self): log.debug('Order initiated') self._collect_checked_nodes() if not self._checked_item_type_nodes: self.show_message(f'No checked items to order', level=Qgis.Warning, duration=10) return tool_resources = {} if self._main_filters.leAOI.text(): tool_resources['aoi'] = self._main_filters.leAOI.text() else: tool_resources['aoi'] = None dlg = PlanetOrdersDialog( self._checked_item_type_nodes, p_client=self.p_client, tool_resources=tool_resources, parent=self, iface=self._iface, ) dlg.setMinimumWidth(700) dlg.setMinimumHeight(750) dlg.exec_() @pyqtSlot() def add_preview_layer(self): # TODO: Once checked IDs are avaiable log.debug('Preview layer added to map') @pyqtSlot() def copy_checked_ids(self): if not self._checked_queue_set: self.show_message('No checked IDs to copy', level=Qgis.Warning, duration=10) return sorted_checked = sorted(self._checked_queue_set) cb = QgsApplication.clipboard() cb.setText(','.join(sorted_checked)) self.show_message('Checked IDs copied to clipboard') @pyqtSlot() def copy_curl(self): # TODO: Once checked IDs are avaiable self.show_message('cURL command copied to clipboard') @pyqtSlot() def copy_api_key(self): cb = QgsApplication.clipboard() cb.setText(self.p_client.api_key()) self.show_message('API key copied to clipboard') def _setup_mosaics_panel(self): self.treeMosaicSeries = MosaicSeriesTreeWidget(self) self.grpBoxSeries.layout().addWidget(self.treeMosaicSeries) self.listSingleMosaics = MosaicsListWidget(self) self.grpBoxSingleMosaics.layout().addWidget(self.listSingleMosaics) self.leMosaicName.textChanged.connect(self._filters_have_changed) def _setup_group_type_selector(self): self.cmbBoxItemGroupType.clear() for i in range(len(ITEM_GROUPS)): self.cmbBoxItemGroupType.insertItem( i, ITEM_GROUPS[i]['display_name'], userData=ITEM_GROUPS[i]['resource_type']) def _setup_main_filters(self): """Main filters: AOI visual extent, date range and text""" self._main_filters = PlanetMainFilters(parent=self.grpBoxMainFilters, plugin=self, iface=self._iface) self.grpBoxMainFilters.layout().addWidget(self._main_filters) self._main_filters.filtersChanged.connect(self._filters_have_changed) self._main_filters.zoomToAOIRequested.connect( self._prepare_for_zoom_to_main_aoi) def _setup_filter_widgets(self): """Filters related to item groups""" for i in range(len(ITEM_GROUPS)): wdgt = ITEM_GROUPS[i]['filter_widget'] if wdgt is not None: self._filter_widget_registry[i] = \ wdgt( parent=self.stckdWidgetFilters, plugin=self ) self._filter_widget(i).filtersChanged.connect( self._filters_have_changed) self.stckdWidgetFilters.addWidget(self._filter_widget(i)) def _filter_widget(self, indx): if indx in self._filter_widget_registry: return self._filter_widget_registry[indx] else: log.debug('Item group type filter widget not found') def _setup_cog_button(self): cog_menu = QMenu(self) # add_menu_section_action('Previews', cog_menu) # # add_prev_act = QAction('Add preview layer with checked', # cog_menu) # add_prev_act.triggered[bool].connect(self.add_preview_layer) # cog_menu.addAction(add_prev_act) add_menu_section_action('API', cog_menu) copy_menu = cog_menu.addMenu('Copy to clipboard') ids_act = QAction('Checked item IDs', cog_menu) ids_act.triggered[bool].connect(self.copy_checked_ids) copy_menu.addAction(ids_act) # curl_act = QAction('cURL command', cog_menu) # curl_act.triggered[bool].connect(self.copy_curl) # copy_menu.addAction(curl_act) api_act = QAction('API key', cog_menu) api_act.triggered[bool].connect(self.copy_api_key) copy_menu.addAction(api_act) self.btnCog.setMenu(cog_menu) # Also show menu on click, to keep disclosure triangle visible self.btnCog.clicked.connect(self.btnCog.showMenu) def _setup_info_button(self): info_menu = QMenu(self) self._p_sec_act = add_menu_section_action('Planet', info_menu) p_com_act = QAction(QIcon(EXT_LINK), 'planet.com', info_menu) p_com_act.triggered[bool].connect( lambda: self._open_link_with_browser(PLANET_COM)) info_menu.addAction(p_com_act) p_explorer_act = QAction(QIcon(EXT_LINK), 'Planet Explorer web app', info_menu) p_explorer_act.triggered[bool].connect( lambda: self._open_link_with_browser(PLANET_EXPLORER)) info_menu.addAction(p_explorer_act) p_sat_act = QAction(QIcon(EXT_LINK), 'Satellite specs PDF', info_menu) p_sat_act.triggered[bool].connect( lambda: self._open_link_with_browser(SAT_SPECS_PDF)) info_menu.addAction(p_sat_act) p_support_act = QAction(QIcon(EXT_LINK), 'Support Community', info_menu) p_support_act.triggered[bool].connect( lambda: self._open_link_with_browser(PLANET_SUPPORT_COMMUNITY)) info_menu.addAction(p_support_act) self._info_act = add_menu_section_action('Documentation', info_menu) terms_act = QAction('Terms', info_menu) terms_act.triggered[bool].connect(self._show_terms) info_menu.addAction(terms_act) self.btnInfo.setMenu(info_menu) # Also show menu on click, to keep disclosure triangle visible self.btnInfo.clicked.connect(self.btnInfo.showMenu) def _setup_user_button(self): user_menu = QMenu(self) # user_menu.aboutToShow.connect(self.p_client.update_user_quota) self._user_act = add_menu_section_action('User', user_menu) acct_act = QAction(QIcon(EXT_LINK), 'Account', user_menu) acct_act.triggered[bool].connect( lambda: self._open_link_with_browser(ACCOUNT_URL)) user_menu.addAction(acct_act) # quota_menu = user_menu.addMenu('Quota (sqkm)') # quota_menu.addAction( # f'Enabled: {str(self.p_client.user_quota_enabled())}') # quota_menu.addAction( # f'Size: {str(self.p_client.user_quota_size())}') # quota_menu.addAction( # f'Used: {str(self.p_client.user_quota_used())}') # quota_menu.addAction( # f'Remaining: {str(self.p_client.user_quota_remaining())}') # quota_act.setMenu(quota_menu) # user_menu.addAction(quota_act) logout_act = QAction('Logout', user_menu) logout_act.triggered[bool].connect(self.logout) user_menu.addAction(logout_act) add_menu_section_action('Orders', user_menu) monitor_orders_act = QAction('Monitor orders', user_menu) monitor_orders_act.triggered[bool].connect( self.show_orders_monitor_dialog) user_menu.addAction(monitor_orders_act) open_orders_folder_act = QAction('Open orders folder', user_menu) open_orders_folder_act.triggered[bool].connect( open_orders_download_folder) user_menu.addAction(open_orders_folder_act) self.btnUser.setMenu(user_menu) # Also show menu on click, to keep disclosure triangle visible self.btnUser.clicked.connect(self.btnUser.showMenu) @pyqtSlot() def _filters_have_changed(self): """ Main slot for when any filter value has changed. Planet API searches should not be initiated automatically on filter changes (i.e. here), but when the user clicks the search button. :return: """ self._toggle_search_highlight(True) log.debug('Filters have changed') # TODO: Fix signal-triggered collection of filters # self._collect_sources_filters() @pyqtSlot('QString', str, 'PyQt_PyObject', 'PyQt_PyObject') def _passthru_message(self, msg, level, duration, show_more): if level == 'Warning': qgis_level = Qgis.Warning elif level == 'Critical': qgis_level = Qgis.Critical elif level == 'Success': qgis_level = Qgis.Success else: # default qgis_level = Qgis.Info self.show_message(msg, level=qgis_level, duration=duration, show_more=show_more) def show_message(self, message, level=Qgis.Info, duration=None, show_more=None): """Skips bold title, i.e. sets first param (below) to empty string""" if duration is None: duration = self._iface.messageTimeout() if show_more is not None: self.msgBar.pushMessage('', message, show_more, level, duration) else: self.msgBar.pushMessage('', message, level, duration) @pyqtSlot(int) def save_credentials_changed(self, state): if state == 0: self._remove_auth_creds() self._save_creds = state > 0 setPluginSetting(SAVE_CREDS_KEY, self._save_creds, namespace=SETTINGS_NAMESPACE) def _store_auth_creds(self): auth_creds_str = AUTH_STRING.format(user=self.leUser.text(), password=self.lePass.text(), api_key=self.p_client.api_key(), sep=AUTH_SEP) self._auth_man.storeAuthSetting(AUTH_CREDS_KEY, auth_creds_str, True) def _retrieve_auth_creds(self): auth_creds_str = self._auth_man.authSetting(AUTH_CREDS_KEY, defaultValue='', decrypt=True) creds = auth_creds_str.split(AUTH_SEP) return { 'user': creds[0] if len(creds) > 0 else None, 'password': creds[1] if len(creds) > 1 else None, 'api_key': creds[2] if len(creds) > 2 else None } def _set_credential_fields(self): self.lePass.setPasswordVisibility(False) if not self._save_creds: self.chkBxSaveCreds.setChecked(False) else: self.chkBxSaveCreds.setChecked(True) auth_creds = self._retrieve_auth_creds() self.leUser.setText(auth_creds['user']) self.lePass.setText(auth_creds['password']) self.api_key = auth_creds['api_key'] def _remove_auth_creds(self): if not self._auth_man.removeAuthSetting(AUTH_CREDS_KEY): self.show_message('Credentials setting removal failed', level=Qgis.Warning, duration=10) # noinspection PyUnusedLocal @pyqtSlot(bool) def _show_terms(self, _): if self._terms_browser is None: self._terms_browser = QTextBrowser() self._terms_browser.setReadOnly(True) self._terms_browser.setOpenExternalLinks(True) self._terms_browser.setMinimumSize(600, 700) # TODO: Template terms.html first section, per subscription level # Collect subscription info from self.p_client.user self._terms_browser.setSource( QUrl('qrc:/plugins/planet_explorer/terms.html')) self._terms_browser.setWindowModality(Qt.ApplicationModal) self._terms_browser.show() @pyqtSlot(str) def _open_link_with_browser(self, url): QDesktopServices.openUrl(QUrl(url)) def clean_up(self): self._main_filters.clean_up() for i in range(self.tabWidgetResults.count()): wdgt = self.tabWidgetResults.widget(i) if wdgt and hasattr(wdgt, 'clean_up'): wdgt.clean_up() def closeEvent(self, event): self.clean_up() self.closingPlugin.emit() event.accept()
class PlanetExplorer(object): def __init__(self, iface): self.iface = iface # Initialize plugin directory self.plugin_dir = os.path.dirname(__file__) # Initialize locale locale = QSettings().value('locale/userLocale')[0:2] locale_path = os.path.join( self.plugin_dir, 'i18n', '{0}Plugin_{1}.qm'.format(PE, locale) ) if os.path.exists(locale_path): self.translator = QTranslator() self.translator.load(locale_path) QCoreApplication.installTranslator(self.translator) # Declare instance attributes self.actions = [] self.menu = self.tr('&{0}'.format(P_E)) self.toolbar = None # noinspection PyTypeChecker self.explorer_dock_widget = None self._terms_browser = None readSettings() if is_segments_write_key_valid(): analytics.write_key = segments_write_key() if is_sentry_dsn_valid(): sentry_sdk.init(sentry_dsn(), default_integrations=False) self.qgis_hook = sys.excepthook def plugin_hook(t, value, tb): trace = "".join(traceback.format_exception(t, value, tb)) if PLUGIN_NAMESPACE in trace.lower(): try: sentry_sdk.capture_exception(value) except: pass # we swallow all exceptions here, to avoid entering an endless loop self.qgis_hook(t, value, tb) sys.excepthook = plugin_hook metadataFile = os.path.join(os.path.dirname(__file__), "metadata.txt") cp = configparser.ConfigParser() with codecs.open(metadataFile, "r", "utf8") as f: cp.read_file(f) if is_sentry_dsn_valid(): version = cp["general"]["version"] with sentry_sdk.configure_scope() as scope: scope.set_context("plugin_version", version) scope.set_context("qgis_version", Qgis.QGIS_VERSION) # noinspection PyMethodMayBeStatic def tr(self, message): """Get the translation for a string using Qt translation API. We implement this ourselves since we do not inherit QObject. :param message: String for translation. :type message: str, QString :returns: Translated version of message. :rtype: QString """ # noinspection PyTypeChecker,PyArgumentList,PyCallByClass return QCoreApplication.translate(PE, message) def add_action( self, icon_path, text, callback, enabled_flag=True, add_to_menu=True, add_to_toolbar=True, status_tip=None, whats_this=None, parent=None): """Add a toolbar icon to the toolbar. :param icon_path: Path to the icon for this action. Can be a resource path (e.g. ':/plugins/foo/bar.png') or a normal file system path. :type icon_path: str :param text: Text that should be shown in menu items for this action. :type text: str :param callback: Function to be called when the action is triggered. :type callback: function :param enabled_flag: A flag indicating if the action should be enabled by default. Defaults to True. :type enabled_flag: bool :param add_to_menu: Flag indicating whether the action should also be added to the menu. Defaults to True. :type add_to_menu: bool :param add_to_toolbar: Flag indicating whether the action should also be added to the toolbar. Defaults to True. :type add_to_toolbar: bool :param status_tip: Optional text to show in a popup when mouse pointer hovers over the action. :type status_tip: str :param parent: Parent widget for the new action. Defaults None. :type parent: QWidget :param whats_this: Optional text to show in the status bar when the mouse pointer hovers over the action. :returns: The action that was created. Note that the action is also added to self.actions list. :rtype: QAction """ icon = QIcon(icon_path) action = QAction(icon, text, parent) action.triggered.connect(callback) if add_to_toolbar: self.toolbar.addAction(action) if add_to_menu: self.iface.addPluginToWebMenu( self.menu, action) self.actions.append(action) return action # noinspection PyPep8Naming def initGui(self): self.toolbar = self.iface.addToolBar(P_E) self.toolbar.setObjectName(P_E) self.showdailyimages_act = self.add_action( os.path.join(plugin_path, "resources", "search.svg"), text=self.tr(P_E), callback=toggle_images_search, add_to_menu=True, add_to_toolbar=True, parent=self.iface.mainWindow()) self.showbasemaps_act = self.add_action( os.path.join(plugin_path, "resources", "basemap.svg"), text=self.tr("Show Basemaps Search"), callback=toggle_mosaics_search, add_to_menu=True, add_to_toolbar=True, parent=self.iface.mainWindow()) self.showinspector_act = self.add_action( os.path.join(plugin_path, "resources", "inspector.svg"), text=self.tr("Show Planet Inspector..."), callback=toggle_inspector, add_to_menu=False, add_to_toolbar=True, parent=self.iface.mainWindow()) self.showtasking_act = self.add_action( os.path.join(plugin_path, "resources", "tasking.svg"), text=self.tr("Show Tasking..."), callback=toggle_tasking_widget, add_to_menu=False, add_to_toolbar=True, parent=self.iface.mainWindow()) self.add_central_toolbar_button() self.showorders_act = self.add_action( os.path.join(plugin_path, "resources", "orders.svg"), text=self.tr("Show Orders Monitor..."), callback=toggle_orders_monitor, add_to_menu=False, add_to_toolbar=True, parent=self.iface.mainWindow()) self.add_user_button() self.add_info_button() addSettingsMenu(P_E, self.iface.addPluginToWebMenu) addAboutMenu(P_E, self.iface.addPluginToWebMenu) self.provider = BasemapLayerWidgetProvider() QgsGui.layerTreeEmbeddedWidgetRegistry().addProvider(self.provider) QgsProject.instance().projectSaved.connect(self.project_saved) QgsProject.instance().layersAdded.connect(self.layers_added) QgsProject.instance().layerRemoved.connect(self.layer_removed) PlanetClient.getInstance().loginChanged.connect(self.login_changed) self.enable_buttons(False) def add_central_toolbar_button(self): widget = QWidget() widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) layout = QHBoxLayout() layout.addStretch() self.btnLogin = QPushButton() palette = self.btnLogin.palette() palette.setColor(QPalette.Button, PLANET_COLOR) self.btnLogin.setPalette(palette) self.btnLogin.setText("Log in") #self.btnLogin.setAutoRaise(True) self.btnLogin.setAttribute(Qt.WA_TranslucentBackground) self.btnLogin.clicked.connect(self.btn_login_clicked) icon = QIcon(os.path.join(plugin_path, "resources", "planet-logo-p.svg")) labelIcon = QLabel() labelIcon.setPixmap(icon.pixmap(QSize(16, 16))) layout.addWidget(labelIcon) self.labelLoggedIn = QLabel() self.labelLoggedIn.setText("") layout.addWidget(self.labelLoggedIn) layout.addWidget(self.btnLogin) layout.addStretch() widget.setLayout(layout) self.toolbar.addWidget(widget) def btn_login_clicked(self): if PlanetClient.getInstance().has_api_key(): self.logout() else: self.login() def layer_removed(self, layer): self.provider.layerWasRemoved(layer) def layers_added(self, layers): for layer in layers: add_widget_to_layer(layer) def login_changed(self, loggedin): self.provider.updateLayerWidgets() self.enable_buttons(loggedin) if not loggedin: hide_orders_monitor() hide_inspector() def add_info_button(self): info_menu = QMenu() p_sec_act = add_menu_section_action('Planet', info_menu) p_com_act = QAction(QIcon(EXT_LINK), 'planet.com', info_menu) p_com_act.triggered[bool].connect( lambda: open_link_with_browser(PLANET_COM) ) info_menu.addAction(p_com_act) p_explorer_act = QAction(QIcon(EXT_LINK), 'Planet Explorer web app', info_menu) p_explorer_act.triggered[bool].connect( lambda: open_link_with_browser(PLANET_EXPLORER) ) info_menu.addAction(p_explorer_act) p_sat_act = QAction(QIcon(EXT_LINK), 'Satellite specs PDF', info_menu) p_sat_act.triggered[bool].connect( lambda: open_link_with_browser(SAT_SPECS_PDF) ) info_menu.addAction(p_sat_act) p_support_act = QAction(QIcon(EXT_LINK), 'Support Community', info_menu) p_support_act.triggered[bool].connect( lambda: open_link_with_browser(PLANET_SUPPORT_COMMUNITY) ) info_menu.addAction(p_support_act) p_whatsnew_act = QAction(QIcon(EXT_LINK), "What's new", info_menu) p_whatsnew_act.triggered[bool].connect( lambda: open_link_with_browser(PLANET_INTEGRATIONS) ) info_menu.addAction(p_whatsnew_act) p_sales_act = QAction(QIcon(EXT_LINK), "Sales", info_menu) p_sales_act.triggered[bool].connect( lambda: open_link_with_browser(PLANET_SALES) ) info_menu.addAction(p_sales_act) add_menu_section_action('Documentation', info_menu) terms_act = QAction('Terms', info_menu) terms_act.triggered[bool].connect(self.show_terms) info_menu.addAction(terms_act) btn = QToolButton() btn.setIcon(QIcon(os.path.join(plugin_path, "resources", "info.svg"),)) btn.setMenu(info_menu) btn.setPopupMode(QToolButton.MenuButtonPopup) # Also show menu on click, to keep disclosure triangle visible btn.clicked.connect(btn.showMenu) self.toolbar.addWidget(btn) def add_user_button(self): user_menu = QMenu() self.acct_act = QAction(QIcon(EXT_LINK), 'Account', user_menu) self.acct_act.triggered[bool].connect( lambda: QDesktopServices.openUrl(QUrl(ACCOUNT_URL)) ) user_menu.addAction(self.acct_act) self.logout_act = QAction('Logout', user_menu) self.logout_act.triggered[bool].connect(self.logout) user_menu.addAction(self.logout_act) self.user_button = QToolButton() self.user_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon); self.user_button.setIcon(QIcon(os.path.join(plugin_path, "resources", "account.svg"),)) self.user_button.setMenu(user_menu) self.user_button.setPopupMode(QToolButton.MenuButtonPopup) # Also show menu on click, to keep disclosure triangle visible self.user_button.clicked.connect(self.user_button.showMenu) self.toolbar.addWidget(self.user_button) def unload(self): """Removes the plugin menu item and icon from QGIS GUI.""" PlanetClient.getInstance().log_out() self.provider.updateLayerWidgets() removeSettingsMenu(P_E, self.iface.removePluginWebMenu) # removeHelpMenu(P_E, self.iface.removePluginWebMenu) removeAboutMenu(P_E, self.iface.removePluginWebMenu) for action in self.actions: self.iface.removePluginWebMenu( self.tr('&{0}'.format(P_E)), action) self.iface.removeToolBarIcon(action) # remove the toolbar if self.toolbar is not None: del self.toolbar remove_inspector() remove_explorer() remove_orders_monitor() remove_tasking_widget() sys.excepthook = self.qgis_hook QgsProject.instance().projectSaved.disconnect(self.project_saved) QgsProject.instance().layersAdded.disconnect(self.layers_added) QgsProject.instance().layerRemoved.disconnect(self.layer_removed) # ----------------------------------------------------------- def show_terms(self, _): if self._terms_browser is None: self._terms_browser = QTextBrowser() self._terms_browser.setReadOnly(True) self._terms_browser.setOpenExternalLinks(True) self._terms_browser.setMinimumSize(600, 700) # TODO: Template terms.html first section, per subscription level # Collect subscription info from self.p_client.user self._terms_browser.setSource( QUrl('qrc:/plugins/planet_explorer/terms.html')) self._terms_browser.setWindowModality(Qt.ApplicationModal) self._terms_browser.show() def login(self): show_explorer() def logout(self): PlanetClient.getInstance().log_out() def enable_buttons(self, loggedin): self.btnLogin.setVisible(not loggedin) labelText = (f"<b>Welcome to Planet</b>" if not loggedin else "<b>Planet</b>") self.labelLoggedIn.setText(labelText) self.showdailyimages_act.setEnabled(loggedin) self.showbasemaps_act.setEnabled(loggedin) self.showinspector_act.setEnabled(loggedin) self.showorders_act.setEnabled(loggedin) self.showtasking_act.setEnabled(loggedin) self.user_button.setEnabled(loggedin) self.user_button.setText( PlanetClient.getInstance().user()['user_name'] if loggedin else "") if loggedin: self.showdailyimages_act.setToolTip("Show / Hide the Planet Imagery Search Panel") self.showbasemaps_act.setToolTip("Show / Hide the Planet Basemaps Search Panel") self.showorders_act.setToolTip("Show / Hide the Order Status Panel") self.showinspector_act.setToolTip("Show / Hide the Planet Inspector Panel") self.showtasking_act.setToolTip("Show / Hide the Tasking Panel") else: self.showdailyimages_act.setToolTip("Login to access Imagery Search") self.showbasemaps_act.setToolTip("Login to access Basemaps Search") self.showorders_act.setToolTip("Login to access Order Status") self.showinspector_act.setToolTip("Login to access Planet Inspector") self.showtasking_act.setToolTip("Login to access Tasking Panel") def project_saved(self): if PlanetClient.getInstance().has_api_key(): def resave(): path = QgsProject.instance().absoluteFilePath() if path.lower().endswith(".qgs"): with open(path) as f: s = f.read() with open(path, "w") as f: f.write(s.replace(PlanetClient.getInstance().api_key(), "")) else: tmpfilename = path + ".temp" qgsfilename = os.path.splitext(os.path.basename(path))[0] + ".qgs" with zipfile.ZipFile(path, 'r') as zin: with zipfile.ZipFile(tmpfilename, 'w') as zout: zout.comment = zin.comment for item in zin.infolist(): if not item.filename.lower().endswith(".qgs"): zout.writestr(item, zin.read(item.filename)) else: s = zin.read(item.filename).decode("utf-8") s = s.replace(PlanetClient.getInstance().api_key(), "") qgsfilename = item.filename os.remove(path) os.rename(tmpfilename, path) with zipfile.ZipFile(path, mode='a', compression=zipfile.ZIP_DEFLATED) as zf: zf.writestr(qgsfilename, s) QTimer.singleShot(100, resave)
class PlanetExplorer(object): def __init__(self, iface): self.iface = iface # Initialize plugin directory self.plugin_dir = os.path.dirname(__file__) # Initialize locale locale = QSettings().value("locale/userLocale")[0:2] locale_path = os.path.join( self.plugin_dir, "i18n", "{0}Plugin_{1}.qm".format(PE, locale) ) if os.path.exists(locale_path): self.translator = QTranslator() self.translator.load(locale_path) QCoreApplication.installTranslator(self.translator) # Declare instance attributes self.actions = [] self.menu = self.tr("&{0}".format(P_E)) self.toolbar = None # noinspection PyTypeChecker self.explorer_dock_widget = None self._terms_browser = None if is_segments_write_key_valid(): analytics.write_key = segments_write_key() if is_sentry_dsn_valid(): try: sentry_sdk.init(sentry_dsn(), release=plugin_version(True)) sentry_sdk.set_context( "qgis", { "type": "runtime", "name": Qgis.QGIS_RELEASE_NAME, "version": Qgis.QGIS_VERSION, }, ) system = platform.system() if system == "Darwin": sentry_sdk.set_context( "mac", { "type": "os", "name": "macOS", "version": platform.mac_ver()[0], "kernel_version": platform.uname().release, }, ) if system == "Linux": sentry_sdk.set_context( "linux", { "type": "os", "name": "Linux", "version": platform.release(), "build": platform.version(), }, ) if system == "Windows": sentry_sdk.set_context( "windows", { "type": "os", "name": "Windows", "version": platform.version(), }, ) except Exception: QMessageBox.warning( self.iface.mainWindow(), "Error", "Error initializing Planet Explorer.\n" "Please restart QGIS to load updated libraries.", ) self.qgis_hook = sys.excepthook def plugin_hook(t, value, tb): trace = "".join(traceback.format_exception(t, value, tb)) if PLUGIN_NAMESPACE in trace.lower(): s = "" if issubclass(t, exceptions.Timeout): s = "Connection to Planet server timed out." elif issubclass(t, exceptions.ConnectionError): s = ( "Connection error.\n Verify that your computer is correctly" " connected to the Internet" ) elif issubclass(t, (exceptions.ProxyError, exceptions.InvalidProxyURL)): s = ( "ProxyError.\n Verify that your proxy is correctly configured" " in the QGIS settings" ) elif issubclass(t, planet.api.exceptions.ServerError): s = "Server Error.\n Please, try again later" elif issubclass(t, urllib3.exceptions.ProxySchemeUnknown): s = ( "Proxy Error\n Proxy URL must start with 'http://' or" " 'https://'" ) if s: QMessageBox.warning(self.iface.mainWindow(), "Error", s) else: try: sentry_sdk.capture_exception(value) except Exception: pass # we swallow all exceptions here, to avoid entering an endless loop self.qgis_hook(t, value, tb) else: self.qgis_hook(t, value, tb) sys.excepthook = plugin_hook def tr(self, message): """Get the translation for a string using Qt translation API. We implement this ourselves since we do not inherit QObject. :param message: String for translation. :type message: str, QString :returns: Translated version of message. :rtype: QString """ # noinspection PyTypeChecker,PyArgumentList,PyCallByClass return QCoreApplication.translate(PE, message) def add_action( self, icon_path, text, callback, enabled_flag=True, add_to_menu=True, add_to_toolbar=True, status_tip=None, whats_this=None, parent=None, ): """Add a toolbar icon to the toolbar. :param icon_path: Path to the icon for this action. Can be a resource path (e.g. ':/plugins/foo/bar.png') or a normal file system path. :type icon_path: str :param text: Text that should be shown in menu items for this action. :type text: str :param callback: Function to be called when the action is triggered. :type callback: function :param enabled_flag: A flag indicating if the action should be enabled by default. Defaults to True. :type enabled_flag: bool :param add_to_menu: Flag indicating whether the action should also be added to the menu. Defaults to True. :type add_to_menu: bool :param add_to_toolbar: Flag indicating whether the action should also be added to the toolbar. Defaults to True. :type add_to_toolbar: bool :param status_tip: Optional text to show in a popup when mouse pointer hovers over the action. :type status_tip: str :param parent: Parent widget for the new action. Defaults None. :type parent: QWidget :param whats_this: Optional text to show in the status bar when the mouse pointer hovers over the action. :returns: The action that was created. Note that the action is also added to self.actions list. :rtype: QAction """ icon = QIcon(icon_path) action = QAction(icon, text, parent) action.triggered.connect(callback) if add_to_toolbar: self.toolbar.addAction(action) if add_to_menu: self.iface.addPluginToWebMenu(self.menu, action) self.actions.append(action) return action # noinspection PyPep8Naming def initGui(self): self.toolbar = self.iface.addToolBar(P_E) self.toolbar.setObjectName(P_E) self.showdailyimages_act = self.add_action( os.path.join(plugin_path, "resources", "search.svg"), text=self.tr(P_E), callback=toggle_images_search, add_to_menu=True, add_to_toolbar=True, parent=self.iface.mainWindow(), ) self.showbasemaps_act = self.add_action( os.path.join(plugin_path, "resources", "basemap.svg"), text=self.tr("Show Basemaps Search"), callback=toggle_mosaics_search, add_to_menu=True, add_to_toolbar=True, parent=self.iface.mainWindow(), ) self.showinspector_act = self.add_action( os.path.join(plugin_path, "resources", "inspector.svg"), text=self.tr("Show Planet Inspector..."), callback=toggle_inspector, add_to_menu=False, add_to_toolbar=True, parent=self.iface.mainWindow(), ) self.showtasking_act = self.add_action( os.path.join(plugin_path, "resources", "tasking.svg"), text=self.tr("Show Tasking..."), callback=toggle_tasking_widget, add_to_menu=False, add_to_toolbar=True, parent=self.iface.mainWindow(), ) self.add_central_toolbar_button() self.showorders_act = self.add_action( os.path.join(plugin_path, "resources", "orders.svg"), text=self.tr("Show Orders Monitor..."), callback=toggle_orders_monitor, add_to_menu=False, add_to_toolbar=True, parent=self.iface.mainWindow(), ) self.add_user_button() self.add_info_button() self.settings_act = self.add_action( os.path.join(plugin_path, "resources", "cog.svg"), text=self.tr("Settings..."), callback=self.show_settings, add_to_menu=True, add_to_toolbar=False, parent=self.iface.mainWindow(), ) self.provider = BasemapLayerWidgetProvider() QgsGui.layerTreeEmbeddedWidgetRegistry().addProvider(self.provider) QgsProject.instance().projectSaved.connect(self.project_saved) QgsProject.instance().layersAdded.connect(self.layers_added) QgsProject.instance().layerRemoved.connect(self.layer_removed) PlanetClient.getInstance().loginChanged.connect(self.login_changed) self.enable_buttons(False) def add_central_toolbar_button(self): widget = QWidget() widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) layout = QHBoxLayout() layout.addStretch() self.btnLogin = QPushButton() palette = self.btnLogin.palette() palette.setColor(QPalette.Button, PLANET_COLOR) self.btnLogin.setPalette(palette) self.btnLogin.setText("Log in") # self.btnLogin.setAutoRaise(True) self.btnLogin.setAttribute(Qt.WA_TranslucentBackground) self.btnLogin.clicked.connect(self.btn_login_clicked) icon = QIcon(os.path.join(plugin_path, "resources", "planet-logo-p.svg")) labelIcon = QLabel() labelIcon.setPixmap(icon.pixmap(QSize(16, 16))) layout.addWidget(labelIcon) self.labelLoggedIn = QLabel() self.labelLoggedIn.setText("") layout.addWidget(self.labelLoggedIn) layout.addWidget(self.btnLogin) layout.addStretch() widget.setLayout(layout) self.toolbar.addWidget(widget) def btn_login_clicked(self): if PlanetClient.getInstance().has_api_key(): self.logout() else: self.login() def layer_removed(self, layer): self.provider.layerWasRemoved(layer) def layers_added(self, layers): for layer in layers: add_widget_to_layer(layer) def login_changed(self, loggedin): self.provider.updateLayerWidgets() self.enable_buttons(loggedin) if not loggedin: hide_orders_monitor() hide_inspector() def add_info_button(self): info_menu = QMenu() add_menu_section_action("Planet", info_menu) p_com_act = QAction(QIcon(EXT_LINK), "planet.com", info_menu) p_com_act.triggered[bool].connect(lambda: open_link_with_browser(PLANET_COM)) info_menu.addAction(p_com_act) p_explorer_act = QAction(QIcon(EXT_LINK), "Planet Explorer web app", info_menu) p_explorer_act.triggered[bool].connect( lambda: open_link_with_browser(PLANET_EXPLORER) ) info_menu.addAction(p_explorer_act) p_sat_act = QAction(QIcon(EXT_LINK), "Satellite specs PDF", info_menu) p_sat_act.triggered[bool].connect(lambda: open_link_with_browser(SAT_SPECS_PDF)) info_menu.addAction(p_sat_act) p_support_act = QAction(QIcon(EXT_LINK), "Support Community", info_menu) p_support_act.triggered[bool].connect( lambda: open_link_with_browser(PLANET_SUPPORT_COMMUNITY) ) info_menu.addAction(p_support_act) p_whatsnew_act = QAction(QIcon(EXT_LINK), "What's new", info_menu) p_whatsnew_act.triggered[bool].connect( lambda: open_link_with_browser(PLANET_INTEGRATIONS) ) info_menu.addAction(p_whatsnew_act) p_sales_act = QAction(QIcon(EXT_LINK), "Sales", info_menu) p_sales_act.triggered[bool].connect( lambda: open_link_with_browser(PLANET_SALES) ) info_menu.addAction(p_sales_act) add_menu_section_action("Documentation", info_menu) terms_act = QAction("Terms", info_menu) terms_act.triggered[bool].connect(self.show_terms) info_menu.addAction(terms_act) btn = QToolButton() btn.setIcon( QIcon( os.path.join(plugin_path, "resources", "info.svg"), ) ) btn.setMenu(info_menu) btn.setPopupMode(QToolButton.MenuButtonPopup) # Also show menu on click, to keep disclosure triangle visible btn.clicked.connect(btn.showMenu) self.toolbar.addWidget(btn) def add_user_button(self): user_menu = QMenu() self.acct_act = QAction(QIcon(EXT_LINK), "Account", user_menu) self.acct_act.triggered[bool].connect( lambda: QDesktopServices.openUrl(QUrl(ACCOUNT_URL)) ) user_menu.addAction(self.acct_act) self.logout_act = QAction("Logout", user_menu) self.logout_act.triggered[bool].connect(self.logout) user_menu.addAction(self.logout_act) self.user_button = QToolButton() self.user_button.setToolButtonStyle(Qt.ToolButtonTextBesideIcon) self.user_button.setIcon( QIcon( os.path.join(plugin_path, "resources", "account.svg"), ) ) self.user_button.setMenu(user_menu) self.user_button.setPopupMode(QToolButton.MenuButtonPopup) # Also show menu on click, to keep disclosure triangle visible self.user_button.clicked.connect(self.user_button.showMenu) self.toolbar.addWidget(self.user_button) def unload(self): """Removes the plugin menu item and icon from QGIS GUI.""" PlanetClient.getInstance().log_out() self.provider.updateLayerWidgets() for action in self.actions: self.iface.removePluginWebMenu(self.tr("&{0}".format(P_E)), action) self.iface.removeToolBarIcon(action) # remove the toolbar if self.toolbar is not None: del self.toolbar remove_inspector() remove_explorer() remove_orders_monitor() remove_tasking_widget() QgsGui.layerTreeEmbeddedWidgetRegistry().removeProvider(self.provider.id()) sys.excepthook = self.qgis_hook QgsProject.instance().projectSaved.disconnect(self.project_saved) QgsProject.instance().layersAdded.disconnect(self.layers_added) QgsProject.instance().layerRemoved.disconnect(self.layer_removed) # ----------------------------------------------------------- def show_settings(self): dlg = SettingsDialog() dlg.exec() def show_terms(self, _): if self._terms_browser is None: self._terms_browser = QTextBrowser() self._terms_browser.setReadOnly(True) self._terms_browser.setOpenExternalLinks(True) self._terms_browser.setMinimumSize(600, 700) # TODO: Template terms.html first section, per subscription level # Collect subscription info from self.p_client.user self._terms_browser.setSource( QUrl("qrc:/plugins/planet_explorer/terms.html") ) self._terms_browser.setWindowModality(Qt.ApplicationModal) self._terms_browser.show() def login(self): if Qgis.QGIS_VERSION_INT >= 32000 and platform.system() == "Darwin": text = ( "WARNING: Your configuration may encounter serious issues with the" " Planet QGIS Plugin using QGIS V3.20. We are actively troubleshooting" " the issue with the QGIS team, you can track <a" " href='https://github.com/qgis/QGIS/issues/44182'>Issue 44182" " here</a>. In the meantime, we recommend that you use a QGIS version" " between 3.10 and 3.20, such as the 3.16 long term stable release. For" " further information including instructions on how to downgrade QGIS," " please refer to our <a" " href='https://support.planet.com/hc/en-us/articles/4404372169233'>support" " page here</a>." ) QMessageBox.warning(self.iface.mainWindow(), "Planet Explorer", text) show_explorer() def logout(self): PlanetClient.getInstance().log_out() def enable_buttons(self, loggedin): self.btnLogin.setVisible(not loggedin) labelText = "<b>Welcome to Planet</b>" if not loggedin else "<b>Planet</b>" self.labelLoggedIn.setText(labelText) self.showdailyimages_act.setEnabled(loggedin) self.showbasemaps_act.setEnabled(loggedin) self.showinspector_act.setEnabled(loggedin) self.showorders_act.setEnabled(loggedin) self.showtasking_act.setEnabled(loggedin) self.user_button.setEnabled(loggedin) self.user_button.setText( PlanetClient.getInstance().user()["user_name"] if loggedin else "" ) if loggedin: self.showdailyimages_act.setToolTip( "Show / Hide the Planet Imagery Search Panel" ) self.showbasemaps_act.setToolTip( "Show / Hide the Planet Basemaps Search Panel" ) self.showorders_act.setToolTip("Show / Hide the Order Status Panel") self.showinspector_act.setToolTip("Show / Hide the Planet Inspector Panel") self.showtasking_act.setToolTip("Show / Hide the Tasking Panel") else: self.showdailyimages_act.setToolTip("Login to access Imagery Search") self.showbasemaps_act.setToolTip("Login to access Basemaps Search") self.showorders_act.setToolTip("Login to access Order Status") self.showinspector_act.setToolTip("Login to access Planet Inspector") self.showtasking_act.setToolTip("Login to access Tasking Panel") def project_saved(self): if PlanetClient.getInstance().has_api_key(): def resave(): try: path = QgsProject.instance().absoluteFilePath() if path.lower().endswith(".qgs"): with open(path, encoding="utf-8") as f: s = f.read() with open(path, "w", encoding="utf-8") as f: f.write(s.replace(PlanetClient.getInstance().api_key(), "")) else: tmpfilename = path + ".temp" qgsfilename = ( os.path.splitext(os.path.basename(path))[0] + ".qgs" ) with zipfile.ZipFile(path, "r") as zin: with zipfile.ZipFile(tmpfilename, "w") as zout: zout.comment = zin.comment for item in zin.infolist(): if not item.filename.lower().endswith(".qgs"): zout.writestr(item, zin.read(item.filename)) else: s = zin.read(item.filename).decode("utf-8") s = s.replace( PlanetClient.getInstance().api_key(), "" ) qgsfilename = item.filename os.remove(path) os.rename(tmpfilename, path) with zipfile.ZipFile( path, mode="a", compression=zipfile.ZIP_DEFLATED ) as zf: zf.writestr(qgsfilename, s) except Exception: QMessageBox.warning( self.iface.mainWindow(), "Error saving project", "There was an error while removing API keys from QGIS project" " file.\nThe project that you have just saved might contain" " Planet API keys in plain text.", ) QTimer.singleShot(100, resave)