diff --git a/python/plugins/CMakeLists.txt b/python/plugins/CMakeLists.txt index 84844568b01..6c6a4d15533 100644 --- a/python/plugins/CMakeLists.txt +++ b/python/plugins/CMakeLists.txt @@ -1 +1 @@ -SUBDIRS(plugin_installer mapserver_export fTools) +SUBDIRS(plugin_installer mapserver_export fTools osm) diff --git a/python/plugins/osm/CMakeLists.txt b/python/plugins/osm/CMakeLists.txt new file mode 100644 index 00000000000..810c66e93b7 --- /dev/null +++ b/python/plugins/osm/CMakeLists.txt @@ -0,0 +1,12 @@ +#TODO: Need to configure cmake to run pyrcc4 and pyuic4 as required when the resource +# file or the ui change + +SET (OSM_PLUGIN_DIR ${QGIS_DATA_DIR}/python/plugins/osm) + +FILE (GLOB PYTHON_FILES *.py) +FILE (GLOB MAPTOOLS_PYTHON_FILES map_tools/*.py) +FILE (GLOB STYLE_FILES styles/*.style) + +INSTALL (FILES ${PYTHON_FILES} DESTINATION ${OSM_PLUGIN_DIR}) +INSTALL (FILES ${MAPTOOLS_PYTHON_FILES} DESTINATION ${OSM_PLUGIN_DIR}/map_tools) +INSTALL (FILES ${STYLE_FILES} DESTINATION ${OSM_PLUGIN_DIR}/styles) diff --git a/python/plugins/osm/DatabaseManager.py b/python/plugins/osm/DatabaseManager.py new file mode 100644 index 00000000000..710509fe514 --- /dev/null +++ b/python/plugins/osm/DatabaseManager.py @@ -0,0 +1,2589 @@ +"""@package DatabaseManager +This module provides methods to manipulate with database where OSM data are stored. + +DatabaseManager is the only part of OSM Plugin that has the right to access OSM (sqlite) database. +If any other part of plugin wants to manipulate with OSM data, it is expected to use constructs of this module. + +This module can manage more than one database at a time. +But only one of such databases is considered to be the "current database" and all operations are done on it. + +Module provides pretty good way to change "current database". +""" + + +from PyQt4.QtCore import * +from PyQt4.QtGui import * +from PyQt4.QtNetwork import * +from qgis.core import * + +import sqlite3 +import datetime + + + +class DatabaseManager: + """This is the only class of DatabaseManager module. Its purpose is to manage work with OSM databases. + + DatabaseManager class provides method to add new OSM database. + + It holds all connections to all databases and holds the information + on which database is currently used with all operations. + + If anyone wants to read data from (or write data to) other database then the "current one", + method for changing current database must be called first. + + Class also provides method for performing both commit and rollback on current database. + """ + + def __init__(self,plugin): + """The constructor. + + Initializes inner structures of DatabaseManager and connect signals to appropriate slots. + """ + + self.plugin=plugin + self.canvas=plugin.canvas + self.pluginName="QGIS OSM v0.4" + + self.dbConns={} # map dbFileName->sqlite3ConnectionObject + self.pointLayers={} + self.lineLayers={} + self.polygonLayers={} + self.currentKey=None + self.removing=False + + QObject.connect(self.plugin.iface,SIGNAL("currentLayerChanged(QgsMapLayer*)"),self.currLayerChanged) + QObject.connect(QgsMapLayerRegistry.instance(),SIGNAL("layerWillBeRemoved(QString)"),self.layerRemoved) + + + def currLayerChanged(self,layer): + """Function is called after currentLayerChanged(QgsMapLayer*) signal is emitted. + + Information that we work with another OSM database from now is very important and almost all plugins' modules + should know about this change. + + Identifying, editing, uploading, downloading,... - these all actions are performed on current database. + If current database changes, operations must be terminated violently. + + Function set new current database and tells other modules about this change... + + @param layer pointer to QgsVectorLayer object that was set as new current layer + """ + + # set new currentKey and tell all other plugin components + if not layer: + self.currentKey=None + self.plugin.undoredo.databaseChanged(None) + self.plugin.dockWidget.databaseChanged(None) + return + + if layer.dataProvider().name()<>"osm": + self.currentKey=None + self.plugin.undoredo.databaseChanged(None) + self.plugin.dockWidget.databaseChanged(None) + return + + # find out filename of new current database + layerSource=layer.source() + pos=layerSource.lastIndexOf("?") + dbFileName=layerSource.left(pos)+".db" + + if dbFileName not in self.dbConns.keys(): + self.currentKey=None + return + + if dbFileName.toLatin1().data()<>self.currentKey: + self.currentKey=dbFileName.toLatin1().data() + self.plugin.undoredo.databaseChanged(self.currentKey) + self.plugin.dockWidget.databaseChanged(self.currentKey) + + + def layerRemoved(self,layerID): + """Function is called after layerWillBeRemoved(QString) signal is emitted. + + Through this signal Quantum GIS gives our plugin chance to prepare itself on removing of a vector layer. + + Plugin must find out if this layer is one of OSM layers. + If it is, not only this layer but also the two with the same data source (OSM database) has to be removed. + + @param layerID unique Quantum GIS identifier of layer + """ + + if self.removing: + return + + # get appropriate qgsvectorlayer object + layer=QgsMapLayerRegistry.instance().mapLayer(layerID) + + if not layer: + return # strange situation + + if layer.dataProvider().name()<>"osm": + return # it's not OSM layer -> just ignore it + + # yes, it's osm layer; find out database file it's getting OSM data from + layerSource=layer.source() + pos=layerSource.lastIndexOf("?") + dbFileName=layerSource.left(pos)+".db" + + # remove all map layers that belong to dbFileName database + allLayers=QgsMapLayerRegistry.instance().mapLayers() + for ix in allLayers.keys(): + l=allLayers[ix] # layer object + if not l: + continue + + lSource=l.source() + if not lSource: + continue + + p=lSource.lastIndexOf("?") + dbFName=lSource.left(p)+".db" + + if dbFName==dbFileName and l.getLayerID()<>layer.getLayerID(): + self.removing=True + QgsMapLayerRegistry.instance().removeMapLayer(l.getLayerID(),True) + self.removing=False + + # removed map items of key + key=dbFileName.toLatin1().data() + del self.dbConns[key] + del self.pointLayers[key] + del self.lineLayers[key] + del self.polygonLayers[key] + + + def addDatabase(self,dbFileName,pointLayer,lineLayer,polygonLayer): + """Function provides possibility to add new OSM data. + + It's called (mainly) from OSM data loader. + New data (OSM database) is added into inner structures of DatabaseManager. + New database is automatically considered new current (!) OSM database. + + @param dbFileName filename of new OSM database + @param pointLayer pointer to QGIS vector layer that represents OSM points (of new data) + @param lineLayer pointer to QGIS vector layer that represents OSM lines (of new data) + @param polygonLayer pointer to QGIS vector layer that represents OSM polygons (of new data) + """ + + # put new sqlite3 db connection into map (rewrite if its already there) + key=dbFileName.toLatin1().data() + if key in self.dbConns.keys(): + self.dbConns[key].close() + del self.dbConns[key] + del self.pointLayers[key] + del self.lineLayers[key] + del self.polygonLayers[key] + + self.dbConns[key]=sqlite3.connect(key) + self.pointLayers[key]=pointLayer + self.lineLayers[key]=lineLayer + self.polygonLayers[key]=polygonLayer + self.currentKey=key + + # tell everyone that database changes + self.plugin.undoredo.databaseChanged(self.currentKey) + self.plugin.dockWidget.databaseChanged(self.currentKey) + + + def getConnection(self): + """Function finds out current OSM data and returns pointer to related sqlite3 database. + + @return pointer to sqlite3 database connection object of current OSM data + """ + + if not self.currentKey in self.dbConns.keys(): + return None + + dbConn=self.dbConns[self.currentKey] + if not dbConn: + return None # not OSM layer + + return dbConn + + + def __getFreeFeatureId(self): + """Finds out the highest feature id (less than zero), + that is still not used and can be assigned to new feature. + + @return free identifier to be assigned to new feature + """ + + c=self.getConnection().cursor() + c.execute("SELECT min(id) FROM (SELECT min(id) id FROM node UNION SELECT min(id) id FROM way UNION SELECT min(id) id FROM relation)") + for rec in c: + freeId = rec[0] + c.close() + + if freeId<0: + return freeId-1 + return -1 + + + def getTolerance(self): + """Functions finds out default tolerance from qgis settings. + + Required value (of qgis settings) is returned by QgsTolerance.defaultTolerance(...) calling. + If returned value equals 0.0, we ignore it and calculate our own tolerance from current extent width. + + @return default tolerance + """ + + qgisTol=QgsTolerance.defaultTolerance(self.pointLayers[self.currentKey],self.canvas.mapRenderer()) + + if not self.currentKey or qgisTol==0.0: + extent=self.canvas.extent() + if self.plugin.dockWidget.coordXform is not None: + extent=self.plugin.dockWidget.coordXform.transform(extent) + + w=extent.xMaximum()-extent.xMinimum() + return w/220 + + return qgisTol + + + def findFeature(self,mapPoint): + """Function finds exactly one feature at specified map point (within QGIS tolerance). + + It ignores the fact there can be more features at the same place. + Typical usage is simple marking features when cursor goes over them. + (use it anytime when you prefer speed to completeness) + + @param mapPoint point of the map where to search for feature + @return found feature - pair (QgsFeature object,featureType) + """ + + if not self.currentKey: # no layer loaded + return None + + # finds out tolerance for snapping + tolerance=self.getTolerance() + area=QgsRectangle(mapPoint.x()-tolerance,mapPoint.y()-tolerance,mapPoint.x()+tolerance,mapPoint.y()+tolerance) + + feat=QgsFeature() + lay=self.pointLayers[self.currentKey] + lay.select([],area,True,True) + result=lay.nextFeature(feat) + lay.dataProvider().rewind() + + if result: + return (feat,'Point') + + feat=QgsFeature() + lay=self.lineLayers[self.currentKey] + lay.select([],area,True,True) + result=lay.nextFeature(feat) + lay.dataProvider().rewind() + + if result: + # line vertices + c=self.getConnection().cursor() + c.execute("select n.id,n.lat,n.lon from node n,way_member wm where n.u=1 and wm.u=1 and wm.way_id=:lineId and wm.node_id=n.id and n.status<>'R' and n.lat>=:minLat and n.lat<=:maxLat and n.lon>=:minLon and n.lon<=:maxLon" + ,{"minLat":area.yMinimum(),"maxLat":area.yMaximum(),"minLon":area.xMinimum(),"maxLon":area.xMaximum(),"lineId":str(feat.id())}) + + for rec in c: + feat2=QgsFeature(rec[0],"Point") + feat2.setGeometry(QgsGeometry.fromPoint(QgsPoint(rec[2],rec[1]))) + # without features' attributes here! we don't need them... + c.close() + return (feat2,'Point') + + c.close() + return (feat,'Line') + + feat=QgsFeature() + lay=self.polygonLayers[self.currentKey] + lay.select([],area,True,True) + result=lay.nextFeature(feat) + lay.dataProvider().rewind() + + if result: + # polygon vertices + c=self.getConnection().cursor() + c.execute("select n.id,n.lat,n.lon from node n,way_member wm where n.u=1 and wm.u=1 and wm.way_id=:polygonId and wm.node_id=n.id and n.status<>'R' and n.lat>=:minLat and n.lat<=:maxLat and n.lon>=:minLon and n.lon<=:maxLon" + ,{"minLat":area.yMinimum(),"maxLat":area.yMaximum(),"minLon":area.xMinimum(),"maxLon":area.xMaximum(),"polygonId":str(feat.id())}) + + for rec in c: + feat2=QgsFeature(rec[0],"Point") + feat2.setGeometry(QgsGeometry.fromPoint(QgsPoint(rec[2],rec[1]))) + # without features' attributes here! we don't need them... + c.close() + return (feat2,'Point') + + c.close() + return (feat,'Polygon') + return None + + + def findAllFeatures(self,mapPoint): + """Function finds all features at specified map point (within QGIS tolerance). + (use it anytime when you prefer completeness to speed) + + @param mapPoint point of the map where to search for features + @return list of found features - pairs of (QgsFeature object,featureType) + """ + + if not self.currentKey: # no layer loaded + return [] + + foundPoints=[] + foundLines=[] + foundPolygons=[] + + # finds out tolerance for snapping + tolerance=self.getTolerance() + area=QgsRectangle(mapPoint.x()-tolerance,mapPoint.y()-tolerance,mapPoint.x()+tolerance,mapPoint.y()+tolerance) + + lay=self.pointLayers[self.currentKey] + lay.select([],area,True,True) + feat=QgsFeature() + result=lay.nextFeature(feat) + featMap={} + + while result: + foundPoints.append((feat,'Point')) + feat=QgsFeature() + result=lay.nextFeature(feat) + + lay=self.lineLayers[self.currentKey] + lay.select([],area,True,True) + feat=QgsFeature() + result=lay.nextFeature(feat) + + while result: + # line vertices + c=self.getConnection().cursor() + c.execute("select n.id,n.lat,n.lon from node n,way_member wm where n.u=1 and wm.u=1 and wm.way_id=:lineId and wm.node_id=n.id and n.status<>'R' and n.lat>=:minLat and n.lat<=:maxLat and n.lon>=:minLon and n.lon<=:maxLon" + ,{"minLat":area.yMinimum(),"maxLat":area.yMaximum(),"minLon":area.xMinimum(),"maxLon":area.xMaximum(),"lineId":str(feat.id())}) + + for rec in c: + feat2=QgsFeature(rec[0],"Point") + feat2.setGeometry(QgsGeometry.fromPoint(QgsPoint(rec[2],rec[1]))) + # without features' attributes here! we don't need them... + featMap[feat2.id()]=feat2 + + c.close() + + foundLines.append((feat,'Line')) + feat=QgsFeature() + result=lay.nextFeature(feat) + + lay=self.polygonLayers[self.currentKey] + lay.select([],area,True,True) + feat=QgsFeature() + result=lay.nextFeature(feat) + + while result: + # polygon vertices + c=self.getConnection().cursor() + c.execute("select n.id,n.lat,n.lon from node n,way_member wm where n.u=1 and wm.u=1 and wm.way_id=:polygonId and wm.node_id=n.id and n.status<>'R' and n.lat>=:minLat and n.lat<=:maxLat and n.lon>=:minLon and n.lon<=:maxLon" + ,{"minLat":area.yMinimum(),"maxLat":area.yMaximum(),"minLon":area.xMinimum(),"maxLon":area.xMaximum(),"polygonId":str(feat.id())}) + + for rec in c: + feat2=QgsFeature(rec[0],"Point") + feat2.setGeometry(QgsGeometry.fromPoint(QgsPoint(rec[2],rec[1]))) + # without features' attributes here! we don't need them... + featMap[feat2.id()]=feat2 + + c.close() + + foundPolygons.append((feat,'Polygon')) + feat=QgsFeature() + result=lay.nextFeature(feat) + + res=foundPoints + for key in featMap.keys(): + res.append((featMap[key],'Point')) + res=res+foundLines+foundPolygons + return res + + + def createPoint(self,mapPoint,snapFeat,snapFeatType,doCommit=True): + """Function creates new point. + + @param mapPoint is QgsPoint; it says where to create new point + @param snapFeat is QgsFeature object to which snapping is performed + @param snapFeatType is type of object to which snapping is performed + @param doCommit flag; tells if this function should call commit before it finishes + @return identifier of new node + """ + + # we need to get new id which will represents this new point in osm database + nodeId=self.__getFreeFeatureId() + + affected=set() + feat=QgsFeature(nodeId,"Point") + feat.setGeometry(QgsGeometry.fromPoint(QgsPoint(mapPoint.x(),mapPoint.y()))) + + # should snapping be done? if not, everything's easy + if not snapFeat: + c=self.getConnection().cursor() + c.execute("insert into node (id,lat,lon,usage,status) values (:nodeId,:lat,:lon,0,'A')" + ,{"nodeId":str(nodeId),"lat":str(mapPoint.y()),"lon":str(mapPoint.x())}) + self.insertTag(nodeId,"Point","created_by",self.pluginName,False) + c.close() + + if doCommit: + self.commit() + + affected.add((nodeId,'Point')) + return (feat,affected) + + if snapFeatType=='Point': + # what to do? we are trying to snap point to existing point + QMessageBox.information(self.plugin.dockWidget,"OSM point creation" + ,"Point creation failed. Two points cannot be at the same place.") + return (None,[]) + + # well, we are snapping to 'Line' or 'Polygon', actions are the same in both cases... + c=self.getConnection().cursor() + c.execute("insert into node (id,lat,lon,usage,status) values (:nodeId,:lat,:lon,1,'A')" + ,{"nodeId":str(nodeId),"lat":str(mapPoint.y()),"lon":str(mapPoint.x())}) + c.close() + + # finding out exact position index of closest vertex in line (or polygon) geometry + (a,b,vertexIx)=snapFeat.geometry().closestSegmentWithContext(mapPoint) + newMemberIx=vertexIx+1 + + # we need to shift indexes of all vertexes that will be after new vertex + d=self.getConnection().cursor() + d.execute("select way_id,pos_id from way_member where way_id=:wayId and pos_id>:posId and u=1 order by pos_id desc" + ,{"wayId":str(snapFeat.id()),"posId":str(vertexIx)}) + + # for all such vertexes + for rec in d: + posId=rec[1] # original position index + c.execute("update way_member set pos_id=:newPosId where way_id=:wayId and pos_id=:posId and u=1" + ,{"wayId":str(snapFeat.id()),"posId":str(posId),"newPosId":str(posId+1)}) + d.close() + + # putting new node into set of lines/polygons members + c.execute("insert into way_member (way_id,node_id,pos_id) values (:wayId,:nodeId,:posId)" + ,{"wayId":str(snapFeat.id()),"nodeId":str(nodeId),"posId":str(newMemberIx)}) + + # original line/polygon geometry must be forgotten (new one will be created by provider on canvas refresh) + c.execute("update way set wkb=:wkb where id=:wayId and u=1" + ,{"wkb":sqlite3.Binary(""),"wayId":str(snapFeat.id())}) + + if snapFeatType=='Line': + self.changeLineStatus(snapFeat.id(),"N","U") + elif snapFeatType=='Polygon': + self.changePolygonStatus(snapFeat.id(),"N","U") + + # insert created_by tag of the new point + self.insertTag(nodeId,"Point","created_by",self.pluginName,False) + + # finishing + c.close() + + if doCommit: + self.commit() + + affected.add((nodeId,'Point')) + if snapFeatType=='Line': + affected.add((snapFeat.id(),'Line')) + elif snapFeatType=='Polygon': + affected.add((snapFeat.id(),'Polygon')) + + return (feat,affected) + + + def createLine(self,mapPoints, doCommit=True): + """Function creates new line. + + @param mapPoints is list of line points + @param doCommit flag; tells if this function should call commit before it finishes + @return identifier of new line + """ + + # getting id which will represent this new line in osm database + lineId=self.__getFreeFeatureId() + + affected=set() + pline=[] + + # creating database cursor and inits... + c=self.getConnection().cursor() + cnt=len(mapPoints) + i,minLat,minLon,maxLat,maxLon=0,9999999,9999999,-9999999,-9999999 + + # go through the line points + for (mapPoint,snapFeat,snapFeatType) in mapPoints: + + lat=mapPoint.y() + lon=mapPoint.x() + pline.append(QgsPoint(mapPoint.x(),mapPoint.y())) + + # we need to know max and min latitude/longitude of the whole line (its boundary) + if latmaxLat: + maxLat=lat + if lon>maxLon: + maxLon=lon + + nodeId=None + if snapFeat: + if snapFeatType=='Point': + nodeId=snapFeat.id() + # update record of existing node to which we has snapped + c.execute("update node set usage=usage+1 where id=:nodeId and u=1" + ,{"nodeId":str(nodeId)}) + else: + # snapping to non-point features is forbidden! + return (None,[]) + + else: + # this vertex is a new one (no snapping to existing one) + nodeId=lineId-i-1 + c.execute("insert into node (id,lat,lon,usage,status) values (:nodeId,:lat,:lon,1,'A')" + ,{ "nodeId":str(nodeId),"lat":str(lat),"lon":str(lon) }) + + self.insertTag(nodeId,"Point","created_by",self.pluginName, False) + affected.add((nodeId,'Point')) + + # insert record into table of way members + c.execute("insert into way_member (way_id,pos_id,node_id) values (:wayId,:posId,:nodeId)",{ + "wayId":str(lineId) + ,"posId":str(i+1) + ,"nodeId":str(nodeId) + }) + i=i+1 + + # create half-empty database record for new line + c.execute("insert into way (id,wkb,membercnt,closed,min_lat,min_lon,max_lat,max_lon,status) values (?,?,?,0,?,?,?,?,'A')" + ,(str(lineId),sqlite3.Binary(""),str(cnt),str(minLat),str(minLon),str(maxLat),str(maxLon))) + + self.insertTag(lineId,"Line","created_by",self.pluginName, False) + + # finishing... + c.close() + feat=QgsFeature(lineId,"Line") + feat.setGeometry(QgsGeometry.fromPolyline(pline)) + + if doCommit: + self.commit() + + affected.add((lineId,'Line')) + return (feat,affected) + + + def createPolygon(self,mapPoints, doCommit=True): + """Function creates new polygon. + + @param mapPoints is list of polygon points + @param doCommit flag; tells if this function should call commit before it finishes + @return identifier of new polygon + """ + + # getting id which will represents this new polygon in osm database + polygonId=self.__getFreeFeatureId() + + affected=set() + pline=[] + + # creating database cursor and inits... + c=self.getConnection().cursor() + cnt=len(mapPoints) + i,minLat,minLon,maxLat,maxLon=0,9999999,9999999,-9999999,-9999999 + + # go through the polygon points + for (mapPoint,snapFeat,snapFeatType) in mapPoints: + + lat=mapPoint.y() + lon=mapPoint.x() + pline.append(QgsPoint(mapPoint.x(),mapPoint.y())) + + # we need to know max and min latitude/longitude of the whole polygon (its boundary) + if latmaxLat: + maxLat=lat + if lon>maxLon: + maxLon=lon + + nodeId=None + if snapFeat: + if snapFeatType=='Point': + nodeId=snapFeat.id() + # update record of existing node to which snapping was done + c.execute("update node set usage=usage+1 where id=:nodeId and u=1" + ,{"nodeId":str(nodeId)}) + else: + # snapping to non-point feature is not allowed + nodeId=None + + else: + # this vertex is a new one (no snapping to existing one) + nodeId=polygonId-i-1 + c.execute("insert into node (id,lat,lon,usage,status) values (:nodeId,:lat,:lon,1,'A')" + ,{ "nodeId":str(nodeId),"lat":str(lat),"lon":str(lon) }) + + self.insertTag(nodeId,"Point","created_by",self.pluginName, False) + affected.add((nodeId,'Point')) + + # insert record into table of way members + c.execute("insert into way_member (way_id,pos_id,node_id) values (:wayId,:posId,:nodeId)",{ + "wayId":str(polygonId) + ,"posId":str(i+1) + ,"nodeId":str(nodeId) + }) + i=i+1 + + # create half-empty database record for new polygon + c.execute("insert into way (id,wkb,membercnt,closed,min_lat,min_lon,max_lat,max_lon,status) values (?,?,?,1,?,?,?,?,'A')" + ,(str(polygonId),sqlite3.Binary(""),str(cnt),str(minLat),str(minLon),str(maxLat),str(maxLon))) + + self.insertTag(polygonId,"Polygon","created_by",self.pluginName, False) + + # finish + c.close() + feat=QgsFeature(polygonId,"Polygon") + polygon=[] + polygon.append(pline) + feat.setGeometry(QgsGeometry.fromPolygon(polygon)) + + if doCommit: + self.commit() + + affected.add((polygonId,'Polygon')) + return (feat,affected) + + + def createRelation(self,relType,relMems): + """Function creates new relation. + + @param relType is name of relation type + @param relMems is list of relation members + @return unique identifier of new relation + """ + + # we need to get new id which will represents new relation in osm database + relId=self.__getFreeFeatureId() + + c=self.getConnection().cursor() + + # insert relation record + c.execute("insert into relation (id, type, timestamp, user, status) values (:id, :type, :timestamp, :user, :status)" + ,{"id":relId,"type":relType,"timestamp":'',"user":'',"status":"A"}) + + # insert relation members records into "relation_member" table + posId=1 + for relMem in relMems: + + memId=relMem[0] + memType=relMem[1] + memRole=relMem[2] + + osmType=self.convertToOsmType(memType) + c.execute("insert into relation_member (relation_id,pos_id,member_id,member_type,role) values (?,?,?,?,?)" + ,(str(relId),posId,memId,osmType,memRole)) + + # increase position number + posId=posId+1 + + c.close() + return relId + + + def removePoint(self,pointId): + """Function removes an existing point. + Point is given by its identifier. + + @param pointId identifier of point to remove + @return list of all features affected with this operation + """ + + affected=set() + + # first change status of point to 'R' ~ Removed + self.changePointStatus(pointId,None,"R") + + # remove all points' tags + self.removeFeaturesTags(pointId,"Point") + + # remove point from all lines + affeA=self.__removePointFromAllLines(pointId) + + # remove point from all polygons + affeB=self.__removePointFromAllPolygons(pointId) + + # remove point from all its relations + self.__removeFeatureFromAllRelations(pointId,"Point") + + self.commit() + + affected.add((pointId,'Point')) + affected.update(affeA) + affected.update(affeB) + + return affected + + + def __removeIsolatedPoint(self,pointId): + """Removes an existing point which is not part of any other feature. + Point is given by its identifier. + Function must be private, coz' it doesn't verify if point is really isolated. + + @param pointId is a point identifier + """ + + # first change status of point to 'R' ~ Removed + self.changePointStatus(pointId,None,"R") + + # remove all points' tags + self.removeFeaturesTags(pointId,"Point") + + # remove point from all its relations + self.__removeFeatureFromAllRelations(pointId,"Point") + + self.commit() + + + def __removePointFromAllPolygons(self,pointId): + """Function removes given point from all polygons. + It's possible that point has multiple occurance in the same polygon, + function removes all the occurances in all polygons. + + @param pointId identifier of point to remove + @return list of all features affected with this operation + """ + + affected=set() + c=self.getConnection().cursor() + c.execute("select w.id from way w where w.closed=1 and w.u=1 and exists(select 1 from way_member wm \ + where wm.node_id=:pointId and wm.way_id=w.id and wm.u=1)" + ,{"pointId":str(pointId)}) + + for rec in c: + polId=rec[0] + aff=self.__removePointFromPolygon(polId,pointId) + affected.update(aff) + + # and that's all + c.close() + self.commit() + + affected.add((pointId,'Point')) + return affected + + + def __removePointFromPolygon(self,polygonId,pointId): + """Function removes given point from given polygon. + It's possible that point has multiple occurance in the polygon, + function removes all such occurances. + + @param polygonId identifier of polygon to remove point from + @param pointId identifier of point to remove + @return list of all features affected with this operation + """ + + affected=set() + c=self.getConnection().cursor() + + # find all occurences of point in polygon (get list of position identifiers) + c.execute("select pos_id from way_member where way_id=:polygonId and node_id=:pointId and u=1" + ,{"polygonId":str(polygonId),"pointId":str(pointId)}) + + for rec in c: + aff=self.__removePolygonMember(polygonId,pointId,rec[0]) + affected.update(aff) + + c.execute("update node set usage=usage-1 where id=:pointId and u=1 and usage>0" + ,{"pointId":str(pointId)}) + + # mark polygon as 'Updated' and clear its geometry to make provider able to generate a new one + c.execute("update way set wkb=:wkb where id=:polygonId and u=1" + ,{"wkb":sqlite3.Binary(""),"polygonId":str(polygonId)}) + self.changePolygonStatus(polygonId,"N","U") + + # verify that polygon has still sufficient number of members (>2) required by polygon definition + c.execute("select count(pos_id) from way_member where way_id=:polygonId and u=1" + ,{"polygonId":str(polygonId)}) + + polMembersCnt=0 + for rec in c: + polMembersCnt=rec[0] + + if polMembersCnt<1: + # the feature can no longer be called "polygon", it has no members + aff=self.removePolygon(polygonId,True) + affected.update(aff) + + elif polMembersCnt<3: + # the feature can no longer be called "polygon", is it a line now? + aff=self.__convertPseudoPolygonToLine(polygonId) + affected.update(aff) + + # a-a-and that's all folks! + c.close() + self.commit() + + affected.add((polygonId,'Polygon')) + affected.add((pointId,'Point')) + + return affected + + + def __removePolygonMember(self,polygonId,pointId,posId): + """Function removes exactly one member given by its position in polygon. + + "Polygon member" is interpreted as exactly one occurance of a point in a polygon. + So... two members of the same polygon can still be the same point. + + All polygon members that were at higher positions will get new position numbers (their oldPosition-1). + + @param polygonId identifier of polygon to remove member from + @param pointId identifier of point + @param posId identifier of exact position from where to remove a point + @return list of all features affected with this operation + """ + + affected=set() + c=self.getConnection().cursor() + c.execute("delete from way_member where way_id=:polygonId and pos_id=:posId and u=1" + ,{"polygonId":str(polygonId),"posId":str(posId)}) + + c.execute("update way_member set pos_id=pos_id-1 where way_id=:polygonId and pos_id>:posId and u=1" + ,{"polygonId":str(polygonId),"posId":str(posId)}) + c.close() + + affected.add((polygonId,'Polygon')) + affected.add((pointId,'Point')) + + return affected + + + def __removePointFromAllLines(self,pointId): + """Function removes given point from all lines. + It's possible that point has multiple occurance in the same line, + function removes all the occurances in all lines. + + @param pointId identifier of point to remove + @return list of all features affected with this operation + """ + + affected=set() + c=self.getConnection().cursor() + c.execute("select w.id from way w where w.closed=0 and w.u=1 and exists(select 1 from way_member wm \ + where wm.node_id=:pointId and wm.way_id=w.id and wm.u=1)" + ,{"pointId":str(pointId)}) + + for rec in c: + lineId=rec[0] + aff=self.__removePointFromLine(lineId,pointId) + affected.update(aff) + + # and that's all + c.close() + self.commit() + + affected.add((pointId,'Point')) + return affected + + + def __removePointFromLine(self,lineId,pointId): + """Function removes given point from given line. + It's possible that point has multiple occurance in the line, + function removes all such occurances. + + @param lineId identifier of line to remove point from + @param pointId identifier of point to remove + @return list of all features affected with this operation + """ + + affected=set() + c=self.getConnection().cursor() + # find all occurences of point in line (get list of position identifiers) + c.execute("select pos_id from way_member where way_id=:lineId and node_id=:pointId and u=1" + ,{"lineId":str(lineId),"pointId":str(pointId)}) + + for rec in c: + aff=self.__removeLineMember(lineId,pointId,rec[0]) + affected.update(aff) + + c.execute("update node set usage=usage-1 where id=:pointId and u=1 and usage>0" + ,{"pointId":str(pointId)}) + + # mark line as 'Updated' and clear its geometry to make provider able to generate a new one + c.execute("update way set wkb=:wkb where id=:lineId and u=1" + ,{"wkb":sqlite3.Binary(""),"lineId":str(lineId)}) + self.changeLineStatus(lineId,"N","U") + + # verify that line has still sufficient number of members (>1) required by line definition + c.execute("select count(pos_id) from way_member where way_id=:lineId and u=1" + ,{"lineId":str(lineId)}) + + lineMembersCnt=0 + for rec in c: + lineMembersCnt=rec[0] + + c.close() + self.commit() + + if lineMembersCnt<2: + # the feature can no longer be called "line", is it a point now? + aff=self.__convertPseudoLineToPoint(lineId) + affected.update(aff) + + # a-a-and that's all folks! + affected.add((pointId,'Point')) + affected.add((lineId,'Line')) + + return affected + + + def __removeLineMember(self,lineId,pointId,posId): + """Function removes exactly one member given by its position in line. + + "Line member" is interpreted as exactly one occurance of a point in a line. + So... two members of the same line can still be the same point. + + All line members that were at higher positions will get new position numbers (their oldPosition-1). + + @param lineId identifier of line to remove member from + @param pointId identifier of point + @param posId identifier of exact position from where to remove a point + @return list of all features affected with this operation + """ + + affected=set() + c=self.getConnection().cursor() + c.execute("delete from way_member where way_id=:lineId and pos_id=:posId and u=1" + ,{"lineId":str(lineId),"posId":str(posId)}) + + c.execute("update way_member set pos_id=pos_id-1 where way_id=:lineId and pos_id>:posId and u=1" + ,{"lineId":str(lineId),"posId":str(posId)}) + c.close() + + affected.add((lineId,'Line')) + affected.add((pointId,'Point')) + + return affected + + + def removeLine(self,lineId,delMembers=True): + """Function removes an existing line. + Line is given by its identifier. + + @param lineId identifier of line to remove + @param delMembers if True line will be removed with all its "isolated" (after line's removing) points; if False points won't be removed + @return list of all features affected with this operation + """ + + affected=set() + # first change status of line to 'R' ~ Removed + self.changeLineStatus(lineId,None,"R") + + # remove all lines' tags + self.removeFeaturesTags(lineId,"Line") + + if delMembers: + # just remove all points for which this line is the only line/polygon they are members of + points=[] + c=self.getConnection().cursor() + c.execute("select id from node where exists( select 1 from way_member wm where wm.node_id=id and wm.way_id=:lineId and wm.u=1 ) \ + and usage=1 and u=1 and status<>'R'",{"lineId":str(lineId)}) + # collection ids of line members + for rec in c: + points.append(rec[0]) + c.close() + + # now remove them one by one + for pId in points: + self.__removeIsolatedPoint(pId) + affected.add((pId,'Point')) + else: + # all points has to be removed from line + points=[] + c=self.getConnection().cursor() + c.execute("select node_id from way_member wm where wm.way_id=:lineId and wm.u=1" + ,{"lineId":str(lineId)}) + # collection ids of line members + for rec in c: + points.append(rec[0]) + c.close() + + # now remove them one by one + for pId in points: + aff=self.__removePointFromLine(lineId,pId) + affected.update(aff) + + # remove all relevant line-point connections + c=self.getConnection().cursor() + c.execute("delete from way_member where way_id=:lineId and u=1" + ,{"lineId":str(lineId)}) + + c.close() + + # don't forget to remove line from existing relations + self.__removeFeatureFromAllRelations(lineId,"Line") + self.commit() + + affected.add((lineId,'Line')) + return affected + + + def removePolygon(self,polId,delMembers=True): + """Function removes an existing polygon. + Polygon is given by its identifier. + + @param polId identifier of polygon to remove + @param delMembers if True polygon will be removed with all its "isolated" (after line's removing) points; if False points won't be removed + @return list of all features affected with this operation + """ + + affected=set() + # first change status of polygon to 'R' ~ Removed + self.changeLineStatus(polId,None,"R") + + # remove all polygons' tags + self.removeFeaturesTags(polId,"Line") + + if delMembers: + # just remove all points for which this polygon is the only line/polygon they are members of + points=[] + c=self.getConnection().cursor() + c.execute("select id from node where exists( select 1 from way_member wm where wm.node_id=id and wm.way_id=:polId and wm.u=1 ) \ + and usage=1 and u=1",{"polId":str(polId)}) + # collection ids of line members + for rec in c: + points.append(rec[0]) + c.close() + + # now remove them one by one + for pId in points: + self.__removeIsolatedPoint(pId) + affected.add((pId,'Point')) + + else: + # all points has to be removed from polygon + points=[] + c=self.getConnection().cursor() + c.execute("select node_id from way_member wm where wm.way_id=:polId and wm.u=1" + ,{"polId":str(polId)}) + + # collection ids of line members + for rec in c: + points.append(rec[0]) + c.close() + + # now remove them one by one + for pId in points: + aff=self.__removePointFromPolygon(polId,pId) + affected.update(aff) + + # remove all relevant polygon-point connections + c=self.getConnection().cursor() + c.execute("delete from way_member where way_id=:polId and u=1" + ,{"polId":str(polId)}) + + c.close() + + # don't forget to remove polygon from existing relations + self.__removeFeatureFromAllRelations(polId,"Polygon") + + self.commit() + + affected.add((polId,'Polygon')) + return affected + + + def removeRelation(self,relId): + """Function removes an existing relation. + Relation is given by its identifier. + + @param relId identifier of relation to remove + """ + + c=self.getConnection().cursor() + c.execute("delete from relation_member where relation_id=:relId and u=1" + ,{"relId":str(relId)}) + + c.execute("update relation set status='R' where id=:relId and u=1" + ,{"relId":str(relId)}) + + # remove all tags connected to the relation + c.execute("delete from tag where object_id=:relId and object_type='relation' and u=1" + ,{"relId":str(relId)}) + + # don't forget to remove relation (as member) from existing relations + self.__removeFeatureFromAllRelations(relId,"Relation") + + # finish + c.close() + + + def __removeFeatureFromAllRelations(self,featId,featType): + """Removes feature from all its relations. + + If feature occures at more than one position in some relation, all its occurences will be removed. + If some relation becomes empty with members deletion, the whole relation is removed. + + @param featId identifier of feature to remove + @param featType type of feature to remove + """ + + c=self.getConnection().cursor() + osmType=self.convertToOsmType(featType) + + # remove feature from all its relations + c.execute("select relation_id from relation_member where member_id=:memId and member_type=:memType and u=1" + ,{"memId":str(featId),"memType":osmType}) + + for rec in c: + relId=rec[0] + self.__removeFeatureFromRelation(relId,featId,featType) + + # and that's all + c.close() + + + def changeAllRelationMembers(self,relId,newMembers): + """Function first removes all relation members and then inserts new ones. + + @param relId identifier of relation + @param newMembers new relation members + """ + + c=self.getConnection().cursor() + c.execute("delete from relation_member where relation_id=:relId" + ,{"relId":str(relId)}) + + # insert relation members records into "relation_member" table + posId=1 + for relMem in newMembers: + + memId=relMem[0] + memType=relMem[1] + memRole=relMem[2] + + osmType=self.convertToOsmType(memType) + c.execute("insert into relation_member (relation_id,pos_id,member_id,member_type,role) values (?,?,?,?,?)" + ,(str(relId),posId,memId,osmType,memRole)) + + # increase position number + posId=posId+1 + c.close() + + + def __removeFeatureFromRelation(self,relId,featId,featType): + """Funcion removes feature from given relation. + + If feature occures at more than one position in relation, all its occurences are removed. + If given relation becomes empty with members deletion, the whole relation is removed. + + @param relId identifier of relation + @param featId identifier of feature to remove + @param featType type of feature to remove + """ + + c=self.getConnection().cursor() + osmType=self.convertToOsmType(featType) + + # find all occurences of feature in relation (get list of position identifiers) + c.execute("select pos_id from relation_member where relation_id=:relId and member_id=:memId and member_type=:memType and u=1" + ,{"relId":str(relId),"memId":str(featId),"memType":osmType}) + + for rec in c: + self.__removeRelationMember(relId,rec[0]) + + c.execute("select count(pos_id) from relation_member where relation_id=:relId and u=1" + ,{"relId":str(relId)}) + + relMembersCnt=0 + for rec in c: + relMembersCnt=rec[0] + + if relMembersCnt==0: + self.removeRelation(relId) + + c.close() + + + def __removeRelationMember(self,relId,posId): + """Function removes exactly one member given by its position in line. + + @param relId identifier of relation to remove member from + @param posId member position + """ + + c=self.getConnection().cursor() + c.execute("delete from relation_member where relation_id=:relId and pos_id=:posId and u=1" + ,{"relId":str(relId),"posId":str(posId)}) + + c.execute("update relation_member set pos_id=pos_id-1 where relation_id=:relId and pos_id>:posId and u=1" + ,{"relId":str(relId),"posId":str(posId)}) + c.close() + + + def movePoint(self,feat,deltaX,deltaY,snapFeat=None,snapFeatType=None): + """Function moves an existing point. + It performs commit. + + @param feat QgsFeature object of feature to move + @param deltaX how far to move on X axis + @param deltaY how far to move on Y axis + @param snapFeat QgsFeature object of feature to where snapping is performed + @param snapFeatType type of feature to where snapping is performed + @return list of all features affected with this operation + """ + + affected=set() + # count new coordinates + point=feat.geometry().asPoint() + newLat=point.y()+deltaY + newLon=point.x()+deltaX + targetPoint=QgsPoint(newLon,newLat) + d=self.getConnection().cursor() + + if not snapFeat: + # change node record; new coordinates + d.execute("update node set lat=:lat,lon=:lon where id=:nodeId and u=1" + ,{"lat":str(newLat),"lon":str(newLon),"nodeId":str(feat.id())}) + self.changePointStatus(feat.id(),"N","U") + + # if point belongs to line/polygon, geometry of them must be deleted (osm provider will make new one later) + d.execute("select wm.way_id,w.closed from way_member wm,way w where wm.node_id=:nodeId and wm.way_id=w.id and w.u=1 and wm.u=1" + ,{"nodeId":str(feat.id())}) + + ways=[] + for rec in d: + t='Line' + if rec[1]==1: + t='Polygon' + ways.append((rec[0],t)) + + for (wayId,t) in ways: + + d.execute("update way set wkb=:wkb where id=:wayId and u=1" + ,{"wkb":sqlite3.Binary(""),"wayId":str(wayId)}) + if t=='Line': + self.changeLineStatus(wayId,"N","U") + else: + self.changePolygonStatus(wayId,"N","U") + affected.add((wayId,t)) + + d.close() + # well, finishing.. + self.commit() + + affected.add((feat.id(),'Point')) + return affected + + # well, snapping was done + if snapFeatType=='Point': + # merging two points + aff=self.mergeTwoPoints(snapFeat.id(),feat.id()) + affected.update(aff) + d.close() + self.commit() + return affected + + # well, we snapped to 'Line' or 'Polygon', actions are same in both cases + d.execute("update node set lat=:lat,lon=:lon,usage=usage+1 where id=:nodeId and u=1" + ,{"lat":str(newLat),"lon":str(newLon),"nodeId":str(feat.id())}) + self.changePointStatus(feat.id(),"N","U") + affected.add((feat.id(),'Point')) + + # finding out exact position index of closest vertex in line (or polygon) geometry + (a,b,vertexIx)=snapFeat.geometry().closestSegmentWithContext(targetPoint) + newMemberIx=vertexIx+1 + + # we need to shift indexes of all vertexes that will be after new vertex + d.execute("select pos_id from way_member where way_id=:wayId and pos_id>:posId and u=1 order by pos_id desc" + ,{"wayId":str(snapFeat.id()),"posId":str(vertexIx)}) + + # for all such vertexes + for rec in d: + posId=rec[0] # original position index + + e=self.getConnection().cursor() + e.execute("update way_member set pos_id=:newPosId where way_id=:wayId and pos_id=:posId and u=1" + ,{"wayId":str(snapFeat.id()),"posId":str(posId),"newPosId":str(posId+1)}) + e.close() + affected.add((snapFeat.id(),snapFeatType)) + + # putting new node into set of lines/polygons members + d.execute("insert into way_member (way_id,node_id,pos_id) values (:wayId,:nodeId,:posId)" + ,{"wayId":str(snapFeat.id()),"nodeId":str(feat.id()),"posId":str(newMemberIx)}) + + + # if point belongs to line/polygon, geometry of them must be deleted (osm provider will make new one later) + d.execute("select wm.way_id,w.closed from way_member wm,way w where wm.node_id=:nodeId and wm.way_id=w.id and w.u=1 and wm.u=1" + ,{"nodeId":str(feat.id())}) + + ways=[] + for rec in d: + t='Line' + if rec[1]==1: + t='Polygon' + ways.append((rec[0],t)) + + for (wayId,t) in ways: + d.execute("update way set wkb=:wkb where id=:wayId and u=1" + ,{"wkb":sqlite3.Binary(""),"wayId":str(wayId)}) + if t=='Line': + self.changeLineStatus(wayId,"N","U") + else: + self.changePolygonStatus(wayId,"N","U") + affected.add((wayId,t)) + + # ending transaction + d.close() + self.commit() + + return affected + + + def moveLine(self,feat,deltaX,deltaY,snapFeat=None,snapFeatType=None,snapIndex=-1): + """Function moves an existing line. + It performs commit. + + @param feat QgsFeature object of feature to move + @param deltaX how far to move on X axis + @param deltaY how far to move on Y axis + @param snapFeat QgsFeature object of feature to where snapping is performed + @param snapFeatType type of feature to where snapping is performed + @param snapIndex vertex index of snapFeat to which snapping was done + @return list of all features affected with this operation + """ + + affected=set() + if snapFeat and snapFeatType<>'Point': + return affected + + # we are moving line with its context; now if snapFeat is not None, exactly one of lines' members + # has to be merged with another existing node + c=self.getConnection().cursor() + c.execute("select n.id,n.lat,n.lon,wm.pos_id from node n, way_member wm where wm.node_id=n.id and wm.way_id=:wayId and n.u=1 and wm.u=1" + ,{"wayId":str(feat.id())}) + + # going through all line members + for rec in c: + nodeId=rec[0] + # count new coordinates + newLat=rec[1]+deltaY + newLon=rec[2]+deltaX + posId=rec[3] + + if snapFeat and posId==snapIndex+1: + # merging two points => snapping two features of non-point type together + aff=self.mergeTwoPoints(nodeId,snapFeat.id()) + affected.update(aff) + + # and changing their coordinates + d=self.getConnection().cursor() + d.execute("update node set lat=:lat,lon=:lon where id=:nodeId and u=1" + ,{"lat":str(newLat),"lon":str(newLon),"nodeId":str(nodeId)}) + self.changePointStatus(nodeId,"N","U") + affected.add((nodeId,'Point')) + + # delete all geometries that contains node being moved + d.execute("select wm.way_id,w.closed from way_member wm,way w where wm.node_id=:nodeId and wm.way_id=w.id and w.u=1 and wm.u=1" + ,{"nodeId":str(nodeId)}) + + ways=[] + for rec in d: + t='Line' + if rec[1]==1: + t='Polygon' + ways.append((rec[0],t)) + + for (wayId,t) in ways: + d.execute("update way set wkb=:wkb where id=:wayId and u=1" + ,{"wkb":sqlite3.Binary(""),"wayId":str(wayId)}) + if t=='Line': + self.changeLineStatus(wayId,"N","U") + else: + self.changePolygonStatus(wayId,"N","U") + affected.add((wayId,t)) + d.close() + + # finally delete geometry of line (osm provider will make new one later) + c.execute("update way set wkb=:wkb where id=:wayId and u=1" + ,{"wkb":sqlite3.Binary(""),"wayId":str(feat.id())}) + self.changeLineStatus(feat.id(),"N","U") + + # finish it + c.close() + self.commit() + + affected.add((feat.id(),'Line')) + return affected + + + def movePolygon(self,feat,deltaX,deltaY,snapFeat=None,snapFeatType=None,snapIndex=-1): + """Function moves an existing polygon. + It performs commit. + + @param feat QgsFeature object of feature to move + @param deltaX how far to move on X axis + @param deltaY how far to move on Y axis + @param snapFeat QgsFeature object of feature to where snapping is performed + @param snapFeatType type of feature to where snapping is performed + @param snapIndex vertex index of snapFeat to which snapping was done + @return list of all features affected with this operation + """ + + affected=set() + if snapFeat and snapFeatType<>'Point': + return affected + + # we are moving polygon with its context; now if snapFeat is not None, exactly one of polygons' members + # has to be merged with another existing node + c=self.getConnection().cursor() + c.execute("select n.id,n.lat,n.lon,wm.pos_id from node n, way_member wm where wm.node_id=n.id and wm.way_id=:wayId and n.u=1 and wm.u=1" + ,{"wayId":str(feat.id())}) + + # going through all polygon members + for rec in c: + nodeId=rec[0] + # count new coordinates + newLat=rec[1]+deltaY + newLon=rec[2]+deltaX + posId=rec[3] + + if snapFeat and posId==snapIndex+1: + # merging two points => snapping two features of non-point type together + aff=self.mergeTwoPoints(nodeId,snapFeat.id()) + affected.update(aff) + + # and changing their coordinates.. + d=self.getConnection().cursor() + d.execute("update node set lat=:lat,lon=:lon where id=:nodeId and u=1" + ,{"lat":str(newLat),"lon":str(newLon),"nodeId":str(nodeId)}) + self.changePointStatus(nodeId,"N","U") + affected.add((nodeId,'Point')) + + # delete all geometries that contains node being moved + d.execute("select wm.way_id,w.closed from way_member wm,way w where wm.node_id=:nodeId and wm.way_id=w.id and w.u=1 and wm.u=1" + ,{"nodeId":str(nodeId)}) + + ways=[] + for rec in d: + t='Line' + if rec[1]==1: + t='Polygon' + ways.append((rec[0],t)) + + for (wayId,t) in ways: + d.execute("update way set wkb=:wkb where id=:wayId and u=1" + ,{"wkb":sqlite3.Binary(""),"wayId":str(wayId)}) + if t=='Line': + self.changeLineStatus(wayId,"N","U") + else: + self.changePolygonStatus(wayId,"N","U") + affected.add((wayId,t)) + d.close() + + # finally delete geometry of polygon (osm provider will make new one later) + c.execute("update way set wkb=:wkb where id=:wayId and u=1" + ,{"wkb":sqlite3.Binary(""),"wayId":str(feat.id())}) + self.changeLineStatus(feat.id(),"N","U") + + # finish it + c.close() + self.commit() + + affected.add((feat.id(),'Polygon')) + return affected + + + def mergeTwoPoints(self,firstId,secondId): + """Function merges two existing points. + + Second point (second parameter of function) will be removed. + First point will get all tags/properties of second point. If both points has tag with the same key, + new tag is created for the first point with key "oldkey_1" and value of second point's tag. + + All features and relations that contain the second point will contain the first point instead. + + @param firstId id of first node to merge + @param firstId id of second node to merge + @return list of all features affected with this operation + """ + + affected=set() + + # delete all geometries that contain secondId-node + d=self.getConnection().cursor() + d.execute("select wm.way_id, w.closed, w.membercnt, \ + (select node_id from way_member where way_id=w.id and pos_id=1) first_id, \ + (select node_id from way_member where way_id=w.id and pos_id=w.membercnt) last_id \ + from way_member wm,way w where wm.node_id=:nodeId and wm.way_id=w.id and w.u=1 and wm.u=1" + ,{"nodeId":str(secondId)}) + ways=[] + for rec in d: + t='Line' + if rec[1]==1: + t='Polygon' + ways.append((rec[0],t,rec[2],rec[3],rec[4])) + + for (wayId,t,memcnt,fid,lid) in ways: + + if t=='Line': + # check if the line is changing into polygon with this mergePoints action + if fid==secondId: + fid=firstId + if lid==secondId: + lid=firstId + + if fid==lid: + d.execute("update way set wkb=:wkb,closed=1 where id=:wayId and u=1" + ,{"wkb":sqlite3.Binary(""),"wayId":str(wayId)}) + self.changePolygonStatus(wayId,"N","U") + d.execute("delete from way_member where way_id=:wayId and node_id=:lastId and pos_id=:posId and u=1" + ,{"wayId":str(wayId),"lastId":str(lid),"posId":str(memcnt)}) + affected.add((wayId,'Polygon')) + else: + d.execute("update way set wkb=:wkb where id=:wayId and u=1" + ,{"wkb":sqlite3.Binary(""),"wayId":str(wayId)}) + self.changeLineStatus(wayId,"N","U") + else: + d.execute("update way set wkb=:wkb where id=:wayId and u=1" + ,{"wkb":sqlite3.Binary(""),"wayId":str(wayId)}) + self.changePolygonStatus(wayId,"N","U") + + affected.add((wayId,t)) + + # finding out new "usage" column value for firstId-node + d.execute("select count(distinct way_id) from way_member where node_id in (:firstId,:secondId) and u=1" + ,{"firstId":str(firstId),"secondId":str(secondId)}) + row=d.fetchone() + usage=row[0] + + d.execute("update way_member set node_id=:firstId where node_id=:secondId and u=1" + ,{"firstId":str(firstId),"secondId":str(secondId)}) + + d.execute("update tag set object_id=:firstId, key=key||'_1' where object_id=:secondId and object_type='node' and u=1" + ,{"firstId":str(firstId),"secondId":str(secondId)}) + + d.execute("update node set status='R' where id=:secondId and u=1" + ,{"secondId":str(secondId)}) + + d.execute("update node set usage=:usage where id=:firstId and u=1" + ,{"usage":str(usage),"firstId":str(firstId)}) + + d.execute("update relation_member set member_id=:firstId where member_id=:secondId and member_type='node' and u=1" + ,{"firstId":str(firstId),"secondId":str(secondId)}) + d.close() + + affected.add((firstId,'Point')) + affected.add((secondId,'Point')) + return affected + + + def getFeatureOwner(self,featId,featType): + """Gets login of user who created/uploaded given feature. + Feature is given by its identifier and type. + + @param featId id of feature + @param featType type of feature + @return login of user who creates/lately-edits this feature + """ + + featOwner=None + c=self.getConnection().cursor() + c.execute("select user from node where id=:featId and status<>'R' and u=1 union select user from way where id=:featId and status<>'R' and u=1" + ,{"featId":str(featId)}) + + for rec in c: + featOwner=rec[0] + + c.close() + return featOwner + + + def getFeatureCreated(self,featId,featType): + """Gets timestamp of when given feature was created/uploaded. + Feature is given by its identifier and type. + + @param featId id of feature + @param featType type of feature + @return timestamp of when this feature was created/uploaded. + """ + + featCreated=None + c=self.getConnection().cursor() + c.execute("select timestamp from node where id=:featId and status<>'R' and u=1 \ + union select timestamp from way where id=:featId and status<>'R' and u=1" + ,{"featId":str(featId)}) + + row=c.fetchone() + if row<>None: + featCreated=row[0] + + c.close() + return featCreated + + + def getFeatureGeometry(self,featId,featType): + """Function constructs geometry of given feature. + Feature is given by its identifier and type. + + @param featId id of feature + @param featType type of feature + @return geometry of given feature + """ + + featGeom=None + if featType=='Point': + + c=self.getConnection().cursor() + c.execute("select lat,lon from node where status<>'R' and id=:featId and u=1" + ,{"featId":str(featId)}) + + for rec in c: + featGeom=QgsGeometry.fromPoint(QgsPoint(rec[1],rec[0])) # QgsPoint(lon,lat) + + c.close() + + # it's not a point + elif featType in ('Line','Polygon'): + + c=self.getConnection().cursor() + c.execute("select w.wkb FROM way w WHERE w.status<>'R' and id=:featId and u=1" + ,{"featId":str(featId)}) + + for rec in c: + featWKB=str(rec[0]) + theGeom = QgsGeometry() + theGeom.fromWkb(featWKB) + featGeom=theGeom + break + + c.close() + + # well, finish now + return featGeom + + + def getFeatureTags(self,featId,featType): + """Gets all tags of given feature. + Feature is given by its identifier and type. + The tags are returned no matter what's feature status: 'N','A','U','R'. + + @param featId id of feature + @param featType type of feature + @return list of pairs (tagKey,tagValue) + """ + + tags=[] + osmType=self.convertToOsmType(featType) + c=self.getConnection().cursor() + + c.execute("select key, val from tag where object_id=:objId and object_type=:objType and u=1" + ,{"objId":str(featId),"objType":str(osmType)}) + + for tagRec in c: + tags.append((tagRec[0],tagRec[1])) + + c.close() + return tags + + + def getTagValue(self,featId,featType,tagKey): + """Gets tag value of given feature. + Feature is given by its identifier and type. + Tag is returned no matter what's feature status: 'N','A','U','R'. + + @param featId identifier of feature + @param featType type of feature + @param tagKey key of tag to which we search the value + @return list of pairs (tagKey,tagValue) + """ + + val="" + osmType=self.convertToOsmType(featType) + c=self.getConnection().cursor() + + c.execute("select val from tag where object_id=:objId and object_type=:objType and key=:tagKey and u=1" + ,{"objId":str(featId),"objType":osmType,"tagKey":tagKey}) + + for rec in c: + val=rec[0] + c.close() + return val + + c.close() + return val + + + def setTagValue(self,featId,featType,tagKey,tagValue): + """Function sets value of given feature and given key. + Feature is given by its identifier and type. + Tag is updated no matter what's feature status: 'N','A','U','R'. + + @param featId identifier of feature + @param featType type of feature + @param tagKey key of tag + """ + + val="" + osmType=self.convertToOsmType(featType) + c=self.getConnection().cursor() + + c.execute("update tag set val=:tagVal where object_id=:objId and object_type=:objType and key=:tagKey and u=1" + ,{"tagVal":tagValue,"objId":str(featId),"objType":osmType,"tagKey":tagKey}) + + for rec in c: + val=rec[0] + c.close() + return val + + c.close() + self.commit() + return val + + + def getFeatureRelations(self,featId,featType): + """Gets all relations connected to given feature. + Feature is given by its identifier and type. + + @param featId id of feature + @param featType type of feature + @return list of relation identifiers. + """ + + rels=[] + c=self.getConnection().cursor() + + c.execute("select distinct relation_id from relation_member where member_id=:objId and u=1" + ,{"objId":str(featId)}) + for record in c: + rels.append(record[0]) + + c.close() + return rels + + + def changePointStatus(self,pointId,oldStatus,newStatus): + """Function changes status of given point. + Allowed statuses and their meanings: 'U'=Updated,'R'=Removed,'N'=Normal,'A'=Added. + If oldStatus is not specified, status is changed to newStatus no matter what was its previous value. + + @param pointId id of feature + @param oldStatus old status that should be change + @param newStatus new feature status + """ + + c=self.getConnection().cursor() + + if not oldStatus: + c.execute("update node set status=:newStatus where id=:pointId and u=1" + ,{"newStatus":newStatus,"pointId":str(pointId)}) + else: + c.execute("update node set status=:newStatus where id=:pointId and status=:oldStatus and u=1" + ,{"newStatus":newStatus,"pointId":str(pointId),"oldStatus":oldStatus}) + c.close() + self.commit() + + + def changeLineStatus(self,lineId,oldStatus,newStatus): + """Function changes status of given line. + Allowed statuses and their meanings: 'U'=Updated,'R'=Removed,'N'=Normal,'A'=Added. + If oldStatus is not specified, status is changed to newStatus no matter what was its previous value. + + @param lineId id of feature + @param oldStatus old status that should be change + @param newStatus new feature status + """ + + c=self.getConnection().cursor() + + if not oldStatus: + c.execute("update way set status=:newStatus where id=:lineId and u=1" + ,{"newStatus":newStatus,"lineId":str(lineId)}) + else: + c.execute("update way set status=:newStatus where id=:lineId and status=:oldStatus and u=1" + ,{"newStatus":newStatus,"lineId":str(lineId),"oldStatus":oldStatus}) + c.close() + self.commit() + + + def changePolygonStatus(self,polygonId,oldStatus,newStatus): + """Function changes status of given polygon. + Allowed statuses and their meanings: 'U'=Updated,'R'=Removed,'N'=Normal,'A'=Added. + If oldStatus is not specified, status is changed to newStatus no matter what was its previous value. + + @param polygonId id of feature + @param oldStatus old status that should be change + @param newStatus new feature status + """ + + c=self.getConnection().cursor() + + if not oldStatus: + c.execute("update way set status=:newStatus where id=:polygonId and u=1" + ,{"newStatus":newStatus,"polygonId":str(polygonId)}) + else: + c.execute("update way set status=:newStatus where id=:polygonId and status=:oldStatus and u=1" + ,{"newStatus":newStatus,"polygonId":str(polygonId),"oldStatus":oldStatus}) + c.close() + self.commit() + + + def changeWayStatus(self,wayId,oldStatus,newStatus): + """Function changes status of given OSM way + no matter if exact feature type is 'Line' or 'Polygon'. + Allowed statuses and their meanings: 'U'=Updated,'R'=Removed,'N'=Normal,'A'=Added. + If oldStatus is not specified, status is changed to newStatus no matter what was its previous value. + + @param wayId id of feature + @param oldStatus old status that should be change + @param newStatus new feature status + """ + + c=self.getConnection().cursor() + + if not oldStatus: + c.execute("update way set status=:newStatus where id=:wayId and u=1" + ,{"newStatus":newStatus,"wayId":str(wayId)}) + else: + c.execute("update way set status=:newStatus where id=:wayId and status=:oldStatus and u=1" + ,{"newStatus":newStatus,"wayId":str(wayId),"oldStatus":oldStatus}) + c.close() + self.commit() + + + def changeRelationStatus(self,relId,oldStatus,newStatus): + """Function changes status of given relation. + Allowed statuses and their meanings: 'U'=Updated,'R'=Removed,'N'=Normal,'A'=Added + If oldStatus is not specified, status is changed to newStatus no matter what was its previous value. + + @param relId id of feature + @param oldStatus old status that should be change + @param newStatus new feature status + """ + + c=self.getConnection().cursor() + + if not oldStatus: + c.execute("update relation set status=:newStatus where id=:relId and u=1" + ,{"newStatus":newStatus,"relId":str(relId)}) + else: + c.execute("update relation set status=:newStatus where id=:relId and status=:oldStatus and u=1" + ,{"newStatus":newStatus,"relId":str(relId),"oldStatus":oldStatus}) + c.close() + self.commit() + + + def changeRelationType(self,relId,newType): + """Function changes type of relation. + + If relation status is 'N' (Normal), it is automatically changed to 'U' + (Updated) to keep data consistent. + + @param relId identifier of relation + @param newType name of new relation type + """ + + c=self.getConnection().cursor() + c.execute("update relation set type=:newType where id=:relId and u=1" + ,{"newType":newType,"relId":str(relId)}) + c.close() + + # update relation record in database; mark relation as updated + self.changeRelationStatus(relId,'N','U') + + # update tag with key "type" + self.changeTagValue(relId,'Relation','type',newType) + self.commit() + + + def changeTagValue(self,featId,featType,key,value): + """Function changes value for given feature and tag key. + Feature is given by its id and type. + + @param featId id of feature + @param featType type of feature + @param key unique key of tag + @param value new tag value of feature + """ + + osmType=self.convertToOsmType(featType) + c=self.getConnection().cursor() + c.execute("update tag set val=:val where object_id=:objId and object_type=:objType and key=:key and u=1" + ,{"val":value,"objId":str(featId),"objType":osmType,"key":key}) + c.close() + self.commit() + + + def isTagDefined(self,featId,featType,key): + """Finds out if tag with given key is defined for given feature. + + @param featId id of feature + @param featType type of feature + @return True if given tag already exists for given feature; False otherwise + """ + + tagEx=False + osmType=self.convertToOsmType(featType) + c=self.getConnection().cursor() + c.execute("select 1 from tag where object_id=:objId and object_type=:objType and key=:key and u=1" + ,{"objId":str(featId),"objType":osmType,"key":key}) + + for rec in c: + tagEx=True + + c.close() + return tagEx + + + def updateVersionId(self,featId,featOSMType,newVerId): + """Function updates version identifier of given feature. + It performs commit. + + @param featId identifier of feature + @param featOSMType OSM type of feature (one of 'node','way','relation') + @param newVerId new version id + """ + + if not newVerId: + return + + c=self.getConnection().cursor() + c.execute("update version set version_id=:verId where object_id=:featId and object_type=:featType" + ,{"verId":str(newVerId),"featId":str(featId),"featType":featOSMType}) + c.close() + self.commit() + + + def updateUser(self,featId,featOSMType,user): + """Function updates user (owner) of given feature. + It performs commit. + + @param featId identifier of feature + @param featOSMType OSM type of feature (one of 'node','way','relation') + @param user new owner of feature + """ + + c=self.getConnection().cursor() + + if featOSMType=='node': + c.execute("update node set user=:user where id=:featId and u=1" + ,{"user":user.toAscii().data(),"featId":str(featId)}) + + elif featOSMType=='way': + c.execute("update way set user=:user where id=:featId and u=1" + ,{"user":user.toAscii().data(),"featId":str(featId)}) + + elif featOSMType=='relation': + c.execute("update relation set user=:user where id=:featId and u=1" + ,{"user":user.toAscii().data(),"featId":str(featId)}) + + c.close() + self.commit() + + + def convertToOsmType(self,featType): + """Function converts feature type ('Point','Line','Polygon','Relation') + to relevant osm type ('way','node','relation'). + + @param featType type of feature + @return OSM type that is corresponding to featType + """ + + osmType="" + if featType in ('Point','point'): + osmType="node" + elif featType in ('Polygon','Line','polygon','line'): + osmType="way" + elif featType in ('Relation','relation'): + osmType="relation" + + return osmType + + + def insertTag(self,featId,featType,key,value, doCommit=True): + """Function inserts new tag to given feature. + + @param featId id of feature + @param featType type of feature + @param key key of new feature tag + @param value value of new feature tag + @param doCommit if True then commit is performed after tag insertion + """ + + osmType=self.convertToOsmType(featType) + c=self.getConnection().cursor() + c.execute("insert into tag (object_id, object_type, key, val) values (:objId, :objType, :key, :val)" + ,{"objId":str(featId),"objType":osmType,"key":key,"val":value}) + c.close() + if doCommit: + self.commit() + + + def insertTags(self,featId,featType,tagRecords): + """Function inserts new tags to given feature. + It doesn't verify that tag keys are unique. + Function performs commit. + + @param featId id of feature + @param featType type of feature + @param tagRecords list of new features' tags + """ + + for tagRecord in tagRecords: + key=tagRecord[0] + val=tagRecord[1] + self.insertTag(featId,featType,key,val,False) + + self.commit() + + + def removeTag(self,featId,featType,key): + """Function removes tag of given feature. Tag is given by its key. + It performs commit. + + @param featId id of feature + @param featType type of feature + @param key unique key of tag to remove + """ + + osmType=self.convertToOsmType(featType) + c=self.getConnection().cursor() + c.execute("delete from tag where object_id=:objId and object_type=:objType and key=:key and u=1" + ,{"objId":str(featId),"objType":osmType,"key":key}) + c.close() + self.commit() + + + def removeFeaturesTags(self,featId,featType): + """Function removes all features' tags. + It performs commit. + + @param featId id of feature + @param featType type of feature + """ + + osmType=self.convertToOsmType(featType) + c=self.getConnection().cursor() + c.execute("delete from tag where object_id=:objId and object_type=:objType and u=1" + ,{"objId":str(featId),"objType":osmType}) + c.close() + self.commit() + + + def getLinePolygonMembers(self,featId): + """Function returns all lines'/polygons' members. + + @param featId id of feature + @return list of all lines'/polygons' members + """ + + mems=[] + c=self.getConnection().cursor() + + c.execute("select n.id,n.lat,n.lon from node n,way_member wm where n.u=1 and wm.u=1 and wm.way_id=:featId and wm.node_id=n.id and n.status<>'R'" + ,{"featId":str(featId)}) + + for rec in c: + mems.append((rec[0],rec[1],rec[2])) + + c.close() + return mems + + + def getRelationMembers(self,relId): + """Function returns all members of given relation. + It doesn't check if relation has non-removed (<>'R') status. + + @param relId identifier of relation + @return list of all relations' members + """ + + c=self.getConnection().cursor() + + c.execute("select member_id, member_type, role from relation_member where relation_id=:relId and u=1" + ,{"relId":str(relId)}) + + mems=[] + for row in c: + mems.append((row[0],row[1],row[2])) + c.close() + + out=[] + for mem in mems: + + featId=mem[0] + osmType=mem[1] + featRole=mem[2] + + # finding out if feature is polygon + isPol=self.isPolygon(featId) + + if osmType=="way" and isPol: + featType="Polygon" + elif osmType=="way": + featType="Line" + elif osmType=="node": + featType="Point" + elif osmType=="relation": + featType = "Relation" + + out.append((featId,featType,featRole)) + return out + + + def getNodeParents(self,node): + """Function gets all features (lines,polygons) where given node is part of them. + + It returns list where each item consists of three values: (parentGeometry,memberIndex,isPolygonFlag). + is QgsGeometry of one node's parent. Position of node in this geometry is determined by . + just says if return geometry is of type "polygon". + + @param node QgsFeature object of node + @return list of parents + """ + + if not self.currentKey: # no layer loaded + return [] + + # initialization + parentFeats=[] + memberIndexes=[] + isPolygonFlags=[] + + mapPoint=node.geometry().asPoint() + area=QgsRectangle(mapPoint.x()-0.00001,mapPoint.y()-0.00001,mapPoint.x()+0.00001,mapPoint.y()+0.00001) + + lay=self.lineLayers[self.currentKey] + lay.select([],area,True,True) + feat=QgsFeature() + result=lay.nextFeature(feat) + + while result: + parentFeats.append(feat) + + pos=[] + c=self.getConnection().cursor() + c.execute("select pos_id from way_member where way_id=:polId and node_id=:pointId and u=1 order by 1 asc" + ,{"polId":feat.id(),"pointId":node.id()}) + for rec in c: + pos.append(rec[0]) + c.close() + + memberIndexes.append(pos) + isPolygonFlags.append(False) + + feat=QgsFeature() + result=lay.nextFeature(feat) + + lay=self.polygonLayers[self.currentKey] + lay.select([],area,True,True) + feat=QgsFeature() + result=lay.nextFeature(feat) + + while result: + parentFeats.append(feat) + + pos=[] + c=self.getConnection().cursor() + c.execute("select pos_id from way_member where way_id=:polId and node_id=:pointId and u=1 order by 1 asc" + ,{"polId":feat.id(),"pointId":node.id()}) + for rec in c: + pos.append(rec[0]) + c.close() + + memberIndexes.append(pos) + isPolygonFlags.append(True) + + feat=QgsFeature() + result=lay.nextFeature(feat) + + return (parentFeats,memberIndexes,isPolygonFlags) + + + def removeFromRelation(self,relId,memberId): + """Function removes (all occurances of) given feature from existing relation. + It performs commit. + + @param relId identifier of relation + @param memberId identifier of feature to remove + """ + + c=self.getConnection().cursor() + c.execute("delete from relation_member where relation_id=:relId and member_id=:memberId and u=1" + ,{"relId":relId,"memberId":str(memberId)}) + c.execute("update relation set status='U' where id=:relId and u=1 and status='N'" + ,{"relId":relId}) + c.close() + self.commit() + + + def __convertPseudoLineToPoint(self,lineId): + """Function converts line with only 1 member ("pseudo-line") to point. + It performs commit. + + @param lineId identifier of line + """ + + affected=set() + # first change status of old line to 'R' ~ Removed + self.changeLineStatus(lineId,None,"R") + + c=self.getConnection().cursor() + c.execute("select node_id from way_member where u=1 and way_id=:lineId" + ,{"lineId":str(lineId)}) + + for rec in c: + d=self.getConnection().cursor() + d.execute("update node set usage=usage-1 where id=:pointId and u=1" + ,{"pointId":str(rec[0])}) + affected.add((rec[0],'Point')) + d.close() + + c.execute("delete from way_member where way_id=:lineId and u=1" + ,{"lineId":str(lineId)}) + + # remove all lines' tags + self.removeFeaturesTags(lineId,"Line") + + # and that's all... + c.close() + + # don't forget to remove pseudo-line from existing relations + # note: we won't put node into relations instead of the line! + self.__removeFeatureFromAllRelations(lineId,"Line") + self.commit() + + affected.add((lineId,'Line')) + return affected + + + def __convertPseudoPolygonToLine(self,polId): + """Function converts polygon with only 2 members ("pseudo-polygon") to line. + Identifier of resulting line is the same as id of original polygon. + + @param polId identifier of pseudo-polygon + """ + + affected=set() + + # it's quite simple! + c=self.getConnection().cursor() + c.execute("update way set closed=0,wkb=:wkb where id=:polId and u=1" + ,{"polId":str(polId),"wkb":sqlite3.Binary("")}) + c.close() + self.commit() + + affected.add((polId,'Polygon')) + affected.add((polId,'Line')) # there's not a mistake here! + + return affected + + + def isPolygon(self,featId): + """Function finds out if feature given by its identifier is polygon or not. + + @param featId identifier of feature + @return True if feature is polygon; False otherwise + """ + + isPol=False + c=self.getConnection().cursor() + + c.execute("select 1 from way where id=:featId and closed=1 and u=1" + ,{"featId":str(featId)}) + + for rec in c: + isPol=True + c.close() + + return isPol + + + def recacheAffectedNow(self,affected): + """Function calls recaching of all features that are given in parameter. + Recaching is called only if it is provided by vector layer interface. + + @param affected list of features to be recached + """ + + if affected==None or len(affected)<1 or not self.currentKey: + return + + settings=QSettings() + cacheMode=settings.value("qgis/vectorLayerCacheMode",QVariant("heuristics")).toString() + + if cacheMode=="nothing": + return + + if not hasattr(self.pointLayers[self.currentKey],'recacheFeature'): + return + + for (fid,ftype) in affected: + # cache is enabled; cache must refetch all affected features + if ftype=='Point': + self.pointLayers[self.currentKey].recacheFeature(fid) + elif ftype=='Line': + self.lineLayers[self.currentKey].recacheFeature(fid) + elif ftype=='Polygon': + self.polygonLayers[self.currentKey].recacheFeature(fid) + + + def updateNodeId(self, nodePseudoId, newNodeId, user): + """Function updates node identification in sqlite3 database. + Used after uploading. + + @param nodePseudoId identifier which was used for temporary identification of the node in database + @param newNodeId identifier that is for node valid in OSM server database + @param user new user name + """ + + c=self.getConnection().cursor() + now = datetime.datetime.now() + nowfmt = now.strftime("%Y-%m-%dT%H:%M:%SZ") + + c.execute("update node set id=?, user=?, timestamp=?, status=? where id=?" + ,(newNodeId, user.toAscii().data(), nowfmt, 'N', nodePseudoId)) + c.execute("update tag set object_id=? where object_id=?",(newNodeId, nodePseudoId)) + c.execute("update way_member set node_id=? where node_id=?",(newNodeId, nodePseudoId)) + c.execute("insert into version (object_id,object_type,version_id) values (?,'node',?)",(newNodeId,1)) + + c.close() + self.commit() + + + def updateWayId(self, pseudoId, newId, user): + """Function updates way identification in sqlite3 database. + Used after uploading. + + @param pseudoId identifier which was used for temporary identification of the way in database + @param newId identifier that is for that way valid in OSM server database + @param user new user name + """ + + c=self.getConnection().cursor() + now=datetime.datetime.now() + nowfmt=now.strftime("%Y-%m-%dT%H:%M:%SZ") + + c.execute("update way set id=?, user=?, timestamp=?, status=? where id=?" + ,(newId, user.toAscii().data(), nowfmt, 'N', pseudoId)) + c.execute("update tag set object_id=? where object_id=? and object_type='way'",(newId, pseudoId)) + c.execute("update way_member set way_id=? where way_id=?",(newId, pseudoId)) + c.execute("insert into version (object_id,object_type,version_id) values (?,'way',?)",(newId,1)) + + c.close() + self.commit() + + + def updateRelationId(self, relPseudoId, newRelId, user): + """Function updates relation identification in sqlite3 database. + Used after uploading. + + @param relPseudoId identifier which was used for temporary identification of the relation in database + @param newRelId identifier that is for relation valid in OSM server database + @param user new user name + """ + + c=self.getConnection().cursor() + now = datetime.datetime.now() + nowfmt = now.strftime("%Y-%m-%dT%H:%M:%SZ") + + c.execute("update relation set id=?, user=?, timestamp=?, status=? where id=?" + ,(newRelId, user.toAscii().data(), nowfmt, 'N', relPseudoId)) + c.execute("update tag set object_id=? where object_id=? and object_type='relation'",(newRelId, relPseudoId)) + c.execute("update relation_member set relation_id=? where relation_id=?",(newRelId, relPseudoId)) + c.execute("insert into version (object_id,object_type,version_id) values (?,'relation',?)",(newRelId,1)) + + c.close() + self.commit() + + + def removeUselessRecords(self): + """Function removes all records with 'R' status. + + Uploader calls this function after upload process finishes. + It performs commit. + """ + + c=self.getConnection().cursor() + + c.execute("delete from version where object_type='way' and exists( select 1 from way w where object_id=w.id and w.status='R' )") + c.execute("delete from version where object_type='node' and exists( select 1 from node n where object_id=n.id and n.status='R' )") + c.execute("delete from version where object_type='relation' and exists( select 1 from relation r where object_id=r.id and r.status='R' )") + + c.execute("delete from node where status='R'") + c.execute("delete from way where status='R'") + c.execute("delete from relation where status='R'") + + c.close() + self.commit() + + + def removePointRecord(self,featId): + """Function removes database record of given point. + + This deletion is not just setting point status to 'R' (Removed) like removePoint() function does. + This function removes point permanently and it should be called after upload operation only. + + Function performs commit. + + @param featId identifier of feature/point + """ + + c=self.getConnection().cursor() + c.execute("delete from node where status='R' and id=:pointId and u=1",{"pointId":str(featId)}) + c.close() + self.commit() + + + def removeWayRecord(self,featId): + """Function removes database record of given OSM way. + + This deletion is not just setting way status to 'R' (Removed) like removeLine()/removePolygon() function does. + This function removes way permanently and it should be called after upload operation only. + + Function performs commit. + + @param featId identifier of feature (way) + """ + + c=self.getConnection().cursor() + c.execute("delete from way where status='R' and id=:wayId and u=1",{"wayId":str(featId)}) + c.close() + self.commit() + + + def removeRelationRecord(self,featId): + """Function removes database record of given relation. + + This deletion is not just setting relation status to 'R' (Removed) like removeRelation() function does. + This function removes relation permanently and it should be called after upload operation only. + + Function performs commit. + + @param featId identifier of feature/relation + """ + + c=self.getConnection().cursor() + c.execute("delete from relation where status='R' and id=:relId and u=1",{"relId":str(featId)}) + c.close() + self.commit() + + + def commit(self): + """Performs commit on current database. + """ + + self.getConnection().commit() + + + def rollback(self): + """Performs rollback on current database. + """ + + self.getConnection().rollback() + + + def getCurrentActionNumber(self): + """Function finds out the highest identifier in editing history. + + @return the highest identifier in editing history + """ + + c=self.getConnection().cursor() + c.execute("select max(change_id) from change_step") + + for rec in c: + c.close() + if not rec[0]: + return 0 + return rec[0] + + + def removeAllChangeSteps(self): + """Function removes all records of editing history. + It performs commit. + """ + + c=self.getConnection().cursor() + c.execute("delete from change_step") + c.close() + self.commit() + + + def removeChangeStepsBetween(self,fromId,toId): + """Function removes all records of editing history that falls to given interval. + It performs commit. + + @param fromId identifier of the row + @param toId identifier of the row + """ + + c=self.getConnection().cursor() + c.execute("delete from change_step where change_id>=:fromId and change_id<=:toId" + ,{"fromId":str(fromId),"toId":str(toId)}) + c.close() + self.commit() + + + def getChangeSteps(self,startId,stopId): + """Function returns all records of editing history that falls to given interval. + + @param startId identifier of the row + @param stopId identifier of the row + @return list of editing history records + """ + + c=self.getConnection().cursor() + c.execute("select change_type,tab_name,row_id,col_name,old_value,new_value from change_step \ + where change_id>:startId and change_id<=:stopId order by change_id desc" + ,{"startId":str(startId),"stopId":str(stopId)}) + chSteps=[] + for rec in c: + chSteps.append(rec) + + c.close() + return chSteps + + + def setRowDeleted(self,tabName,rowId): + """Function marks given row of given table as deleted. + + @param tabName name of table + @param rowId identifier of the row + """ + + d=self.getConnection().cursor() + d.execute("update "+tabName+" set u=0 where i=:rowId" + ,{"rowId":rowId}) + d.close() + + + def setRowNotDeleted(self,tabName,rowId): + """Function marks given row of given table as not deleted. + + @param tabName name of table + @param rowId identifier of the row + """ + + d=self.getConnection().cursor() + d.execute("update "+tabName+" set u=1 where i=:rowId" + ,{"rowId":rowId}) + d.close() + + + def setRowColumnValue(self,tabName,colName,newValue,rowId): + """Function sets new value in given table, row and column. + It doesn't perform commit. + + @param tabName name of table + @param colName name of column + @param newValue new value to set + @param rowId identifier of the row + """ + + d=self.getConnection().cursor() + d.execute("update "+tabName+" set "+colName+"='"+newValue+"' where i=:rowId" + ,{"rowId":str(rowId)}) + d.close() + + diff --git a/python/plugins/osm/DlgAddRelation.py b/python/plugins/osm/DlgAddRelation.py new file mode 100755 index 00000000000..44883af4183 --- /dev/null +++ b/python/plugins/osm/DlgAddRelation.py @@ -0,0 +1,798 @@ +"""@package DlgAddRelation +The main class of this module (DlgAddRelation) is descendant of "Create OSM Relation" dialog. + +The dialog either shows detail info on existing relation or is empty when no relation id is passed to constructor. +In brief this module (and its main class) just provides easy way to create or change OSM relation. +... +""" + + +from DlgAddRelation_ui import Ui_DlgAddRelation +from map_tools.IdentifyMapTool import IdentifyMapTool + +from PyQt4.QtCore import * +from PyQt4.QtGui import * +from PyQt4 import * +from sip import unwrapinstance +from qgis.core import * +from qgis.gui import * + +import sqlite3 + + + +class DlgAddRelation(QDialog, Ui_DlgAddRelation): + """This class is direct descendant of "Create OSM Relation" dialog. It provides easy way to create + or change OSM relation. Methods of DlgAddRelation class catch signals emitted when changing relations + type, tags or members, submitting or rejecting the whole dialog. After catching signal, methods must + perform appropriate operation using methods of DatabaseManager. The other methods serve to initialize + dialog when displaying info on existing relation.""" + + + def __init__(self, plugin, newRelationFirstMember=None, relationToEdit=None): + """The constructor. + + @param plugin pointer to OSM Plugin object; parent of this object + @param newRelationFirstMember info on feature (in form: "idSPACEtype") which will be first member of new relation + @param relationToEdit if relation is given, this dialog is for editing of existing relation, not for creation a new one + """ + + QDialog.__init__(self,None) + self.setupUi(self) + self.dockWidget=plugin.dockWidget + self.plugin=plugin + self.dbm=plugin.dbm + self.ur=plugin.undoredo + self.canvas=plugin.canvas + + # set icons for tool buttons (identify,move,createPoint,createLine,createPolygon) + self.chooseMemberButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_identify.png")) + self.removeMemberButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_remove.png")) + self.removeTagButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_remove.png")) + self.loadStandardTagsButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_star.png")) + self.typeInfoButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_questionMark.png")) + + self.info = dict() + self.newTagLabel = "" + self.newMemberLabel = "" + self.tagInfoTextEdit.setText("") + + if relationToEdit: + # we are editing existing relation + self.editing = True + self.relId = relationToEdit + self.createRelButton.setText("Save") + self.setWindowTitle("Edit OSM relation") + else: + self.editing = False + # we are adding new relation + if newRelationFirstMember: + ix = newRelationFirstMember.indexOf(" ") + self.firstMemberType = QString(newRelationFirstMember).left(ix) + self.firstMemberId = QString(newRelationFirstMember).right(len(newRelationFirstMember)-ix-1) + + self.relTags=[] + self.relTagsEditIndex=-1 + self.relMembersRoleEditIndex=-1 + self.relMembersTypeEditIndex=-1 + + self.connectDlgSignals() + + # clear all dialog items first + self.clear() + + # load default values to combobox determining relation type + self.relationTypes=["boundary","multipolygon","restriction","route","enforcement"] + self.typeCombo.addItem("") + self.typeCombo.addItems(self.relationTypes) + + if self.editing: + # we are editing existing relation, we'll load relation data first + self.loadRelationData(self.relId) + else: + if newRelationFirstMember: + self.addRelationMember(self.firstMemberId,self.firstMemberType,None) + + # enable related tool buttons + self.removeMemberButton.setEnabled(True) + self.relMembersLoaded = True + + self.relMembersTable.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.relTagsTable.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.relMembersTable.setSelectionBehavior(QAbstractItemView.SelectRows) + self.relTagsTable.setSelectionBehavior(QAbstractItemView.SelectRows) + + + def connectDlgSignals(self): + """Function connects important dialog signals to appropriate slots. + """ + + # signals on working with tag and member tables + QObject.connect(self.typeCombo, SIGNAL("currentIndexChanged(const QString &)"), self.__onTypeSelectionChanged) + QObject.connect(self.relTagsTable, SIGNAL("itemDoubleClicked(QTableWidgetItem*)"), self.__onTagsItemDoubleClicked) + QObject.connect(self.relMembersTable, SIGNAL("itemDoubleClicked(QTableWidgetItem*)"), self.__onMembersItemDoubleClicked) + QObject.connect(self.relTagsTable, SIGNAL("cellChanged(int,int)"), self.__onTagsCellChanged) + QObject.connect(self.relMembersTable, SIGNAL("cellChanged(int,int)"), self.__onMembersCellChanged) + + # signals on buttons clicking + QObject.connect(self.createRelButton, SIGNAL("clicked()"), self.createOrUpdateRelation) + QObject.connect(self.stornoButton, SIGNAL("clicked()"), self.stornoDialog) + QObject.connect(self.typeInfoButton, SIGNAL("clicked()"), self.__showTypeInfo) + QObject.connect(self.loadStandardTagsButton, SIGNAL("clicked()"), self.loadStandardTags) + QObject.connect(self.removeTagButton, SIGNAL("clicked()"), self.removeSelectedRelTags) + QObject.connect(self.removeMemberButton, SIGNAL("clicked()"), self.removeSelectedRelMembers) + QObject.connect(self.chooseMemberButton, SIGNAL("clicked()"), self.__startIdentifyingMember) + + + def addRelationMember(self,memberId,memberType,memberRole): + """Function inserts one record into table representing list of all relations' members. + New record is put to the first place of the list. + + @param memberId identifier of new relation member + @param memberType type of new relation member + @param memberRole role of new relation member + """ + + # insert row for new relation member + self.relMembersTable.insertRow(0) + + self.relMembersTable.setItem(0,0,QTableWidgetItem(str(memberId))) + self.relMembersTable.setItem(0,1,QTableWidgetItem(str(memberType))) + self.relMembersTable.setItem(0,2,QTableWidgetItem(str(memberRole))) + self.relMembersTable.item(0,0).setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) + self.relMembersTable.item(0,1).setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) + self.relMembersTable.item(0,2).setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) + + + def addRelationTag(self,tagKey,tagValue): + """Function inserts one record into table representing list of all relations' tags. + New record is put to the first place of the list. + + @param tagKey key of inserted tag + @param tagValue value of inserted tag + """ + + # insert row for new relation tag + self.relTagsTable.insertRow(0) + + self.relTagsTable.setItem(0,0,QTableWidgetItem(tagKey)) + self.relTagsTable.setItem(0,1,QTableWidgetItem(tagValue)) + self.relTagsTable.item(0,0).setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) + self.relTagsTable.item(0,1).setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) + + + def loadRelationData(self,relId): + """Function fills dialog items with data of given relation. + Data of relation means its type, tags and members. + + @param relId identifier of relation + """ + + # load tags of specified relation + self.relTags=self.dbm.getFeatureTags(relId,'Relation') + + rowCount = len(self.relTags) + for i in range(0,rowCount): + key = self.relTags[i][0] + value = self.relTags[i][1] + + if key=="type": + # tag with key "type" is not shown in relation tags table, there is special combobox for it instead + ix=self.typeCombo.findText(value) + if ix<>-1: + self.typeCombo.setCurrentIndex(ix) + else: + self.typeCombo.setEditText(value) + else: + # all other tags are displayed in tags table + self.addRelationTag(key,value) + + + # fill relation members table with relation members data + mems=self.dbm.getRelationMembers(relId) + + # printing members + for i in range(0,len(mems)): + self.addRelationMember(mems[i][0],mems[i][1],mems[i][2]) + + # enable related tool buttons + self.removeMemberButton.setEnabled(True) + + # set new flags values + self.relTagsLoaded = True + self.relMembersLoaded = True + + + def __startIdentifyingMember(self): + """Function enables maptool for identifying relation members directly on map. + """ + + if self.chooseMemberButton.isChecked(): + self.mapTool=IdentifyMapTool(self.canvas, self.dockWidget, self.dbm) + self.canvas.setMapTool(self.mapTool) + self.canvas.setCursor(QCursor(Qt.ArrowCursor)) + self.canvas.setFocus(Qt.OtherFocusReason) + else: + self.addRelationMember(self.dockWidget.feature.id(),self.dockWidget.featureType,"") + self.canvas.unsetMapTool(self.mapTool) + del self.mapTool + self.mapTool=None + + + def removeSelectedRelTags(self): + """Function removes all selected tags from relation tags table. + """ + + # remove selected tags (rows) + selectedItems=self.relTagsTable.selectedItems() + selectedRowsIndexes=[] + lastRowIndex=self.relTagsTable.rowCount()-1 + self.relTagsTable.setCurrentCell(lastRowIndex,0) + + for i in selectedItems: + if i.column()==0 and not i.row()==lastRowIndex: + selectedRowsIndexes.append(i.row()) + + selectedRowsIndexes.sort() + selectedRowsIndexes.reverse() + + for ix in selectedRowsIndexes: + key=self.relTagsTable.item(ix,0).text() + self.relTagsTable.removeRow(ix) + + + def removeSelectedRelMembers(self): + """Function removes all selected members from relation members table. + """ + + # remove selected members (rows) + selectedItems=self.relMembersTable.selectedItems() + selectedRowsIndexes=[] + lastRowIndex=self.relMembersTable.rowCount()-1 + self.relMembersTable.setCurrentCell(lastRowIndex,0) + + for i in selectedItems: + if i.column()==0 and not i.row()==lastRowIndex: + selectedRowsIndexes.append(i.row()) + + selectedRowsIndexes.sort() + selectedRowsIndexes.reverse() + + for ix in selectedRowsIndexes: + key=self.relMembersTable.item(ix,0).text() + self.relMembersTable.removeRow(ix) + + + def __onTagsCellChanged(self,row,column): + """Function to handle user changes in cells of relations' tags table. + + It's called automatically whenever signal "cellChanged(...)" is emitted. + + @param row index of a row that has changed + @param column index of a column that has changed + """ + + if not self.relTagsLoaded: + return + + if row==self.relTagsTable.rowCount()-1: + # changing value of the last row + key = self.relTagsTable.item(row,0).text() + if key=="" or key==self.newTagLabel: + return + + # adding new tag to table + if column==0: + newLastRow = row+1 + self.relTagsTable.setRowCount(row+2) + self.relTagsTable.setItem(newLastRow,0,QTableWidgetItem(self.newTagLabel)) + self.relTagsTable.setItem(newLastRow,1,QTableWidgetItem("")) + self.relTagsTable.item(newLastRow,0).setFlags(Qt.ItemIsEnabled | Qt.ItemIsEditable) + self.relTagsTable.item(newLastRow,1).setFlags(Qt.ItemIsEnabled) + + self.relTagsTable.item(row,0).setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) + self.relTagsTable.item(row,1).setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) + + + def __onMembersCellChanged(self,row,column): + """Function to handle user changes in cells of relations' members table. + + it's called automatically whenever signal "cellChanged(...)" is emitted. + + @param row index of a row that has changed + @param column index of a column that has changed + """ + + if not self.relMembersLoaded: + return + + if row==self.relMembersTable.rowCount()-1: + # changing value of the last row + memberId = self.relMembersTable.item(row,0).text() + if memberId=="" or memberId==self.newMemberLabel: + return + + # adding new member + if column==0: + newLastRow = row+1 + self.relMembersTable.setRowCount(row+2) + self.relMembersTable.setItem(newLastRow,0,QTableWidgetItem(self.newMemberLabel)) + self.relMembersTable.setItem(newLastRow,1,QTableWidgetItem("")) + self.relMembersTable.setItem(newLastRow,2,QTableWidgetItem("")) + self.relMembersTable.item(newLastRow,0).setFlags(Qt.ItemIsEnabled | Qt.ItemIsEditable) + self.relMembersTable.item(newLastRow,1).setFlags(Qt.ItemIsEnabled) + self.relMembersTable.item(newLastRow,2).setFlags(Qt.ItemIsEnabled) + + self.relMembersTable.item(row,0).setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) + self.relMembersTable.item(row,1).setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) + self.relMembersTable.item(row,2).setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) + + + def clear(self): + """Function removes content of all dialog items (with one exception) and set them to default values. + + The only exception is combobox determining relation type. + It stays untouched. + """ + + # clear table of relation properties(tags) and all related buttons + self.relTagsTable.clear() + self.removeTagButton.setEnabled(True) + + # clear table of relation members and all related buttons + self.relMembersTable.clear() + self.chooseMemberButton.setEnabled(True) + self.removeMemberButton.setEnabled(False) + + # clear information panel + self.tagInfoTextEdit.setText("") + self.tagInfoTextEdit.setEnabled(False) + + # set loading flags to false + self.relTagsLoaded = True + self.relMembersLoaded = True + + # load default data into relation members table + self.relMembersTable.setColumnCount(3) + self.relMembersTable.setHorizontalHeaderItem(0,QTableWidgetItem("Id")) + self.relMembersTable.setHorizontalHeaderItem(1,QTableWidgetItem("Type")) + self.relMembersTable.setHorizontalHeaderItem(2,QTableWidgetItem("Role")) + + self.relMembersTable.setRowCount(1) + self.relMembersTable.setItem(0,0,QTableWidgetItem(self.newMemberLabel)) + self.relMembersTable.setItem(0,1,QTableWidgetItem("")) + self.relMembersTable.setItem(0,2,QTableWidgetItem("")) + self.relMembersTable.item(0,0).setFlags(Qt.ItemIsEnabled | Qt.ItemIsEditable) + self.relMembersTable.item(0,1).setFlags(Qt.ItemIsEnabled) + self.relMembersTable.item(0,2).setFlags(Qt.ItemIsEnabled) + + # load default data into relation tags table + self.relTagsTable.setColumnCount(2) + self.relTagsTable.setHorizontalHeaderItem(0,QTableWidgetItem("Key")) + self.relTagsTable.setHorizontalHeaderItem(1,QTableWidgetItem("Value")) + + self.relTagsTable.setRowCount(1) + self.relTagsTable.removeCellWidget(0,1) + self.relTagsTable.setItem(0,0,QTableWidgetItem(self.newTagLabel)) + self.relTagsTable.setItem(0,1,QTableWidgetItem("")) + self.relTagsTable.item(0,0).setFlags(Qt.ItemIsEnabled | Qt.ItemIsEditable) + self.relTagsTable.item(0,1).setFlags(Qt.ItemIsEnabled) + + + def loadStandardTags(self): + """Function clears relations' tags table and then loads tags that are typical + for chosen relation type. + + This provides good way how to help users with creating standard relations. + User doesn't need to find out what tags are usually used for some relation. + """ + + # clear relation tags table first + self.relTagsTable.clear() + self.relTagsTable.setColumnCount(2) + self.relTagsTable.setHorizontalHeaderItem(0,QTableWidgetItem("Key")) + self.relTagsTable.setHorizontalHeaderItem(1,QTableWidgetItem("Value")) + self.relTagsTable.setRowCount(1) + self.relTagsTable.removeCellWidget(0,1) + self.relTagsTable.setItem(0,0,QTableWidgetItem(self.newTagLabel)) + self.relTagsTable.setItem(0,1,QTableWidgetItem("")) + self.relTagsTable.item(0,0).setFlags(Qt.ItemIsEnabled | Qt.ItemIsEditable) + self.relTagsTable.item(0,1).setFlags(Qt.ItemIsEnabled) + + # find tags that are recommended for chosen relation type + self.relTags = self.determineSuitableTags(self.typeCombo.currentText()) + + # put found tags into relation tags table + rowCount = len(self.relTags) + for i in range(0,rowCount): + self.addRelationTag(list(self.relTags)[i],"") + + # set variables and enable buttons for better manipulation with table + self.removeTagButton.setEnabled(True) + self.relTagsLoaded = True + + + def __onTypeSelectionChanged(self,typeName): + """Function is called after currentIndexChanged(...) signal is emitted on "RELATION TYPE" combobox. + + That means user select new relation type. Either one of predefined types or a new one. + + Function doesn't perform change of relation type this time. Everything will be done + after submiting the whole dialog. + + @param typeName name of new selected type + """ + + # if non-standard typename was set up, loadStandardTagsButton is useless + if typeName.toAscii().data() in self.relationTypes: + self.loadStandardTagsButton.setEnabled(True) + else: + self.loadStandardTagsButton.setEnabled(False) + + + def determineSuitableMemberRoles(self,relType): + """Function is used to find typical member roles to given relation type. + With help of this function plugin gives advice to user on relation creation. + + @param relType name of relation type + @return list of typical roles to given type of relation + """ + + roles = [] + if relType=="boundary": + roles = ["enclave","exclave"] + elif relType=="multipolygon": + roles = ["outer","inner"] + elif relType=="restriction": + roles = ["from","to","via","location_hint"] + elif relType=="route": + roles = ["forward","backward","stop_0","stop_1","stop_2","stop_3","stop_4","stop_5","stop_6","stop_7","stop_8","stop_9" + ,"forward_stop_0","forward_stop_1","forward_stop_2","forward_stop_3","forward_stop_4","forward_stop_5","forward_stop_6" + ,"forward_stop_7","forward_stop_8","forward_stop_9","backward_stop_0","backward_stop_1","backward_stop_2" + ,"backward_stop_3","backward_stop_4","backward_stop_5","backward_stop_6","backward_stop_7","backward_stop_8","backward_stop_9"] + elif relType=="enforcement": + roles = [] + return roles + + + def determineSuitableTags(self,relType): + """Function is used to find typical tags to given relation type. + With of this function plugin gives advice to user on relation creation. + + @param relType name of relation type + @return list of typical tags to given type of relation + """ + + tags = [] + if relType=="boundary": + tags = dict( + boundary='For a real boundary (sometimes in the middle of a river or 12 Miles away from coastline).' + ,land_area='For coastline and real boundaries on land.' + ,name='' + ,admin_level='' + ) + elif relType=="multipolygon": + tags = dict() + elif relType=="restriction": + tags = { + "restriction":"If the first word is \"no\", then no routing is possible from the \"from\" to the \"to\" member, and if it is \"only_\", then you know that the only routing originating from the \"from\" member leads to the \"to\" member. The \"from\" and \"to\" members must start/end at the \"via\" node (see 1)." + ,"except":"The restriction does not apply to these vehicle types (possible more than one: except=bicycle;psv)" + ,"day_on":"For example, no right turn in the morning peak on weekdays might be day_on=Monday;day_off=Friday;hour_on=07:30;hour_off=09:30." + ,"day_off":"" + ,"hour_on":"" + ,"hour_off":"" + } + elif relType=="route": + tags = dict( + route='A road (e.g. the ways making up the A14 trunk road), bicycle route, hiking route or whatever route.' + ,name='The route is known by this name (e.g. "Jubilee Cycle Route", "Pembrokeshire Coastal Path").' + ,ref='The route is known by this reference (e.g. "A14", "NCN 11", "Citi 4" (bus number); in germany there is always a space between character and number, e.g. "A 1", "L 130", "K 5"; in france too).' + ,network='A wider network of routes of which this is one example. For example, the UKs national cycle network; the Cambridge Citi bus network; the UKs long distance footpath network. (The "uk_" bit isnt particularly to identify it as belonging to the uk, merely to be a conventional way to separate the namespace.)' + ,operator='The route is operated by this authority/company etc. e.g. "Stagecoach Cambridge", "Eurostar".' + ,state='Sometimes routes may not be permanent (ie: diversions), or may be in a proposed state (ie: UK NCN routes are sometimes not official routes pending some negotiation or development). Connection is used for routes linking two different routes or linking a route with for example a village centre.' + ,symbol='Describes the symbol that is used to mark the way along the route, e.g., "Red cross on white ground" for the "Frankenweg" in Franconia, Germany.' + ,color='(optional) Color code noted in hex triplet format. Especially useful for public transport routes. Example: "#008080" for teal color.' + ,description='Description tells what is special about this route.' + ,distance='(optional) The distance covered by this route, if known. For information of users and automatic evaluation e.g. of completeness. Given including a unit and with a dot for decimals. (e.g. "12.5km")' + ) + elif relType=="enforcement": + tags = dict() + return tags + + + def determineSuitableTagValues(self,relType,tagKey): + """Function is used to find typical tag values for given relation type and given key. + With help of this function plugin gives advice to user on relation creation. + + @param relType name of relation type + @param tagKey key of tag + @return list of typical tag values to given relation type + """ + + vals = [] + if relType=="boundary": + if tagKey=="boundary": + vals = ["administrative","national_park","political","civil"] + elif tagKey=="land_area": + vals = ["administrative"] + elif tagKey=="admin_level": + vals = ["1","2","3","4","5","6","7","8","9","10","11"] + elif relType=="restriction": + if tagKey=="restriction": + vals = ["no_right_turn","no_left_turn","no_u_turn","no_straight_on","only_right_turn","only_left_turn","only_straight_on"] + elif tagKey=="except": + vals = ["psv","bicycle","hgv","motorcar"] + elif relType=="route": + if tagKey=="route": + vals = ["road","bicycle","foot","hiking","bus","pilgrimage","detour","railway","tram","mtb","roller_skate","running","horse"] + elif tagKey=="network": + vals = ["ncn","rcn","lcn","uk_ldp","lwn","rwn","nwn","e-road"] + elif tagKey=="state": + vals = ["proposed","alternate","temporary","connection"] + return vals + + + def createRelation(self): + """Function creates new OSM relation from dialog data. + It performs commit. + """ + + # collect relation members data into a list + relMems=[] + for i in range(0,self.relMembersTable.rowCount()-1): # except from the last row + memId = self.relMembersTable.item(i,0).text().toUtf8().data() + memType = self.relMembersTable.item(i,1).text().toUtf8().data() + memRole = self.relMembersTable.item(i,2).text().toUtf8().data() + + relMems.append((memId,memType,memRole)) + + # find out relation type + relType=self.typeCombo.currentText().toUtf8().data() + + # call relation creation + self.ur.startAction("Create relation.") + relId=self.dbm.createRelation(relType,relMems) + + relTags=[] + for i in range(0,self.relTagsTable.rowCount()-1): # except from the last row + key=self.relTagsTable.item(i,0).text().toUtf8().data() + val=self.relTagsTable.item(i,1).text().toUtf8().data() + + relTags.append((key,val)) + + # relation type has to be stored as tag too + relTags.append(('type',relType)) + + # insert relation tags + self.dbm.insertTags(relId,'Relation',relTags) + + # make actions persistent + self.dbm.commit() + self.ur.stopAction() + + + def updateRelation(self): + """Function updates existing OSM relation. + It performs commit. + + Relation is not given in parameter; it's in member variable of this class. + """ + + self.ur.startAction("Update relation.") + + # find out relation type and change it + relType=self.typeCombo.currentText().toUtf8().data() + self.dbm.changeRelationType(self.relId,relType) + + # remove all relation tags and members + self.dbm.removeFeaturesTags(self.relId,'Relation') + + # collect relation members into a list + relMems=[] + for i in range(0,self.relMembersTable.rowCount()-1): # except from the last row + memId = self.relMembersTable.item(i,0).text().toUtf8().data() + memType = self.relMembersTable.item(i,1).text().toUtf8().data() + memRole = self.relMembersTable.item(i,2).text().toUtf8().data() + + relMems.append((memId,memType,memRole)) + + # remove old members and insert new ones + self.dbm.changeAllRelationMembers(self.relId,relMems) + + # collect relation tags into a list + relTags=[] + for i in range(0,self.relTagsTable.rowCount()-1): # except from the last row + key=self.relTagsTable.item(i,0).text().toUtf8().data() + val=self.relTagsTable.item(i,1).text().toUtf8().data() + + relTags.append((key,val)) + + # relation type has to be stored as tag too + relTags.append(('type',relType)) + + # insert relation tags + self.dbm.insertTags(self.relId,'Relation',relTags) + + # make actions persistent + self.ur.stopAction() + self.dbm.commit() + + + def createOrUpdateRelation(self): + """Function starts process of creation of new OSM relation from dialog data. + When in editing mode function updates opened relation instead. + """ + + if not self.editing: + # lets create new relation from predefined information + self.createRelation() + else: + # lets update existing relation + self.updateRelation() + + # close addRelation dialog + self.close() + + #load features' relations info into dockWidget again + if self.dockWidget.feature: + self.dockWidget.reloadFeatureRelations() + + + def stornoDialog(self): + """Function just cancels the whole dialog. + It is called after clicked() signal is emitted on "Storno" button. + """ + + # close addRelation dialog + self.close() + + + def __onTagsItemDoubleClicked(self,item): + """Function is called after itemDoubleClicked(...) signal is emitted on table of relation tags. + + It shows combobox with possible values for given item of table. + + @param item item of table of relation tags + """ + + if item.column()==0: + return + + if self.relTagsEditIndex<>None: + row=self.relTagsEditIndex + if row<>-1: + value=self.relTagsTable.cellWidget(row,1).currentText() + self.relTagsTable.item(row,1).setText(value) + self.relTagsTable.removeCellWidget(row,1) + + tagValues = self.determineSuitableTagValues(self.typeCombo.currentText(),self.relTagsTable.item(item.row(),0).text()) + if len(tagValues)>0: + valCombo=QComboBox() + valCombo.setEditable(True) + valCombo.addItem("") + valCombo.addItems(tagValues) + ix=valCombo.findText(self.relTagsTable.item(item.row(),1).text()) + valCombo.setCurrentIndex(ix) + + self.relTagsTable.setCellWidget(item.row(),1,valCombo) + self.relTagsEditIndex=item.row() + QObject.connect(valCombo, SIGNAL("currentIndexChanged(const QString &)"), self.__onValueSelectionChanged) + + + def __onMembersItemDoubleClicked(self,item): + """Function is called after itemDoubleClicked(...) signal is emitted on table of relation members. + + It shows combobox with possible values for given item of table. + + @param item item of table of relation members + """ + + if (item.column()==0) or (item.row()==self.relMembersTable.rowCount()-1): + return + + if item.column()==2: + + if self.relMembersRoleEditIndex<>None: + row=self.relMembersRoleEditIndex + if row<>-1: + role=self.relMembersTable.cellWidget(row,2).currentText() + self.relMembersTable.item(row,2).setText(role) + self.relMembersTable.removeCellWidget(row,2) + + memberRoles = self.determineSuitableMemberRoles(self.typeCombo.currentText()) + if len(memberRoles)>0: + rolesCombo=QComboBox() + rolesCombo.setEditable(True) + rolesCombo.addItem("") + rolesCombo.addItems(memberRoles) + ix=rolesCombo.findText(self.relMembersTable.item(item.row(),1).text()) + rolesCombo.setCurrentIndex(ix) + + self.relMembersTable.setCellWidget(item.row(),2,rolesCombo) + self.relMembersRoleEditIndex=item.row() + QObject.connect(rolesCombo, SIGNAL("currentIndexChanged(const QString &)"), self.__onRoleSelectionChanged) + + elif item.column()==1: + + if self.relMembersTypeEditIndex<>None: + row=self.relMembersTypeEditIndex + if row<>-1: + memType=self.relMembersTable.cellWidget(row,1).currentText() + self.relMembersTable.item(row,1).setText(memType) + self.relMembersTable.removeCellWidget(row,1) + + memberTypes=["Point","Line","Polygon","Relation"] + memTypesCombo=QComboBox() + memTypesCombo.setEditable(True) + memTypesCombo.addItem("") + memTypesCombo.addItems(memberTypes) + ix=memTypesCombo.findText(self.relMembersTable.item(item.row(),1).text()) + memTypesCombo.setCurrentIndex(ix) + + self.relMembersTable.setCellWidget(item.row(),1,memTypesCombo) + self.relMembersTypeEditIndex=item.row() + QObject.connect(memTypesCombo, SIGNAL("currentIndexChanged(const QString &)"), self.__onMemTypeSelectionChanged) + + + def __onValueSelectionChanged(self,value): + """Function is called after currentIndexChanged(...) signal is emitted on combobox of table item. + This combobox is related to table of relation tags (column Value). + + @param value new current value in combobox + """ + + row=self.relTagsEditIndex + self.relTagsTable.item(row,1).setText(value) + self.relTagsTable.removeCellWidget(row,1) + self.relTagsEditIndex=-1 + + + def __onRoleSelectionChanged(self,role): + """Function is called after currentIndexChanged(...) signal is emitted on combobox of table item. + This combobox is related to table of relation members (column Role). + + @param role new current value in combobox + """ + + row=self.relMembersRoleEditIndex + self.relMembersTable.item(row,2).setText(role) + self.relMembersTable.removeCellWidget(row,2) + self.relMembersRoleEditIndex=-1 + + + def __onMemTypeSelectionChanged(self,memType): + """Function is called after currentIndexChanged(...) signal is emitted on combobox of table item. + This combobox is related to table of relation members (column Type). + + @param memType new current value in combobox + """ + + row=self.relMembersTypeEditIndex + self.relMembersTable.item(row,1).setText(memType) + self.relMembersTable.removeCellWidget(row,1) + self.relMembersTypeEditIndex=-1 + + + def __showTypeInfo(self): + """Function shows messagebox with brief information on currently selected relation type. + """ + + typeName = self.typeCombo.currentText() + info = "" + + if typeName=="boundary": + info = "for grouping boundaries and marking enclaves / exclaves" + elif typeName=="multipolygon": + info = "to put holes into areas (might have to be renamed, see article)" + elif typeName=="restriction": + info = "any kind of turn restriction" + elif typeName=="route": + info = "like bus routes, cycle routes and numbered highways" + elif typeName=="enforcement": + info = "traffic enforcement devices; speed cameras, redlight cameras, weight checks, ..." + + QMessageBox.information(self, self.tr("OSM Information") + ,self.tr(info)) + + + diff --git a/python/plugins/osm/DlgAddRelation_ui.py b/python/plugins/osm/DlgAddRelation_ui.py new file mode 100644 index 00000000000..61f5af701fa --- /dev/null +++ b/python/plugins/osm/DlgAddRelation_ui.py @@ -0,0 +1,140 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ui_files/DlgAddRelation.ui' +# +# Created: Tue Jul 14 14:44:27 2009 +# by: PyQt4 UI code generator 4.4.4 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +class Ui_DlgAddRelation(object): + def setupUi(self, DlgAddRelation): + DlgAddRelation.setObjectName("DlgAddRelation") + DlgAddRelation.resize(620, 461) + DlgAddRelation.setModal(False) + self.vboxlayout = QtGui.QVBoxLayout(DlgAddRelation) + self.vboxlayout.setObjectName("vboxlayout") + self.hboxlayout = QtGui.QHBoxLayout() + self.hboxlayout.setObjectName("hboxlayout") + self.label = QtGui.QLabel(DlgAddRelation) + self.label.setObjectName("label") + self.hboxlayout.addWidget(self.label) + spacerItem = QtGui.QSpacerItem(24, 20, QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Minimum) + self.hboxlayout.addItem(spacerItem) + self.typeCombo = QtGui.QComboBox(DlgAddRelation) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.typeCombo.sizePolicy().hasHeightForWidth()) + self.typeCombo.setSizePolicy(sizePolicy) + self.typeCombo.setMinimumSize(QtCore.QSize(164, 24)) + self.typeCombo.setMaximumSize(QtCore.QSize(164, 16777215)) + self.typeCombo.setEditable(True) + self.typeCombo.setObjectName("typeCombo") + self.hboxlayout.addWidget(self.typeCombo) + self.typeInfoButton = QtGui.QToolButton(DlgAddRelation) + self.typeInfoButton.setObjectName("typeInfoButton") + self.hboxlayout.addWidget(self.typeInfoButton) + spacerItem1 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.hboxlayout.addItem(spacerItem1) + self.vboxlayout.addLayout(self.hboxlayout) + self.hboxlayout1 = QtGui.QHBoxLayout() + self.hboxlayout1.setObjectName("hboxlayout1") + self.vboxlayout1 = QtGui.QVBoxLayout() + self.vboxlayout1.setObjectName("vboxlayout1") + self.hboxlayout2 = QtGui.QHBoxLayout() + self.hboxlayout2.setObjectName("hboxlayout2") + self.label_2 = QtGui.QLabel(DlgAddRelation) + self.label_2.setObjectName("label_2") + self.hboxlayout2.addWidget(self.label_2) + spacerItem2 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.hboxlayout2.addItem(spacerItem2) + self.loadStandardTagsButton = QtGui.QToolButton(DlgAddRelation) + self.loadStandardTagsButton.setObjectName("loadStandardTagsButton") + self.hboxlayout2.addWidget(self.loadStandardTagsButton) + self.removeTagButton = QtGui.QToolButton(DlgAddRelation) + self.removeTagButton.setObjectName("removeTagButton") + self.hboxlayout2.addWidget(self.removeTagButton) + self.vboxlayout1.addLayout(self.hboxlayout2) + self.relTagsTable = QtGui.QTableWidget(DlgAddRelation) + self.relTagsTable.setMinimumSize(QtCore.QSize(240, 0)) + self.relTagsTable.setObjectName("relTagsTable") + self.relTagsTable.setColumnCount(0) + self.relTagsTable.setRowCount(0) + self.vboxlayout1.addWidget(self.relTagsTable) + self.hboxlayout1.addLayout(self.vboxlayout1) + self.vboxlayout2 = QtGui.QVBoxLayout() + self.vboxlayout2.setObjectName("vboxlayout2") + self.hboxlayout3 = QtGui.QHBoxLayout() + self.hboxlayout3.setObjectName("hboxlayout3") + self.label_5 = QtGui.QLabel(DlgAddRelation) + self.label_5.setObjectName("label_5") + self.hboxlayout3.addWidget(self.label_5) + spacerItem3 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.hboxlayout3.addItem(spacerItem3) + self.chooseMemberButton = QtGui.QToolButton(DlgAddRelation) + self.chooseMemberButton.setCheckable(True) + self.chooseMemberButton.setObjectName("chooseMemberButton") + self.hboxlayout3.addWidget(self.chooseMemberButton) + self.removeMemberButton = QtGui.QToolButton(DlgAddRelation) + self.removeMemberButton.setObjectName("removeMemberButton") + self.hboxlayout3.addWidget(self.removeMemberButton) + self.vboxlayout2.addLayout(self.hboxlayout3) + self.relMembersTable = QtGui.QTableWidget(DlgAddRelation) + self.relMembersTable.setMinimumSize(QtCore.QSize(346, 0)) + self.relMembersTable.setObjectName("relMembersTable") + self.relMembersTable.setColumnCount(0) + self.relMembersTable.setRowCount(0) + self.vboxlayout2.addWidget(self.relMembersTable) + self.tagInfoTextEdit = QtGui.QTextEdit(DlgAddRelation) + self.tagInfoTextEdit.setEnabled(False) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.tagInfoTextEdit.sizePolicy().hasHeightForWidth()) + self.tagInfoTextEdit.setSizePolicy(sizePolicy) + self.tagInfoTextEdit.setMaximumSize(QtCore.QSize(16777215, 140)) + self.tagInfoTextEdit.setReadOnly(True) + self.tagInfoTextEdit.setObjectName("tagInfoTextEdit") + self.vboxlayout2.addWidget(self.tagInfoTextEdit) + self.hboxlayout1.addLayout(self.vboxlayout2) + self.vboxlayout.addLayout(self.hboxlayout1) + self.hboxlayout4 = QtGui.QHBoxLayout() + self.hboxlayout4.setObjectName("hboxlayout4") + spacerItem4 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.hboxlayout4.addItem(spacerItem4) + self.createRelButton = QtGui.QPushButton(DlgAddRelation) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.createRelButton.sizePolicy().hasHeightForWidth()) + self.createRelButton.setSizePolicy(sizePolicy) + self.createRelButton.setMinimumSize(QtCore.QSize(0, 30)) + self.createRelButton.setAutoDefault(False) + self.createRelButton.setObjectName("createRelButton") + self.hboxlayout4.addWidget(self.createRelButton) + self.stornoButton = QtGui.QPushButton(DlgAddRelation) + self.stornoButton.setMinimumSize(QtCore.QSize(0, 30)) + self.stornoButton.setAutoDefault(False) + self.stornoButton.setObjectName("stornoButton") + self.hboxlayout4.addWidget(self.stornoButton) + self.vboxlayout.addLayout(self.hboxlayout4) + + self.retranslateUi(DlgAddRelation) + QtCore.QMetaObject.connectSlotsByName(DlgAddRelation) + + def retranslateUi(self, DlgAddRelation): + DlgAddRelation.setWindowTitle(QtGui.QApplication.translate("DlgAddRelation", "Create OSM relation", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("DlgAddRelation", "Relation type:", None, QtGui.QApplication.UnicodeUTF8)) + self.typeInfoButton.setText(QtGui.QApplication.translate("DlgAddRelation", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.label_2.setText(QtGui.QApplication.translate("DlgAddRelation", "Properties", None, QtGui.QApplication.UnicodeUTF8)) + self.loadStandardTagsButton.setText(QtGui.QApplication.translate("DlgAddRelation", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.removeTagButton.setText(QtGui.QApplication.translate("DlgAddRelation", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.label_5.setText(QtGui.QApplication.translate("DlgAddRelation", "Members", None, QtGui.QApplication.UnicodeUTF8)) + self.chooseMemberButton.setText(QtGui.QApplication.translate("DlgAddRelation", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.removeMemberButton.setText(QtGui.QApplication.translate("DlgAddRelation", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.createRelButton.setText(QtGui.QApplication.translate("DlgAddRelation", "Create", None, QtGui.QApplication.UnicodeUTF8)) + self.stornoButton.setText(QtGui.QApplication.translate("DlgAddRelation", "Storno", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/python/plugins/osm/DlgDownloadOSM.py b/python/plugins/osm/DlgDownloadOSM.py new file mode 100755 index 00000000000..87a42a873a8 --- /dev/null +++ b/python/plugins/osm/DlgDownloadOSM.py @@ -0,0 +1,491 @@ +"""@package DlgDownloadOSM +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 +neighter latitude nor longitude extent of downloaded region can be larger than 0.25 degree. + +Each error response from OSM server is caught by OSM Plugin and display to its user. +""" + + +from DlgDownloadOSM_ui import Ui_DlgDownloadOSM +from PyQt4.QtCore import * +from PyQt4.QtGui import * +from PyQt4.QtNetwork import * +from PyQt4 import * +from time import * +from qgis.core import * + + +class DlgDownloadOSM(QDialog, Ui_DlgDownloadOSM): + """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 + neighter latitude nor longitude extent of downloaded region can be larger than 0.25 degree. + + 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) + self.downloadButton.setEnabled(False) + + # determining default area for download + currentExtent=plugin.canvas.extent() + + # 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) + self.downloadButton.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. + """ + + if self.finished: + return + + self.downloadButton.setEnabled(False) + self.disconnectDlgSignals() + + # 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 + + # 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.setEnabled(False) + self.progressDialog.setEnabled(True) + 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 actully 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 immediatelly 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 + + # 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 self.errMessage==None: + self.errMessage="Check your internet connection" + QMessageBox.information(self, self.tr("OSM Download Error") + ,self.tr("Download failed: %1.").arg(self.errMessage)) + else: + self.httpSuccess=True + + # well, download process has finished successfully; + # close progress dialog and the whole download dialog + self.progressDialog.close() + 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. + """ + + if self.finished: + return + + # display file open dialog and get absolute path to selected directory + fileSelected = QFileDialog.getSaveFileName(self, "Choose file to save","download.osm", "OSM Files (*.osm)"); + # insert selected directory path into line edit control + if not fileSelected.isNull(): + self.destdirLineEdit.setText(fileSelected) + self.downloadButton.setEnabled(True) + + + 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. + """ + + if self.finished: + return + + prefix=QString("/tmp/") + 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 self.finished: + return + + 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. + """ + + if self.finished: + return + + 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 neighter latitude nor longitude extent of downloaded region can be larger than 0.25 degree. 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. + """ + + if self.finished: + return + + 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. + """ + + if self.finished: + return + + # cancel download with no message for user + self.cancelDownload() + + + 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. + """ + + if self.finished: + return + + # 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 neccessary 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) + + + diff --git a/python/plugins/osm/DlgDownloadOSM_ui.py b/python/plugins/osm/DlgDownloadOSM_ui.py new file mode 100644 index 00000000000..e797bfc604a --- /dev/null +++ b/python/plugins/osm/DlgDownloadOSM_ui.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ui_files/DlgDownloadOSM.ui' +# +# Created: Fri Jul 10 15:27:30 2009 +# by: PyQt4 UI code generator 4.4.4 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +class Ui_DlgDownloadOSM(object): + def setupUi(self, DlgDownloadOSM): + DlgDownloadOSM.setObjectName("DlgDownloadOSM") + DlgDownloadOSM.setWindowModality(QtCore.Qt.ApplicationModal) + DlgDownloadOSM.resize(595, 357) + DlgDownloadOSM.setContextMenuPolicy(QtCore.Qt.NoContextMenu) + DlgDownloadOSM.setModal(True) + self.vboxlayout = QtGui.QVBoxLayout(DlgDownloadOSM) + self.vboxlayout.setObjectName("vboxlayout") + self.groupBox = QtGui.QGroupBox(DlgDownloadOSM) + self.groupBox.setMaximumSize(QtCore.QSize(16777215, 16777215)) + self.groupBox.setObjectName("groupBox") + self.vboxlayout1 = QtGui.QVBoxLayout(self.groupBox) + self.vboxlayout1.setObjectName("vboxlayout1") + self.gridlayout = QtGui.QGridLayout() + self.gridlayout.setObjectName("gridlayout") + self.label = QtGui.QLabel(self.groupBox) + self.label.setObjectName("label") + self.gridlayout.addWidget(self.label, 0, 1, 1, 1) + self.label_4 = QtGui.QLabel(self.groupBox) + self.label_4.setObjectName("label_4") + self.gridlayout.addWidget(self.label_4, 0, 2, 1, 1) + self.latFromLineEdit = QtGui.QLineEdit(self.groupBox) + self.latFromLineEdit.setEnabled(True) + self.latFromLineEdit.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.latFromLineEdit.setObjectName("latFromLineEdit") + self.gridlayout.addWidget(self.latFromLineEdit, 0, 3, 1, 1) + self.label_6 = QtGui.QLabel(self.groupBox) + self.label_6.setObjectName("label_6") + self.gridlayout.addWidget(self.label_6, 0, 4, 1, 1) + self.latToLineEdit = QtGui.QLineEdit(self.groupBox) + self.latToLineEdit.setEnabled(True) + self.latToLineEdit.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.latToLineEdit.setObjectName("latToLineEdit") + self.gridlayout.addWidget(self.latToLineEdit, 0, 5, 1, 1) + self.label_2 = QtGui.QLabel(self.groupBox) + self.label_2.setObjectName("label_2") + self.gridlayout.addWidget(self.label_2, 1, 1, 1, 1) + self.label_5 = QtGui.QLabel(self.groupBox) + self.label_5.setObjectName("label_5") + self.gridlayout.addWidget(self.label_5, 1, 2, 1, 1) + self.lonFromLineEdit = QtGui.QLineEdit(self.groupBox) + self.lonFromLineEdit.setEnabled(True) + self.lonFromLineEdit.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.lonFromLineEdit.setObjectName("lonFromLineEdit") + self.gridlayout.addWidget(self.lonFromLineEdit, 1, 3, 1, 1) + self.label_7 = QtGui.QLabel(self.groupBox) + self.label_7.setObjectName("label_7") + self.gridlayout.addWidget(self.label_7, 1, 4, 1, 1) + self.lonToLineEdit = QtGui.QLineEdit(self.groupBox) + self.lonToLineEdit.setEnabled(True) + self.lonToLineEdit.setAlignment(QtCore.Qt.AlignRight|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self.lonToLineEdit.setObjectName("lonToLineEdit") + self.gridlayout.addWidget(self.lonToLineEdit, 1, 5, 1, 1) + self.vboxlayout1.addLayout(self.gridlayout) + spacerItem = QtGui.QSpacerItem(20, 8, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Fixed) + self.vboxlayout1.addItem(spacerItem) + self.hboxlayout = QtGui.QHBoxLayout() + self.hboxlayout.setObjectName("hboxlayout") + spacerItem1 = QtGui.QSpacerItem(20, 20, QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Minimum) + self.hboxlayout.addItem(spacerItem1) + self.extentInfoLabel = QtGui.QLabel(self.groupBox) + self.extentInfoLabel.setMargin(0) + self.extentInfoLabel.setObjectName("extentInfoLabel") + self.hboxlayout.addWidget(self.extentInfoLabel) + self.helpButton = QtGui.QToolButton(self.groupBox) + self.helpButton.setMaximumSize(QtCore.QSize(23, 23)) + self.helpButton.setObjectName("helpButton") + self.hboxlayout.addWidget(self.helpButton) + self.vboxlayout1.addLayout(self.hboxlayout) + self.vboxlayout.addWidget(self.groupBox) + self.gridlayout1 = QtGui.QGridLayout() + self.gridlayout1.setObjectName("gridlayout1") + self.label_9 = QtGui.QLabel(DlgDownloadOSM) + self.label_9.setObjectName("label_9") + self.gridlayout1.addWidget(self.label_9, 0, 0, 1, 2) + self.destdirLineEdit = QtGui.QLineEdit(DlgDownloadOSM) + self.destdirLineEdit.setEnabled(False) + self.destdirLineEdit.setObjectName("destdirLineEdit") + self.gridlayout1.addWidget(self.destdirLineEdit, 1, 0, 1, 1) + self.choosedirButton = QtGui.QPushButton(DlgDownloadOSM) + self.choosedirButton.setObjectName("choosedirButton") + self.gridlayout1.addWidget(self.choosedirButton, 1, 1, 1, 1) + self.vboxlayout.addLayout(self.gridlayout1) + self.autoLoadCheckBox = QtGui.QCheckBox(DlgDownloadOSM) + self.autoLoadCheckBox.setChecked(True) + self.autoLoadCheckBox.setObjectName("autoLoadCheckBox") + self.vboxlayout.addWidget(self.autoLoadCheckBox) + self.hboxlayout1 = QtGui.QHBoxLayout() + self.hboxlayout1.setObjectName("hboxlayout1") + spacerItem2 = QtGui.QSpacerItem(15, 20, QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Minimum) + self.hboxlayout1.addItem(spacerItem2) + self.chkReplaceData = QtGui.QCheckBox(DlgDownloadOSM) + self.chkReplaceData.setChecked(False) + self.chkReplaceData.setObjectName("chkReplaceData") + self.hboxlayout1.addWidget(self.chkReplaceData) + self.vboxlayout.addLayout(self.hboxlayout1) + self.hboxlayout2 = QtGui.QHBoxLayout() + self.hboxlayout2.setSpacing(0) + self.hboxlayout2.setContentsMargins(-1, 0, -1, -1) + self.hboxlayout2.setObjectName("hboxlayout2") + spacerItem3 = QtGui.QSpacerItem(15, 20, QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Minimum) + self.hboxlayout2.addItem(spacerItem3) + self.chkCustomRenderer = QtGui.QCheckBox(DlgDownloadOSM) + self.chkCustomRenderer.setChecked(True) + self.chkCustomRenderer.setObjectName("chkCustomRenderer") + self.hboxlayout2.addWidget(self.chkCustomRenderer) + spacerItem4 = QtGui.QSpacerItem(20, 20, QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Minimum) + self.hboxlayout2.addItem(spacerItem4) + self.styleCombo = QtGui.QComboBox(DlgDownloadOSM) + self.styleCombo.setMinimumSize(QtCore.QSize(182, 0)) + self.styleCombo.setMaximumSize(QtCore.QSize(182, 16777215)) + self.styleCombo.setObjectName("styleCombo") + self.hboxlayout2.addWidget(self.styleCombo) + spacerItem5 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.hboxlayout2.addItem(spacerItem5) + self.vboxlayout.addLayout(self.hboxlayout2) + self.hboxlayout3 = QtGui.QHBoxLayout() + self.hboxlayout3.setObjectName("hboxlayout3") + spacerItem6 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.hboxlayout3.addItem(spacerItem6) + self.downloadButton = QtGui.QPushButton(DlgDownloadOSM) + self.downloadButton.setObjectName("downloadButton") + self.hboxlayout3.addWidget(self.downloadButton) + self.cancelButton = QtGui.QPushButton(DlgDownloadOSM) + self.cancelButton.setObjectName("cancelButton") + self.hboxlayout3.addWidget(self.cancelButton) + self.vboxlayout.addLayout(self.hboxlayout3) + + self.retranslateUi(DlgDownloadOSM) + QtCore.QMetaObject.connectSlotsByName(DlgDownloadOSM) + + def retranslateUi(self, DlgDownloadOSM): + DlgDownloadOSM.setWindowTitle(QtGui.QApplication.translate("DlgDownloadOSM", "Download OSM data", None, QtGui.QApplication.UnicodeUTF8)) + self.groupBox.setTitle(QtGui.QApplication.translate("DlgDownloadOSM", "Extent", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("DlgDownloadOSM", "Latitude:", None, QtGui.QApplication.UnicodeUTF8)) + self.label_4.setText(QtGui.QApplication.translate("DlgDownloadOSM", " From", None, QtGui.QApplication.UnicodeUTF8)) + self.label_6.setText(QtGui.QApplication.translate("DlgDownloadOSM", "To", None, QtGui.QApplication.UnicodeUTF8)) + self.label_2.setText(QtGui.QApplication.translate("DlgDownloadOSM", "Longitude:", None, QtGui.QApplication.UnicodeUTF8)) + self.label_5.setText(QtGui.QApplication.translate("DlgDownloadOSM", " From", None, QtGui.QApplication.UnicodeUTF8)) + self.label_7.setText(QtGui.QApplication.translate("DlgDownloadOSM", "To", None, QtGui.QApplication.UnicodeUTF8)) + self.extentInfoLabel.setText(QtGui.QApplication.translate("DlgDownloadOSM", "", None, QtGui.QApplication.UnicodeUTF8)) + self.helpButton.setText(QtGui.QApplication.translate("DlgDownloadOSM", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.label_9.setText(QtGui.QApplication.translate("DlgDownloadOSM", "Download to:", None, QtGui.QApplication.UnicodeUTF8)) + self.choosedirButton.setText(QtGui.QApplication.translate("DlgDownloadOSM", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.autoLoadCheckBox.setText(QtGui.QApplication.translate("DlgDownloadOSM", "Open data automatically after download", None, QtGui.QApplication.UnicodeUTF8)) + self.chkReplaceData.setText(QtGui.QApplication.translate("DlgDownloadOSM", "Replace current data (current layer will be removed)", None, QtGui.QApplication.UnicodeUTF8)) + self.chkCustomRenderer.setText(QtGui.QApplication.translate("DlgDownloadOSM", "Use custom renderer", None, QtGui.QApplication.UnicodeUTF8)) + self.downloadButton.setText(QtGui.QApplication.translate("DlgDownloadOSM", "Download", None, QtGui.QApplication.UnicodeUTF8)) + self.cancelButton.setText(QtGui.QApplication.translate("DlgDownloadOSM", "Cancel", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/python/plugins/osm/DlgImport.py b/python/plugins/osm/DlgImport.py new file mode 100644 index 00000000000..0a40bfa49c0 --- /dev/null +++ b/python/plugins/osm/DlgImport.py @@ -0,0 +1,252 @@ +# -*- coding: utf-8 -*- +"""@package DlgImport +This module is used to import OSM data from standard QGIS vector layer. +""" + + +from DlgImport_ui import Ui_DlgImport + +from PyQt4.QtCore import * +from PyQt4.QtGui import * + +from qgis.core import * + + +class dummyPoint: + """A wrapper around QgsPoint which adds hash calculation. + """ + + def __init__(self, pt): + """The constructor.""" + self.pt = pt + + def __hash__(self): + return int(self.pt.x()*230783 + self.pt.y()*680091) + + def __eq__(self, other): + return self.pt.x() == other.pt.x() and self.pt.y() == other.pt.y() + + + +class dummyFeat: + """A dummy QgsFeature that just returns its feature id. + """ + + def __init__(self, fid): + """The constructor.""" + self.fid = fid + + def id(self): + return self.fid + + +class DlgImport(QDialog, Ui_DlgImport): + """This class provides structures and methods neccessary for import OSM data. + Class is direct descendant of OSM Import dialog. + + After confirming OSM Import dialog process is immediately started. + """ + + def __init__(self, plugin): + """The constructor. + + @param plugin is pointer to instance of OSM Plugin + """ + + QDialog.__init__(self, None) + self.setupUi(self) + + self.plugin=plugin + self.dbm = plugin.dbm + self.affected=set() + + self.populateLayers() + + QObject.connect(self.buttonBox,SIGNAL("accepted()"),self.onOK) + + QObject.connect(self.cboLayer,SIGNAL("currentIndexChanged(int)"), self.updateUi) + + if self.cboLayer.count() > 0: + self.updateUi(0) + + + def updateUi(self, index): + """Function checks whether there is any selection in the layer. + """ + + layerId = self.cboLayer.itemData(index).toString() + layer = QgsMapLayerRegistry.instance().mapLayer(layerId) + if layer is None or len(layer.selectedFeaturesIds()) == 0: + self.chkOnlySelection.setChecked(False) + self.chkOnlySelection.setEnabled(False) + + + def populateLayers(self): + """Funtion populates layers. + """ + + self.cboLayer.clear() + layers = QgsMapLayerRegistry.instance().mapLayers() + for lyrId,lyr in layers.iteritems(): + if lyr.type() == QgsMapLayer.VectorLayer and lyr.dataProvider().name() != "osm": + self.cboLayer.addItem(lyr.name(), QVariant(lyrId) ) + + + def onOK(self): + """Function does OSM data importing. + """ + + layerId = self.cboLayer.itemData(self.cboLayer.currentIndex()).toString() + onlySel = self.chkOnlySelection.isChecked() + + layer = QgsMapLayerRegistry.instance().mapLayer(layerId) + if layer is None: + QMessageBox.warning(self, "Layer doesn't exist", "The selected layer doesn't exist anymore!") + return + + self.progress = QProgressDialog("Importing features...", "Cancel", 0, 100, self) + self.progress.setWindowModality(Qt.WindowModal) + + self.nodes = { } + + if onlySel: + # only selected features + features = layer.selectedFeatures() + count = len(features) + self.progress.setMaximum(count) + for f in features: + if not self.updateProgress(): + break + self.addFeature(f) + else: + # all features from layer + count = layer.featureCount() + self.progress.setMaximum(count) + layer.select() + f = QgsFeature() + while layer.nextFeature(f): + if not self.updateProgress(): + break + self.addFeature(f) + + self.dbm.commit() + self.progress.setValue(count) + + self.dbm.recacheAffectedNow(self.affected) + self.plugin.canvas.refresh() + + QMessageBox.information(self, "Import", "Import has been completed.") + self.accept() + + + def updateProgress(self): + """Function updates progress dialog. + """ + + self.progress.setValue( self.progress.value()+1 ) + return not self.progress.wasCanceled() + + + def addFeature(self,f): + """Function adds given feature. + + @param f feature to be added + """ + + g = f.geometry() + if g is None: + return + wkbType = g.wkbType() + + if wkbType == QGis.WKBPoint: + self.extractPoint(g.asPoint()) + + elif wkbType == QGis.WKBLineString: + self.extractLineString(g.asPolyline()) + + elif wkbType == QGis.WKBPolygon: + self.extractPolygon(g.asPolygon()) + + elif wkbType == QGis.WKBMultiPoint: + for pnt in g.asMultiPoint(): + self.extractPoint(pnt) + + elif wkbType == QGis.WKBMultiLineString: + for line in g.asMultiPolyline(): + self.extractLineString(line) + + elif wkbType == QGis.WKBMultiPolygon: + for polygon in g.asMultiPolygon(): + self.extractPolygon(polygon) + + + def extractPoint(self, pnt): + """Function extracts a point. + + @param pnt point to extract + """ + + # TODO: check that another point isn't already at the same position + (node,affected)=self.dbm.createPoint(pnt, None, None, False) + self.affected.update(affected) + + + def snapPoint(self, pnt): + """ Function checks whether there's already other point with the same position. + If so, it pretends it got snapped to that point. + + @param pnt point to snap + """ + + dp = dummyPoint(pnt) + if dp in self.nodes: + return (pnt, self.nodes[dp], 'Point') + else: + return (pnt,None,None) + + + def extractLineString(self, line): + """Function extracts a line. + + @param line line to extract + """ + + points = map(self.snapPoint, line) + + (feat,affected) = self.dbm.createLine(points, False) + self.affected.update(affected) + + # this is a hack that uses the knowledge of inner working of createLine. + # that sucks. + nodeId = feat.id()-1 + for p in points: + if p[1] is None: + dp = dummyPoint(p[0]) + self.nodes[dp] = dummyFeat(nodeId) + nodeId -= 1 + + + def extractPolygon(self, polygon): + """Function extracts a polygon. + + @param polygon polygon to extract + """ + + # TODO: do something with holes? + + points=map(self.snapPoint, polygon[0]) + if len(points)>0: + points.pop() + + (feat,affected)=self.dbm.createPolygon(points, False) + self.affected.update(affected) + + # this is a hack that uses the knowledge of inner working of createPolygon. + # that sucks. + nodeId = feat.id()-1 + for p in points: + if p[1] is None: + dp = dummyPoint(p[0]) + self.nodes[dp] = dummyFeat(nodeId) + nodeId -= 1 + diff --git a/python/plugins/osm/DlgImport_ui.py b/python/plugins/osm/DlgImport_ui.py new file mode 100644 index 00000000000..53afb76c271 --- /dev/null +++ b/python/plugins/osm/DlgImport_ui.py @@ -0,0 +1,62 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ui_files/DlgImport.ui' +# +# Created: Tue Jul 14 14:44:27 2009 +# by: PyQt4 UI code generator 4.4.4 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +class Ui_DlgImport(object): + def setupUi(self, DlgImport): + DlgImport.setObjectName("DlgImport") + DlgImport.setWindowModality(QtCore.Qt.ApplicationModal) + DlgImport.resize(248, 228) + DlgImport.setModal(True) + self.vboxlayout = QtGui.QVBoxLayout(DlgImport) + self.vboxlayout.setObjectName("vboxlayout") + self.label = QtGui.QLabel(DlgImport) + self.label.setWordWrap(True) + self.label.setObjectName("label") + self.vboxlayout.addWidget(self.label) + spacerItem = QtGui.QSpacerItem(20, 29, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.vboxlayout.addItem(spacerItem) + self.hboxlayout = QtGui.QHBoxLayout() + self.hboxlayout.setObjectName("hboxlayout") + self.label_2 = QtGui.QLabel(DlgImport) + self.label_2.setObjectName("label_2") + self.hboxlayout.addWidget(self.label_2) + self.cboLayer = QtGui.QComboBox(DlgImport) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.cboLayer.sizePolicy().hasHeightForWidth()) + self.cboLayer.setSizePolicy(sizePolicy) + self.cboLayer.setObjectName("cboLayer") + self.hboxlayout.addWidget(self.cboLayer) + self.vboxlayout.addLayout(self.hboxlayout) + self.chkOnlySelection = QtGui.QCheckBox(DlgImport) + self.chkOnlySelection.setObjectName("chkOnlySelection") + self.vboxlayout.addWidget(self.chkOnlySelection) + spacerItem1 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.vboxlayout.addItem(spacerItem1) + self.buttonBox = QtGui.QDialogButtonBox(DlgImport) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.NoButton|QtGui.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.vboxlayout.addWidget(self.buttonBox) + + self.retranslateUi(DlgImport) + QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("rejected()"), DlgImport.reject) + QtCore.QMetaObject.connectSlotsByName(DlgImport) + DlgImport.setTabOrder(self.cboLayer, self.chkOnlySelection) + DlgImport.setTabOrder(self.chkOnlySelection, self.buttonBox) + + def retranslateUi(self, DlgImport): + DlgImport.setWindowTitle(QtGui.QApplication.translate("DlgImport", "Import data to OSM", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("DlgImport", "In this dialog you can import a layer loaded in QGIS into active OSM data.", None, QtGui.QApplication.UnicodeUTF8)) + self.label_2.setText(QtGui.QApplication.translate("DlgImport", "Layer", None, QtGui.QApplication.UnicodeUTF8)) + self.chkOnlySelection.setText(QtGui.QApplication.translate("DlgImport", "Import only current selection", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/python/plugins/osm/DlgLoadOSM.py b/python/plugins/osm/DlgLoadOSM.py new file mode 100644 index 00000000000..e3e2443f2fc --- /dev/null +++ b/python/plugins/osm/DlgLoadOSM.py @@ -0,0 +1,320 @@ +# -*- coding: utf-8 -*- +"""@package DlgLoadOSM +This module provides all structures and methods neccessary for OSM data loading. + +Loading is done from XML file. After XML file selection and confirming the dialog three vector layers +are created in Quantum GIS. Layer for points, one for lines and one for polygons. + +All these layers are created with OSM data provider. +Data provider is the one, who parses an input XML file. + +OSM data loading can be canceled, in such case system returns to the same state as the one before loading. +""" + + +from DlgLoadOSM_ui import Ui_DlgLoadOSM + +from PyQt4.QtCore import * +from PyQt4.QtGui import * +from PyQt4 import * +from sip import unwrapinstance +from qgis.core import QgsVectorLayer, QgsMapLayerRegistry, QgsRectangle +from sip import * + + + +class DlgLoadOSM(QDialog, Ui_DlgLoadOSM): + """This class provides all structures and methods neccessary for OSM data loading. + + Loading is done from XML file. After XML file selection and confirming the dialog three vector layers + are created in Quantum GIS. Layer for points, one for lines and one for polygons. + + All these layers are created with OSM data provider. + Data provider is the one, who parses an input XML file. + + OSM data loading can be canceled, in such case system returns to the same state as the one before loading. + """ + + def __init__(self, plugin): + """The constructor. + + @param plugin is pointer to instance of OSM Plugin + """ + + QDialog.__init__(self, None) + self.setupUi(self) + + self.canvas=plugin.canvas + self.dbm=plugin.dbm + self.progress = None + + # we must connect action "click on browse button" with method for showing open file dialog + QObject.connect(self.browseOSMButton,SIGNAL("clicked()"),self.showOpenFileDialog) + QObject.connect(self.buttonBox,SIGNAL("accepted()"), self.onOK) + + for tag in ['name','place','highway','landuse','waterway','railway','amenity','tourism','learning']: + item = QListWidgetItem(tag, self.lstTags) + item.setFlags( Qt.ItemIsSelectable | Qt.ItemIsUserCheckable | Qt.ItemIsEnabled) + item.setCheckState(Qt.Checked if tag == 'name' else Qt.Unchecked) + + # gui initialization + self.initGui() + + + def initGui(self): + """Initializes GUI of OSM Load dialog. + """ + + # load default values to combobox determining style for custom renderer + self.styles=["Small scale","Medium scale","Large scale"] + thisFile=QString(__file__) + directory=thisFile.left(thisFile.lastIndexOf('/')) + self.styleFiles=[directory+"/styles/small_scale.style", directory+"/styles/medium_scale.style", directory+"/styles/big_scale.style"] + self.styleCombo.addItems(self.styles) + + if not self.dbm.currentKey: + self.chkReplaceData.setEnabled(False) + + + def showOpenFileDialog(self): + """Function opens dialog for selecting XML file. + + Only files with extension .osm can be selected. + Default directory for file selection is remembered/reload. + """ + + settings=QSettings() + lastDir=settings.value("/OSM_Plugin/lastDir", QVariant(QString())).toString() + + # display file open dialog and get absolute path to selected file + fileSelected=QFileDialog.getOpenFileName(self,"Choose an Open Street Map file",lastDir,"OSM Files (*.osm)"); + # insert OSM file path into line edit control + if not fileSelected.isNull(): + self.OSMFileEdit.setText(fileSelected) + + # save directory + fi=QFileInfo(fileSelected) + settings.setValue("/OSM_Plugin/lastDir", QVariant(fi.path()) ) + + + def onOK(self): + """Function is called after clicking on OK button of OSM Load dialog. + + It performs all actions neccessary for OSM data loading. + It creates three QGIS vector layers for loaded data (point,line,polygon layer). + """ + + self.buttonBox.setEnabled(False) + + # after closing a dialog, we want to add map layers using osm provider + self.fname = self.OSMFileEdit.text() + + if self.fname=='': + QMessageBox.information(self, "OSM Load", QString("Please enter path to OSM data file.")) + self.buttonBox.setEnabled(True) + return + + osmfile = QFileInfo(self.fname) + observer = "&observer="+str(unwrapinstance(self)) + basename = osmfile.baseName() + + if not osmfile.exists(): + QMessageBox.information(self, "OSM Load", QString("Path to OSM file is invalid: %1.").arg(self.fname)) + return + + fLoaded=self.filesLoaded() + replacing=self.chkReplaceData.isChecked() + newDB=self.fname.toAscii().data()+".db" + curDB=self.dbm.currentKey + + if basename in fLoaded and newDB<>curDB: + QMessageBox.information(self, "Error", QString("Layers of OSM file \"%1\" are loaded already.").arg(self.fname)) + return + + if replacing: + # remove layers of current data first + QgsMapLayerRegistry.instance().removeMapLayer(self.canvas.currentLayer().getLayerID(),True) + + tags = "&tags=yes" + + if self.chkCustomRenderer.isChecked(): + styleFile=self.styleFiles[self.styleCombo.currentIndex()] + style="&style="+styleFile + else: + style="" + + # some specific tags? + tag = "" + for row in xrange(self.lstTags.count()): + item = self.lstTags.item(row) + if item.checkState() == Qt.Checked: + if len(tag) > 0: tag += "+" + tag += item.text() + if len(tag) > 0: + tag = "&tag=" + tag + + # freeze map canvas until all vector layers are created + self.canvas.freeze(True) + + self.loadingCanceled=False + self.setProperty("osm_stop_parsing",QVariant(0)) + + # add polygon layer + polygonLayer = QgsVectorLayer(self.fname+"?type=polygon"+observer + tags + tag + style, basename+" polygons", "osm") + + if self.loadingCanceled: + polygonLayer=None + return + if not polygonLayer.isValid(): + QMessageBox.information(self,"Error",QString("Failed to load polygon layer.")) + return + + if self.chkCustomRenderer.isChecked(): + self.setCustomRenderer(polygonLayer) + QgsMapLayerRegistry.instance().addMapLayer(polygonLayer) + + # add line layer + lineLayer = QgsVectorLayer(self.fname+"?type=line"+observer + tags + tag + style, basename+" lines", "osm") + + if self.loadingCanceled: + lineLayer=None + return + if not lineLayer.isValid(): + QMessageBox.information(self,"Error",QString("Failed to load line layer.")) + return + + if self.chkCustomRenderer.isChecked(): + self.setCustomRenderer(lineLayer) + QgsMapLayerRegistry.instance().addMapLayer(lineLayer) + + # add point layer + pointLayer = QgsVectorLayer(self.fname+"?type=point"+observer + tags + tag + style, basename+" points", "osm") + + if self.loadingCanceled: + pointLayer=None + return + if not pointLayer.isValid(): + QMessageBox.information(self,"Error",QString("Failed to load point layer.")) + return + + if self.chkCustomRenderer.isChecked(): + self.setCustomRenderer(pointLayer) + QgsMapLayerRegistry.instance().addMapLayer(pointLayer) + + # remember layers + self.polygonLayer=polygonLayer + self.lineLayer=lineLayer + self.pointLayer=pointLayer + + self.canvas.freeze(False) + rect=self.canvas.extent() + + if self.chkCustomRenderer.isChecked(): + midX=rect.xMinimum()+(rect.xMaximum()-rect.xMinimum())/2 + midY=rect.yMinimum()+(rect.yMaximum()-rect.yMinimum())/2 + rX=rect.xMaximum()-midX + rY=rect.yMaximum()-midY + + st=self.styles[self.styleCombo.currentIndex()] + if st=="Small scale": + rect=QgsRectangle(midX-rX/15,midY-rY/15,midX+rX/15,midY+rY/15) + elif st=="Medium scale": + rect=QgsRectangle(midX-rX/8,midY-rY/8,midX+rX/8,midY+rY/8) + else: + rect=QgsRectangle(midX-rX/1.2,midY-rY/1.2,midX+rX/1.2,midY+rY/1.2) + + self.canvas.setExtent(rect) + self.canvas.refresh() + self.accept() + + + def setCustomRenderer(self, layer): + """Function provides a way how to set custom renderer. + + For more check changeAttributeValues() implementation of OSM provider. + + @param layer point to QGIS vector layer + """ + + import sip + layerAddr = sip.unwrapinstance(layer) + layer.dataProvider().changeAttributeValues( { 0x12345678 : { 0 : QVariant(layerAddr) } } ) + + + def filesLoaded(self): + """Function returns list of keys of all currently loaded vector layers. + Note that names are not absolute and not unique. + + @return list of keys of all currently loaded vector layers + """ + + mapLayers=QgsMapLayerRegistry.instance().mapLayers() + fLoaded=[] + for ix in mapLayers.keys(): + fileName=QString(ix) + pos=ix.lastIndexOf("_") + fileName=fileName.left(pos) + if fileName not in fLoaded: + fLoaded.append(fileName) + return fLoaded + + + def cancelLoading(self): + """Function is called when progress dialog is canceled + + It's purpose is to tell OSM provider to stop XML file parsing. + """ + + self.setProperty("osm_stop_parsing",QVariant(1)) + self.loadingCanceled=True + + + def event(self, e): + """Function is used for OSM provider <-> OSM Plugin communication. + """ + + if e.type() == QEvent.DynamicPropertyChange: + if e.propertyName() == "osm_status": + # we're starting new part + if not self.progress: + self.progress = QProgressDialog(self) + self.progress.setAutoClose(False) + self.progress.setModal(True) + QObject.connect(self.progress,SIGNAL("canceled()"),self.cancelLoading) + self.progress.show() + status = self.property("osm_status").toString() + self.progress.setLabelText(status) + self.progress.setValue(0) + + if e.propertyName() == "osm_max": + if not self.loadingCanceled: + # we've got new max. value + osm_max = self.property("osm_max").toInt()[0] + self.progress.setMaximum(osm_max) + + elif e.propertyName() == "osm_value": + if not self.loadingCanceled: + # update in progressbar + osm_val = self.property("osm_value").toInt()[0] + self.progress.setValue(osm_val) + + elif e.propertyName() == "osm_done": + if not self.loadingCanceled: + # we're done + QObject.disconnect(self.progress,SIGNAL("canceled()"),self.cancelLoading) + self.progress.close() + self.progress = None + + elif e.propertyName() == "osm_failure": + if not self.loadingCanceled: + self.loadingCanceled=True + QObject.disconnect(self.progress,SIGNAL("canceled()"),self.cancelLoading) + self.progress.close() + self.progress = None + QMessageBox.information(self,"Error",QString("Failed to load layers: %1") + .arg(self.property("osm_failure").toString())) + + qApp.processEvents() + return QDialog.event(self,e) + + diff --git a/python/plugins/osm/DlgLoadOSM_ui.py b/python/plugins/osm/DlgLoadOSM_ui.py new file mode 100644 index 00000000000..e2d4933ae42 --- /dev/null +++ b/python/plugins/osm/DlgLoadOSM_ui.py @@ -0,0 +1,86 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ui_files/DlgLoadOSM.ui' +# +# Created: Wed Jul 22 12:16:56 2009 +# by: PyQt4 UI code generator 4.4.4 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +class Ui_DlgLoadOSM(object): + def setupUi(self, DlgLoadOSM): + DlgLoadOSM.setObjectName("DlgLoadOSM") + DlgLoadOSM.setWindowModality(QtCore.Qt.ApplicationModal) + DlgLoadOSM.resize(508, 309) + DlgLoadOSM.setModal(True) + self.gridlayout = QtGui.QGridLayout(DlgLoadOSM) + self.gridlayout.setObjectName("gridlayout") + self.gridlayout1 = QtGui.QGridLayout() + self.gridlayout1.setObjectName("gridlayout1") + self.label = QtGui.QLabel(DlgLoadOSM) + self.label.setIndent(-1) + self.label.setObjectName("label") + self.gridlayout1.addWidget(self.label, 0, 0, 1, 2) + self.OSMFileEdit = QtGui.QLineEdit(DlgLoadOSM) + self.OSMFileEdit.setObjectName("OSMFileEdit") + self.gridlayout1.addWidget(self.OSMFileEdit, 1, 0, 1, 1) + self.browseOSMButton = QtGui.QPushButton(DlgLoadOSM) + self.browseOSMButton.setObjectName("browseOSMButton") + self.gridlayout1.addWidget(self.browseOSMButton, 1, 1, 1, 1) + self.gridlayout.addLayout(self.gridlayout1, 0, 0, 1, 2) + self.label_2 = QtGui.QLabel(DlgLoadOSM) + self.label_2.setObjectName("label_2") + self.gridlayout.addWidget(self.label_2, 1, 0, 1, 1) + self.hboxlayout = QtGui.QHBoxLayout() + self.hboxlayout.setObjectName("hboxlayout") + self.lstTags = QtGui.QListWidget(DlgLoadOSM) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.lstTags.sizePolicy().hasHeightForWidth()) + self.lstTags.setSizePolicy(sizePolicy) + self.lstTags.setObjectName("lstTags") + self.hboxlayout.addWidget(self.lstTags) + self.gridlayout.addLayout(self.hboxlayout, 2, 0, 1, 2) + self.hboxlayout1 = QtGui.QHBoxLayout() + self.hboxlayout1.setObjectName("hboxlayout1") + self.chkCustomRenderer = QtGui.QCheckBox(DlgLoadOSM) + self.chkCustomRenderer.setChecked(True) + self.chkCustomRenderer.setObjectName("chkCustomRenderer") + self.hboxlayout1.addWidget(self.chkCustomRenderer) + self.styleCombo = QtGui.QComboBox(DlgLoadOSM) + self.styleCombo.setMinimumSize(QtCore.QSize(182, 0)) + self.styleCombo.setMaximumSize(QtCore.QSize(182, 16777215)) + self.styleCombo.setObjectName("styleCombo") + self.hboxlayout1.addWidget(self.styleCombo) + self.gridlayout.addLayout(self.hboxlayout1, 4, 0, 1, 1) + self.buttonBox = QtGui.QDialogButtonBox(DlgLoadOSM) + self.buttonBox.setMaximumSize(QtCore.QSize(110, 16777215)) + self.buttonBox.setBaseSize(QtCore.QSize(110, 0)) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.gridlayout.addWidget(self.buttonBox, 4, 1, 1, 1) + self.chkReplaceData = QtGui.QCheckBox(DlgLoadOSM) + self.chkReplaceData.setChecked(False) + self.chkReplaceData.setObjectName("chkReplaceData") + self.gridlayout.addWidget(self.chkReplaceData, 3, 0, 1, 1) + + self.retranslateUi(DlgLoadOSM) + QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("rejected()"), DlgLoadOSM.reject) + QtCore.QMetaObject.connectSlotsByName(DlgLoadOSM) + DlgLoadOSM.setTabOrder(self.OSMFileEdit, self.browseOSMButton) + DlgLoadOSM.setTabOrder(self.browseOSMButton, self.lstTags) + DlgLoadOSM.setTabOrder(self.lstTags, self.chkCustomRenderer) + DlgLoadOSM.setTabOrder(self.chkCustomRenderer, self.buttonBox) + + def retranslateUi(self, DlgLoadOSM): + DlgLoadOSM.setWindowTitle(QtGui.QApplication.translate("DlgLoadOSM", "Load OSM", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("DlgLoadOSM", "OpenStreetMap file to load:", None, QtGui.QApplication.UnicodeUTF8)) + self.browseOSMButton.setText(QtGui.QApplication.translate("DlgLoadOSM", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.label_2.setText(QtGui.QApplication.translate("DlgLoadOSM", "Add columns for tags:", None, QtGui.QApplication.UnicodeUTF8)) + self.chkCustomRenderer.setText(QtGui.QApplication.translate("DlgLoadOSM", "Use custom renderer", None, QtGui.QApplication.UnicodeUTF8)) + self.chkReplaceData.setText(QtGui.QApplication.translate("DlgLoadOSM", "Replace current data (current layers will be removed)", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/python/plugins/osm/DlgSaveOSM.py b/python/plugins/osm/DlgSaveOSM.py new file mode 100644 index 00000000000..7c4fa67273d --- /dev/null +++ b/python/plugins/osm/DlgSaveOSM.py @@ -0,0 +1,314 @@ +# -*- coding: utf-8 -*- +"""@package DlgSaveOSM +This module is used to save OSM data into XML file. + +Of course, user is asked where to save the current data first. +""" + + +from DlgSaveOSM_ui import Ui_DlgSaveOSM + +from PyQt4.QtCore import * +from PyQt4.QtGui import * +from PyQt4.QtXml import * +from PyQt4 import * +from sip import unwrapinstance +from qgis.core import QgsVectorLayer, QgsMapLayerRegistry +#from sip import * + +import sqlite3 + + + +class DlgSaveOSM(QDialog, Ui_DlgSaveOSM): + """This class provides all structures and methods neccessary for current OSM data saving. + + Saving is done to XML file. + After XML file selection and confirming the dialog, process is started. + """ + + def __init__(self, plugin): + """The constructor. + + @param plugin is pointer to instance of OSM Plugin + """ + + QDialog.__init__(self, None) + self.setupUi(self) + + self.plugin=plugin + self.ur=plugin.undoredo + self.dbm=plugin.dbm + + self.progressDialog = QProgressDialog(self) + self.progressDialog.setModal(True) + self.progressDialog.setAutoClose(False) + + QObject.connect(self.browseOSMButton,SIGNAL("clicked()"),self.showSaveFileDialog) + QObject.connect(self.buttonBox,SIGNAL("accepted()"),self.onOK) + QObject.connect(self.progressDialog, SIGNAL("canceled()"), self.cancelSaving) + + + def cancelSaving(self): + """Function stops the whole OSM Saving process. + + Destination file is closed and removed. + """ + + # writing into output file was canceled, file must be enclosed + self.outFile.close() + + # end removed + self.outFile.remove() + self.outFile=None + +# if self.xml.device().isOpen(): +# self.xml.device().close() + + # close the whole Save OSM dialog + self.close() + + # todo: segfault... why???? + + + def showSaveFileDialog(self): + """Function opens dialog for selecting XML file. + + Only files with extension .osm can be selected. + Default directory for file selection is remembered/reload. + """ + + settings = QSettings() + lastDir = settings.value("/OSM_Plugin/lastDir", QVariant(QString())).toString() + + # display file open dialog and get absolute path to selected file + fileSelected = QFileDialog.getSaveFileName(self,"Choose an Open Street Map file",lastDir,"OSM Files (*.osm)"); + # insert OSM file path into line edit control + if not fileSelected.isNull(): + self.OSMFileEdit.setText(fileSelected) + + # remember directory + fi = QFileInfo(fileSelected) + settings.setValue("/OSM_Plugin/lastDir", QVariant(fi.path()) ) + + + def onOK(self): + """Function is called after clicking on OK button of OSM Save dialog. + + It performs all actions neccessary for OSM data saving. + """ + + # after closing a dialog, we want to save data into osm + self.fname=self.OSMFileEdit.text() + self.outFile=QFile(self.fname) + + if not self.outFile.open(QIODevice.WriteOnly): + QMessageBox.information(self,self.tr("Save OSM to file"),self.tr("Unable to save the file %1: %2.") + .arg(self.fname).arg(self.outFile.errorString())) + self.outFile=None + return + + points=self.chkPoints.isChecked() + lines=self.chkLines.isChecked() + polys=self.chkPolygons.isChecked() + rels=self.chkRelations.isChecked() + tags=self.chkTags.isChecked() + + self.xml=QXmlStreamWriter(self.outFile) + self.xml.setCodec(QTextCodec.codecForName("utf-8")) + self.xml.setAutoFormatting(True) + + self.dbConnection=sqlite3.connect(self.plugin.dbFileName.toLatin1().data()) + c=self.dbConnection.cursor() + + cntPoints=cntLines=cntPolys=cntRels=0 + c.execute("select count(*) from node") + for rec in c: + cntPoints=rec[0] + c.execute("select count(*) from way where closed=0") + for rec in c: + cntLines=rec[0] + c.execute("select count(*) from way where closed=1") + for rec in c: + cntPolys=rec[0] + c.execute("select count(*) from relation") + for rec in c: + cntRels=rec[0] + + self.xml.writeStartDocument() + self.xml.writeStartElement("osm") + self.xml.writeAttribute("version","0.6") + self.xml.writeAttribute("generator","OpenStreetMap server") + + self.progressDialog.setWindowTitle(self.tr("Save OSM to file")) + self.progressDialog.setLabelText(self.tr("Initializing...")) + self.progressDialog.setMaximum(1) + self.progressDialog.setValue(0) + self.progressDialog.show() + + # todo: element? + # todo: and what about uid? changeset? are they compulsory? + + if points: + self.progressDialog.setLabelText(self.tr("Saving nodes...")) + self.progressDialog.setMaximum(cntPoints) + self.progressDialog.setValue(0) + i=0 + + c.execute("select n.id,n.lat,n.lon,v.version_id,n.user,n.timestamp from \ + node n,version v where v.object_id=n.id and v.object_type='node'") + for rec in c: + anyTags=False + tagList=[] + + if tags: + tagList=self.dbm.getFeatureTags(rec[0],'Point') + if len(tagList)>0: + anyTags=True + + if anyTags: + self.xml.writeStartElement("node") + else: + self.xml.writeEmptyElement("node") + + self.xml.writeAttribute("id",str(rec[0])) + self.xml.writeAttribute("lat",str(rec[1])) + self.xml.writeAttribute("lon",str(rec[2])) + self.xml.writeAttribute("version",str(rec[3])) + self.xml.writeAttribute("user",rec[4]) + self.xml.writeAttribute("visible","true") + self.xml.writeAttribute("timestamp",rec[5]) + + if anyTags: + for r in tagList: + self.xml.writeEmptyElement("tag") + self.xml.writeAttribute("k",r[0]) + self.xml.writeAttribute("v",r[1]) + + if anyTags: + self.xml.writeEndElement() + i=i+1 + self.progressDialog.setValue(i) + + if lines: + self.progressDialog.setLabelText(self.tr("Saving lines...")) + self.progressDialog.setMaximum(cntLines) + self.progressDialog.setValue(0) + i=0 + + c.execute("select w.id,v.version_id,w.user,w.timestamp from way w,version v \ + where w.closed=0 and v.object_id=w.id and v.object_type='way'") + for rec in c: + self.xml.writeStartElement("way") + self.xml.writeAttribute("id",str(rec[0])) + self.xml.writeAttribute("visible","true") + self.xml.writeAttribute("timestamp",rec[3]) + self.xml.writeAttribute("version",str(rec[1])) + self.xml.writeAttribute("user",rec[2]) + + d=self.dbConnection.cursor() + d.execute("select node_id from way_member where way_id=:wayId",{"wayId":rec[0]}) + for r in d: + self.xml.writeStartElement("nd") + self.xml.writeAttribute("ref",str(r[0])) + self.xml.writeEndElement() + d.close() + + if tags: + tagList=self.dbm.getFeatureTags(rec[0],'Line') + for r in tagList: + self.xml.writeEmptyElement("tag") + self.xml.writeAttribute("k",r[0]) + self.xml.writeAttribute("v",r[1]) + + self.xml.writeEndElement() + i=i+1 + self.progressDialog.setValue(i) + + if polys: + self.progressDialog.setLabelText(self.tr("Saving polygons...")) + self.progressDialog.setMaximum(cntPolys) + self.progressDialog.setValue(0) + i=0 + + c.execute("select w.id,v.version_id,w.user,w.timestamp from way w,version v \ + where w.closed=1 and v.object_id=w.id and v.object_type='way'") + for rec in c: + self.xml.writeStartElement("way") + self.xml.writeAttribute("id",str(rec[0])) + self.xml.writeAttribute("visible","true") + self.xml.writeAttribute("timestamp",rec[3]) + self.xml.writeAttribute("version",str(rec[1])) + self.xml.writeAttribute("user",rec[2]) + + d=self.dbConnection.cursor() + d.execute("select node_id from way_member where way_id=:wayId",{"wayId":rec[0]}) + for r in d: + self.xml.writeStartElement("nd") + self.xml.writeAttribute("ref",str(r[0])) + self.xml.writeEndElement() + + d.close() + + if tags: + tagList=self.dbm.getFeatureTags(rec[0],'Polygon') + for r in tagList: + self.xml.writeEmptyElement("tag") + self.xml.writeAttribute("k",r[0]) + self.xml.writeAttribute("v",r[1]) + + self.xml.writeEndElement() + i=i+1 + self.progressDialog.setValue(i) + + if rels: + self.progressDialog.setLabelText(self.tr("Saving relations...")) + self.progressDialog.setMaximum(cntRels) + self.progressDialog.setValue(0) + i=0 + + c.execute("select r.id,v.version_id,r.user,r.timestamp from relation r,version v \ + where v.object_id=r.id and v.object_type='relation'") + for rec in c: + self.xml.writeStartElement("relation") + self.xml.writeAttribute("id",str(rec[0])) + self.xml.writeAttribute("visible","true") + self.xml.writeAttribute("timestamp",rec[3]) + self.xml.writeAttribute("version",str(rec[1])) + self.xml.writeAttribute("user",rec[2]) + + d=self.dbConnection.cursor() + d.execute("select member_id,member_type,role from relation_member where relation_id=:relId",{"relId":rec[0]}) + for r in d: + self.xml.writeStartElement("member") + self.xml.writeAttribute("type",r[1]) + self.xml.writeAttribute("ref",str(r[0])) + self.xml.writeAttribute("role",r[2]) + self.xml.writeEndElement() + d.close() + + if tags: + tagList=self.dbm.getFeatureTags(rec[0],'Relation') + for r in tagList: + self.xml.writeEmptyElement("tag") + self.xml.writeAttribute("k",r[0]) + self.xml.writeAttribute("v",r[1]) + + self.xml.writeEndElement() + i=i+1 + self.progressDialog.setValue(i) + + self.xml.writeEndElement() # osm + self.xml.writeEndDocument() + + c.close() + self.disconnect(self.progressDialog, SIGNAL("canceled()"), self.cancelSaving) + self.progressDialog.close() + + # writing into output file was finished, file can be enclosed + if self.outFile and self.outFile.exists(): + self.outFile.close() + self.close() + + + diff --git a/python/plugins/osm/DlgSaveOSM_ui.py b/python/plugins/osm/DlgSaveOSM_ui.py new file mode 100644 index 00000000000..6202939a9bd --- /dev/null +++ b/python/plugins/osm/DlgSaveOSM_ui.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ui_files/DlgSaveOSM.ui' +# +# Created: Tue Jul 14 14:44:25 2009 +# by: PyQt4 UI code generator 4.4.4 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +class Ui_DlgSaveOSM(object): + def setupUi(self, DlgSaveOSM): + DlgSaveOSM.setObjectName("DlgSaveOSM") + DlgSaveOSM.setWindowModality(QtCore.Qt.ApplicationModal) + DlgSaveOSM.resize(370, 206) + DlgSaveOSM.setModal(True) + self.vboxlayout = QtGui.QVBoxLayout(DlgSaveOSM) + self.vboxlayout.setObjectName("vboxlayout") + self.gridlayout = QtGui.QGridLayout() + self.gridlayout.setObjectName("gridlayout") + self.label = QtGui.QLabel(DlgSaveOSM) + self.label.setIndent(-1) + self.label.setObjectName("label") + self.gridlayout.addWidget(self.label, 0, 0, 1, 2) + self.OSMFileEdit = QtGui.QLineEdit(DlgSaveOSM) + self.OSMFileEdit.setObjectName("OSMFileEdit") + self.gridlayout.addWidget(self.OSMFileEdit, 1, 0, 1, 1) + self.browseOSMButton = QtGui.QPushButton(DlgSaveOSM) + self.browseOSMButton.setMaximumSize(QtCore.QSize(50, 16777215)) + self.browseOSMButton.setObjectName("browseOSMButton") + self.gridlayout.addWidget(self.browseOSMButton, 1, 1, 1, 1) + self.vboxlayout.addLayout(self.gridlayout) + self.label_2 = QtGui.QLabel(DlgSaveOSM) + self.label_2.setObjectName("label_2") + self.vboxlayout.addWidget(self.label_2) + self.hboxlayout = QtGui.QHBoxLayout() + self.hboxlayout.setContentsMargins(15, -1, 0, 10) + self.hboxlayout.setObjectName("hboxlayout") + self.vboxlayout1 = QtGui.QVBoxLayout() + self.vboxlayout1.setObjectName("vboxlayout1") + self.chkPoints = QtGui.QCheckBox(DlgSaveOSM) + self.chkPoints.setChecked(True) + self.chkPoints.setObjectName("chkPoints") + self.vboxlayout1.addWidget(self.chkPoints) + self.chkLines = QtGui.QCheckBox(DlgSaveOSM) + self.chkLines.setChecked(True) + self.chkLines.setObjectName("chkLines") + self.vboxlayout1.addWidget(self.chkLines) + self.chkPolygons = QtGui.QCheckBox(DlgSaveOSM) + self.chkPolygons.setChecked(True) + self.chkPolygons.setObjectName("chkPolygons") + self.vboxlayout1.addWidget(self.chkPolygons) + self.hboxlayout.addLayout(self.vboxlayout1) + self.vboxlayout2 = QtGui.QVBoxLayout() + self.vboxlayout2.setObjectName("vboxlayout2") + self.chkRelations = QtGui.QCheckBox(DlgSaveOSM) + self.chkRelations.setChecked(True) + self.chkRelations.setObjectName("chkRelations") + self.vboxlayout2.addWidget(self.chkRelations) + self.chkTags = QtGui.QCheckBox(DlgSaveOSM) + self.chkTags.setChecked(True) + self.chkTags.setObjectName("chkTags") + self.vboxlayout2.addWidget(self.chkTags) + spacerItem = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.vboxlayout2.addItem(spacerItem) + self.hboxlayout.addLayout(self.vboxlayout2) + spacerItem1 = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.hboxlayout.addItem(spacerItem1) + self.vboxlayout3 = QtGui.QVBoxLayout() + self.vboxlayout3.setObjectName("vboxlayout3") + spacerItem2 = QtGui.QSpacerItem(20, 40, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Expanding) + self.vboxlayout3.addItem(spacerItem2) + self.buttonBox = QtGui.QDialogButtonBox(DlgSaveOSM) + self.buttonBox.setMaximumSize(QtCore.QSize(110, 16777215)) + self.buttonBox.setBaseSize(QtCore.QSize(110, 0)) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Cancel|QtGui.QDialogButtonBox.NoButton|QtGui.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.vboxlayout3.addWidget(self.buttonBox) + self.hboxlayout.addLayout(self.vboxlayout3) + self.vboxlayout.addLayout(self.hboxlayout) + + self.retranslateUi(DlgSaveOSM) + QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("rejected()"), DlgSaveOSM.reject) + QtCore.QMetaObject.connectSlotsByName(DlgSaveOSM) + DlgSaveOSM.setTabOrder(self.OSMFileEdit, self.browseOSMButton) + + def retranslateUi(self, DlgSaveOSM): + DlgSaveOSM.setWindowTitle(QtGui.QApplication.translate("DlgSaveOSM", "Save OSM", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("DlgSaveOSM", "Where to save:", None, QtGui.QApplication.UnicodeUTF8)) + self.browseOSMButton.setText(QtGui.QApplication.translate("DlgSaveOSM", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.label_2.setText(QtGui.QApplication.translate("DlgSaveOSM", "Features to save:", None, QtGui.QApplication.UnicodeUTF8)) + self.chkPoints.setText(QtGui.QApplication.translate("DlgSaveOSM", "Points", None, QtGui.QApplication.UnicodeUTF8)) + self.chkLines.setText(QtGui.QApplication.translate("DlgSaveOSM", "Lines", None, QtGui.QApplication.UnicodeUTF8)) + self.chkPolygons.setText(QtGui.QApplication.translate("DlgSaveOSM", "Polygons", None, QtGui.QApplication.UnicodeUTF8)) + self.chkRelations.setText(QtGui.QApplication.translate("DlgSaveOSM", "Relations", None, QtGui.QApplication.UnicodeUTF8)) + self.chkTags.setText(QtGui.QApplication.translate("DlgSaveOSM", "Tags", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/python/plugins/osm/DlgUploadOSM.py b/python/plugins/osm/DlgUploadOSM.py new file mode 100755 index 00000000000..baf3b5b93f7 --- /dev/null +++ b/python/plugins/osm/DlgUploadOSM.py @@ -0,0 +1,1548 @@ +"""@package DlgUploadOSM +Module provides simple way of uploading current OSM data. + +When performing edit operations all changed features are marked as 'U'=Updated. +All new features are marked as 'A'=Added and all removed features are marked as 'R'=Removed. +Uploader checks features statuses first. + +Then it creates HTTP connection. Upload is done in correct order +so that data on OSM server always stay consistent. + +Upload phases and their exact order: +- phases: 0.changeset creation, 1.nodes creation, 2.ways deletion, 3.ways update, + 4.ways addition, 5.nodes deletion, 6.nodes update, 7.relation creation, + 8.relation deletion, 9.relation update, 10.changeset closing +""" + + +from DlgUploadOSM_ui import Ui_DlgUploadOSM + +from PyQt4.QtCore import * +from PyQt4.QtGui import * +from PyQt4.QtNetwork import * +from PyQt4 import * +from sys import * + + + +class DlgUploadOSM(QDialog, Ui_DlgUploadOSM): + """Class provides simple way of uploading current OSM data. + + When performing edit operations all changed features are marked as 'U'=Updated. + All new features are marked as 'A'=Added and all removed features are marked as 'R'=Removed. + + Uploader checks features statuses first. + Then it creates HTTP connection. Upload is done in correct order + so that data on OSM server always stay consistent. + + Upload phases and their exact order: + - phases: 0.changeset creation, 1.nodes creation, 2.ways deletion, 3.ways update, + 4.ways addition, 5.nodes deletion, 6.nodes update, 7.relation creation, + 8.relation deletion, 9.relation update, 10.changeset closing + """ + + def __init__(self,plugin): + """The constructor. + + @param plugin is pointer to instance of OSM Plugin. + """ + + QDialog.__init__(self,None) + self.setupUi(self) + + self.dockWidget=plugin.dockWidget + self.plugin=plugin + self.dbm=plugin.dbm + self.ur=plugin.undoredo + + self.urlHost = "api.openstreetmap.org" + self.uploadButton = self.buttonBox.addButton("Upload", QDialogButtonBox.ActionRole) + + self.uploadChangesTable.setColumnCount(5) + self.uploadChangesTable.setColumnWidth(0,80) + self.uploadChangesTable.setColumnWidth(1,60) + self.uploadChangesTable.setColumnWidth(2,60) + self.uploadChangesTable.setColumnWidth(3,60) + self.uploadChangesTable.setColumnWidth(4,60) + + self.uploadChangesTable.setHeaderLabels(["","Points","Lines","Polygons","Relations"]) + key=QString(self.dbm.currentKey) + p1=key.lastIndexOf(".") + p2=key.lastIndexOf("/") + key=key.mid(p2+1,p1-p2-1) + self.groupBox.setTitle(QString("Changes in ").append(key)) + + item = QTreeWidgetItem(["Added","0","0","0","0"]) + for i in range(1,5): + item.setTextAlignment(i,Qt.AlignHCenter) + self.uploadChangesTable.addTopLevelItem(item) + + item = QTreeWidgetItem(["Removed","0","0","0","0"]) + for i in range(1,5): + item.setTextAlignment(i,Qt.AlignHCenter) + self.uploadChangesTable.addTopLevelItem(item) + + item = QTreeWidgetItem(["Changed","0","0","0","0"]) + for i in range(1,5): + item.setTextAlignment(i,Qt.AlignHCenter) + self.uploadChangesTable.addTopLevelItem(item) + + self.userLineEdit.setFocus() + self.passwdLineEdit.setEchoMode(QLineEdit.Password) + self.uploadButton.setEnabled(False) + self.pseudoId_map={} + self.featureId_map={} + self.qhttp_map={} + self.progressDialog=QProgressDialog(self) + self.progressDialog.setModal(True) + self.finished=False + self.httpRequestAborted=False + self.savePasswd=False + self.changesetId=None + + settings=QSettings() + if settings.contains("/OSM_Plugin/uploadUser"): + uplUser=settings.value("/OSM_Plugin/uploadUser",QVariant(QString())).toString() + self.userLineEdit.setText(uplUser) + if settings.contains("/OSM_Plugin/uploadPasswd"): + uplPasswd=settings.value("/OSM_Plugin/uploadPasswd",QVariant(QString())).toString() + self.passwdLineEdit.setText(uplPasswd) + self.chkSavePasswd.setChecked(True) + + self.commentTextEdit.setFocus() + + # phases: 0.changeset creation, 1.nodes creation, 2.ways deletion, 3.ways update, + # 4.ways addition, 5.nodes deletion, 6.nodes update, 7.relation creation, + # 8.relation deletion, 9.relation update, 10.changeset closing + self.phase=-1 + self.cntPhases=11 + self.cntActionsInPhase=[0]*self.cntPhases + self.cntActionsInPhaseDone=0 + self.cntActionsDone=0 + + self.createStatistics() + if self.cntActionsAll<=2: + # no action to upload (except for changeset opening and closing) + self.commentTextEdit.setEnabled(False) + self.accountGroupBox.setEnabled(False) + + self.showStatistics() + self.__prepareDatabaseQueries() + + # connect signals + self.connect(self.uploadButton, SIGNAL("clicked()"), self.uploadChanges) + self.connect(self.userLineEdit, SIGNAL("textChanged(const QString &)"), self.enableUploadButton) + self.connect(self.passwdLineEdit, SIGNAL("textChanged(const QString &)"), self.enableUploadButton) + self.connect(self.chkShowPasswd, SIGNAL("clicked()"), self.__showPassword) + self.connect(self.chkSavePasswd, SIGNAL("clicked()"), self.__savePassword) + self.connect(self.progressDialog, SIGNAL("canceled()"), self.__cancelProgressDlg) + + self.enableUploadButton() + + # http connection + self.http=QHttp(self) + + self.connect(self.http,SIGNAL("responseHeaderReceived(QHttpResponseHeader)"), self.__readResponseHeader) + self.connect(self.http,SIGNAL("authenticationRequired(const QString &, quint16, QAuthenticator *)"), self.__authRequired) + + self.__setProxy() + self.reqSetHost=self.http.setHost(self.urlHost, 80) + self.httpRequestAborted = False + self.httpRequestCanceled = False + self.httpRequestZombie = False + self.responseHeader="" + + # increase maximum recursion depth in python (__uploadStep is recursive function) + setrecursionlimit(1000000) + + + def uploadChanges(self): + """Main function; starts the whole upload process. + If checkbox for password saving was checked, password is stored to Quantum GIS settings. + """ + + self.progressDialog.setWindowTitle(self.tr("OSM Upload")) + self.progressDialog.setLabelText(self.tr("Uploading data...")) + self.uploadButton.setEnabled(False) + + self.cursor=self.dbm.getConnection().cursor() + + settings=QSettings() + settings.setValue("/OSM_Plugin/uploadUser",QVariant(self.userLineEdit.text())) + if self.savePasswd: + settings.setValue("/OSM_Plugin/uploadPasswd",QVariant(self.passwdLineEdit.text())) + else: + settings.remove("/OSM_Plugin/uploadPasswd") + + self.reqSetUser=self.http.setUser(self.userLineEdit.text(),self.passwdLineEdit.text()) + + # start upload with its first step (the next steps will follow automatically) + self.__uploadStep() + + + def __uploadStep(self): + """Function calls the next step of uploading. + """ + + # first update progressbar value + self.progressDialog.setMaximum(self.cntActionsAll) + self.progressDialog.setValue(self.cntActionsDone) + + # check if (in actual upload phase) number of done actions reach all actions that should be done; + # if it does, switch actual upload phase to the next one in the order + if (self.phase==-1) or (self.cntActionsInPhaseDone==self.cntActionsInPhase[self.phase]): + self.dbm.commit() # commit actions performed in finished phase + + # search for first next phase in which some upload actions should be done + self.phase+=1 + while self.phase=self.cntPhases: + self.cursor.close() # all upload phases were finished! + self.__finishUpload() + return + + # yes, it is ;) tell its number + # print QString("Starting upload phase no.%1.").arg(self.phase) + + # if necessary set up database cursor + if not self.phase in (0,10): + self.cursor.execute(self.selectQuery[self.phase]) + + # no action has been done yet in running phase + self.cntActionsInPhaseDone = 0 + + # just print common info + # print QString("Running step %1 of upload phase %2.").arg(self.cntActionsInPhaseDone+1).arg(self.phase) + + # perform next action in running phase + if self.phase not in (0,10): + record = self.cursor.fetchone() + if record == None: + # strange situation, number of features ready to upload in actual phase was actually lower than it was signalized + # by self.cntActionsAll; well, ignore this step and run the next one, that will start the next upload phase! + self.cntActionsInPhaseDone = self.cntActionsInPhase[self.phase] + self.__uploadStep() + + if self.phase == 0: # compulsory changeset creation + self.__createChangeset() + elif self.phase == 1: # nodes creation + self.__uploadNodeAddition(record) + elif self.phase == 2: # ways deletion + self.__uploadWayDeletion(record) + elif self.phase == 3: # ways update + self.__uploadWayUpdate(record) + elif self.phase == 4: # ways creation + self.__uploadWayAddition(record) + elif self.phase == 5: # nodes deletion + self.__uploadNodeDeletion(record) + elif self.phase == 6: # nodes update + self.__uploadNodeUpdate(record) + elif self.phase == 7: # relations creation + self.__uploadRelationAddition(record) + elif self.phase == 8: # relations deletion + self.__uploadRelationDeletion(record) + elif self.phase == 9: # relations update + self.__uploadRelationUpdate(record) + elif self.phase == 10: # compulsory changeset closing + self.__closeChangeset() + else: + print "Program shouldn't reach this place! (2)" + return + + + def __showPassword(self): + """Function to show hidden password on dialog box, + so that user can verify if he has written it right. + """ + + if self.chkShowPasswd.isChecked(): + self.passwdLineEdit.setEchoMode(QLineEdit.Normal) + else: + self.passwdLineEdit.setEchoMode(QLineEdit.Password) + + + def __savePassword(self): + """Function to show hidden password on dialog box, + so that user can verify if he has written it right. + """ + + if self.chkSavePasswd.isChecked(): + self.savePasswd=True + else: + self.savePasswd=False + + + def createStatistics(self): + """Function finds out number of steps for all upload phases. + These statistics are found out with examining features' statuses. + Results are stored in uploaders' member variables. + """ + + c=self.dbm.getConnection().cursor() + self.cntActionsAll = 0 + + # phases: 0.changeset creation, 1.nodes creation, 2.ways deletion, 3.ways update, + # 4.ways addition, 5.nodes deletion, 6.nodes update, 7.relation creation, + # 8.relation deletion, 9.relation update, 10.changeset closing + + c.execute("select count(*) from node where status='A' and u=1") + for row in c: + self.cntNodesCreated = row[0] + self.cntActionsAll += row[0] + + c.execute("select count(*) from way where status='A' and closed=0 and u=1") + for row in c: + self.cntLinesCreated = row[0] + self.cntActionsAll += row[0] + + c.execute("select count(*) from way where status='A' and closed=1 and u=1") + for row in c: + self.cntPolygonsCreated = row[0] + self.cntActionsAll += row[0] + + c.execute("select count(*) from node where status='R' and u=1") + for row in c: + self.cntNodesRemoved = row[0] + self.cntActionsAll += row[0] + + c.execute("select count(*) from way where status='R' and closed=0 and u=1") + for row in c: + self.cntLinesRemoved = row[0] + self.cntActionsAll += row[0] + + c.execute("select count(*) from way where status='R' and closed=1 and u=1") + for row in c: + self.cntPolygonsRemoved = row[0] + self.cntActionsAll += row[0] + + c.execute("select count(*) from node where status='U' and u=1") + for row in c: + self.cntNodesUpdated = row[0] + self.cntActionsAll += row[0] + + c.execute("select count(*) from way where status='U' and closed=0 and u=1") + for row in c: + self.cntLinesUpdated = row[0] + self.cntActionsAll += row[0] + + c.execute("select count(*) from way where status='U' and closed=1 and u=1") + for row in c: + self.cntPolygonsUpdated = row[0] + self.cntActionsAll += row[0] + + c.execute("select count(*) from relation where status='A' and u=1") + for row in c: + self.cntRelationsCreated = row[0] + self.cntActionsAll += row[0] + + c.execute("select count(*) from relation where status='R' and u=1") + for row in c: + self.cntRelationsRemoved = row[0] + self.cntActionsAll += row[0] + + c.execute("select count(*) from relation where status='U' and u=1") + for row in c: + self.cntRelationsUpdated = row[0] + self.cntActionsAll += row[0] + + self.cntActionsInPhase[0] = 1 + self.cntActionsInPhase[1] = self.cntNodesCreated + self.cntActionsInPhase[2] = self.cntLinesRemoved+self.cntPolygonsRemoved + self.cntActionsInPhase[3] = self.cntLinesUpdated+self.cntPolygonsUpdated + self.cntActionsInPhase[4] = self.cntLinesCreated+self.cntPolygonsCreated + self.cntActionsInPhase[5] = self.cntNodesRemoved + self.cntActionsInPhase[6] = self.cntNodesUpdated + self.cntActionsInPhase[7] = self.cntRelationsCreated + self.cntActionsInPhase[8] = self.cntRelationsRemoved + self.cntActionsInPhase[9] = self.cntRelationsUpdated + self.cntActionsInPhase[10] = 1 + self.cntActionsAll += 2 + c.close() + + + def zeroStatistics(self): + """Function cancels all statistics of uploader. + """ + + for i in range(1,self.cntPhases-1): + self.cntActionsInPhase[i] = 0 + self.cntActionsAll = 2 + + self.cntNodesCreated = 0 + self.cntLinesRemoved = 0 + self.cntPolygonsRemoved = 0 + self.cntLinesCreated = 0 + self.cntPolygonsCreated = 0 + self.cntNodesRemoved = 0 + self.cntNodesUpdated = 0 + self.cntLinesUpdated = 0 + self.cntPolygonsUpdated = 0 + self.cntRelationsCreated = 0 + self.cntRelationsRemoved = 0 + self.cntRelationsUpdated = 0 + + + def showStatistics(self): + """Function shows (statistics) counts of typical actions that are ready for upload. + + Showing them is realized on OSM upload dialog with appropriate table of statistics. + """ + + # phases: 0.changeset creation, 1.nodes creation, 2.ways deletion, 3.ways addition, + # 4.nodes deletion, 5.nodes update, 6.ways update, 7.relations addition, 8.relations deletion, + # 9.relations update, 10.changeset closing + + self.uploadChangesTable.topLevelItem(0).setText(1,str(self.cntNodesCreated)) + self.uploadChangesTable.topLevelItem(1).setText(1,str(self.cntNodesRemoved)) + self.uploadChangesTable.topLevelItem(2).setText(1,str(self.cntNodesUpdated)) + + self.uploadChangesTable.topLevelItem(0).setText(2,str(self.cntLinesCreated)) + self.uploadChangesTable.topLevelItem(1).setText(2,str(self.cntLinesRemoved)) + self.uploadChangesTable.topLevelItem(2).setText(2,str(self.cntLinesUpdated)) + + self.uploadChangesTable.topLevelItem(0).setText(3,str(self.cntPolygonsCreated)) + self.uploadChangesTable.topLevelItem(1).setText(3,str(self.cntPolygonsRemoved)) + self.uploadChangesTable.topLevelItem(2).setText(3,str(self.cntPolygonsUpdated)) + + self.uploadChangesTable.topLevelItem(0).setText(4,str(self.cntRelationsCreated)) + self.uploadChangesTable.topLevelItem(1).setText(4,str(self.cntRelationsRemoved)) + self.uploadChangesTable.topLevelItem(2).setText(4,str(self.cntRelationsUpdated)) + + + def __prepareDatabaseQueries(self): + """Function prepares SQL queries for selecting objects to upload. + Resulting array of queries is stored in member variable. + """ + + self.selectQuery = [ + # changeset creation + "" + # nodes creation + ,"select n.id, n.lat, n.lon, n.user, n.timestamp, (select version_id \ + from version where object_id=n.id and object_type='node') version_id from node n where n.status='A' and n.u=1" + # ways deletion + ,"select w.id, w.user, w.timestamp, (select version_id from version where \ + object_id=w.id and object_type='way') version_id, w.closed from way w where w.status='R' and w.u=1" + # ways update + ,"select w.id, w.user, w.timestamp, (select version_id from version \ + where object_id=w.id and object_type='way') version_id, w.closed from way w where w.status='U' and w.u=1" + # ways creation + ,"select w.id, w.user, w.timestamp, (select version_id from version \ + where object_id=w.id and object_type='way') version_id, w.closed from way w where w.status='A' and w.u=1" + # nodes deletion + ,"select n.id, n.lat, n.lon, n.user, n.timestamp, (select version_id \ + from version where object_id=n.id and object_type='node') version_id from node n where n.status='R' and n.u=1" + # nodes update + ,"select n.id, n.lat, n.lon, n.user, n.timestamp, (select version_id \ + from version where object_id=n.id and object_type='node') version_id from node n where n.status='U' and n.u=1" + + # relations creation + ,"select r.id, r.user, r.timestamp, (select version_id \ + from version where object_id=r.id and object_type='relation') version_id from relation r where r.status='A' and r.u=1" + # relations deletion + ,"select r.id, r.user, r.timestamp, (select version_id \ + from version where object_id=r.id and object_type='relation') version_id from relation r where r.status='R' and r.u=1" + # relations update + ,"select r.id, r.user, r.timestamp, (select version_id \ + from version where object_id=r.id and object_type='relation') version_id from relation r where r.status='U' and r.u=1" + # changeset closing + ,"" + ] + + + def __uploadNodeAddition(self, nodeRecord): + """Sends upload request for addition of new node. + + @param nodeRecord tuple object with information on node + """ + + # create http connection with neccessary authentification + urlPath = "/api/0.6/node/create" + + # set http request's header + header = QHttpRequestHeader("PUT", urlPath,1,1) + header.setValue("Host", self.urlHost) + header.setValue("Connection", "keep-alive") + header.setContentType("text/xml; charset=utf-8") + + # create http request's body (create XML with info about uploaded node) + requestXml = self.createNodeXml(nodeRecord) + + # connect http response signals to appropriate functions + self.connect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpNodeAdditionReqFinished) + + # send prepared request + requestBytes = requestXml.toAscii() + httpSessionId = self.http.request(header, requestBytes) + + # remember http connection object and pseudoId, that was used + # to node's identification in sqlite3 database + self.qhttp_map[httpSessionId]=1 + self.pseudoId_map[httpSessionId]=nodeRecord[0] + + + def __uploadNodeUpdate(self, nodeRecord): + """Sends upload request for updating one node. + + @param nodeRecord tuple object with information on node + """ + + urlPath = QString("/api/0.6/node/%1").arg(nodeRecord[0]) + + # set http request's header + header = QHttpRequestHeader("PUT", urlPath,1,1) + header.setValue("Host", self.urlHost) + header.setValue("Connection", "keep-alive") + header.setContentType("text/xml; charset=utf-8") + + # create http request's body (create XML with info about uploaded node) + requestXml = self.createNodeXml(nodeRecord) + + # connect http response signals to appropriate functions + self.connect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpNodeUpdateReqFinished) + + # send prepared request + requestBytes=requestXml.toAscii() + httpSessionId=self.http.request(header, requestBytes) + + # remember http connection object + self.qhttp_map[httpSessionId]=1 + self.featureId_map[httpSessionId]=nodeRecord[0] + + + def __uploadNodeDeletion(self, nodeRecord): + """Send upload request for deletion of one node. + + @param nodeRecord tuple object with information on node + """ + + # create http connection with neccessary authentification + urlPath = QString("/api/0.6/node/%1").arg(nodeRecord[0]) + + # set http request's header + header = QHttpRequestHeader("DELETE", urlPath,1,1) + header.setValue("Host", self.urlHost) + header.setValue("Connection", "keep-alive") + header.setContentType("text/xml; charset=utf-8") + + # connect http response signals to appropriate functions + self.connect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpNodeDeletionReqFinished) + + requestXml=self.createNodeDelXml(nodeRecord) + + # send prepared request + requestBytes = requestXml.toAscii() + httpSessionId = self.http.request(header, requestBytes) + + # remember http connection object + self.qhttp_map[httpSessionId]=1 + self.featureId_map[httpSessionId]=nodeRecord[0] + + + def createNodeXml(self, nodeRecord): + """Function creates XML that will be added as http body to node addition request. + + @param nodeRecord tuple object with information on node + """ + + requestXml = QString("") + version = nodeRecord[5] # version_id + requestXml.append(QString("")) + requestXml.append(QString("None: + requestXml.append(QString(" version=\"%1\"").arg(str(version))) + requestXml.append(QString(" changeset=\"%1\">") + .arg(str(self.changesetId)) + ) + + # selecting tags to construct correct XML + ct=self.dbm.getConnection().cursor() + ct.execute("select key, val from tag where object_id=:nodeId and object_type=\"node\"",{"nodeId":nodeRecord[0]}) + for tagRecord in ct: + requestXml.append(QString("").arg(tagRecord[0]).arg(tagRecord[1])) + ct.close() + + requestXml.append(QString("")) + return requestXml + + + def createNodeDelXml(self, nodeRecord): + """Function creates XML that will be added as http body to node deletion request. + + @param nodeRecord tuple object with information on node + """ + + requestXml = QString("") + requestXml.append(QString("")) + requestXml.append(QString("").arg(str(self.changesetId))) + requestXml.append(QString("")) + return requestXml + + + def createWayXml(self, wayRecord): + """Creates XML that have to be added as http body to way addition request. + + @param wayRecord tuple object with information on way + """ + + pseudoWayId = wayRecord[0] + requestXml = QString("") + + version = wayRecord[3] # version_id + closed = wayRecord[4] + requestXml.append(QString("")) + requestXml.append(QString("None: + requestXml.append(QString(" version=\"%1\"").arg(str(version))) + requestXml.append(QString(" changeset=\"%1\">") + .arg(str(self.changesetId)) + ) + + # selecting way members to construct correct XML + firstMember = None + cwm=self.dbm.getConnection().cursor() + cwm.execute("select node_id from way_member where way_id=:pseudoWayId and u=1",{"pseudoWayId": pseudoWayId}) + for wayMemberRecord in cwm: + if firstMember==None: + firstMember=wayMemberRecord[0] + requestXml.append(QString("").arg(wayMemberRecord[0])) + cwm.close() + + if closed==1: + tmp=None + + if firstMember: + requestXml.append(QString("").arg(firstMember)) + + # selecting tags to construct correct XML + ct=self.dbm.getConnection().cursor() + ct.execute("select key, val from tag where object_id=:pseudoWayId and u=1",{"pseudoWayId": pseudoWayId}) + for tagRecord in ct: + tmp=None + requestXml.append(QString("").arg(tagRecord[0]).arg(tagRecord[1])) + ct.close() + + # finish XML construction + requestXml.append(QString("")) + return requestXml + + + def __uploadWayAddition(self, wayRecord): + """Sends upload request for addition of new way. + + @param wayRecord tuple object with information on way + """ + + pseudoWayId = wayRecord[0] + + # create http connection with neccessary authentification + urlPath = "/api/0.6/way/create" + + # set http request's header + header = QHttpRequestHeader("PUT", urlPath,1,1) + header.setValue("Host", self.urlHost) + header.setValue("Connection", "keep-alive") + header.setContentType("text/xml; charset=utf-8") + + # connect http response signals to appropriate functions + self.connect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpWayAdditionReqFinished) + + # create http request's body (create XML with info about uploaded way) + requestXml = self.createWayXml(wayRecord) + + # send prepared request + requestBytes = requestXml.toAscii() + httpSessionId = self.http.request(header, requestBytes) + + # remember pseudoId, that was used to nodes identification in sqlite3 database + self.qhttp_map[httpSessionId]=1 + self.pseudoId_map[httpSessionId]=pseudoWayId + + + def __uploadWayUpdate(self, wayRecord): + """Send upload request for updating given way. + + @param wayRecord tuple object with information on way + """ + + # create http connection with neccessary authentification + urlPath = QString("/api/0.6/way/%1").arg(wayRecord[0]) + + # set http request's header + header=QHttpRequestHeader("PUT", urlPath,1,1) + header.setValue("Host", self.urlHost) + header.setValue("Connection", "keep-alive") + header.setContentType("text/xml; charset=utf-8") + + # connect http response signals to appropriate functions + self.connect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpWayUpdateReqFinished) + + # create http request's body (create XML with info about uploaded way) + requestXml = self.createWayXml(wayRecord) + + # send prepared request + requestBytes = requestXml.toAscii() + httpSessionId = self.http.request(header, requestBytes) + + self.qhttp_map[httpSessionId]=1 + self.featureId_map[httpSessionId]=wayRecord[0] + + + def __uploadWayDeletion(self, wayRecord): + """Send upload request for removing given way. + + @param wayRecord tuple object with information on way + """ + + # create http connection with neccessary authentification + urlPath = QString("/api/0.6/way/%1").arg(wayRecord[0]) + + # set http request's header + header = QHttpRequestHeader("DELETE", urlPath,1,1) + header.setValue("Host", self.urlHost) + header.setValue("Connection", "keep-alive") + header.setContentType("text/xml; charset=utf-8") + + # connect http response signals to appropriate functions + self.connect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpWayDeletionReqFinished) + + # create http request's body (create XML with info about uploaded way deletion) + requestXml = self.createWayXml(wayRecord) + + # send prepared request + requestBytes = requestXml.toAscii() + httpSessionId = self.http.request(header, requestBytes) + + # remember http connection object + self.qhttp_map[httpSessionId]=1 + self.featureId_map[httpSessionId]=wayRecord[0] + + + def createRelationXml(self, relRecord): + """Creates XML that will be added to relation addition/deletion/update http request. + + @param relRecord tuple object with information on relation + """ + + pseudoRelId = relRecord[0] + requestXml = QString("") + + version = relRecord[3] # version_id + requestXml.append(QString("")) + requestXml.append(QString("None: + requestXml.append(QString(" version=\"%1\"").arg(str(version))) + requestXml.append(QString(" changeset=\"%1\">").arg(str(self.changesetId))) + + # selecting way members to construct correct XML + firstMember = None + c=self.dbm.getConnection().cursor() + c.execute("select member_type, role, member_id from relation_member where relation_id=:pseudoRelId and u=1",{"pseudoRelId": pseudoRelId}) + for relMemberRecord in c: + requestXml.append(QString("None: + requestXml.append(QString(" role=\"%1\"").arg(relMemberRecord[1])) + requestXml.append(QString(" ref=\"%1\"/>").arg(relMemberRecord[2])) + c.close() + + # selecting tags to construct correct XML + ct=self.dbm.getConnection().cursor() + ct.execute("select key, val from tag where object_id=:pseudoRelId and object_type='relation' and u=1",{"pseudoRelId": pseudoRelId}) + for tagRecord in ct: + requestXml.append(QString("").arg(tagRecord[0]).arg(tagRecord[1])) + ct.close() + + # finish XML construction + requestXml.append(QString("")) + return requestXml + + + def __uploadRelationAddition(self, relRecord): + """Sends upload request for addition of new relation. + + @param relRecord tuple object with information on relation + """ + + pseudoRelId = relRecord[0] + + # create http connection with neccessary authentification + urlPath = "/api/0.6/relation/create" + + # set http request's header + header = QHttpRequestHeader("PUT", urlPath,1,1) + header.setValue("Host", self.urlHost) + header.setValue("Connection", "keep-alive") + header.setContentType("text/xml; charset=utf-8") + + # connect http response signals to appropriate functions + self.connect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpRelationAdditionReqFinished) + + # create http request's body (create XML with info about uploaded way) + requestXml = self.createRelationXml(relRecord) + + # send prepared request + requestBytes = requestXml.toAscii() + httpSessionId = self.http.request(header, requestBytes) + + # remember pseudoId, that was used to nodes identification in sqlite3 database + self.qhttp_map[httpSessionId]=1 + self.pseudoId_map[httpSessionId]=pseudoRelId + + + def __uploadRelationUpdate(self,relRecord): + """Sends upload request for updating given relation. + + @param relRecord tuple object with information on relation + """ + + # create http connection with neccessary authentification + urlPath = QString("/api/0.6/relation/%1").arg(relRecord[0]) + + # set http request's header + header=QHttpRequestHeader("PUT", urlPath,1,1) + header.setValue("Host", self.urlHost) + header.setValue("Connection", "keep-alive") + header.setContentType("text/xml; charset=utf-8") + + # connect http response signals to appropriate functions + self.connect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpRelationUpdateReqFinished) + + # create http request's body (create XML with info about uploaded way) + requestXml = self.createRelationXml(relRecord) + + # send prepared request + requestBytes = requestXml.toAscii() + httpSessionId = self.http.request(header, requestBytes) + + self.qhttp_map[httpSessionId]=1 + self.featureId_map[httpSessionId]=relRecord[0] + + + def __uploadRelationDeletion(self, relRecord): + """Function sends upload request for removing given relation. + + @param relRecord tuple object with information on relation + """ + + + # create http connection with neccessary authentification + urlPath = QString("/api/0.6/relation/%1").arg(relRecord[0]) + + # set http request's header + header=QHttpRequestHeader("DELETE", urlPath,1,1) + header.setValue("Host", self.urlHost) + header.setValue("Connection", "keep-alive") + header.setContentType("text/xml; charset=utf-8") + + # connect http response signals to appropriate functions + self.connect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpRelationDeletionReqFinished) + + # create http request's body (create XML with info about uploaded way deletion) + requestXml = self.createRelationXml(relRecord) + + # send prepared request + requestBytes=requestXml.toAscii() + httpSessionId=self.http.request(header, requestBytes) + + # remember http connection object + self.qhttp_map[httpSessionId]=1 + self.featureId_map[httpSessionId]=relRecord[0] + + + def escape_html(self,text): + """Function replaces chars that are problematic for html, + such as < > & " ' with < > & " ' + + @param text text to replace problematic characters in + @return transformed text + """ + + text=text.replace(QString("&"), QString("&")); # must be first + text=text.replace(QString("<"), QString("<")); + text=text.replace(QString(">"), QString(">")); + text=text.replace(QString("\""), QString(""")); + text=text.replace(QString("'"), QString("'")); + return text + + + def __createChangeset(self): + """Function sends request for creating new OSM changeset. + + Changeset creation has to be first upload action. + Changeset closing has to be the last one. + """ + + # create http connection with neccessary authentification + urlPath="/api/0.6/changeset/create" + + # set http request's header + header=QHttpRequestHeader("PUT",urlPath,1,1) + header.setValue("Host", self.urlHost) + header.setValue("Connection", "keep-alive") + header.setContentType("text/xml; charset=utf-8") + + # connect http response signals to appropriate functions + self.connect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpChangesetCreationReqFinished) + + # get user comment on upload actions + userComment=self.commentTextEdit.toPlainText() + userComment=self.escape_html(userComment) + userCommentBytes=userComment.toUtf8() + + # create http request's body (create XML with info about uploaded way) + requestXml=QString("\n\n\n\n\n") + + # send prepared request + requestBytes=requestXml.toAscii() + httpSessionId=self.http.request(header, requestBytes) + + # remember http connection object + self.qhttp_map[httpSessionId]=1 + + + def __closeChangeset(self): + """Function sends request for closing opened OSM changeset. + + Changeset creation has to be first upload action. + Changeset closing has to be the last one. + """ + + # create http connection with neccessary authentification + urlPath = QString("/api/0.6/changeset/%1/close").arg(self.changesetId) + + # set http request's header + header = QHttpRequestHeader("PUT",urlPath,1,1) + header.setValue("Host", self.urlHost) + header.setValue("Connection", "keep-alive") + header.setContentLength(0) + + # connect http response signals to appropriate functions + self.connect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpChangesetClosingReqFinished) + self.changesetId=None + + # send prepared request + httpSessionId=self.http.request(header) + + # remember http connection object + self.qhttp_map[httpSessionId]=1 + + + def enableUploadButton(self): + """Function finds out if it is possible to enable upload button. + If yes, function just enables it, else it disables it. + + Following condition has to be satisfied to enable upload button: + -There has to be something to upload. + -Both user login and password has to be filled. + """ + + user=self.userLineEdit.text() + passwd=self.passwdLineEdit.text() + + if not self.finished and user<>"" and passwd<>"" and self.cntActionsAll>2: + self.uploadButton.setEnabled(True) + else: + self.uploadButton.setEnabled(False) + + + def __finishUpload(self): + """This is function that runs after all edit actions changes are uploaded to OSM server. + + It clears edit changes tables, hide progress bar, clear statistics, + clear undo/redo data, etc. + """ + + self.finished = True + self.progressDialog.hide() + + # delete database records with status='R' (Remove) + self.dbm.removeUselessRecords() + + # show info that there are no more actions to upload + self.zeroStatistics() + self.showStatistics() + + # disable dialog items + self.userLineEdit.setEnabled(False) + self.passwdLineEdit.setEnabled(False) + self.chkShowPasswd.setEnabled(False) + + self.accept() + + # clear undo/redo + self.ur.clear() + + + def cancelUpload(self,errMessage=None): + """Function aborts the whole upload operation. + In errMessage function gets the reason of upload canceling (error message). + + If OSM changeset was already opened while uploading, HTTP request to close it is send immediately. + The HTTP connection of uploader is aborted, progress dialog is closed and the whole upload dialog is rejected. + + If there is some errMessage parameter, the message is shown to user. + + @param errMessage message to tell user after canceling upload + """ + + if self.httpRequestAborted: + return + + if self.changesetId: + + urlPath=QString("/api/0.6/changeset/%1/close").arg(self.changesetId) + header=QHttpRequestHeader("PUT",urlPath,1,1) + header.setValue("Host", self.urlHost) + header.setValue("Connection", "keep-alive") + httpSessionId=self.http.request(header) + + self.httpRequestAborted=True + self.http.abort() + + self.uploadButton.setEnabled(True) + self.disconnect(self.progressDialog, SIGNAL("canceled()"), self.__cancelProgressDlg) + self.progressDialog.close() + self.reject() + + if errMessage and errMessage<>"": + QMessageBox.information(self,self.tr("OSM Upload"),errMessage) + + + def __cancelProgressDlg(self): + """This functions catches the signal emitted when clicking on Cancel button of progress dialog. + It aborts opened HTTP connection and closes upload dialog. + """ + + if self.httpRequestAborted: + return + + self.httpRequestAborted=True + self.http.abort() + + self.uploadButton.setEnabled(True) + self.reject() + + + ########################################################## + # !!!!! HTTP RESPONSES FROM OPENSTREETMAP SERVER !!!!! # + ########################################################## + + + def __httpNodeAdditionReqFinished(self, requestId, error): + """Function is called when requestFinished(...) signal is emitted on global HTTP connection object + and "node adition" is current uploader's phase. + + OSM server returns id that was assigned to uploaded node. + + @param requestId identifier of http request + @param error True if error occured on given request; False otherwise + """ + + if self.httpRequestAborted: + return + + if not requestId in self.qhttp_map: + self.__examineResponse(requestId,error) + return + + self.disconnect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpNodeAdditionReqFinished) + + nodePseudoId=self.pseudoId_map[requestId] + del self.qhttp_map[requestId] + del self.pseudoId_map[requestId] + + if error: + self.cancelUpload("Node addition failed.") + return + + newNodeIdStr=QString(self.http.readAll().data()) + newNodeId=(newNodeIdStr.toInt())[0] + + # update node's identification in sqlite3 database + self.dbm.updateNodeId(nodePseudoId,newNodeId,self.userLineEdit.text()) + self.dbm.recacheAffectedNow([(nodePseudoId,'Point'),(newNodeId,'Point')]) + + # call the next upload step + self.cntActionsInPhaseDone+=1 + self.cntActionsDone+=1 + self.__uploadStep() + + + def __httpNodeUpdateReqFinished(self, requestId, error): + """Function is called when requestFinished(...) signal is emitted on global HTTP connection object + and "node updating" is current uploader's phase. + + OSM server returns id of new node's version. + + @param requestId identifier of http request + @param error True if error occured on given request; False otherwise + """ + + if self.httpRequestAborted: + return + + if not requestId in self.qhttp_map: + self.__examineResponse(requestId,error) + return + + self.disconnect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpNodeUpdateReqFinished) + + pointId=self.featureId_map[requestId] + del self.qhttp_map[requestId] + del self.featureId_map[requestId] + + if error: + self.cancelUpload("Node update failed.") + return + + newVersionIdStr=QString(self.http.readAll().data()) + newVersionId=(newVersionIdStr.toInt())[0] + + self.dbm.updateVersionId(pointId,'node',newVersionId) + self.dbm.updateUser(pointId,'node',self.userLineEdit.text()) + self.dbm.changePointStatus(pointId,'U','N') + self.dbm.recacheAffectedNow([(pointId,'Point')]) + + # call the next upload step + self.cntActionsInPhaseDone+=1 + self.cntActionsDone+=1 + self.__uploadStep() + + + def __httpNodeDeletionReqFinished(self, requestId, error): + """Function is called when requestFinished(...) signal is emitted on global HTTP connection object + and "node deletion" is current uploader's phase. + + @param requestId identifier of http request + @param error True if error occured on given request; False otherwise + """ + + if self.httpRequestAborted: + return + + if not requestId in self.qhttp_map: + self.__examineResponse(requestId,error) + return + + self.disconnect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpNodeDeletionReqFinished) + + pointId=self.featureId_map[requestId] + del self.qhttp_map[requestId] + del self.featureId_map[requestId] + + if error: + self.cancelUpload("Node deletion failed.") + return + + self.dbm.removePointRecord(pointId) + self.dbm.recacheAffectedNow([(pointId,'Point')]) + + # call the next upload step + self.cntActionsInPhaseDone+=1 + self.cntActionsDone+=1 + self.__uploadStep() + + + def __httpWayAdditionReqFinished(self, requestId, error): + """Function is called when requestFinished(...) signal is emitted on global HTTP connection object + and "way addition" is current uploader's phase. + + OSM server returns id that was assigned to uploaded way. + + @param requestId identifier of http request + @param error True if error occured on given request; False otherwise + """ + + if self.httpRequestAborted: + return + + if not requestId in self.qhttp_map: + self.__examineResponse(requestId,error) + return + + self.disconnect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpWayAdditionReqFinished) + + wayPseudoId = self.pseudoId_map[requestId] + del self.qhttp_map[requestId] + del self.pseudoId_map[requestId] + + if error: + self.cancelUpload("Way addition failed.") + return + + newWayIdStr = QString(self.http.readAll().data()) + newWayId = (newWayIdStr.toInt())[0] + + # update way identification in sqlite3 database + self.dbm.updateWayId(wayPseudoId,newWayId,self.userLineEdit.text()) + self.dbm.recacheAffectedNow([(wayPseudoId,'Line'),(newWayId,'Line'),(wayPseudoId,'Polygon'),(newWayId,'Polygon')]) + + # call the next upload step + self.cntActionsInPhaseDone+=1 + self.cntActionsDone+=1 + self.__uploadStep() + + + def __httpWayUpdateReqFinished(self, requestId, error): + """Function is called when requestFinished(...) signal is emitted on global HTTP connection object + and "way updating" is current uploader's phase. + + OSM server returns id of new way's version. + + @param requestId identifier of http request + @param error True if error occured on given request; False otherwise + """ + + if self.httpRequestAborted: + return + + if not requestId in self.qhttp_map: + self.__examineResponse(requestId,error) + return + + self.disconnect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpWayUpdateReqFinished) + + wayId=self.featureId_map[requestId] + del self.qhttp_map[requestId] + del self.featureId_map[requestId] + + if error: + self.cancelUpload("Way update failed.") + return + + newVersionIdStr=QString(self.http.readAll().data()) + newVersionId=(newVersionIdStr.toInt())[0] + + self.dbm.updateVersionId(wayId,'way',newVersionId) + self.dbm.updateUser(wayId,'way',self.userLineEdit.text()) + self.dbm.changeWayStatus(wayId,'U','N') + self.dbm.recacheAffectedNow([(wayId,'Line'),(wayId,'Polygon')]) + + # call the next upload step + self.cntActionsInPhaseDone+=1 + self.cntActionsDone+=1 + self.__uploadStep() + + + def __httpWayDeletionReqFinished(self, requestId, error): + """Function is called when requestFinished(...) signal is emitted on global HTTP connection object + and "way deletion" is current uploader's phase. + + @param requestId identifier of http request + @param error True if error occured on given request; False otherwise + """ + + if self.httpRequestAborted: + return + + if not requestId in self.qhttp_map: + self.__examineResponse(requestId,error) + return + + self.disconnect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpWayDeletionReqFinished) + + wayId=self.featureId_map[requestId] + del self.qhttp_map[requestId] + del self.featureId_map[requestId] + + if error: + self.cancelUpload("Way deletion failed.") + return + + self.dbm.removeWayRecord(wayId) + self.dbm.recacheAffectedNow([(wayId,'Line'),(wayId,'Polygon')]) + + # call the next upload step + self.cntActionsInPhaseDone+=1 + self.cntActionsDone+=1 + self.__uploadStep() + + + def __httpRelationAdditionReqFinished(self, requestId, error): + """Function is called when requestFinished(...) signal is emitted on global HTTP connection object + and "relation addition" is current uploader's phase. + + OSM server returns id that was assigned to uploaded relation. + + @param requestId identifier of http request + @param error True if error occured on given request; False otherwise + """ + + if self.httpRequestAborted: + return + + if not requestId in self.qhttp_map: + self.__examineResponse(requestId,error) + return + + self.disconnect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpRelationAdditionReqFinished) + + relPseudoId = self.pseudoId_map[requestId] + del self.qhttp_map[requestId] + del self.pseudoId_map[requestId] + + if error: + self.cancelUpload("Relation addition failed.") + return + + newRelIdStr=QString(self.http.readAll().data()) + newRelId=(newRelIdStr.toInt())[0] + + # update way identification in sqlite3 database + self.dbm.updateRelationId(relPseudoId,newRelId,self.userLineEdit.text()) + + # call the next upload step + self.cntActionsInPhaseDone+=1 + self.cntActionsDone+=1 + self.__uploadStep() + + + def __httpRelationUpdateReqFinished(self, requestId, error): + """Function is called when requestFinished(...) signal is emitted on global HTTP connection object + and "relation updating" is current uploader's phase. + + OSM server returns id of new relation's version. + + @param requestId identifier of http request + @param error True if error occured on given request; False otherwise + """ + + if self.httpRequestAborted: + return + + if not requestId in self.qhttp_map: + self.__examineResponse(requestId,error) + return + + self.disconnect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpRelationUpdateReqFinished) + + relId=self.featureId_map[requestId] + del self.qhttp_map[requestId] + del self.featureId_map[requestId] + + if error: + self.cancelUpload("Relation update failed.") + return + + newVersionIdStr=QString(self.http.readAll().data()) + newVersionId=(newVersionIdStr.toInt())[0] + + self.dbm.updateVersionId(relId,'relation',newVersionId) + self.dbm.updateUser(relId,'relation',self.userLineEdit.text()) + self.dbm.changeRelationStatus(relId,'U','N') + + # call the next upload step + self.cntActionsInPhaseDone+=1 + self.cntActionsDone+=1 + self.__uploadStep() + + + def __httpRelationDeletionReqFinished(self, requestId, error): + """Function is called when requestFinished(...) signal is emitted on global HTTP connection object + and "relation deletion" is current uploader's phase. + + @param requestId identifier of http request + @param error True if error occured on given request; False otherwise + """ + + if self.httpRequestAborted: + return + + if not requestId in self.qhttp_map: + self.__examineResponse(requestId,error) + return + + self.disconnect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpRelationDeletionReqFinished) + + relId=self.featureId_map[requestId] + del self.qhttp_map[requestId] + del self.featureId_map[requestId] + + if error: + self.cancelUpload("Relation deletion failed.") + return + + self.dbm.removeRelationRecord(relId) + + # call the next upload step + self.cntActionsInPhaseDone+=1 + self.cntActionsDone+=1 + self.__uploadStep() + + + def __httpChangesetCreationReqFinished(self, requestId, error): + """Function is called when requestFinished(...) signal is emitted on global HTTP connection object + and "changeset creation" is current uploader's phase. + + OSM server returns id that was assigned to created changeset. + + @param requestId identifier of http request + @param error True if error occured on given request; False otherwise + """ + + if self.httpRequestAborted: + return + + if not requestId in self.qhttp_map: + self.__examineResponse(requestId,error) + return + + self.disconnect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpChangesetCreationReqFinished) + + if error: + self.cancelUpload("Connection to OpenStreetMap server cannot be established. Please check your proxy settings, firewall settings and try again.") + return + + del self.qhttp_map[requestId] + changesetIdStr=QString(self.http.readAll().data()) + self.changesetId=(changesetIdStr.toInt())[0] + + # call the next upload step + self.cntActionsInPhaseDone+=1 + self.cntActionsDone+=1 + self.__uploadStep() + + + def __httpChangesetClosingReqFinished(self, requestId, error): + """Function is called when requestFinished(...) signal is emitted on global HTTP connection object + and "changeset closing" is current uploader's phase. + + @param requestId identifier of http request + @param error True if error occured on given request; False otherwise + """ + + if self.httpRequestAborted: + return + + if not requestId in self.qhttp_map: + self.__examineResponse(requestId,error) + return + + self.disconnect(self.http, SIGNAL("requestFinished(int, bool)"), self.__httpChangesetClosingReqFinished) + + if error: + self.cancelUpload("Changeset closing failed.") + return + + # call the next upload step + self.cntActionsInPhaseDone+=1 + self.cntActionsDone+=1 + self.__uploadStep() + + + 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 responseHeader.statusCode() != 200: + + self.cancelUpload(QString("Upload process failed. OpenStreetMap server response: %1 - %2.") + .arg(responseHeader.reasonPhrase()) + .arg(responseHeader.value("Error"))) + + + def __authRequired(self,s,a,b): + """Function is called when authenticationRequired(...) signal is emitted + on global HTTP connection object. + + We are really not interested in function parameters - we just cancel the connection. + """ + + self.cancelUpload("Authentification failed. Please try again with correct login and password.") + + + def __setProxy(self): + """Function sets proxy to HTTP connection of uploader. + + HTTP connection object is not given in function parameter, + because it's global - accessible for the whole uploader. + """ + + # 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.reqSetProxy=self.http.__setProxy(self.proxy) + + + def __examineResponse(self,requestId,error): + """If error occured function examines requestId and cancel upload for appropriate reason. + + @param requestId identifier of http request + @param error True if error occured on given request; False otherwise + """ + + if self.httpRequestAborted or not error: + return + + if requestId==self.reqSetHost: + self.cancelUpload("Setting host failed.") + + elif requestId==self.reqSetUser: + self.cancelUpload("Setting user and password failed.") + + elif requestId==self.reqSetProxy: + self.cancelUpload("Setting proxy failed.") + + + diff --git a/python/plugins/osm/DlgUploadOSM_ui.py b/python/plugins/osm/DlgUploadOSM_ui.py new file mode 100644 index 00000000000..8d0ea5af63a --- /dev/null +++ b/python/plugins/osm/DlgUploadOSM_ui.py @@ -0,0 +1,118 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ui_files/DlgUploadOSM.ui' +# +# Created: Tue Jul 14 14:44:26 2009 +# by: PyQt4 UI code generator 4.4.4 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +class Ui_DlgUploadOSM(object): + def setupUi(self, DlgUploadOSM): + DlgUploadOSM.setObjectName("DlgUploadOSM") + DlgUploadOSM.setWindowModality(QtCore.Qt.ApplicationModal) + DlgUploadOSM.resize(373, 468) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(DlgUploadOSM.sizePolicy().hasHeightForWidth()) + DlgUploadOSM.setSizePolicy(sizePolicy) + DlgUploadOSM.setModal(True) + self.vboxlayout = QtGui.QVBoxLayout(DlgUploadOSM) + self.vboxlayout.setObjectName("vboxlayout") + self.groupBox = QtGui.QGroupBox(DlgUploadOSM) + self.groupBox.setObjectName("groupBox") + self.vboxlayout1 = QtGui.QVBoxLayout(self.groupBox) + self.vboxlayout1.setObjectName("vboxlayout1") + self.uploadChangesTable = QtGui.QTreeWidget(self.groupBox) + self.uploadChangesTable.setEnabled(True) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.uploadChangesTable.sizePolicy().hasHeightForWidth()) + self.uploadChangesTable.setSizePolicy(sizePolicy) + self.uploadChangesTable.setMinimumSize(QtCore.QSize(330, 90)) + self.uploadChangesTable.setMaximumSize(QtCore.QSize(330, 90)) + self.uploadChangesTable.setAutoFillBackground(False) + self.uploadChangesTable.setTextElideMode(QtCore.Qt.ElideLeft) + self.uploadChangesTable.setRootIsDecorated(False) + self.uploadChangesTable.setItemsExpandable(False) + self.uploadChangesTable.setColumnCount(5) + self.uploadChangesTable.setObjectName("uploadChangesTable") + self.vboxlayout1.addWidget(self.uploadChangesTable) + self.label_4 = QtGui.QLabel(self.groupBox) + self.label_4.setObjectName("label_4") + self.vboxlayout1.addWidget(self.label_4) + self.commentTextEdit = QtGui.QTextEdit(self.groupBox) + self.commentTextEdit.setEnabled(True) + self.commentTextEdit.setMaximumSize(QtCore.QSize(16777215, 85)) + self.commentTextEdit.setObjectName("commentTextEdit") + self.vboxlayout1.addWidget(self.commentTextEdit) + self.vboxlayout.addWidget(self.groupBox) + self.accountGroupBox = QtGui.QGroupBox(DlgUploadOSM) + self.accountGroupBox.setEnabled(True) + self.accountGroupBox.setObjectName("accountGroupBox") + self.vboxlayout2 = QtGui.QVBoxLayout(self.accountGroupBox) + self.vboxlayout2.setObjectName("vboxlayout2") + self.gridlayout = QtGui.QGridLayout() + self.gridlayout.setObjectName("gridlayout") + self.label = QtGui.QLabel(self.accountGroupBox) + self.label.setObjectName("label") + self.gridlayout.addWidget(self.label, 0, 0, 1, 1) + self.userLineEdit = QtGui.QLineEdit(self.accountGroupBox) + self.userLineEdit.setEnabled(True) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.userLineEdit.sizePolicy().hasHeightForWidth()) + self.userLineEdit.setSizePolicy(sizePolicy) + self.userLineEdit.setObjectName("userLineEdit") + self.gridlayout.addWidget(self.userLineEdit, 0, 1, 1, 1) + self.label_2 = QtGui.QLabel(self.accountGroupBox) + self.label_2.setObjectName("label_2") + self.gridlayout.addWidget(self.label_2, 1, 0, 1, 1) + self.passwdLineEdit = QtGui.QLineEdit(self.accountGroupBox) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Preferred, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.passwdLineEdit.sizePolicy().hasHeightForWidth()) + self.passwdLineEdit.setSizePolicy(sizePolicy) + self.passwdLineEdit.setObjectName("passwdLineEdit") + self.gridlayout.addWidget(self.passwdLineEdit, 1, 1, 1, 1) + self.chkShowPasswd = QtGui.QCheckBox(self.accountGroupBox) + self.chkShowPasswd.setObjectName("chkShowPasswd") + self.gridlayout.addWidget(self.chkShowPasswd, 2, 1, 1, 1) + self.chkSavePasswd = QtGui.QCheckBox(self.accountGroupBox) + self.chkSavePasswd.setObjectName("chkSavePasswd") + self.gridlayout.addWidget(self.chkSavePasswd, 3, 1, 1, 1) + self.vboxlayout2.addLayout(self.gridlayout) + self.vboxlayout.addWidget(self.accountGroupBox) + self.buttonBox = QtGui.QDialogButtonBox(DlgUploadOSM) + self.buttonBox.setStandardButtons(QtGui.QDialogButtonBox.Close) + self.buttonBox.setObjectName("buttonBox") + self.vboxlayout.addWidget(self.buttonBox) + + self.retranslateUi(DlgUploadOSM) + QtCore.QObject.connect(self.buttonBox, QtCore.SIGNAL("rejected()"), DlgUploadOSM.reject) + QtCore.QMetaObject.connectSlotsByName(DlgUploadOSM) + DlgUploadOSM.setTabOrder(self.userLineEdit, self.passwdLineEdit) + DlgUploadOSM.setTabOrder(self.passwdLineEdit, self.chkShowPasswd) + DlgUploadOSM.setTabOrder(self.chkShowPasswd, self.buttonBox) + + def retranslateUi(self, DlgUploadOSM): + DlgUploadOSM.setWindowTitle(QtGui.QApplication.translate("DlgUploadOSM", "Upload OSM data", None, QtGui.QApplication.UnicodeUTF8)) + self.groupBox.setTitle(QtGui.QApplication.translate("DlgUploadOSM", "Ready for upload", None, QtGui.QApplication.UnicodeUTF8)) + self.uploadChangesTable.headerItem().setText(0, QtGui.QApplication.translate("DlgUploadOSM", "1", None, QtGui.QApplication.UnicodeUTF8)) + self.uploadChangesTable.headerItem().setText(1, QtGui.QApplication.translate("DlgUploadOSM", "2", None, QtGui.QApplication.UnicodeUTF8)) + self.uploadChangesTable.headerItem().setText(2, QtGui.QApplication.translate("DlgUploadOSM", "3", None, QtGui.QApplication.UnicodeUTF8)) + self.uploadChangesTable.headerItem().setText(3, QtGui.QApplication.translate("DlgUploadOSM", "4", None, QtGui.QApplication.UnicodeUTF8)) + self.uploadChangesTable.headerItem().setText(4, QtGui.QApplication.translate("DlgUploadOSM", "5", None, QtGui.QApplication.UnicodeUTF8)) + self.label_4.setText(QtGui.QApplication.translate("DlgUploadOSM", "Comment on your changes:", None, QtGui.QApplication.UnicodeUTF8)) + self.accountGroupBox.setTitle(QtGui.QApplication.translate("DlgUploadOSM", "OSM account", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("DlgUploadOSM", "Username:", None, QtGui.QApplication.UnicodeUTF8)) + self.label_2.setText(QtGui.QApplication.translate("DlgUploadOSM", "Password:", None, QtGui.QApplication.UnicodeUTF8)) + self.chkShowPasswd.setText(QtGui.QApplication.translate("DlgUploadOSM", "Show password", None, QtGui.QApplication.UnicodeUTF8)) + self.chkSavePasswd.setText(QtGui.QApplication.translate("DlgUploadOSM", "Save password", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/python/plugins/osm/DockUndoRedo.py b/python/plugins/osm/DockUndoRedo.py new file mode 100644 index 00000000000..b3297ecbb93 --- /dev/null +++ b/python/plugins/osm/DockUndoRedo.py @@ -0,0 +1,441 @@ +"""@package DockUndoRedo +This module holds evidence of user edit actions. + +Such evidence exists for each loaded OSM data. + +Module provides easy way how to call undo/redo actions. +""" + +from PyQt4.QtCore import * +from PyQt4.QtGui import * +from qgis.core import * +from qgis.gui import * + +from DockUndoRedo_ui import Ui_OsmUndoRedoDockWidget +from DatabaseManager import DatabaseManager + +import sqlite3 +from math import * +from time import * + + + +class DockUndoRedo(QDockWidget, Ui_OsmUndoRedoDockWidget, object): + """This class extends functionality of Ui_UndoRedo dialog which displays history of user edit actions. + Such history exists for each loaded OSM data. + + This class provides easy way how to call undo/redo actions. + """ + + def __init__(self, plugin): + """The constructor. + + Does basic initialization, connecting dialog signals to appropriate slots + and setting icons to its buttons. + + @param plugin pointer to OSM Plugin instance + """ + + QDockWidget.__init__(self, None) + + self.setupUi(self) + self.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) + + self.undoButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_undo.png")) + self.redoButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_redo.png")) + self.clearButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_remove.png")) + + self.canvas=plugin.canvas + self.iface=plugin.iface + self.dbm=plugin.dbm + self.plugin=plugin + + self.actionList.setDragDropMode(QAbstractItemView.NoDragDrop) + + QObject.connect(self.undoButton,SIGNAL("clicked()"),self.undo) + QObject.connect(self.redoButton,SIGNAL("clicked()"),self.redo) + QObject.connect(self.clearButton,SIGNAL("clicked()"),self.clear) + QObject.connect(self.actionList,SIGNAL("currentRowChanged(int)"),self.currRowChanged) + + # structures for evidence of user edit changes (required by undo and redo operations) + self.mapActions={} + self.mapIxAction={} + self.currentMapKey=None + + self.actStartId=self.actStopId=self.actNote=None + self.actionInProgress=False + + self.redoCounter=0 + self.undoCounter=0 + self.affected=set() + self.urIsBusy=False + + self.actionList.addItem(QString("")) + self.actionList.setCurrentRow(0) + + + def currRowChanged(self,row): + """This function is called after currentRowChanged(...) signal is emmited on dialog's list of edit actions. + Functions calls as many undo actions (as many redo actions) as needed to jump in editing history to selected row. + + @param row row number from the list of edit actions + """ + + if row<0: + return + self.goToAction(row) + + + def setContentEnabled(self,flag): + + if flag: + if self.undoCounter>0: + self.undoButton.setEnabled(True) + if self.redoCounter>0: + self.redoButton.setEnabled(True) + else: + self.undoButton.setEnabled(False) + self.redoButton.setEnabled(False) + + self.clearButton.setEnabled(flag) + self.actionList.setEnabled(flag) + + + def clear(self): + """Function clears (re-initializes) the whole undo/redo dialog. + """ + + if self.dbm and self.dbm.currentKey: + self.dbm.removeAllChangeSteps() + + self.actionList.clear() + self.actStartId=self.actStopId=self.actNote=None + self.actionInProgress=False + self.urIsBusy=False + + self.actionList.addItem(QString("")) + self.actionList.setCurrentRow(0) + + self.redoCounter=0 + self.redoButton.setEnabled(False) + self.undoCounter=0 + self.undoButton.setEnabled(False) + + if self.plugin: + if self.plugin.dockWidget: + self.plugin.dockWidget.redoButton.setEnabled(False) + self.plugin.dockWidget.undoButton.setEnabled(False) + + if self.currentMapKey: + self.mapActions[self.currentMapKey]=[] + self.mapIxAction[self.currentMapKey]=-1 + + + def databaseChanged(self,dbKey): + """Functions is called when current database of OSM Plugin changed. + + OSM Undo/Redo module clears its list of actions and loads the one for new current database. + If dbKey parameter is None, there is no current database. In this case function just clears the list + and reinitialize its inner structures. + + @param dbKey key/name of new current database file + """ + + # clear the list widget + self.actionList.clear() + + # clear inner structures + self.actStartId=self.actStopId=self.actNote=None + self.actionInProgress=False + self.currentMapKey=dbKey + self.urIsBusy=False + + if not dbKey: + self.setContentEnabled(False) + return + + self.setContentEnabled(True) + if dbKey in self.mapActions.keys(): + + # load the list widget + self.redoCounter=0 + self.redoButton.setEnabled(False) + self.plugin.dockWidget.redoButton.setEnabled(False) + self.undoCounter=0 + self.undoButton.setEnabled(False) + self.plugin.dockWidget.undoButton.setEnabled(False) + + self.actionList.addItem(QString("")) + + for action in self.mapActions[self.currentMapKey]: + self.actionList.addItem(action[2]) # 2 ~ actionNote! + + ixAction=self.mapIxAction[self.currentMapKey] + self.undoCounter=ixAction+1 + self.redoCounter=len(self.mapActions[self.currentMapKey])-self.undoCounter + self.actionList.setCurrentRow(ixAction+1) + + if self.undoCounter>0: + self.undoButton.setEnabled(True) + self.plugin.dockWidget.undoButton.setEnabled(True) + if self.redoCounter>0: + self.redoButton.setEnabled(True) + self.plugin.dockWidget.redoButton.setEnabled(True) + return + + # new dbKey has no undo/redo history yet + self.actionList.addItem(QString("")) + self.actionList.setCurrentRow(0) + + self.redoCounter=0 + self.redoButton.setEnabled(False) + self.plugin.dockWidget.redoButton.setEnabled(False) + self.undoCounter=0 + self.undoButton.setEnabled(False) + self.plugin.dockWidget.undoButton.setEnabled(False) + + self.mapActions[dbKey]=[] + self.mapIxAction[dbKey]=-1 + + + def goToAction(self,row): + """Functions goes to the selected row in history of edit actions. + It calls as many undo/redo operations, as needed. + + @param row row index to list of edit actions history + """ + curr=self.actionList.currentRow() + self.actionList.setEnabled(False) + + if not self.currentMapKey in self.mapIxAction: + return + + ixGoto=row # ix of row which was clicked + ixCurrent=self.mapIxAction[self.currentMapKey]+1 # current action index + + # how many undo/redo actions are necessary? + howFar=0 + self.affected=set() + + if ixCurrentixGoto: + # undo actions; we move "whitespace item" before the row which was clicked + howFar=ixCurrent-ixGoto + for ix in range(0,howFar): + self.undo(False) + else: + self.actionList.setEnabled(True) + QObject.disconnect(self.actionList,SIGNAL("currentRowChanged(int)"),self.currRowChanged) + self.actionList.setCurrentRow(curr) + QObject.connect(self.actionList,SIGNAL("currentRowChanged(int)"),self.currRowChanged) + return + + self.dbm.commit() + self.dbm.recacheAffectedNow(self.affected) + self.affected=set() + self.canvas.refresh() + + if self.plugin.dockWidget: + lFeat=self.plugin.dockWidget.feature + lFeatType=self.plugin.dockWidget.featureType + self.plugin.dockWidget.loadFeature(lFeat,lFeatType) + + self.actionList.setEnabled(True) + QObject.disconnect(self.actionList,SIGNAL("currentRowChanged(int)"),self.currRowChanged) + self.actionList.setCurrentRow(curr) + QObject.connect(self.actionList,SIGNAL("currentRowChanged(int)"),self.currRowChanged) + + + def startAction(self,actNote): + """Function remembers current state of system. + + This function is called before performing an action that should be put into editing history. + It's expected that you call stopAction() function after edit actions finishes. + Then new record in history will be created. + + @param actNote action description (in brief) + """ + + if not self.dbm.currentKey: + # there is no current database + return + + if self.actionInProgress: + print "failed! change in progress!" + return + + self.actionInProgress=True + self.actStartId=self.dbm.getCurrentActionNumber() + self.actStopId=None + self.actNote=actNote + + + def stopAction(self,affected=set()): + """Function is called after an edit action. It stores current state of system + and the state from last calling of startAction(). This two states of system + are considered new edit history record together and are stored in database. + + @param affected list of all OSM features that was affected with just finished action""" + + if not self.dbm.currentKey: + # there is no current database + return + + if affected==None: + affected=set() + + self.actStopId=self.dbm.getCurrentActionNumber() + if self.actStopId<=self.actStartId: + self.actionInProgress=False + return + + for ix in range(len(self.mapActions[self.currentMapKey]),self.actionList.currentRow(),-1): + self.actionList.takeItem(ix) + self.redoCounter=self.redoCounter-1 + + ixAction=self.mapIxAction[self.currentMapKey] + cntActions=len(self.mapActions[self.currentMapKey]) + if ixAction>-1: + fromId=self.mapActions[self.currentMapKey][ixAction][1]+1 + self.dbm.removeChangeStepsBetween(fromId,self.actStartId) + + del self.mapActions[self.currentMapKey][ixAction+1:cntActions] + + if self.redoCounter==0: + self.redoButton.setEnabled(False) + self.plugin.dockWidget.redoButton.setEnabled(False) + + self.mapIxAction[self.currentMapKey]=ixAction+1 + ixAction=ixAction+1 + + # increase undo counter + self.undoCounter=self.undoCounter+1 + self.undoButton.setEnabled(True) + self.plugin.dockWidget.undoButton.setEnabled(True) + + self.mapActions[self.currentMapKey].append((self.actStartId,self.actStopId,self.actNote,affected)) + self.actionList.addItem(self.actNote) + self.actionList.setCurrentRow(ixAction+1) + + self.actStartId=self.actStopId=self.actNote=None + self.actionInProgress=False + + + def undo(self,standAlone=True): + """Functions performs exactly one undo operation in system. + + Last edit action is reverted, list of editing history on undo/redo dialog + shifts its current row up. + + @param refresh if False, no canvas refresh will be performed after reverting an action; default is True + """ + + self.undoButton.setEnabled(False) + self.plugin.dockWidget.undoButton.setEnabled(False) + self.redoButton.setEnabled(False) + self.plugin.dockWidget.redoButton.setEnabled(False) + + (startId,stopId,note,affected)=self.mapActions[self.currentMapKey][self.mapIxAction[self.currentMapKey]] + + # shift up in the list widget + ixCurrent=self.actionList.currentRow() + QObject.disconnect(self.actionList,SIGNAL("currentRowChanged(int)"),self.currRowChanged) + self.actionList.setCurrentRow(ixCurrent-1) + QObject.connect(self.actionList,SIGNAL("currentRowChanged(int)"),self.currRowChanged) + + self.mapIxAction[self.currentMapKey]=self.mapIxAction[self.currentMapKey]-1 + + changeSteps=self.dbm.getChangeSteps(startId,stopId) + + for (change_type,tab_name,row_id,col_name,old_value,new_value) in changeSteps: + + if change_type=='I': + self.dbm.setRowDeleted(tab_name,row_id) + elif change_type=='D': + self.dbm.setRowNotDeleted(tab_name,row_id) + elif change_type=='U': + self.dbm.setRowColumnValue(tab_name,col_name,old_value,row_id) + + # increase redo counter + self.redoCounter=self.redoCounter+1 + # decrease undo counter + self.undoCounter=self.undoCounter-1 + + # refresh + if standAlone: + self.dbm.commit() + self.dbm.recacheAffectedNow(affected) + self.canvas.refresh() + self.plugin.dockWidget.loadFeature(self.plugin.dockWidget.feature,self.plugin.dockWidget.featureType) + else: + self.affected.update(affected) + + self.redoButton.setEnabled(True) + self.plugin.dockWidget.redoButton.setEnabled(True) + if self.undoCounter>0: + self.undoButton.setEnabled(True) + self.plugin.dockWidget.undoButton.setEnabled(True) + + + def redo(self,standAlone=True): + """Functions performs exactly one redo operation in system. + + Last reverted edit action is redone again. List of editing history on undo/redo dialog + shifts its current row down. + + @param refresh if False, no canvas refresh will be performed after redo action; default is True + """ + + self.undoButton.setEnabled(False) + self.plugin.dockWidget.undoButton.setEnabled(False) + self.redoButton.setEnabled(False) + self.plugin.dockWidget.redoButton.setEnabled(False) + + (startId,stopId,note,affected)=self.mapActions[self.currentMapKey][self.mapIxAction[self.currentMapKey]+1] + + # shift down in the list widget + ixCurrent=self.actionList.currentRow() + QObject.disconnect(self.actionList,SIGNAL("currentRowChanged(int)"),self.currRowChanged) + self.actionList.setCurrentRow(ixCurrent+1) + QObject.connect(self.actionList,SIGNAL("currentRowChanged(int)"),self.currRowChanged) + + self.mapIxAction[self.currentMapKey]=self.mapIxAction[self.currentMapKey]+1 + if self.mapIxAction[self.currentMapKey]==len(self.mapActions[self.currentMapKey]): + self.redoButton.setEnabled(False) + self.plugin.dockWidget.redoButton.setEnabled(False) + + changeSteps=self.dbm.getChangeSteps(startId,stopId) + for (change_type,tab_name,row_id,col_name,old_value,new_value) in changeSteps: + + if change_type=='I': + self.dbm.setRowNotDeleted(tab_name,row_id) + elif change_type=='D': + self.dbm.setRowDeleted(tab_name,row_id) + elif change_type=='U': + self.dbm.setRowColumnValue(tab_name,col_name,new_value,row_id) + + # decrease redo counter + self.redoCounter=self.redoCounter-1 + # increase undo counter + self.undoCounter=self.undoCounter+1 + + # refresh + if standAlone: + self.dbm.commit() + self.dbm.recacheAffectedNow(affected) + self.canvas.refresh() + self.plugin.dockWidget.loadFeature(self.plugin.dockWidget.feature,self.plugin.dockWidget.featureType) + else: + self.affected.update(affected) + + self.undoButton.setEnabled(True) + self.plugin.dockWidget.undoButton.setEnabled(True) + if self.redoCounter>0: + self.redoButton.setEnabled(True) + self.plugin.dockWidget.redoButton.setEnabled(True) + + diff --git a/python/plugins/osm/DockUndoRedo_ui.py b/python/plugins/osm/DockUndoRedo_ui.py new file mode 100644 index 00000000000..1172f3f9144 --- /dev/null +++ b/python/plugins/osm/DockUndoRedo_ui.py @@ -0,0 +1,59 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ui_files/DockUndoRedo.ui' +# +# Created: Wed Jul 29 12:14:34 2009 +# by: PyQt4 UI code generator 4.4.4 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +class Ui_OsmUndoRedoDockWidget(object): + def setupUi(self, OsmUndoRedoDockWidget): + OsmUndoRedoDockWidget.setObjectName("OsmUndoRedoDockWidget") + OsmUndoRedoDockWidget.resize(227, 374) + OsmUndoRedoDockWidget.setAllowedAreas(QtCore.Qt.AllDockWidgetAreas) + self.dockWidgetContents = QtGui.QWidget() + self.dockWidgetContents.setObjectName("dockWidgetContents") + self.vboxlayout = QtGui.QVBoxLayout(self.dockWidgetContents) + self.vboxlayout.setObjectName("vboxlayout") + self.hboxlayout = QtGui.QHBoxLayout() + self.hboxlayout.setObjectName("hboxlayout") + self.clearButton = QtGui.QToolButton(self.dockWidgetContents) + self.clearButton.setObjectName("clearButton") + self.hboxlayout.addWidget(self.clearButton) + spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.hboxlayout.addItem(spacerItem) + self.undoButton = QtGui.QToolButton(self.dockWidgetContents) + self.undoButton.setEnabled(False) + self.undoButton.setObjectName("undoButton") + self.hboxlayout.addWidget(self.undoButton) + self.redoButton = QtGui.QToolButton(self.dockWidgetContents) + self.redoButton.setEnabled(False) + self.redoButton.setObjectName("redoButton") + self.hboxlayout.addWidget(self.redoButton) + self.vboxlayout.addLayout(self.hboxlayout) + self.actionList = QtGui.QListWidget(self.dockWidgetContents) + self.actionList.setObjectName("actionList") + self.vboxlayout.addWidget(self.actionList) + OsmUndoRedoDockWidget.setWidget(self.dockWidgetContents) + + self.retranslateUi(OsmUndoRedoDockWidget) + QtCore.QMetaObject.connectSlotsByName(OsmUndoRedoDockWidget) + + def retranslateUi(self, OsmUndoRedoDockWidget): + OsmUndoRedoDockWidget.setWindowTitle(QtGui.QApplication.translate("OsmUndoRedoDockWidget", "OSM Edit History", None, QtGui.QApplication.UnicodeUTF8)) + self.clearButton.setToolTip(QtGui.QApplication.translate("OsmUndoRedoDockWidget", "Clear all", None, QtGui.QApplication.UnicodeUTF8)) + self.clearButton.setStatusTip(QtGui.QApplication.translate("OsmUndoRedoDockWidget", "Clear all", None, QtGui.QApplication.UnicodeUTF8)) + self.clearButton.setWhatsThis(QtGui.QApplication.translate("OsmUndoRedoDockWidget", "Clear all", None, QtGui.QApplication.UnicodeUTF8)) + self.clearButton.setText(QtGui.QApplication.translate("OsmUndoRedoDockWidget", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.undoButton.setToolTip(QtGui.QApplication.translate("OsmUndoRedoDockWidget", "Undo", None, QtGui.QApplication.UnicodeUTF8)) + self.undoButton.setStatusTip(QtGui.QApplication.translate("OsmUndoRedoDockWidget", "Undo", None, QtGui.QApplication.UnicodeUTF8)) + self.undoButton.setWhatsThis(QtGui.QApplication.translate("OsmUndoRedoDockWidget", "Undo", None, QtGui.QApplication.UnicodeUTF8)) + self.undoButton.setText(QtGui.QApplication.translate("OsmUndoRedoDockWidget", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.redoButton.setToolTip(QtGui.QApplication.translate("OsmUndoRedoDockWidget", "Redo", None, QtGui.QApplication.UnicodeUTF8)) + self.redoButton.setStatusTip(QtGui.QApplication.translate("OsmUndoRedoDockWidget", "Redo", None, QtGui.QApplication.UnicodeUTF8)) + self.redoButton.setWhatsThis(QtGui.QApplication.translate("OsmUndoRedoDockWidget", "Redo", None, QtGui.QApplication.UnicodeUTF8)) + self.redoButton.setText(QtGui.QApplication.translate("OsmUndoRedoDockWidget", "...", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/python/plugins/osm/DockWidget.py b/python/plugins/osm/DockWidget.py new file mode 100644 index 00000000000..b559d6ab1bb --- /dev/null +++ b/python/plugins/osm/DockWidget.py @@ -0,0 +1,1243 @@ +"""@package DockWidget +This module is descendant of "OSM Feature" dockable widget (in Quantum GIS) and makes user able +to view and edit information on selected OSM feature. + +DockWidget module shows details of selected feature - its basic info, tags and relations. +It provides methods for editing features' tags so that user can edit them directly on the widget. + +There are also some identify and edit buttons on "DockWidget" widget - this modul implements all the methods that are called +after clicking on these buttons. Such methods creates (and set) map tool that coresponds to specified button. +""" + + +from PyQt4.QtCore import * +from PyQt4.QtGui import * +from qgis.core import * +from qgis.gui import * + +from DockWidget_ui import Ui_OsmDockWidget +from DlgAddRelation import DlgAddRelation + +# include all osm map tools +from map_tools.CreatePointMapTool import CreatePointMapTool +from map_tools.CreateLineMapTool import CreateLineMapTool +from map_tools.CreatePolygonMapTool import CreatePolygonMapTool +from map_tools.MoveMapTool import MoveMapTool +from map_tools.IdentifyMapTool import IdentifyMapTool + + +class DockWidget(QDockWidget, Ui_OsmDockWidget, object): + """This class shows details of selected feature - its basic info, tags and relations. + + It provides methods for editing features' tags so that user can edit them directly on the widget. + + There are also some identify and edit buttons on "DockWidget" widget - this modul implements all the methods that are called + after clicking them. Such methods creates (and set) map tool that coresponds to specified button. + """ + + + def __init__(self, plugin): + """The constructor.""" + + QDockWidget.__init__(self, None) + self.setupUi(self) + self.setAllowedAreas(Qt.LeftDockWidgetArea | Qt.RightDockWidgetArea) + + # set icons for tool buttons (identify,move,createPoint,createLine,createPolygon) + self.identifyButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_identify.png")) + self.moveButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_move.png")) + self.createPointButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_createPoint.png")) + self.createLineButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_createLine.png")) + self.createPolygonButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_createPolygon.png")) + self.createRelationButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_createRelation.png")) + self.removeButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_remove.png")) + self.deleteTagsButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_remove.png")) + self.undoButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_undo.png")) + self.redoButton.setIcon(QIcon(":/plugins/osm_plugin/images/osm_redo.png")) + + self.toolButtons=QButtonGroup(self) + self.dummyButton.setVisible(False) + self.toolButtons.addButton(self.dummyButton) + self.toolButtons.addButton(self.identifyButton) + self.toolButtons.addButton(self.moveButton) + self.toolButtons.addButton(self.createPointButton) + self.toolButtons.addButton(self.createLineButton) + self.toolButtons.addButton(self.createPolygonButton) + self.toolButtons.setExclusive(True) + + self.tagsTableWidget.setColumnCount(2) + self.tagsTableWidget.setHorizontalHeaderItem(0,QTableWidgetItem("Key")) + self.tagsTableWidget.setHorizontalHeaderItem(1,QTableWidgetItem("Value")) + self.newTagLabel = "" + + self.plugin=plugin + self.__mapTool=None + self.__dlgAddRel=None + + # get qgis settings of line width and color for rubberband + settings=QSettings() + qgsLineWidth=settings.value( "/qgis/digitizing/line_width", QVariant(10) ).toInt() + qgsLineRed=settings.value( "/qgis/digitizing/line_color_red", QVariant(255) ).toInt() + qgsLineGreen=settings.value( "/qgis/digitizing/line_color_green", QVariant(0) ).toInt() + qgsLineBlue=settings.value( "/qgis/digitizing/line_color_blue", QVariant(0) ).toInt() + + self.rubBandPol=QgsRubberBand(plugin.canvas,True) + self.rubBandPol.setColor(QColor(qgsLineRed[0],qgsLineGreen[0],qgsLineBlue[0])) + self.rubBandPol.setWidth(qgsLineWidth[0]) + + self.rubBand=QgsRubberBand(plugin.canvas,False) + self.rubBand.setColor(QColor(qgsLineRed[0],qgsLineGreen[0],qgsLineBlue[0])) + self.rubBand.setWidth(qgsLineWidth[0]) + + self.verMarker=QgsVertexMarker(plugin.canvas) + self.verMarker.setIconType(2) + self.verMarker.setIconSize(13) + self.verMarker.setColor(QColor(qgsLineRed[0],qgsLineGreen[0],qgsLineBlue[0])) + self.verMarker.setPenWidth(qgsLineWidth[0]) + + self.verMarkers=[] + + self.relRubBandPol=QgsRubberBand(plugin.canvas,True) + self.relRubBandPol.setColor(QColor(qgsLineRed[0],50,50)) + self.relRubBandPol.setWidth(qgsLineWidth[0]+4) + + self.relRubBand=QgsRubberBand(plugin.canvas,False) + self.relRubBand.setColor(QColor(qgsLineRed[0],50,50)) + self.relRubBand.setWidth(qgsLineWidth[0]+4) + + self.relVerMarker=QgsVertexMarker(plugin.canvas) + self.relVerMarker.setIconType(2) + self.relVerMarker.setIconSize(13) + self.relVerMarker.setColor(QColor(qgsLineRed[0],50,50)) + self.relVerMarker.setPenWidth(qgsLineWidth[0]) + + self.__activeEditButton=self.dummyButton + self.__tagsLoaded=False + self.__relTagsLoaded=False + self.feature=None + self.featureType=None + self.featRels=[] + self.featRelTags=[] + self.featRelMembers=[] + + self.tagsEditIndex=-1 + + # clear all widget items + self.clear() + + self.__connectWidgetSignals() + + self.removeButton.setEnabled(False) + self.createRelationButton.setCheckable(False) + + self.relTagsTreeWidget.setSelectionMode(QAbstractItemView.NoSelection) + self.tagsTableWidget.setSelectionMode(QAbstractItemView.ExtendedSelection) + self.tagsTableWidget.setSelectionBehavior(QAbstractItemView.SelectRows) + + # set current tab to "Properties" + self.propRelBox.setCurrentIndex(0) + + self.plugin.canvas.setFocus(Qt.OtherFocusReason) + + # init coordinate transform + self.projectionChanged() + + renderer=self.plugin.canvas.mapRenderer() + self.connect(renderer, SIGNAL("hasCrsTransformEnabled(bool)"), self.projectionChanged) + self.connect(renderer, SIGNAL("destinationSrsChanged()"), self.projectionChanged) + + + def setContentEnabled(self,flag): + + self.featInfoBox.setEnabled(flag) + self.propRelBox.setEnabled(flag) + self.identifyButton.setEnabled(flag) + self.moveButton.setEnabled(flag) + self.createPointButton.setEnabled(flag) + self.createLineButton.setEnabled(flag) + self.createPolygonButton.setEnabled(flag) + self.createRelationButton.setEnabled(flag) + + if flag: + if self.plugin.undoredo.undoCounter>0: + self.undoButton.setEnabled(True) + if self.plugin.undoredo.redoCounter>0: + self.redoButton.setEnabled(True) + else: + self.undoButton.setEnabled(False) + self.redoButton.setEnabled(False) + + self.urDetailsButton.setEnabled(flag) + + + def projectionChanged(self): + """Function is connected to signals from QgsMapRenderer. + It updates coordinate transforms. + """ + + renderer = self.plugin.canvas.mapRenderer() + if renderer.hasCrsTransformEnabled(): + self.coordXform = QgsCoordinateTransform(renderer.destinationSrs(), QgsCoordinateReferenceSystem(4326)) + else: + self.coordXform = None + + + def canvasToOsmCoords(self, point): + """Performs conversion from canvas to map coordinates. + + @param point canvas coordinates to convert + """ + point = self.plugin.canvas.getCoordinateTransform().toMapCoordinates( point ) + + # optional conversion from map to layer coordinates + if self.coordXform is not None: + point = self.coordXform.transform( point ) + + return point + + + def __connectWidgetSignals(self): + """Function connects all neccessary signals to appropriate slots. + """ + + # signals emitted on clicking with tag and member tables + QObject.connect(self.identifyButton, SIGNAL("clicked()"), self.__startIdentifyingFeature) + QObject.connect(self.moveButton, SIGNAL("clicked()"), self.__startMovingFeature) + QObject.connect(self.createPointButton, SIGNAL("clicked()"), self.__startPointCreation) + QObject.connect(self.createLineButton, SIGNAL("clicked()"), self.__startLineCreation) + QObject.connect(self.createPolygonButton, SIGNAL("clicked()"), self.__startPolygonCreation) + QObject.connect(self.createRelationButton, SIGNAL("clicked()"), self.__createRelation) + QObject.connect(self.removeButton, SIGNAL("clicked()"), self.removeFeature) + QObject.connect(self.relListWidget, SIGNAL("currentRowChanged(int)"), self.loadRelationStuff) + QObject.connect(self.relMembersList, SIGNAL("currentRowChanged(int)"), self.__showRelMemberOnMap) + QObject.connect(self.addRelationButton, SIGNAL("clicked()"), self.createRelationWithMember) + QObject.connect(self.editRelationButton, SIGNAL("clicked()"), self.editSelectedRelation) + QObject.connect(self.removeRelationButton, SIGNAL("clicked()"), self.removeSelectedRelation) + QObject.connect(self.deleteTagsButton, SIGNAL("clicked()"), self.removeSelectedTags) + QObject.connect(self.tagsTableWidget, SIGNAL("cellChanged(int,int)"), self.__onTagsCellChanged) + QObject.connect(self.tagsTableWidget, SIGNAL("itemDoubleClicked(QTableWidgetItem*)"), self.__onTagsItemDoubleClicked) + QObject.connect(self.undoButton, SIGNAL("clicked()"), self.__undo) + QObject.connect(self.redoButton, SIGNAL("clicked()"), self.__redo) + QObject.connect(self.urDetailsButton, SIGNAL("clicked()"), self.__urDetailsChecked) + + + def databaseChanged(self,dbKey): + """This function is called when current OSM database of plugin changes. + The DockWidget performs neccessary actions and tells current map tool about the change. + + @param dbKey key (name) of new current database + """ + + if self.__dlgAddRel and dbKey: + + self.__dlgAddRel.close() + self.__dlgAddRel=None + self.setContentEnabled(True) + self.plugin.undoredo.setContentEnabled(True) + + QMessageBox.information(self, self.tr("OSM Plugin") + ,self.tr("The 'Create OSM Relation' dialog was closed automatically because current OSM database was changed.")) + + # clear the whole OSM Feature dockwidget as well as all related rubberbands and vertex markers + self.clear() + + # if some mapTool is currently set tell it about database changing + if self.__mapTool: + self.__mapTool.databaseChanged(dbKey) + self.__activeEditButton=self.dummyButton + + # and if new database is None, disable the whole dockwidget + if not dbKey: + self.setContentEnabled(False) + if self.__mapTool: + self.plugin.canvas.unsetMapTool(self.__mapTool) + self.__mapTool=None + self.plugin.canvas.setCursor(QCursor(Qt.ArrowCursor)) + return + + self.setContentEnabled(True) + + + def clear(self): + """Function clears all widget items. + It resets rubberbands, vertexmarkers, re-initializes DockWidget inner structures. + """ + + # clear common feature infos + self.typeIdLabel.setText("") + self.userLabel.setText("") + self.createdLabel.setText("") + + # clear table with information about feature's tags + self.tagsTableWidget.clear() + self.tagsTableWidget.setEnabled(False) + self.tagsTableWidget.setRowCount(0) + self.tagsTableWidget.setColumnCount(0) + self.tagsEditIndex=-1 + + # clear table with info about feature's relations + self.relListWidget.clear() + self.relTagsTreeWidget.clear() + self.relMembersList.clear() + self.relTagsTreeWidget.setColumnCount(0) + + self.relListWidget.setEnabled(False) + self.relTagsTreeWidget.setEnabled(False) + self.relMembersList.setEnabled(False) + + # disable widget buttons + self.deleteTagsButton.setEnabled(False) + self.editRelationButton.setEnabled(False) + self.removeRelationButton.setEnabled(False) + self.addRelationButton.setEnabled(False) + self.removeButton.setEnabled(False) + + # remove previous rubber bands + self.rubBand.reset(False) + self.rubBandPol.reset(True) + self.verMarker.setCenter(QgsPoint(-1000,-1000)) + self.verMarker.hide() + self.__removeMemberMarkers() + self.verMarkers=[] + + self.relRubBand.reset(False) + self.relRubBandPol.reset(True) + self.relVerMarker.setCenter(QgsPoint(-1000,-1000)) + self.relVerMarker.hide() + + # clear member variables + self.__tagsLoaded=False + self.__relTagsLoaded=False + self.feature=None + self.featureType=None + self.featRels=[] + self.featRelTags=[] + self.featRelMembers=[] + + + def __createRelation(self): + """Function calls relation creating process. + Creation is started by displaying appropriate dialog. + """ + + # clear dockwidget + self.clear() + self.plugin.iface.mainWindow().statusBar().showMessage("") + + self.plugin.canvas.unsetMapTool(self.__mapTool) + del self.__mapTool + self.__mapTool=None + + self.plugin.canvas.setCursor(QCursor(Qt.ArrowCursor)) + self.__activeEditButton=self.dummyButton + + self.setContentEnabled(False) + self.plugin.undoredo.setContentEnabled(False) + self.plugin.toolBar.setEnabled(False) + + # DlgAddRelation parameters: plugin, newRelationFirstMember, relationToEdit + self.__dlgAddRel=DlgAddRelation(self.plugin, None, None) + self.__dlgAddRel.setWindowModality(Qt.WindowModal) + self.__dlgAddRel.exec_() + self.__dlgAddRel=None + + self.setContentEnabled(True) + self.plugin.undoredo.setContentEnabled(True) + self.plugin.toolBar.setEnabled(True) + + + def createRelationWithMember(self): + """Function calls relation creating process. Creation is started by displaying appropriate dialog. + Function pre-fills dialog with information on currently loaded feature. + """ + + self.__dlgAddRel=DlgAddRelation(self.plugin, QString(self.featureType+" %1").arg(self.feature.id()), None) + + # clear dockwidget + self.clear() + self.plugin.iface.mainWindow().statusBar().showMessage("") + + self.plugin.canvas.unsetMapTool(self.__mapTool) + del self.__mapTool + self.__mapTool=None + + self.plugin.canvas.setCursor(QCursor(Qt.ArrowCursor)) + self.__activeEditButton=self.dummyButton + + self.setContentEnabled(False) + self.plugin.undoredo.setContentEnabled(False) + self.plugin.toolBar.setEnabled(False) + + self.__dlgAddRel.setWindowModality(Qt.WindowModal) + self.__dlgAddRel.exec_() + self.__dlgAddRel=None + + self.setContentEnabled(True) + self.plugin.undoredo.setContentEnabled(True) + self.plugin.toolBar.setEnabled(True) + + + def editSelectedRelation(self): + """Function calls editing of a relation. Editing is started by displaying appropriate dialog. + Relation identifier is not passed to this function. Function has to find it out from current row + of appropriate list widget. + """ + + # show modal dialog "Edit relation" + if not self.feature: + QMessageBox.information(self, self.tr("OSM Feature Dock Widget"), self.tr("Choose OSM feature first.")) + return + + item=self.relListWidget.item(self.relListWidget.currentRow()) + if not item: + QMessageBox.information(self, self.tr("OSM Feature Dock Widget"), self.tr("Choose relation for editing first.")) + return + + relId=self.featRels[self.relListWidget.currentRow()] + + self.setContentEnabled(False) + self.plugin.undoredo.setContentEnabled(False) + self.plugin.toolBar.setEnabled(False) + + self.__dlgAddRel=DlgAddRelation(self.plugin, None, relId) + self.__dlgAddRel.setWindowModality(Qt.WindowModal) + self.__dlgAddRel.exec_() + self.__dlgAddRel=None + + self.setContentEnabled(True) + self.plugin.undoredo.setContentEnabled(True) + self.plugin.toolBar.setEnabled(True) + + + def __onTagsCellChanged(self,row,column): + """Function is called after cellChanged(int,int) signal is emitted on table of all features' relations. + It means that user is changed key or value of some existing tag. + + @param row index of row in table of tags + @param column index of column in table of tags + """ + + if not self.__tagsLoaded: + # this signal was emitted during table initialization, + # but we are interested in user actions only + return + + if rowNone: + row=self.tagsEditIndex + if row<>-1: + value=self.tagsTableWidget.cellWidget(row,1).currentText() + self.tagsTableWidget.item(row,1).setText(value) + self.tagsTableWidget.removeCellWidget(row,1) + + key=self.tagsTableWidget.item(item.row(),0).text() + tagValues=self.determineSuitableTagValues(self.featureType,key) + + if len(tagValues)>0: + valCombo=QComboBox() + valCombo.setEditable(True) + valCombo.addItems(tagValues) + currentComboText=self.tagsTableWidget.item(item.row(),1).text() + ix=valCombo.findText(currentComboText) + if ix==-1: + valCombo.setEditText(currentComboText) + else: + valCombo.setCurrentIndex(ix) + + self.tagsTableWidget.setCellWidget(item.row(),1,valCombo) + self.tagsEditIndex=item.row() + QObject.connect(valCombo, SIGNAL("currentIndexChanged(const QString &)"), self.__onTagValueSelectionChanged) + + + def __onTagValueSelectionChanged(self,value): + """TODO: Function is called after currentIndexChanged(...) signal is emitted on combobox of table item. + This combobox is related to table of relation tags (column Value). + + @param value new current value in combobox + """ + + row=self.tagsEditIndex + self.tagsTableWidget.item(row,1).setText(value) + self.tagsTableWidget.removeCellWidget(row,1) + self.tagsEditIndex=-1 + + + def determineSuitableTagValues(self,featType,tagKey): + """TODO: Function is used to find typical tag values for given relation type and given key. + With help of this function plugin gives advice to user on relation creation. + + @param relType name of relation type + @param tagKey key of tag + @return list of typical tag values to given relation type + """ + + vals = [] + if featType in ('Point','Line','Polygon','Relation'): + + if tagKey=="highway": + vals = ["trunk","motorway","primary","secondary","tertiary","residential"] + + elif tagKey=="boundary": + vals = ["administrative","national_park","political","civil"] + elif tagKey=="land_area": + vals = ["administrative"] + elif tagKey=="admin_level": + vals = ["1","2","3","4","5","6","7","8","9","10","11"] + elif tagKey=="restriction": + vals = ["no_right_turn","no_left_turn","no_u_turn","no_straight_on","only_right_turn","only_left_turn","only_straight_on"] + elif tagKey=="except": + vals = ["psv","bicycle","hgv","motorcar"] + elif tagKey=="route": + vals = ["road","bicycle","foot","hiking","bus","pilgrimage","detour","railway","tram","mtb","roller_skate","running","horse"] + elif tagKey=="network": + vals = ["ncn","rcn","lcn","uk_ldp","lwn","rwn","nwn","e-road"] + elif tagKey=="state": + vals = ["proposed","alternate","temporary","connection"] + + return vals + + + def __startIdentifyingFeature(self): + """Function prepares feature identification. + The appropriate map tool (IdentifyMapTool) is set to map canvas. + """ + + if self.__activeEditButton==self.identifyButton: + return + + # clear dockwidget + self.clear() + + self.plugin.iface.mainWindow().statusBar().showMessage("") + + self.__mapTool=IdentifyMapTool(self.plugin.canvas, self, self.plugin.dbm) + self.plugin.canvas.unsetMapTool(self.plugin.canvas.mapTool()) + self.plugin.canvas.setMapTool(self.__mapTool) + self.plugin.canvas.setCursor(QCursor(Qt.ArrowCursor)) + self.__activeEditButton=self.identifyButton + self.plugin.canvas.setFocus(Qt.OtherFocusReason) + + + def __startMovingFeature(self): + """Function prepares feature moving. + The appropriate map tool (MoveMapTool) is set to map canvas. + """ + + if self.__activeEditButton==self.moveButton: + return + + # clear dockwidget + self.clear() + self.plugin.iface.mainWindow().statusBar().showMessage("Snapping ON. Hold Ctrl to disable it.") + + self.__mapTool=MoveMapTool(self.plugin) + self.plugin.canvas.setMapTool(self.__mapTool) + self.plugin.canvas.setCursor(QCursor(Qt.CrossCursor)) + self.__activeEditButton=self.moveButton + self.plugin.canvas.setFocus(Qt.OtherFocusReason) + + + def __startPointCreation(self): + """Function prepares point creating operation. + The appropriate map tool (CreatePointMapTool) is set to map canvas. + """ + + if self.__activeEditButton==self.createPointButton: + return + + self.plugin.iface.mainWindow().statusBar().showMessage("Snapping ON. Hold Ctrl to disable it.") + + self.__mapTool=CreatePointMapTool(self.plugin) + self.plugin.canvas.setMapTool(self.__mapTool) + self.plugin.canvas.setCursor(QCursor(Qt.ArrowCursor)) + self.__activeEditButton=self.createPointButton + self.plugin.canvas.setFocus(Qt.OtherFocusReason) + + + def __startLineCreation(self): + """Function prepares line creating operation. + The appropriate map tool (CreateLineMapTool) is set to map canvas. + """ + + if self.__activeEditButton==self.createLineButton: + return + + self.plugin.iface.mainWindow().statusBar().showMessage("Snapping ON. Hold Ctrl to disable it.") + + self.__mapTool=CreateLineMapTool(self.plugin) + self.plugin.canvas.setMapTool(self.__mapTool) + self.plugin.canvas.setCursor(QCursor(Qt.ArrowCursor)) + self.__activeEditButton=self.createLineButton + self.plugin.canvas.setFocus(Qt.OtherFocusReason) + + + def __startPolygonCreation(self): + """Function prepares polygon creating operation. + The appropriate map tool (CreatePolygonMapTool) is set to map canvas. + """ + + if self.__activeEditButton==self.createPolygonButton: + return + + self.plugin.iface.mainWindow().statusBar().showMessage("Snapping ON. Hold Ctrl to disable it.") + + self.__mapTool=CreatePolygonMapTool(self.plugin) + self.plugin.canvas.setMapTool(self.__mapTool) + self.plugin.canvas.setCursor(QCursor(Qt.ArrowCursor)) + self.__activeEditButton=self.createPolygonButton + self.plugin.canvas.setFocus(Qt.OtherFocusReason) + + + def removeFeature(self): + """Function completely removes feature that is currently loaded on "OSM Feature" widget. + """ + + self.removeButton.setDown(False) + self.removeButton.setChecked(False) + self.plugin.iface.mainWindow().statusBar().showMessage("") + + # remove object that was identified by "identify tool" + featId=self.feature.id() + featType=self.featureType + affected=[] + + if featType=='Point': + self.plugin.undoredo.startAction("Remove point.") + affected=self.plugin.dbm.removePoint(featId) + + elif featType=='Line': + self.plugin.undoredo.startAction("Remove line.") + affected=self.plugin.dbm.removeLine(featId,True) # todo: False when Ctrl pressed + + elif featType=='Polygon': + self.plugin.undoredo.startAction("Remove polygon.") + affected=self.plugin.dbm.removePolygon(featId,True) # todo: False when Ctrl pressed + + elif featType=='Relation': + self.plugin.undoredo.startAction("Remove relation.") + self.plugin.dbm.removeRelation(featId) + else: + return # strange situation + + self.plugin.undoredo.stopAction(affected) + self.plugin.dbm.recacheAffectedNow(affected) + + # refresh map canvas so that changes take effect + self.plugin.canvas.refresh() + + # clear dockwidget + self.clear() + + self.plugin.iface.mainWindow().statusBar().showMessage("") + + self.plugin.canvas.unsetMapTool(self.__mapTool) + del self.__mapTool + self.__mapTool=IdentifyMapTool(self.plugin.canvas, self, self.plugin.dbm) + self.plugin.canvas.setMapTool(self.__mapTool) + self.plugin.canvas.setCursor(QCursor(Qt.ArrowCursor)) + self.plugin.canvas.setFocus(Qt.OtherFocusReason) + self.__activeEditButton=self.identifyButton + self.__activeEditButton.setChecked(True) + + + def removeSelectedTags(self): + """Function completely removes all tags that are currently selected in the appropriate + list of the "OSM Feature" widget. More than one tag can be selected using Ctrl and clicking. + """ + + # remove selected tags (rows) + selectedItems=self.tagsTableWidget.selectedItems() + selectedRowsIndexes=[] + lastRowIndex=self.tagsTableWidget.rowCount()-1 + self.tagsTableWidget.setCurrentCell(lastRowIndex,0) + + for i in selectedItems: + if i.column()==0 and not i.row()==lastRowIndex: + selectedRowsIndexes.append(i.row()) + + self.plugin.undoredo.startAction("Remove tags.") + + selectedRowsIndexes.sort() + selectedRowsIndexes.reverse() + + for ix in selectedRowsIndexes: + + key=self.tagsTableWidget.item(ix,0).text() + # updating feature status (from Normal to Updated) + if self.featureType=='Point': + self.plugin.dbm.changePointStatus(self.feature.id(),'N','U') + + elif self.featureType=='Line': + self.plugin.dbm.changeLineStatus(self.feature.id(),'N','U') + + elif self.featureType=='Polygon': + self.plugin.dbm.changePolygonStatus(self.feature.id(),'N','U') + + elif self.featureType=='Relation': + self.plugin.dbm.changeRelationStatus(self.feature.id(),'N','U') + + # perform tag removing + self.plugin.dbm.removeTag(self.feature.id(),self.featureType,key.toUtf8().data()) + + self.tagsTableWidget.removeRow(ix) + + # make this action permanent + self.plugin.dbm.commit() + affected=[(self.feature.id(),self.featureType)] + self.plugin.undoredo.stopAction(affected) + self.plugin.dbm.recacheAffectedNow(affected) + + # refresh map canvas so that changes take effect + self.plugin.canvas.refresh() + + + def removeSelectedRelation(self): + """Function completely removes all relations that are selected in the appropriate list of the "OSM Feature" widget. + More than one relation can be selected using Ctrl and clicking. + """ + + # find out id of relation which is selected + item=self.relListWidget.item(self.relListWidget.currentRow()) + if not item or not self.plugin.dbm.currentKey: + return + + relId=self.featRels[self.relListWidget.currentRow()] + + self.plugin.undoredo.startAction("Remove relation.") + self.plugin.dbm.removeRelation(relId) + self.plugin.dbm.commit() + self.plugin.undoredo.stopAction() + + # reload list of feature's relations + self.__loadFeatureRelations(self.feature.id(),self.featureType) + + + def __showRelMemberOnMap(self,ixRow): + """Functions marks relation member on the map. + + Marking is realized with simple rubberBand (in case of line/polygon) or vertexMarker (for point). + Relation member is given by its index to the list of all members of currently loaded relation. + + @param ixRow index to the list of all relations on mentioned widget + """ + + # move rubberband to selected feature + self.relRubBand.reset(False) + self.relRubBandPol.reset(True) + self.relVerMarker.hide() + + if ixRow==-1 or not self.plugin.dbm.currentKey: + return + mem=self.featRelMembers[ixRow] + self.showFeatureOnMap(mem[0],mem[1]) # id & type + + + def loadRelationStuff(self,ixRow): + """Functions loads information on specified relation into "OSM Feature" widget. + + Relation is given by its index to the list of all relations on mentioned widget. + Relation "stuff" means its basic info, tags and members. + + @param ixRow index to the list of all relations on mentioned widget + """ + + if ixRow==-1 or not self.plugin.dbm.currentKey: + return + relId=self.featRels[ixRow] + + # show all tags connected to selected relation + self.__loadRelationTags(relId) + + # show all relation members on osm dock widget + self.__loadRelationMembers(relId) + + # enable list of members and buttons for relation removing and editing + self.relMembersList.setEnabled(True) + self.editRelationButton.setEnabled(True) + self.removeRelationButton.setEnabled(True) + + + def __loadRelationMembers(self,relId): + """Functions loads the list of members of specified relation. + Relation is given by its identifier. + + Loading is realized into the appropriate QListWidget of "OSM Feature" dockable widget. + Resulting list contains info on member's identifier, type and its role in specified relation. + + @param relId identifier of relation + """ + + self.relMembersList.clear() + # ask database manager for all relation members + self.featRelMembers=self.plugin.dbm.getRelationMembers(relId) + + # printing members + for i in range(0,len(self.featRelMembers)): + memId=self.featRelMembers[i][0] + memType=self.featRelMembers[i][1] + memRole=self.featRelMembers[i][2] + + listRow=QString("(%1) - %2").arg(memId).arg(memType) + if memRole and memRole<>"": + listRow=listRow.append(QString(", role:%3").arg(memRole)) + + self.relMembersList.addItem(listRow) + + + def __loadRelationTags(self,relId): + """Functions loads the list of tags of specified relation. + Relation is given by its identifier. + + Loading is realized into the appropriate QTreeWidget of "OSM Feature" dockable widget. + Resulting tags table has two columns "Key","Value" for easy representing of tag pairs. + + @param relId identifier of relation + """ + + self.relTagsTreeWidget.clear() + self.__relTagsLoaded=False + # ask database manager for all relation tags + self.featRelTags=self.plugin.dbm.getFeatureTags(relId,"Relation") + + self.relTagsTreeWidget.setColumnCount(2) + self.relTagsTreeWidget.setHeaderLabels(["Key","Value"]) + + for i in range(0,len(self.featRelTags)): + self.relTagsTreeWidget.addTopLevelItem(QTreeWidgetItem([self.featRelTags[i][0],self.featRelTags[i][1]])) + + self.__relTagsLoaded=True + + + def __loadFeatureInformation(self,featId,featType): + """Functions shows up the basic information on feature. + Feature is given by its identifier and its type. + + Info is loaded to appropriate place of the "OSM Feature" widget. + Basic info consists (mainly) of feature's identifier, type, owner and timestamp. + + @param featId identifier of feature to load + @param featType type of feature to load - one of 'Point','Line','Polygon' + """ + + # asking DatabaseManager for missing information + featUser=self.plugin.dbm.getFeatureOwner(featId,featType) + featCreated=self.plugin.dbm.getFeatureCreated(featId,featType) # returned as string + # timestamp example: "2008-02-18T15:34:14Z" + + self.typeIdLabel.setText("") + self.userLabel.setText("") + self.createdLabel.setText("") + + # put non-tags feature information on dock widget + if featType: + self.typeIdLabel.setText(QString("%1 %2").arg(featType).arg(str(featId))) + if featUser: + self.userLabel.setText(QString("%1").arg(featUser)) + if featCreated: + # format timestamp + DT_format_osm=Qt.ISODate + # "yyyy-MM-ddThh:mm:ssZ" + DT_format_plugin="yy/MM/dd - h:mm" + self.createdLabel.setText(QDateTime.fromString(featCreated,DT_format_osm).toString(DT_format_plugin)) + + + def __loadFeatureTags(self,featId,featType): + """Functions loads the list of tags of specified feature. + Feature is given by its identifier and its type. + + Loading is realized into the appropriate QTableWidget of "OSM Feature" dockable widget. + Resulting tags table has two columns "Key","Value" for easy representing of tag pairs. + + @param featId identifier of feature to load + @param featType type of feature to load - one of 'Point','Line','Polygon' + """ + + # clear table with information about feature's tags + self.tagsTableWidget.clear() + + # fill tableWidget with tags of selected feature + tableData=self.plugin.dbm.getFeatureTags(featId,featType) + rowCount=len(tableData) + self.__tagsLoaded=False + + self.tagsTableWidget.setRowCount(rowCount+1) + self.tagsTableWidget.setColumnCount(2) + self.tagsTableWidget.setHorizontalHeaderItem(0,QTableWidgetItem("Key")) + self.tagsTableWidget.setHorizontalHeaderItem(1,QTableWidgetItem("Value")) + + for i in range(0,rowCount): + self.tagsTableWidget.setItem(i,0,QTableWidgetItem(tableData[i][0])) + self.tagsTableWidget.setItem(i,1,QTableWidgetItem(tableData[i][1])) + self.tagsTableWidget.item(i,0).setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled) + self.tagsTableWidget.item(i,1).setFlags(Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) + + self.tagsTableWidget.setItem(rowCount,0,QTableWidgetItem(self.newTagLabel)) + self.tagsTableWidget.setItem(rowCount,1,QTableWidgetItem("")) + self.tagsTableWidget.item(rowCount,0).setFlags(Qt.ItemIsEnabled | Qt.ItemIsEditable) + self.tagsTableWidget.item(rowCount,1).setFlags(Qt.ItemIsEnabled) + self.__tagsLoaded=True + + # enable tags table for editing + self.tagsTableWidget.setEnabled(True) + self.deleteTagsButton.setEnabled(True) + + + def __loadFeatureRelations(self,featId,featType): + """Functions loads the list of relations of specified feature. + Feature is given by its identifier and its type. + + Loading is realized into the appropriate QListWidget of "OSM Feature" dockable widget. + If no relation exists for specified feature, listWidget is filled with the only row with text: "". + + @param featId identifier of feature to load + @param featType type of feature to load - one of 'Point','Line','Polygon' + """ + + self.relTagsTreeWidget.setColumnCount(0) + self.relMembersList.setEnabled(False) + + # disable widget buttons + self.editRelationButton.setEnabled(False) + self.removeRelationButton.setEnabled(False) + + # clear all tables connected to relations + self.relListWidget.clear() + self.relTagsTreeWidget.clear() + self.relMembersList.clear() + + # load relations for selected feature + self.featRels=self.plugin.dbm.getFeatureRelations(featId,featType) + + for i in range(0,len(self.featRels)): + self.relListWidget.addItem(self.__getRelationInfo(self.featRels[i])) + + if len(self.featRels)==0: + self.relListWidget.addItem("") + self.addRelationButton.setEnabled(True) + return + + # enable relation tables and button for relation addition + self.addRelationButton.setEnabled(True) + self.relListWidget.setEnabled(True) + self.relTagsTreeWidget.setEnabled(True) + + + def reloadFeatureRelations(self): + """Functions reloads the list of relations for currently loaded feature. + + Loading is realized into the appropriate QListWidget of "OSM Feature" dockable widget. + If no relation exists for specified feature, listWidget is filled with the only row with text: "". + """ + + self.relTagsTreeWidget.setColumnCount(0) + self.relMembersList.setEnabled(False) + + # disable widget buttons + self.editRelationButton.setEnabled(False) + self.removeRelationButton.setEnabled(False) + + # clear all tables connected to relations + self.relListWidget.clear() + self.relTagsTreeWidget.clear() + self.relMembersList.clear() + + # load relations for selected feature + self.featRels=self.plugin.dbm.getFeatureRelations(self.feature.id(),self.featureType) + + for i in range(0,len(self.featRels)): + self.relListWidget.addItem(self.__getRelationInfo(self.featRels[i])) + + if len(self.featRels)==0: + self.relListWidget.addItem("") + self.addRelationButton.setEnabled(True) + return + + # enable relation tables and button for relation addition + self.addRelationButton.setEnabled(True) + self.relListWidget.setEnabled(True) + self.relTagsTreeWidget.setEnabled(True) + + + def putMarkersOnMembers(self,feat,featType): + """Function adds additional vertexMarkers to the map. + + Additional vertexMarker are used to provide better marking of line/polygon. + In that case line/polygon geometry is marked with rubberband first, and second one vertexMarker is put + on each its vertex. Such extended marking of line/polygon is used (for example) when calling loadFeature() method with + "markingMode" parameter set to 2. + + @param featId identifier of feature to load + @param featType type of feature to load - one of 'Point','Line','Polygon' + """ + + if featType=='Point': + return + + pline=None + if featType=='Line': + pline=feat.geometry().asPolyline() + + elif featType=='Polygon': + pline=feat.geometry().asPolygon()[0] + + # get qgis settings of line width and color for rubberband + settings=QSettings() + qgsLineWidth=settings.value("/qgis/digitizing/line_width",QVariant(10)).toInt() + qgsLineRed=settings.value("/qgis/digitizing/line_color_red",QVariant(255)).toInt() + qgsLineGreen=settings.value("/qgis/digitizing/line_color_green",QVariant(0)).toInt() + qgsLineBlue=settings.value("/qgis/digitizing/line_color_blue",QVariant(0)).toInt() + + for i in range(0,len(pline)): + verMarker=QgsVertexMarker(self.plugin.canvas) + verMarker.setIconType(3) + verMarker.setIconSize(6) + verMarker.setColor(QColor(qgsLineRed[0],qgsLineGreen[0],qgsLineBlue[0])) + verMarker.setPenWidth(qgsLineWidth[0]) + verMarker.setCenter(pline[i]) + verMarker.show() + self.verMarkers.append(verMarker) + + + def __removeMemberMarkers(self): + """Function removes additional vertexMarkers from the map. + + Additional vertexMarker are used to provide better marking of line/polygon. + In that case line/polygon geometry is marked with rubberband first, and second one vertexMarker is put + on each its vertex. Such extended marking of line/polygon is used (for example) when calling loadFeature() method with + "markingMode" parameter set to 2. + """ + + for verMarker in self.verMarkers: + self.plugin.canvas.scene().removeItem(verMarker) + del verMarker + self.verMarkers=[] + + + def showFeatureOnMap(self,featId,featType): + """Function just shows up specified feature on the map canvas. + + Showing feature up is realized by putting rubberBand (vertexMarker) on its whole geometry. + This rubberBand (vM) is the same as when using loadFeature() method with "markingMode" parameter set to 1. + + Feature is given by its identifier and its type. Feature data are not loaded into "OSM Feature" widget. + Rubberbands (vertexMarkers) of other features are not removed from the map canvas before this action. + + @param featId identifier of feature to load + @param featType type of feature to load - one of 'Point','Line','Polygon' + """ + + # we have to know feature's geometry to be able to display it on map + featGeom=self.plugin.dbm.getFeatureGeometry(featId,featType) + if not featGeom: + return # nothing to show :-/ + + if featType=='Polygon': + self.relRubBandPol.setToGeometry(featGeom,self.plugin.canvas.currentLayer()) + elif featType=='Point': + self.relVerMarker.setCenter(featGeom.asPoint()) + self.relVerMarker.show() + elif featType=='Line': + self.relRubBand.setToGeometry(featGeom,self.plugin.canvas.currentLayer()) + + + def loadFeature(self,feat,featType,markingMode=1): + """Function loads information on specified feature into "OSM Feature" widget elements. + It shows up features identifier, type, osm user, timestamp, all its tags and related OSM relations. + + According to the value of parameter "markingMode" is marks feature on the map canvas. If "markingMode" equals to zero + feature is not marked on the map. If it equals to 1, simple rubberband (or vertexMarker) is put on the feature (this is + default behavior). If "markingMode" equals to 2, extended rubberband is shown on the map, especially for non-point features. + + @param feat QgsFeature object of feature to load + @param featType type of feature to load - one of 'Point','Line','Polygon' + @param markingMode not compulsory; defines quality of feature's rubberband on map + """ + + if not feat or not featType: + self.clear() + return + + # remember which feature is loaded + self.featureType=featType + self.feature=feat + + # move rubberband to selected feature + self.rubBandPol.reset(True) + self.rubBand.reset(False) + self.verMarker.hide() + self.__removeMemberMarkers() + + if markingMode>0: + if self.featureType=='Polygon': + self.rubBandPol.setToGeometry(feat.geometry(),self.plugin.canvas.currentLayer()) + if markingMode>1: + self.putMarkersOnMembers(feat,featType) + elif self.featureType=='Point': + self.verMarker.setCenter(feat.geometry().asPoint()) + self.verMarker.show() + elif self.featureType=='Line': + self.rubBand.setToGeometry(feat.geometry(),self.plugin.canvas.currentLayer()) + if markingMode>1: + self.putMarkersOnMembers(feat,featType) + + # show common feature information (id,feature originator,created,feature type,...) + self.__loadFeatureInformation(feat.id(),featType) + + # show all tags connected to feature onto osm widget dialog + self.__loadFeatureTags(feat.id(),featType) + + # show all relations connected to feature onto osm widget dialog + self.__loadFeatureRelations(feat.id(),featType) + + # feature has been loaded; enable "remove feature" button + self.removeButton.setEnabled(True) + + + def __getRelationInfo(self,relId): + """Function returns brief info on specified relation. Information consists of relation identifier, + its type and other important relation properties and it is returned simply in QString() object. + + Information are good to be shown in some list (of relations). + + @param relId identifier of relation + @return brief info on relation concatenated in string + """ + + relInfo=relType=relRoute=relBoundary=relRef=relRestr="" + tags=self.plugin.dbm.getFeatureTags(relId,"Relation") + + for i in range(0,len(tags)): + key=tags[i][0] + value=tags[i][1] + + if key=="type": + relType=value # type: route / boundary, ... + elif key=="route": + relRoute=value # route: road / bicycle / foot / hiking / bus / railway / tram, ... + elif key=="boundary": + relBoundary=value # boundary: administrative, ... + elif key=="ref": + relRef=value + elif key=="restriction": + relRestr=value + + if relType=="route": + relInfo = QString("(%1) "+relRoute+" "+relType+" "+relRef).arg(relId) + + elif relType=="boundary": + relInfo = QString("(%1) "+relBoundary+" "+relType).arg(relId) + + elif relType=="restriction": + relInfo = QString("(%1) "+relRestr+" "+relType).arg(relId) + + elif relType=="": + relInfo = QString("(%1) relation of unknown type").arg(relId) + + else: + relInfo = QString("(%1) "+relType).arg(relId) + + return relInfo + + + def __undo(self): + """Function performs exactly one undo operation. + """ + + self.plugin.undoredo.undo() + + + def __redo(self): + """Function performs exactly one redo operation. + """ + + self.plugin.undoredo.redo() + + + def __urDetailsChecked(self): + """Function is called after clicking on urDetailsButton checkbox. + It shows or hides OSM Edit History widget. + """ + + if self.urDetailsButton.isChecked(): + self.plugin.undoredo.show() + else: + self.plugin.undoredo.hide() + + diff --git a/python/plugins/osm/DockWidget_ui.py b/python/plugins/osm/DockWidget_ui.py new file mode 100644 index 00000000000..15f6939660e --- /dev/null +++ b/python/plugins/osm/DockWidget_ui.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'ui_files/DockWidget.ui' +# +# Created: Wed Jul 29 12:14:33 2009 +# by: PyQt4 UI code generator 4.4.4 +# +# WARNING! All changes made in this file will be lost! + +from PyQt4 import QtCore, QtGui + +class Ui_OsmDockWidget(object): + def setupUi(self, OsmDockWidget): + OsmDockWidget.setObjectName("OsmDockWidget") + OsmDockWidget.setWindowModality(QtCore.Qt.NonModal) + OsmDockWidget.setEnabled(True) + OsmDockWidget.resize(265, 776) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Fixed, QtGui.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(OsmDockWidget.sizePolicy().hasHeightForWidth()) + OsmDockWidget.setSizePolicy(sizePolicy) + OsmDockWidget.setMinimumSize(QtCore.QSize(261, 357)) + OsmDockWidget.setMaximumSize(QtCore.QSize(524287, 524287)) + self.dockWidgetContents = QtGui.QWidget() + self.dockWidgetContents.setObjectName("dockWidgetContents") + self.vboxlayout = QtGui.QVBoxLayout(self.dockWidgetContents) + self.vboxlayout.setObjectName("vboxlayout") + self.hboxlayout = QtGui.QHBoxLayout() + self.hboxlayout.setSpacing(1) + self.hboxlayout.setContentsMargins(-1, -1, 0, -1) + self.hboxlayout.setObjectName("hboxlayout") + self.dummyButton = QtGui.QToolButton(self.dockWidgetContents) + self.dummyButton.setMinimumSize(QtCore.QSize(1, 25)) + self.dummyButton.setMaximumSize(QtCore.QSize(1, 27)) + self.dummyButton.setFocusPolicy(QtCore.Qt.NoFocus) + self.dummyButton.setCheckable(True) + self.dummyButton.setObjectName("dummyButton") + self.hboxlayout.addWidget(self.dummyButton) + self.identifyButton = QtGui.QToolButton(self.dockWidgetContents) + self.identifyButton.setMinimumSize(QtCore.QSize(26, 25)) + self.identifyButton.setMaximumSize(QtCore.QSize(25, 26)) + self.identifyButton.setFocusPolicy(QtCore.Qt.NoFocus) + self.identifyButton.setCheckable(True) + self.identifyButton.setObjectName("identifyButton") + self.hboxlayout.addWidget(self.identifyButton) + self.moveButton = QtGui.QToolButton(self.dockWidgetContents) + self.moveButton.setMinimumSize(QtCore.QSize(26, 25)) + self.moveButton.setMaximumSize(QtCore.QSize(25, 26)) + self.moveButton.setFocusPolicy(QtCore.Qt.NoFocus) + self.moveButton.setCheckable(True) + self.moveButton.setObjectName("moveButton") + self.hboxlayout.addWidget(self.moveButton) + self.createPointButton = QtGui.QToolButton(self.dockWidgetContents) + self.createPointButton.setMinimumSize(QtCore.QSize(26, 25)) + self.createPointButton.setMaximumSize(QtCore.QSize(25, 26)) + self.createPointButton.setFocusPolicy(QtCore.Qt.NoFocus) + self.createPointButton.setCheckable(True) + self.createPointButton.setObjectName("createPointButton") + self.hboxlayout.addWidget(self.createPointButton) + self.createLineButton = QtGui.QToolButton(self.dockWidgetContents) + self.createLineButton.setMinimumSize(QtCore.QSize(26, 25)) + self.createLineButton.setFocusPolicy(QtCore.Qt.NoFocus) + self.createLineButton.setCheckable(True) + self.createLineButton.setObjectName("createLineButton") + self.hboxlayout.addWidget(self.createLineButton) + self.createPolygonButton = QtGui.QToolButton(self.dockWidgetContents) + self.createPolygonButton.setMinimumSize(QtCore.QSize(26, 25)) + self.createPolygonButton.setMaximumSize(QtCore.QSize(25, 26)) + self.createPolygonButton.setFocusPolicy(QtCore.Qt.NoFocus) + self.createPolygonButton.setCheckable(True) + self.createPolygonButton.setObjectName("createPolygonButton") + self.hboxlayout.addWidget(self.createPolygonButton) + self.createRelationButton = QtGui.QToolButton(self.dockWidgetContents) + self.createRelationButton.setMinimumSize(QtCore.QSize(26, 25)) + self.createRelationButton.setMaximumSize(QtCore.QSize(25, 26)) + self.createRelationButton.setFocusPolicy(QtCore.Qt.NoFocus) + self.createRelationButton.setCheckable(True) + self.createRelationButton.setObjectName("createRelationButton") + self.hboxlayout.addWidget(self.createRelationButton) + self.undoButton = QtGui.QToolButton(self.dockWidgetContents) + self.undoButton.setMaximumSize(QtCore.QSize(25, 26)) + self.undoButton.setObjectName("undoButton") + self.hboxlayout.addWidget(self.undoButton) + self.redoButton = QtGui.QToolButton(self.dockWidgetContents) + self.redoButton.setMaximumSize(QtCore.QSize(25, 26)) + self.redoButton.setObjectName("redoButton") + self.hboxlayout.addWidget(self.redoButton) + self.urDetailsButton = QtGui.QToolButton(self.dockWidgetContents) + self.urDetailsButton.setMaximumSize(QtCore.QSize(25, 26)) + self.urDetailsButton.setCheckable(True) + self.urDetailsButton.setObjectName("urDetailsButton") + self.hboxlayout.addWidget(self.urDetailsButton) + self.vboxlayout.addLayout(self.hboxlayout) + self.featInfoBox = QtGui.QGroupBox(self.dockWidgetContents) + self.featInfoBox.setEnabled(True) + self.featInfoBox.setMinimumSize(QtCore.QSize(0, 95)) + self.featInfoBox.setMaximumSize(QtCore.QSize(16777215, 95)) + self.featInfoBox.setObjectName("featInfoBox") + self.vboxlayout1 = QtGui.QVBoxLayout(self.featInfoBox) + self.vboxlayout1.setSpacing(0) + self.vboxlayout1.setContentsMargins(18, 4, 3, 6) + self.vboxlayout1.setObjectName("vboxlayout1") + self.gridlayout = QtGui.QGridLayout() + self.gridlayout.setSpacing(0) + self.gridlayout.setObjectName("gridlayout") + self.label = QtGui.QLabel(self.featInfoBox) + self.label.setObjectName("label") + self.gridlayout.addWidget(self.label, 0, 0, 1, 1) + self.label_4 = QtGui.QLabel(self.featInfoBox) + self.label_4.setObjectName("label_4") + self.gridlayout.addWidget(self.label_4, 1, 0, 1, 1) + self.label_5 = QtGui.QLabel(self.featInfoBox) + self.label_5.setObjectName("label_5") + self.gridlayout.addWidget(self.label_5, 2, 0, 1, 1) + self.typeIdLabel = QtGui.QLabel(self.featInfoBox) + self.typeIdLabel.setObjectName("typeIdLabel") + self.gridlayout.addWidget(self.typeIdLabel, 0, 1, 1, 1) + self.createdLabel = QtGui.QLabel(self.featInfoBox) + self.createdLabel.setObjectName("createdLabel") + self.gridlayout.addWidget(self.createdLabel, 1, 1, 1, 1) + self.userLabel = QtGui.QLabel(self.featInfoBox) + self.userLabel.setObjectName("userLabel") + self.gridlayout.addWidget(self.userLabel, 2, 1, 1, 1) + self.removeButton = QtGui.QToolButton(self.featInfoBox) + self.removeButton.setMaximumSize(QtCore.QSize(25, 25)) + self.removeButton.setObjectName("removeButton") + self.gridlayout.addWidget(self.removeButton, 2, 2, 1, 1) + self.vboxlayout1.addLayout(self.gridlayout) + self.vboxlayout.addWidget(self.featInfoBox) + self.propRelBox = QtGui.QTabWidget(self.dockWidgetContents) + self.propRelBox.setMinimumSize(QtCore.QSize(0, 175)) + self.propRelBox.setMaximumSize(QtCore.QSize(16777215, 16777215)) + self.propRelBox.setTabPosition(QtGui.QTabWidget.North) + self.propRelBox.setTabShape(QtGui.QTabWidget.Rounded) + self.propRelBox.setObjectName("propRelBox") + self.Properties = QtGui.QWidget() + self.Properties.setObjectName("Properties") + self.vboxlayout2 = QtGui.QVBoxLayout(self.Properties) + self.vboxlayout2.setObjectName("vboxlayout2") + self.tagsTableWidget = QtGui.QTableWidget(self.Properties) + sizePolicy = QtGui.QSizePolicy(QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Expanding) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.tagsTableWidget.sizePolicy().hasHeightForWidth()) + self.tagsTableWidget.setSizePolicy(sizePolicy) + self.tagsTableWidget.setMinimumSize(QtCore.QSize(205, 100)) + self.tagsTableWidget.setMaximumSize(QtCore.QSize(16777215, 16777215)) + self.tagsTableWidget.setFrameShape(QtGui.QFrame.Box) + self.tagsTableWidget.setObjectName("tagsTableWidget") + self.tagsTableWidget.setColumnCount(0) + self.tagsTableWidget.setRowCount(0) + self.vboxlayout2.addWidget(self.tagsTableWidget) + self.hboxlayout1 = QtGui.QHBoxLayout() + self.hboxlayout1.setObjectName("hboxlayout1") + spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.hboxlayout1.addItem(spacerItem) + self.deleteTagsButton = QtGui.QToolButton(self.Properties) + self.deleteTagsButton.setObjectName("deleteTagsButton") + self.hboxlayout1.addWidget(self.deleteTagsButton) + self.vboxlayout2.addLayout(self.hboxlayout1) + self.propRelBox.addTab(self.Properties, "") + self.Relations = QtGui.QWidget() + self.Relations.setObjectName("Relations") + self.vboxlayout3 = QtGui.QVBoxLayout(self.Relations) + self.vboxlayout3.setObjectName("vboxlayout3") + self.hboxlayout2 = QtGui.QHBoxLayout() + self.hboxlayout2.setObjectName("hboxlayout2") + self.relListWidget = QtGui.QListWidget(self.Relations) + self.relListWidget.setEnabled(False) + self.relListWidget.setMinimumSize(QtCore.QSize(0, 60)) + self.relListWidget.setMaximumSize(QtCore.QSize(16777215, 104)) + self.relListWidget.setFrameShape(QtGui.QFrame.Box) + self.relListWidget.setObjectName("relListWidget") + self.hboxlayout2.addWidget(self.relListWidget) + self.vboxlayout4 = QtGui.QVBoxLayout() + self.vboxlayout4.setObjectName("vboxlayout4") + self.addRelationButton = QtGui.QPushButton(self.Relations) + self.addRelationButton.setEnabled(False) + self.addRelationButton.setMinimumSize(QtCore.QSize(26, 25)) + self.addRelationButton.setMaximumSize(QtCore.QSize(26, 25)) + self.addRelationButton.setObjectName("addRelationButton") + self.vboxlayout4.addWidget(self.addRelationButton) + self.editRelationButton = QtGui.QPushButton(self.Relations) + self.editRelationButton.setEnabled(False) + self.editRelationButton.setMinimumSize(QtCore.QSize(26, 25)) + self.editRelationButton.setMaximumSize(QtCore.QSize(26, 25)) + self.editRelationButton.setObjectName("editRelationButton") + self.vboxlayout4.addWidget(self.editRelationButton) + self.removeRelationButton = QtGui.QPushButton(self.Relations) + self.removeRelationButton.setEnabled(False) + self.removeRelationButton.setMinimumSize(QtCore.QSize(26, 25)) + self.removeRelationButton.setMaximumSize(QtCore.QSize(26, 25)) + self.removeRelationButton.setObjectName("removeRelationButton") + self.vboxlayout4.addWidget(self.removeRelationButton) + spacerItem1 = QtGui.QSpacerItem(26, 0, QtGui.QSizePolicy.Minimum, QtGui.QSizePolicy.Minimum) + self.vboxlayout4.addItem(spacerItem1) + self.hboxlayout2.addLayout(self.vboxlayout4) + self.vboxlayout3.addLayout(self.hboxlayout2) + self.label_2 = QtGui.QLabel(self.Relations) + self.label_2.setObjectName("label_2") + self.vboxlayout3.addWidget(self.label_2) + self.relTagsTreeWidget = QtGui.QTreeWidget(self.Relations) + self.relTagsTreeWidget.setEnabled(False) + self.relTagsTreeWidget.setMinimumSize(QtCore.QSize(0, 115)) + self.relTagsTreeWidget.setMaximumSize(QtCore.QSize(16777215, 16777215)) + self.relTagsTreeWidget.setProperty("cursor", QtCore.QVariant(QtCore.Qt.ForbiddenCursor)) + self.relTagsTreeWidget.setContextMenuPolicy(QtCore.Qt.NoContextMenu) + self.relTagsTreeWidget.setFrameShape(QtGui.QFrame.Box) + self.relTagsTreeWidget.setFrameShadow(QtGui.QFrame.Sunken) + self.relTagsTreeWidget.setIndentation(0) + self.relTagsTreeWidget.setRootIsDecorated(False) + self.relTagsTreeWidget.setItemsExpandable(False) + self.relTagsTreeWidget.setColumnCount(1) + self.relTagsTreeWidget.setObjectName("relTagsTreeWidget") + self.vboxlayout3.addWidget(self.relTagsTreeWidget) + self.label_3 = QtGui.QLabel(self.Relations) + self.label_3.setObjectName("label_3") + self.vboxlayout3.addWidget(self.label_3) + self.relMembersList = QtGui.QListWidget(self.Relations) + self.relMembersList.setEnabled(False) + self.relMembersList.setMinimumSize(QtCore.QSize(0, 115)) + self.relMembersList.setMaximumSize(QtCore.QSize(16777215, 16777215)) + self.relMembersList.setFrameShape(QtGui.QFrame.Box) + self.relMembersList.setObjectName("relMembersList") + self.vboxlayout3.addWidget(self.relMembersList) + self.propRelBox.addTab(self.Relations, "") + self.vboxlayout.addWidget(self.propRelBox) + OsmDockWidget.setWidget(self.dockWidgetContents) + + self.retranslateUi(OsmDockWidget) + self.propRelBox.setCurrentIndex(0) + QtCore.QMetaObject.connectSlotsByName(OsmDockWidget) + + def retranslateUi(self, OsmDockWidget): + OsmDockWidget.setWindowTitle(QtGui.QApplication.translate("OsmDockWidget", "OSM Feature", None, QtGui.QApplication.UnicodeUTF8)) + self.dummyButton.setText(QtGui.QApplication.translate("OsmDockWidget", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.identifyButton.setToolTip(QtGui.QApplication.translate("OsmDockWidget", "Identify object", None, QtGui.QApplication.UnicodeUTF8)) + self.identifyButton.setStatusTip(QtGui.QApplication.translate("OsmDockWidget", "Identify object", None, QtGui.QApplication.UnicodeUTF8)) + self.identifyButton.setWhatsThis(QtGui.QApplication.translate("OsmDockWidget", "Identify object", None, QtGui.QApplication.UnicodeUTF8)) + self.identifyButton.setText(QtGui.QApplication.translate("OsmDockWidget", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.moveButton.setToolTip(QtGui.QApplication.translate("OsmDockWidget", "Move object", None, QtGui.QApplication.UnicodeUTF8)) + self.moveButton.setText(QtGui.QApplication.translate("OsmDockWidget", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.createPointButton.setToolTip(QtGui.QApplication.translate("OsmDockWidget", "Create point", None, QtGui.QApplication.UnicodeUTF8)) + self.createPointButton.setText(QtGui.QApplication.translate("OsmDockWidget", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.createLineButton.setToolTip(QtGui.QApplication.translate("OsmDockWidget", "Create line", None, QtGui.QApplication.UnicodeUTF8)) + self.createLineButton.setText(QtGui.QApplication.translate("OsmDockWidget", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.createPolygonButton.setToolTip(QtGui.QApplication.translate("OsmDockWidget", "Create polygon", None, QtGui.QApplication.UnicodeUTF8)) + self.createPolygonButton.setText(QtGui.QApplication.translate("OsmDockWidget", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.createRelationButton.setToolTip(QtGui.QApplication.translate("OsmDockWidget", "Create relation", None, QtGui.QApplication.UnicodeUTF8)) + self.createRelationButton.setText(QtGui.QApplication.translate("OsmDockWidget", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.undoButton.setToolTip(QtGui.QApplication.translate("OsmDockWidget", "Undo", None, QtGui.QApplication.UnicodeUTF8)) + self.undoButton.setStatusTip(QtGui.QApplication.translate("OsmDockWidget", "Undo", None, QtGui.QApplication.UnicodeUTF8)) + self.undoButton.setWhatsThis(QtGui.QApplication.translate("OsmDockWidget", "Undo", None, QtGui.QApplication.UnicodeUTF8)) + self.undoButton.setText(QtGui.QApplication.translate("OsmDockWidget", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.redoButton.setToolTip(QtGui.QApplication.translate("OsmDockWidget", "Redo", None, QtGui.QApplication.UnicodeUTF8)) + self.redoButton.setStatusTip(QtGui.QApplication.translate("OsmDockWidget", "Redo", None, QtGui.QApplication.UnicodeUTF8)) + self.redoButton.setWhatsThis(QtGui.QApplication.translate("OsmDockWidget", "Redo", None, QtGui.QApplication.UnicodeUTF8)) + self.redoButton.setText(QtGui.QApplication.translate("OsmDockWidget", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.urDetailsButton.setToolTip(QtGui.QApplication.translate("OsmDockWidget", "Show/Hide OSM Edit History", None, QtGui.QApplication.UnicodeUTF8)) + self.urDetailsButton.setStatusTip(QtGui.QApplication.translate("OsmDockWidget", "Show/Hide OSM Edit History", None, QtGui.QApplication.UnicodeUTF8)) + self.urDetailsButton.setWhatsThis(QtGui.QApplication.translate("OsmDockWidget", "Show/Hide OSM Edit History", None, QtGui.QApplication.UnicodeUTF8)) + self.urDetailsButton.setText(QtGui.QApplication.translate("OsmDockWidget", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.featInfoBox.setTitle(QtGui.QApplication.translate("OsmDockWidget", "Feature:", None, QtGui.QApplication.UnicodeUTF8)) + self.label.setText(QtGui.QApplication.translate("OsmDockWidget", "TYPE, ID:", None, QtGui.QApplication.UnicodeUTF8)) + self.label_4.setText(QtGui.QApplication.translate("OsmDockWidget", "CREATED:", None, QtGui.QApplication.UnicodeUTF8)) + self.label_5.setText(QtGui.QApplication.translate("OsmDockWidget", "USER:", None, QtGui.QApplication.UnicodeUTF8)) + self.typeIdLabel.setText(QtGui.QApplication.translate("OsmDockWidget", "unknown", None, QtGui.QApplication.UnicodeUTF8)) + self.createdLabel.setText(QtGui.QApplication.translate("OsmDockWidget", "unknown", None, QtGui.QApplication.UnicodeUTF8)) + self.userLabel.setText(QtGui.QApplication.translate("OsmDockWidget", "unknown", None, QtGui.QApplication.UnicodeUTF8)) + self.removeButton.setText(QtGui.QApplication.translate("OsmDockWidget", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.deleteTagsButton.setText(QtGui.QApplication.translate("OsmDockWidget", "...", None, QtGui.QApplication.UnicodeUTF8)) + self.propRelBox.setTabText(self.propRelBox.indexOf(self.Properties), QtGui.QApplication.translate("OsmDockWidget", "Properties", None, QtGui.QApplication.UnicodeUTF8)) + self.addRelationButton.setToolTip(QtGui.QApplication.translate("OsmDockWidget", "Add new relation", None, QtGui.QApplication.UnicodeUTF8)) + self.addRelationButton.setStatusTip(QtGui.QApplication.translate("OsmDockWidget", "Add new relation", None, QtGui.QApplication.UnicodeUTF8)) + self.addRelationButton.setWhatsThis(QtGui.QApplication.translate("OsmDockWidget", "Add new relation", None, QtGui.QApplication.UnicodeUTF8)) + self.addRelationButton.setText(QtGui.QApplication.translate("OsmDockWidget", "A", None, QtGui.QApplication.UnicodeUTF8)) + self.editRelationButton.setToolTip(QtGui.QApplication.translate("OsmDockWidget", "Edit selected relation", None, QtGui.QApplication.UnicodeUTF8)) + self.editRelationButton.setStatusTip(QtGui.QApplication.translate("OsmDockWidget", "Edit selected relation", None, QtGui.QApplication.UnicodeUTF8)) + self.editRelationButton.setWhatsThis(QtGui.QApplication.translate("OsmDockWidget", "Edit selected relation", None, QtGui.QApplication.UnicodeUTF8)) + self.editRelationButton.setText(QtGui.QApplication.translate("OsmDockWidget", "E", None, QtGui.QApplication.UnicodeUTF8)) + self.removeRelationButton.setToolTip(QtGui.QApplication.translate("OsmDockWidget", "Remove selected relation", None, QtGui.QApplication.UnicodeUTF8)) + self.removeRelationButton.setStatusTip(QtGui.QApplication.translate("OsmDockWidget", "Remove selected relation", None, QtGui.QApplication.UnicodeUTF8)) + self.removeRelationButton.setWhatsThis(QtGui.QApplication.translate("OsmDockWidget", "Remove selected relation", None, QtGui.QApplication.UnicodeUTF8)) + self.removeRelationButton.setText(QtGui.QApplication.translate("OsmDockWidget", "R", None, QtGui.QApplication.UnicodeUTF8)) + self.label_2.setText(QtGui.QApplication.translate("OsmDockWidget", "Relation tags:", None, QtGui.QApplication.UnicodeUTF8)) + self.relTagsTreeWidget.headerItem().setText(0, QtGui.QApplication.translate("OsmDockWidget", "1", None, QtGui.QApplication.UnicodeUTF8)) + self.label_3.setText(QtGui.QApplication.translate("OsmDockWidget", "Relation members:", None, QtGui.QApplication.UnicodeUTF8)) + self.propRelBox.setTabText(self.propRelBox.indexOf(self.Relations), QtGui.QApplication.translate("OsmDockWidget", "Relations", None, QtGui.QApplication.UnicodeUTF8)) + diff --git a/python/plugins/osm/Makefile b/python/plugins/osm/Makefile new file mode 100644 index 00000000000..f43b70b40d5 --- /dev/null +++ b/python/plugins/osm/Makefile @@ -0,0 +1,37 @@ + +GEN_FILES = DlgLoadOSM_ui.py DlgSaveOSM_ui.py DlgDownloadOSM_ui.py DlgUploadOSM_ui.py DockWidget_ui.py DlgAddRelation_ui.py DockUndoRedo_ui.py DlgImport_ui.py resources.py + +all: $(GEN_FILES) + +DlgLoadOSM_ui.py: ui_files/DlgLoadOSM.ui + pyuic4 -o DlgLoadOSM_ui.py ui_files/DlgLoadOSM.ui + +DlgSaveOSM_ui.py: ui_files/DlgSaveOSM.ui + pyuic4 -o DlgSaveOSM_ui.py ui_files/DlgSaveOSM.ui + +DlgDownloadOSM_ui.py: ui_files/DlgDownloadOSM.ui + pyuic4 -o DlgDownloadOSM_ui.py ui_files/DlgDownloadOSM.ui + +DlgUploadOSM_ui.py: ui_files/DlgUploadOSM.ui + pyuic4 -o DlgUploadOSM_ui.py ui_files/DlgUploadOSM.ui + +DockWidget_ui.py: ui_files/DockWidget.ui + pyuic4 -o DockWidget_ui.py ui_files/DockWidget.ui + +DlgAddRelation_ui.py: ui_files/DlgAddRelation.ui + pyuic4 -o DlgAddRelation_ui.py ui_files/DlgAddRelation.ui + +DockUndoRedo_ui.py: ui_files/DockUndoRedo.ui + pyuic4 -o DockUndoRedo_ui.py ui_files/DockUndoRedo.ui + +DlgImport_ui.py: ui_files/DlgImport.ui + pyuic4 -o DlgImport_ui.py ui_files/DlgImport.ui + +resources.py: resources.qrc + pyrcc4 -o resources.py resources.qrc + +clean: + rm -f $(GEN_FILES) *.pyc + +package: + cd .. && rm -f osm_plugin.zip && zip -r osm_plugin.zip osm_plugin -x \*.svn-base -x \*.pyc diff --git a/python/plugins/osm/__init__.py b/python/plugins/osm/__init__.py new file mode 100644 index 00000000000..a44bcab5e67 --- /dev/null +++ b/python/plugins/osm/__init__.py @@ -0,0 +1,65 @@ +"""@package __init__ +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +This is the main module of OpenStreetMap plugin for Quantum GIS. +It initializes the plugin, making it known to QGIS. + +OSM Plugin is viewer and editor for OpenStreetMap data. +""" + + +def name(): + """Function returns name of this plugin. + + @return name of this plugin ~ OpenStreetMap plugin + """ + + return "OpenStreetMap plugin" + + +def description(): + """Function returns brief description of this plugin. + + @return brief description of this plugin. + """ + + return "Viewer and editor for OpenStreetMap data" + + +def version(): + """Function returns version of this plugin. + + @return version of this plugin + """ + + return "Version 0.4" + + +def qgisMinimumVersion(): + """Function returns information on what minimum version + of Quantum GIS this plugin works with. + + @return minimum supported version of QGIS + """ + + return "1.0.0" + + +def classFactory(iface): + """Function returns OSM Plugin instance. + + @return instance of OSM Plugin + """ + + # load TestPlugin class from file testplug.py + from osm_plugin import OSMPlugin + # return object of our plugin with reference to QGIS interface as the only argument + return OSMPlugin(iface) + diff --git a/python/plugins/osm/images/osmIconsMaster.svg b/python/plugins/osm/images/osmIconsMaster.svg new file mode 100644 index 00000000000..d84ba448c40 --- /dev/null +++ b/python/plugins/osm/images/osmIconsMaster.svg @@ -0,0 +1,4016 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 001001000111 + + + 001001000111 + + + 001001000111 + + + 001001000111 + + + 001001000111 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/python/plugins/osm/images/osm_createLine.png b/python/plugins/osm/images/osm_createLine.png new file mode 100644 index 00000000000..301928fd668 Binary files /dev/null and b/python/plugins/osm/images/osm_createLine.png differ diff --git a/python/plugins/osm/images/osm_createPoint.png b/python/plugins/osm/images/osm_createPoint.png new file mode 100644 index 00000000000..18dedd6ae6c Binary files /dev/null and b/python/plugins/osm/images/osm_createPoint.png differ diff --git a/python/plugins/osm/images/osm_createPolygon.png b/python/plugins/osm/images/osm_createPolygon.png new file mode 100644 index 00000000000..cd725694672 Binary files /dev/null and b/python/plugins/osm/images/osm_createPolygon.png differ diff --git a/python/plugins/osm/images/osm_createRelation.png b/python/plugins/osm/images/osm_createRelation.png new file mode 100644 index 00000000000..6bfdbceb626 Binary files /dev/null and b/python/plugins/osm/images/osm_createRelation.png differ diff --git a/python/plugins/osm/images/osm_download.png b/python/plugins/osm/images/osm_download.png new file mode 100755 index 00000000000..37bd080ef40 Binary files /dev/null and b/python/plugins/osm/images/osm_download.png differ diff --git a/python/plugins/osm/images/osm_featureManager.png b/python/plugins/osm/images/osm_featureManager.png new file mode 100644 index 00000000000..225b435a23d Binary files /dev/null and b/python/plugins/osm/images/osm_featureManager.png differ diff --git a/python/plugins/osm/images/osm_identify.png b/python/plugins/osm/images/osm_identify.png new file mode 100644 index 00000000000..621258829a3 Binary files /dev/null and b/python/plugins/osm/images/osm_identify.png differ diff --git a/python/plugins/osm/images/osm_import.png b/python/plugins/osm/images/osm_import.png new file mode 100644 index 00000000000..279dddce646 Binary files /dev/null and b/python/plugins/osm/images/osm_import.png differ diff --git a/python/plugins/osm/images/osm_load.png b/python/plugins/osm/images/osm_load.png new file mode 100755 index 00000000000..a05793d56b6 Binary files /dev/null and b/python/plugins/osm/images/osm_load.png differ diff --git a/python/plugins/osm/images/osm_move.png b/python/plugins/osm/images/osm_move.png new file mode 100644 index 00000000000..07b39fdccaf Binary files /dev/null and b/python/plugins/osm/images/osm_move.png differ diff --git a/python/plugins/osm/images/osm_questionMark.png b/python/plugins/osm/images/osm_questionMark.png new file mode 100644 index 00000000000..cdf52fbebf0 Binary files /dev/null and b/python/plugins/osm/images/osm_questionMark.png differ diff --git a/python/plugins/osm/images/osm_redo.png b/python/plugins/osm/images/osm_redo.png new file mode 100644 index 00000000000..06e69d8bafc Binary files /dev/null and b/python/plugins/osm/images/osm_redo.png differ diff --git a/python/plugins/osm/images/osm_remove.png b/python/plugins/osm/images/osm_remove.png new file mode 100644 index 00000000000..7f10703c792 Binary files /dev/null and b/python/plugins/osm/images/osm_remove.png differ diff --git a/python/plugins/osm/images/osm_save.png b/python/plugins/osm/images/osm_save.png new file mode 100755 index 00000000000..65cb25ed97f Binary files /dev/null and b/python/plugins/osm/images/osm_save.png differ diff --git a/python/plugins/osm/images/osm_star.png b/python/plugins/osm/images/osm_star.png new file mode 100644 index 00000000000..58f8ba8e6c6 Binary files /dev/null and b/python/plugins/osm/images/osm_star.png differ diff --git a/python/plugins/osm/images/osm_undo.png b/python/plugins/osm/images/osm_undo.png new file mode 100644 index 00000000000..3e8f12fe24c Binary files /dev/null and b/python/plugins/osm/images/osm_undo.png differ diff --git a/python/plugins/osm/images/osm_upload.png b/python/plugins/osm/images/osm_upload.png new file mode 100755 index 00000000000..0a7d631f70d Binary files /dev/null and b/python/plugins/osm/images/osm_upload.png differ diff --git a/python/plugins/osm/map_tools/CreateLineMapTool.py b/python/plugins/osm/map_tools/CreateLineMapTool.py new file mode 100644 index 00000000000..fbb7b2b5709 --- /dev/null +++ b/python/plugins/osm/map_tools/CreateLineMapTool.py @@ -0,0 +1,337 @@ +"""@package CreateLineMapTool +This module holds all structures and methods required to perform +"create line" operation on current OSM data. + +Snapping to existing points is supported when creating new line. +Process generates some rubberBands and vertexMarkers so that user can watch +the whole operation on the map in a nice way. + +There is also an interaction with plugin's "OSM Feature" dialog. +Points to which snapping is performed are loaded to it dynamically. +""" + + +from PyQt4.QtCore import * +from PyQt4.QtGui import * +from qgis.core import * +from qgis.gui import * + + +class CreateLineMapTool(QgsMapTool): + """This class holds all structures and methods required to perform + "create line" operation on current OSM data. + + Snapping to existing points is supported when creating new line. + Process generates some rubberBands and vertexMarkers so that user can watch + the whole operation on the map in a nice way. + + There is also an interaction with plugin's "OSM Feature" dialog. + Points to which snapping is performed are loaded to it dynamically. + """ + + + def __init__(self, plugin): + """The constructor. + + Initializes the map tool, creates necessary snappers. + + @param plugin pointer to OSM Plugin instance + """ + + QgsMapTool.__init__(self,plugin.canvas) + + self.canvas=plugin.canvas + self.dockWidget=plugin.dockWidget + self.dbm=plugin.dbm + self.ur=plugin.undoredo + + # initialization + self.snappingEnabled=True + self.lastPointIsStable=True + self.linePoints=[] + self.snappedPoint=None + self.snapFeat=None + self.snapFeatType=None + + # creating rubberband which will be on new line + self.lineRubBand=self.createLineRubberband() + + # creating rubberband for snapped objects + self.snapVerMarker=self.createSnapVertexMarker() + + # creating snapper to this map tool + self.snapper=self.createSnapper(self.canvas.mapRenderer()) + + + def databaseChanged(self,dbKey): + """This function is called automatically when current OSM database has changed. + + Function does re-initialization of maptool and create new snappers again (if necessary). + + @param dbKey key of database with new current OSM data + """ + + # re-initialization + self.snappingEnabled=True + self.snapFeat=None + self.snapFeatType=None + self.snapVerMarker.setCenter(QgsPoint(-1000,-1000)) + del self.snapVerMarker + self.snapVerMarker=self.createSnapVertexMarker() + self.lineRubBand.reset(False) + + self.lastPointIsStable=True + self.linePoints=[] + self.snappedPoint=None + + if dbKey: + del self.snapper + self.snapper=self.createSnapper(self.canvas.mapRenderer()) + + self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("") + + + def createLineRubberband(self): + """Function creates rubberband that is used for marking new line on the map. + + @return rubberband that marks new line + """ + + # get qgis settings of line width and color for rubberband + settings=QSettings() + qgsLineWidth=settings.value("/qgis/digitizing/line_width",QVariant(10)).toInt() + qgsLineRed=settings.value("/qgis/digitizing/line_color_red",QVariant(255)).toInt() + qgsLineGreen=settings.value("/qgis/digitizing/line_color_green",QVariant(0)).toInt() + qgsLineBlue=settings.value("/qgis/digitizing/line_color_blue",QVariant(0)).toInt() + + rband=QgsRubberBand(self.canvas,False) + rband.setColor(QColor(qgsLineRed[0],qgsLineGreen[0],qgsLineBlue[0])) + rband.setWidth(qgsLineWidth[0]) + + return rband + + + def createSnapVertexMarker(self): + """Function creates vertexMarker that is used for marking feature + to which snapping was done. + + @return vertex marker - QgsVertexMarker object + """ + + # get qgis settings + settings=QSettings() + qgsLineWidth=settings.value("/qgis/digitizing/line_width",QVariant(10)).toInt() + qgsLineRed=settings.value("/qgis/digitizing/line_color_red",QVariant(255)).toInt() + qgsLineGreen=settings.value("/qgis/digitizing/line_color_green",QVariant(0)).toInt() + qgsLineBlue=settings.value("/qgis/digitizing/line_color_blue",QVariant(0)).toInt() + + verMarker=QgsVertexMarker(self.canvas) + verMarker.setIconType(2) + verMarker.setIconSize(13) + verMarker.setColor(QColor(qgsLineRed[0],qgsLineGreen[0],qgsLineBlue[0])) + verMarker.setPenWidth(qgsLineWidth[0]) + verMarker.setCenter(QgsPoint(-1000,-1000)) + + return verMarker + + + def createSnapper(self,canvasRenderer): + """Function creates snapper that snaps within standard qgis tolerance. + + Snapping of this snapper is done to all segments and vertexes + of all three layers of current OSM database. + + @param canvasRenderer renderer of current map canvas + @return instance of vertex+segment QgsSnapper + """ + + if not self.dbm.currentKey: + # there is no current database -> no layer for snapping + return QgsSnapper(self.canvas.mapRenderer()) + + snapper=QgsSnapper(self.canvas.mapRenderer()) + snapLayers=[] + + # snap to osm layers from current database only + sLayer=QgsSnapper.SnapLayer() + sLayer.mLayer=self.dbm.pointLayers[self.dbm.currentKey] + sLayer.mTolerance=QgsTolerance.vertexSearchRadius(sLayer.mLayer,self.canvas.mapRenderer()) + sLayer.mSnapTo=QgsSnapper.SnapToVertex + snapLayers.append(sLayer) + + sLayer=QgsSnapper.SnapLayer() + sLayer.mLayer=self.dbm.lineLayers[self.dbm.currentKey] + sLayer.mTolerance=QgsTolerance.vertexSearchRadius(sLayer.mLayer,self.canvas.mapRenderer()) + sLayer.mSnapTo=QgsSnapper.SnapToVertex + snapLayers.append(sLayer) + + sLayer=QgsSnapper.SnapLayer() + sLayer.mLayer=self.dbm.polygonLayers[self.dbm.currentKey] + sLayer.mTolerance=QgsTolerance.vertexSearchRadius(sLayer.mLayer,self.canvas.mapRenderer()) + sLayer.mSnapTo=QgsSnapper.SnapToVertex + snapLayers.append(sLayer) + + snapper.setSnapLayers(snapLayers) + return snapper + + + def deactivate(self): + """Functions is called when create line map-tool is being deactivated. + + Function performs standard cleaning; re-initialization etc. + """ + + self.lineRubBand.reset() + self.snapVerMarker.setCenter(QgsPoint(-1000,-1000)) + self.snappingEnabled=True + self.lastPointIsStable=True + self.linePoints=[] + + self.dockWidget.toolButtons.setExclusive(False) + self.dockWidget.createLineButton.setChecked(False) + self.dockWidget.toolButtons.setExclusive(True) + self.dockWidget.activeEditButton=self.dockWidget.dummyButton + + + def keyPressEvent(self, event): + """This function is called after keyPressEvent(QKeyEvent *) signal + is emmited when using this map tool. + + If Control key was pressed, function disables snapping til key is released again. + + @param event event that occured when key pressing + """ + + if (event.key() == Qt.Key_Control): + self.snappingEnabled = False + self.snapVerMarker.setCenter(QgsPoint(-1000,-1000)) + self.snappedPoint=None + self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("Snapping OFF.") + + + def keyReleaseEvent(self, event): + """This function is called after keyReleaseEvent(QKeyEvent *) signal + is emmited when using this map tool. + + If Control key was released, function enables snapping again. + + @param event event that occured when key releasing + """ + + if (event.key() == Qt.Key_Control): + self.snappingEnabled = True + self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("Snapping ON. Hold Ctrl to disable it.") + + + def canvasMoveEvent(self, event): + """This function is called when mouse moving. + + @param event event that occured when mouse moving. + """ + + self.mapPoint=self.dockWidget.canvasToOsmCoords(event.pos()) + + if len(self.linePoints)>0: + if not self.lastPointIsStable: + self.lineRubBand.removeLastPoint() + self.lineRubBand.addPoint(QgsPoint(self.mapPoint.x(),self.mapPoint.y())) + self.lastPointIsStable=False + + if not self.snappingEnabled: + self.snapVerMarker.setCenter(self.mapPoint) + return + + # snapping! first reset old snapping vertexMarker + self.snapVerMarker.setCenter(QgsPoint(-1000,-1000)) + + # try snapping to the closest vertex/segment + (retval,snappingResults)=self.snapper.snapPoint(event.pos(),[]) + + if len(snappingResults)==0: + self.snapVerMarker.setCenter(self.mapPoint) + self.snappedPoint=None + self.snapFeat=None + self.snapFeatType=None + + if self.dockWidget.feature: + self.dockWidget.clear() + return + + # process snapping result (get point, set vertex marker) + self.snappedPoint=QgsPoint(snappingResults[0].snappedVertex) + self.snapVerMarker.setCenter(self.snappedPoint) + + if len(self.linePoints)>0: + self.lineRubBand.removeLastPoint() + self.lineRubBand.addPoint(QgsPoint(self.snappedPoint.x(),self.snappedPoint.y())) + + # start identification + feature=self.dbm.findFeature(self.snappedPoint) + if feature: + (self.snapFeat,self.snapFeatType)=feature + if not self.dockWidget.feature or self.snapFeat.id()<>self.dockWidget.feature.id(): + self.dockWidget.loadFeature(self.snapFeat,self.snapFeatType) + + + def canvasReleaseEvent(self, event): + """This function is called after mouse button releasing when using this map tool. + + If left button is released new vertex of line is created (pre-created). + If right button is released the whole process of line creation is finished. + + @param event event that occured when button releasing + """ + + # we are interested in left/right button clicking only + if event.button() not in (Qt.LeftButton,Qt.RightButton): + return + + if event.button()==Qt.LeftButton: + + # where we are exactly? + actualMapPoint = self.dockWidget.canvasToOsmCoords(event.pos()) + + # what point will be the next line member? + newLinePoint=actualMapPoint + if self.snappedPoint: + newLinePoint=self.snappedPoint + + # add new point into rubberband (and removing last one if neccessary) and into new line members list + if not self.lastPointIsStable: + self.lineRubBand.removeLastPoint() + self.lastPointIsStable=True + + self.lineRubBand.addPoint(newLinePoint) + self.linePoints.append((newLinePoint,self.snapFeat,self.snapFeatType)) + + # right button clicking signalizes the last line member! + elif event.button()==Qt.RightButton: + + # line must have at least 2 member points (else it's point rather than line) + if len(self.linePoints)<2: + + self.lineRubBand.reset() + self.snapVerMarker.setCenter(QgsPoint(-1000,-1000)) + self.lastPointIsStable=True + self.linePoints=[] + return + + self.ur.startAction("Create a line.") + # call function of database manager that will create new line + (line,affected)=self.dbm.createLine(self.linePoints) + self.ur.stopAction(affected) + self.dbm.recacheAffectedNow(affected) + + if line: + self.dockWidget.loadFeature(line,"Line",2) + + # cleaning.. + self.lineRubBand.reset() + self.linePoints=[] + + # after line creation canvas must be refresh so that changes take effect on map + self.canvas.refresh() + + + + diff --git a/python/plugins/osm/map_tools/CreatePointMapTool.py b/python/plugins/osm/map_tools/CreatePointMapTool.py new file mode 100644 index 00000000000..ae2ce7b5e02 --- /dev/null +++ b/python/plugins/osm/map_tools/CreatePointMapTool.py @@ -0,0 +1,265 @@ +"""@package CreatePointMapTool +This module holds all structures and methods required to perform +"create point" operation on current OSM data. + +Snapping to existing segments of lines/polygons is supported when creating new point. + +Process generates vertexMarkers so that user can watch results +of the operation on the map in a nice way. + +There is also an interaction with plugin's "OSM Feature" dialog. +New points are loaded to it dynamically. +""" + + +from PyQt4.QtCore import * +from PyQt4.QtGui import * +from qgis.core import * +from qgis.gui import * + +import sqlite3 +from math import * + + + +class CreatePointMapTool(QgsMapTool): + """This class holds all structures and methods required to perform + "create point" operation on current OSM data. + + Snapping to existing segments of lines/polygons is supported when creating new point. + + Process generates vertexMarkers so that user can watch results + of the operation on the map in a nice way. + + There is also an interaction with plugin's "OSM Feature" dialog. + New points are loaded to it dynamically. + """ + + + def __init__(self,plugin): + """The constructor. + + Initializes the map tool, creates necessary snappers. + + @param plugin pointer to OSM Plugin instance + """ + + QgsMapTool.__init__(self,plugin.canvas) + self.canvas=plugin.canvas + self.dockWidget=plugin.dockWidget + self.dbm=plugin.dbm + self.ur=plugin.undoredo + self.snappingEnabled=True + self.snapFeat=None + self.snapFeatType=None + + # creating vertex marker + self.verMarker=self.createVertexMarker() + + # creating snapper to this map tool + self.snapper=self.createSnapper(self.canvas.mapRenderer()) + + + def databaseChanged(self,dbKey): + """This function is called automatically when current OSM database has changed. + + Function does re-initialization of maptool and create new snapper again (if necessary). + + @param dbKey key of database with new current OSM data + """ + + # re-initialization + self.snappingEnabled=True + self.snapFeat=None + self.snapFeatType=None + self.verMarker.setCenter(QgsPoint(-1000,-1000)) + self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("") + + if self.verMarker: + self.canvas.scene().removeItem(self.verMarker) + del self.verMarker + self.verMarker=None + + if not dbKey: + return + + self.verMarker=self.createVertexMarker() + del self.snapper + self.snapper=self.createSnapper(self.canvas.mapRenderer()) + + + def createVertexMarker(self): + """Function creates vertexMarker that is used for marking new point on map. + + @return vertex marker - QgsVertexMarker object + """ + + # get qgis settings + settings=QSettings() + qgsLineWidth=settings.value("/qgis/digitizing/line_width",QVariant(10)).toInt() + qgsLineRed=settings.value("/qgis/digitizing/line_color_red",QVariant(255)).toInt() + qgsLineGreen=settings.value("/qgis/digitizing/line_color_green",QVariant(0)).toInt() + qgsLineBlue=settings.value("/qgis/digitizing/line_color_blue",QVariant(0)).toInt() + + verMarker=QgsVertexMarker(self.canvas) + verMarker.setIconType(2) + verMarker.setIconSize(13) + verMarker.setColor(QColor(qgsLineRed[0],qgsLineGreen[0],qgsLineBlue[0])) + verMarker.setPenWidth(qgsLineWidth[0]) + verMarker.setCenter(QgsPoint(-1000,-1000)) + + return verMarker + + + def createSnapper(self,canvasRenderer): + """Function creates snapper that snaps within standard qgis tolerance. + + Snapping is done to all segments of both line and polygon layer. + + @param canvasRenderer renderer of current map canvas + @return instance of segment QgsSnapper + """ + + if not self.dbm.currentKey: + # there is no current database -> no layer for snapping + return QgsSnapper(self.canvas.mapRenderer()) + + snapper=QgsSnapper(self.canvas.mapRenderer()) + snapLayers=[] + + # snap to line and polygon layer from current database only + sLayer=QgsSnapper.SnapLayer() + sLayer.mLayer=self.dbm.lineLayers[self.dbm.currentKey] + sLayer.mTolerance=QgsTolerance.vertexSearchRadius(sLayer.mLayer,self.canvas.mapRenderer()) + sLayer.mSnapTo=QgsSnapper.SnapToSegment + snapLayers.append(sLayer) + + sLayer=QgsSnapper.SnapLayer() + sLayer.mLayer=self.dbm.polygonLayers[self.dbm.currentKey] + sLayer.mTolerance=QgsTolerance.vertexSearchRadius(sLayer.mLayer,self.canvas.mapRenderer()) + sLayer.mSnapTo=QgsSnapper.SnapToSegment + snapLayers.append(sLayer) + + snapper.setSnapLayers(snapLayers) + return snapper + + + def deactivate(self): + """Functions is called when create point map-tool is being deactivated. + + Function performs standard cleaning; re-initialization etc. + """ + + self.dockWidget.toolButtons.setExclusive(False) + self.dockWidget.createPointButton.setChecked(False) + self.dockWidget.toolButtons.setExclusive(True) + self.dockWidget.activeEditButton=self.dockWidget.dummyButton + + if self.verMarker: + self.verMarker.setCenter(QgsPoint(-1000,-1000)) + self.canvas.scene().removeItem(self.verMarker) + del self.verMarker + self.verMarker=None + + self.dockWidget.clear() + + + def keyPressEvent(self, event): + """This function is called after keyPressEvent(QKeyEvent *) signal + is emmited when using this map tool. + + If Control key was pressed, function disables snapping til key is released again. + + @param event event that occured when key pressing + """ + + if (event.key()==Qt.Key_Control): + self.snappingEnabled=False + if self.verMarker: + self.verMarker.setCenter(QgsPoint(-1000,-1000)) + self.snappedPoint=None + self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("Snapping OFF.") + + + def keyReleaseEvent(self, event): + """This function is called after keyReleaseEvent(QKeyEvent *) signal + is emmited when using this map tool. + + If Control key was released, function enables snapping again. + + @param event event that occured when key releasing + """ + + if (event.key()==Qt.Key_Control): + self.snappingEnabled=True + self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("Snapping ON. Hold Ctrl to disable it.") + + + def canvasReleaseEvent(self,event): + """This function is called after mouse button releasing when using this map tool. + + If left button is released new point is created. + Right (and other) clicking does nothing. + + @param event event that occured when button releasing + """ + + if event.button()<>Qt.LeftButton: + return # nothing to do + + if not self.snappedPoint: + newPoint = self.dockWidget.canvasToOsmCoords(event.pos()) + else: + newPoint=self.snappedPoint + + self.ur.startAction("Create a point.") + (node,affected)=self.dbm.createPoint(newPoint,self.snapFeat,self.snapFeatType) + self.ur.stopAction(affected) + self.dbm.recacheAffectedNow(affected) + + if node: + self.dockWidget.loadFeature(node,"Point",2) + + self.canvas.refresh() + + + def canvasMoveEvent(self,event): + """This function is called when mouse moving. + + @param event event that occured when mouse moving. + """ + + self.mapPoint = self.dockWidget.canvasToOsmCoords(event.pos()) + + # try snapping to the closest vertex/segment + if not self.snappingEnabled: + self.verMarker.setCenter(self.mapPoint) + return + + # snapping! first reset old snapping vertexMarker + self.verMarker.setCenter(QgsPoint(-1000,-1000)) + + (retval,snappingResults)=self.snapper.snapPoint(event.pos(),[]) + + if len(snappingResults)==0: + self.verMarker.setCenter(self.mapPoint) + self.snappedPoint=None + self.snapFeat=None + self.snapFeatType=None + + if self.dockWidget.feature: + self.dockWidget.clear() + return + + self.snappedPoint=QgsPoint(snappingResults[0].snappedVertex) + self.verMarker.setCenter(self.snappedPoint) + + # start identification + feature=self.dbm.findFeature(self.snappedPoint) + if feature: + (self.snapFeat,self.snapFeatType)=feature + if not self.dockWidget.feature or self.snapFeat.id()<>self.dockWidget.feature.id(): + self.dockWidget.loadFeature(self.snapFeat,self.snapFeatType) + + + diff --git a/python/plugins/osm/map_tools/CreatePolygonMapTool.py b/python/plugins/osm/map_tools/CreatePolygonMapTool.py new file mode 100644 index 00000000000..de1ede18ade --- /dev/null +++ b/python/plugins/osm/map_tools/CreatePolygonMapTool.py @@ -0,0 +1,336 @@ +"""@package CreatePolygonMapTool +This module holds all structures and methods required to perform +"create polygon" operation on current OSM data. + +Snapping to existing points is supported when creating new polygon. +Process generates some rubberBands and vertexMarkers so that user can watch +the whole operation on the map in a nice way. + +There is also an interaction with plugin's "OSM Feature" dialog. +Points to which snapping is performed are loaded to it dynamically. +""" + + +from PyQt4.QtCore import * +from PyQt4.QtGui import * +from qgis.core import * +from qgis.gui import * + + +class CreatePolygonMapTool(QgsMapTool): + """This class holds all structures and methods required to perform + "create polygon" operation on current OSM data. + + Snapping to existing points is supported when creating new polygon. + Process generates some rubberBands and vertexMarkers so that user can watch + the whole operation on the map in a nice way. + + There is also an interaction with plugin's "OSM Feature" dialog. + Points to which snapping is performed are loaded to it dynamically. + """ + + + def __init__(self, plugin): + """The constructor. + + Initializes the map tool, creates necessary snappers. + + @param plugin pointer to OSM Plugin instance + """ + + QgsMapTool.__init__(self,plugin.canvas) + + self.canvas=plugin.canvas + self.dockWidget=plugin.dockWidget + self.dbm=plugin.dbm + self.ur=plugin.undoredo + + # initialization + self.snappingEnabled=True + self.lastPointIsStable=True + self.polygonPoints=[] + self.snappedPoint=None + self.snapFeat=None + self.snapFeatType=None + + # creating rubberband which will be on new polygon + self.polygonRubBand=self.createPolygonRubberband() + + # creating rubberband for snapped objects + self.snapVerMarker=self.createSnapVertexMarker() + + # creating snapper to this map tool + self.snapper=self.createSnapper(self.canvas.mapRenderer()) + + + def databaseChanged(self,dbKey): + """This function is called automatically when current OSM database has changed. + + Function does re-initialization of maptool and create new snappers again (if necessary). + + @param dbKey key of database with new current OSM data + """ + + # re-initialization + self.snappingEnabled=True + self.snapFeat=None + self.snapFeatType=None + self.snapVerMarker.setCenter(QgsPoint(-1000,-1000)) + del self.snapVerMarker + self.snapVerMarker=self.createSnapVertexMarker() + self.polygonRubBand.reset(True) + + self.lastPointIsStable=True + self.polygonPoints=[] + self.snappedPoint=None + + if dbKey: + del self.snapper + self.snapper=self.createSnapper(self.canvas.mapRenderer()) + + self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("") + + + def createPolygonRubberband(self): + """Function creates rubberband that is used for marking new polygon on the map. + + @return rubberband that marks new polygon + """ + + # get qgis settings of line width and color for rubberband + settings = QSettings() + qgsLineWidth = settings.value( "/qgis/digitizing/line_width", QVariant(10) ).toInt() + qgsLineRed = settings.value( "/qgis/digitizing/line_color_red", QVariant(255) ).toInt() + qgsLineGreen = settings.value( "/qgis/digitizing/line_color_green", QVariant(0) ).toInt() + qgsLineBlue = settings.value( "/qgis/digitizing/line_color_blue", QVariant(0) ).toInt() + + rband=QgsRubberBand(self.canvas,True) + rband.setColor( QColor(qgsLineRed[0],qgsLineGreen[0],qgsLineBlue[0]) ) + rband.setWidth( qgsLineWidth[0] ) + + return rband + + + def createSnapVertexMarker(self): + """Function creates vertexMarker that is used for marking feature + to which snapping was done. + + @return vertex marker - QgsVertexMarker object + """ + + # get qgis settings + settings=QSettings() + qgsLineWidth=settings.value("/qgis/digitizing/line_width",QVariant(10)).toInt() + qgsLineRed=settings.value("/qgis/digitizing/line_color_red",QVariant(255)).toInt() + qgsLineGreen=settings.value("/qgis/digitizing/line_color_green",QVariant(0)).toInt() + qgsLineBlue=settings.value("/qgis/digitizing/line_color_blue",QVariant(0)).toInt() + + verMarker=QgsVertexMarker(self.canvas) + verMarker.setIconType(2) + verMarker.setIconSize(13) + verMarker.setColor(QColor(qgsLineRed[0],qgsLineGreen[0],qgsLineBlue[0])) + verMarker.setPenWidth(qgsLineWidth[0]) + verMarker.setCenter(QgsPoint(-1000,-1000)) + + return verMarker + + + def createSnapper(self,canvasRenderer): + """Function creates snapper that snaps within standard qgis tolerance. + + Snapping of this snapper is done to all segments and vertexes + of all three layers of current OSM database. + + @param canvasRenderer renderer of current map canvas + @return instance of vertex+segment QgsSnapper + """ + + if not self.dbm.currentKey: + # there is no current database -> no layer for snapping + return QgsSnapper(self.canvas.mapRenderer()) + + snapper=QgsSnapper(self.canvas.mapRenderer()) + snapLayers=[] + + # snap to osm layers from current database only + sLayer=QgsSnapper.SnapLayer() + sLayer.mLayer=self.dbm.pointLayers[self.dbm.currentKey] + sLayer.mTolerance=QgsTolerance.vertexSearchRadius(sLayer.mLayer,self.canvas.mapRenderer()) + sLayer.mSnapTo=QgsSnapper.SnapToVertex + snapLayers.append(sLayer) + + sLayer=QgsSnapper.SnapLayer() + sLayer.mLayer=self.dbm.lineLayers[self.dbm.currentKey] + sLayer.mTolerance=QgsTolerance.vertexSearchRadius(sLayer.mLayer,self.canvas.mapRenderer()) + sLayer.mSnapTo=QgsSnapper.SnapToVertex + snapLayers.append(sLayer) + + sLayer=QgsSnapper.SnapLayer() + sLayer.mLayer=self.dbm.polygonLayers[self.dbm.currentKey] + sLayer.mTolerance=QgsTolerance.vertexSearchRadius(sLayer.mLayer,self.canvas.mapRenderer()) + sLayer.mSnapTo=QgsSnapper.SnapToVertex + snapLayers.append(sLayer) + + snapper.setSnapLayers(snapLayers) + return snapper + + + def deactivate(self): + """Functions is called when create polygon map-tool is being deactivated. + + Function performs standard cleaning; re-initialization etc. + """ + + self.polygonRubBand.reset(True) + self.snapVerMarker.setCenter(QgsPoint(-1000,-1000)) + self.snappingEnabled=True + self.lastPointIsStable=True + self.polygonPoints=[] + + self.dockWidget.toolButtons.setExclusive(False) + self.dockWidget.createPolygonButton.setChecked(False) + self.dockWidget.toolButtons.setExclusive(True) + self.dockWidget.activeEditButton=self.dockWidget.dummyButton + + + def keyPressEvent(self, event): + """This function is called after keyPressEvent(QKeyEvent *) signal + is emmited when using this map tool. + + If Control key was pressed, function disables snapping til key is released again. + + @param event event that occured when key pressing + """ + + if (event.key() == Qt.Key_Control): + self.snappingEnabled = False + self.snapVerMarker.setCenter(QgsPoint(-1000,-1000)) + self.snappedPoint=None + self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("Snapping OFF.") + + + def keyReleaseEvent(self, event): + """This function is called after keyReleaseEvent(QKeyEvent *) signal + is emmited when using this map tool. + + If Control key was released, function enables snapping again. + + @param event event that occured when key releasing + """ + + if (event.key() == Qt.Key_Control): + self.snappingEnabled = True + self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("Snapping ON - hold Ctrl to disable it.") + + + def canvasMoveEvent(self, event): + """This function is called when mouse moving. + + @param event event that occured when mouse moving. + """ + + self.mapPoint=self.dockWidget.canvasToOsmCoords(event.pos()) + + if len(self.polygonPoints)>0: + if not self.lastPointIsStable: + self.polygonRubBand.removeLastPoint() + self.polygonRubBand.addPoint(QgsPoint(self.mapPoint.x(),self.mapPoint.y())) + self.lastPointIsStable=False + + if not self.snappingEnabled: + self.snapVerMarker.setCenter(self.mapPoint) + return + + # snapping! first reset old snapping vertexMarker + self.snapVerMarker.setCenter(QgsPoint(-1000,-1000)) + + # try snapping to the closest vertex/segment + (retval,snappingResults)=self.snapper.snapPoint(event.pos(),[]) + + if len(snappingResults)==0: + self.snapVerMarker.setCenter(self.mapPoint) + self.snappedPoint=None + self.snapFeat=None + self.snapFeatType=None + + if self.dockWidget.feature: + self.dockWidget.clear() + return + + # process snapping result (get point, set rubberband) + self.snappedPoint=QgsPoint(snappingResults[0].snappedVertex) + self.snapVerMarker.setCenter(self.snappedPoint) + + if len(self.polygonPoints)>0: + self.polygonRubBand.removeLastPoint() + self.polygonRubBand.addPoint(QgsPoint(self.snappedPoint.x(),self.snappedPoint.y())) + + # start identification + feature=self.dbm.findFeature(self.snappedPoint) + if feature: + (self.snapFeat,self.snapFeatType)=feature + if not self.dockWidget.feature or self.snapFeat.id()<>self.dockWidget.feature.id(): + self.dockWidget.loadFeature(self.snapFeat,self.snapFeatType) + + + def canvasReleaseEvent(self, event): + """This function is called after mouse button releasing when using this map tool. + + If left button is released new vertex of polygon is created (pre-created). + If right button is released the whole process of polygon creation is finished. + + @param event event that occured when button releasing + """ + + # we are interested only in left/right button clicking + if event.button() not in (Qt.LeftButton,Qt.RightButton): + return + + if event.button()==Qt.LeftButton: + + # where we are exactly? + actualMapPoint = self.dockWidget.canvasToOsmCoords(event.pos()) + + # what point will be the next polygon member? + newPolygonPoint=actualMapPoint + if self.snappedPoint: + newPolygonPoint=self.snappedPoint + + # add new point into rubberband (and removing last one if neccessary) and into new polygon members list + if not self.lastPointIsStable: + self.polygonRubBand.removeLastPoint() + self.lastPointIsStable=True + + self.polygonRubBand.addPoint(newPolygonPoint) + self.polygonPoints.append((newPolygonPoint,self.snapFeat,self.snapFeatType)) + + # right button clicking signalizes the last line member! + elif event.button()==Qt.RightButton: + + # polygon must have at least three member points (triangle) + if len(self.polygonPoints)<3: + + self.polygonRubBand.reset(True) + self.snapVerMarker.setCenter(QgsPoint(-1000,-1000)) + self.lastPointIsStable=True + self.polygonPoints=[] + return + + self.ur.startAction("Create a polygon.") + # call function of database manager that will create new polygon + (polyg,affected)=self.dbm.createPolygon(self.polygonPoints) + self.ur.stopAction(affected) + self.dbm.recacheAffectedNow(affected) + + if polyg: + self.dockWidget.loadFeature(polyg,'Polygon',2) + + # cleaning.. + self.polygonRubBand.reset(True) + self.polygonPoints=[] + + # after polygon creation canvas must be refresh so that changes take effect on map + self.canvas.refresh() + + + diff --git a/python/plugins/osm/map_tools/IdentifyMapTool.py b/python/plugins/osm/map_tools/IdentifyMapTool.py new file mode 100644 index 00000000000..31ae6b94b37 --- /dev/null +++ b/python/plugins/osm/map_tools/IdentifyMapTool.py @@ -0,0 +1,292 @@ +"""@package IdentifyMapTool +This module holds all structures and methods required to perform +"identify feature" operation on current OSM data. + +When feature is identified its id, type, timestamp, owner, properties/tags, relations are loaded +into OSM Feature widget. Feature is also marked with rubberband (or vertexmarker for points) on map. + +If you want to identify some feature, just left-click on it. + +If OSM Plugin marked wrong feature after that, repeat RIGHT-clicking til the right one is marked. +(Right-clicking gives you one by one each feature that is in the place where left-click was done.) + +If no feature is marked after your left-clicking, you missed the feature :-) Try again. + +If you are not able to hit any feature, be sure that map data you are trying to identify are the current OSM data. + +If they are, maybe there is something wrong in your QGIS settings. Be sure that there aren't too small values +in QGIS Settings -> Digitalization -> Tolerance/Snapping. + +If you've just identified the wrong feature or want to identify a new one, +just left-click to continue the identification process. +""" + + +from PyQt4.QtCore import * +from PyQt4.QtGui import * +from qgis.core import * +from qgis.gui import * + + +class IdentifyMapTool(QgsMapTool): + """This class holds all structures and methods required to perform + "identify feature" operation on current OSM data. + + When feature is identified its id, type, timestamp, owner, properties/tags, relations are loaded + into OSM Feature widget. Feature is also marked with rubberband (or vertexmarker for points) on map. + + If you want to identify some feature, just left-click on it. + + If OSM Plugin marked wrong feature after that, repeat RIGHT-clicking til the right one is marked. + (Right-clicking gives you one by one each feature that is in the place where left-click was done.) + + If no feature is marked after your left-clicking, you missed the feature :-) Try again. + + If you are not able to hit any feature, be sure that map data you are trying to identify are the current OSM data. + + If they are, maybe there is something wrong in your QGIS settings. Be sure that there aren't too small values + in QGIS Settings -> Digitalization -> Tolerance/Snapping. + + If you've just identified the wrong feature or want to identify a new one, + just left-click to continue the identification process. + """ + + + def __init__(self, canvas, dockWidget, dbManager): + """The constructor. + Initializes the map tool. + + @param canvas map canvas + @param dockWidget pointer to the main widget (OSM Feature widget) of OSM Plugin + @param dbManager pointer to instance of DatabaseManager; for communication with sqlite3 database + """ + + QgsMapTool.__init__(self,canvas) + + self.canvas=canvas + self.dockWidget=dockWidget + self.dbm=dbManager + self.moves=0 + self.pause=False + self.doubleclick=False + self.featuresFound=[] + self.ixFeature=0 + + self.dlgSelect=QDialog(self.dockWidget) + self.dlgSelect.setWindowTitle("Feature identification") + butBox=QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel,Qt.Horizontal,self.dlgSelect) + + self.lw=QListWidget(self.dlgSelect) + + layout=QVBoxLayout(self.dlgSelect) + layout.addWidget(self.lw) + layout.addWidget(butBox) + self.dlgSelect.setLayout(layout) + + QObject.connect(butBox,SIGNAL("accepted()"),self.onSelectDlgOK) + QObject.connect(butBox,SIGNAL("rejected()"),self.onSelectDlgCancel) + + self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("") + + + def databaseChanged(self,dbKey): + """This function is called automatically when current OSM database has changed. + + Function does re-initialization of maptool. + + @param dbKey key of database with new current OSM data + """ + + # re-initialization + self.pause=False + self.doubleclick=False + self.featuresFound=[] + self.ixFeature=0 + + self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("") + + + def canvasDoubleClickEvent(self,event): + """This function is called after doubleclick is done on map when using this map tool. + Function is interested into left-doubleclicking only. + + It finds out all features that are currently at the place where doubleclick was done. + Then it shows simple dialog with the list of all these features. User can select the required one + and close dialog. + + Selected feature is then loaded into OSM Feature widget. + + @param event event that occured when double clicking + """ + + if event.button()<>Qt.LeftButton: + return + + self.dockWidget.clear() + + # find out map coordinates from mouse click + mapPoint=self.dockWidget.canvasToOsmCoords(event.pos()) + + # display modal dialog with features selection + self.featuresFound=self.dbm.findAllFeatures(mapPoint) + self.ixFeature=0 + + lwItems=[] + for f in self.featuresFound: + feat=f[0] + featType=f[1] + name=self.dbm.getTagValue(feat.id(),featType,"name") + lwItems.append(QString("[%1] ").arg(feat.id()).append(featType).append(QString(" ")).append(name)) + + self.lw.clear() + self.lw.addItems(lwItems) + + self.pause=False + self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("") + self.doubleclick=True + + # continue only if OK button was clicked + if self.dlgSelect.exec_()==0: + return + + + def canvasReleaseEvent(self,event): + """This function is called after mouse button is released on map when using this map tool. + + It finds out all features that are currently at place where releasing was done. + + OSM Plugin then marks the first of them. User can repeat right-clicking to mark + the next one, the next one, the next one... periodically... + Note that only one feature is marked at a time. + + Each marked feature is also loaded into OSM Feature widget. + + @param event event that occured when button releasing + """ + + if self.doubleclick: + self.doubleclick=False + return + + # we are interested only in left/right button clicking + if event.button() not in (Qt.LeftButton,Qt.RightButton): + return + + # find out map coordinates from mouse click + mapPoint=self.dockWidget.canvasToOsmCoords(event.pos()) + + if event.button()==Qt.LeftButton: + + if self.pause: + self.pause=False + self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("") + self.featuresFound=[] + self.ixFeature=0 + return + + # start identification + self.featuresFound=self.dbm.findAllFeatures(mapPoint) + self.ixFeature=0 + + if len(self.featuresFound)>0: + + self.pause=True + self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("PAUSED. Left-click to continue.") + + (feat,featType)=self.featuresFound[self.ixFeature] + + self.dockWidget.loadFeature(feat,featType,2) + + elif self.dockWidget.feature: + self.dockWidget.clear() + + elif event.button()==Qt.RightButton: + + if len(self.featuresFound)<1: + return + + self.ixFeature=self.ixFeature+1 + if self.ixFeature>=len(self.featuresFound): + self.ixFeature=0 + + (feat,featType)=self.featuresFound[self.ixFeature] + self.dockWidget.loadFeature(feat,featType,2) + + + def canvasMoveEvent(self,event): + """This function is called after mouse moving. + + Feature are marked and loaded dynamically when going over them. + + @param event event that occured when mouse moving. + """ + + if self.pause: + return + + if self.moves<>1: + self.moves=self.moves+1 + return + + self.moves=0 + + # find out map coordinates from mouse click + mapPoint=self.dockWidget.canvasToOsmCoords(event.pos()) + + # start identification + feature=self.dbm.findFeature(mapPoint) + + if feature: + (feat,featType)=feature + + if not self.dockWidget.feature or feat.id()<>self.dockWidget.feature.id(): + self.dockWidget.loadFeature(feat,featType,1) + + elif self.dockWidget.feature: + + self.dockWidget.clear() + + + def onSelectDlgOK(self): + """This function handles clicking on OK button of selection dialog. + """ + + self.dlgSelect.close() + + if not self.lw.currentItem(): + return + + self.ixFeature=self.lw.currentRow() + (feat,featType)=self.featuresFound[self.ixFeature] + + if feat: + self.dockWidget.loadFeature(feat,featType,2) + self.pause=True + + + def onSelectDlgCancel(self): + """This function handles clicking on Cancel button of selection dialog. + """ + + self.dlgSelect.close() + + + def deactivate(self): + """Functions is called when identify-map-tool is being deactivated. + + It performs standard cleaning; + re-initialization etc. + """ + + self.dockWidget.toolButtons.setExclusive(False) + self.dockWidget.identifyButton.setChecked(False) + self.dockWidget.toolButtons.setExclusive(True) + self.dockWidget.activeEditButton=self.dockWidget.dummyButton + self.pause=False + self.doubleclick=False + + self.dockWidget.clear() + self.featuresFound=[] + self.ixFeature=0 + + diff --git a/python/plugins/osm/map_tools/MoveMapTool.py b/python/plugins/osm/map_tools/MoveMapTool.py new file mode 100644 index 00000000000..118345cbdb2 --- /dev/null +++ b/python/plugins/osm/map_tools/MoveMapTool.py @@ -0,0 +1,768 @@ +"""@package MoveMapTool +This module holds all structures and methods required to perform move operation on OSM data. + +Snapping to existing features is supported when moving a feature. +Moving process generates some rubberBands and vertexMarkers so that user can watch +the whole action on the map in a nice way. + +There is also an interaction with plugin's "OSM Feature" dialog. Affected features are dynamically +loaded to it; thanks to that user is not confused about what (s)he is moving. +""" + + +from PyQt4.QtCore import * +from PyQt4.QtGui import * +from qgis.core import * +from qgis.gui import * +from math import * + + +class MoveMapTool(QgsMapTool): + """This class represents map tool for feature moving (see QgsMapTool from Quantum GIS API). + + It enables to move any OSM feature. User is expected to left click, select and move... The second phase (selecting) + is necessary, because there can be more than one feature at the same place. + + At the beginning of action left-click is used to choose the position on the map. + Repeatable right-clicking is then used to select required feature from specified position. + After that mouse moving moves selected feature. + The last action is left-clicking again (that confirms the moving operation) or right-clicking (canceling operation). + + Snapping to existing features is supported when moving a feature. When moving a line/polygon, only three closest + vertexes to the mouse position can be snapped. If snapping is enabled for all vertexes, operation will be very slow + on features with many vertexes. + When moving a point (also vertex of line/polygon) snapping to both vertexes and segments is done. + When moving a line/polygon snapping to vertexes is supported only. + + Moving process generates some rubberBands and vertexMarkers so that user can watch + the whole action on the map in a nice way. + + There is also an interaction with plugin's "OSM Feature" dialog. Affected features are dynamically + loaded to it; thanks to that user is not confused about what (s)he is moving. + + Map tool catches the signal of changing OSM database. It such case not-ended operation is canceled. + """ + + + def __init__(self, plugin): + """The constructor. + Initializes the map tool, creates necessary snappers. + + @param plugin pointer to OSM Plugin instance + """ + + QgsMapTool.__init__(self,plugin.canvas) + self.canvas=plugin.canvas + self.dockWidget=plugin.dockWidget + self.dbm=plugin.dbm + self.ur=plugin.undoredo + + # init info about feature that is being moved! + self.mapPointFrom=None + self.mapPointTo=None + self.movFeatType=None + self.movFeat=None + self.movIsPolygon=False + self.movIndexes=[] + + self.snapDeltas=None + self.snapVertexIx=None + self.snapFeat=None + self.snapFeatType=None + + # init variables that keeps system state + self.snappingEnabled=True + self.doubleclick=False + self.movingMode="INTRO" # -> "SELECTION" -> "MOVING" + self.moves=0 + self.featuresFound=[] + self.ixFeature=0 + + # init objects to display on canvas + self.rubBands=[] # rubBands of neighbour (non-point) feats that are also affected by moving operation + self.isPolygonFlags=[] # isPolygon flags for self.rubBands[] + self.memberIndexes=[] + self.snapRubBand=self.__createSnapRubberband() # creating rubberband for snapped objects + self.verMarker=self.__createVertexMarker() # creating vertex marker for point moving + + # creating Vertex & Segment snapper + creating Vertex (only!) snapper + self.snapperVS=self.__createVSSnapper(self.canvas.mapRenderer()) + self.snapperV=self.__createVSnapper(self.canvas.mapRenderer()) + + # create dialog with feature selection + self.dlgSelect=QDialog(self.dockWidget) + self.dlgSelect.setWindowTitle("Feature identification") + butBox=QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel,Qt.Horizontal,self.dlgSelect) + + self.lw=QListWidget(self.dlgSelect) + + layout=QVBoxLayout(self.dlgSelect) + layout.addWidget(self.lw) + layout.addWidget(butBox) + self.dlgSelect.setLayout(layout) + + # set dialog signals + QObject.connect(butBox,SIGNAL("accepted()"),self.__onSelectDlgOK) + QObject.connect(butBox,SIGNAL("rejected()"),self.__onSelectDlgCancel) + + self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("") + + + def reinit(self): + """Function re-initializes the map tool, prepare necessary rubberbands again. + + After calling this function, move map tool is in the same state as after its creation. + """ + + # reinit info about feature that is being moved! + self.mapPointFrom=None + self.mapPointTo=None + self.movFeatType=None + self.movFeat=None + self.movIsPolygon=False + self.movIndexes=[] + + self.snapDeltas=None + self.snapVertexIx=None + self.snapFeat=None + self.snapFeatType=None + + # reinit variables that keeps system state + self.snappingEnabled=True + self.doubleclick=False + self.movingMode="INTRO" # -> "SELECTION" -> "MOVING" + self.moves=0 + self.featuresFound=[] + self.ixFeature=0 + + # reinit objects to display on canvas + for ix in range(0,len(self.rubBands)): + self.rubBands[ix].reset(self.isPolygonFlags[ix]) + + del self.rubBands + self.rubBands=[] # rubBands of neighbour feats that are also affected by moving operation + + del self.isPolygonFlags + del self.memberIndexes + self.isPolygonFlags=[] # isPolygon flags for self.rubBands[] + self.memberIndexes=[] + + self.snapRubBand.reset() # todo: ??? polygon ??? + del self.snapRubBand + self.snapRubBand=self.__createSnapRubberband() # recreating rubberband for snapped objects + + self.verMarker.setCenter(QgsPoint(-1000,-1000)) + del self.verMarker + self.verMarker=self.__createVertexMarker() # recreating vertex marker for point moving + + self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("") + + + def databaseChanged(self,dbKey): + """This function is called automatically when current OSM database has changed. + + Function calls re-initialization of maptool and create new snappers again. + + @param dbKey key of database with new current OSM data + """ + + # re-initialization + self.reinit() + + # plus creation of new snappers + if dbKey: + del self.snapperVS + self.snapperVS=self.__createVSSnapper(self.canvas.mapRenderer()) + del self.snapperV + self.snapperV=self.__createVSnapper(self.canvas.mapRenderer()) + + + def __createFeatRubberband(self,isPolygon): + """Function creates rubberband that is used for marking moved feature on the map. + + @param isPolygon is hint for this function; it says if feature is of polygon type or not + @return rubberband for marking moved feature on the map + """ + + # get qgis settings of line width and color for rubberband + settings=QSettings() + qgsLineWidth=settings.value( "/qgis/digitizing/line_width", QVariant(10) ).toInt() + qgsLineRed=settings.value( "/qgis/digitizing/line_color_red", QVariant(255) ).toInt() + qgsLineGreen=settings.value( "/qgis/digitizing/line_color_green", QVariant(0) ).toInt() + qgsLineBlue=settings.value( "/qgis/digitizing/line_color_blue", QVariant(0) ).toInt() + + rband=QgsRubberBand(self.canvas,isPolygon) + rband.setColor(QColor(qgsLineRed[0],qgsLineGreen[0],qgsLineBlue[0])) + rband.setWidth( qgsLineWidth[0] ) + + return rband + + + def __createSnapRubberband(self): + """Function creates rubberband that is used for marking map features + to which snapping will be performed. + + @return rubberband for marking map features + """ + + # get qgis settings of line width and color for rubberband + settings=QSettings() + qgsLineWidth=settings.value( "/qgis/digitizing/line_width", QVariant(10) ).toInt() + + rband=QgsRubberBand(self.canvas,False) + rband.setColor(QColor(255,0,0)) + rband.setWidth(qgsLineWidth[0]) + + return rband + + + def __createVertexMarker(self): + """Function creates vertexMarker that is used for marking moved feature (point) on the map. + + @return vertex marker for marking moved feature on map + """ + + # get qgis settings + settings=QSettings() + qgsLineWidth=settings.value("/qgis/digitizing/line_width",QVariant(10)).toInt() + qgsLineRed=settings.value("/qgis/digitizing/line_color_red",QVariant(255)).toInt() + qgsLineGreen=settings.value("/qgis/digitizing/line_color_green",QVariant(0)).toInt() + qgsLineBlue=settings.value("/qgis/digitizing/line_color_blue",QVariant(0)).toInt() + + verMarker=QgsVertexMarker(self.canvas) + verMarker.setIconType(2) + verMarker.setIconSize(13) + verMarker.setColor(QColor(qgsLineRed[0],qgsLineGreen[0],qgsLineBlue[0])) + verMarker.setPenWidth(qgsLineWidth[0]) + verMarker.setCenter(QgsPoint(-1000,-1000)) + + return verMarker + + + def __createVSSnapper(self,canvasRenderer): + """Function creates snapper that snaps within standard qgis tolerance. + + Snapping of this snapper is done to all segments and vertexes + of all three layers of current OSM database. + + @param canvasRenderer renderer of current map canvas + @return instance of vertex+segment QgsSnapper + """ + + if not self.dbm.currentKey: + # there is no current database -> no layer for snapping + return QgsSnapper(canvasRenderer) + + snapper=QgsSnapper(canvasRenderer) + snapLayers=[] + + # snap to osm layers from current database only + sLayer=QgsSnapper.SnapLayer() + sLayer.mLayer=self.dbm.pointLayers[self.dbm.currentKey] + sLayer.mTolerance=QgsTolerance.vertexSearchRadius(sLayer.mLayer,canvasRenderer) + sLayer.mSnapTo=QgsSnapper.SnapToVertex + snapLayers.append(sLayer) + + sLayer=QgsSnapper.SnapLayer() + sLayer.mLayer=self.dbm.lineLayers[self.dbm.currentKey] + sLayer.mTolerance=QgsTolerance.vertexSearchRadius(sLayer.mLayer,canvasRenderer) + sLayer.mSnapTo=QgsSnapper.SnapToVertexAndSegment + snapLayers.append(sLayer) + + sLayer=QgsSnapper.SnapLayer() + sLayer.mLayer=self.dbm.polygonLayers[self.dbm.currentKey] + sLayer.mTolerance=QgsTolerance.vertexSearchRadius(sLayer.mLayer,canvasRenderer) + sLayer.mSnapTo=QgsSnapper.SnapToVertexAndSegment + snapLayers.append(sLayer) + + snapper.setSnapLayers(snapLayers) + return snapper + + + def __createVSnapper(self,canvasRenderer): + """Function creates snapper that snaps within standard qgis tolerance. + + Snapping of this snapper is done to all vertexes (but not segments) + of all three layers of current OSM database. + + @param canvasRenderer renderer of current map canvas + @return instance of vertex QgsSnapper + """ + + if not self.dbm.currentKey: + # there is no current database -> no layer for snapping + return QgsSnapper(canvasRenderer) + + snapper=QgsSnapper(canvasRenderer) + snapLayers=[] + + # snap to osm layers from current database only + sLayer=QgsSnapper.SnapLayer() + sLayer.mLayer=self.dbm.pointLayers[self.dbm.currentKey] + sLayer.mTolerance=QgsTolerance.vertexSearchRadius(sLayer.mLayer,canvasRenderer) + sLayer.mSnapTo=QgsSnapper.SnapToVertex + snapLayers.append(sLayer) + + sLayer=QgsSnapper.SnapLayer() + sLayer.mLayer=self.dbm.lineLayers[self.dbm.currentKey] + sLayer.mTolerance=QgsTolerance.vertexSearchRadius(sLayer.mLayer,self.canvas.mapRenderer()) + sLayer.mSnapTo=QgsSnapper.SnapToVertex + snapLayers.append(sLayer) + + sLayer=QgsSnapper.SnapLayer() + sLayer.mLayer=self.dbm.polygonLayers[self.dbm.currentKey] + sLayer.mTolerance=QgsTolerance.vertexSearchRadius(sLayer.mLayer,canvasRenderer) + sLayer.mSnapTo=QgsSnapper.SnapToVertex + snapLayers.append(sLayer) + + snapper.setSnapLayers(snapLayers) + return snapper + + + def deactivate(self): + """Functions is called when move-map-tool is being deactivated. + + Function performs standard cleaning; re-initialization etc. + """ + + self.reinit() + + self.dockWidget.toolButtons.setExclusive(False) + self.dockWidget.moveButton.setChecked(False) + self.dockWidget.toolButtons.setExclusive(True) + self.dockWidget.activeEditButton=self.dockWidget.dummyButton + + + def keyPressEvent(self, event): + """This function is called after keyPressEvent(QKeyEvent *) signal is emmited when using move map tool. + If Control key was pressed, function disables snapping til key is released. + + @param event event that occured when key pressing + """ + + if (event.key() == Qt.Key_Control): + self.snappingEnabled=False + self.snapRubBand.reset() + self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("Snapping OFF.") + + + def keyReleaseEvent(self, event): + """This function is called after keyReleaseEvent(QKeyEvent *) signal is emmited when using move map tool. + If Control key was released, function enables snapping again. + + @param event event that occured when key releasing + """ + + if (event.key() == Qt.Key_Control): + self.snappingEnabled = True + self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("Snapping ON - hold Ctrl to disable it.") + + + def canvasDoubleClickEvent(self,event): + """This function is called after doubleclick on map when using move map tool. + Function is interested into left-doubleclicking only. + + It finds out all features that are currently at the place where doubleclick was done. + Then it shows simple dialog with the list of all these features. User can select the required one, + close dialog and continue moving. + + @param event event that occured when double clicking + """ + + if event.button()<>Qt.LeftButton: + return + + self.dockWidget.clear() + #self.removeMarkers() + + # find out map coordinates from mouse click + mapPoint=self.dockWidget.canvasToOsmCoords(event.pos()) + + # display modal dialog with features selection + self.featuresFound=self.dbm.findAllFeatures(mapPoint) + self.ixFeature=0 + + lwItems=[] + for f in self.featuresFound: + feat=f[0] + featType=f[1] + name=self.dbm.getTagValue(feat.id(),featType,"name") + lwItems.append(QString("[%1] ").arg(feat.id()).append(featType).append(QString(" ")).append(name)) + + self.lw.clear() + self.lw.addItems(lwItems) + + self.dockWidget.plugin.iface.mainWindow().statusBar().showMessage("") + self.doubleclick=True + + if self.dlgSelect: + # continue only if OK button was clicked + if self.dlgSelect.exec_()==0: + return + + + def canvasReleaseEvent(self, event): + """This function is called after mouse button releasing on map when using move map tool. + + Such button releasing can have a lot of meanings. + It depends on the current phase of moving. See documentation of the whole move map tool + to know how moving works and how many times user has to release the mouse button to perform the whole moving. + + @param event event that occured when button releasing + """ + + if self.doubleclick: + self.doubleclick=False + return + + if self.movingMode=="INTRO": + + # we are interested only in left button clicking + if event.button()<>Qt.LeftButton: + return + + # find out map coordinates from mouse click + self.mapPointFrom = self.dockWidget.canvasToOsmCoords(event.pos()) + + # what to move? + self.featuresFound=self.dbm.findAllFeatures(self.mapPointFrom) + self.ixFeature=0 + + if len(self.featuresFound)>0: + self.movingMode="SELECTION" + + (feat,featType)=self.featuresFound[self.ixFeature] + self.dockWidget.loadFeature(feat,featType,2) + + elif self.dockWidget.feature: + self.dockWidget.clear() + self.mapPointFrom=None + return + + elif self.movingMode=="SELECTION": + + # we are interested only in left/right button clicking + if event.button() not in (Qt.LeftButton,Qt.RightButton): + return + + if event.button()==Qt.RightButton: + if len(self.featuresFound)<1: + return + + self.ixFeature=self.ixFeature+1 + if self.ixFeature>=len(self.featuresFound): + self.ixFeature=0 + + (feat,featType)=self.featuresFound[self.ixFeature] + self.dockWidget.loadFeature(feat,featType,2) + + else: # LeftButton + + self.reinit() + if self.dockWidget.feature: + self.dockWidget.clear() + + elif self.movingMode=="MOVING": + # finish feature moving + + # we are interested only in left button clicking; other buttons just cancel moving operation + if event.button()<>Qt.LeftButton: + self.reinit() + return + + whereIAm = self.dockWidget.canvasToOsmCoords(event.pos()) + + if self.snapDeltas: + self.mapPointTo=QgsPoint(whereIAm.x()+self.snapDeltas[0],whereIAm.y()+self.snapDeltas[1]) + else: + # no snapping for this moving + self.snapVertexIx=-1 + self.mapPointTo=whereIAm + + deltaX=self.mapPointTo.x()-self.mapPointFrom.x() + deltaY=self.mapPointTo.y()-self.mapPointFrom.y() + + self.__finishFeatureMoving(deltaX,deltaY) + + + def __tryIdentifyFeature(self,event): + """Function just finds first feature at the place when event occured. + Feature is marked on map with rubberBand (or vertexMarker) and is loaded to OSM Feature widget. + + @param event event that occured + """ + + # find out map coordinates from mouse click + mapPoint=self.dockWidget.canvasToOsmCoords(event.pos()) + feature=self.dbm.findFeature(mapPoint) + + if feature: + (feat,featType)=feature + if not self.dockWidget.feature or feat.id()<>self.dockWidget.feature.id(): + self.dockWidget.loadFeature(feat,featType,1) + + elif self.dockWidget.feature: + self.dockWidget.clear() + + + def canvasMoveEvent(self,event): + """This function is called after mouse moving. + + Mouse moving is ignored before feature (to move) is selected. + + @param event event that occured when mouse moving. + """ + + # ignore one move from each two moves + if self.moves<>1: + self.moves=self.moves+1 + return + self.moves=0 + + + if self.movingMode=="INTRO": + self.__tryIdentifyFeature(event) + return + + if self.movingMode=="SELECTION": + + # remember what to move + self.movFeat=self.featuresFound[self.ixFeature][0] + self.movFeatType=self.featuresFound[self.ixFeature][1] + self.movIsPolygon=False + self.featuresFound=[] + self.ixFeature=0 + + # initializing rubberbands + if self.movFeatType in ('Polygon','Line'): + + layer=self.dbm.lineLayers[self.dbm.currentKey] + if self.movFeatType=='Polygon': + self.movIsPolygon=True + layer=self.dbm.polygonLayers[self.dbm.currentKey] + + # finding out three closest vertexes (for these snapping will be enabled) + self.movIndexes=[] + (p,ix,ixB,ixA,dis)=self.movFeat.geometry().closestVertex(self.mapPointFrom) + self.movIndexes.append(ix) + self.movIndexes.append(ixB) + self.movIndexes.append(ixA) + + rubBand=self.__createFeatRubberband(self.movIsPolygon) + rubBand.setToGeometry(self.movFeat.geometry(),layer) + self.rubBands.append(rubBand) + self.isPolygonFlags.append(self.movIsPolygon) + + elif self.movFeatType in ('Point'): + + # find out parent features + (parentFeats,self.memberIndexes,self.isPolygonFlags)=self.dbm.getNodeParents(self.movFeat) + self.movParentVertices=[] + + if len(parentFeats)==0: + self.verMarker.setCenter(self.movFeat.geometry().asPoint()) + + for ix in range(0,len(parentFeats)): + layer=None + if self.isPolygonFlags[ix]: + layer=self.dbm.polygonLayers[self.dbm.currentKey] + self.movParentVertices=self.movParentVertices+(parentFeats[ix].geometry().asPolygon())[0] + else: + layer=self.dbm.lineLayers[self.dbm.currentKey] + self.movParentVertices=self.movParentVertices+parentFeats[ix].geometry().asPolyline() + + parentRubBand=self.__createFeatRubberband(self.isPolygonFlags[ix]) + parentRubBand.setToGeometry(parentFeats[ix].geometry(),layer) + self.rubBands.append(parentRubBand) + + if self.dockWidget.feature: + self.dockWidget.clear() + + # change moving mode to the last one! + self.movingMode="MOVING" + + # movingMode ~ "MOVING" + if self.movFeatType=='Point': + + (deltaX,deltaY)=self.__getDeltaForPoint(event) # snapping is done in this function + targetPoint=QgsPoint(self.mapPointFrom.x()+deltaX,self.mapPointFrom.y()+deltaY) + + if len(self.rubBands)==0: + point=self.movFeat.geometry().asPoint() + self.verMarker.setCenter(QgsPoint(point.x()+deltaX,point.y()+deltaY)) + + # move rubberbands + for ix in range(0,len(self.rubBands)): + for j in range(0,len(self.memberIndexes[ix])): + vertexIx=self.memberIndexes[ix][j] + lastVertexIx=self.rubBands[ix].numberOfVertices()-1 + self.rubBands[ix].movePoint(vertexIx,targetPoint) + + if self.isPolygonFlags[ix]: + if vertexIx==1: + self.rubBands[ix].movePoint(lastVertexIx,targetPoint) + elif vertexIx==lastVertexIx: + self.rubBands[ix].movePoint(vertexIx,targetPoint) + + if vertexIx==1: + self.rubBands[ix].movePoint(0,targetPoint) + + elif self.movFeatType in ('Line','Polygon'): + + (deltaX,deltaY)=self.__getDeltaForLinePolygon(event) + # move feature rubberband + self.rubBands[0].setTranslationOffset(deltaX,deltaY) + + + def __getDeltaForPoint(self,event): + """Function gets an event object, performs snapping from place where event occured and then counts distance + of found position from the place where the whole moving operation has started. + + Special version for points. + + @param event event that occured + """ + + # find out where and how far (from the place where moving was started) we are now + mapPoint = self.dockWidget.canvasToOsmCoords(event.pos()) + + if not self.snappingEnabled: + self.snapDeltas=self.snapFeat=self.snapFeatType=None + # returns how far from the place where moving was started we are now + return (mapPoint.x()-self.mapPointFrom.x(),mapPoint.y()-self.mapPointFrom.y()) + + # perform snapping + self.movParentVertices.append(self.movFeat.geometry().asPoint()) + (retval,snappingResults)=self.snapperVS.snapPoint(event.pos(),self.movParentVertices) + + if len(snappingResults)==0: + self.snapDeltas=self.snapFeat=self.snapFeatType=None + # returns how far from the place where moving was started we are now + return (mapPoint.x()-self.mapPointFrom.x(),mapPoint.y()-self.mapPointFrom.y()) + + # we snapped successfully to something + snappedPoint=QgsPoint(snappingResults[0].snappedVertex) + + self.snapDeltas=(snappedPoint.x()-mapPoint.x(),snappedPoint.y()-mapPoint.y()) + # use snappingResults[0].layer in findFeature() ??? findFeatureInLayer() ??? + (self.snapFeat,self.snapFeatType)=self.dbm.findFeature(snappedPoint) + + # returns how far from the place where moving was started we are now + return (snappedPoint.x()-self.mapPointFrom.x(),snappedPoint.y()-self.mapPointFrom.y()) + + + def __getDeltaForLinePolygon(self,event): + """Function gets an event object, performs snapping from place where event occured and then counts distance + of found position from the place where the whole moving operation has started. + + Special version for lines and polygons. + + @param event event that occured + """ + + # find out where and how far (from the place where moving was started) we are now + mapPoint = self.dockWidget.canvasToOsmCoords(event.pos()) + deltaX=mapPoint.x()-self.mapPointFrom.x() + deltaY=mapPoint.y()-self.mapPointFrom.y() + + if not self.snappingEnabled: + self.snapDeltas=self.snapFeat=self.snapFeatType=None + return (deltaX,deltaY) + + lineMembers=[] + for ix in self.movIndexes: + lineMembers.append(self.movFeat.geometry().vertexAt(ix)) + + allMembers=[] + if self.movFeatType=='Line': + allMembers=self.movFeat.geometry().asPolyline() + else: + polygon=self.movFeat.geometry().asPolygon() + allMembers=polygon[0] + + minDistance=99999 + bestSnappedPoint=None + bestActualLineMember=None + + for i in range(0,len(lineMembers)): + + actualLineMember=QgsPoint(lineMembers[i].x()+deltaX,lineMembers[i].y()+deltaY) + point=self.canvas.getCoordinateTransform().transform(actualLineMember) + (retval,snappingResults)=self.snapperV.snapPoint(QPoint(point.x(),point.y()),allMembers) + + if len(snappingResults)>0: + snappedPoint=QgsPoint(snappingResults[0].snappedVertex) + dX=snappedPoint.x()-(actualLineMember.x()+deltaX) + dY=snappedPoint.y()-(actualLineMember.y()+deltaY) + dist=sqrt(pow(dX,2)+pow(dY,2)) # pythagoras ;) + + if dist + + images/osm_load.png + + + images/osm_import.png + + + images/osm_save.png + + + images/osm_download.png + + + images/osm_upload.png + + + images/osm_move.png + + + images/osm_identify.png + + + images/osm_createPolygon.png + + + images/osm_createPoint.png + + + images/osm_createLine.png + + + images/osm_featureManager.png + + + images/osm_createRelation.png + + + images/osm_remove.png + + + images/osm_star.png + + + images/osm_questionMark.png + + + images/osm_undo.png + + + images/osm_redo.png + + diff --git a/python/plugins/osm/styles/big_scale.style b/python/plugins/osm/styles/big_scale.style new file mode 100644 index 00000000000..0b4feb03740 --- /dev/null +++ b/python/plugins/osm/styles/big_scale.style @@ -0,0 +1,34 @@ +#LINE +highway trunk 3 1 150,0,0 +highway primary 2 1 219,112,147 +highway secondary 1 1 255,120,0 +highway tertiary 0 0 238,230,133 +highway pedestrian 0 0 255,255,0 +highway residential 0 0 100,100,100 +highway footway 0 0 170,170,170 +highway * 1 1 210,200,210 +railway subway 1 2 131,111,255 +railway rail 1 4 0,0,0 +railway * 1 0 0,0,0 +boundary administrative 1 3 85,26,139 +power line 1 0 139,139,131 +#POLYGON +leisure park 0 1 0,0,0 88,245,168 +leisure garden 0 1 0,0,0 88,245,168 +landuse forest 0 1 0,0,0 88,245,168 +landuse allotments 0 1 0,0,0 222,184,135 +tourism zoo 0 1 0,0,0 108,255,185 +building yes 0 1 0,0,0 226,226,226 +natural water 0 1 0,0,0 135,206,255 +waterway riverbank 0 1 0,0,0 135,206,255 +place island 0 1 0,0,0 255,255,255 +highway trunk 3 1 150,0,0 255,255,255 +highway primary 2 1 219,112,147 255,255,255 +highway secondary 1 1 255,120,0 255,255,255 +highway tertiary 0 0 238,230,133 255,255,255 +highway pedestrian 0 0 255,255,0 255,255,255 +highway residential 0 0 100,100,100 255,255,255 +highway footway 0 0 170,170,170 255,255,255 +* * 1 1 0,0,255 255,255,255 +#POINT +amenity hospital emergency/amenity=hospital.svg 6 diff --git a/python/plugins/osm/styles/medium_scale.style b/python/plugins/osm/styles/medium_scale.style new file mode 100644 index 00000000000..4f9889a14bf --- /dev/null +++ b/python/plugins/osm/styles/medium_scale.style @@ -0,0 +1,45 @@ +#LINE +highway trunk 6 1 150,0,0 +highway primary 6 1 219,112,147 +highway secondary 4.5 1 255,120,0 +highway tertiary 3 1 238,230,133 +highway pedestrian 1 1 255,255,0 +highway residential 1 1 100,100,100 +highway footway 2 3 170,170,170 +highway * 1 1 0,255,0 +railway subway 1 2 131,111,255 +railway rail 2 4 0,0,0 +railway * 1 0 0,0,0 +boundary administrative 2 3 85,26,139 +power line 2 1 139,139,131 +#POLYGON +leisure park 0 1 0,0,0 88,245,168 +leisure garden 0 1 0,0,0 88,245,168 +landuse forest 0 1 0,0,0 88,245,168 +landuse allotments 0 1 0,0,0 222,184,135 +tourism zoo 0 1 0,0,0 108,255, +building yes 0.01 1 0,0,0 226,226,226 +natural water 0 1 0,0,0 135,206,255 +waterway riverbank 0 1 0,0,0 135,206,255 +place island 1 1 0,0,0 255,255,255 +highway trunk 6 1 150,0,0 255,255,255 +highway primary 6 1 219,112,147 255,255,255 +highway secondary 4.5 1 255,120,0 255,255,255 +highway tertiary 3 1 238,230,133 255,255,255 +highway pedestrian 1 1 255,255,0 255,255,255 +highway residential 1 1 100,100,100 255,255,255 +highway footway 2 3 170,170,170 255,255,255 +* * 1 1 0,0,255 255,255,255 +#POINT +source:addr uir_adr gpsicons/house.svg 15 +power tower symbol/Cross4.svg 15 +amenity hospital emergency/amenity=hospital.svg 15 +amenity parking transport/amenity=parking.svg 15 +amenity bus_station transport/highway=bus_stop.svg 15 +amenity restaurant entertainment/amenity=restaurant.svg 15 +amenity theatre entertainment/amenity=theatre.svg 15 +amenity pub entertainment/amenity=pub.svg 15 +amenity fast_food entertainment/amenity=fast_food.svg 15 +amenity cinema entertainment/amenity=cinema.svg 15 +amenity cafe entertainment/amenity=cafe.svg 15 +amenity bar entertainment/amenity=bar.svg 15 diff --git a/python/plugins/osm/styles/small_scale.style b/python/plugins/osm/styles/small_scale.style new file mode 100644 index 00000000000..353ecb3fd65 --- /dev/null +++ b/python/plugins/osm/styles/small_scale.style @@ -0,0 +1,48 @@ +#LINE +highway trunk 4.3 1 150,0,0 +highway primary 4.3 1 219,112,147 +highway secondary 3.5 1 255,120,0 +highway tertiary 2.2 1 238,230,133 +highway pedestrian 0.8 1 255,255,0 +highway residential 0.8 1 100,100,100 +highway footway 1.4 3 170,170,170 +highway * 1 1 0,255,0 +railway subway 0.7 2 131,111,255 +railway rail 1 4 0,0,0 +railway tram 1 4 175,0,0 +railway * 0.7 0 0,0,0 +boundary administrative 2 3 85,26,139 +power line 2 1 139,139,131 +* * 1 1 200,200,200 +#POLYGON +leisure park 0 1 0,0,0 88,245,168 +leisure garden 0 1 0,0,0 88,245,168 +landuse forest 0 1 0,0,0 88,245,168 +landuse allotments 0 1 0,0,0 222,184,135 +tourism zoo 0 1 0,0,0 108,255,185 +building yes 0.01 1 0,0,0 226,226,226 +natural water 0 1 0,0,0 135,206,255 +waterway riverbank 0 1 0,0,0 135,206,255 +place island 1 1 0,0,0 255,255,255 +highway trunk 6 1 150,0,0 255,255,255 +highway primary 6 1 219,112,147 255,255,255 +highway secondary 4.5 1 255,120,0 255,255,255 +highway tertiary 3 1 238,230,133 255,255,255 +highway pedestrian 1 1 255,255,0 255,255,255 +highway residential 1 1 100,100,100 255,255,255 +highway footway 2 3 170,170,170 255,255,255 +* * 1 1 0,0,255 255,255,255 +#POINT +source:addr uir_adr gpsicons/point.svg 7 +power tower symbol/Cross4.svg 10 +amenity hospital emergency/amenity=hospital.svg 10 +amenity parking transport/amenity=parking.svg 10 +amenity bus_station transport/highway=bus_stop.svg 10 +amenity restaurant entertainment/amenity=restaurant.svg 10 +amenity theatre entertainment/amenity=theatre.svg 10 +amenity pub entertainment/amenity=pub.svg 10 +amenity fast_food entertainment/amenity=fast_food.svg 10 +amenity cinema entertainment/amenity=cinema.svg 10 +amenity cafe entertainment/amenity=cafe.svg 10 +amenity bar entertainment/amenity=bar.svg 10 +* * gpsicons/point.svg 7 \ No newline at end of file diff --git a/python/plugins/osm/ui_files/DlgAddRelation.ui b/python/plugins/osm/ui_files/DlgAddRelation.ui new file mode 100755 index 00000000000..3cedd3ae47b --- /dev/null +++ b/python/plugins/osm/ui_files/DlgAddRelation.ui @@ -0,0 +1,289 @@ + + DlgAddRelation + + + + 0 + 0 + 620 + 461 + + + + Create OSM relation + + + + + + + + + false + + + + + + + + Relation type: + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 24 + 20 + + + + + + + + + 0 + 0 + + + + + 164 + 24 + + + + + 164 + 16777215 + + + + true + + + + + + + ... + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + + + + Properties + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + ... + + + + + + + ... + + + + + + + + + + 240 + 0 + + + + + + + + + + + + + + Members + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + ... + + + true + + + + + + + ... + + + + + + + + + + 346 + 0 + + + + + + + + false + + + + 0 + 0 + + + + + 16777215 + 140 + + + + true + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + 0 + 0 + + + + + 0 + 30 + + + + Create + + + false + + + + + + + + 0 + 30 + + + + Storno + + + false + + + + + + + + + + diff --git a/python/plugins/osm/ui_files/DlgDownloadOSM.ui b/python/plugins/osm/ui_files/DlgDownloadOSM.ui new file mode 100644 index 00000000000..becc5f7bd3e --- /dev/null +++ b/python/plugins/osm/ui_files/DlgDownloadOSM.ui @@ -0,0 +1,375 @@ + + DlgDownloadOSM + + + Qt::ApplicationModal + + + + 0 + 0 + 595 + 357 + + + + Qt::NoContextMenu + + + Download OSM data + + + + + + + + + true + + + + + + + 16777215 + 16777215 + + + + Extent + + + + + + + + Latitude: + + + + + + + From + + + + + + + true + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + To + + + + + + + true + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Longitude: + + + + + + + From + + + + + + + true + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + To + + + + + + + true + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + + + Qt::Vertical + + + QSizePolicy::Fixed + + + + 20 + 8 + + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + <nothing> + + + 0 + + + + + + + + 23 + 23 + + + + ... + + + + + + + + + + + + + + Download to: + + + + + + + false + + + + + + + ... + + + + + + + + + Open data automatically after download + + + true + + + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 15 + 20 + + + + + + + + Replace current data (current layer will be removed) + + + false + + + + + + + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 15 + 20 + + + + + + + + Use custom renderer + + + true + + + + + + + Qt::Horizontal + + + QSizePolicy::Fixed + + + + 20 + 20 + + + + + + + + + 182 + 0 + + + + + 182 + 16777215 + + + + + + + + Qt::Horizontal + + + QSizePolicy::Expanding + + + + 40 + 20 + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Download + + + + + + + Cancel + + + + + + + + + + diff --git a/python/plugins/osm/ui_files/DlgImport.ui b/python/plugins/osm/ui_files/DlgImport.ui new file mode 100644 index 00000000000..a4f0259d80d --- /dev/null +++ b/python/plugins/osm/ui_files/DlgImport.ui @@ -0,0 +1,122 @@ + + DlgImport + + + Qt::ApplicationModal + + + + 0 + 0 + 248 + 228 + + + + Import data to OSM + + + true + + + + + + In this dialog you can import a layer loaded in QGIS into active OSM data. + + + true + + + + + + + Qt::Vertical + + + + 20 + 29 + + + + + + + + + + Layer + + + + + + + + 0 + 0 + + + + + + + + + + Import only current selection + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::NoButton|QDialogButtonBox::Ok + + + + + + + cboLayer + chkOnlySelection + buttonBox + + + + + buttonBox + rejected() + DlgImport + reject() + + + 238 + 213 + + + 247 + 227 + + + + + diff --git a/python/plugins/osm/ui_files/DlgLoadOSM.ui b/python/plugins/osm/ui_files/DlgLoadOSM.ui new file mode 100644 index 00000000000..1e12a22114a --- /dev/null +++ b/python/plugins/osm/ui_files/DlgLoadOSM.ui @@ -0,0 +1,163 @@ + + DlgLoadOSM + + + Qt::ApplicationModal + + + + 0 + 0 + 508 + 309 + + + + Load OSM + + + + + + + + + true + + + + + + + + OpenStreetMap file to load: + + + -1 + + + + + + + + + + ... + + + + + + + + + Add columns for tags: + + + + + + + + + + 0 + 0 + + + + + + + + + + + + Use custom renderer + + + true + + + + + + + + 182 + 0 + + + + + 182 + 16777215 + + + + + + + + + + + 110 + 16777215 + + + + + 110 + 0 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + Replace current data (current layers will be removed) + + + false + + + + + + + OSMFileEdit + browseOSMButton + lstTags + chkCustomRenderer + buttonBox + + + + + buttonBox + rejected() + DlgLoadOSM + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/python/plugins/osm/ui_files/DlgSaveOSM.ui b/python/plugins/osm/ui_files/DlgSaveOSM.ui new file mode 100644 index 00000000000..423d8bff14e --- /dev/null +++ b/python/plugins/osm/ui_files/DlgSaveOSM.ui @@ -0,0 +1,226 @@ + + DlgSaveOSM + + + Qt::ApplicationModal + + + + 0 + 0 + 370 + 206 + + + + Save OSM + + + + + + + + + true + + + + + + + + Where to save: + + + -1 + + + + + + + + + + + 50 + 16777215 + + + + ... + + + + + + + + + Features to save: + + + + + + + 15 + + + 0 + + + 10 + + + + + + + Points + + + true + + + + + + + Lines + + + true + + + + + + + Polygons + + + true + + + + + + + + + + + Relations + + + true + + + + + + + Tags + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + 110 + 16777215 + + + + + 110 + 0 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::NoButton|QDialogButtonBox::Ok + + + + + + + + + + + OSMFileEdit + browseOSMButton + + + + + buttonBox + rejected() + DlgSaveOSM + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/python/plugins/osm/ui_files/DlgUploadOSM.ui b/python/plugins/osm/ui_files/DlgUploadOSM.ui new file mode 100644 index 00000000000..c04ab53c204 --- /dev/null +++ b/python/plugins/osm/ui_files/DlgUploadOSM.ui @@ -0,0 +1,229 @@ + + DlgUploadOSM + + + Qt::ApplicationModal + + + + 0 + 0 + 373 + 468 + + + + + 0 + 0 + + + + Upload OSM data + + + + + + + + + true + + + + + + Ready for upload + + + + + + true + + + + 0 + 0 + + + + + 330 + 90 + + + + + 330 + 90 + + + + false + + + Qt::ElideLeft + + + false + + + false + + + 5 + + + + 1 + + + + + 2 + + + + + 3 + + + + + 4 + + + + + 5 + + + + + + + + Comment on your changes: + + + + + + + true + + + + 16777215 + 85 + + + + + + + + + + + true + + + OSM account + + + + + + + + Username: + + + + + + + true + + + + 0 + 0 + + + + + + + + Password: + + + + + + + + 0 + 0 + + + + + + + + Show password + + + + + + + Save password + + + + + + + + + + + + QDialogButtonBox::Close + + + + + + + userLineEdit + passwdLineEdit + chkShowPasswd + buttonBox + + + + + buttonBox + rejected() + DlgUploadOSM + reject() + + + 308 + 337 + + + 265 + 308 + + + + + diff --git a/python/plugins/osm/ui_files/DockUndoRedo.ui b/python/plugins/osm/ui_files/DockUndoRedo.ui new file mode 100644 index 00000000000..dfaf56b722e --- /dev/null +++ b/python/plugins/osm/ui_files/DockUndoRedo.ui @@ -0,0 +1,100 @@ + + + OsmUndoRedoDockWidget + + + + 0 + 0 + 227 + 374 + + + + Qt::AllDockWidgetAreas + + + OSM Edit History + + + + + + + + + Clear all + + + Clear all + + + Clear all + + + ... + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + false + + + Undo + + + Undo + + + Undo + + + ... + + + + + + + false + + + Redo + + + Redo + + + Redo + + + ... + + + + + + + + + + + + + + diff --git a/python/plugins/osm/ui_files/DockWidget.ui b/python/plugins/osm/ui_files/DockWidget.ui new file mode 100644 index 00000000000..386f1117631 --- /dev/null +++ b/python/plugins/osm/ui_files/DockWidget.ui @@ -0,0 +1,731 @@ + + + OsmDockWidget + + + Qt::NonModal + + + true + + + + 0 + 0 + 265 + 776 + + + + + 0 + 0 + + + + + 261 + 357 + + + + + 524287 + 524287 + + + + OSM Feature + + + + + + + 1 + + + 0 + + + + + + 1 + 25 + + + + + 1 + 27 + + + + Qt::NoFocus + + + ... + + + true + + + + + + + + 26 + 25 + + + + + 25 + 26 + + + + Qt::NoFocus + + + Identify object + + + Identify object + + + Identify object + + + ... + + + true + + + + + + + + 26 + 25 + + + + + 25 + 26 + + + + Qt::NoFocus + + + Move object + + + ... + + + true + + + + + + + + 26 + 25 + + + + + 25 + 26 + + + + Qt::NoFocus + + + Create point + + + ... + + + true + + + + + + + + 26 + 25 + + + + Qt::NoFocus + + + Create line + + + ... + + + true + + + + + + + + 26 + 25 + + + + + 25 + 26 + + + + Qt::NoFocus + + + Create polygon + + + ... + + + true + + + + + + + + 26 + 25 + + + + + 25 + 26 + + + + Qt::NoFocus + + + Create relation + + + ... + + + true + + + + + + + + 25 + 26 + + + + Undo + + + Undo + + + Undo + + + ... + + + + + + + + 25 + 26 + + + + Redo + + + Redo + + + Redo + + + ... + + + + + + + + 25 + 26 + + + + Show/Hide OSM Edit History + + + Show/Hide OSM Edit History + + + Show/Hide OSM Edit History + + + ... + + + true + + + + + + + + + true + + + + 0 + 95 + + + + + 16777215 + 95 + + + + Feature: + + + + 0 + + + 18 + + + 4 + + + 3 + + + 6 + + + + + 0 + + + + + TYPE, ID: + + + + + + + CREATED: + + + + + + + USER: + + + + + + + unknown + + + + + + + unknown + + + + + + + unknown + + + + + + + + 25 + 25 + + + + ... + + + + + + + + + + + + + 0 + 175 + + + + + 16777215 + 16777215 + + + + QTabWidget::North + + + QTabWidget::Rounded + + + 0 + + + + Properties + + + + + + + 0 + 0 + + + + + 205 + 100 + + + + + 16777215 + 16777215 + + + + QFrame::Box + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + ... + + + + + + + + + + Relations + + + + + + + + false + + + + 0 + 60 + + + + + 16777215 + 104 + + + + QFrame::Box + + + + + + + + + false + + + + 26 + 25 + + + + + 26 + 25 + + + + Add new relation + + + Add new relation + + + Add new relation + + + A + + + + + + + false + + + + 26 + 25 + + + + + 26 + 25 + + + + Edit selected relation + + + Edit selected relation + + + Edit selected relation + + + E + + + + + + + false + + + + 26 + 25 + + + + + 26 + 25 + + + + Remove selected relation + + + Remove selected relation + + + Remove selected relation + + + R + + + + + + + Qt::Vertical + + + QSizePolicy::Minimum + + + + 26 + 0 + + + + + + + + + + + + Relation tags: + + + + + + + false + + + + 0 + 115 + + + + + 16777215 + 16777215 + + + + ForbiddenCursor + + + Qt::NoContextMenu + + + QFrame::Box + + + QFrame::Sunken + + + 0 + + + false + + + false + + + 1 + + + + 1 + + + + + + + + Relation members: + + + + + + + false + + + + 0 + 115 + + + + + 16777215 + 16777215 + + + + QFrame::Box + + + + + + + + + + + + + diff --git a/src/providers/CMakeLists.txt b/src/providers/CMakeLists.txt index ee42f62dc6c..4c9aaf57df0 100644 --- a/src/providers/CMakeLists.txt +++ b/src/providers/CMakeLists.txt @@ -1,5 +1,5 @@ -SUBDIRS (memory ogr wms delimitedtext) +SUBDIRS (memory ogr wms delimitedtext osm) IF (POSTGRES_FOUND) SUBDIRS (postgres) diff --git a/src/providers/osm/CMakeLists.txt b/src/providers/osm/CMakeLists.txt new file mode 100644 index 00000000000..0cf5da18879 --- /dev/null +++ b/src/providers/osm/CMakeLists.txt @@ -0,0 +1,45 @@ + +######################################################## +# Files + +SET(OSM_SRCS +osmhandler.cpp +osmprovider.cpp +osmrenderer.cpp +osmstyle.cpp +) + +SET(OSM_MOC_HDRS +osmprovider.h +osmrenderer.h +osmstyle.h +osmhandler.h +) + + +######################################################## +# Build + +QT4_WRAP_CPP(OSM_MOC_SRCS ${OSM_MOC_HDRS}) + +INCLUDE_DIRECTORIES ( + ../../core + ../../core/renderer + ${GEOS_INCLUDE_DIR} +) + +ADD_LIBRARY (osmprovider MODULE ${OSM_SRCS} ${OSM_MOC_SRCS}) + +TARGET_LINK_LIBRARIES (osmprovider + ${QT_QTCORE_LIBRARY} + ${QT_QTXML_LIBRARY} + qgis_core +) + + +######################################################## +# Install + +INSTALL(TARGETS osmprovider + RUNTIME DESTINATION ${QGIS_PLUGIN_DIR} + LIBRARY DESTINATION ${QGIS_PLUGIN_DIR}) diff --git a/src/providers/osm/osmhandler.cpp b/src/providers/osm/osmhandler.cpp new file mode 100644 index 00000000000..096457c6b2f --- /dev/null +++ b/src/providers/osm/osmhandler.cpp @@ -0,0 +1,382 @@ +/*************************************************************************** + osmhandler.cpp - handler for parsing OSM data + ------------------ + begin : October 2008 + copyright : (C) 2008 by Lukas Berka + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "osmhandler.h" + +#include +#include + +#include "qgslogger.h" +#include "qgsapplication.h" +#include "qgsgeometry.h" + +#include +#include +#include +#include +#include +#include + +#define MAX_FEATURE_ID 99999999 +#define COMMIT_AFTER_TAGS 300000 + + +// object construction +OsmHandler::OsmHandler(QFile *f, sqlite3 *database) +{ + mDatabase = database; + mCnt=0; + mPointCnt = mLineCnt = mPolygonCnt = 0; + mPosId = 1; + xMin = yMin = MAX_FEATURE_ID; + xMax = yMax = -MAX_FEATURE_ID; + firstWayMemberId = ""; + mFirstMemberAppeared=0; + + char sqlInsertNode[] = "INSERT INTO node ( id, lat, lon, timestamp, user, usage ) VALUES (?,?,?,?,?,'0');"; + if ( sqlite3_prepare_v2(mDatabase, sqlInsertNode, sizeof(sqlInsertNode), &mStmtInsertNode, 0) != SQLITE_OK ) + { + QgsDebugMsg("failed to prepare sqlInsertNode!!!"); + } + + char sqlInsertWay[] = "INSERT INTO way ( id, timestamp, user, closed ) VALUES (?,?,?,?);"; + if ( sqlite3_prepare_v2(mDatabase, sqlInsertWay, sizeof(sqlInsertWay), &mStmtInsertWay, 0) != SQLITE_OK ) + { + QgsDebugMsg("failed to prepare sqlInsertWay!!!"); + } + + char sqlInsertTag[] = "INSERT INTO tag ( key, val, object_id, object_type ) VALUES (?,?,?,?);"; + if ( sqlite3_prepare_v2(mDatabase, sqlInsertTag, sizeof(sqlInsertTag), &mStmtInsertTag, 0) != SQLITE_OK ) + { + QgsDebugMsg("failed to prepare sqlInsertTag!!!"); + } + + char sqlInsertWayMember[] = "INSERT INTO way_member ( way_id, pos_id, node_id ) VALUES (?,?,?);"; + if ( sqlite3_prepare_v2(mDatabase, sqlInsertWayMember, sizeof(sqlInsertWayMember), &mStmtInsertWayMember, 0) != SQLITE_OK ) + { + QgsDebugMsg("failed to prepare sqlInsertWayMember!!!"); + } + + char sqlInsertRelation[] = "INSERT INTO relation ( id, timestamp, user, type ) VALUES (?,?,?,?);"; + if ( sqlite3_prepare_v2(mDatabase, sqlInsertRelation, sizeof(sqlInsertRelation), &mStmtInsertRelation, 0) != SQLITE_OK ) + { + QgsDebugMsg("failed to prepare sqlInsertRelation!!!"); + } + + char sqlInsertRelationMember[] = "INSERT INTO relation_member ( relation_id, pos_id, member_id, member_type, role ) VALUES (?,?,?,?,?);"; + if ( sqlite3_prepare_v2(mDatabase, sqlInsertRelationMember, sizeof(sqlInsertRelationMember), &mStmtInsertRelationMember, 0) != SQLITE_OK ) + { + QgsDebugMsg("failed to prepare sqlInsertRelationMember!!!"); + } + + char sqlInsertVersion[] = "INSERT INTO version (object_id,object_type,version_id) VALUES (?,?,?);"; + if ( sqlite3_prepare_v2(mDatabase, sqlInsertVersion, sizeof(sqlInsertVersion), &mStmtInsertVersion, 0) != SQLITE_OK ) + { + QgsDebugMsg("failed to prepare sqlInsertVersion!!!"); + } +} + +OsmHandler::~OsmHandler() +{ + sqlite3_finalize(mStmtInsertTag); + sqlite3_finalize(mStmtInsertRelation); + sqlite3_finalize(mStmtInsertRelationMember); + sqlite3_finalize(mStmtInsertVersion); +} + + +bool OsmHandler::startDocument() +{ + sqlite3_exec(mDatabase, "BEGIN;", 0, 0, 0); + return true; +} + + +QString OsmHandler::errorString() +{ + return mError; +} + + +bool OsmHandler::startElement(const QString & pUri, const QString & pLocalName, const QString & pName, const QXmlAttributes & pAttrs) +{ + QString name = pLocalName; + + if (name == "osm") + { + if (pAttrs.value("version") != "0.6") + { + mError = "Invalid OSM version. Only files of v0.6 are supported."; + return false; + } + } + else if (name == "node") + { + //todo: test if pAttrs.value("visible").toUtf8() is "true" -> if not, node has to be ignored! + + mObjectId = pAttrs.value("id"); + mObjectType = "node"; + + double id = pAttrs.value("id").toInt(); + double lat = pAttrs.value("lat").toDouble(); + double lon = pAttrs.value("lon").toDouble(); + QString timestamp = pAttrs.value("timestamp"); + QString user = pAttrs.value("user"); + + if (latyMax) yMax=lat; + if (lonxMax) xMax=lon; + + sqlite3_bind_int(mStmtInsertNode, 1, id); + sqlite3_bind_double(mStmtInsertNode, 2, lat); + sqlite3_bind_double(mStmtInsertNode, 3, lon); + sqlite3_bind_text(mStmtInsertNode, 4, timestamp.toUtf8(), -1, SQLITE_TRANSIENT); // TODO: maybe static? + sqlite3_bind_text(mStmtInsertNode, 5, user.toUtf8(), -1, SQLITE_TRANSIENT); // TODO: maybe static? + + if (sqlite3_step(mStmtInsertNode) != SQLITE_DONE) + { + QgsDebugMsg("Storing node information into database failed."); + return false; + } + + sqlite3_reset(mStmtInsertNode); // make ready for next insert + + // store version number of this object + sqlite3_bind_text(mStmtInsertVersion, 1, pAttrs.value("id").toUtf8(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(mStmtInsertVersion, 2, mObjectType.toUtf8(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(mStmtInsertVersion, 3, pAttrs.value("version").toUtf8(), -1, SQLITE_TRANSIENT); + + if (sqlite3_step(mStmtInsertVersion) != SQLITE_DONE) + { + QgsDebugMsg("Storing version information into database failed."); + return false; + } + sqlite3_reset(mStmtInsertVersion); // make ready for next insert + + // increase node counter + mPointCnt++; + } + else if (name == "way") + { + if (mObjectType!="way") + { + sqlite3_finalize(mStmtInsertNode); + } + + mObjectId = pAttrs.value("id"); + mObjectType = "way"; + mPosId = 1; + mFirstMemberAppeared=0; + + //todo: test if pAttrs.value("visible").toUtf8() is "true" -> if not, way has to be ignored! + + sqlite3_bind_text(mStmtInsertWay, 1, mObjectId.toUtf8(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(mStmtInsertWay, 2, pAttrs.value("timestamp").toUtf8(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(mStmtInsertWay, 3, pAttrs.value("user").toUtf8(), -1, SQLITE_TRANSIENT); + + // store version number of this object + sqlite3_bind_text(mStmtInsertVersion, 1, pAttrs.value("id").toUtf8(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(mStmtInsertVersion, 2, mObjectType.toUtf8(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(mStmtInsertVersion, 3, pAttrs.value("version").toUtf8(), -1, SQLITE_TRANSIENT); + + if (sqlite3_step(mStmtInsertVersion) != SQLITE_DONE) + { + QgsDebugMsg("Storing version information into database failed."); + return false; + } + sqlite3_reset(mStmtInsertVersion); // make ready for next insert + } + else if (name == "nd") + { + // store id of the first and last way member to be able to decide if the way is closed (polygon) or not + if (firstWayMemberId=="") { + firstWayMemberId = pAttrs.value("ref"); + } + lastWayMemberId = pAttrs.value("ref"); + + if (firstWayMemberId==lastWayMemberId) + mFirstMemberAppeared++; + + if ((firstWayMemberId!=lastWayMemberId) || (mFirstMemberAppeared<2)) + { + sqlite3_bind_text(mStmtInsertWayMember, 1, mObjectId.toUtf8(), -1, SQLITE_TRANSIENT); // TODO: maybe static? + sqlite3_bind_int(mStmtInsertWayMember, 2, mPosId); + sqlite3_bind_text(mStmtInsertWayMember, 3, pAttrs.value("ref").toUtf8(), -1, SQLITE_TRANSIENT); + + if (sqlite3_step(mStmtInsertWayMember) != SQLITE_DONE) + { + QgsDebugMsg("Storing way-node relationship into database failed."); + return false; + }; + sqlite3_reset(mStmtInsertWayMember); + } + mPosId++; + } + else if (name == "relation") + { + if (mObjectType!="relation") + { + sqlite3_finalize(mStmtInsertWay); + sqlite3_finalize(mStmtInsertWayMember); + } + + mObjectId = pAttrs.value("id"); + mRelationType = ""; + mObjectType = "relation"; + mPosId = 1; + + //todo: test if pAttrs.value("visible").toUtf8() is "true" -> if not, relation has to be ignored! + + sqlite3_bind_text(mStmtInsertRelation, 1, mObjectId.toUtf8(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(mStmtInsertRelation, 2, pAttrs.value("timestamp").toUtf8(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(mStmtInsertRelation, 3, pAttrs.value("user").toUtf8(), -1, SQLITE_TRANSIENT); + + // store version number of this object + sqlite3_bind_text(mStmtInsertVersion, 1, pAttrs.value("id").toUtf8(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(mStmtInsertVersion, 2, mObjectType.toUtf8(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(mStmtInsertVersion, 3, pAttrs.value("version").toUtf8(), -1, SQLITE_TRANSIENT); + + if (sqlite3_step(mStmtInsertVersion) != SQLITE_DONE) + { + QgsDebugMsg("Storing version information into database failed."); + return false; + } + sqlite3_reset(mStmtInsertVersion); // make ready for next insert + } + else if (name == "member") + { + sqlite3_bind_text(mStmtInsertRelationMember, 1, mObjectId.toUtf8(),-1,SQLITE_TRANSIENT); + sqlite3_bind_int(mStmtInsertRelationMember, 2, mPosId); + sqlite3_bind_text(mStmtInsertRelationMember, 3, pAttrs.value("ref").toUtf8(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(mStmtInsertRelationMember, 4, pAttrs.value("type").toUtf8(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(mStmtInsertRelationMember, 5, pAttrs.value("role").toUtf8(), -1, SQLITE_TRANSIENT); + + if (sqlite3_step(mStmtInsertRelationMember) != SQLITE_DONE) + { + QgsDebugMsg("Storing relation-feature relationship into database failed."); + return false; + }; + + sqlite3_reset(mStmtInsertRelationMember); + mPosId++; + } + else if (name == "tag") + { + if (mCnt==COMMIT_AFTER_TAGS) + { + sqlite3_exec(mDatabase, "COMMIT;", 0, 0, 0); + sqlite3_exec(mDatabase, "BEGIN;", 0, 0, 0); + mCnt=0; + } + mCnt++; + + sqlite3_bind_text(mStmtInsertTag, 1, pAttrs.value("k").toUtf8(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(mStmtInsertTag, 2, pAttrs.value("v").toUtf8(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(mStmtInsertTag, 3, mObjectId.toUtf8(), -1, SQLITE_TRANSIENT); + sqlite3_bind_text(mStmtInsertTag, 4, mObjectType.toUtf8(), -1, SQLITE_TRANSIENT); + + // we've got node parameters -> let's create new database record + if ( sqlite3_step(mStmtInsertTag) != SQLITE_DONE ) + { + QgsDebugMsg(QString("Storing tag into database failed. K:%1, V:%2.").arg(pAttrs.value("k")).arg(pAttrs.value("v"))); + return false; + } + sqlite3_reset(mStmtInsertTag); + + // if we are under xml tag and we reach xml tag , lets insert prepared relation into DB + if ((mObjectType == "relation") && (pAttrs.value("k")=="type")) + { + mRelationType=pAttrs.value("v"); + } + } + else if (name == "bounds") + { + // e.g. + // notice: getting boundaries from OSM file tag was not correct for some maps - cannot be used + + // xMin = pAttrs.value("minlon").toDouble(); + // xMax = pAttrs.value("maxlon").toDouble(); + // yMin = pAttrs.value("minlat").toDouble(); + // yMax = pAttrs.value("maxlat").toDouble(); + } + return true; +} + + +bool OsmHandler::endElement( const QString & pURI, const QString & pLocalName, const QString & pName ) +{ + QString name = pLocalName; + if (name == "way") + { + int isPolygon=false; + int cntMembers=mPosId-1; + + if (firstWayMemberId==lastWayMemberId) + isPolygon=true; + + // test if polygon is correct; it should have >2 member points + if ((isPolygon) && (cntMembers<4)) { + sqlite3_reset(mStmtInsertWay); + return true; + } + + // test if way is correct; it should have more then 1 member point + if (cntMembers<2) { + sqlite3_reset(mStmtInsertWay); + return true; + } + + // we should bind the last information needed for way insertion -> if the way is closed (polygon) or not + sqlite3_bind_int(mStmtInsertWay, 4, (isPolygon ? 1 : 0)); + + // well, insert new way + if (sqlite3_step(mStmtInsertWay) != SQLITE_DONE) + { + QgsDebugMsg("Storing way information into database failed."); + return false; + }; + + // make statement ready for next insert + sqlite3_reset(mStmtInsertWay); + + if (isPolygon) + mPolygonCnt++; + else + mLineCnt++; + + // make variables ready for next way parsing + firstWayMemberId=""; + } + else if (name == "relation") + { + sqlite3_bind_text(mStmtInsertRelation, 4, mRelationType.toUtf8(), -1, SQLITE_TRANSIENT); + + if ( sqlite3_step(mStmtInsertRelation) != SQLITE_DONE ) + { + QgsDebugMsg(QString("Storing relation into database failed.")); + return false; + } + sqlite3_reset(mStmtInsertRelation); + } + return true; +} + + +bool OsmHandler::endDocument() +{ + // first commit all database actions connected to xml parsing + sqlite3_exec(mDatabase, "COMMIT;", 0, 0, 0); +} + diff --git a/src/providers/osm/osmhandler.h b/src/providers/osm/osmhandler.h new file mode 100644 index 00000000000..8e394f00c6a --- /dev/null +++ b/src/providers/osm/osmhandler.h @@ -0,0 +1,86 @@ +/*************************************************************************** + osmhandler.h - handler for parsing OSM data + ------------------ + begin : October 2008 + copyright : (C) 2008 by Lukas Berka + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include +#include +#include +#include +#include + +#include + +#include +using namespace std; + +/** + * XML SAX handler -> while processing XML file, + * stores data to specified sqlite database + */ +class OsmHandler: public QXmlDefaultHandler +{ +public: +// member variables + + QFile mFile; + sqlite3 *mDatabase; + int mCnt; + + int mFileSize; + QString mError; + // xml processing information + QString mObjectId; //last node, way or relation id while parsing file + QString mObjectType; //one of strings "node", "way", "relation" + QString mRelationType; + float mLat; + float mLon; + double xMin, xMax, yMin, yMax; + + int mPointCnt; + int mLineCnt; + int mPolygonCnt; + + int mCurrent_way_id; + int mPosId; + QString firstWayMemberId; + QString lastWayMemberId; + int mFirstMemberAppeared; + +//functions + + // object construction + OsmHandler(QFile *f, sqlite3 *database); + ~OsmHandler(); + // xml processing + + bool startDocument(); + QString errorString(); + bool startElement( const QString & pUri, const QString & pLocalName, const QString & pName, const QXmlAttributes & pAttrs ); + bool endElement( const QString & pURI, const QString & pLocalName, const QString & pName ); + bool endDocument(); + +private: + sqlite3_stmt *mStmtInsertNode; + sqlite3_stmt *mStmtInsertWay; + sqlite3_stmt *mStmtInsertTag; + sqlite3_stmt *mStmtInsertWayMember; + sqlite3_stmt *mStmtInsertRelation; + sqlite3_stmt *mStmtInsertRelationMember; + sqlite3_stmt *mStmtUpdateNode; + sqlite3_stmt *mStmtInsertVersion; +}; + + + + + diff --git a/src/providers/osm/osmprovider.cpp b/src/providers/osm/osmprovider.cpp new file mode 100644 index 00000000000..b0804cf2267 --- /dev/null +++ b/src/providers/osm/osmprovider.cpp @@ -0,0 +1,1672 @@ +/*************************************************************************** + osmprovider.cpp - provider for OSM; stores OSM data in sqlite3 DB + ------------------ + begin : October 2008 + copyright : (C) 2008 by Lukas Berka + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "osmprovider.h" +#include "osmhandler.h" +#include "osmrenderer.h" + +#include "qgsfeature.h" +#include "qgsfield.h" +#include "qgsgeometry.h" +#include "qgslogger.h" +#include "qgsvectordataprovider.h" +#include "qgsapplication.h" + +#include +#include +#include +#include +#include +#include + +using namespace std; + + +static const QString TEXT_PROVIDER_KEY = "osm"; +static const QString TEXT_PROVIDER_DESCRIPTION = "Open Street Map data provider"; +static const QString DATE_TIME_FMT = "dd.MM.yyyy HH:mm:ss"; +static const QString PLUGIN_VERSION = "0.4"; + +// supported attributes +const char* QgsOSMDataProvider::attr[] = { "timestamp", "user", "tags" }; + + + +QgsOSMDataProvider::QgsOSMDataProvider(QString uri) + : QgsVectorDataProvider(uri) +{ + mDatabaseStmt = NULL; + mValid = false; + QgsDebugMsg("Initializing provider: "+uri); + + // set the selection rectangle to null + mSelectionRectangle = 0; + mSelectionRectangleGeom = NULL; + mDatabase = NULL; + mInitObserver = NULL; + mFeatureType = PointType; // default + mTagsRetrieval = false; + // set default boundaries + xMin = -DEFAULT_EXTENT; + xMax = DEFAULT_EXTENT; + yMin = -DEFAULT_EXTENT; + yMax = DEFAULT_EXTENT; + + // get the filename and the type parameter from the URI + int fileNameEnd = uri.indexOf('?'); + if (fileNameEnd == -1) + { + QgsDebugMsg("Bad URI - you need to specify the feature type, OSM provider cannot be constructed."); + return; + } + + QString uriEnd = uri.mid(fileNameEnd+1); + QStringList props = uriEnd.split("&"); + for (QStringList::iterator it = props.begin(); it != props.end(); ++it) + { + QStringList prop = it->split("="); + if (prop.count() != 2) + { + QgsDebugMsg("Incorrectly formed input property!"); + return; + } + + QString propName = prop[0]; + QString propValue = prop[1]; + + if (propName == "type") + { + if (propValue == "line") + mFeatureType = LineType; + else if (propValue == "point") + mFeatureType = PointType; + else if (propValue == "polygon") + mFeatureType = PolygonType; + else + QgsDebugMsg("Unknown feature type: "+propValue); + } + if (propName == "observer") + { + ulong observerAddr = propValue.toULong(); + mInitObserver = (QObject*) observerAddr; + mInitObserver->setProperty("osm_state", QVariant(1)); + } + if (propName == "tags") + { + mTagsRetrieval = (propValue == "yes"); + } + if (propName == "tag") + { + mCustomTagsList = propValue.split("+"); + } + if (propName == "style") + { + mStyleFileName = propValue; + int p1=mStyleFileName.lastIndexOf("/"); + int p2=mStyleFileName.lastIndexOf("_"); + + mStyle=mStyleFileName.mid(p1+1,p2-p1-1); // "medium", "big", "small" + } + } + + // set up the attributes and the geometry type depending on the feature type - same attributes for both point and way type so far + mAttributeFields[TimestampAttr] = QgsField( attr[TimestampAttr], QVariant::String, "string" ); + mAttributeFields[UserAttr] = QgsField( attr[UserAttr], QVariant::String, "string" ); + mAttributeFields[TagAttr] = QgsField( attr[TagAttr], QVariant::String, "string" ); + // add custom tags + for (int tagId = 0; tagId < mCustomTagsList.count(); ++tagId) + { + mAttributeFields[CustomTagAttr+tagId] = QgsField(mCustomTagsList[tagId], QVariant::String, "string"); + } + + // get source file name and database file name + mFileName = uri.left(fileNameEnd); + mDatabaseFileName = mFileName+".db"; + QFile dbFile(mDatabaseFileName); + QFile osmFile(mFileName); + bool databaseExists = dbFile.exists(); + + // open (and create schema if neccessary) database + if (!openDatabase()) + { + QgsDebugMsg("Opening sqlite3 database failed, OSM provider cannot be constructed."); + closeDatabase(); + return; + }; + + // flag determining if OSM file parsing is neccessary + bool shouldParse = true; + + // test if db file belonging to source OSM file exists and if it has the right version + if (databaseExists && isDatabaseCompatibleWithInput(mFileName) && isDatabaseCompatibleWithPlugin()) + shouldParse = false; + + if (shouldParse) + { + if (databaseExists) + dropDatabaseSchema(); + + if (!createDatabaseSchema()) + { + QgsDebugMsg("Creating OSM database schema failed, OSM provider cannot be constructed."); + dropDatabaseSchema(); + return; + } + + // load OSM file to database (uses XML parsing) + if (!(loadOsmFile(mFileName))) + { + QgsDebugMsg("Parsing OSM data file failed, OSM provider cannot be constructed."); + // process of creating osm database wasn't successfull -> remove all new database files + if (dbFile.exists()) + dbFile.remove(); + // and remove also new database journal file if any + QFile dbJournalFile(QString(mFileName+".db-journal")); + if (dbJournalFile.exists()) + dbJournalFile.remove(); + // now stop the osmprovider construction! + return; + } + } + else + { + // there was no parsing, we must find out default area boundaries from database meta information + // prepare select command + QString cmd = QString("SELECT val FROM meta WHERE key='default-area-boundaries';"); + + // just conversion "cmd" to "const char*" + QByteArray cmd_bytes = cmd.toAscii(); + const char *ptr = cmd_bytes.data(); + + sqlite3_stmt *databaseStmt; + if ( sqlite3_prepare_v2(mDatabase, ptr, cmd_bytes.size(), &databaseStmt, 0) != SQLITE_OK ) + { + QgsDebugMsg("Getting default area boundaries failed."); + // don't worry, we just let default values in xMax, yMax, xMin and yMin variables + } + else + { + // select command has just run successful + if (sqlite3_step(databaseStmt) != SQLITE_ROW) + { + QgsDebugMsg("Getting default area boundaries failed."); + // don't worry again, we just let default values in boundary variables + } + else + { + const unsigned char *boundaries_char = sqlite3_column_text(databaseStmt,0); + QString boundaries((const char *) boundaries_char); + // boundaries should be string of the following format: "xMin-yMin-xMax-yMax" + int separ1_pos = boundaries.indexOf("-"); + int separ2_pos = boundaries.indexOf("-",separ1_pos+1); + int separ3_pos = boundaries.indexOf("-",separ2_pos+1); + xMin = boundaries.left(separ1_pos).toDouble(); + yMin = boundaries.mid(separ1_pos+1,separ2_pos-separ1_pos-1).toDouble(); + xMax = boundaries.mid(separ2_pos+1,separ3_pos-separ2_pos-1).toDouble(); + yMax = boundaries.right(boundaries.size()-separ3_pos-1).toDouble(); + } + } + + // destroy database statement + sqlite3_finalize(databaseStmt); + } + + // prepare statement for tag retrieval + const char *zSql = "SELECT key, val FROM tag WHERE object_id=? AND object_type=?"; + int rc = sqlite3_prepare_v2(mDatabase, zSql, -1, &mTagsStmt, 0); + if ( rc!=SQLITE_OK ) + QgsDebugMsg("tags for object - prepare failed."); + + const char *zSqlC = "SELECT val FROM tag WHERE object_id=? AND object_type=? AND key=?"; + rc = sqlite3_prepare_v2(mDatabase, zSqlC, -1, &mCustomTagsStmt, 0); + if ( rc!=SQLITE_OK ) + QgsDebugMsg("custom tags for object - prepare failed."); + + // prepare statements for feature retrieval + const char *zSqlW = "SELECT id, wkb, timestamp, user FROM way WHERE id=? AND status<>'R' AND u=1"; + rc = sqlite3_prepare_v2(mDatabase, zSqlW, -1, &mWayStmt, 0); + if ( rc!=SQLITE_OK ) + QgsDebugMsg("sqlite3 statement for way retrieval - prepare failed."); + + const char *zSqlN = "SELECT id, lat, lon, timestamp, user FROM node WHERE id=? AND usage=0 AND status<>'R' AND u=1"; + rc = sqlite3_prepare_v2(mDatabase, zSqlN, -1, &mNodeStmt, 0); + if ( rc!=SQLITE_OK ) + QgsDebugMsg("sqlite3 statement for node retrieval - prepare failed."); + + mValid = true; +} + + +QgsOSMDataProvider::~QgsOSMDataProvider() +{ + // destruct selected geometry + delete mSelectionRectangleGeom; + sqlite3_finalize(mTagsStmt); + sqlite3_finalize(mCustomTagsStmt); + sqlite3_finalize(mWayStmt); + sqlite3_finalize(mNodeStmt); + sqlite3_finalize(mDatabaseStmt); + closeDatabase(); +} + + +bool QgsOSMDataProvider::isDatabaseCompatibleWithInput(QString mFileName) +{ + QFile osmFile(mFileName); + QFileInfo osmFileInfo(osmFile); + mOsmFileLastModif = osmFileInfo.lastModified(); + + QString cmd = QString("SELECT val FROM meta WHERE key='osm-file-last-modified';"); + QByteArray cmd_bytes = cmd.toAscii(); + const char *ptr = cmd_bytes.data(); + + sqlite3_stmt *databaseStmt; + if ( sqlite3_prepare_v2(mDatabase, ptr, cmd_bytes.size(), &databaseStmt, 0) == SQLITE_OK ) + { + if (sqlite3_step(databaseStmt) == SQLITE_ROW) + { + QString oldOsmLastModifString = (const char *) sqlite3_column_text(databaseStmt,0); + QDateTime oldOsmFileLastModif = QDateTime::fromString(oldOsmLastModifString,DATE_TIME_FMT); + + if (mOsmFileLastModif == oldOsmFileLastModif) { + sqlite3_finalize(databaseStmt); + return true; + } + } + } + // destroy database statement + sqlite3_finalize(databaseStmt); + return false; +} + + +bool QgsOSMDataProvider::isDatabaseCompatibleWithPlugin() +{ + QString cmd = QString("SELECT val FROM meta WHERE key='osm-plugin-version';"); + QByteArray cmd_bytes = cmd.toAscii(); + const char *ptr = cmd_bytes.data(); + + sqlite3_stmt *databaseStmt; + if ( sqlite3_prepare_v2(mDatabase, ptr, cmd_bytes.size(), &databaseStmt, 0) == SQLITE_OK ) + { + if (sqlite3_step(databaseStmt) == SQLITE_ROW) + { + QString osmPluginVersion = (const char *) sqlite3_column_text(databaseStmt,0); + + if (osmPluginVersion == PLUGIN_VERSION) { + sqlite3_finalize(databaseStmt); + return true; + } + } + } + // destroy database statement + sqlite3_finalize(databaseStmt); + return false; +} + + +QString QgsOSMDataProvider::storageType() const +{ + // just return string reprezenting data storage type + return tr("Open Street Map format"); +} + + +void QgsOSMDataProvider::select(QgsAttributeList fetchAttributes, + QgsRectangle rect, + bool fetchGeometry, + bool useIntersect) +{ + // clear + delete mSelectionRectangleGeom; + if (mDatabaseStmt) + sqlite3_finalize(mDatabaseStmt); + + // store list of attributes to fetch, rectangle of area, geometry, etc. + mSelectionRectangle = rect; + mSelectionRectangleGeom = QgsGeometry::fromRect(rect); + mAttributesToFetch = fetchAttributes; + + // set flags + mFetchGeom = fetchGeometry; + mSelectUseIntersect = useIntersect; + + QString cmd; + // select data from sqlite3 database + if (mFeatureType == PointType) + { + // prepare select: get all the NODE identifiers from specified area -> future calls of nextFeature () will pick them up one-by-one + cmd = QString("SELECT id, lat, lon, timestamp, user FROM node WHERE usage=0 AND status<>'R' AND u=1"); + + if (!mSelectionRectangle.isEmpty()) + { + cmd += QString(" AND lat>=%1 AND lat<=%2 AND lon>=%3 AND lon<=%4;") + .arg(mSelectionRectangle.yMinimum(),0,'f',20) + .arg(mSelectionRectangle.yMaximum(),0,'f',20) + .arg(mSelectionRectangle.xMinimum(),0,'f',20) + .arg(mSelectionRectangle.xMaximum(),0,'f',20); + } + } + else if (mFeatureType == LineType) + { + cmd = "SELECT w.id, w.wkb, w.timestamp, w.user FROM way w WHERE w.closed=0 AND w.status<>'R' AND w.u=1"; + + if (!mSelectionRectangle.isEmpty()) + { + cmd += QString(" AND (((w.max_lat between %1 AND %2) OR (w.min_lat between %1 AND %2) OR (w.min_lat<%1 AND w.max_lat>%2)) OR ((w.max_lon between %3 AND %4) OR (w.min_lon between %3 AND %4) OR (w.min_lon<%3 AND w.max_lon>%4)));") + .arg(mSelectionRectangle.yMinimum(),0,'f',20) + .arg(mSelectionRectangle.yMaximum(),0,'f',20) + .arg(mSelectionRectangle.xMinimum(),0,'f',20) + .arg(mSelectionRectangle.xMaximum(),0,'f',20); + } + } + else if (mFeatureType == PolygonType) + { + cmd = "SELECT w.id, w.wkb, w.timestamp, w.user FROM way w WHERE w.closed=1 AND w.status<>'R' AND w.u=1"; + + if (!mSelectionRectangle.isEmpty()) + { + cmd += QString(" AND (((w.max_lat between %1 AND %2) OR (w.min_lat between %1 AND %2) OR (w.min_lat<%1 AND w.max_lat>%2)) OR ((w.max_lon between %3 AND %4) OR (w.min_lon between %3 AND %4) OR (w.min_lon<%3 AND w.max_lon>%4)));") + .arg(mSelectionRectangle.yMinimum(),0,'f',20) + .arg(mSelectionRectangle.yMaximum(),0,'f',20) + .arg(mSelectionRectangle.xMinimum(),0,'f',20) + .arg(mSelectionRectangle.xMaximum(),0,'f',20); + } + } + + // just conversion "cmd" to "const char*" + QByteArray cmd_bytes = cmd.toAscii(); + const char *ptr = cmd_bytes.data(); + + if ( sqlite3_prepare_v2(mDatabase, ptr, cmd_bytes.size(), &mDatabaseStmt, 0) != SQLITE_OK ) + { + QgsDebugMsg("Selecting object information failed."); + return; + } +QgsDebugMsg(QString("SELECTING FEATURES OF TYPE %1.").arg(mFeatureType)); +QgsDebugMsg(cmd); +} + + +int QgsOSMDataProvider::wayMemberCount(int wayId) +{ + // prepare select: get all the WAY members + QString cmd = QString("SELECT count(n.id) FROM way_member wm, node n WHERE wm.way_id=%1 AND wm.node_id=n.id AND wm.u=1 AND n.u=1;") + .arg(wayId); + + // just conversion "cmd" to "const char*" + QByteArray cmd_bytes = cmd.toAscii(); + const char *ptr = cmd_bytes.data(); + + sqlite3_stmt *databaseStmt; + if ( sqlite3_prepare_v2(mDatabase, ptr, cmd_bytes.size(), &databaseStmt, 0) != SQLITE_OK ) + { + QgsDebugMsg("Selecting way members failed."); + return -1; + } + + if (sqlite3_step(databaseStmt) != SQLITE_ROW) + { + QgsDebugMsg("Cannot find out way members count."); + return -1; + } + int wayMemberCnt = sqlite3_column_int(databaseStmt,0); + // destroy database statement + sqlite3_finalize(databaseStmt); + + return wayMemberCnt; +} + + +bool QgsOSMDataProvider::nextFeature(QgsFeature& feature) +{ + // load next requested feature from sqlite3 database + switch (sqlite3_step(mDatabaseStmt)) + { + case SQLITE_DONE: // no more features to return + feature.setValid(false); + return false; + + case SQLITE_ROW: // another feature to return + if (mFeatureType == PointType) + return fetchNode(feature, mDatabaseStmt, mFetchGeom, mAttributesToFetch); + else if (mFeatureType == LineType) + return fetchWay(feature, mDatabaseStmt, mFetchGeom, mAttributesToFetch); + else if (mFeatureType == PolygonType) + return fetchWay(feature, mDatabaseStmt, mFetchGeom, mAttributesToFetch); + + default: + if (mFeatureType == PointType) + QgsDebugMsg("Getting next feature of type failed."); + else if (mFeatureType == LineType) + QgsDebugMsg("Getting next feature of type failed."); + else if (mFeatureType == PolygonType) + QgsDebugMsg("Getting next feature of type failed."); + feature.setValid(false); + return false; + } +} + + +bool QgsOSMDataProvider::featureAtId( int featureId, + QgsFeature& feature, + bool fetchGeometry, + QgsAttributeList fetchAttributes) +{ +//QgsDebugMsg(QString("!!! Asking for feature:%1.").arg(featureId)); + + // load exact feature from sqlite3 database + if (mFeatureType == PointType) + { + sqlite3_bind_int(mNodeStmt,1,featureId); + + if (sqlite3_step(mNodeStmt) != SQLITE_ROW) { + QgsDebugMsg(QString("Getting information about point with id=%1 failed.").arg(featureId)); + sqlite3_reset(mNodeStmt); + return false; + } + + fetchNode(feature, mNodeStmt, fetchGeometry, fetchAttributes); + + // prepare statement for next call + sqlite3_reset(mNodeStmt); + } + else if ((mFeatureType == LineType) || (mFeatureType == PolygonType)) + { + sqlite3_bind_int(mWayStmt,1,featureId); + + if (sqlite3_step(mWayStmt) != SQLITE_ROW) { + QgsDebugMsg(QString("Getting information about way with id=%1 failed.").arg(featureId)); + sqlite3_reset(mWayStmt); + return false; + } + + fetchWay(feature, mWayStmt, fetchGeometry, fetchAttributes); + + // prepare statement for next call + sqlite3_reset(mWayStmt); + } + return true; +} + + +bool QgsOSMDataProvider::fetchNode(QgsFeature& feature, sqlite3_stmt* stmt, bool fetchGeometry, QgsAttributeList& fetchAttrs) +{ + int selId = sqlite3_column_int(stmt,0); + double selLat = sqlite3_column_double(stmt,1); + double selLon = sqlite3_column_double(stmt,2); + const char* selTimestamp = (const char*) sqlite3_column_text(stmt,3); + const char* selUser = (const char*) sqlite3_column_text(stmt,4); + + // fetch feature's geometry + if (fetchGeometry) + { + char* geo = new char[21]; + std::memset( geo, 0, 21 ); + geo[0] = QgsApplication::endian(); + geo[geo[0] == QgsApplication::NDR ? 1 : 4] = QGis::WKBPoint; + std::memcpy( geo + 5, &selLon, sizeof( double ) ); + std::memcpy( geo + 13, &selLat, sizeof( double ) ); + feature.setGeometryAndOwnership(( unsigned char * )geo, sizeof( wkbPoint ) ); + } + + // fetch attributes + QgsAttributeList::const_iterator iter; + for (iter = fetchAttrs.begin(); iter != fetchAttrs.end(); ++iter) + { + switch (*iter) + { + case TimestampAttr: + feature.addAttribute(TimestampAttr, QString::fromUtf8(selTimestamp) ); break; + case UserAttr: + feature.addAttribute(UserAttr, QString::fromUtf8(selUser) ); break; + case TagAttr: + if (mTagsRetrieval) + feature.addAttribute(TagAttr, tagsForObject("node",selId)); + else + feature.addAttribute(TagAttr, QString()); + break; + + default: // suppose it's a custom tag + if (*iter >= CustomTagAttr && *iter < CustomTagAttr + mCustomTagsList.count()) + { + feature.addAttribute(*iter, tagForObject("node",selId,mCustomTagsList[*iter-CustomTagAttr])); + } + } + } + + feature.setFeatureId(selId); + feature.setValid(true); + return true; +} + + +bool QgsOSMDataProvider::fetchWay(QgsFeature& feature, sqlite3_stmt* stmt, bool fetchGeometry, QgsAttributeList& fetchAttrs) +{ + int selId; + const char* selTimestamp; + const char* selUser; + int selWayMemberCount; + QgsGeometry *theGeometry = NULL; + bool fetchMoreRows = true; + int rc=-1; + + do + { + selId = sqlite3_column_int(stmt,0); + selTimestamp = (const char*) sqlite3_column_text(stmt,2); + selUser = (const char*) sqlite3_column_text(stmt,3); + unsigned char *pzBlob=0; + int pnBlob=0; + + if (fetchGeometry || mSelectUseIntersect || !mSelectionRectangle.isEmpty()) + { + pnBlob = sqlite3_column_bytes(stmt, 1); + pzBlob = new unsigned char[pnBlob]; + memcpy(pzBlob, sqlite3_column_blob(stmt, 1), pnBlob); + + // create geometry + theGeometry = new QgsGeometry(); + theGeometry->fromWkb((unsigned char *) pzBlob, pnBlob ); + } + + if (theGeometry && (theGeometry->type()==3) && (selId!=0)) + { + // line/polygon geometry is not cached! + char *geo; + int geolen; + updateWayWKB(selId,(mFeatureType==LineType) ? 0 : 1,&geo,&geolen); + theGeometry->fromWkb((unsigned char *) geo, (size_t) geolen ); + } + + if (mSelectUseIntersect) + { + // when using intersect, some features might be ignored if they don't intersect the selection rect + // intersect is a costly operation, use rectangle converted to geos for less conversions + // (this is usually used during identification of an object) + if (theGeometry->intersects(mSelectionRectangleGeom)) + fetchMoreRows=false; + } + else if (!mSelectionRectangle.isEmpty()) + { + // when using selection rectangle but without exact intersection, check only overlap of bounding box + // (usually used when drawing) + if (mSelectionRectangle.intersects(theGeometry->boundingBox())) + fetchMoreRows=false; + } + else + { + // no filter => always accept the new feature + // (used in attribute table) + fetchMoreRows = false; + } + + // delete the geometry (if any) in case we're not going to use it anyway + if (fetchMoreRows) + delete theGeometry; + } + while (fetchMoreRows && ((rc = sqlite3_step(stmt))==SQLITE_ROW)); + + // no more features to return + if (rc == SQLITE_DONE) + { + sqlite3_exec(mDatabase,"COMMIT;",0,0,0); + feature.setValid(false); + return false; + } + + // fetch feature's geometry + if (fetchGeometry) + { + feature.setGeometry( theGeometry ); + } + else + { + delete theGeometry; // make sure it's deleted + } + + // fetch attributes + QgsAttributeList::const_iterator iter; + for (iter = fetchAttrs.begin(); iter != fetchAttrs.end(); ++iter) + { + switch (*iter) + { + case TimestampAttr: + feature.addAttribute(TimestampAttr, QString::fromUtf8(selTimestamp) ); + break; + case UserAttr: + feature.addAttribute(UserAttr, QString::fromUtf8(selUser) ); + break; + case TagAttr: + if (mTagsRetrieval) + feature.addAttribute(TagAttr, tagsForObject("way",selId)); + else + feature.addAttribute(TagAttr, QString()); + break; + default: // suppose it's a custom tag + if (*iter >= CustomTagAttr && *iter < CustomTagAttr + mCustomTagsList.count()) + { + feature.addAttribute(*iter, tagForObject("way",selId,mCustomTagsList[*iter-CustomTagAttr])); + } + } + } + feature.setFeatureId(selId); + feature.setValid(true); + return true; +} + + +int QgsOSMDataProvider::relationMemberCount(int relId) +{ + const char *zSql = "select count(*) from relation_member rm, way_member wm, node n where rm.relation_id=? and rm.member_type='way' and rm.member_id=wm.way_id and wm.node_id=n.id and wm.u=1 and n.u=1 and rm.u=1;"; + sqlite3_stmt *pStmt; + int rc = sqlite3_prepare_v2(mDatabase, zSql, -1, &pStmt, 0); + + if ( rc!=SQLITE_OK ) { + QgsDebugMsg(QString("Failed (1).")); + return 0; + } + + sqlite3_bind_int(pStmt, 1, relId); + + rc = sqlite3_step(pStmt); + if (rc != SQLITE_ROW) { + QgsDebugMsg(QString("Failed (2).")); + return 0; + } + + int memberCnt = sqlite3_column_int(pStmt,0); + sqlite3_finalize(pStmt); + return memberCnt; +} + + +QString QgsOSMDataProvider::tagForObject(const char* type, int id, QString tagKey) +{ + sqlite3_bind_int(mCustomTagsStmt, 1, id); + sqlite3_bind_text(mCustomTagsStmt, 2, type, -1, 0); + QByteArray tag = tagKey.toUtf8(); // must keep the byte array until the query is run + sqlite3_bind_text(mCustomTagsStmt, 3, tag.data(), -1, 0); + + QString value; + int rc; + + if ((rc=sqlite3_step(mCustomTagsStmt)) == SQLITE_ROW) + { + const char* tagVal = (const char*) sqlite3_column_text(mCustomTagsStmt,0); + value = QString::fromUtf8(tagVal); + } + else + { + // tag wasn't found + //QgsDebugMsg(QString("tag for object failed (%1): type %2 id %3 tag %4").arg(rc).arg(type).arg(id).arg(tagKey)); + } + + sqlite3_reset(mCustomTagsStmt); // make ready for next retrieval + return value; +} + + +QString QgsOSMDataProvider::tagsForObject(const char* type, int id) +{ + sqlite3_bind_int(mTagsStmt, 1, id); + sqlite3_bind_text(mTagsStmt, 2, type, -1, 0); + + QString tags; + int rc; + + while ((rc = sqlite3_step(mTagsStmt)) == SQLITE_ROW) + { + const char* tagKey = (const char*) sqlite3_column_text(mTagsStmt,0); + const char* tagVal = (const char*) sqlite3_column_text(mTagsStmt,1); + QString key=QString::fromUtf8(tagKey); + QString val=QString::fromUtf8(tagVal); + + // we concatenate tags this way: "key1"="val1","key2"="val2","key3"="val3" + // -all ; in keyX and valX are replaced by ;; + // -all , in keyX and valX are replaced by ; + // -all - in keyX and valX are replaced by -- + // -all = in keyX and valX are replaced by - + key=key.replace(';',";;"); + val=val.replace(';',";;"); + key=key.replace(',',";"); + val=val.replace(',',";"); + + key=key.replace('-',"--"); + val=val.replace('-',"--"); + key=key.replace('=',"-"); + val=val.replace('=',"-"); + + if (tags.count() > 0) + tags += ","; + + tags += QString("\"%1\"=\"%2\"").arg(key).arg(val); + } + + if (rc != SQLITE_DONE) + { + // no tags for object + //QgsDebugMsg(QString("tags for object failed: type %1 id %2").arg(type).arg(id)); + } + + sqlite3_reset(mTagsStmt); // make ready for next retrieval + return tags; +} + + +QgsRectangle QgsOSMDataProvider::extent() +{ + return QgsRectangle(xMin,yMin,xMax,yMax); +} + + +QGis::WkbType QgsOSMDataProvider::geometryType() const +{ + if ( mFeatureType == PointType ) + return QGis::WKBPoint; + if ( mFeatureType == LineType ) + return QGis::WKBLineString; + if ( mFeatureType == PolygonType ) + return QGis::WKBPolygon; + + return QGis::WKBUnknown; +} + + +long QgsOSMDataProvider::featureCount() const +{ + sqlite3_stmt* countStmt; + long cnt=0; + + if (mFeatureType == PointType) + sqlite3_prepare_v2(mDatabase, "SELECT COUNT(*) FROM node where usage=0", -1, &countStmt, 0); + else if (mFeatureType == LineType) +// sqlite3_prepare_v2(mDatabase, "SELECT COUNT(*) FROM way WHERE closed=0", -1, &countStmt, 0); + sqlite3_prepare_v2(mDatabase, "SELECT count(*) FROM way w WHERE w.closed=0 AND w.status<>'R' AND w.u=1", -1, &countStmt, 0); + else if (mFeatureType == PolygonType) +// sqlite3_prepare_v2(mDatabase, "SELECT COUNT(*) FROM way WHERE closed=1", -1, &countStmt, 0); + sqlite3_prepare_v2(mDatabase, "SELECT count(*) FROM way w WHERE w.closed=1 AND w.status<>'R' AND w.u=1", -1, &countStmt, 0); + else return -1; + + if (sqlite3_step(countStmt) == SQLITE_ROW) + cnt = sqlite3_column_int(countStmt, 0); + + sqlite3_finalize(countStmt); +QgsDebugMsg(QString("Type:%1,FeatureCnt:%2").arg(mFeatureType).arg(cnt)); + return cnt; +} + + +uint QgsOSMDataProvider::fieldCount() const +{ + return mAttributeFields.size();; +} + + +bool QgsOSMDataProvider::isValid() +{ + return mValid; +} + + +QString QgsOSMDataProvider::name() const +{ + // return key representing this provider + return TEXT_PROVIDER_KEY; +} + + +QString QgsOSMDataProvider::description() const +{ + // return description of this provider + return TEXT_PROVIDER_DESCRIPTION; +} + + +QGISEXTERN QgsOSMDataProvider *classFactory(const QString *uri) +{ + return new QgsOSMDataProvider(*uri); +} + + +/** + * Required key function (used to map the plugin to a data store type) + */ +QGISEXTERN QString providerKey() +{ + // return key representing this provider + return TEXT_PROVIDER_KEY; +} + + +/** + * Required description function + */ +QGISEXTERN QString description() +{ + // just return simple one-line provider description + return TEXT_PROVIDER_DESCRIPTION; +} + + +/** + * Required isProvider function. Used to determine if this shared library + * is a data provider plugin + */ +QGISEXTERN bool isProvider() +{ + // just return positive answer + return true; +} + + +const QgsFieldMap & QgsOSMDataProvider::fields() const +{ + return mAttributeFields; +} + + +QgsCoordinateReferenceSystem QgsOSMDataProvider::crs() +{ + return QgsCoordinateReferenceSystem(); // use default CRS - it's WGS84 +} + + +void QgsOSMDataProvider::rewind() +{ + // we have to reset precompiled database statement; thanx to this action the first feature + // (returned by the query) will be selected again with the next calling of sqlite3_step(mDatabaseStmt) + if (mDatabaseStmt) + sqlite3_reset(mDatabaseStmt); +} + + +int QgsOSMDataProvider::freeFeatureId() +{ + // todo: optimalization - wouldn't be better to keep minimum id in meta table? + const char *zSql = "SELECT min(id) FROM (SELECT min(id) id FROM node \ + UNION SELECT min(id) id FROM way \ + UNION SELECT min(id) id FROM relation)"; + + sqlite3_stmt *pStmt; + int rc = sqlite3_prepare_v2(mDatabase, zSql, -1, &pStmt, 0); + + if ( rc!=SQLITE_OK ) { + QgsDebugMsg(QString("Getting pseudo id for new feature failed (1).")); + return 0; + } + + rc = sqlite3_step(pStmt); + if (rc != SQLITE_ROW) { + QgsDebugMsg(QString("Getting pseudo id for new feature failed (2).")); + return 0; + } + + int newFeatureId = sqlite3_column_int(pStmt,0)-1; + + // destroy database statement + sqlite3_finalize(pStmt); + return (newFeatureId >= 0) ? -1 : newFeatureId; +} + + +bool QgsOSMDataProvider::changeAttributeValues(const QgsChangedAttributesMap & attr_map) +{ + QgsDebugMsg(QString("In changeAttributeValues(...).")); + + // VERY VERY ugly hack to assign custom renderer for OSM layer + // but probably there's no simple way how to set our custom renderer from python plugin + if (attr_map.contains(0x12345678)) + { + const QgsAttributeMap& x = attr_map.value(0x12345678); + QgsVectorLayer* layer = (QgsVectorLayer*) x.value(0).toUInt(); + QgsDebugMsg("SETTING CUSTOM RENDERER!"); + layer->setRenderer( new OsmRenderer( layer->geometryType(), mStyleFileName ) ); + } + return true; +} + + +bool QgsOSMDataProvider::changeGeometryValue(const int & featid, QgsGeometry & geom) +{ + if ( mFeatureType == PointType ) + { + QgsDebugMsg(QString("Changing geometry of point with id=%1.").arg(featid)); + QgsPoint point = geom.asPoint(); + + const char *zSql = "UPDATE node SET lat=?, lon=?, status='U' WHERE id=? AND u=1"; + sqlite3_stmt *pStmt; + int rc; + + rc = sqlite3_prepare_v2(mDatabase, zSql, -1, &pStmt, 0); + if( rc!=SQLITE_OK ) + return rc; + + sqlite3_bind_double(pStmt, 1, point.y()); + sqlite3_bind_double(pStmt, 2, point.x()); + sqlite3_bind_int(pStmt, 3, featid); + + rc = sqlite3_step(pStmt); + rc = sqlite3_finalize(pStmt); + } + else if ( mFeatureType == LineType ) + { + QgsDebugMsg(QString("Changing geometry of way with id=%1.").arg(featid)); + QgsPolyline way = geom.asPolyline(); + unsigned char *wkb = geom.asWkb(); + + const char *zSql = "UPDATE way SET wkb=?, membercnt=?, status='U' WHERE id=? AND u=1"; + sqlite3_stmt *pStmt; + int rc; + + rc = sqlite3_prepare_v2(mDatabase, zSql, -1, &pStmt, 0); + if( rc!=SQLITE_OK ) + return rc; + + sqlite3_bind_blob(pStmt, 1, wkb, 9 + 16 * way.size(), SQLITE_STATIC); + sqlite3_bind_int(pStmt, 2, way.size()); + sqlite3_bind_int(pStmt, 3, featid); + + rc = sqlite3_step(pStmt); + rc = sqlite3_finalize(pStmt); + + } + return true; +} + + +int QgsOSMDataProvider::capabilities() const +{ + return QgsVectorDataProvider::SelectAtId | QgsVectorDataProvider::SelectGeometryAtId; +} + + +bool QgsOSMDataProvider::updateWayWKB(int wayId, int isClosed, char **geo, int *geolen) +{ + sqlite3_stmt *stmtSelectMembers; + char sqlSelectMembers[]="SELECT n.lat, n.lon, n.id FROM way_member wm, node n WHERE wm.way_id=? AND wm.node_id=n.id AND n.u=1 AND wm.u=1 ORDER BY wm.pos_id ASC;"; + if ( sqlite3_prepare_v2(mDatabase, sqlSelectMembers, sizeof(sqlSelectMembers), &stmtSelectMembers, 0) != SQLITE_OK ) + { + QgsDebugMsg("Failed to prepare sqlSelectMembers!!!"); + return false; + } + + sqlite3_stmt *stmtUpdateWay; + char sqlUpdateWay[]="UPDATE way SET wkb=?, membercnt=?, min_lat=?, min_lon=?, max_lat=?, max_lon=? WHERE id=? AND u=1"; + if ( sqlite3_prepare_v2(mDatabase, sqlUpdateWay, sizeof(sqlUpdateWay), &stmtUpdateWay, 0) != SQLITE_OK ) + { + QgsDebugMsg("Failed to prepare sqlUpdateWay!!!"); + return false; + } + + // create wkb for selected way (if way is closed then it's polygon and it's geometry is different) + int memberCnt = wayMemberCount(wayId); + if (memberCnt == -1) + return false; + + double minLat = 1000.0, minLon = 1000.0; + double maxLat = -1000.0, maxLon = -1000.0; + + if (!isClosed) + { + (*geo)=new char[9 + 16 * memberCnt]; + (*geolen)=9 + 16 * memberCnt; + + std::memset( (*geo), 0, 9 + 16 * memberCnt ); + + (*geo)[0]=QgsApplication::endian(); + (*geo)[(*geo)[0] == QgsApplication::NDR ? 1 : 4] = QGis::WKBLineString; + std::memcpy( (*geo) + 5, &memberCnt, 4 ); + + sqlite3_bind_int(stmtSelectMembers, 1, wayId); + + int step_result; + int i=0; + while ((step_result = sqlite3_step(stmtSelectMembers)) != SQLITE_DONE) + { + if (step_result != SQLITE_ROW) + { + QgsDebugMsg(QString("Selecting members of way %1 failed.").arg(wayId)); + break; + } + + double selLat = sqlite3_column_double(stmtSelectMembers,0); + double selLon = sqlite3_column_double(stmtSelectMembers,1); + int selNodeId = sqlite3_column_int(stmtSelectMembers,2); + + if (selLatmaxLat) maxLat = selLat; + if (selLon>maxLon) maxLon = selLon; + + std::memcpy( (*geo) + 9 + 16 * i, &selLon, sizeof( double ) ); + std::memcpy( (*geo) + 9 + 16 * i + 8, &selLat, sizeof( double ) ); + i++; + } + + sqlite3_bind_blob(stmtUpdateWay, 1, (*geo), 9 + 16 * memberCnt, SQLITE_TRANSIENT); + } + else + { + // it's a polygon + int ringsCnt = 1; + memberCnt++; + (*geo) = new char[13 + 16 * memberCnt]; + (*geolen)=13 + 16 * memberCnt; + std::memset( (*geo), 0, 13 + 16 * memberCnt ); + (*geo)[0] = QgsApplication::endian(); + (*geo)[(*geo)[0] == QgsApplication::NDR ? 1 : 4] = QGis::WKBPolygon; + std::memcpy( (*geo) + 5, &ringsCnt, 4 ); + std::memcpy( (*geo) + 9, &memberCnt, 4 ); + + sqlite3_bind_int(stmtSelectMembers, 1, wayId); + + int step_result; + int i=0; + double firstLat=-1000.0; + double firstLon=-1000.0; + while ((step_result = sqlite3_step(stmtSelectMembers)) != SQLITE_DONE) + { + if (step_result != SQLITE_ROW) + { + QgsDebugMsg(QString("Selecting members of polygon %1 failed.").arg(wayId)); + break; + } + + double selLat = sqlite3_column_double(stmtSelectMembers,0); + double selLon = sqlite3_column_double(stmtSelectMembers,1); + int selNodeId = sqlite3_column_int(stmtSelectMembers,2); + + if (selLatmaxLat) maxLat = selLat; + if (selLon>maxLon) maxLon = selLon; + + std::memcpy( (*geo) + 13 + 16 * i, &selLon, sizeof( double ) ); + std::memcpy( (*geo) + 13 + 16 * i + 8, &selLat, sizeof( double ) ); + + if (firstLat==-1000.0) + firstLat=selLat; + if (firstLon==-1000.0) + firstLon=selLon; + i++; + } + // add last polygon line + std::memcpy( (*geo) + 13 + 16 * i, &firstLon, sizeof( double ) ); + std::memcpy( (*geo) + 13 + 16 * i + 8, &firstLat, sizeof( double ) ); + + sqlite3_bind_blob(stmtUpdateWay, 1, (*geo), 13 + 16 * memberCnt, SQLITE_TRANSIENT); + } + + sqlite3_reset(stmtSelectMembers); + + // now update way record + sqlite3_bind_int(stmtUpdateWay, 2, memberCnt); + sqlite3_bind_double(stmtUpdateWay, 3, minLat); + sqlite3_bind_double(stmtUpdateWay, 4, minLon); + sqlite3_bind_double(stmtUpdateWay, 5, maxLat); + sqlite3_bind_double(stmtUpdateWay, 6, maxLon); + sqlite3_bind_int(stmtUpdateWay, 7, wayId); + + if (sqlite3_step(stmtUpdateWay) != SQLITE_DONE) + { + QgsDebugMsg(QString("Updating way with id=%1 failed.").arg(wayId)); + return false; + } + sqlite3_reset(stmtUpdateWay); + + // destroy database statements + sqlite3_finalize(stmtSelectMembers); + sqlite3_finalize(stmtUpdateWay); + return true; +} + + +bool QgsOSMDataProvider::updateNodes() +{ + char sqlUpdateNodes[]="update node set usage=(select count(distinct way_id) from way_member wm where wm.node_id=id);"; + if ( sqlite3_exec(mDatabase,sqlUpdateNodes,0,0,0) != SQLITE_OK ) + { + QgsDebugMsg("Failed to update node table!!!"); + return false; + } + return true; +} + + +bool QgsOSMDataProvider::removeIncorrectWays() +{ + sqlite3_exec(mDatabase,"BEGIN;",0,0,0); + int wayId; + + char sqlRemoveWay[]="delete from way where id=?"; + sqlite3_stmt *stmtRemoveWay; + if ( sqlite3_prepare_v2(mDatabase, sqlRemoveWay, sizeof(sqlRemoveWay), &stmtRemoveWay, 0) != SQLITE_OK ) + { + QgsDebugMsg("failed to prepare stmtRemoveWay!!!"); + } + + char sqlRemoveWayMembers[]="delete from way_member where way_id=?"; + sqlite3_stmt *stmtRemoveWayMembers; + if ( sqlite3_prepare_v2(mDatabase, sqlRemoveWayMembers, sizeof(sqlRemoveWayMembers), &stmtRemoveWayMembers, 0) != SQLITE_OK ) + { + QgsDebugMsg("failed to prepare stmtRemoveWayMembers!!!"); + } + + char sqlRemoveWayTags[]="delete from tag where object_id=? and object_type='way'"; + sqlite3_stmt *stmtRemoveWayTags; + if ( sqlite3_prepare_v2(mDatabase, sqlRemoveWayTags, sizeof(sqlRemoveWayTags), &stmtRemoveWayTags, 0) != SQLITE_OK ) + { + QgsDebugMsg("failed to prepare stmtRemoveWayTags!!!"); + } + + char sqlSelectWays[]="select distinct way_id wid from way_member wm where not exists(select 1 from node n where wm.node_id=n.id);"; + sqlite3_stmt *stmtSelectWays; + if ( sqlite3_prepare_v2(mDatabase, sqlSelectWays, sizeof(sqlSelectWays), &stmtSelectWays, 0) != SQLITE_OK ) + { + QgsDebugMsg("failed to prepare stmtSelectWays!!!"); + } + + int i=0; + while (sqlite3_step(stmtSelectWays)==SQLITE_ROW) + { + // remove both way, tag records, way_member records + wayId = sqlite3_column_int(stmtSelectWays,0); + + sqlite3_bind_int(stmtRemoveWay,1,wayId); + sqlite3_bind_int(stmtRemoveWayMembers,1,wayId); + sqlite3_bind_int(stmtRemoveWayTags,1,wayId); + + // run steps + if (sqlite3_step(stmtRemoveWay)!=SQLITE_DONE) + { + QgsDebugMsg("Removing way failed."); + sqlite3_exec(mDatabase,"ROLLBACK;",0,0,0); + return false; + } + if (sqlite3_step(stmtRemoveWayMembers)!=SQLITE_DONE) + { + QgsDebugMsg("Removing way members failed."); + sqlite3_exec(mDatabase,"ROLLBACK;",0,0,0); + return false; + } + if (sqlite3_step(stmtRemoveWayTags)!=SQLITE_DONE) + { + QgsDebugMsg("Removing way tags failed."); + sqlite3_exec(mDatabase,"ROLLBACK;",0,0,0); + return false; + } + + // make statements ready for the next run + sqlite3_reset(stmtRemoveWay); + sqlite3_reset(stmtRemoveWayMembers); + sqlite3_reset(stmtRemoveWayTags); + i++; + } + // destroy database statements + sqlite3_finalize(stmtRemoveWay); + sqlite3_finalize(stmtRemoveWayMembers); + sqlite3_finalize(stmtRemoveWayTags); + sqlite3_finalize(stmtSelectWays); + + // commit actions + sqlite3_exec(mDatabase,"COMMIT;",0,0,0); +} + + +bool QgsOSMDataProvider::postparsing() +{ + if (mInitObserver) mInitObserver->setProperty("osm_status", QVariant("Post-parsing: Nodes.")); + if (mInitObserver) mInitObserver->setProperty("osm_max", QVariant(3)); + if (mInitObserver) mInitObserver->setProperty("osm_value", QVariant(0)); + + // update node table + updateNodes(); + + if (mInitObserver) mInitObserver->setProperty("osm_status", QVariant("Post-parsing: Removing incorrect ways.")); + if (mInitObserver) mInitObserver->setProperty("osm_max", QVariant(3)); + if (mInitObserver) mInitObserver->setProperty("osm_value", QVariant(1)); + + removeIncorrectWays(); + + if (mInitObserver) mInitObserver->setProperty("osm_status", QVariant("Post-parsing: Caching ways geometries.")); + if (mInitObserver) mInitObserver->setProperty("osm_max", QVariant(3)); + if (mInitObserver) mInitObserver->setProperty("osm_value", QVariant(2)); + + // select ways, for each of them compute its wkb and store it into database + sqlite3_exec(mDatabase,"BEGIN;",0,0,0); + + int wayId, isClosed; + // prepare select: get information about one specified point + QString cmd = QString("SELECT id, closed FROM way;"); + QByteArray cmd_bytes = cmd.toAscii(); + const char *ptr = cmd_bytes.data(); + + sqlite3_stmt *databaseStmt; + if ( sqlite3_prepare_v2(mDatabase, ptr, cmd_bytes.size(), &databaseStmt, 0) != SQLITE_OK ) + { + QgsDebugMsg(QString("Creating BLOBs in postprocessing failed.")); + sqlite3_exec(mDatabase,"ROLLBACK;",0,0,0); + return false; + } + + while (sqlite3_step(databaseStmt) == SQLITE_ROW) + { + if ((mInitObserver) && (mInitObserver->property("osm_stop_parsing").toInt()==1)) + { + QgsDebugMsg(QString("Loading the OSM data was stopped.")); + sqlite3_exec(mDatabase,"ROLLBACK;",0,0,0); + return false; + } + wayId = sqlite3_column_int(databaseStmt,0); + isClosed = sqlite3_column_int(databaseStmt,1); + char *geo; + int geolen; + updateWayWKB(wayId,isClosed,&geo,&geolen); // todo: return value! + } + + // destroy database statements + sqlite3_finalize(databaseStmt); + + // commit our actions + sqlite3_exec(mDatabase,"COMMIT;",0,0,0); + + if (mInitObserver) mInitObserver->setProperty("osm_max", QVariant(3)); + if (mInitObserver) mInitObserver->setProperty("osm_value", QVariant(3)); + + return true; +} + + +bool QgsOSMDataProvider::loadOsmFile(QString osm_filename) +{ + QFile f(osm_filename); + if (!f.exists()) + return false; + + if (mInitObserver) mInitObserver->setProperty("osm_status", QVariant("Parsing the OSM file.")); + + OsmHandler *handler = new OsmHandler(&f,mDatabase); + QXmlSimpleReader reader; + reader.setContentHandler(handler); + + const int sectorSize = 8192; + int cntSectors = f.size()/sectorSize; + if (mInitObserver) mInitObserver->setProperty("osm_max", QVariant(cntSectors)); + + if (!f.open(QIODevice::ReadOnly)) + { + QgsDebugMsg("Unable to open the OSM file!"); + return false; + } + + QXmlInputSource source; + source.setData(f.read(sectorSize)); + int sector = 1; + + QgsDebugMsg(QString("Parsing file: %1").arg(osm_filename)); + bool res = reader.parse(&source, true); + while (!f.atEnd()) + { + if ((mInitObserver) && (mInitObserver->property("osm_stop_parsing").toInt()==1)) + { + QgsDebugMsg(QString("Parsing the OSM XML was stopped.")); + sqlite3_exec(mDatabase,"ROLLBACK;",0,0,0); + return false; + } + if ((!res) && (sectorsetProperty("osm_failure", QVariant(handler->errorString())); + return false; + } + // parsing process can continue + source.setData(f.read(sectorSize)); + if (mInitObserver) mInitObserver->setProperty("osm_value", QVariant(++sector)); + res = reader.parseContinue(); + } + f.close(); + + QgsDebugMsg("Parsing complete. Result: " + QString::number(res)); + + QgsDebugMsg("Creating indexes..."); + if (mInitObserver) mInitObserver->setProperty("osm_status", QVariant("Creating indexes.")); + createIndexes(); + + sqlite3_exec(mDatabase,"COMMIT;",0,0,0); + + QgsDebugMsg("Starting postprocessing..."); + if ((mInitObserver) && (mInitObserver->property("osm_stop_parsing").toInt()==1)) + { + QgsDebugMsg(QString("Loading the OSM data was stopped.")); + sqlite3_exec(mDatabase,"ROLLBACK;",0,0,0); + return false; + } + +// if (mInitObserver) mInitObserver->setProperty("osm_status", QVariant("Running post-parsing actions.")); + postparsing(); + QgsDebugMsg("Postprocessing complete."); + + QgsDebugMsg("Creating triggers..."); + if (mInitObserver) mInitObserver->setProperty("osm_status", QVariant("Creating triggers.")); + createTriggers(); + + if (mInitObserver) mInitObserver->setProperty("osm_done", QVariant(true)); + + // storing osm file last modified information into database + + QFile osmFile(mFileName); + QFileInfo osmFileInfo(osmFile); + + QString cmd = "INSERT INTO meta ( key, val ) VALUES ('osm-file-last-modified','"+osmFileInfo.lastModified().toString(DATE_TIME_FMT)+"');"; + QByteArray cmd_bytes = cmd.toAscii(); + const char *ptr = cmd_bytes.data(); + + if ( sqlite3_exec(mDatabase,ptr,0,0,0) != SQLITE_OK ) + { + cout << "Storing osm-file-last-modified info into database failed." << endl; + // its not fatal situation, lets continue.. + } + + QString cmd2 = "INSERT INTO meta ( key, val ) VALUES ('osm-plugin-version','"+PLUGIN_VERSION+"');"; + QByteArray cmd_bytes2 = cmd2.toAscii(); + const char *ptr2 = cmd_bytes2.data(); + + if ( sqlite3_exec(mDatabase,ptr2,0,0,0) != SQLITE_OK ) + { + cout << "Storing osm-plugin-version info into database failed." << endl; + return false; + } + + // store information got with handler into provider member variables + xMin = handler->xMin; // boundaries defining the area of all features + xMax = handler->xMax; + yMin = handler->yMin; + yMax = handler->yMax; + + // storing boundary information into database + QString cmd3 = QString("INSERT INTO meta ( key, val ) VALUES ('default-area-boundaries','%1-%2-%3-%4');") + .arg(xMin,0,'f',20).arg(yMin,0,'f',20).arg(xMax,0,'f',20).arg(yMax,0,'f',20); + QByteArray cmd_bytes3 = cmd3.toAscii(); + const char *ptr3 = cmd_bytes3.data(); + + if ( sqlite3_exec(mDatabase,ptr3,0,0,0) != SQLITE_OK ) + { + cout << "Storing default area boundaries information into database failed." << endl; + // its not critical situation + } + + sqlite3_exec(mDatabase,"COMMIT;",0,0,0); + + if ((mInitObserver) && (mInitObserver->property("osm_stop_parsing").toInt()==1)) + { + QgsDebugMsg(QString("Loading the OSM data was stopped.")); + sqlite3_exec(mDatabase,"ROLLBACK;",0,0,0); + return false; + } + sqlite3_exec(mDatabase,"COMMIT;",0,0,0); + return true; +} + + +bool QgsOSMDataProvider::createDatabaseSchema() +{ + QgsDebugMsg("Creating database schema for OSM..."); + + const char* createTables[] = { + "CREATE TABLE node ( i INTEGER PRIMARY KEY, u INTEGER DEFAULT 1, id INTEGER, lat REAL, lon REAL, timestamp VARCHAR2, user VARCHAR2, usage INTEGER DEFAULT 0, status VARCHAR2 DEFAULT 'N' );", + "CREATE TABLE way ( i INTEGER PRIMARY KEY, u INTEGER DEFAULT 1, id INTEGER, wkb BLOB, timestamp VARCHAR2, user VARCHAR2, membercnt INTEGER DEFAULT 0, closed INTEGER, min_lat REAL, min_lon REAL, max_lat REAL, max_lon REAL, status VARCHAR2 DEFAULT 'N' );", + "CREATE TABLE relation ( i INTEGER PRIMARY KEY, u INTEGER DEFAULT 1, id INTEGER, type VARCHAR2, timestamp VARCHAR2, user VARCHAR2, status VARCHAR2 DEFAULT 'N' );", + + "CREATE TABLE way_member ( i INTEGER PRIMARY KEY, u INTEGER DEFAULT 1, way_id INTEGER, pos_id INTEGER, node_id INTEGER );", + "CREATE TABLE relation_member ( i INTEGER PRIMARY KEY, u INTEGER DEFAULT 1, relation_id INTEGER, pos_id INTEGER, member_id INTEGER, member_type VARCHAR2, role VARCHAR2 );", + + "CREATE TABLE tag ( i INTEGER PRIMARY KEY, u INTEGER DEFAULT 1, object_id INTEGER, object_type VARCHAR2, key VARCHAR2, val VARCHAR2 );", + "CREATE TABLE meta ( key VARCHAR2, val VARCHAR2, PRIMARY KEY (key,val) );", + + // OSM 0.6 API requires storing version_id to each feature -> adding table version + "CREATE TABLE version ( object_id INTEGER, object_type VARCHAR2, version_id INTEGER, PRIMARY KEY (object_id, object_type) );", + "CREATE TABLE change_step ( change_id INTEGER PRIMARY KEY, change_type VARCHAR2, tab_name VARCHAR2, row_id INTEGER, col_name VARCHAR2, old_value VARCHAR2, new_value VARCHAR2 );" + }; + + int count = sizeof(createTables) / sizeof(const char*); + + for (int i = 0; i < count; i++) + { + if ( sqlite3_exec(mDatabase, createTables[i], 0,0,&mError) != SQLITE_OK ) + { + QgsDebugMsg(QString("Creating table \"%1\"").arg(QString::fromUtf8(createTables[i]))); + return false; + } + } + // database schema created successfully + QgsDebugMsg("Database schema for OSM was created successfully."); + return true; +} + + +bool QgsOSMDataProvider::createIndexes() +{ + const char* indexes[] = { + "CREATE INDEX IF NOT EXISTS main.ix_node_id ON node ( id );", + "CREATE INDEX IF NOT EXISTS main.ix_node_us ON node ( usage,status );", + "CREATE INDEX IF NOT EXISTS main.ix_way_id ON way ( id );", + "CREATE INDEX IF NOT EXISTS main.ix_way_cs ON way ( closed,status );", + "CREATE INDEX IF NOT EXISTS main.ix_wm_wid ON way_member ( way_id );", + "CREATE INDEX IF NOT EXISTS main.ix_wm_nid ON way_member ( node_id );", + "CREATE INDEX IF NOT EXISTS main.ix_rm_rid ON relation_member ( relation_id );", + "CREATE INDEX IF NOT EXISTS main.ix_tag_id_type ON tag ( object_id ASC, object_type ASC );", + "CREATE INDEX IF NOT EXISTS main.ix_version_id_type ON version ( object_id, object_type );" + }; + int count = sizeof(indexes) / sizeof(const char*); + + if (mInitObserver) mInitObserver->setProperty("osm_max", QVariant(count)); + + for (int i = 0; i < count; i++) + { + if ( sqlite3_exec(mDatabase, indexes[i],0,0,&mError) != SQLITE_OK ) + { + QgsDebugMsg(QString("Creating index \"%1\" failed.").arg(QString::fromUtf8(indexes[i]))); + // absence of index shouldn't be critical for this application -> but everything will be slow + } + if (mInitObserver) mInitObserver->setProperty("osm_value", QVariant(i+1)); + } + return true; +} + + +bool QgsOSMDataProvider::createTriggers() +{ + const char* triggers[] = { + // tag table + "create trigger if not exists main.trg_tag_oi_update after update of object_id on tag begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','tag',old.i,'object_id',old.object_id,new.object_id); end;", + + "create trigger if not exists main.trg_tag_ot_update after update of object_type on tag begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','tag',old.i,'object_type',old.object_type,new.object_type); end;", + + "create trigger if not exists main.trg_tag_k_update after update of key on tag begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','tag',old.i,'key',old.key,new.key); end;", + + "create trigger if not exists main.trg_tag_v_update after update of val on tag begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','tag',old.i,'val',old.val,new.val); end;", + + "create trigger if not exists main.trg_tag_insert after insert on tag begin insert into change_step (change_type,tab_name,row_id) values ('I','tag',new.i); end;", + + "create trigger if not exists main.trg_tag_delete before delete on tag begin insert into change_step (change_type,tab_name,row_id) values ('D','tag',old.i); update tag set u=0 where i=old.i; select raise(ignore); end;", + + // node table + "create trigger if not exists main.trg_node_lat_update after update of lat on node begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','node',old.i,'lat',old.lat,new.lat); end;", + + "create trigger if not exists main.trg_node_lon_update after update of lon on node begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','node',old.i,'lon',old.lon,new.lon); end;", + + "create trigger if not exists main.trg_node_t_update after update of timestamp on node begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','node',old.i,'timestamp',old.timestamp,new.timestamp); end;", + + "create trigger if not exists main.trg_node_use_update after update of user on node begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','node',old.i,'user',old.user,new.user); end;", + + "create trigger if not exists main.trg_node_usa_update after update of usage on node begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','node',old.i,'usage',old.usage,new.usage); end;", + + "create trigger if not exists main.trg_node_s_update after update of status on node begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','node',old.i,'status',old.status,new.status); end;", + + "create trigger if not exists main.trg_node_insert after insert on node begin insert into change_step (change_type,tab_name,row_id) values ('I','node',new.i); end;", + + "create trigger if not exists main.trg_node_delete before delete on node begin insert into change_step (change_type,tab_name,row_id) values ('D','node',old.i); update node set u=0 where i=old.i; select raise(ignore); end;", + + // way table + "create trigger if not exists main.trg_way_t_update after update of timestamp on way begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','way',old.i,'timestamp',old.timestamp,new.timestamp); end;", + + "create trigger if not exists main.trg_way_u_update after update of user on way begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','way',old.i,'user',old.user,new.user); end;", + + "create trigger if not exists main.trg_way_c_update after update of closed on way begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','way',old.i,'closed',old.closed,new.closed); end;", + + "create trigger if not exists main.trg_way_s_update after update of status on way begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','way',old.i,'status',old.status,new.status); end;", + + "create trigger if not exists main.trg_way_w_update after update of wkb on way begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','way',old.i,'wkb','',''); end;", + + "create trigger if not exists main.trg_way_insert after insert on way begin insert into change_step (change_type,tab_name,row_id) values ('I','way',new.i); end;", + + "create trigger if not exists main.trg_way_delete before delete on way begin insert into change_step (change_type,tab_name,row_id) values ('D','way',old.i); update way set u=0 where i=old.i; select raise(ignore); end;", + + // relation table + "create trigger if not exists main.trg_relation_ty_update after update of type on relation begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','relation',old.i,'type',old.type,new.type); end;", + + "create trigger if not exists main.trg_relation_ti_update after update of timestamp on relation begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','relation',old.i,'timestamp',old.timestamp,new.timestamp); end;", + + "create trigger if not exists main.trg_relation_u_update after update of user on relation begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','relation',old.i,'user',old.user,new.user); end;", + + "create trigger if not exists main.trg_relation_s_update after update of status on relation begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','relation',old.i,'status',old.status,new.status); end;", + + "create trigger if not exists main.trg_relation_insert after insert on relation begin insert into change_step (change_type,tab_name,row_id) values ('I','relation',new.i); end;", + + "create trigger if not exists main.trg_relation_delete before delete on relation begin insert into change_step (change_type,tab_name,row_id) values ('D','relation',old.i); update relation set u=0 where i=old.i; select raise(ignore); end;", + + // way_member table + "create trigger if not exists main.trg_way_member_wi_update after update of way_id on way_member begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','way_member',old.i,'way_id',old.way_id,new.way_id); end;", + + "create trigger if not exists main.trg_way_member_pi_update after update of pos_id on way_member begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','way_member',old.i,'pos_id',old.pos_id,new.pos_id); end;", + + "create trigger if not exists main.trg_way_member_ni_update after update of node_id on way_member begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','way_member',old.i,'node_id',old.node_id,new.node_id); end;", + + "create trigger if not exists main.trg_way_member_insert after insert on way_member begin insert into change_step (change_type,tab_name,row_id) values ('I','way_member',new.i); end;", + + "create trigger if not exists main.trg_way_member_delete before delete on way_member begin insert into change_step (change_type,tab_name,row_id) values ('D','way_member',old.i); update way_member set u=0 where i=old.i; select raise(ignore); end;", + + // relation_member table + "create trigger if not exists main.trg_relation_member_ri_update after update of relation_id on relation_member begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','relation_member',old.i,'relation_id',old.relation_id,new.relation_id); end;", + + "create trigger if not exists main.trg_relation_member_pi_update after update of pos_id on relation_member begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','relation_member',old.i,'pos_id',old.pos_id,new.pos_id); end;", + + "create trigger if not exists main.trg_relation_member_mi_update after update of member_id on relation_member begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','relation_member',old.i,'member_id',old.member_id,new.member_id); end;", + + "create trigger if not exists main.trg_relation_member_mt_update after update of member_type on relation_member begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','relation_member',old.i,'member_type',old.member_type,new.member_type); end;", + + "create trigger if not exists main.trg_relation_member_r_update after update of role on relation_member begin insert into change_step (change_type,tab_name,row_id,col_name,old_value,new_value) values ('U','relation_member',old.i,'role',old.role,new.role); end;", + + "create trigger if not exists main.trg_relation_member_insert after insert on relation_member begin insert into change_step (change_type,tab_name,row_id) values ('I','relation_member',new.i); end;", + + "create trigger if not exists main.trg_relation_member_delete before delete on relation_member begin insert into change_step (change_type,tab_name,row_id) values ('D','relation_member',old.i); update relation_member set u=0 where i=old.i; select raise(ignore); end;" + }; + + int count = sizeof(triggers) / sizeof(const char*); + + if (mInitObserver) mInitObserver->setProperty("osm_max", QVariant(count)); + + for (int i = 0; i < count; i++) + { + if ( sqlite3_exec(mDatabase, triggers[i],0,0,&mError) != SQLITE_OK ) + { + QgsDebugMsg(QString("Creating trigger \"%1\" failed.").arg(QString::fromUtf8(triggers[i]))); + return false; + } + if (mInitObserver) mInitObserver->setProperty("osm_value", QVariant(i+1)); + } + return true; +} + + +bool QgsOSMDataProvider::dropDatabaseSchema() +{ + QgsDebugMsg("Dropping database schema for OSM..."); + + // dropping database schema -> failures of individual droppings are not fatal for the whole process; + // the more object we will drop, the happies we will be ;) + + const char* drops[] = { + // dropping indexes + "DROP INDEX IF EXISTS main.ix_node_id;", + "DROP INDEX IF EXISTS main.ix_node_us;", // selecting nodes + "DROP INDEX IF EXISTS main.ix_way_id;", + "DROP INDEX IF EXISTS main.ix_way_cs;", // selecting ways + "DROP INDEX IF EXISTS main.ix_wm_wid;", + "DROP INDEX IF EXISTS main.ix_wm_nid;", + "DROP INDEX IF EXISTS main.ix_rm_rid;", + "DROP INDEX IF EXISTS main.ix_tag_id_type;", + "DROP INDEX IF EXISTS main.ix_version_id_type;", + + // dropping base tables + "DROP TABLE node;", + "DROP TABLE way;", + "DROP TABLE relation;", + "DROP TABLE way_member;", + "DROP TABLE relation_member;", + "DROP TABLE tag;", + "DROP TABLE meta;", + + "DROP TABLE version;", + "DROP TABLE change_step;" + }; + int count = sizeof(drops) / sizeof(const char*); + + for (int i = 0; i < count; i++) + { + if ( sqlite3_exec(mDatabase, drops[i], 0,0,&mError) != SQLITE_OK ) + { + QgsDebugMsg(QString("Dropping table \"%1\" failed.").arg(QString::fromUtf8(drops[i]))); + } + } + + // database schema droped successfully + QgsDebugMsg("Database schema for OSM dropped."); + return true; +} + + +bool QgsOSMDataProvider::openDatabase() +{ + QgsDebugMsg("Opening database."); + + QByteArray dbfn_bytes = mDatabaseFileName.toAscii(); + const char *ptr = dbfn_bytes.data(); + + // open database + if ( sqlite3_open(ptr, &mDatabase) != SQLITE_OK ) + { + mError = (char *) "Opening SQLite3 database failed."; + sqlite3_close(mDatabase); + return false; + } + return true; +}; + + +bool QgsOSMDataProvider::closeDatabase() +{ + QgsDebugMsg("Closing sqlite3 database."); + + // close database + if ( sqlite3_close(mDatabase) != SQLITE_OK ) + { + mError = (char *) "Closing SQLite3 database failed."; + return false; + } + return true; +}; + + diff --git a/src/providers/osm/osmprovider.h b/src/providers/osm/osmprovider.h new file mode 100644 index 00000000000..ca88d8f3752 --- /dev/null +++ b/src/providers/osm/osmprovider.h @@ -0,0 +1,302 @@ +/*************************************************************************** + osmprovider.h - provider for OSM; stores OSM data in sqlite3 DB + ------------------ + begin : October 2008 + copyright : (C) 2008 by Lukas Berka + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "qgsvectordataprovider.h" + +#include +#include +#include + + + + +typedef QMap QgsFeatureMap; + + + +class QgsOSMDataProvider: public QgsVectorDataProvider +{ +public: + + /** + * Constructor of the vector provider. + * @param uri uniform resource locator (URI) for a dataset + */ + QgsOSMDataProvider(QString uri); + + /** + * Destructor. + */ + virtual ~QgsOSMDataProvider(); + + + // Implementation of functions from QgsVectorDataProvider + + /** + * Returns the permanent storage type for this layer as a friendly name. + */ + virtual QString storageType() const; + + /** Select features based on a bounding rectangle. Features can be retrieved with calls to getNextFeature. + * @param fetchAttributes list of attributes which should be fetched + * @param rect spatial filter + * @param fetchGeometry true if the feature geometry should be fetched + * @param useIntersect true if an accurate intersection test should be used, + * false if a test based on bounding box is sufficient + */ + virtual void select(QgsAttributeList fetchAttributes = QgsAttributeList(), + QgsRectangle rect = QgsRectangle(), + bool fetchGeometry = true, + bool useIntersect = false); + + /** + * Get the next feature resulting from a select operation. + * @param feature feature which will receive data from the provider + * @return true when there was a feature to fetch, false when end was hit + */ + virtual bool nextFeature(QgsFeature& feature); + + /** + * Gets the feature at the given feature ID. + * @param featureId id of the feature + * @param feature feature which will receive the data + * @param fetchGeometry if true, geometry will be fetched from the provider + * @param fetchAttributes a list containing the indexes of the attribute fields to copy + * @return True when feature was found, otherwise false + */ + virtual bool featureAtId(int featureId, + QgsFeature& feature, + bool fetchGeometry = true, + QgsAttributeList fetchAttributes = QgsAttributeList()); + + /** + * Get feature type. + * @return int representing the feature type + */ + virtual QGis::WkbType geometryType() const; + + /** + * Number of features in the layer + * @return long containing number of features + */ + virtual long featureCount() const; + + /** + * Number of attribute fields for a feature in the layer + */ + virtual uint fieldCount() const; + + /** + * Return a map of indexes with field names for this layer + * @return map of fields + */ + virtual const QgsFieldMap & fields() const; + + /** + * Restart reading features from previous select operation. + */ + virtual void rewind(); + + /** + * Changes attribute values of existing features. + * @param attr_map a map containing changed attributes + * @return true in case of success and false in case of failure + */ + virtual bool changeAttributeValues(const QgsChangedAttributesMap & attr_map); + + /** + * Returns a bitmask containing the supported capabilities + * Note, some capabilities may change depending on whether + * a spatial filter is active on this provider, so it may + * be prudent to check this value per intended operation. + */ + virtual int capabilities() const; + + + // Implementation of functions from QgsDataProvider + + /** + * Returns a provider name. + */ + virtual QString name() const; + + /** + * Returns a provider description. + */ + virtual QString description() const; + + /** + * Return the extent for this data layer + */ + virtual QgsRectangle extent(); + + /** + * Returns true if this is a valid provider + */ + virtual bool isValid(); + + /** + * Get the QgsCoordinateReferenceSystem for this layer. + */ + virtual QgsCoordinateReferenceSystem crs(); + + +private: + enum { PointType, LineType, PolygonType } mFeatureType; + enum Attribute { TimestampAttr = 0, UserAttr = 1, TagAttr, CustomTagAttr }; + const static int DEFAULT_EXTENT = 100; + + static const char* attr[]; + + QString mFileName; + QString mDatabaseFileName; + QDateTime mOsmFileLastModif; + bool mValid; + + sqlite3 *mDatabase; + sqlite3_stmt *mDatabaseStmt; + + char *mError; + + //! object that receives notifications from init + QObject* mInitObserver; + + double xMin, xMax, yMin, yMax; // boundary + + // selection + QgsAttributeList mAttributesToFetch; + QgsFieldMap mAttributeFields; + QgsRectangle mSelectionRectangle; + QgsGeometry* mSelectionRectangleGeom; + + // flags + bool mSelectUseIntersect; + + // private methods + sqlite3_stmt *mTagsStmt; + bool mTagsRetrieval; + QString tagsForObject(const char* type, int id); + + sqlite3_stmt *mCustomTagsStmt; + QStringList mCustomTagsList; + QString tagForObject(const char* type, int id, QString tagKey); + + sqlite3_stmt *mWayStmt; + sqlite3_stmt *mNodeStmt; + + QString mStyleFileName; + QString mStyle; + + // manipulation with sqlite database + + bool isDatabaseCompatibleWithInput(QString mFileName); + bool isDatabaseCompatibleWithPlugin(); + + /** + * Create Open Street Map database schema, using c++ library for attempt to sqlite database. + * @return true in case of success and false in case of failure + */ + bool createDatabaseSchema(); + + /** + * Create indexes for OSM database schema, using c++ library for attempt to sqlite database. + * @return true in case of success and false in case of failure + */ + bool createIndexes(); + bool createTriggers(); + + /** + * Drop the whole OSM database schema, using c++ library for attempt to sqlite database. + * @return true in case of success and false in case of failure + */ + bool dropDatabaseSchema(); + + /** + * Open sqlite3 database. + * @return true in case of success and false in case of failure + */ + bool openDatabase(); + + /** + * Close opened sqlite3 database. + */ + bool closeDatabase(); + + /** + * Process Open Street Map file, parse it and store data in sqlite database. + * Function doesn't require much memory: uses simple SAX XML parser + * and stores data directly to database while processing OSM file. + * @param osm_filename name of file with OSM data to parse into sqlite3 database + * @return true in case of success and false in case of failure + */ + bool loadOsmFile(QString osm_filename); + + bool updateWayWKB(int wayId, int isClosed, char **geo, int *geolen); + bool updateNodes(); + bool removeIncorrectWays(); + + + /** + * This function is part of postparsing. OpenStreetMaps nodes have to be divided in two categories here for better manipulation. + * First category is "significant OSM nodes" - these nodes are loaded to Point vector layer and hold some significant information (in tags), + * except the fact that they may be parts of ways geometries. The second category are "not significant OSM nodes". These are considered + * to be a part of some way geometry only but nothing more. These nodes are not loaded to Point layer, they don't have any significant tags + * like "name","ref",etc; OSM plugin even doesn't care of these nodes when some way geometry is changing. Plugin will just remove + * all not significant nodes of that way and will create new ones instead of them. + * @return true in case of success and false in case of failure + */ + bool splitNodes(); + + /** + * This function is postprocess after osm file parsing. Parsing stored all information into database, but + * such database schema is not optimal e.g. for way selection, that is called very often. It should be better + * to have way members (with their coordinates) store directly in way table - in WKB (well known binary) format + * @return true in case of success and false in case of failure + */ + bool postparsing(); + + /** + * Gets first free feature id in database. Searching for the biggest + * NEGATIVE integer that is not assigned to any feature. + * @return free id (negative) for feature, 0 if id cannot be returned + */ + int freeFeatureId(); + + /** + * Get number of members of specified way. + * @param wayId way identifier + * @return number of way members + */ + int wayMemberCount(int wayId); + + int relationMemberCount(int relId); + + // fetch node from current statement + bool fetchNode(QgsFeature& feature, sqlite3_stmt* stmt, bool fetchGeometry, QgsAttributeList& fetchAttrs); + + // fetch way from current statement + bool fetchWay(QgsFeature& feature, sqlite3_stmt* stmt, bool fetchGeometry, QgsAttributeList& fetchAttrs); + + // Change geometry of one feature (used by changeGeometryValues()) + bool changeGeometryValue(const int & featid, QgsGeometry & geom); + + struct wkbPoint { + char byteOrder; + unsigned wkbType; + double x; + double y; + }; + wkbPoint mWKBpt; +}; + diff --git a/src/providers/osm/osmrenderer.cpp b/src/providers/osm/osmrenderer.cpp new file mode 100644 index 00000000000..0cf27584f6f --- /dev/null +++ b/src/providers/osm/osmrenderer.cpp @@ -0,0 +1,183 @@ +/*************************************************************************** + osmrenderer.cpp - handler for parsing OSM data + ------------------ + begin : April 2009 + copyright : (C) 2009 by Lukas Berka + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "osmrenderer.h" + +#include + +#include "qgslogger.h" +#include "qgsapplication.h" +#include "qgsgeometry.h" + +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + + + +OsmRenderer::OsmRenderer(QGis::GeometryType geometryType, QString styleFileName) + : QgsRenderer(), osmstyle(styleFileName), mGeomType(geometryType) +{ +} + + +QMap OsmRenderer::parse_tags(QString tags) +{ + QMap t; + if (tags.size()==0) + { + QgsDebugMsg("no tags for feature!"); + return t; + } + + // tags: "key1"="val1","key2"="val2","key3"="val3" + // -all original ; in keyX and valX are replaced by ;; + // -all original , in keyX and valX are replaced by ; + // -all original - in keyX and valX are replaced by -- + // -all original = in keyX and valX are replaced by - + + QStringList tag_pairs = tags.split(","); + for (int i = 0; i < tag_pairs.size(); ++i) + { + QStringList duo = tag_pairs.at(i).split("="); + if (duo.count() != 2) + { + QgsDebugMsg("invalid tag value: " + tag_pairs.at(i)); + continue; + } + QString key = duo[0]; + QString val = duo[1]; + + key=key.replace(';',","); + val=val.replace(';',","); + key=key.replace(";;",";"); + val=val.replace(";;",";"); + + key=key.replace('-',"="); + val=val.replace('-',"="); + key=key.replace("--","-"); + val=val.replace("--","-"); + + // dequoting + key = key.mid(1,key.size()-2); + val = val.mid(1,val.size()-2); + // put tag into map + t.insert(key,val); + } + return t; +} + + +bool OsmRenderer::willRenderFeature (QgsFeature *f) +{ + // todo: return what? + return true; +} + + +void OsmRenderer::renderFeature( QgsRenderContext &renderContext, QgsFeature& f, QImage* pic, bool selected ) +{ +// QgsDebugMsg("RENDERING FEAT:" + f.id()); + QPainter* p = renderContext.painter(); + QgsAttributeMap attr_map = f.attributeMap(); + QMap tags = parse_tags( attr_map[2].toString() ); + + if (mGeomType== QGis::Line) + { + QPen pen = osmstyle.get_pen(tags); + QColor penColor = pen.color(); + int red = penColor.red(); + int green = penColor.green(); + int blue = penColor.blue(); + p->setPen( osmstyle.get_pen(tags) ); + p->setOpacity(1.0); + } + else if (mGeomType==QGis::Polygon) + { + QBrush br; + p->setPen( osmstyle.get_pen_brush(tags,br) ); + p->setBrush(br); + p->setBackgroundMode( Qt::TransparentMode ); + p->setOpacity(0.5); + } + else if (mGeomType==QGis::Point) + { + *pic = osmstyle.get_image(tags); + p->setOpacity(1.0); + } +} + + +int OsmRenderer::readXML(const QDomNode &rnode, QgsVectorLayer &vl) +{ + return 0; +} + + +bool OsmRenderer::writeXML(QDomNode &layer_node, QDomDocument &document, const QgsVectorLayer &vl) const +{ + return true; +} + + +bool OsmRenderer::needsAttributes() const +{ + return true; +} + + +QgsAttributeList OsmRenderer::classificationAttributes() const +{ + QgsAttributeList attr_list; + attr_list.append(2); + return attr_list; +} + + +QString OsmRenderer::name() const +{ + return QString("OSM"); +} + + +const QList< QgsSymbol * > OsmRenderer::symbols () const +{ + const QList sym; + return sym; +} + + +QgsRenderer *OsmRenderer::clone () const +{ + return 0; +} + + +bool OsmRenderer::containsPixmap () const +{ + return false; +} + + +bool OsmRenderer::usesTransparency () const +{ + return false; +} + diff --git a/src/providers/osm/osmrenderer.h b/src/providers/osm/osmrenderer.h new file mode 100644 index 00000000000..fe0a3fdf15a --- /dev/null +++ b/src/providers/osm/osmrenderer.h @@ -0,0 +1,77 @@ +/*************************************************************************** + osmrenderer.h - Quantum GIS renderer for OpenStreetMap vector layers. + ------------------ + begin : April 2009 + copyright : (C) 2009 by Lukas Berka + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include +#include +#include +#include + +#include "qgsrenderer.h" +#include "qgsfeature.h" +#include "osmstyle.h" + +using namespace std; + + +/** + * Quantum GIS renderer for OpenStreetMap vector layers. + */ +class OsmRenderer : public QgsRenderer +{ +public: + // Object construction + OsmRenderer(QGis::GeometryType geometryType, QString styleFileName); +// ~OsmRenderer(); + + QMap parse_tags(QString tags); + + // ??? Determines if a feature will be rendered or not. + bool willRenderFeature (QgsFeature *f); + + // A vector layer passes features to a renderer object to change the brush and pen of the qpainter. + void renderFeature( QgsRenderContext &renderContext, QgsFeature& f, QImage* pic, bool selected ); + + // Reads the renderer configuration from an XML file. + int readXML(const QDomNode &rnode, QgsVectorLayer &vl); + + // Writes the contents of the renderer to a configuration file @ return true in case of success. + bool writeXML(QDomNode &layer_node, QDomDocument &document, const QgsVectorLayer &vl) const; + + // Returns true, if attribute values are used by the renderer and false otherwise. + bool needsAttributes() const; + + // Returns a list with indexes of classification attributes. + QgsAttributeList classificationAttributes () const; + + // Returns the renderers name. + QString name() const; + + // Return symbology items. + const QList< QgsSymbol * > symbols () const; + + //Returns a copy of the renderer (a deep copy on the heap). + QgsRenderer *clone () const; + + // ??? Returns true if this renderer returns a pixmap in the render method + bool containsPixmap () const; + + // ??? Returns true if this renderer uses its own transparency settings + bool usesTransparency () const; + +protected: +// member variables + OsmStyle osmstyle; + QGis::GeometryType mGeomType; +}; + diff --git a/src/providers/osm/osmstyle.cpp b/src/providers/osm/osmstyle.cpp new file mode 100644 index 00000000000..0b78e9e9cdf --- /dev/null +++ b/src/providers/osm/osmstyle.cpp @@ -0,0 +1,206 @@ +/*************************************************************************** + osmstyle.cpp - Class representing OSM stylesheet. + ------------------ + begin : April 2009 + copyright : (C) 2009 by Lukas Berka + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "osmstyle.h" +#include "symbology/qgssymbol.h" +#include + +#include + +OsmStyle::OsmStyle(QString filename) +{ + rules_line.clear(); + rules_polygon.clear(); + rules_point.clear(); + + QString rule_type = "unknown"; + QFile file(filename); + + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QgsDebugMsg("failed to open style file: " + filename); + return; + } + + while (!file.atEnd()) + { + QByteArray line_bytes = file.readLine(); + QString line = line_bytes.data(); + QgsDebugMsg("line: " + line); + if (line.at(0)=='#') + // change of rule type + rule_type = line.mid(1).trimmed(); + else + { + if (rule_type == "LINE") + parse_rule_line(line); + if (rule_type == "POLYGON") + parse_rule_polygon(line); + if (rule_type == "POINT") + parse_rule_point(line); + } + } + + QgsDebugMsg(QString("OSM style parsing done: %1 / %2 / %3") + .arg(rules_line.count()).arg(rules_polygon.count()).arg(rules_point.count())); +} + + +OsmStyle::~OsmStyle() +{ +} + + +void OsmStyle::parse_rule_line(QString line) +{ + // split line into "key","val","width","color" parts + QStringList line_parts = line.split(" "); + QString key = line_parts[0]; + QString val = line_parts[1]; + QString width = line_parts[2]; + QString penStyle = line_parts[3]; + QString color = line_parts[4]; + + // split color into its typical parts + QStringList color_parts = color.split(","); + QString r = color_parts[0]; + QString g = color_parts[1]; + QString b = color_parts[2]; + + // create pen + QPen pen = QPen(QColor(r.toInt(),g.toInt(),b.toInt())); + pen.setWidth(width.toFloat()); + pen.setStyle((Qt::PenStyle) penStyle.toInt()); + + // add rule + rules_line.append( Rule(key,val,pen,QBrush(),QImage()) ); +} + + +void OsmStyle::parse_rule_polygon(QString line) +{ + // split line into "key","val","width","color","fill" parts + QStringList line_parts = line.split(" "); + QString key = line_parts[0]; + QString val = line_parts[1]; + QString width = line_parts[2]; + QString penStyle = line_parts[3]; + QString color = line_parts[4]; + QString fill = line_parts[5]; + + // split color into red, green and blue parts + QStringList color_parts = color.split(","); + QString r = color_parts[0]; + QString g = color_parts[1]; + QString b = color_parts[2]; + + // create pen + QPen pen = QPen(QColor(r.toInt(),g.toInt(),b.toInt())); + pen.setWidth(width.toFloat()); + pen.setStyle((Qt::PenStyle) penStyle.toInt()); + + // split fill into red, green and blue parts + color_parts = fill.split(","); + r = color_parts[0]; + g = color_parts[1]; + b = color_parts[2]; + QColor col(r.toInt(),g.toInt(),b.toInt(),120); + + QBrush brush = QBrush(col); + brush.setStyle(Qt::SolidPattern); + + // add rule + rules_polygon.append( Rule(key,val,pen,brush,QImage()) ); +} + + +void OsmStyle::parse_rule_point(QString line) +{ + // split line into "key","val","width","color" parts + QStringList line_parts = line.split(" "); + QString key = line_parts[0]; + QString val = line_parts[1]; + QString name = line_parts[2]; + QString size = line_parts[3]; + + double widthScale = 1.0; + bool selected = false; + QColor mSelectionColor(255,255,0); + double rasterScaleFactor = 1.0; + + QgsSymbol sym(QGis::Point); + sym.setNamedPointSymbol(QString("svg:%1%2").arg(QgsApplication::svgPath()).arg(name)); + sym.setPointSize(size.toFloat()); + + QImage img = sym.getPointSymbolAsImage(widthScale,selected,mSelectionColor); + + // add rule + rules_point.append( Rule(key,val,QPen(),QBrush(),img) ); +} + + +QPen OsmStyle::get_pen(QMap tags) +{ + // go through rules one by one. the valid rule is applied + for (int i = 0; i < rules_line.size(); ++i) + { + const Rule& rule = rules_line.at(i); + QString key=rule.key.trimmed(); + QString val=rule.val.trimmed(); + + // todo: tmp comm, from python: if rule[0] == '*' or (tags.has_key(rule[0]) and (tags[rule[0]] == rule[1] or rule[1] == '*')) + if ((key=="*") || ((tags.find(key)!=tags.end()) && ((tags.value(key)==rule.val) || (val=="*")))) + { + return rule.pen; + } + } + QgsDebugMsg("not drawing."); + return QPen(Qt::NoPen); +} + + +QPen OsmStyle::get_pen_brush(QMap tags, QBrush &brush) // todo: return both pen and brush +{ + // go through rules one by one. the valid rule is applied + for (int i = 0; i < rules_polygon.size(); ++i) + { + const Rule& rule = rules_polygon.at(i); + QString key=rule.key.trimmed(); + QString val=rule.val.trimmed(); + + // todo: tmp comm, from python: if rule[0] == '*' or (tags.has_key(rule[0]) and (tags[rule[0]] == rule[1] or rule[1] == '*')) + if ((key=="*") || ((tags.find(key)!=tags.end()) && ((tags.value(key)==val) || (val=="*")))) { + brush=rule.brush; + return rule.pen; // todo: and brush? + } + } + brush=Qt::NoBrush; + return QPen(Qt::NoPen); // todo: and brush? +} + + +QImage OsmStyle::get_image(QMap tags) +{ + // go through rules one by one. the valid rule is applied + for (int i = 0; i < rules_point.size(); ++i) + { + const Rule& rule = rules_point.at(i); + // todo: tmp comm, from python: if rule[0] == '*' or (tags.has_key(rule[0]) and (tags[rule[0]] == rule[1] or rule[1] == '*')) + if ((rule.key=="*") || ((tags.find(rule.key)!=tags.end()) && ((tags.value(rule.key)==rule.val) || (rule.val=="*")))) + return rule.img; + } + return QImage(); +} + + diff --git a/src/providers/osm/osmstyle.h b/src/providers/osm/osmstyle.h new file mode 100644 index 00000000000..bb7104917c1 --- /dev/null +++ b/src/providers/osm/osmstyle.h @@ -0,0 +1,73 @@ +/*************************************************************************** + osmstyle.h - Class representing OSM stylesheet. + ------------------ + begin : April 2009 + copyright : (C) 2009 by Lukas Berka + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + + +/** + * Class to represent a paint rule. + */ +class Rule +{ +public: + // construction, destruction + Rule(QString pKey, QString pVal, QPen pPen, QBrush pBrush, QImage pImg) + :key(pKey), val(pVal), pen(pPen), brush(pBrush), img(pImg) {}; + + // class members + QString key; + QString val; + QPen pen; + QBrush brush; + QImage img; +}; + + +/** + * Class representing OSM stylesheet. + */ +class OsmStyle +{ +public: + OsmStyle(QString filename); + ~OsmStyle(); + + QList rules_line; + QList rules_polygon; + QList rules_point; + + + void parse_rule_line(QString line); + + void parse_rule_polygon(QString line); + + void parse_rule_point(QString line); + + QPen get_pen(QMap tags); + + QPen get_pen_brush(QMap tags, QBrush &brush); // todo: return both pen and brush + + QImage get_image(QMap tags); +}; + +