QGIS/python/plugins/osm/OsmDownloadDlg.py
2011-09-15 21:33:42 +03:00

467 lines
19 KiB
Python
Executable File

# -*- 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 <http://wiki.openstreetmap.org/wiki/Getting_Data> 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)