class QgisCloudPluginDialog(QDockWidget): COLUMN_LAYERS = 0 COLUMN_DATA_SOURCE = 1 COLUMN_TABLE_NAME = 2 COLUMN_GEOMETRY_TYPE = 3 COLUMN_SRID = 4 GEOMETRY_TYPES = { QGis.WKBUnknown: "Unknown", QGis.WKBPoint: "Point", QGis.WKBMultiPoint: "MultiPoint", QGis.WKBLineString: "LineString", QGis.WKBMultiLineString: "MultiLineString", QGis.WKBPolygon: "Polygon", QGis.WKBMultiPolygon: "MultiPolygon", 100: "No geometry", # Workaround (missing Python binding?): QGis.WKBNoGeometry / ogr.wkbNone QGis.WKBPoint25D: "Point", QGis.WKBLineString25D: "LineString", QGis.WKBPolygon25D: "Polygon", QGis.WKBMultiPoint25D: "MultiPoint", QGis.WKBMultiLineString25D: "MultiLineString", QGis.WKBMultiPolygon25D: "MultiPolygon" } def __init__(self, iface, version): QDockWidget.__init__(self, None) self.iface = iface self.clouddb = True self.version = version # Set up the user interface from Designer. self.ui = Ui_QgisCloudPlugin() self.ui.setupUi(self) myAbout = DlgAbout() self.ui.aboutText.setText(myAbout.aboutString() + myAbout.contribString() + myAbout.licenseString() + "<p>Version: " + version + "</p>") self.ui.tblLocalLayers.setColumnCount(5) header = ["Layers", "Data source", "Table name", "Geometry type", "SRID"] self.ui.tblLocalLayers.setHorizontalHeaderLabels(header) self.ui.tblLocalLayers.resizeColumnsToContents() # TODO; delegate for read only columns self.ui.btnUploadData.setEnabled(False) self.ui.uploadProgressBar.hide() self.ui.btnPublishMapUpload.hide() self.ui.btnLogout.hide() self.ui.lblLoginStatus.hide() # map<data source, table name> self.data_sources_table_names = {} # flag to disable update of local data sources during upload self.do_update_local_data_sources = True QObject.connect(self.ui.btnLogin, SIGNAL("clicked()"), self.login) QObject.connect(self.ui.btnDbCreate, SIGNAL("clicked()"), self.create_database) QObject.connect(self.ui.btnDbDelete, SIGNAL("clicked()"), self.delete_database) QObject.connect(self.ui.btnDbRefresh, SIGNAL("clicked()"), self.refresh_databases) QObject.connect(self.ui.tabDatabases, SIGNAL("itemSelectionChanged()"), self.select_database) QObject.connect(self.ui.btnPublishMap, SIGNAL("clicked()"), self.publish_map) QObject.connect(self.ui.btnRefreshLocalLayers, SIGNAL("clicked()"), self.refresh_local_data_sources) QObject.connect(self.iface, SIGNAL("newProjectCreated()"), self.reset_load_data) QObject.connect(QgsMapLayerRegistry.instance(), SIGNAL("layerWillBeRemoved(QString)"), self.remove_layer) QObject.connect(QgsMapLayerRegistry.instance(), SIGNAL("layerWasAdded(QgsMapLayer *)"), self.add_layer) QObject.connect(self.ui.cbUploadDatabase, SIGNAL("currentIndexChanged(int)"), self.upload_database_selected) QObject.connect(self.ui.btnUploadData, SIGNAL("clicked()"), self.upload_data) QObject.connect(self.ui.btnPublishMapUpload, SIGNAL("clicked()"), self.publish_map) self.ui.editServer.textChanged.connect(self.serverURL) self.ui.resetUrlBtn.clicked.connect(self.resetApiUrl) self.read_settings() self.api = API() self.db_connections = DbConnections() self.local_data_sources = LocalDataSources() self.data_upload = DataUpload(self.iface, self.statusBar(), self.ui.uploadProgressBar, self.api, self.db_connections) if self.URL == "": self.ui.editServer.setText(self.api.api_url()) else: self.ui.editServer.setText(self.URL) self.palette_red = QPalette(self.ui.serviceLinks.palette()) self.palette_red.setColor(QPalette.WindowText, QColor('red')) def __del__(self): QObject.disconnect(self.iface, SIGNAL("newProjectCreated()"), self.reset_load_data) QObject.disconnect(QgsMapLayerRegistry.instance(), SIGNAL("layerWillBeRemoved(QString)"), self.remove_layer) QObject.disconnect(QgsMapLayerRegistry.instance(), SIGNAL("layerWasAdded(QgsMapLayer *)"), self.add_layer) def statusBar(self): return self.iface.mainWindow().statusBar() def map(self): project = QgsProject.instance() name = os.path.splitext(os.path.basename(unicode(project.fileName())))[0] #Allowed chars for QGISCloud map name: /\A[A-Za-z0-9\_\-]*\Z/ name = unicode(name).lower().encode('ascii', 'replace') # Replace non-ascii chars name = re.compile("\W+", re.UNICODE).sub("_", name) # Replace withespace return name def resetApiUrl(self): self.ui.editServer.setText(self.api.api_url()) def serverURL(self, URL): self.URL = URL self.store_settings() def store_settings(self): s = QSettings() s.setValue("qgiscloud/user", self.user) s.setValue("qgiscloud/URL", self.URL) def read_settings(self): s = QSettings() self.user = s.value("qgiscloud/user", "", type=str) self.URL = s.value("qgiscloud/URL", "", type=str) def _update_clouddb_mode(self, clouddb): self.clouddb = clouddb self.ui.groupBoxDatabases.setVisible(self.clouddb) tab_index = 1 tab_name = QApplication.translate("QgisCloudPlugin", "Upload Data") visible = (self.ui.tabWidget.indexOf(self.ui.upload) == tab_index) if visible and not self.clouddb: self.ui.tabWidget.removeTab(tab_index) elif not visible and self.clouddb: self.ui.tabWidget.insertTab(tab_index, self.ui.upload, tab_name) def _version_info(self): return {'versions': {'plugin': self.version, 'QGIS': QGis.QGIS_VERSION, 'OGR': ogr_version_info(), 'OS': platform.platform(), 'Python': sys.version}} def _update_versions(self, current_plugin_version): version_ok = True self.ui.lblVersionQGIS.setText(QGis.QGIS_VERSION) self.ui.lblVersionPlugin.setText(self.version) if StrictVersion(self.version) < StrictVersion(current_plugin_version): self.ui.lblVersionPlugin.setPalette(self.palette_red) version_ok = False self.ui.lblVersionOGR.setText(ogr_version_info()) if ogr_version_num() < 1900: self.ui.lblVersionOGR.setPalette(self.palette_red) version_ok = False self.ui.lblVersionPython.setText(sys.version) self.ui.lblVersionOS.setText(platform.platform()) return version_ok def check_login(self): version_ok = True if not self.api.check_auth(): login_dialog = QDialog(self) login_dialog.ui = Ui_LoginDialog() login_dialog.ui.setupUi(login_dialog) login_dialog.ui.editUser.setText(self.user) login_ok = False while not login_ok and version_ok: self.api.set_url(self.api_url()) if not login_dialog.exec_(): self.api.set_auth(user=login_dialog.ui.editUser.text(), password=None) return login_ok self.api.set_auth(user=login_dialog.ui.editUser.text(), password=login_dialog.ui.editPassword.text()) try: login_info = self.api.check_login(version_info=self._version_info()) #{u'paid_until': None, u'plan': u'Free', u'current_plugin': u'0.8.0'} self.user = login_dialog.ui.editUser.text() self._update_clouddb_mode(login_info['clouddb']) version_ok = self._update_versions(login_info['current_plugin']) self.ui.serviceLinks.setCurrentWidget(self.ui.pageVersions) self.store_settings() self.ui.btnLogin.hide() self.ui.lblSignup.hide() self.ui.btnLogout.show() self.ui.lblLoginStatus.setText(self.tr_uni("Logged in as {0} ({1})").format(self.user, login_info['plan'])) self.ui.lblLoginStatus.show() self._push_message(self.tr("QGIS Cloud"), self.tr_uni("Logged in as {0}").format(self.user), level=0, duration=2) if not version_ok: self._push_message(self.tr("QGIS Cloud"), self.tr("Unsupported versions detected. Please check your versions first!"), level=1) version_ok = False self.ui.tabWidget.setCurrentWidget(self.ui.services) login_ok = True except (UnauthorizedError, TokenRequiredError, ConnectionException): QMessageBox.critical(self, self.tr("Login failed"), self.tr("Wrong user name or password")) login_ok = False return version_ok def create_database(self): if self.check_login(): db = self.api.create_database() # {u'username': u'jhzgpfwi_qgiscloud', u'host': u'beta.spacialdb.com', u'password': u'11d7338c', u'name': u'jhzgpfwi_qgiscloud', u'port': 9999} self.show_api_error(db) self.refresh_databases() def delete_database(self): if self.check_login(): name = self.ui.tabDatabases.currentItem().text() msgBox = QMessageBox() msgBox.setText(self.tr("Delete QGIS Cloud database.")) msgBox.setInformativeText(self.tr_uni("Do you want to delete the database \"%s\"?") % name) msgBox.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) msgBox.setDefaultButton(QMessageBox.Cancel) msgBox.setIcon(QMessageBox.Question) ret = msgBox.exec_() if ret == QMessageBox.Ok: self.setCursor(Qt.WaitCursor) result = self.api.delete_database(name) self.show_api_error(result) self.ui.btnDbDelete.setEnabled(False) time.sleep(2) self.refresh_databases() self.unsetCursor() def select_database(self): self.ui.btnDbDelete.setEnabled(len(self.ui.tabDatabases.selectedItems()) > 0) def login(self): if self.check_login(): self.refresh_databases() @pyqtSignature('') def on_btnLogout_clicked(self): self.api.reset_auth() self.ui.btnLogout.hide() self.ui.lblLoginStatus.hide() self.ui.btnLogin.show() def refresh_databases(self): if self.clouddb and self.check_login(): db_list = self.api.read_databases() if self.show_api_error(db_list): return self.db_connections = DbConnections() for db in db_list: #db example: {"host":"spacialdb.com","connection_string":"postgres://*****:*****@spacialdb.com:9999/sekpjr_jpyled","name":"sekpjr_jpyled","username":"******","port":9999,"password":"******"} self.db_connections.add_from_json(db) self.ui.tabDatabases.clear() self.ui.btnDbDelete.setEnabled(False) self.ui.cbUploadDatabase.clear() if self.db_connections.count() == 0: self.ui.cbUploadDatabase.addItem(self.tr("Create new database")) elif self.db_connections.count() > 1: self.ui.cbUploadDatabase.addItem(self.tr("Select database")) for name, db in self.db_connections.iteritems(): it = QListWidgetItem(name) it.setToolTip(db.description()) self.ui.tabDatabases.addItem(it) self.ui.cbUploadDatabase.addItem(name) self.db_connections.refresh(self.user) def api_url(self): return unicode(self.ui.editServer.text()) def update_urls(self): self.update_url(self.ui.lblWebmap, self.api_url(), 'http://', u'{0}/{1}'.format(self.user, self.map())) if self.clouddb: self.update_url(self.ui.lblMobileMap, self.api_url(), 'http://m.', u'{0}/{1}'.format(self.user, self.map())) self.update_url(self.ui.lblWMS, self.api_url(), 'http://wms.', u'{0}/{1}'.format(self.user, self.map())) else: self.update_url(self.ui.lblMobileMap, self.api_url(), 'http://', u'{0}/{1}/mobile'.format(self.user, self.map())) self.update_url(self.ui.lblWMS, self.api_url(), 'http://', u'{0}/{1}/wms'.format(self.user, self.map())) self.update_url(self.ui.lblMaps, self.api_url(), 'http://', 'maps') def update_url(self, label, api_url, prefix, path): base_url = string.replace(api_url, 'https://api.', prefix) url = u'{0}/{1}'.format(base_url, path) text = re.sub(r'http[^"]+', url, unicode(label.text())) label.setText(text) def read_maps(self): #map = self.api.read_map("1") if self.check_login(): self.api.read_maps() def check_project_saved(self): cancel = False project = QgsProject.instance() fname = unicode(project.fileName()) if project.isDirty() or fname == '': msgBox = QMessageBox() msgBox.setText(self.tr("The project has been modified.")) msgBox.setInformativeText(self.tr("Do you want to save your changes?")) if not fname: msgBox.setStandardButtons(QMessageBox.Save | QMessageBox.Cancel) else: msgBox.setStandardButtons(QMessageBox.Save | QMessageBox.Ignore | QMessageBox.Cancel) msgBox.setDefaultButton(QMessageBox.Save) ret = msgBox.exec_() if ret == QMessageBox.Save: if not fname: project.setFileName(QFileDialog.getSaveFileName(self, "Save Project", "", "QGIS Project Files (*.qgs)")) if not unicode(project.fileName()): cancel = True else: project.write() elif ret == QMessageBox.Cancel: cancel = True return cancel def publish_map(self): cancel = self.check_project_saved() if cancel: self.statusBar().showMessage(self.tr("Cancelled")) return if self.check_login() and self.check_layers(): self.statusBar().showMessage(self.tr("Publishing map")) try: fullExtent = self.iface.mapCanvas().fullExtent() config = { 'fullExtent': { 'xmin': fullExtent.xMinimum(), 'ymin': fullExtent.yMinimum(), 'xmax': fullExtent.xMaximum(), 'ymax': fullExtent.yMaximum() #}, #'svgPaths': QgsApplication.svgPaths() #For resolving absolute symbol paths in print composer } } fname = unicode(QgsProject.instance().fileName()) map = self.api.create_map(self.map(), fname, config)['map'] #QMessageBox.information(self, "create_map", str(map['config'])) self.show_api_error(map) if map['config']['missingSvgSymbols']: self.publish_symbols(map['config']['missingSvgSymbols']) self.update_urls() self.ui.serviceLinks.setCurrentWidget(self.ui.pageLinks) self.ui.btnPublishMapUpload.hide() self._push_message(self.tr("QGIS Cloud"), self.tr("Map successfully published"), level=0, duration=2) self.statusBar().showMessage(self.tr("Map successfully published")) except Exception: self.statusBar().showMessage("") self._exception_message(self.tr("Error uploading project")) def _exception_message(self, title): stack = traceback.format_exc().splitlines() msgBox = QMessageBox() msgBox.setText(self.tr_uni("An error occurred: %s") % stack[-1]) msgBox.setInformativeText(self.tr("Do you want to send the exception info to qgiscloud.com?")) msgBox.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) msgBox.setIcon(QMessageBox.Question) ret = msgBox.exec_() if ret == QMessageBox.Ok: project_fname = unicode(QgsProject.instance().fileName()) self.api.create_exception(str(traceback.format_exc()), self._version_info(), project_fname) def publish_symbols(self, missingSvgSymbols): self.statusBar().showMessage(self.tr("Uploading SVG symbols")) qDebug("publish_symbols: %s" % missingSvgSymbols) search_paths = QgsApplication.svgPaths() if hasattr(QgsProject.instance(), 'homePath'): search_paths += [QgsProject.instance().homePath()] #Search and upload symbol files for sym in missingSvgSymbols: #Absolute custom path if os.path.isfile(sym): self.api.create_graphic(sym, sym) else: for path in search_paths: fullpath = os.path.join(unicode(path), sym) if os.path.isfile(fullpath): qDebug("api.create_graphic: %s" % fullpath) self.api.create_graphic(sym, fullpath) self.statusBar().showMessage("") def reset_load_data(self): self.update_local_data_sources([]) self.ui.btnUploadData.setEnabled(False) self.ui.btnPublishMapUpload.hide() def remove_layer(self, layer_id): if self.db_connections.refreshed() and self.do_update_local_data_sources: # skip layer if layer will be removed self.update_local_layers(layer_id) self.activate_upload_button() def add_layer(self): if self.db_connections.refreshed() and self.do_update_local_data_sources: self.update_local_layers() self.activate_upload_button() def update_local_layers(self, skip_layer_id=None): local_layers, unsupported_layers = self.local_data_sources.local_layers(skip_layer_id) try: self.update_local_data_sources(local_layers) except: self._exception_message(self.tr("Error checking local data sources")) return local_layers, unsupported_layers def check_layers(self): local_layers, unsupported_layers = self.update_local_layers() if (local_layers and self.clouddb) or unsupported_layers: message = "" if local_layers: title = self.tr("Local layers found") message += self.tr("Some layers are using local data. You can upload local layers to your cloud database in the 'Upload Data' tab.\n\n") if unsupported_layers: title = self.tr("Unsupported layers found") message += self.tr("Raster, plugin or geometryless layers are not supported:\n\n") layer_types = ["No geometry", "Raster", "Plugin"] for layer in sorted(unsupported_layers, key=lambda layer: layer.name()): message += self.tr_uni(" - %s (%s)\n") % (layer.name(), layer_types[layer.type()]) message += self.tr("\nPlease remove or replace above layers before publishing your map.\n") message += self.tr("For raster data you can use public WMS layers or the OpenLayers Plugin.") QMessageBox.information(self, title, message) self.refresh_databases() self.ui.tabWidget.setCurrentWidget(self.ui.upload) return False return True def update_local_data_sources(self, local_layers): # update table names lookup self.update_data_sources_table_names() self.local_data_sources.update_local_data_sources(local_layers) # update GUI while self.ui.tblLocalLayers.rowCount() > 0: self.ui.tblLocalLayers.removeRow(0) for data_source, layers in self.local_data_sources.iteritems(): layer_names = [] for layer in layers: layer_names.append(unicode(layer.name())) layers_item = QTableWidgetItem(", ".join(layer_names)) layers_item.setToolTip("\n".join(layer_names)) data_source_item = QTableWidgetItem(data_source) data_source_item.setToolTip(data_source) table_name = layers[0].name() # find a better table name if there are multiple layers with same data source? if data_source in self.data_sources_table_names: # use current table name if available to keep changes by user table_name = self.data_sources_table_names[data_source] table_name_item = QTableWidgetItem(QgisCloudPluginDialog.launder_pg_name(table_name)) wkbType = layers[0].wkbType() if wkbType not in self.GEOMETRY_TYPES: raise Exception(self.tr("Unsupported geometry type '%s' in layer '%s'") % (wkbType, layers[0].name())) geometry_type_item = QTableWidgetItem(self.GEOMETRY_TYPES[wkbType]) if layers[0].providerType() == "ogr": geometry_type_item.setToolTip(self.tr("Note: OGR features will be converted to MULTI-type")) srid_item = QTableWidgetItem(layers[0].crs().authid()) row = self.ui.tblLocalLayers.rowCount() self.ui.tblLocalLayers.insertRow(row) self.ui.tblLocalLayers.setItem(row, self.COLUMN_LAYERS, layers_item) self.ui.tblLocalLayers.setItem(row, self.COLUMN_DATA_SOURCE, data_source_item) self.ui.tblLocalLayers.setItem(row, self.COLUMN_TABLE_NAME, table_name_item) self.ui.tblLocalLayers.setItem(row, self.COLUMN_GEOMETRY_TYPE, geometry_type_item) self.ui.tblLocalLayers.setItem(row, self.COLUMN_SRID, srid_item) if self.local_data_sources.count() > 0: self.ui.tblLocalLayers.resizeColumnsToContents() self.ui.tblLocalLayers.setColumnWidth(self.COLUMN_LAYERS, 100) self.ui.tblLocalLayers.setColumnWidth(self.COLUMN_DATA_SOURCE, 100) self.ui.tblLocalLayers.sortItems(self.COLUMN_DATA_SOURCE) self.ui.tblLocalLayers.setSortingEnabled(False) else: self.ui.btnUploadData.setEnabled(False) self.statusBar().showMessage(self.tr("Updated local data sources")) @staticmethod def launder_pg_name(name): #OGRPGDataSource::LaunderName #return re.sub(r"[#'-]", '_', unicode(name).lower()) input_string = unicode(name).lower().encode('ascii', 'replace') return re.compile("\W+", re.UNICODE).sub("_", input_string) def refresh_local_data_sources(self): if not self.db_connections.refreshed(): # get dbs on first refresh self.refresh_databases() self.do_update_local_data_sources = True self.update_local_layers() self.activate_upload_button() def update_data_sources_table_names(self): if self.local_data_sources.count() == 0: self.data_sources_table_names.clear() else: # remove table names without data sources keys_to_remove = [] for key in self.data_sources_table_names.iterkeys(): if self.local_data_sources.layers(key) is None: keys_to_remove.append(key) for key in keys_to_remove: del self.data_sources_table_names[key] # update table names for row in range(0, self.ui.tblLocalLayers.rowCount()): data_source = unicode(self.ui.tblLocalLayers.item(row, self.COLUMN_DATA_SOURCE).text()) table_name = unicode(self.ui.tblLocalLayers.item(row, self.COLUMN_TABLE_NAME).text()) self.data_sources_table_names[data_source] = table_name def upload_database_selected(self, index): self.activate_upload_button() def activate_upload_button(self): self.ui.btnUploadData.setEnabled((self.db_connections.count() <= 1 or self.ui.cbUploadDatabase.currentIndex() > 0) and self.local_data_sources.count() > 0) self.ui.btnPublishMapUpload.hide() def upload_data(self): if self.check_login(): if self.local_data_sources.count() == 0: return if self.db_connections.count() == 0: # create db self.statusBar().showMessage(self.tr("Create new database...")) QApplication.processEvents() # refresh status bar self.create_database() self.statusBar().showMessage("") db_name = self.ui.cbUploadDatabase.currentText() if not self.db_connections.isPortOpen(db_name): uri = self.db_connections.cloud_layer_uri(db_name, "", "") host = str(uri.host()) port = uri.port() QMessageBox.critical(self, self.tr("Network Error"), self.tr("Could not connect to database server ({0}) on port {1}. Please contact your system administrator or internet provider".format(host, port))) return # disable update of local data sources during upload, as there are temporary layers added and removed self.do_update_local_data_sources = False self.statusBar().showMessage(self.tr("Uploading data...")) self.setCursor(Qt.WaitCursor) # Map<data_source, {table: table, layers: layers}> data_sources_items = {} for row in range(0, self.ui.tblLocalLayers.rowCount()): data_source = unicode(self.ui.tblLocalLayers.item(row, self.COLUMN_DATA_SOURCE).text()) layers = self.local_data_sources.layers(data_source) if layers is not None: table_name = unicode(self.ui.tblLocalLayers.item(row, self.COLUMN_TABLE_NAME).text()) data_sources_items[data_source] = {'table': table_name, 'layers': layers} try: success = self.data_upload.upload(self.db_connections.db(db_name), data_sources_items, self.ui.cbReplaceLocalLayers.isChecked()) except Exception: success = False QgsMessageLog.logMessage(str(traceback.format_exc()), 'QGISCloud') if not success: self._show_log_window() QMessageBox.warning(self, self.tr("Upload data"), self.tr("Data upload error.\nSee Log Messages for more information.")) self.unsetCursor() self.statusBar().showMessage("") self.do_update_local_data_sources = True if success and self.ui.cbReplaceLocalLayers.isChecked(): self.update_local_layers() # show save project dialog msgBox = QMessageBox() msgBox.setWindowTitle(self.tr("QGIS Cloud")) msgBox.setText(self.tr("The project is ready for publishing.")) msgBox.setInformativeText(self.tr("Do you want to save your changes?")) msgBox.setStandardButtons(QMessageBox.Save | QMessageBox.Cancel) msgBox.setDefaultButton(QMessageBox.Save) ret = msgBox.exec_() if ret == QMessageBox.Save: self.iface.actionSaveProjectAs().trigger() self.ui.btnPublishMapUpload.show() def _show_log_window(self): logDock = self.iface.mainWindow().findChild(QDockWidget, 'MessageLog') logDock.show() def _push_message(self, title, text, level=0, duration=0): if hasattr(self.iface, 'messageBar') and hasattr(self.iface.messageBar(), 'pushMessage'): # QGIS >= 2.0 self.iface.messageBar().pushMessage(title, text, level, duration) else: QMessageBox.information(self, title, text) def show_api_error(self, result): if 'error' in result: QMessageBox.critical(self, self.tr("QGIS Cloud Error"), "%s" % result['error']) self.statusBar().showMessage(self.tr("Error")) return True else: return False def tr_uni(self, str): return unicode(self.tr(str))