#*************************************************************************** # ms_export.py # -------------------------------------- # Date : Sun Sep 16 12:33:46 AKDT 2007 # Copyright : (C) 2008 by Gary E. Sherman # Email : sherman at mrcc dot com #*************************************************************************** #* * #* 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 class exports a QGIS project file to a mapserver .map file. # All the work is done in the writeMapFile method. The msexport binary # presents a Qt based GUI that collects the needed information for this # script. # Matthew Perry contributed major portions of this work. # Adapted by Erik van de Pol # # CHANGES SHOULD NOT BE MADE TO THE writeMapFile METHOD UNLESS YOU # ARE CHANGING THE QgsMapserverExport CLASS AND YOU KNOW WHAT YOU ARE # DOING from xml.dom import minidom from string import * import platform from qgis.core import QgsDataSourceURI from qgis.core import QgsMapLayerRegistry #from qgis.core import QgsProject from PyQt4.QtCore import QString from PyQt4.QtCore import QVariant # symbol map qgis2map_symbol = { "hard:circle" : "circle", "hard:triangle" : "triangle", "hard:equilateral_triangle" : "equilateral-triangle", "hard:rectangle" : "square", "hard:regular_star" : "star", "hard:diamond" : "diamond" } # alignment/position map qgis2map_aligment2position = { "center" : "cc", "above" : "uc", "right" : "cr", "below" : "lc", "left" : "cl", "aboveright" : "ur", "belowright" : "lr", "belowleft" : "ll", "aboveleft" : "ul" } # the keys are fonts that must be available in QGis # the values in this dictionary must correspond to # the fonts in the file denoted by the FONTSET in the mapfile # # "MS Shell Dlg 2" is the default label font in QGis. # The automatic mapping to "Tahoma" is correct for Windows 2000, Windows XP, # Windows Server 2003, Windows Vista and Windows 7. # See: http://msdn.microsoft.com/en-us/library/dd374112%28VS.85%29.aspx qgis2map_fontset = { "Arial" : "arial", "Courier" : "courier", "Georgia" : "georgia", "Times New Roman" : "times", "Trebuchet MS" : "trebuchet_ms", "Verdana" : "verdana", "Tahoma" : "tahoma", "MS Shell Dlg 2" : "tahoma" } # Note that tahoma-italic and tahoma-bold-italic do not exist. # Therefore a mapping to the corresponding verdana-variants is made # in the fonts file pointed to by the fontsPath. Feel free to map to another font there. bool2str = {True: "true", False: "false"} # This is a magic number now. Rationale? symbolSizeMultiplier = 3.5 class Qgis2MapDefaults: pass defaults = Qgis2MapDefaults() defaults.fontsPath = "./fonts/fonts.txt" defaults.symbolsPath = "./symbols/symbols.txt" if platform.system() == "Windows": defaults.mapServerUrl = "http://my.host.com/cgi-bin/mapserv.exe" else: defaults.mapServerUrl = "http://localhost/cgi-bin/mapserv" defaults.width = "100" defaults.height = "100" defaults.dump = True defaults.force = True defaults.partials = True defaults.antialias = True class Qgis2Map: def __init__(self, mapFile): self.mapFile = mapFile # init the other members that are not set by the constructor self.mapServerUrl = defaults.mapServerUrl self.fontsPath = defaults.fontsPath self.symbolsPath = defaults.symbolsPath self.units = '' self.imageType = '' self.mapName = '' self.width = defaults.width self.height = defaults.height self.minimumScale = '' self.maximumScale = '' self.template = '' self.header = '' self.footer = '' self.dump = bool2str[defaults.dump] self.force = bool2str[defaults.force] self.antialias = bool2str[defaults.antialias] self.partials = bool2str[defaults.partials] self.symbolQueue = {} def setQgsProject(self, projectFileName): try: self.projectFileName = projectFileName # create the DOM self.qgs = minidom.parse(unicode(self.projectFileName)) return True except: return False # Set the options collected from the GUI def setOptions(self, msUrl, units, image, mapname, width, height, template, header, footer, dump, force, antialias, partials, exportLayersOnly, fontsPath, symbolsPath): if msUrl.encode('utf-8') != "": self.mapServerUrl = msUrl.encode('utf-8') if fontsPath.encode('utf-8') != "": self.fontsPath = fontsPath.encode('utf-8') if symbolsPath.encode('utf-8') != "": self.symbolsPath = symbolsPath.encode('utf-8') if width.encode('utf-8') != "": self.width = width.encode('utf-8') if height.encode('utf-8') != "": self.height = height.encode('utf-8') self.units = units.encode('utf-8') self.imageType = image.encode('utf-8') self.mapName = mapname.encode('utf-8') #self.minimumScale = minscale #self.maximumScale = maxscale # TEMPLATE is needed for getFeatureInfo requests in WMS: # always set someting ... template = template.encode('utf-8') if template == "": template = "fooOnlyForWMSGetFeatureInfo" self.template = template self.header = header.encode('utf-8') self.footer = footer.encode('utf-8') #print units, image, mapname, width, height, template, header, footer self.dump = bool2str[dump] self.force = bool2str[force] self.antialias = bool2str[antialias] self.partials = bool2str[partials] self.exportLayersOnly = exportLayersOnly # method to check the project file for the exitence of postgis layers # if so it should be loaded in qgis before exporting, because a connection # to the database is needed to determine primary keys etc etc def projectHasPostgisLayers(self): # get the list of maplayer nodes maplayers = self.qgs.getElementsByTagName("maplayer") for lyr in maplayers: try: providerString = lyr.getElementsByTagName("provider")[0].childNodes[0].nodeValue.encode('utf-8') except: print "ERROR getting provider string from layer" # if providerString is null providerString = '' if providerString == 'postgres': #print "POSTGIS LAYER !!" return True return False # method to check if there are layers which use 'New Symbology' def projectHasNewSymbology(self): maplayers = self.qgs.getElementsByTagName("maplayer") for lyr in maplayers: if len(lyr.getElementsByTagName("renderer-v2"))>0: return True return False ## All real work happens here by calling methods to write the ## various sections of the map file def writeMapFile(self): # open the output file print "creating the map file" self.outFile = open(self.mapFile, 'w') logmsg = "Starting\n" if self.exportLayersOnly == False: # write the general map and web settings print " --- python : map section " self.writeMapSection() logmsg += "Wrote map section\n" print " --- python : map section done" # write the projection section print " --- python : proj section " self.writeProjectionSection() logmsg += "Wrote projection section\n" print " --- python : proj section done" # write the output format section print " --- python : outputformat section " self.writeOutputFormat() logmsg += "Wrote output format section\n" print " --- python : outputformat section done" # write the legend section print " --- python : legend section" self.writeLegendSection() logmsg += "Wrote legend section\n" print " --- python : legend section done" # write the WEB section print " --- python : web section " webMsg = self.writeWebSection() logmsg += "Wrote web section\n" logmsg += webMsg print " --- python : web section done" # write the LAYER sections print " --- python : layer section " layersMsg = self.writeMapLayers() logmsg += "Wrote map layers\n" logmsg += layersMsg print " --- python : layer section done" if self.exportLayersOnly == False: # we use an external synbol set instead # write the symbol defs section # must happen after layers so we can build a symbol queue #print " --- python : symbol section " #self.writeSymbolSection() #logmsg += "Wrote symbol section\n" #print " --- python : symbol section done" # END and close the map file self.outFile.write("END") self.outFile.close() logmsg += "Map file completed for " + self.projectFileName + "\n" logmsg += "Map file saved as " + self.mapFile + "\n" if self.exportLayersOnly: logmsg += "\n> We only saved the LAYER portion of the map file. \nMerge this into an excisting map file to see it working\n" else: logmsg += "\n> If this mapfile is accessible by your mapserver, you\nshould be able to see the capabilities by firing this url:\n" + self.mapServerUrl + "?MAP="+self.mapFile+"&SERVICE=WMS&VERSION=1.1.1&REQUEST=GetCapabilities\n" logmsg += "\n> if this mapfile is accessible by your mapserver, you\nshould be able to see a map by firing this url:\n" + self.mapServerUrl + "?MAP="+self.mapFile+"&LAYERS=ALL&MODE=MAP\n" return logmsg # Write the general parts of the map section def writeMapSection(self): self.outFile.write("# Map file created from QGIS project file " + str(self.projectFileName).encode('utf-8') + "\n") self.outFile.write("# Edit this file to customize for your map interface\n") self.outFile.write("# (Created with PyQgis MapServer Export plugin)\n") self.outFile.write("MAP\n") self.outFile.write(" NAME \"" + self.mapName + "\"\n") self.outFile.write(" # Map image size\n") if self.width == '' or self.height == '': self.outFile.write(" SIZE 0 0\n") else: self.outFile.write(" SIZE " + str(self.width) + " " + str(self.height) + "\n") self.outFile.write(" UNITS %s\n" % (self.units)) self.outFile.write("\n") # extents self.outFile.write(self.getExtentString()) self.outFile.write(" FONTSET '" + self.fontsPath + "'\n") # use of external symbol set self.outFile.write(" SYMBOLSET '" + self.symbolsPath + "'\n") def getExtentString(self): stringToAddTo = "" xmin = self.qgs.getElementsByTagName("xmin") stringToAddTo += " EXTENT " stringToAddTo += xmin[0].childNodes[0].nodeValue.encode('utf-8') stringToAddTo += " " ymin = self.qgs.getElementsByTagName("ymin") stringToAddTo += ymin[0].childNodes[0].nodeValue.encode('utf-8') stringToAddTo += " " xmax = self.qgs.getElementsByTagName("xmax") stringToAddTo += xmax[0].childNodes[0].nodeValue.encode('utf-8') stringToAddTo += " " ymax = self.qgs.getElementsByTagName("ymax") stringToAddTo += ymax[0].childNodes[0].nodeValue.encode('utf-8') stringToAddTo += "\n" return stringToAddTo # Write the OUTPUTFORMAT section def writeOutputFormat(self): self.outFile.write(" # Background color for the map canvas -- change as desired\n") self.outFile.write(" IMAGECOLOR 255 255 255\n") self.outFile.write(" IMAGEQUALITY 95\n") self.outFile.write(" IMAGETYPE " + self.imageType + "\n") self.outFile.write("\n") self.outFile.write(" OUTPUTFORMAT\n") self.outFile.write(" NAME " + self.imageType + "\n") if self.imageType == 'agg': self.outFile.write(" DRIVER AGG/PNG\n") self.outFile.write(" IMAGEMODE RGB\n") else: self.outFile.write(" DRIVER 'GD/" + self.imageType.upper() + "'\n") self.outFile.write(" MIMETYPE 'image/" + lower(self.imageType) + "'\n") if self.imageType.lower() != "gif": self.outFile.write(" IMAGEMODE RGBA\n") self.outFile.write(" EXTENSION '" + lower(self.imageType) + "'\n") self.outFile.write(" END\n") # Write Projection section def writeProjectionSection(self): # Need to get the destination srs from one of the map layers since # the project file doesn't contain the epsg id or proj4 text for # the map apart from that defined in each layer self.outFile.write(" PROJECTION\n") # Get the proj4 text from the first map layer's destination SRS destsrs = self.qgs.getElementsByTagName("destinationsrs")[0] proj4Text = destsrs.getElementsByTagName("proj4")[0].childNodes[0].nodeValue.encode('utf-8') # the proj4 text string needs to be reformatted to make mapserver happy self.outFile.write(self.formatProj4(proj4Text)) self.outFile.write(" END\n\n") # Write the LEGEND section def writeLegendSection(self): self.outFile.write(" # Legend\n") self.outFile.write(" LEGEND\n") self.outFile.write(" IMAGECOLOR 255 255 255\n") self.outFile.write(" STATUS ON\n") self.outFile.write(" KEYSIZE 18 12\n") self.outFile.write(" LABEL\n") self.outFile.write(" TYPE BITMAP\n") self.outFile.write(" SIZE MEDIUM\n") self.outFile.write(" COLOR 0 0 89\n") self.outFile.write(" END\n") self.outFile.write(" END\n\n") # groups are ignored as of yet self.legendlayerfileNodesById = {} for legendlayerfileNode in self.qgs.getElementsByTagName("legend")[0].getElementsByTagName("legendlayerfile"): key = legendlayerfileNode.getAttribute("layerid").encode("utf-8") if (key != ""): self.legendlayerfileNodesById[key] = legendlayerfileNode # Write the symbol definitions def writeSymbolSection(self): for symbol in self.symbolQueue.keys(): self.outFile.write( self.symbolQueue[symbol] ) self.outFile.write( "\n" ) # Write the WEB section of the map file def writeWebSection(self): resultMsg = "" self.outFile.write(" # Web interface definition. Only the template parameter\n") self.outFile.write(" # is required to display a map. See MapServer documentation\n") self.outFile.write(" WEB\n") self.outFile.write(" # Set IMAGEPATH to the path where MapServer should\n") self.outFile.write(" # write its output.\n") self.outFile.write(" IMAGEPATH '/tmp/'\n") self.outFile.write("\n") self.outFile.write(" # Set IMAGEURL to the url that points to IMAGEPATH\n") self.outFile.write(" # as defined in your web server configuration\n") self.outFile.write(" IMAGEURL '/tmp/'\n") self.outFile.write("\n") destsrs = self.qgs.getElementsByTagName("destinationsrs")[0] try: epsg = destsrs.getElementsByTagName("srid")[0].childNodes[0].nodeValue.encode("utf-8") except: # default to epsg epsg="4326" self.outFile.write(" # WMS server settings\n") self.outFile.write(" METADATA\n") self.outFile.write(" 'ows_title' '" + self.mapName + "'\n") # if mapserverurl is still defaults.mapServerUrl, give warning if defaults.mapServerUrl==self.mapServerUrl: resultMsg += " ! MapServer url still default value: '" + defaults.mapServerUrl + \ "'?\n Be sure there is a valid mapserverurl in the 'ows_onlineresource'.\n" self.outFile.write(" 'ows_onlineresource' '" + self.mapServerUrl + "?" + "map" + "=" + self.mapFile + "'\n") self.outFile.write(" 'ows_srs' 'EPSG:" + epsg + "'\n") self.outFile.write(" END\n\n") self.outFile.write(" #Scale range at which web interface will operate\n") if self.minimumScale != "": self.outFile.write(" MINSCALE " + self.minimumScale + "\n") if self.maximumScale != "": self.outFile.write(" MAXSCALE " + self.maximumScale + "\n") self.outFile.write(" # Template and header/footer settings\n") self.outFile.write(" # Only the template parameter is required to display a map. See MapServer documentation\n") if self.template != "": self.outFile.write(" TEMPLATE '" + self.template + "'\n") if self.header != "": self.outFile.write(" HEADER '" + self.header + "'\n") if self.footer != "": self.outFile.write(" FOOTER '" + self.footer + "'\n") self.outFile.write(" END\n\n") return resultMsg # Write the map layers - we have to defer writing to disk so we # can invert the order of the layes, since they are opposite in QGIS # compared to mapserver def writeMapLayers(self): resultMsg = '' # get the layers from the legend to be able to determine the order later legend_layers = self.qgs.getElementsByTagName("legendlayerfile") self.layer_order = list() for legend_layer in legend_layers: self.layer_order.append(legend_layer.getAttribute("layerid").encode('utf-8')) # get the list of maplayer nodes maplayers = self.qgs.getElementsByTagName("maplayer") print "Processing ", len(maplayers), " layers" count = 0 layer_list = dict() layer_names = [] for lyr in maplayers: count += 1 print "Processing layer ", count # The attributes of the maplayer tag contain the scale dependent settings, # visibility, and layer type layer_def = " LAYER\n" # store name of the layer - replace space with underscore for wms compliance layer_name = lyr.getElementsByTagName("layername")[0].childNodes[0].nodeValue.encode('utf-8').replace("\"", "").replace(" ","_") # layername is not unique in qgis, store id of layer layer_id = lyr.getElementsByTagName("id")[0].childNodes[0].nodeValue.encode('utf-8') # first check to see if there is a name if len(layer_name) > 0: # WMS layernames should be unique, so # if the layer_name already excists in our layer_list of names: if layer_name in layer_names: # we give it the old name plus number layer_name = layer_name + str(count) else: # if no name for the layer, manufacture one layer_name = 'layer' + str(count) # store the name to be able to check for double names layer_names.append(layer_name) layer_def += " NAME '%s'\n" % layer_name if lyr.getAttribute("type").encode('utf-8') == 'vector': layer_def += " TYPE " + lyr.getAttribute("geometry").encode('utf-8').upper() + "\n" elif lyr.getAttribute("type").encode('utf-8') == 'raster': layer_def += " TYPE " + lyr.getAttribute("type").encode('utf-8').upper() + "\n" # Use (global) default value from the gui layer_def += " DUMP " + self.dump + "\n" # id dump = true: add TEMPLATE to be able to use getFeatureInfoRequests if self.dump=="true": layer_def += " TEMPLATE fooOnlyForWMSGetFeatureInfo\n" # Set min/max scales if lyr.getAttribute('hasScaleBasedVisibilityFlag').encode('utf-8') == 1: layer_def += " MINSCALE " + lyr.getAttribute('minimumScale').encode('utf-8') + "\n" layer_def += " MAXSCALE " + lyr.getAttribute('maximumScale').encode('utf-8') + "\n" layer_def += self.getExtentString() # data dataString = lyr.getElementsByTagName("datasource")[0].childNodes[0].nodeValue.encode('utf-8') # test if it is a postgis, grass or WMS layer # is there a better way to do this? probably. try: providerString = lyr.getElementsByTagName("provider")[0].childNodes[0].nodeValue.encode('utf-8') except: # if providerString is null providerString = '' if providerString == 'postgres': # it's a postgis layer uri = QgsDataSourceURI(dataString) layer_def += " CONNECTIONTYPE postgis\n" connectionInfo = str(uri.connectionInfo()) # if connectionInfo does NOT contain a password, warn user: if connectionInfo.find("password")<0: resultMsg += " ! No password in connection string for postgres layer '" + layer_name + \ "' \n Add it, or make sure mapserver can connect to postgres.\n" layer_def += " CONNECTION \"" + connectionInfo + "\"\n" # EvdP: it seems that the uri.geometryColumn() is quoted automatically by PostGIS. # To prevent double quoting, we don't quote here. # Now we are unable to process uri.geometryColumn()s with special characters (uppercase... etc.) #layer_def += " DATA '\"" + uri.geometryColumn() + "\" FROM " + uri.quotedTablename() + "'\n" #layer_def += " DATA '" + uri.geometryColumn() + " FROM " + uri.quotedTablename() + "'\n" layer_id = lyr.getElementsByTagName("id")[0].childNodes[0].nodeValue.encode("utf-8") uniqueId = self.getPrimaryKey(layer_id, uri.table()) # %tablename% is returned when no uniqueId is found: inform user if uniqueId.find("%") >= 0: resultMsg += " ! No primary key found for postgres layer '" + layer_name + \ "' \n Make sure you edit the mapfile and change the DATA-string \n containing '" + uniqueId + "'\n" epsg = self.getEpsg(lyr) layer_def += " DATA '" + uri.geometryColumn() + " FROM " + uri.quotedTablename() + " USING UNIQUE " + uniqueId + " USING srid=" + epsg + "'\n" # don't write the filter keyword if there isn't one if uri.sql() != "": layer_def += " FILTER ( " + uri.sql() + " )\n" elif providerString == 'wms' and lyr.getAttribute("type").encode('utf-8').upper() == 'RASTER': # it's a WMS layer layer_def += " CONNECTIONTYPE WMS\n" layer_def += " CONNECTION '" + dataString + "'\n" rasterProp = lyr.getElementsByTagName("rasterproperties")[0] # loop thru wmsSubLayers wmsSubLayers = rasterProp.getElementsByTagName('wmsSublayer') wmsNames = [] wmsStyles = [] for wmsLayer in wmsSubLayers: wmsNames.append( wmsLayer.getElementsByTagName('name')[0].childNodes[0].nodeValue.encode('utf-8').replace("\"", "") ) try: wmsStyles.append( wmsLayer.getElementsByTagName('style')[0].childNodes[0].nodeValue.encode('utf-8') ) except: wmsStyles.append( '' ) # Create necesssary wms metadata format = rasterProp.getElementsByTagName('wmsFormat')[0].childNodes[0].nodeValue.encode('utf-8') layer_def += " METADATA\n" layer_def += " 'ows_name' '" + ','.join(wmsNames) + "'\n" layer_def += " 'wms_server_version' '1.1.1'\n" try: #ct = lyr.getElementsByTagName('coordinatetransform')[0] #srs = ct.getElementsByTagName('sourcesrs')[0].getElementsByTagName('spatialrefsys')[0] #epsg = srs.getElementsByTagName('epsg')[0].childNodes[0].nodeValue.encode('utf-8') #layer_def += " 'wms_srs' 'EPSG:4326 EPSG:" + epsg + "'\n" layer_def += " 'ows_srs' 'EPSG:" + self.getEpsg(lyr) + "'\n" # TODO: add epsg to all METADATA tags?? except: print "ERROR while trying to write ows_srs METADATA" pass layer_def += " 'wms_format' '" + format + "'\n" layer_def += " 'wms_style' '" + ','.join(wmsStyles) + "'\n" layer_def += " END\n" elif providerString == 'ogr': layer_def += " DATA '" + dataString.split('|')[0] + "'\n" else: # it's a standard gdal or grass layer layer_def += " DATA '" + dataString + "'\n" # WMS settings for all layers layer_def += " METADATA\n" layer_def += " 'ows_title' '" + layer_name + "'\n" layer_def += " END\n" layer_def += " STATUS OFF\n" # turn status in MapServer on or off based on visibility in QGis: # layer_id = lyr.getElementsByTagName("id")[0].childNodes[0].nodeValue.encode("utf-8") # legendLayerNode = self.legendlayerfileNodesById[layer_id] # if legendLayerNode.getAttribute("visible").encode("utf-8") == "1": # layer_def += " STATUS ON\n" # else: # layer_def += " STATUS OFF\n" opacity = int ( 100.0 * float(lyr.getElementsByTagName("transparencyLevelInt")[0].childNodes[0].nodeValue.encode('utf-8')) / 255.0 ) layer_def += " TRANSPARENCY " + str(opacity) + "\n" layer_def += " PROJECTION\n" # Get the destination srs for this layer and use it to create the projection section destsrs = self.qgs.getElementsByTagName("destinationsrs")[0] proj4Text = destsrs.getElementsByTagName("proj4")[0].childNodes[0].nodeValue.encode('utf-8') # TODO: you would think: take DATA-srs here, but often projected # shapefiles do not contain srs data ... If we want to do this # we should take make the map-srs the qgs-project srs first # instead of taking the srs of the first layer # Get the data srs for this layer and use it to create the projection section # datasrs = lyr.getElementsByTagName("srs")[0] # proj4Text = datasrs.getElementsByTagName("proj4")[0].childNodes[0].nodeValue.encode('utf-8') # the proj4 text string needs to be reformatted to make mapserver happy layer_def += self.formatProj4(proj4Text) layer_def += " END\n" scaleDependent = lyr.getAttribute("hasScaleBasedVisibilityFlag").encode('utf-8') if scaleDependent == '1': # get the min and max scale settings minscale = lyr.getAttribute("minimumScale").encode('utf-8') maxscale = lyr.getAttribute("maximumScale").encode('utf-8') if minscale > '': layer_def += " MINSCALE " + minscale + "\n" if maxscale > '': layer_def += " MAXSCALE " + maxscale + "\n" # Check for label field (ie LABELITEM) and label status try: labelElements = lyr.getElementsByTagName("label") labelOn = '0' labelField = None # there are actually 3 different label-element in a layer element: for element in labelElements: labelParent = element.parentNode.localName if labelParent == 'maplayer': labelOn = element.childNodes[0].nodeValue.encode('utf-8') if labelParent == 'labelattributes': labelField = element.getAttribute('fieldname').encode('utf-8') if labelField != '' and labelField is not None and labelOn == "1" and labelOn is not None: layer_def += " LABELITEM '" + labelField + "'\n" except: # no labels pass # write the CLASS section for rendering # First see if there is a single symbol renderer if lyr.getElementsByTagName("singlesymbol").length > 0: layer_def += self.simpleRenderer(lyr, lyr.getElementsByTagName("singlesymbol")[0].getElementsByTagName('symbol')[0] ) elif lyr.getElementsByTagName("graduatedsymbol").length > 0: layer_def += self.graduatedRenderer(lyr, lyr.getElementsByTagName("graduatedsymbol")[0].getElementsByTagName('symbol')[0] ) elif lyr.getElementsByTagName("continuoussymbol").length > 0: layer_def += self.continuousRenderer(lyr, lyr.getElementsByTagName("continuoussymbol")[0] ) elif lyr.getElementsByTagName("uniquevalue").length > 0: layer_def += self.uniqueRenderer(lyr, lyr.getElementsByTagName("uniquevalue")[0].getElementsByTagName('symbol')[0] ) # end of LAYER layer_def += " END\n\n" # add the layer to the list with layer_id as key layer_list[layer_id] = layer_def # all layers have been processed, reverse the layer_order and write # output layer_def's in order as they appear in legend (as seen by user) self.layer_order.reverse() for layerid in self.layer_order: self.outFile.write(layer_list[layerid]) return resultMsg def getEpsg(self, lyr): try: srs = lyr.getElementsByTagName('srs')[0].getElementsByTagName('spatialrefsys')[0] return srs.getElementsByTagName('srid')[0].childNodes[0].nodeValue.encode('utf-8') except: #Use 4326 as a sensible default if the above fails return "4326" def getPrimaryKey(self, layerId, tableName): """ Since we have no Python bindings for "src\providers\postgres\qgspostgresprovider.cpp" we approximate the primary key by finding an integer type field containing "fid" or "id" This is obviously a lousy solution at best. This script requires the project you export to be open in QGis!! This method will return either the primary key of this table, or the string %tablename% in case we're not able to find it... """ mlr = QgsMapLayerRegistry.instance() layers = mlr.mapLayers() if not QString(layerId) in layers: # layerId of this postgis layer NOT in the layerlist... # probably the project is not loaded in qgis #raise Exception("ERROR: layer not found in project layers.... \nThis happens with postgis layers in a project which \nis not loaded in QGis.\nDid you load this project into QGis? \nIf not please load project first, and then export it to mapserver.") return str("%" + tableName + "_id%") layer = layers[QString(layerId)] dataProvider = layer.dataProvider() fields = dataProvider.fields() intTypes = [QVariant.Int, QVariant.LongLong, QVariant.UInt, QVariant.ULongLong] integerFields = [] for id, field in fields.iteritems(): if field.type() in intTypes: integerFields.append(id) # fid end fidIntegerFields = [] for id in integerFields: if fields[id].name().endsWith("fid"): fidIntegerFields.append(id) if len(fidIntegerFields) == 1: return str(fields[fidIntegerFields[0]].name()) # fid start fidIntegerFields[:] = [] for id in integerFields: if fields[id].name().startsWith("fid"): fidIntegerFields.append(id) if len(fidIntegerFields) == 1: return str(fields[fidIntegerFields[0]].name()) # id end idIntegerFields = [] for id in integerFields: if fields[id].name().endsWith("id"): idIntegerFields.append(id) if len(idIntegerFields) == 1: return str(fields[idIntegerFields[0]].name()) # id start idIntegerFields[:] = [] for id in integerFields: if fields[id].name().startsWith("id"): idIntegerFields.append(id) if len(idIntegerFields) == 1: return str(fields[idIntegerFields[0]].name()) # if we arrive here we have ambiguous or no primary keys #print "Error: Could not find primary key from field type and field name information.\n" # using a mapfile pre-processor, the proper id field can be substituted in the following: return str("%" + tableName + "_id%") # Get RGB code from a XML color node, returning string like '45 124 255' def getRgbFromNode(self, symbolNode, elementName): if symbolNode == None or symbolNode.getElementsByTagName(elementName).length == 0: return '' colorNode = symbolNode.getElementsByTagName(elementName)[0] return colorNode.getAttribute('red').encode('utf-8') + ' ' + colorNode.getAttribute('green').encode('utf-8') + ' ' + colorNode.getAttribute('blue').encode('utf-8') # Get a size (multiplied by symbolSizeMultiplier) from given sizeNode's child with elementName def getSizeStringFromNode(self, symbolNode, elementName): if symbolNode == None or symbolNode.getElementsByTagName(elementName).length == 0: return '' size = float(symbolNode.getElementsByTagName(elementName)[0].childNodes[0].nodeValue.encode('utf-8')) symbolSize = size * symbolSizeMultiplier return str(symbolSize) def getSymbolProperty(self, symbolNode, elementName): if symbolNode == None or symbolNode.getElementsByTagName(elementName).length == 0: return '' return str(symbolNode.getElementsByTagName(elementName)[0].childNodes[0].nodeValue.encode('utf-8')) def writeClassStyleContent(self, symbolNode, geometry): outlinecolor = self.getRgbFromNode(symbolNode, 'outlinecolor') outlinestyle = self.getSymbolProperty(symbolNode, 'outlinestyle') outlinewidth = self.getSizeStringFromNode(symbolNode, 'outlinewidth') fillcolor = self.getRgbFromNode(symbolNode, 'fillcolor') pointsize = self.getSizeStringFromNode(symbolNode, 'pointsize') fillpattern = self.getSymbolProperty(symbolNode, 'fillpattern'); class_def = " STYLE\n" # for POINT by SYMBOL and SIZE if geometry == 'POINT': # use the point symbol map to lookup the mapserver symbol type symbol = self.msSymbol( geometry, symbolNode ) class_def += " SYMBOL " + symbol + " \n" class_def += " SIZE " + pointsize + " \n" # for LINE and POLYGON size define by WIDTH else: class_def += " WIDTH " + outlinewidth + " \n" # for LINE defined by COLOR from outlinecolor if geometry == 'LINE': class_def += " COLOR " + outlinecolor + "\n" # for POLYGON and POINT defined by COLOR from fillcolor and OUTLINECOLOR from outlinecolor else: if 'NoPen' != outlinestyle: class_def += " OUTLINECOLOR " + outlinecolor + "\n" if 'NoBrush' != fillpattern: class_def += " COLOR " + fillcolor + "\n" class_def += " END\n" return class_def # Simple renderer ouput # We need the layer node and symbol node def simpleRenderer(self, layerNode, symbolNode): # get the layers geometry type geometry = layerNode.getAttribute("geometry").encode('utf-8').upper() class_def = " CLASS\n" class_def += " NAME '" + layerNode.getElementsByTagName("layername")[0].childNodes[0].nodeValue.encode('utf-8').replace("\"", "") + "' \n" class_def += self.writeClassStyleContent(symbolNode, geometry) class_def += self.msLabel( layerNode ) # end of CLASS class_def += " END\n" return class_def # Graduated symbol renderer output def graduatedRenderer(self, layerNode, symbolNode): # get the layers geometry type geometry = layerNode.getAttribute("geometry").encode('utf-8').upper() # get the renderer field for building up the classes classField = layerNode.getElementsByTagName('classificationattribute')[0].childNodes[0].nodeValue.encode('utf-8') # write the render item class_def = " CLASSITEM '" + classField + "'\n" # write the rendering info for each class classes = layerNode.getElementsByTagName('symbol') for cls in classes: class_def += " CLASS\n" lower = cls.getElementsByTagName('lowervalue')[0].childNodes[0].nodeValue.encode('utf-8') upper = cls.getElementsByTagName('uppervalue')[0].childNodes[0].nodeValue.encode('utf-8') # If there's a label use it, otherwise autogenerate one try: label = cls.getElementsByTagName('label')[0].childNodes[0].nodeValue.encode('utf-8') class_def += " NAME '" + label + "'\n" except: class_def += " NAME '" + lower + " < " + classField + " < " + upper + "'\n" class_def += " EXPRESSION ( ([" + classField + "] >= " + lower + ") AND ([" + classField + "] <= " + upper + ") )\n" class_def += self.writeClassStyleContent(cls, geometry) # label class_def += self.msLabel( layerNode ) # end of CLASS class_def += " END\n" return class_def # Continuous symbol renderer output def continuousRenderer(self, layerNode, symbolNode): # get the layers geometry type geometry = layerNode.getAttribute("geometry").encode('utf-8').upper() # get the renderer field for building up the classes classField = layerNode.getElementsByTagName('classificationattribute')[0].childNodes[0].nodeValue.encode('utf-8') # write the rendering info for each class class_def = " CLASS\n" # Class name irrelevant for color ramps since mapserver can't render their legend #self.outFile.write(" NAME '" + classField + "'\n") # color lower = symbolNode.getElementsByTagName('lowestsymbol')[0].getElementsByTagName('symbol')[0] upper = symbolNode.getElementsByTagName('highestsymbol')[0].getElementsByTagName('symbol')[0] lowerColor = lower.getElementsByTagName('outlinecolor')[0] upperColor = upper.getElementsByTagName('outlinecolor')[0] # outline color outlineNode = lower.getElementsByTagName('outlinecolor')[0] class_def += " STYLE\n" # The first and last color of the ramp ( r g b r g b ) class_def += " COLORRANGE " + lowerColor.getAttribute('red').encode('utf-8') + " " + lowerColor.getAttribute('green').encode('utf-8') + " " + lowerColor.getAttribute('blue').encode('utf-8') + " " + upperColor.getAttribute('red').encode('utf-8') + " " + upperColor.getAttribute('green').encode('utf-8') + " " + upperColor.getAttribute('blue').encode('utf-8') + "\n" # The range of values over which to ramp the colors class_def += " DATARANGE " + lower.getElementsByTagName('lowervalue')[0].childNodes[0].nodeValue.encode('utf-8') + ' ' + upper.getElementsByTagName('lowervalue')[0].childNodes[0].nodeValue.encode('utf-8') + '\n' class_def += " RANGEITEM '" + classField + "'\n" # upper and lower have same size outlinecolor = self.getRgbFromNode(upper, 'outlinecolor') fillcolor = self.getRgbFromNode(upper, 'fillcolor') outlinewidth = self.getSizeStringFromNode(upper, 'outlinewidth') size = float(lower.getElementsByTagName('pointsize')[0].childNodes[0].nodeValue.encode('utf-8')) pointsize = 2 * size * symbolSizeMultiplier # for POINT by SYMBOL and SIZE if geometry == 'POINT': # use the point symbol map to lookup the mapserver symbol type symbol = self.msSymbol( geometry, symbolNode ) class_def += " SYMBOL " + symbol + " \n" class_def += " SIZE " + str(pointsize) + " \n" # for LINE and POLYGON size define by WIDTH else: class_def += " WIDTH " + outlinewidth + " \n" # for LINE defined by COLOR from outlinecolor if geometry == 'LINE': class_def += " COLOR " + outlinecolor + "\n" # for POLYGON and POINT defined by COLOR from fillcolor and OUTLINECOLOR from outlinecolor else: class_def += " COLOR " + fillcolor + "\n" class_def += " END\n" # only outlines for polygons outline = symbolNode.getElementsByTagName('polygonoutline') if geometry == 'POLYGON' and outline is not None and '1' == outline[0].childNodes[0].nodeValue.encode('utf-8'): class_def += " STYLE\n" class_def += " WIDTH 1 " + "\n" class_def += " OUTLINECOLOR 0 0 0" + "\n" class_def += " END\n" # label class_def += self.msLabel( layerNode ) # end of CLASS class_def += " END\n" return class_def # Unique value renderer output def uniqueRenderer(self, layerNode, symbolNode): # get the renderer field for building up the classes classField = layerNode.getElementsByTagName('classificationattribute')[0].childNodes[0].nodeValue.encode('utf-8') # get the layers geometry type geometry = layerNode.getAttribute("geometry").encode('utf-8').upper() # write the render item class_def = " CLASSITEM '" + classField + "'\n" # write the rendering info for each class classes = layerNode.getElementsByTagName('symbol') for cls in classes: class_def += " CLASS\n" try: lower = cls.getElementsByTagName('lowervalue')[0].childNodes[0].nodeValue.encode('utf-8') except IndexError: # set to blank in the case where the field used for rendering has no value lower = "" # If there's a label use it, otherwise autogenerate one try: label = cls.getElementsByTagName('label')[0].childNodes[0].nodeValue.encode('utf-8') class_def += ' NAME "' + label + '"\n' except: class_def += ' NAME "' + classField + ' = ' + lower + '" \n' class_def += ' EXPRESSION "' + lower + '" \n' class_def += self.writeClassStyleContent(cls, geometry) # label class_def += self.msLabel( layerNode ) # end of CLASS class_def += " END\n" return class_def # Utility method to format a proj4 text string into mapserver format def formatProj4(self, proj4text): parms = proj4text.split(" ") ret = "" for p in parms: p = p.replace("+","") ret = ret + " '" + p + "'\n" return ret def getProj4(self, proj4text): """Returns the proj4 string as a dictionary with key value pairs.""" parms = proj4text.split(" ") ret = {} for p in parms: p = p.replace("+","") keyValue = p.split("=") key = keyValue[0] value = "" try: value = keyValue[1] except: value = "" if key != "": ret[key] = value return ret # Determines the symbol name and adds it to the symbol queue def msSymbol(self, geometry, symbolNode): # contains the same markup for a layer regardless of type # so we infer a symbol type based on the geometry symbolName = '' symbol = '0' if geometry == 'POLYGON': #symbol = '0' pass elif geometry == 'LINE': #symbol = '0' pass elif geometry == 'POINT': try: symbolName = qgis2map_symbol[symbolNode.getElementsByTagName('pointsymbol')[0].childNodes[0].nodeValue.encode('utf-8')] except: symbolName = "circle" # make sure it's double quoted symbol = "\"" + symbolName + "\"" # a symbol set in an external symbol.txt file is used; see comment on top of this file # if symbolName == 'CIRCLE': # self.symbolQueue['CIRCLE'] = """ # #Circle symbol # SYMBOL # NAME 'CIRCLE' # TYPE ellipse # FILLED true # POINTS # 1 1 # END # END """ # # if symbolName == 'TRIANGLE': # self.symbolQueue['TRIANGLE'] = """ # SYMBOL # NAME "TRIANGLE" # TYPE vector # FILLED true # POINTS # 0 1 # .5 0 # 1 1 # 0 1 # END # END """ return symbol # Label block creation def msLabel(self, layerNode): # currently a very basic bitmap font labelNode = layerNode.getElementsByTagName('labelattributes')[0] #labelField = labelNode.getElementsByTagName('label')[0].getAttribute('field').encode('utf-8') # why was the attribute 'field' and not 'fieldname'? labelField = labelNode.getElementsByTagName('label')[0].getAttribute('fieldname').encode('utf-8') if labelField != '' and labelField is not None: labelBlock = " LABEL \n" # see comment at 'qgis2ms_fontset' fontQgis = labelNode.getElementsByTagName('family')[0].getAttribute('name').encode('utf-8') fontMs = "" try: fontMs = qgis2map_fontset[fontQgis] except: # we default to the first font in the fontset, if any are present if len(qgis2map_fontset) > 0: try: fontMs = qgis2map_fontset["MS Shell Dialog 2"] except: sortedKeys = qgis2map_fontset.keys() sortedKeys.sort() fontMs = qgis2map_fontset[sortedKeys[0]] else: fontMs = "" bold = bool(int(labelNode.getElementsByTagName('bold')[0].getAttribute('on').encode("utf-8"))) italic = bool(int(labelNode.getElementsByTagName('italic')[0].getAttribute('on').encode("utf-8"))) # "-bold" and "-italic" must correspond with the fontset file # font can be both bold and italic labelBlock += " FONT " + fontMs if bold: labelBlock += "-bold" if italic: labelBlock += "-italic" labelBlock += "\n" labelBlock += " TYPE truetype\n" size = self.getFieldName(labelNode, 'size') if size == "": sizeNode = labelNode.getElementsByTagName('size')[0] units = sizeNode.getAttribute("units").encode("utf-8") sizeValue = int(sizeNode.getAttribute("value").encode("utf-8")) # we must convert to px for use in the mapfile sizePx = 11 # default sizePx = sizeValue #if units == "pt": sizePx = int(sizeValue / 0.75) # TODO: find appropriate conversion metric from map units to pixels #elif unit == "mu": #proj4Elem = labelNode.parentNode.getElementsByTagName("proj4")[0].childNodes[0] #proj4str = proj4Elem.nodeValue.encode('utf-8') #proj4Dict = self.getProj4(proj4str) #for i,j in proj4Dict.iteritems(): #labelBlock += str(i) + ":: " + str(j) + "\n" # #sizePx = ????? proj4Dict["units"] ?? # non-used unit types: #elif unit == "em": sizePx = int(size * 16.0) #elif unit == "px": sizePx = size size = str(sizePx) labelBlock += " SIZE " + size + "\n" color = self.getFieldName(labelNode, 'color') if color == "": colorNode = labelNode.getElementsByTagName('color')[0] r = int(colorNode.getAttribute("red")) g = int(colorNode.getAttribute("green")) b = int(colorNode.getAttribute("blue")) color = str(r) + " " + str(g) + " " + str(b) labelBlock += " COLOR " + color + "\n" # Include label angle if specified # Note that angles only work for truetype fonts angle = self.getFieldName(labelNode, 'angle') if angle == "": angle = labelNode.getElementsByTagName('angle')[0].getAttribute('value').encode('utf-8') labelBlock += " ANGLE " + angle + "\n" # Include label buffer if specified # Note that the buffer has different meaning in qgis vs mapserver # mapserver just adds blank space around the label while # qgis uses a fill color around the label # Note that buffer only works for truetype fonts buffer = labelNode.getElementsByTagName('buffersize')[0].getAttribute('value').encode('utf-8') bufferon = labelNode.getElementsByTagName('bufferenabled')[0].getAttribute('on').encode('utf-8') buffercolor= self.getRgbFromNode(labelNode, 'buffercolor') if bufferon is not None and '1' == bufferon: # not sure if we should use this BUFFER #labelBlock += " BUFFER " + buffer + "\n" labelBlock += " BACKGROUNDCOLOR " + buffercolor + "\n" # alignment in QGis corresponds to position in MapServer alignment = labelNode.getElementsByTagName('alignment')[0].getAttribute('value').encode('utf-8') try: labelBlock += " POSITION " + qgis2map_aligment2position[alignment] + "\n" except: # default to center if we encounter a nonsensical value labelBlock += " POSITION cc\n" #values from the gui: labelBlock += " FORCE " + self.force + "\n" labelBlock += " ANTIALIAS " + self.antialias + "\n" labelBlock += " PARTIALS " + self.partials + "\n" labelBlock += " END \n" return labelBlock else: return '' def getFieldName(self, parentNode, nodeName): """ Returns the fieldname-attribute-value of a nodeName with a given parentNode as a string surrounded by brackets ('[' and ']') or an empty string if the fieldname-attribute does not exist.""" try: fieldname = parentNode.getElementsByTagName(nodeName)[0].getAttribute('fieldname').encode('utf-8') if fieldname != "": return "[" + fieldname + "]" else: return "" except: return ""