# -*- coding: utf-8 -*- """@package OsmDownloadDlg Module provides simple way how to download OSM data. First user is asked to choose download region, output file etc. Then HTTP connection to OpenStreetMap server is created and download operation is started. Note that OpenStreetMap server you are downloading OSM data from (~api.openstreetmap.org) has fixed limitations of how much data you can get. As written on wiki.openstreetmap.org neither latitude nor longitude extent of downloaded region can be larger than 0.25 degrees. Each error response from OSM server is caught by OSM Plugin and display to its user. """ from ui_OsmDownloadDlg import Ui_OsmDownloadDlg from PyQt4.QtCore import * from PyQt4.QtGui import * from PyQt4.QtNetwork import * from time import * from qgis.core import * class OsmDownloadDlg(QDialog, Ui_OsmDownloadDlg): """This is the main class of this module. It's direct descendant of "OSM Download" dialog. It provides simple way how to download OSM data. First user is asked to choose download region, output file etc. Then HTTP connection to OpenStreetMap server is created and download operation is started. Note that OpenStreetMap server you are downloading OSM data from (~api.openstreetmap.org) has fixed limitations of how much data you can get. As written on wiki.openstreetmap.org neither latitude nor longitude extent of downloaded region can be larger than 0.25 degrees. Each error response from OSM server is caught by OSM Plugin and display to its user. """ def __init__(self, plugin): """The constructor. Performs initialization of OSM Download dialog and inner structures. Default download region is set according to current canvas extent. @param plugin is pointer to instance of OSM Plugin. """ QDialog.__init__(self, None) self.setupUi(self) self.dbm=plugin.dbm self.urlHost="api.openstreetmap.org" self.urlPathPrefix="/api/0.6/map?bbox=" self.downloadButton.setDefault(True) # determining default area for download if QgsMapLayerRegistry.instance().count()>0: currentExtent=plugin.canvas.extent() else: # if no layer is loaded default download extent is "part of the Prague city center" :-D currentExtent=QgsRectangle(14.4271398308,50.0768156358,14.4324358906,50.0812613868) # check whether the extent needs to be projected back to WGS84 mapRenderer = plugin.canvas.mapRenderer() if mapRenderer.hasCrsTransformEnabled(): crsMap=mapRenderer.destinationSrs() crsWgs84=QgsCoordinateReferenceSystem(4326) xform=QgsCoordinateTransform(crsMap, crsWgs84) currentExtent=xform.transformBoundingBox(currentExtent) self.latFromLineEdit.setText(QString("%1").arg(currentExtent.yMinimum(),0,'f',10)) self.latToLineEdit.setText(QString("%1").arg(currentExtent.yMaximum(),0,'f',10)) self.lonFromLineEdit.setText(QString("%1").arg(currentExtent.xMinimum(),0,'f',10)) self.lonToLineEdit.setText(QString("%1").arg(currentExtent.xMaximum(),0,'f',10)) # create object for http connection self.outFile=None self.httpGetId=0 self.httpSuccess=False self.errMessage=None self.finished=False self.responseHeader="" # connect all important signals to slots self.connectDlgSignals() self.helpButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_questionMark.png")) # set special font for extentInfoLabel myFont = self.extentInfoLabel.font() myFont.setPointSize( myFont.pointSize()+1 ) myFont.setBold(True) self.extentInfoLabel.setFont(myFont) # generating default name for output file defaultFileName=self.generateDefFileName() self.destdirLineEdit.setText(defaultFileName) self.destdirLineEdit.setEnabled(True) # check default extent self.checkExtent() # load default values to combobox determining style for custom renderer self.styles=["Small scale","Medium scale","Large scale"] self.styleCombo.addItems(self.styles) # just determine if "replace data" checkbox should be checked if not plugin.dbm.currentKey: self.chkReplaceData.setEnabled(False) def downloadFile(self): """Function starts thw whole download process. It's called after click() signal is emitted on Download button. """ # finding out which area should be downloaded, and to where urlPath = self.urlPathPrefix + self.lonFromLineEdit.text() + "," + self.latFromLineEdit.text() + "," + self.lonToLineEdit.text() + "," + self.latToLineEdit.text() fileName = self.destdirLineEdit.text() # remove the old database file if QFile.exists(fileName+".db"): QFile.remove(fileName+".db") self.outFile=QFile(fileName) if not self.outFile.open(QIODevice.WriteOnly): QMessageBox.information(self, self.tr("OSM Download"), self.tr("Unable to save the file %1: %2.") .arg(fileName).arg(self.outFile.errorString())) self.outFile = None return self.setEnabled(False) self.finished = False # creating progress dialog for download self.progressDialog=QProgressDialog(self) # !!! don't set progress dialog modal !!! it would cause serious problems! self.progressDialog.setAutoClose(False) self.progressDialog.setWindowTitle(self.tr("OSM Download")) self.connect(self.progressDialog,SIGNAL("canceled()"), self.progressDlgCanceled) self.progressDialog.show() self.progressDialog.setLabelText(self.tr("Waiting for OpenStreetMap server ...")) self.progressDialog.setMaximum(1) self.progressDialog.setValue(0) # create object for http connection self.http=QHttp(self) # catching http signals! self.connect(self.http,SIGNAL("requestFinished(int, bool)"), self.httpRequestFinished) self.connect(self.http,SIGNAL("dataReadProgress(int, int)"), self.updateDataReadProgress) self.connect(self.http,SIGNAL("responseHeaderReceived(QHttpResponseHeader)"), self.readResponseHeader) self.connect(self.http,SIGNAL("stateChanged(int)"), self.stateChanged) self.connect(self.http,SIGNAL("done(bool)"), self.httpDone) self.setProxy() self.http.setHost(self.urlHost, 80) self.httpGetId=self.http.get(urlPath, self.outFile) def httpRequestFinished(self, requestId, error): """Function is called when requestFinished(...) signal is emitted on global HTTP connection object. @param requestId identifier of http request that was finished @param error True if error occured on given request; False otherwise """ if self.finished: return def readResponseHeader(self, responseHeader): """Function is called when responseHeaderReceived(...) signal is emitted on global HTTP connection object. If statusCode of responseHeader doesn't equal to 200, function cancels the whole connection. @param responseHeader header of HTTP response from the OSM server """ if self.finished: return if responseHeader.statusCode() != 200: self.cancelDownload(self.tr("Download process failed. OpenStreetMap server response: %1 - %2") .arg(responseHeader.reasonPhrase()) .arg(responseHeader.value("Error"))) def updateDataReadProgress(self, bytesRead, totalBytes): """Function is called after dataReadProgress(...) signal is emitted on global HTTP connection object. It updates progress dialog. @param bytesRead total number of bytes that has been already read through the HTTP connection @param totalBytes total number of bytes that will be received through HTTP connection """ if self.finished: return # note that progress dialog mustn't be modal! self.progressDialog.setMaximum(totalBytes) self.progressDialog.setValue(bytesRead) def stateChanged(self,newState): """Function is called after stateChanged(...) signal is emitted on HTTP connection. OSM Downloader does actually nothing in here. Maybe in future function will be used. @param newState number representing new state of the connection """ if self.finished: return def httpDone(self,error): """Function is called after done(...) signal is emitted on HTTP connection. (Done signal is emitted immediately after all requests of HTTP connection are finished ~ emits an requestFinished(...) signal). @param error True if error occured on any of HTTP connection requests; False otherwise """ if self.finished: return self.finished=True self.outFile.flush() self.outFile.close() # we are no more interested in signals emitted on QHttp object self.disconnect(self.http,SIGNAL("done(bool)"), self.httpDone) self.disconnect(self.http,SIGNAL("requestFinished(int, bool)"), self.httpRequestFinished) self.disconnect(self.http,SIGNAL("dataReadProgress(int, int)"), self.updateDataReadProgress) self.disconnect(self.http,SIGNAL("responseHeaderReceived(QHttpResponseHeader)"), self.readResponseHeader) self.disconnect(self.http,SIGNAL("stateChanged(int)"), self.stateChanged) del self.http self.http=None self.progressDialog.close() self.setEnabled(True) # request was not aborted if error: self.httpSuccess=False # remove output file if self.outFile and self.outFile.exists(): self.outFile.remove() del self.outFile self.outFile=None # and tell user (if the download wasn't cancelled by user) if self.errMessage != "__cancel__": if self.errMessage==None: self.errMessage=QCoreApplication.translate( "OsmDownloadDlg", "Check your internet connection" ) QMessageBox.information(self, self.tr("OSM Download Error") ,self.tr("Download failed: %1.").arg(self.errMessage)) return self.httpSuccess=True # well, download process has finished successfully; # close the whole download dialog self.close() def cancelDownload(self,errMessage=None): """Function aborts global HTTP connection. It gets an error message and just store it into member variable. It will be displayed to Quantum GIS user later after done(...) will be emitted. @param errMessage error message ~ the reason why connection is canceled """ if self.finished: return self.errMessage=errMessage # stop http communication self.http.abort() #################################################################################### ############ NON-HTTP FUNCTIONS #################################################### #################################################################################### def showChooseDirectoryDialog(self): """Function just shows dialog for directory selection. Only OSM files can be selected. """ # display file open dialog and get absolute path to selected directory fileSelected = QFileDialog.getSaveFileName(self, self.tr("Choose file to save"),"download.osm", self.tr("OSM Files (*.osm)") ); # insert selected directory path into line edit control if not fileSelected.isNull(): self.destdirLineEdit.setText(fileSelected) def generateDefFileName(self): """This function creates default name for output file. It's called mainly from downloader initialization. Default name is always unique. It consist of current timestamp and a postfix. """ prefix=QDir.tempPath() + "/" if self.dbm.currentKey: key=QString(self.dbm.currentKey) p=key.lastIndexOf("/") prefix=key.left(p+1) timestring=strftime("%y%m%d_%H%M%S",localtime(time())) return prefix.append(QString(timestring)).append("_downloaded.osm") def autoLoadClicked(self): """Function is called after clicking on AutoLoad checkbox. """ if not self.autoLoadCheckBox.isChecked(): self.chkCustomRenderer.setEnabled(False) self.chkReplaceData.setEnabled(False) else: self.chkCustomRenderer.setEnabled(True) self.chkReplaceData.setEnabled(True) def showExtentHelp(self): """Function is called after clicking on Help button. It shows basic information on downloading. """ mb=QMessageBox() mb.setMinimumWidth(390) mb.information(self, self.tr("Getting data"),self.tr("The OpenStreetMap server you are downloading OSM data from (~ api.openstreetmap.org) has fixed limitations of how much data you can get. As written at neither latitude nor longitude extent of downloaded region can be larger than 0.25 degrees. Note that Quantum GIS allows you to specify any extent you want, but OpenStreetMap server will reject all request that won't satisfy downloading limitations.")) def checkExtent(self): """Function checks if extent, currently set on dialog, is valid. It's called whenever download region changed. Result of checking is displayed on dialog. """ lim = 0.25 # download limitations of openstreetmap server in degrees # get coordinates that are currently set latFrom = self.latFromLineEdit.text().toDouble()[0] lonFrom = self.lonFromLineEdit.text().toDouble()[0] latTo = self.latToLineEdit.text().toDouble()[0] lonTo = self.lonToLineEdit.text().toDouble()[0] # tested conditions largeLatExt = False largeLonExt = False if abs(latTo-latFrom)>lim: largeLatExt = True if abs(lonTo-lonFrom)>lim: largeLonExt = True if largeLatExt and largeLonExt: self.extentInfoLabel.setText(self.tr("Both extents are too large!")) elif largeLatExt: self.extentInfoLabel.setText(self.tr("Latitude extent is too large!")) elif largeLonExt: self.extentInfoLabel.setText(self.tr("Longitude extent is too large!")) else: self.extentInfoLabel.setText(self.tr("OK! Area is probably acceptable to server.")) def progressDlgCanceled(self): """Function is called after progress dialog is canceled. It aborts HTTP connection. """ # cancel download with a special message self.cancelDownload("__cancel__") def setProxy(self): """Function sets proxy to HTTP connection of downloader. HTTP connection object is not given in function parameter, because it's global - accessible for the whole downloader. """ # getting and setting proxy information settings=QSettings() proxyHost=QString() proxyUser=QString() proxyPassword=QString() proxyPort=0 proxyType=QNetworkProxy.NoProxy proxyEnabled=settings.value("proxy/proxyEnabled",QVariant(0)).toBool() if proxyEnabled: proxyHost=settings.value("proxy/proxyHost",QVariant("")).toString() proxyPort=settings.value("proxy/proxyPort",QVariant(8080)).toInt()[0] proxyUser=settings.value("proxy/proxyUser",QVariant("")).toString() proxyPassword=settings.value("proxy/proxyPassword",QVariant("")).toString() proxyTypeString=settings.value("proxy/proxyType",QVariant("")).toString() if proxyTypeString=="DefaultProxy": proxyType=QNetworkProxy.DefaultProxy elif proxyTypeString=="Socks5Proxy": proxyType=QNetworkProxy.Socks5Proxy elif proxyTypeString=="HttpProxy": proxyType=QNetworkProxy.HttpProxy elif proxyTypeString=="HttpCachingProxy": proxyType=QNetworkProxy.HttpCachingProxy elif proxyTypeString=="FtpCachingProxy": proxyType=QNetworkProxy.FtpCachingProxy self.proxy=QNetworkProxy() self.proxy.setType(proxyType) self.proxy.setHostName(proxyHost) self.proxy.setPort(proxyPort) self.http.setProxy(self.proxy) def connectDlgSignals(self): """Function connects necessary signals to appropriate slots. """ # whenever extent coordinates are changed, currently set extent has to be tested for validity # (coz openstreetmap has some limitations for how large area and how much data can be downloaded at once) self.connect(self.latFromLineEdit, SIGNAL("textChanged(const QString &)"), self.checkExtent) self.connect(self.latToLineEdit, SIGNAL("textChanged(const QString &)"), self.checkExtent) self.connect(self.lonFromLineEdit, SIGNAL("textChanged(const QString &)"), self.checkExtent) self.connect(self.lonToLineEdit, SIGNAL("textChanged(const QString &)"), self.checkExtent) self.connect(self.helpButton, SIGNAL("clicked()"), self.showExtentHelp) self.connect(self.downloadButton, SIGNAL("clicked()"), self.downloadFile) self.connect(self.cancelButton, SIGNAL("clicked()"), self.close) self.connect(self.choosedirButton, SIGNAL("clicked()"), self.showChooseDirectoryDialog) self.connect(self.autoLoadCheckBox, SIGNAL("clicked()"), self.autoLoadClicked) def disconnectDlgSignals(self): """Function disconnects connected signals. """ self.disconnect(self.latFromLineEdit, SIGNAL("textChanged(const QString &)"), self.checkExtent) self.disconnect(self.latToLineEdit, SIGNAL("textChanged(const QString &)"), self.checkExtent) self.disconnect(self.lonFromLineEdit, SIGNAL("textChanged(const QString &)"), self.checkExtent) self.disconnect(self.lonToLineEdit, SIGNAL("textChanged(const QString &)"), self.checkExtent) self.disconnect(self.helpButton, SIGNAL("clicked()"), self.showExtentHelp) self.disconnect(self.downloadButton, SIGNAL("clicked()"), self.downloadFile) self.disconnect(self.choosedirButton, SIGNAL("clicked()"), self.showChooseDirectoryDialog) self.disconnect(self.autoLoadCheckBox, SIGNAL("clicked()"), self.autoLoadClicked)