def initGui(self): # Create action that will start plugin configuration self.action = QAction(QIcon(":/plugins/simplesvg/icon.png"), \ "Save as SVG", self.iface.mainWindow()) # connect the action to the run method QObject.connect(self.action, SIGNAL("activated()"), self.run) # Add toolbar button and menu item self.iface.addToolBarIcon(self.action) self.dlg = SimpleSvgDialog(self.iface) self.dlg.setFilePath(self.svgFilename) QObject.connect(self.dlg, SIGNAL("showHelp()"), self.showHelp) QObject.connect(self.dlg, SIGNAL("accepted()"), self.writeToFile) QObject.connect(self.dlg, SIGNAL("cbFeaturesInMapcanvasOnlyChanged"), self.setFeaturesInMapcanvasOnly) # about self.aboutAction = QAction(QIcon(":/plugins/simplesvg/help.png"), \ "About", self.iface.mainWindow()) self.aboutAction.setWhatsThis("SimpleSvg Plugin About") QObject.connect(self.aboutAction, SIGNAL("activated()"), self.about) # help self.helpAction = QAction(QIcon(":/plugins/simplesvg/help.png"), \ "Help", self.iface.mainWindow()) self.helpAction.setWhatsThis("SimpleSvg Plugin Help") QObject.connect(self.helpAction, SIGNAL("activated()"), self.showHelp) if hasattr(self.iface, "addPluginToWebMenu"): self.iface.addPluginToWebMenu("&Save as SVG", self.action) self.iface.addPluginToWebMenu("&Save as SVG", self.aboutAction) self.iface.addPluginToWebMenu("&Save as SVG", self.helpAction) else: self.iface.addPluginToMenu("&Save as SVG", self.action) self.iface.addPluginToMenu("&Save as SVG", self.aboutAction) self.iface.addPluginToMenu("&Save as SVG", self.helpAction)
def initGui(self): # Create action that will start plugin configuration self.action = QAction(QIcon(":/plugins/simplesvg/icon.png"), \ "Save as SVG", self.iface.mainWindow()) # connect the action to the run method QObject.connect(self.action, SIGNAL("activated()"), self.run) # Add toolbar button and menu item self.iface.addToolBarIcon(self.action) self.dlg = SimpleSvgDialog(self.iface) self.dlg.setFilePath(self.svgFilename) QObject.connect(self.dlg, SIGNAL("showHelp()"), self.showHelp) QObject.connect(self.dlg, SIGNAL("accepted()"), self.writeToFile) QObject.connect(self.dlg, SIGNAL("cbFeaturesInMapcanvasOnlyChanged"), self.setFeaturesInMapcanvasOnly) # about self.aboutAction = QAction(QIcon(":/plugins/simplesvg/help.png"), \ "About", self.iface.mainWindow()) self.aboutAction.setWhatsThis("SimpleSvg Plugin About") QObject.connect(self.aboutAction, SIGNAL("activated()"), self.about) # help self.helpAction = QAction(QIcon(":/plugins/simplesvg/help.png"), \ "Help", self.iface.mainWindow()) self.helpAction.setWhatsThis("SimpleSvg Plugin Help") QObject.connect(self.helpAction, SIGNAL("activated()"), self.showHelp) if hasattr ( self.iface, "addPluginToWebMenu" ): self.iface.addPluginToWebMenu("&Save as SVG", self.action) self.iface.addPluginToWebMenu("&Save as SVG", self.aboutAction) self.iface.addPluginToWebMenu("&Save as SVG", self.helpAction) else: self.iface.addPluginToMenu("&Save as SVG", self.action) self.iface.addPluginToMenu("&Save as SVG", self.aboutAction) self.iface.addPluginToMenu("&Save as SVG", self.helpAction)
class SimpleSvg: MSG_BOX_TITLE = "QGIS SimpleSvg Plugin " def __init__(self, iface): # Save reference to the QGIS interface self.iface = iface self.svgFilename = QSettings().value('/simplesvg/lastfile', '') self.svgType = SVG_TYPE_PATH self.strokeLineJoin = 'round' # miter, round, bevel # normal usage: current scale, only the features which touch current mapcanvas # if setting this false, ALL features will be taken from the dataprovider # and rendered as vectors, NOTE: not working for raster layers ! self.featuresInMapcanvasOnly = True def initGui(self): # Create action that will start plugin configuration self.action = QAction(QIcon(":/plugins/simplesvg/icon.png"), \ "Save as SVG", self.iface.mainWindow()) # connect the action to the run method QObject.connect(self.action, SIGNAL("activated()"), self.run) # Add toolbar button and menu item self.iface.addToolBarIcon(self.action) self.dlg = SimpleSvgDialog(self.iface) self.dlg.setFilePath(self.svgFilename) QObject.connect(self.dlg, SIGNAL("showHelp()"), self.showHelp) QObject.connect(self.dlg, SIGNAL("accepted()"), self.writeToFile) QObject.connect(self.dlg, SIGNAL("cbFeaturesInMapcanvasOnlyChanged"), self.setFeaturesInMapcanvasOnly) # about self.aboutAction = QAction(QIcon(":/plugins/simplesvg/help.png"), \ "About", self.iface.mainWindow()) self.aboutAction.setWhatsThis("SimpleSvg Plugin About") QObject.connect(self.aboutAction, SIGNAL("activated()"), self.about) # help self.helpAction = QAction(QIcon(":/plugins/simplesvg/help.png"), \ "Help", self.iface.mainWindow()) self.helpAction.setWhatsThis("SimpleSvg Plugin Help") QObject.connect(self.helpAction, SIGNAL("activated()"), self.showHelp) if hasattr ( self.iface, "addPluginToWebMenu" ): self.iface.addPluginToWebMenu("&Save as SVG", self.action) self.iface.addPluginToWebMenu("&Save as SVG", self.aboutAction) self.iface.addPluginToWebMenu("&Save as SVG", self.helpAction) else: self.iface.addPluginToMenu("&Save as SVG", self.action) self.iface.addPluginToMenu("&Save as SVG", self.aboutAction) self.iface.addPluginToMenu("&Save as SVG", self.helpAction) def setFeaturesInMapcanvasOnly(self, checked): if not checked: QMessageBox.information(self.dlg, "Warning", "Be carefull: unchecking this, means QGIS is going to fetch ALL objects from your data.\nHandle with care for big datasets.") self.featuresInMapcanvasOnly = checked def showHelp(self): docFile = os.path.join(os.path.dirname(__file__), "docs","index.html") QDesktopServices.openUrl( QUrl("file:" + docFile) ) def writeToFile(self): self.svgFilename = self.dlg.getFilePath() # save this filename in settings for later QSettings().setValue('/simplesvg/lastfile', self.svgFilename) output = self.writeSVG() file = open(self.svgFilename, "w") #print output for line in output: #print '%s - %s' % (type(line),line) file.write(line.encode('utf-8')) file.close() QMessageBox.information(self.iface.mainWindow(), \ "SimpleSvg Plugin", "Finished writing to svg") def about(self): try: infoString = QString("Written by Richard Duivenvoorde\nEmail - [email protected]\n") infoString = infoString.append("Company - http://www.webmapper.net\n") infoString = infoString.append("Source: http://github.com/rduivenvoorde/simplesvg/") except NameError: infoString = "Written by Richard Duivenvoorde\nEmail - [email protected]\n" infoString += "Company - http://www.webmapper.net\n" infoString += "Source: http://github.com/rduivenvoorde/simplesvg/" QMessageBox.information(self.iface.mainWindow(), \ "SimpleSvg Plugin About", infoString) def unload(self): # Remove the plugin menu item and icon if hasattr ( self.iface, "addPluginToWebMenu" ): self.iface.removePluginWebMenu("&Save as SVG",self.action) self.iface.removePluginWebMenu("&Save as SVG",self.helpAction) self.iface.removePluginWebMenu("&Save as SVG",self.aboutAction) else: self.iface.removePluginMenu("&Save as SVG",self.action) self.iface.removePluginMenu("&Save as SVG",self.helpAction) self.iface.removePluginMenu("&Save as SVG",self.aboutAction) self.iface.removeToolBarIcon(self.action) QObject.disconnect(self.aboutAction, SIGNAL("activated()"), self.about) QObject.disconnect(self.helpAction, SIGNAL("activated()"), self.showHelp) QObject.disconnect(self.action, SIGNAL("activated()"), self.run) QObject.disconnect(self.dlg, SIGNAL("accepted()"), self.writeToFile) # run method that performs all the real work def run(self): self.dlg.show() def writeSVG(self): # determine extent for later use (only write geoms that are at least partially contained) self.currentExtent = self.iface.mapCanvas().extent() w=self.iface.mapCanvas().size().width() h=self.iface.mapCanvas().size().height() # keep the current extent as a Geometry to be able to do some 'contains' tests later self.extentAsPoly = QgsGeometry(); self.extentAsPoly = QgsGeometry.fromRect(self.currentExtent); svg = [u'<?xml version="1.0" standalone="no"?>\n'] svg.append(u'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n') svg.append(u'<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox = "0 0 '+str(w)+' '+str(h)+'" version = "1.1">\n') svg.append(u'<!-- svg generated using QGIS www.qgis.org -->\n') # for all visible layers, from bottom to top (-1) for i in range (self.iface.mapCanvas().layerCount()-1, -1, -1): layer = self.iface.mapCanvas().layer(i) if layer.type()==0: # vector if self.isRendererV2(layer): #QMessageBox.information(self.iface.mainWindow(), "Warning", "New Symbology layer found for layer '"+layer.name()+"'\n\nThe plugin cannot handle layer(s) which use 'New Symbology' yet.\n\nThis layer will be ignored in export.\n\nPlease change symbology of these layer(s) to 'Old Symbology' if you want this layer in svg.") #pass svg.extend(self.writeVectorLayer(layer, False)) else: # old symbology svg.extend(self.writeVectorLayer(layer, False)) elif layer.type()==1: # raster svg.extend(self.writeRaster(layer)) # layers like OpenLayers/OpenStreetmap/Google are plugin layer: write as raster for now elif layer.type()==2: # plugin layer svg.extend(self.writeRaster(layer)) # now layers with labels for i in range (self.iface.mapCanvas().layerCount()-1, -1, -1): layer = self.iface.mapCanvas().layer(i) if layer.type()==0: # vector lblSettings = QgsPalLayerSettings() lblSettings.readFromLayer( layer ) #TODO: fix this if layer.type()==0 and layer.hasLabelsEnabled(): # only vectors have labels if layer.type()==0 and lblSettings.enabled: # only vectors have labels svg.extend(self.writeVectorLayer(layer, True)) # qgis extent, usable for clipping in Inkscape svg.extend(self.writeExtent()) svg.append(u'</svg>') return svg def isRendererV2(self, layer): return (layer.type()==0 and hasattr(layer, 'isUsingRendererV2') and layer.isUsingRendererV2()) or (layer.type()==0 and not hasattr(layer, 'isUsingRendererV2') and ('rendererV2' in dir(layer))) def isRendererV2SIP2(self, layer): return (layer.type()==0 and not hasattr(layer, 'isUsingRendererV2') and ('rendererV2' in dir(layer))) def writeVectorLayer(self, layer, labels=False): # in case of 'on the fly projection' # AND # different crs's for mapCanvas/project and layer we have to reproject stuff if hasattr(self.iface.mapCanvas().mapRenderer(), "destinationSrs"): # QGIS < 2.0 destinationCrs = self.iface.mapCanvas().mapRenderer().destinationSrs() layerCrs = layer.srs() else: destinationCrs = self.iface.mapCanvas().mapRenderer().destinationCrs() layerCrs = layer.crs() if self.featuresInMapcanvasOnly: mapCanvasExtent = self.iface.mapCanvas().extent() else: mapCanvasExtent = layer.extent() #print 'destination crs: %s:' % destinationCrs.toProj4() #print 'layer crs: %s:' % layerCrs.toProj4() doCrsTransform = False if not destinationCrs == layerCrs: # we have to transform the mapCanvasExtent to the data/layer Crs to be able # to retrieve the features from the data provider # but ONLY if we are working with on the fly projection # (because in that case we just 'fly' to the raw coordinates from data) if self.iface.mapCanvas().hasCrsTransformEnabled(): # only if we have 'on te fly transformation' enabled # AND the mapCanvasExtent is the real mapcanvas extent (note: if we # are going to write ALL features, then mapCanvasExtent is actually the layer extent) if self.featuresInMapcanvasOnly: crsTransform = QgsCoordinateTransform(destinationCrs, layerCrs) mapCanvasExtent = crsTransform.transformBoundingBox(mapCanvasExtent) # we have to have a transformer to do the transformation of the geometries # to the mapcanvas crs ourselves: crsTransform = QgsCoordinateTransform(layerCrs, destinationCrs) doCrsTransform = True lblSettings = QgsPalLayerSettings() lblSettings.readFromLayer( layer ) # select features within current extent, # with ALL attributes, WITHIN currentExtent, WITH geom, AND using Intersect instead of bbox # we are going to group all features by their symbol so in svg we can group them in a <g> tag with the symbol style if self.isRendererV2(layer): if hasattr(layer, 'isUsingRendererV2'): # For QGis 1.8 API, new symbology provider = layer.dataProvider(); provider.select(provider.attributeIndexes(), mapCanvasExtent, True, True) else: # For QGis 2.0 cleaned-up API provider = layer.getFeatures( QgsFeatureRequest().setFilterRect(mapCanvasExtent)) renderer = layer.rendererV2() if str(renderer.type()) not in ("singleSymbol", "categorizedSymbol", "graduatedSymbol"): QMessageBox.information(self.iface.mainWindow(), "Warning", "New Symbology layer found for layer '"+layer.name()+"'\n\nThis layer uses a Renderer/Style which cannot be used with this plugin.\n\nThis layer will be ignored in export.") return "" else: # For QGis <= 1.8 API, old symbology provider = layer.dataProvider(); provider.select(provider.attributeIndexes(), mapCanvasExtent, True, True) renderer = layer.renderer() symbols = renderer.symbols() symbolFeatureMap = dict.fromkeys(symbols, []) id=self.sanitizeStr(unicode(layer.name()).lower()) if labels: id=id+'_labels' svg = [u'<g id="'+id+'" inkscape:groupmode="layer" inkscape:label="'+id+'">\n']; # start of layer g-element # now iterate through each feature and group by feature f = QgsFeature(); feature = None #print "1 symbols holds %s symbols" % len(symbols) while provider.nextFeature(f): feature = QgsFeature(f) geom = feature.geometry() if hasattr(layer, "srs"): # QGIS < 2.0 layerCrs = layer.srs() else: layerCrs = layer.crs() if doCrsTransform: if hasattr(geom, "transform"): geom.transform(crsTransform) else: QMessageBox.warning(self.iface.mainWindow(), self.MSG_BOX_TITLE, ("Cannot crs-transform geometry in your QGIS version ...\n" "Only QGIS version 1.5 and above can transform geometries on the fly\n" "As a workaround, you can try to save the layer in the destination crs (eg as shapefile) and reload that layer...\n"), QMessageBox.Ok, QMessageBox.Ok) break symbol = self.symbolForFeature(layer, feature) #print "feature: %s symbol: %s rgb: %s %s %s" % (feature, symbol, symbol.color().red(), symbol.color().green(), symbol.color().blue()) # Continous Color does NOT have all symbols, but ONLY start and end color # that's why we do some extra stuff here... # ONLY needed for Old Symbology (which have 'name()' and not 'type()') if hasattr(renderer, 'name') and renderer.name() == "Continuous Color": symbols.append(symbol) if symbol in symbolFeatureMap: symbolFeatureMap[symbol]=[feature] else: symbolFeatureMap.update({symbol:[feature]}) else: if not symbol in symbolFeatureMap or len(symbolFeatureMap[symbol])==0: symbolFeatureMap[symbol]=[feature] else: symbolFeatureMap[symbol].append(feature) # now iterate over symbols IF there are any features in this view from this layer if feature != None: id=id+'_' i=0 #print "2 symbols holds %s symbols" % len(symbols) for symbol in symbols: if self.isRendererV2(layer): sym = self.symbolV2(feature, symbol) else: sym = self.symbol(feature, symbol) # start of symbol g-element, holds colors and stroke etc if not labels: fill = '' if sym.has_key('fill'): fill = 'fill="' + sym['fill'] + '"' svg.append(u'<g stroke="' + sym['stroke'] + '" '+ fill + ' stroke-linejoin="' + self.strokeLineJoin + '" stroke-width="' + sym['stroke-width'] +'">\n') else: # TODO fix this if False: lc = layer.label().labelAttributes().color() lblColor = u'rgb(%s,%s,%s)' % (lc.red(), lc.green(), lc.blue()) else: # labels all black for now... lblColor = u'rgb(0,0,0)' svg.append(u'<g stroke="none" fill="'+lblColor+'">\n') for feature in symbolFeatureMap[symbol]: i=i+1 # labeltxt is used both for the real labels, AND for the inkscape-label attributes of g and txt elements # TODO fix this if lblSettings.enabled: labeltxt = self.sanitizeStr(feature[lblSettings.fieldName]) else: #labeltxt = self.sanitizeStr(layer.label().fieldValue(0, feature)) # only first field for now. #labeltxt = self.sanitizeStr(unicode(feature.fields().field(0))) labeltxt = self.sanitizeStr('') if not labels: svg.extend(self.writeFeature(feature, id+str(i), labeltxt)) if labels: geom = feature.geometry().centroid() # centroid-method returns a NON-transformed centroid if doCrsTransform: if hasattr(geom, "transform"): geom.transform(crsTransform) else: QMessageBox.warning(self.iface.mainWindow(), self.MSG_BOX_TITLE, ("Cannot crs-transform geometry in your QGIS version ...\n" "Only QGIS version 1.5 and above can transform geometries on the fly\n" "As a workaround, you can try to save the layer in the destination crs (eg as shapefile) and reload that layer...\n"), QMessageBox.Ok, QMessageBox.Ok) break svg.extend(self.label2svg(geom.asPoint(), id+str(i), self.symbolForFeature(layer, feature), labeltxt)) svg.append(u'</g>\n'); # end of symbol svg.append(u'</g>\n'); # end of layer return svg def symbol(self, feature, symbol): sym={} sc = symbol.color() sym['stroke'] = u'rgb(%s,%s,%s)' % (sc.red(), sc.green(), sc.blue()) # fill color: only non line features have fill color, lines have 'none' geom = feature.geometry() if geom.wkbType() == QGis.WKBLineString or geom.wkbType() == QGis.WKBMultiLineString: sym['fill'] = u'none' else: f = symbol.fillColor() sym['fill'] = u'rgb(%s,%s,%s)' % (f.red(), f.green(), f.blue()) # pen: in QT pen can be 0 if symbol.pen().width() < 1: sym['stroke-width'] = u'0.5' else: sym['stroke-width'] = unicode(symbol.pen().width()) return sym def symbolV2(self, feature, symbol): #print '##### symbol: %s, symbollayercount: %s' % (symbol, symbol.symbolLayerCount()) sym={} if symbol.symbolLayerCount() > 1: QMessageBox.information(self.iface.mainWindow(), "Warning", "Layer '"+layer.name()+"' uses New Symbology, and styles with more the one Symbol Layer, only the first one will be use.") sl = symbol.symbolLayer(0) slprops = sl.properties() #print "symbollayer properties: %s" % slprops # region/polgyons have: color_border / style_border / offset / style / color / width_border # {PyQt4.QtCore.QString(u'color_border'): PyQt4.QtCore.QString(u'0,0,0,255'), PyQt4.QtCore.QString(u'style_border'): PyQt4.QtCore.QString(u'solid'), PyQt4.QtCore.QString(u'offset'): PyQt4.QtCore.QString(u'0,0'), PyQt4.QtCore.QString(u'style'): PyQt4.QtCore.QString(u'solid'), PyQt4.QtCore.QString(u'color'): PyQt4.QtCore.QString(u'0,0,255,255'), PyQt4.QtCore.QString(u'width_border'): PyQt4.QtCore.QString(u'0.26')} # markers/points have : color_border / offset / size / color / name / angle: # {PyQt4.QtCore.QString(u'color_border'): PyQt4.QtCore.QString(u'0,0,0,255'), PyQt4.QtCore.QString(u'offset'): PyQt4.QtCore.QString(u'0,0'), PyQt4.QtCore.QString(u'size'): PyQt4.QtCore.QString(u'2'), PyQt4.QtCore.QString(u'color'): PyQt4.QtCore.QString(u'255,0,0,255'), PyQt4.QtCore.QString(u'name'): PyQt4.QtCore.QString(u'circle'), PyQt4.QtCore.QString(u'angle'): PyQt4.QtCore.QString(u'0')} # lines have : color / offset / penstyle / width / use_custom_dash / joinstyle / customdash / capstyle: # {PyQt4.QtCore.QString(u'color'): PyQt4.QtCore.QString(u'255,255,0,255'), PyQt4.QtCore.QString(u'offset'): PyQt4.QtCore.QString(u'0'), PyQt4.QtCore.QString(u'penstyle'): PyQt4.QtCore.QString(u'solid'), PyQt4.QtCore.QString(u'width'): PyQt4.QtCore.QString(u'0.5'), PyQt4.QtCore.QString(u'use_custom_dash'): PyQt4.QtCore.QString(u'0'), PyQt4.QtCore.QString(u'joinstyle'): PyQt4.QtCore.QString(u'bevel'), PyQt4.QtCore.QString(u'customdash'): PyQt4.QtCore.QString(u'5;2'), PyQt4.QtCore.QString(u'capstyle'): PyQt4.QtCore.QString(u'square')} try: strokekey = QString(u'color_border') colorkey = QString(u'color') stylekey = QString(u'style') width_borderkey = QString(u'width_border') widthkey = QString(u'width') except NameError: strokekey = u'color_border' colorkey = u'color' stylekey = u'style' width_borderkey = u'width_border' widthkey = u'width' if slprops.has_key(strokekey): stroke = unicode(slprops[strokekey]) sym['stroke'] = u'rgb(%s)' % (stroke[:stroke.rfind(',')]) else: sym['stroke'] = u'none' # fill color: only non line features have fill color, lines have 'none' geom = feature.geometry() if slprops.has_key(colorkey): fill = unicode(slprops[colorkey]) if geom.wkbType() == QGis.WKBLineString or geom.wkbType() == QGis.WKBMultiLineString: sym['stroke'] = u'rgb(%s)' % (fill[:fill.rfind(',')]) # points have fill and stroke sym['fill'] = u'rgb(%s)' % (fill[:fill.rfind(',')]) # if feature is line OR when there is no brush: set fill to none if geom.wkbType() == QGis.WKBLineString or geom.wkbType() == QGis.WKBMultiLineString or (slprops.has_key(stylekey) and slprops[stylekey] == 'no'): sym['fill'] = u'none' # pen: in QT pen can be 0 if slprops.has_key(width_borderkey): sym['stroke-width'] = unicode(slprops[width_borderkey]) elif slprops.has_key(widthkey): sym['stroke-width'] = unicode(slprops[widthkey]) else: sym['stroke-width'] = u'0.26' #print sym return sym def writeExtent(self): svg = [u'<!-- QGIS extent for clipping, eg in Inkscape -->\n<g id="qgisviewbox" inkscape:groupmode="layer" inkscape:label="qgisviewbox" stroke="rgb(255,0,0)" stroke-width="1" fill="none" >\n'] for ring in self.extentAsPoly.asPolygon(): svg.append(u'<path d="M ') first = True for point in ring: pixpoint = self.w2p(point.x(), point.y(), self.iface.mapCanvas().mapUnitsPerPixel(), self.currentExtent.xMinimum(), self.currentExtent.yMaximum()) if not first: svg.append(u'L ') svg.append((unicode(pixpoint[0]) + ',' + unicode(pixpoint[1]) + ' ')) first = False svg.append(u'" />\n') svg.append(u'</g>') return svg def writeRaster(self, layer): # hide all layers except 'layer' and save as png image in current directory # TODO? maybe inline it in svg? # save visibility of layers visibleList=self.iface.mapCanvas().layers() legend = self.iface.legendInterface() # set all layers invisible EXCEPT layer for lyr in visibleList: if lyr != layer: legend.setLayerVisible(lyr, False) lyrName = unicode(layer.name()) imgName = lyrName+'.png' try: imgPath= self.svgFilename[:self.svgFilename.rfind('/')+1] except NameError: imgPath= self.svgFilename[:self.svgFilename.rfind('/')+1] # save image next to svg but put it in Image tag only the local filename self.iface.mapCanvas().saveAsImage(imgPath+imgName) # <image y="-7.7685061" x="27.115078" id="image3890" xlink:href="nl.png" /> svg = [u'<g id="'+lyrName+'" inkscape:groupmode="layer" inkscape:label="'+lyrName+'">\n']; #svg.append('<image y="0" x="0" xlink:href="'+imgPath+imgName+'" />') svg.append(u'<image y="0" x="0" xlink:href="'+imgName+'" />') svg.append(u'</g>') # end of raster layer # now set earlier visible layers back to visible for lyr in visibleList: legend.setLayerVisible(lyr, True) return svg def label2svg(self, point, fid, symbol, labelTxt): # <g> <text x="262.08704" y="523.79077">abc</text> </g> #point = feature.geometry().centroid().asPoint() xy = self.w2p(point.x(), point.y(), self.iface.mapCanvas().mapUnitsPerPixel(), self.currentExtent.xMinimum(), self.currentExtent.yMaximum()) inkscapeLbl = '' if len(labelTxt)>0: inkscapeLbl = 'inkscape:label="'+unicode(labelTxt+'_lbl')+'"' svg = [u'<text id="'+fid+'" x="'+unicode(xy[0])+'" y="'+unicode(xy[1])+'" '+inkscapeLbl+'>'+unicode(labelTxt)+'</text>\n'] return svg def sanitizeStr(self, string): # TODO: find the right way to do this return unicode(string).replace(' ','_').replace('/','_').replace(',','_').replace('.','_') def writeFeature(self, feature, fid, labelTxt): svg = [] # <g>-element set's style attributes inkscapeLbl = '' if len(labelTxt)>0: inkscapeLbl = 'inkscape:label="'+unicode(labelTxt)+'"' svg.append(u'<g id="' + fid + '" '+inkscapeLbl+'>\n') geom=feature.geometry() currentExtent=self.currentExtent if geom.wkbType() == QGis.WKBPoint: # 1 = WKBPoint svg.extend(self.point2svg(feature, currentExtent)) if geom.wkbType() == QGis.WKBPolygon: # 3 = WKBTYPE.WKBPolygon: polygon = geom.asPolygon() # returns a list svg.extend(self.polygon2svg(feature, polygon, currentExtent)) if geom.wkbType() == QGis.WKBMultiPolygon: # 6 = WKBTYPE.WKBMultiPolygon: multipolygon = geom.asMultiPolygon() # returns a list for polygon in multipolygon: svg.extend(self.polygon2svg(feature, polygon, currentExtent)) if geom.wkbType() == QGis.WKBLineString: # 6 = WKBTYPE.WKBLineString: line = geom.asPolyline() # returns a list of points svg.extend(self.line2svg(feature, line, currentExtent)) if geom.wkbType() == QGis.WKBMultiLineString: # 6 = WKBTYPE.WKBLineString: multiline = geom.asMultiPolyline() # returns a list of points for line in multiline: svg.extend(self.line2svg(feature, line, currentExtent)) svg.append(u'</g>\n'); return svg def point2svg(self, feature, currentExtent): point = feature.geometry().asPoint() xy = self.w2p(point.x(), point.y(), self.iface.mapCanvas().mapUnitsPerPixel(), self.currentExtent.xMinimum(), self.currentExtent.yMaximum()) # TODO take current extent into account svg = ['<circle cx="'+unicode(xy[0])+'" cy="'+unicode(xy[1])+'" r="5" />'] return svg def symbolForFeature(self, layer, feature): if self.isRendererV2(layer): return layer.rendererV2().symbolForFeature(feature) else: # symbolForFeatures seems not to work for Old Symbology?? Do it ourselves: # OLD symbolisation: # Graduated Symbol: every symbol has BOTH upper and lower bound/value # Unique Value: every symbol has BOTH upper and lower value # Continues Color: HAS lower(==value) but NO upper value, BUT has just two symbols: MIN and MAX color, see http://doc.qgis.org/head/qgscontinuouscolorrenderer_8cpp-source.html # Single Simbol: just one symbol, return it renderer = layer.renderer() #print "renderer.name(): %s" % renderer.name() default = None if renderer.name() == "Single Symbol": # there is just one symbol: return it return renderer.symbols()[0] if renderer.name() == "Continuous Color": #pass # already done, should not come here !! minSymbol = renderer.symbols()[0] minValue = (minSymbol.lowerValue()).toDouble()[0] # we know Continuous Color only works with numeric attributes minRed = minSymbol.fillColor().red() minGreen = minSymbol.fillColor().green() minBlue = minSymbol.fillColor().blue() maxSymbol = renderer.symbols()[1] maxValue = (maxSymbol.lowerValue()).toDouble()[0] # we know Continuous Color only works with numeric attributes maxRed = maxSymbol.fillColor().red() maxGreen = maxSymbol.fillColor().green() maxBlue = maxSymbol.fillColor().blue() # create new symbol and set RGB according to calculation in http://doc.qgis.org/head/qgscontinuouscolorrenderer_8cpp-source.html symbol = QgsSymbol(minSymbol) # copy value = (feature.attributeMap()[renderer.classificationAttributes()[0]]).toDouble()[0] # we know Continuous Color only works with numeric attributes if (maxValue - minValue) <> 0: red = int ( maxRed * ( value - minValue ) / ( maxValue - minValue ) + minRed * ( maxValue - value ) / ( maxValue - minValue ) ) green = int ( maxGreen * ( value - minValue ) / ( maxValue - minValue ) + minGreen * ( maxValue - value ) / ( maxValue - minValue ) ) blue = int ( maxBlue * ( value - minValue ) / ( maxValue - minValue ) + minBlue * ( maxValue - value ) / ( maxValue - minValue ) ) else: red = minRed green = minGreen blue = minBlue newFillColor = QColor(red, green, blue) symbol.setFillColor(newFillColor) if renderer.drawPolygonOutline(): # always black color = QColor(0,0,0) symbol.setColor(color) else: symbol.setColor(newFillColor) return symbol # else loop over symbols to find the current symbol for this feature # value can be string or numbers... value = (feature.attributeMap()[renderer.classificationAttributes()[0]]).toString() for symbol in renderer.symbols(): lower = symbol.lowerValue() upper = symbol.upperValue() #print "lower: %s upper: %s value: %s notlower %s, notupper %s, value==lower %s, value=upper %s, str(value)[0].isdigit() %s" % (lower, upper, value, (not lower), (not upper), (value == lower), (value == upper), str(value)[0].isdigit()) if not lower and not upper: # 'default value' given for default in Unique Value default = symbol # Unique Value symbols have the value in both upper and lower value (if values are string!) elif (value == lower and value == upper): return symbol # Unique Value symbols for numbers do not have an upper elif str(value)[0].isdigit() and not upper and float(value) == float(symbol.lowerValue()): return symbol # Graduated Classifications have lower AND uppervalues AND values are always numbers elif str(value)[0].isdigit() and upper and lower and (float(value) >= float(symbol.lowerValue()) and float(value) <= float(symbol.upperValue())): #print "Graduated Classification: %s between %s and %s " % (value, symbol.lowerValue(), symbol.upperValue()) return symbol #else: # print "NEXT..." #print "RETURNING DEFAULT!!" return default def line2svg(self, feature, line, currentExtent): #print "calling line2svg..." linesvg = [] svg = u'' if self.svgType == SVG_TYPE_PATH: svg += '<path d="M ' else: # SVG_TYPE_SHAPE svg += '<polyline points="' lastPixel=[0,0] insideExtent = False coordCount = 0 for point in line: if self.extentAsPoly.contains(point) or not self.featuresInMapcanvasOnly: insideExtent = True pixpoint = self.w2p(point.x(), point.y(), self.iface.mapCanvas().mapUnitsPerPixel(), currentExtent.xMinimum(), currentExtent.yMaximum()) #print pixpoint if lastPixel<>pixpoint: coordCount = coordCount +1 if self.svgType==SVG_TYPE_PATH and coordCount>1: svg += 'L ' svg += (str(pixpoint[0]) + ',' + str(pixpoint[1]) + ' ') lastPixel = pixpoint # if at least ONE pixel of this ring is in current view extent, return the area-string, otherwise return an empty string if not insideExtent: #print "RING FULLY OUTSIDE EXTENT: %s " % ring None else: # check if there are more then 2 coords: very small polygons on current map can have coordinates # which if rounded to pixels all come to the same pixel, resulting in just ONE x,y coordinate # we skip these if coordCount < 2: #print "Line contains just one pixel coordinate pair: skipping" None else: svg += '"/>\n' linesvg.append(svg) return linesvg # SHAPE-svg # <polygon stroke="rgb(0,0,0)" fill="rgb(234,102,228)" stroke-width="1" points="439,238 445,230 ... 439,238"/> # or # <polygon points="439,238 445,230 ... 439,238"/> # PATH-svg # <g stroke="rgb(0,0,0)" fill="rgb(234,102,228)" stroke-width="1" ><path d="M 439 238 L 445 220 ... L 439 238" /> </g> def polygon2svg(self, feature, polygon, currentExtent): #print "calling polygon2svg..." polygonsvg = [''] for ring in polygon: svg = '' if self.svgType == SVG_TYPE_PATH: svg += '<path d="M ' else: # SVG_TYPE_SHAPE svg = '<polygon points="' lastPixel=[0,0] insideExtent = False coordCount = 0 for point in ring: if self.extentAsPoly.contains(point) or not self.featuresInMapcanvasOnly: insideExtent = True pixpoint = self.w2p(point.x(), point.y(), self.iface.mapCanvas().mapUnitsPerPixel(), currentExtent.xMinimum(), currentExtent.yMaximum()) if lastPixel<>pixpoint: coordCount = coordCount +1 if self.svgType==SVG_TYPE_PATH and coordCount>1: svg += 'L ' svg += (str(pixpoint[0]) + ',' + str(pixpoint[1]) + ' ') lastPixel = pixpoint # if at least ONE pixel of this ring is in current view extent, return the area-string, otherwise return an empty string if not insideExtent: #print "RING FULLY OUTSIDE EXTENT: %s " % ring None else: # check if there are more then 2 coords: very small polygons on current map can have coordinates # which if rounded to pixels all come to the same pixel, resulting in just ONE x,y coordinate # we skip these if coordCount < 2: #print "Ring contains just one pixel coordinate pair: skipping" None else: svg += '" />\n' polygonsvg.append(svg) return polygonsvg # NOT WORKING ???? # pixpoint = m2p.transform(point.x(), point.y()) # print m2p.transform(point.x(), point.y()) # so for now: a custom 'world2pixel' method def w2p(self, x, y, mupp, minx, maxy): pixX = (x - minx)/mupp pixY = (y - maxy)/mupp return [int(pixX), int(-pixY)]
class SimpleSvg: MSG_BOX_TITLE = "QGIS SimpleSvg Plugin " def __init__(self, iface): # Save reference to the QGIS interface self.iface = iface self.svgFilename = QSettings().value('/simplesvg/lastfile', '') self.svgType = SVG_TYPE_PATH self.strokeLineJoin = 'round' # miter, round, bevel # normal usage: current scale, only the features which touch current mapcanvas # if setting this false, ALL features will be taken from the dataprovider # and rendered as vectors, NOTE: not working for raster layers ! self.featuresInMapcanvasOnly = True def initGui(self): # Create action that will start plugin configuration self.action = QAction(QIcon(":/plugins/simplesvg/icon.png"), \ "Save as SVG", self.iface.mainWindow()) # connect the action to the run method QObject.connect(self.action, SIGNAL("activated()"), self.run) # Add toolbar button and menu item self.iface.addToolBarIcon(self.action) self.dlg = SimpleSvgDialog(self.iface) self.dlg.setFilePath(self.svgFilename) QObject.connect(self.dlg, SIGNAL("showHelp()"), self.showHelp) QObject.connect(self.dlg, SIGNAL("accepted()"), self.writeToFile) QObject.connect(self.dlg, SIGNAL("cbFeaturesInMapcanvasOnlyChanged"), self.setFeaturesInMapcanvasOnly) # about self.aboutAction = QAction(QIcon(":/plugins/simplesvg/help.png"), \ "About", self.iface.mainWindow()) self.aboutAction.setWhatsThis("SimpleSvg Plugin About") QObject.connect(self.aboutAction, SIGNAL("activated()"), self.about) # help self.helpAction = QAction(QIcon(":/plugins/simplesvg/help.png"), \ "Help", self.iface.mainWindow()) self.helpAction.setWhatsThis("SimpleSvg Plugin Help") QObject.connect(self.helpAction, SIGNAL("activated()"), self.showHelp) if hasattr(self.iface, "addPluginToWebMenu"): self.iface.addPluginToWebMenu("&Save as SVG", self.action) self.iface.addPluginToWebMenu("&Save as SVG", self.aboutAction) self.iface.addPluginToWebMenu("&Save as SVG", self.helpAction) else: self.iface.addPluginToMenu("&Save as SVG", self.action) self.iface.addPluginToMenu("&Save as SVG", self.aboutAction) self.iface.addPluginToMenu("&Save as SVG", self.helpAction) def setFeaturesInMapcanvasOnly(self, checked): if not checked: QMessageBox.information( self.dlg, "Warning", "Be carefull: unchecking this, means QGIS is going to fetch ALL objects from your data.\nHandle with care for big datasets." ) self.featuresInMapcanvasOnly = checked def showHelp(self): docFile = os.path.join(os.path.dirname(__file__), "docs", "index.html") QDesktopServices.openUrl(QUrl("file:" + docFile)) def writeToFile(self): self.svgFilename = self.dlg.getFilePath() # save this filename in settings for later QSettings().setValue('/simplesvg/lastfile', self.svgFilename) output = self.writeSVG() file = open(self.svgFilename, "w") #print output for line in output: #print '%s - %s' % (type(line),line) file.write(line.encode('utf-8')) file.close() QMessageBox.information(self.iface.mainWindow(), \ "SimpleSvg Plugin", "Finished writing to svg") def about(self): try: infoString = QString( "Written by Richard Duivenvoorde\nEmail - [email protected]\n") infoString = infoString.append( "Company - http://www.webmapper.net\n") infoString = infoString.append( "Source: http://github.com/rduivenvoorde/simplesvg/") except NameError: infoString = "Written by Richard Duivenvoorde\nEmail - [email protected]\n" infoString += "Company - http://www.webmapper.net\n" infoString += "Source: http://github.com/rduivenvoorde/simplesvg/" QMessageBox.information(self.iface.mainWindow(), \ "SimpleSvg Plugin About", infoString) def unload(self): # Remove the plugin menu item and icon if hasattr(self.iface, "addPluginToWebMenu"): self.iface.removePluginWebMenu("&Save as SVG", self.action) self.iface.removePluginWebMenu("&Save as SVG", self.helpAction) self.iface.removePluginWebMenu("&Save as SVG", self.aboutAction) else: self.iface.removePluginMenu("&Save as SVG", self.action) self.iface.removePluginMenu("&Save as SVG", self.helpAction) self.iface.removePluginMenu("&Save as SVG", self.aboutAction) self.iface.removeToolBarIcon(self.action) QObject.disconnect(self.aboutAction, SIGNAL("activated()"), self.about) QObject.disconnect(self.helpAction, SIGNAL("activated()"), self.showHelp) QObject.disconnect(self.action, SIGNAL("activated()"), self.run) QObject.disconnect(self.dlg, SIGNAL("accepted()"), self.writeToFile) # run method that performs all the real work def run(self): self.dlg.show() def writeSVG(self): # determine extent for later use (only write geoms that are at least partially contained) self.currentExtent = self.iface.mapCanvas().extent() w = self.iface.mapCanvas().size().width() h = self.iface.mapCanvas().size().height() # keep the current extent as a Geometry to be able to do some 'contains' tests later self.extentAsPoly = QgsGeometry() self.extentAsPoly = QgsGeometry.fromRect(self.currentExtent) svg = [u'<?xml version="1.0" standalone="no"?>\n'] svg.append( u'<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n' ) svg.append( u'<svg xmlns="http://www.w3.org/2000/svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox = "0 0 ' + str(w) + ' ' + str(h) + '" version = "1.1">\n') svg.append(u'<!-- svg generated using QGIS www.qgis.org -->\n') # for all visible layers, from bottom to top (-1) for i in range(self.iface.mapCanvas().layerCount() - 1, -1, -1): layer = self.iface.mapCanvas().layer(i) if layer.type() == 0: # vector if self.isRendererV2(layer): #QMessageBox.information(self.iface.mainWindow(), "Warning", "New Symbology layer found for layer '"+layer.name()+"'\n\nThe plugin cannot handle layer(s) which use 'New Symbology' yet.\n\nThis layer will be ignored in export.\n\nPlease change symbology of these layer(s) to 'Old Symbology' if you want this layer in svg.") #pass svg.extend(self.writeVectorLayer(layer, False)) else: # old symbology svg.extend(self.writeVectorLayer(layer, False)) elif layer.type() == 1: # raster svg.extend(self.writeRaster(layer)) # layers like OpenLayers/OpenStreetmap/Google are plugin layer: write as raster for now elif layer.type() == 2: # plugin layer svg.extend(self.writeRaster(layer)) # now layers with labels for i in range(self.iface.mapCanvas().layerCount() - 1, -1, -1): layer = self.iface.mapCanvas().layer(i) if layer.type() == 0: # vector lblSettings = QgsPalLayerSettings() lblSettings.readFromLayer(layer) #TODO: fix this if layer.type()==0 and layer.hasLabelsEnabled(): # only vectors have labels if layer.type( ) == 0 and lblSettings.enabled: # only vectors have labels svg.extend(self.writeVectorLayer(layer, True)) # qgis extent, usable for clipping in Inkscape svg.extend(self.writeExtent()) svg.append(u'</svg>') return svg def isRendererV2(self, layer): return (layer.type() == 0 and hasattr(layer, 'isUsingRendererV2') and layer.isUsingRendererV2()) or ( layer.type() == 0 and not hasattr(layer, 'isUsingRendererV2') and ('rendererV2' in dir(layer))) def isRendererV2SIP2(self, layer): return (layer.type() == 0 and not hasattr(layer, 'isUsingRendererV2') and ('rendererV2' in dir(layer))) def writeVectorLayer(self, layer, labels=False): # in case of 'on the fly projection' # AND # different crs's for mapCanvas/project and layer we have to reproject stuff if hasattr(self.iface.mapCanvas().mapRenderer(), "destinationSrs"): # QGIS < 2.0 destinationCrs = self.iface.mapCanvas().mapRenderer( ).destinationSrs() layerCrs = layer.srs() else: destinationCrs = self.iface.mapCanvas().mapRenderer( ).destinationCrs() layerCrs = layer.crs() if self.featuresInMapcanvasOnly: mapCanvasExtent = self.iface.mapCanvas().extent() else: mapCanvasExtent = layer.extent() #print 'destination crs: %s:' % destinationCrs.toProj4() #print 'layer crs: %s:' % layerCrs.toProj4() doCrsTransform = False if not destinationCrs == layerCrs: # we have to transform the mapCanvasExtent to the data/layer Crs to be able # to retrieve the features from the data provider # but ONLY if we are working with on the fly projection # (because in that case we just 'fly' to the raw coordinates from data) if self.iface.mapCanvas().hasCrsTransformEnabled(): # only if we have 'on te fly transformation' enabled # AND the mapCanvasExtent is the real mapcanvas extent (note: if we # are going to write ALL features, then mapCanvasExtent is actually the layer extent) if self.featuresInMapcanvasOnly: crsTransform = QgsCoordinateTransform( destinationCrs, layerCrs) mapCanvasExtent = crsTransform.transformBoundingBox( mapCanvasExtent) # we have to have a transformer to do the transformation of the geometries # to the mapcanvas crs ourselves: crsTransform = QgsCoordinateTransform(layerCrs, destinationCrs) doCrsTransform = True lblSettings = QgsPalLayerSettings() lblSettings.readFromLayer(layer) # select features within current extent, # with ALL attributes, WITHIN currentExtent, WITH geom, AND using Intersect instead of bbox # we are going to group all features by their symbol so in svg we can group them in a <g> tag with the symbol style if self.isRendererV2(layer): if hasattr(layer, 'isUsingRendererV2'): # For QGis 1.8 API, new symbology provider = layer.dataProvider() provider.select(provider.attributeIndexes(), mapCanvasExtent, True, True) else: # For QGis 2.0 cleaned-up API provider = layer.getFeatures( QgsFeatureRequest().setFilterRect(mapCanvasExtent)) renderer = layer.rendererV2() if str(renderer.type()) not in ("singleSymbol", "categorizedSymbol", "graduatedSymbol"): QMessageBox.information( self.iface.mainWindow(), "Warning", "New Symbology layer found for layer '" + layer.name() + "'\n\nThis layer uses a Renderer/Style which cannot be used with this plugin.\n\nThis layer will be ignored in export." ) return "" else: # For QGis <= 1.8 API, old symbology provider = layer.dataProvider() provider.select(provider.attributeIndexes(), mapCanvasExtent, True, True) renderer = layer.renderer() symbols = renderer.symbols() symbolFeatureMap = dict.fromkeys(symbols, []) id = self.sanitizeStr(unicode(layer.name()).lower()) if labels: id = id + '_labels' svg = [ u'<g id="' + id + '" inkscape:groupmode="layer" inkscape:label="' + id + '">\n' ] # start of layer g-element # now iterate through each feature and group by feature f = QgsFeature() feature = None #print "1 symbols holds %s symbols" % len(symbols) while provider.nextFeature(f): feature = QgsFeature(f) geom = feature.geometry() if hasattr(layer, "srs"): # QGIS < 2.0 layerCrs = layer.srs() else: layerCrs = layer.crs() if doCrsTransform: if hasattr(geom, "transform"): geom.transform(crsTransform) else: QMessageBox.warning(self.iface.mainWindow( ), self.MSG_BOX_TITLE, ( "Cannot crs-transform geometry in your QGIS version ...\n" "Only QGIS version 1.5 and above can transform geometries on the fly\n" "As a workaround, you can try to save the layer in the destination crs (eg as shapefile) and reload that layer...\n" ), QMessageBox.Ok, QMessageBox.Ok) break symbol = self.symbolForFeature(layer, feature) #print "feature: %s symbol: %s rgb: %s %s %s" % (feature, symbol, symbol.color().red(), symbol.color().green(), symbol.color().blue()) # Continous Color does NOT have all symbols, but ONLY start and end color # that's why we do some extra stuff here... # ONLY needed for Old Symbology (which have 'name()' and not 'type()') if hasattr(renderer, 'name') and renderer.name() == "Continuous Color": symbols.append(symbol) if symbol in symbolFeatureMap: symbolFeatureMap[symbol] = [feature] else: symbolFeatureMap.update({symbol: [feature]}) else: if not symbol in symbolFeatureMap or len( symbolFeatureMap[symbol]) == 0: symbolFeatureMap[symbol] = [feature] else: symbolFeatureMap[symbol].append(feature) # now iterate over symbols IF there are any features in this view from this layer if feature != None: id = id + '_' i = 0 #print "2 symbols holds %s symbols" % len(symbols) for symbol in symbols: if self.isRendererV2(layer): sym = self.symbolV2(feature, symbol) else: sym = self.symbol(feature, symbol) # start of symbol g-element, holds colors and stroke etc if not labels: fill = '' if sym.has_key('fill'): fill = 'fill="' + sym['fill'] + '"' svg.append(u'<g stroke="' + sym['stroke'] + '" ' + fill + ' stroke-linejoin="' + self.strokeLineJoin + '" stroke-width="' + sym['stroke-width'] + '">\n') else: # TODO fix this if False: lc = layer.label().labelAttributes().color() lblColor = u'rgb(%s,%s,%s)' % (lc.red(), lc.green(), lc.blue()) else: # labels all black for now... lblColor = u'rgb(0,0,0)' svg.append(u'<g stroke="none" fill="' + lblColor + '">\n') for feature in symbolFeatureMap[symbol]: i = i + 1 # labeltxt is used both for the real labels, AND for the inkscape-label attributes of g and txt elements # TODO fix this if lblSettings.enabled: labeltxt = self.sanitizeStr( feature[lblSettings.fieldName]) else: #labeltxt = self.sanitizeStr(layer.label().fieldValue(0, feature)) # only first field for now. #labeltxt = self.sanitizeStr(unicode(feature.fields().field(0))) labeltxt = self.sanitizeStr('') if not labels: svg.extend( self.writeFeature(feature, id + str(i), labeltxt)) if labels: geom = feature.geometry().centroid() # centroid-method returns a NON-transformed centroid if doCrsTransform: if hasattr(geom, "transform"): geom.transform(crsTransform) else: QMessageBox.warning(self.iface.mainWindow( ), self.MSG_BOX_TITLE, ( "Cannot crs-transform geometry in your QGIS version ...\n" "Only QGIS version 1.5 and above can transform geometries on the fly\n" "As a workaround, you can try to save the layer in the destination crs (eg as shapefile) and reload that layer...\n" ), QMessageBox.Ok, QMessageBox.Ok) break svg.extend( self.label2svg( geom.asPoint(), id + str(i), self.symbolForFeature(layer, feature), labeltxt)) svg.append(u'</g>\n') # end of symbol svg.append(u'</g>\n') # end of layer return svg def symbol(self, feature, symbol): sym = {} sc = symbol.color() sym['stroke'] = u'rgb(%s,%s,%s)' % (sc.red(), sc.green(), sc.blue()) # fill color: only non line features have fill color, lines have 'none' geom = feature.geometry() if geom.wkbType() in (QGis.WKBLineString, QGis.WKBLineString25D, QGis.WKBMultiLineString, QGis.WKBMultiLineString25D): sym['fill'] = u'none' else: f = symbol.fillColor() sym['fill'] = u'rgb(%s,%s,%s)' % (f.red(), f.green(), f.blue()) # pen: in QT pen can be 0 if symbol.pen().width() < 1: sym['stroke-width'] = u'0.5' else: sym['stroke-width'] = unicode(symbol.pen().width()) return sym def symbolV2(self, feature, symbol): #print '##### symbol: %s, symbollayercount: %s' % (symbol, symbol.symbolLayerCount()) sym = {} if symbol.symbolLayerCount() > 1: QMessageBox.information( self.iface.mainWindow(), "Warning", "Layer '" + layer.name() + "' uses New Symbology, and styles with more the one Symbol Layer, only the first one will be use." ) sl = symbol.symbolLayer(0) slprops = sl.properties() #print("symbollayer properties: %s" % slprops) # region/polgyons have: color_border / style_border / offset / style / color / width_border # {PyQt4.QtCore.QString(u'color_border'): PyQt4.QtCore.QString(u'0,0,0,255'), PyQt4.QtCore.QString(u'style_border'): PyQt4.QtCore.QString(u'solid'), PyQt4.QtCore.QString(u'offset'): PyQt4.QtCore.QString(u'0,0'), PyQt4.QtCore.QString(u'style'): PyQt4.QtCore.QString(u'solid'), PyQt4.QtCore.QString(u'color'): PyQt4.QtCore.QString(u'0,0,255,255'), PyQt4.QtCore.QString(u'width_border'): PyQt4.QtCore.QString(u'0.26')} # markers/points have : color_border / offset / size / color / name / angle: # {PyQt4.QtCore.QString(u'color_border'): PyQt4.QtCore.QString(u'0,0,0,255'), PyQt4.QtCore.QString(u'offset'): PyQt4.QtCore.QString(u'0,0'), PyQt4.QtCore.QString(u'size'): PyQt4.QtCore.QString(u'2'), PyQt4.QtCore.QString(u'color'): PyQt4.QtCore.QString(u'255,0,0,255'), PyQt4.QtCore.QString(u'name'): PyQt4.QtCore.QString(u'circle'), PyQt4.QtCore.QString(u'angle'): PyQt4.QtCore.QString(u'0')} # lines have : color / offset / penstyle / width / use_custom_dash / joinstyle / customdash / capstyle: # {PyQt4.QtCore.QString(u'color'): PyQt4.QtCore.QString(u'255,255,0,255'), PyQt4.QtCore.QString(u'offset'): PyQt4.QtCore.QString(u'0'), PyQt4.QtCore.QString(u'penstyle'): PyQt4.QtCore.QString(u'solid'), PyQt4.QtCore.QString(u'width'): PyQt4.QtCore.QString(u'0.5'), PyQt4.QtCore.QString(u'use_custom_dash'): PyQt4.QtCore.QString(u'0'), PyQt4.QtCore.QString(u'joinstyle'): PyQt4.QtCore.QString(u'bevel'), PyQt4.QtCore.QString(u'customdash'): PyQt4.QtCore.QString(u'5;2'), PyQt4.QtCore.QString(u'capstyle'): PyQt4.QtCore.QString(u'square')} try: strokekey = QString(u'line_color') strokekey2 = QString(u'outline_color') colorkey = QString(u'color') stylekey = QString(u'style') width_borderkey = QString(u'width_border') widthkey = QString(u'width') except NameError: strokekey = u'line_color' strokekey2 = u'outline_color' colorkey = u'color' stylekey = u'style' width_borderkey = u'width_border' widthkey = u'width' if slprops.has_key(strokekey): stroke = unicode(slprops[strokekey]) sym['stroke'] = u'rgb(%s)' % (stroke[:stroke.rfind(',')]) elif slprops.has_key(strokekey2): stroke = unicode(slprops[strokekey2]) sym['stroke'] = u'rgb(%s)' % (stroke[:stroke.rfind(',')]) else: sym['stroke'] = u'none' # fill color: only non line features have fill color, lines have 'none' geom = feature.geometry() if slprops.has_key(colorkey): fill = unicode(slprops[colorkey]) if geom.wkbType() in (QGis.WKBLineString, QGis.WKBLineString25D, QGis.WKBMultiLineString, QGis.WKBMultiLineString25D): sym['stroke'] = u'rgb(%s)' % (fill[:fill.rfind(',')]) # points have fill and stroke sym['fill'] = u'rgb(%s)' % (fill[:fill.rfind(',')]) # if feature is line OR when there is no brush: set fill to none if geom.wkbType() in (QGis.WKBLineString, QGis.WKBLineString25D, QGis.WKBMultiLineString, QGis.WKBMultiLineString25D) \ or (slprops.has_key(stylekey) and slprops[stylekey] == 'no'): sym['fill'] = u'none' # pen: in QT pen can be 0 if slprops.has_key(width_borderkey): sym['stroke-width'] = unicode(slprops[width_borderkey]) elif slprops.has_key(widthkey): sym['stroke-width'] = unicode(slprops[widthkey]) else: sym['stroke-width'] = u'0.40' #print sym return sym def writeExtent(self): svg = [ u'<!-- QGIS extent for clipping, eg in Inkscape -->\n<g id="qgisviewbox" inkscape:groupmode="layer" inkscape:label="qgisviewbox" stroke="rgb(255,0,0)" stroke-width="1" fill="none" >\n' ] for ring in self.extentAsPoly.asPolygon(): svg.append(u'<path d="M ') first = True for point in ring: pixpoint = self.w2p(point.x(), point.y(), self.iface.mapCanvas().mapUnitsPerPixel(), self.currentExtent.xMinimum(), self.currentExtent.yMaximum()) if not first: svg.append(u'L ') svg.append( (unicode(pixpoint[0]) + ',' + unicode(pixpoint[1]) + ' ')) first = False svg.append(u'" />\n') svg.append(u'</g>') return svg def writeRaster(self, layer): # hide all layers except 'layer' and save as png image in current directory # TODO? maybe inline it in svg? # save visibility of layers visibleList = self.iface.mapCanvas().layers() legend = self.iface.legendInterface() # set all layers invisible EXCEPT layer for lyr in visibleList: if lyr != layer: legend.setLayerVisible(lyr, False) lyrName = unicode(layer.name()) imgName = lyrName + '.png' try: imgPath = self.svgFilename[:self.svgFilename.rfind('/') + 1] except NameError: imgPath = self.svgFilename[:self.svgFilename.rfind('/') + 1] # save image next to svg but put it in Image tag only the local filename self.iface.mapCanvas().saveAsImage(imgPath + imgName) # <image y="-7.7685061" x="27.115078" id="image3890" xlink:href="nl.png" /> svg = [ u'<g id="' + lyrName + '" inkscape:groupmode="layer" inkscape:label="' + lyrName + '">\n' ] #svg.append('<image y="0" x="0" xlink:href="'+imgPath+imgName+'" />') svg.append(u'<image y="0" x="0" xlink:href="' + imgName + '" />') svg.append(u'</g>') # end of raster layer # now set earlier visible layers back to visible for lyr in visibleList: legend.setLayerVisible(lyr, True) return svg def label2svg(self, point, fid, symbol, labelTxt): # <g> <text x="262.08704" y="523.79077">abc</text> </g> #point = feature.geometry().centroid().asPoint() xy = self.w2p(point.x(), point.y(), self.iface.mapCanvas().mapUnitsPerPixel(), self.currentExtent.xMinimum(), self.currentExtent.yMaximum()) inkscapeLbl = '' if len(labelTxt) > 0: inkscapeLbl = 'inkscape:label="' + unicode(labelTxt + '_lbl') + '"' svg = [ u'<text id="' + fid + '" x="' + unicode(xy[0]) + '" y="' + unicode(xy[1]) + '" ' + inkscapeLbl + '>' + unicode(labelTxt) + '</text>\n' ] return svg def sanitizeStr(self, string): # TODO: find the right way to do this return unicode(string).replace(' ', '_').replace('/', '_').replace( ',', '_').replace('.', '_') def writeFeature(self, feature, fid, labelTxt): svg = [] # <g>-element set's style attributes inkscapeLbl = '' if len(labelTxt) > 0: inkscapeLbl = 'inkscape:label="' + unicode(labelTxt) + '"' svg.append(u'<g id="' + fid + '" ' + inkscapeLbl + '>\n') geom = feature.geometry() currentExtent = self.currentExtent # https://qgis.org/api/2.18/classQGis.html#a8da456870e1caec209d8ba7502cceff7 #print(geom.wkbType(), QGis.WKBPoint, QGis.WKBPoint25D) if geom.wkbType() in (QGis.WKBPoint, QGis.WKBPoint25D): # 1 = WKBPoint point = geom.asPoint() svg.extend(self.point2svg(point, currentExtent)) if geom.wkbType() in (QGis.WKBMultiPoint, QGis.WKBMultiPoint25D): multipoint = geom.asMultiPoint() for point in multipoint: svg.extend(self.point2svg(point, currentExtent)) if geom.wkbType() in (QGis.WKBPolygon, QGis.WKBPolygon25D): # 3 = WKBTYPE.WKBPolygon: polygon = geom.asPolygon() # returns a list svg.extend(self.polygon2svg(feature, polygon, currentExtent)) if geom.wkbType() in ( QGis.WKBMultiPolygon, QGis.WKBMultiPolygon25D): # 6 = WKBTYPE.WKBMultiPolygon: multipolygon = geom.asMultiPolygon() # returns a list for polygon in multipolygon: svg.extend(self.polygon2svg(feature, polygon, currentExtent)) if geom.wkbType() in ( QGis.WKBLineString, QGis.WKBLineString25D): # 6 = WKBTYPE.WKBLineString: line = geom.asPolyline() # returns a list of points svg.extend(self.line2svg(feature, line, currentExtent)) if geom.wkbType() in ( QGis.WKBMultiLineString, QGis.WKBMultiLineString25D): # 6 = WKBTYPE.WKBLineString: multiline = geom.asMultiPolyline() # returns a list of points for line in multiline: svg.extend(self.line2svg(feature, line, currentExtent)) svg.append(u'</g>\n') return svg def point2svg(self, point, currentExtent): #point = feature.geometry().asPoint() xy = self.w2p(point.x(), point.y(), self.iface.mapCanvas().mapUnitsPerPixel(), self.currentExtent.xMinimum(), self.currentExtent.yMaximum()) #print(point, xy, point.x(), point.y(), self.iface.mapCanvas().mapUnitsPerPixel(), self.currentExtent.xMinimum(), self.currentExtent.yMaximum()) # TODO take current extent into account svg = [ '<circle cx="' + unicode(xy[0]) + '" cy="' + unicode(xy[1]) + '" r="5" />' ] return svg def symbolForFeature(self, layer, feature): if self.isRendererV2(layer): return layer.rendererV2().symbolForFeature(feature) else: # symbolForFeatures seems not to work for Old Symbology?? Do it ourselves: # OLD symbolisation: # Graduated Symbol: every symbol has BOTH upper and lower bound/value # Unique Value: every symbol has BOTH upper and lower value # Continues Color: HAS lower(==value) but NO upper value, BUT has just two symbols: MIN and MAX color, see http://doc.qgis.org/head/qgscontinuouscolorrenderer_8cpp-source.html # Single Simbol: just one symbol, return it renderer = layer.renderer() #print "renderer.name(): %s" % renderer.name() default = None if renderer.name( ) == "Single Symbol": # there is just one symbol: return it return renderer.symbols()[0] if renderer.name() == "Continuous Color": #pass # already done, should not come here !! minSymbol = renderer.symbols()[0] minValue = (minSymbol.lowerValue()).toDouble( )[0] # we know Continuous Color only works with numeric attributes minRed = minSymbol.fillColor().red() minGreen = minSymbol.fillColor().green() minBlue = minSymbol.fillColor().blue() maxSymbol = renderer.symbols()[1] maxValue = (maxSymbol.lowerValue()).toDouble( )[0] # we know Continuous Color only works with numeric attributes maxRed = maxSymbol.fillColor().red() maxGreen = maxSymbol.fillColor().green() maxBlue = maxSymbol.fillColor().blue() # create new symbol and set RGB according to calculation in http://doc.qgis.org/head/qgscontinuouscolorrenderer_8cpp-source.html symbol = QgsSymbol(minSymbol) # copy value = (feature.attributeMap( )[renderer.classificationAttributes()[0]]).toDouble( )[0] # we know Continuous Color only works with numeric attributes if (maxValue - minValue) <> 0: red = int(maxRed * (value - minValue) / (maxValue - minValue) + minRed * (maxValue - value) / (maxValue - minValue)) green = int(maxGreen * (value - minValue) / (maxValue - minValue) + minGreen * (maxValue - value) / (maxValue - minValue)) blue = int(maxBlue * (value - minValue) / (maxValue - minValue) + minBlue * (maxValue - value) / (maxValue - minValue)) else: red = minRed green = minGreen blue = minBlue newFillColor = QColor(red, green, blue) symbol.setFillColor(newFillColor) if renderer.drawPolygonOutline(): # always black color = QColor(0, 0, 0) symbol.setColor(color) else: symbol.setColor(newFillColor) return symbol # else loop over symbols to find the current symbol for this feature # value can be string or numbers... value = (feature.attributeMap()[ renderer.classificationAttributes()[0]]).toString() for symbol in renderer.symbols(): lower = symbol.lowerValue() upper = symbol.upperValue() #print "lower: %s upper: %s value: %s notlower %s, notupper %s, value==lower %s, value=upper %s, str(value)[0].isdigit() %s" % (lower, upper, value, (not lower), (not upper), (value == lower), (value == upper), str(value)[0].isdigit()) if not lower and not upper: # 'default value' given for default in Unique Value default = symbol # Unique Value symbols have the value in both upper and lower value (if values are string!) elif (value == lower and value == upper): return symbol # Unique Value symbols for numbers do not have an upper elif str(value)[0].isdigit() and not upper and float( value) == float(symbol.lowerValue()): return symbol # Graduated Classifications have lower AND uppervalues AND values are always numbers elif str(value)[0].isdigit() and upper and lower and ( float(value) >= float(symbol.lowerValue()) and float(value) <= float(symbol.upperValue())): #print "Graduated Classification: %s between %s and %s " % (value, symbol.lowerValue(), symbol.upperValue()) return symbol #else: # print "NEXT..." #print "RETURNING DEFAULT!!" return default def line2svg(self, feature, line, currentExtent): #print("calling line2svg...") linesvg = [] svg = u'' if self.svgType == SVG_TYPE_PATH: svg += '<path d="M ' else: # SVG_TYPE_SHAPE svg += '<polyline points="' lastPixel = [0, 0] insideExtent = False coordCount = 0 for point in line: if self.extentAsPoly.contains( point) or not self.featuresInMapcanvasOnly: insideExtent = True pixpoint = self.w2p(point.x(), point.y(), self.iface.mapCanvas().mapUnitsPerPixel(), currentExtent.xMinimum(), currentExtent.yMaximum()) #print(pixpoint) if lastPixel <> pixpoint: coordCount = coordCount + 1 if self.svgType == SVG_TYPE_PATH and coordCount > 1: svg += 'L ' svg += (str(pixpoint[0]) + ',' + str(pixpoint[1]) + ' ') lastPixel = pixpoint # if at least ONE pixel of this ring is in current view extent, return the area-string, otherwise return an empty string if not insideExtent: #print("SKIPPING: Ring fully outside extent...?") pass else: # check if there are more then 2 coords: very small polygons on current map can have coordinates # which if rounded to pixels all come to the same pixel, resulting in just ONE x,y coordinate # we skip these if coordCount < 2: #print("SKIPPING: Line contains just one pixel coordinate pair") pass else: svg += '"/>\n' linesvg.append(svg) #print(linesvg) return linesvg # SHAPE-svg # <polygon stroke="rgb(0,0,0)" fill="rgb(234,102,228)" stroke-width="1" points="439,238 445,230 ... 439,238"/> # or # <polygon points="439,238 445,230 ... 439,238"/> # PATH-svg # <g stroke="rgb(0,0,0)" fill="rgb(234,102,228)" stroke-width="1" ><path d="M 439 238 L 445 220 ... L 439 238" /> </g> def polygon2svg(self, feature, polygon, currentExtent): #print "calling polygon2svg..." polygonsvg = [''] for ring in polygon: svg = '' if self.svgType == SVG_TYPE_PATH: svg += '<path d="M ' else: # SVG_TYPE_SHAPE svg = '<polygon points="' lastPixel = [0, 0] insideExtent = False coordCount = 0 for point in ring: if self.extentAsPoly.contains( point) or not self.featuresInMapcanvasOnly: insideExtent = True pixpoint = self.w2p(point.x(), point.y(), self.iface.mapCanvas().mapUnitsPerPixel(), currentExtent.xMinimum(), currentExtent.yMaximum()) if lastPixel <> pixpoint: coordCount = coordCount + 1 if self.svgType == SVG_TYPE_PATH and coordCount > 1: svg += 'L ' svg += (str(pixpoint[0]) + ',' + str(pixpoint[1]) + ' ') lastPixel = pixpoint # if at least ONE pixel of this ring is in current view extent, return the area-string, otherwise return an empty string if not insideExtent: #print "RING FULLY OUTSIDE EXTENT: %s " % ring None else: # check if there are more then 2 coords: very small polygons on current map can have coordinates # which if rounded to pixels all come to the same pixel, resulting in just ONE x,y coordinate # we skip these if coordCount < 2: #print "Ring contains just one pixel coordinate pair: skipping" None else: svg += '" />\n' polygonsvg.append(svg) return polygonsvg # NOT WORKING ???? # pixpoint = m2p.transform(point.x(), point.y()) # print m2p.transform(point.x(), point.y()) # so for now: a custom 'world2pixel' method def w2p(self, x, y, mupp, minx, maxy): pixX = (x - minx) / mupp pixY = (y - maxy) / mupp return [int(pixX), int(-pixY)]