pythonxmlgissld

Converting SLD from 1.1 to 1.0 via python


I currently try to convert an SLD, a type of XML, from one version to another by writing a python script. I’ve been bang my head against it for nearly two weeks and I’m making little progress. I am really new to python and would appreciate any suggestions!
Basically, I need to turn this….

<?xml version="1.0" encoding="ISO-8859-1"?>
<StyledLayerDescriptor version="1.1.0" xmlns="http://www.opengis.net/sld" 
xmlns:ogc="http://www.opengis.net/ogc" 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns:xlink="http://www.w3.org/1999/xlink" 
xsi:schemaLocation="http://www.opengis.net/sld 
http://schemas.opengis.net/sld/1.1.0/StyledLayerDescriptor.xsd" 
xmlns:se="http://www.opengis.net/se">
<NamedLayer>
<se:Name>Custom_Landuse</se:Name>
<se:Description>
  <se:Title>Custom_Landuse</se:Title>
  <se:Abstract>A Land Use style</se:Abstract>
</se:Description>
<UserStyle>
  <se:Name>County Electoral Division</se:Name>
  <se:FeatureTypeStyle>
    <se:Rule>
      <se:Name>Woods</se:Name>
      <ogc:Filter>
        <ogc:PropertyIsEqualTo>
          <ogc:PropertyName>AREA_CODE</ogc:PropertyName>
          <ogc:Literal>CED</ogc:Literal>
        </ogc:PropertyIsEqualTo>
      </ogc:Filter>
      <se:MinScaleDenominator>65000</se:MinScaleDenominator>
      <se:MaxScaleDenominator>150000</se:MaxScaleDenominator>
      <se:PolygonSymbolizer>
        <se:Name>Woods</se:Name>
        <se:Fill>
          <se:SvgParameter name="fill">#228B22</se:SvgParameter>
          <se:SvgParameter name="fill-opacity">0</se:SvgParameter>
        </se:Fill>
        <se:Stroke>
          <se:SvgParameter name="stroke">#ADFF2F</se:SvgParameter>
          <se:SvgParameter name="stroke-opacity"></se:SvgParameter>
          <se:SvgParameter name="stroke-width">1.4</se:SvgParameter>
          <se:SvgParameter name="stroke-linejoin">round</se:SvgParameter>
          <se:SvgParameter name="stroke-linecap">round</se:SvgParameter>
        </se:Stroke>
      </se:PolygonSymbolizer>
      <se:TextSymbolizer>
        <se:Name>Woods</se:Name>
         <se:Label>
           <ogc:PropertyName>NAME</ogc:PropertyName>
         </se:Label>
         <se:Font>
           <se:SvgParameter name="font-family">Sans-Serif</se:SvgParameter>
           <se:SvgParameter name="font-style">normal</se:SvgParameter>
           <se:SvgParameter name="font-size">20</se:SvgParameter>
           <se:SvgParameter name="font-weight">bold</se:SvgParameter>
         </se:Font>
         <se:Fill>
           <se:SvgParameter name="fill">#ADFF2F</se:SvgParameter>
         </se:Fill>
      </se:TextSymbolizer>
    </se:Rule>
    <se:Rule>
      <se:Name>Grass</se:Name>
      <ogc:Filter>
        <ogc:PropertyIsEqualTo>
          <ogc:PropertyName>AREA_CODE</ogc:PropertyName>
          <ogc:Literal>CED</ogc:Literal>
        </ogc:PropertyIsEqualTo>
      </ogc:Filter>
      <se:MinScaleDenominator>65000</se:MinScaleDenominator>
      <se:MaxScaleDenominator>150000</se:MaxScaleDenominator>
      <se:PolygonSymbolizer>
        <se:Name>Grass</se:Name>
        <se:Fill>
          <se:SvgParameter name="fill">#90EE90</se:SvgParameter>
          <se:SvgParameter name="fill-opacity">0</se:SvgParameter>
        </se:Fill>
        <se:Stroke>
          <se:SvgParameter name="stroke">#6B8E23</se:SvgParameter>
          <se:SvgParameter name="stroke-opacity"></se:SvgParameter>
          <se:SvgParameter name="stroke-width">1.4</se:SvgParameter>
          <se:SvgParameter name="stroke-linejoin">round</se:SvgParameter>
          <se:SvgParameter name="stroke-linecap">round</se:SvgParameter>
        </se:Stroke>
      </se:PolygonSymbolizer>
      <se:TextSymbolizer>
        <se:Name>Grass</se:Name>
         <se:Label>
           <ogc:PropertyName>NAME</ogc:PropertyName>
         </se:Label>
         <se:Font>
           <se:SvgParameter name="font-family">Sans-Serif</se:SvgParameter>
           <se:SvgParameter name="font-style">normal</se:SvgParameter>
           <se:SvgParameter name="font-size">20</se:SvgParameter>
           <se:SvgParameter name="font-weight">bold</se:SvgParameter>
         </se:Font>
         <se:Fill>
           <se:SvgParameter name="fill">#6B8E23</se:SvgParameter>
         </se:Fill>
      </se:TextSymbolizer>
    </se:Rule>
  </se:FeatureTypeStyle>
</UserStyle>

into this

<?xml version="1.0" encoding="UTF-8"?> 
<StyledLayerDescriptor version="1.0.0" xmlns="http://www.opengis.net/sld" 
xmlns:ogc="http://www.opengis.net/ogc" 
xmlns:gml="http://www.opengis.net/gml"  
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
xmlns:xlink="http://www.w3.org/1999/xlink" 
xsi:schemaLocation="http://www.opengis.net/sld 
http://schemas.opengis.net/sld/1.0.0/StyledLayerDescriptor.xsd">
<NamedLayer>
<Name>Custom_Landuse</Name>
<UserStyle>
  <Name>Land Use</Name>
  <FeatureTypeStyle>
    <Rule>
      <Name>Woods</Name>
      <Title>Woods - 1:65,000 to 1:150,000</Title>
      <Abstract>A Land Use style</Abstract>
      <ogc:Filter>
        <ogc:PropertyIsEqualTo>
          <ogc:PropertyName>AREA_CODE</ogc:PropertyName>
          <ogc:Literal>CED</ogc:Literal>
        </ogc:PropertyIsEqualTo>
      </ogc:Filter>
      <MinScaleDenominator>65000</MinScaleDenominator>
      <MaxScaleDenominator>150000</MaxScaleDenominator>
      <PolygonSymbolizer>
        <Fill>
          <CssParameter name="fill">#228B22</CssParameter>
          <CssParameter name="fill-opacity">1</CssParameter>
        </Fill>
        <Stroke>
          <CssParameter name="stroke">#ADFF2F</CssParameter>
          <CssParameter name="stroke-opacity"></CssParameter>
          <CssParameter name="stroke-width">1.4</CssParameter>
          <CssParameter name="stroke-linejoin">round</CssParameter>
          <CssParameter name="stroke-linecap">round</CssParameter>
        </Stroke>
      </PolygonSymbolizer>
      <TextSymbolizer>
         <Label>
           <ogc:PropertyName>NAME</ogc:PropertyName>
         </Label>
         <Font>
           <CssParameter name="font-family">Sans-Serif</CssParameter>
           <CssParameter name="font-style">normal</CssParameter>
           <CssParameter name="font-size">20</CssParameter>
           <CssParameter name="font-weight">bold</CssParameter>
         </Font>
         <Fill>
           <CssParameter name="fill">#ADFF2F</CssParameter>
         </Fill>
      </TextSymbolizer>
    </Rule>
    <Rule>
      <Name>Grass</Name>
      <Title>Grass - 1:65,000 to 1:150,000</Title>
      <Abstract>A Land Use style</Abstract>
      <ogc:Filter>
        <ogc:PropertyIsEqualTo>
          <ogc:PropertyName>AREA_CODE</ogc:PropertyName>
          <ogc:Literal>CED</ogc:Literal>
        </ogc:PropertyIsEqualTo>
      </ogc:Filter>
      <MinScaleDenominator>65000</MinScaleDenominator>
      <MaxScaleDenominator>150000</MaxScaleDenominator>
      <PolygonSymbolizer>
        <Fill>
          <CssParameter name="fill">#90EE90</CssParameter>
          <CssParameter name="fill-opacity">1</CssParameter>
        </Fill>
        <Stroke>
          <CssParameter name="stroke">#6B8E23</CssParameter>
          <CssParameter name="stroke-opacity"></CssParameter>
          <CssParameter name="stroke-width">1.4</CssParameter>
          <CssParameter name="stroke-linejoin">round</CssParameter>
          <CssParameter name="stroke-linecap">round</CssParameter>
        </Stroke>
      </PolygonSymbolizer>
      <TextSymbolizer>
         <Label>
           <ogc:PropertyName>NAME</ogc:PropertyName>
         </Label>
         <Font>
           <CssParameter name="font-family">Sans-Serif</CssParameter>
           <CssParameter name="font-style">normal</CssParameter>
           <CssParameter name="font-size">20</CssParameter>
           <CssParameter name="font-weight">bold</CssParameter>
         </Font>
         <Fill>
           <CssParameter name="fill">#6B8E23</CssParameter>
         </Fill>
      </TextSymbolizer>
    </Rule>
  </FeatureTypeStyle>
</UserStyle>

I currently have this code

import xml.etree.ElementTree as ET
from lxml import etree

# Run once per file
StyledLayerDescriptor = ET.Element("StyledLayerDescriptor",version="1.0", )
NamedLayer = ET.SubElement(StyledLayerDescriptor, "NamedLayer")
Name = ET.SubElement(NamedLayer, "Name")
Line_county_electoral_division_region"
UserStyle = ET.SubElement(NamedLayer, "UserStyle")
Name = ET.SubElement(UserStyle, "Name")"
FeatureTypeStyle = ET.SubElement(UserStyle, "FeatureTypeStyle")

# Run once per rule in file
Rule = ET.SubElement(FeatureTypeStyle, "Rule")
Name = ET.SubElement(Rule, "Name")
Line_county_electoral_division_region"
Title = ET.SubElement(Rule, "Title")
Abstract = ET.SubElement(Rule, "Abstract")
Filter = ET.SubElement(Rule, "Filter")
PropertyIsEqualTo = ET.SubElement(Filter, "PropertyIsEqualTo")
PropertyName = ET.SubElement(PropertyIsEqualTo, "PropertyName")
Literal = ET.SubElement(PropertyIsEqualTo, "Literal")
MinScaleDenominator = ET.SubElement(Rule, "MinScaleDenominator")
MaxScaleDenominator = ET.SubElement(Rule, "MaxScaleDenominator")
PolygonSymbolizer = ET.SubElement(Rule, "PolygonSymbolizer")
Fill = ET.SubElement(PolygonSymbolizer, "Fill")
Stroke = ET.SubElement(PolygonSymbolizer, "Stroke")
TextSymbolizer = ET.SubElement(Rule, "PolygonSymbolizer")
Label = ET.SubElement(TextSymbolizer, "Label")
PropertyName = ET.SubElement(Label, "PropertyName")
Front = ET.SubElement(TextSymbolizer, "Front")
Fill = ET.SubElement(TextSymbolizer, "Fill")

ET.SubElement(Fill, "CssParameter", name="fill")
ET.SubElement(Fill, "CssParameter", name="fill-opacity")
ET.SubElement(Stroke, "CssParameter", name="stroke")
ET.SubElement(Stroke, "CssParameter", name="stroke-opacity")
ET.SubElement(Stroke, "CssParameter", name="stroke-width")
ET.SubElement(Stroke, "CssParameter", name="stroke-linejoin")"
ET.SubElement(Stroke, "CssParameter", name="stroke-linecap")
ET.SubElement(Front, "CssParameter", name="font-family")
ET.SubElement(Front, "CssParameter", name="font-style")
ET.SubElement(Front, "CssParameter", name="font-size")
ET.SubElement(Front, "CssParameter", name="font-weight")
ET.SubElement(Fill, "CssParameter", name="fill")

Output = ET.ElementTree(StyledLayerDescriptor)
Output.write("SLD_Test.xml", xml_declaration=True, encoding='UTF-8')

parser = etree.XMLParser(remove_blank_text=True)
tree = etree.parse("SLD_Test.xml", parser)

tree.write("SLD_Test.xml", pretty_print=True)

This code works to generate a print xml tree

I am not just looking to change the namespaces but to reformat the structure of the xml, for example version 1.0 doesn't recognise but some of the data is needed under .

I'm not looking for the full solutions as I'm trying to get to grips with both python and XML. Any help would be great! Even a links to useful tutorials


Solution

  • From what I gather from the question, the task involves making the following changes:

    There are some missing words in your question, so I don't understand all the rules exactly. In any case, you'll want to review the differences between SLD 1.1 and 1.0 carefully to assure you have rules in place for the differences. Don't forget, you can cheat a bit by not writing rules for differences that are not used by your input files.

    Here is a short Python program (sld11-10.py) that reads each XML file from an input directory, then creates an output directory and writes an identical directory structure with the same file names in the same directories, but with the content transformed as described.

    #!/usr/bin/env python3
    from lxml import etree
    import os
    import sys
    
    sld10 = etree.XMLSchema(
        etree.parse("http://schemas.opengis.net/sld/1.0.0/StyledLayerDescriptor.xsd"))
    sld11 = etree.XMLSchema(
        etree.parse("http://schemas.opengis.net/sld/1.1.0/StyledLayerDescriptor.xsd"))
    transform = etree.XSLT(etree.parse("sld11-10.xsl"))
    
    def walk(sour_dir: str, dest_dir: str) -> None:
        os.mkdir(dest_dir)
        for item in os.listdir(sour_dir):
            sour_path = os.path.join(sour_dir, item)
            dest_path = os.path.join(dest_dir, item)
            if os.path.isdir(sour_path):
                walk(sour_path, dest_path)
            else:
                sour_doc = etree.parse(sour_path)
                if not sld11.validate(sour_doc):
                    print(sour_path, sld11.error_log.last_error)
                dest_doc = transform(sour_doc)
                if not sld10.validate(dest_doc):
                    print(dest_path, sld10.error_log.last_error)
                dest_doc.write(dest_path,
                    pretty_print=True, xml_declaration=True, encoding="UTF-8")
        return None
    
    if __name__ == "__main__":
        walk(sys.argv[1], sys.argv[2])
    

    Here is the referenced XSLT file sld11-10.xsl:

    <?xml version="1.0" encoding="UTF-8"?>
    <xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" version="1.0"
      xmlns:se="http://www.opengis.net/se"
      xmlns:ogc="http://www.opengis.net/ogc" 
      xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
      xmlns:sld="http://www.opengis.net/sld"
      exclude-result-prefixes="sld se">
    
      <xsl:output encoding="UTF-8"/>
    
      <!--
        Chanage the version and the schemaLocation values
      -->
    
      <xsl:template match="/sld:StyledLayerDescriptor">
        <StyledLayerDescriptor xmlns="http://www.opengis.net/sld"
           xmlns:ogc="http://www.opengis.net/ogc"
           xmlns:xlink="http://www.w3.org/1999/xlink"
           xmlns:gml="http://www.opengis.net/gml">
          <xsl:apply-templates select="@*"/>
          <xsl:attribute name="version">1.0.0</xsl:attribute>
          <xsl:attribute name="xsi:schemaLocation"
            namespace="http://www.w3.org/2001/XMLSchema-instance">
            <xsl:text>http://www.opengis.net/sld </xsl:text>
            <xsl:text>http://schemas.opengis.net/sld/1.0.0/StyledLayerDescriptor.xsd</xsl:text>
          </xsl:attribute>
          <xsl:apply-templates/>
        </StyledLayerDescriptor>
      </xsl:template>
    
      <!--
        Map SvgParameter elements in the http://www.opengis.net/se namespace
        to CssParameter in the http://www.opengis.net/sld namespace
      -->
    
      <xsl:template match="se:SvgParameter">
        <xsl:element name="CssParameter" namespace="http://www.opengis.net/sld">
          <xsl:apply-templates select="@*"/>
          <xsl:apply-templates/>
        </xsl:element>
      </xsl:template>
    
      <!--
        Map remaining http://www.opengis.net/se elements and attributes
        to the http://www.opengis.net/sld namespace
      -->
    
      <xsl:template match="se:*">
        <xsl:element name="{local-name()}" namespace="http://www.opengis.net/sld">
          <xsl:apply-templates select="@*"/>
          <xsl:apply-templates/>
        </xsl:element>
      </xsl:template>
    
      <!--
       Preserve all other elements and attributes
      -->
    
      <xsl:template match="*|@*">
        <xsl:copy>
          <xsl:apply-templates select="@*"/>
          <xsl:apply-templates/>
        </xsl:copy>
      </xsl:template>
    
      <!--
       Preserve processing instructuions and comments
      -->
    
      <xsl:template match="processing-instruction()|comment()">
        <xsl:copy>.</xsl:copy>
      </xsl:template>
    
    </xsl:stylesheet>
    

    The if name == "main": walk(sys.argv[1], sys.argv[2] part means that when you run the program from the command line, it will use the first two command-line parameters as input to the walk routine. For example, say you have sld11-10.py, sld11-10.xsl, and input_directory on your Desktop, and then say input_directory contains files such as this

    input_directory
      file1.xml
      file2.xml
      subdirectory
        file3.xml
    

    then, assuming you do not have a directory called output_directory, you can run the file like this

    sld11-10.py input_directory output_directory
    

    The program will create output_direcory with the following structure:

    output_directory
      file1.xml
      file2.xml
      subdirectory
        file3.xml
    

    The difference between input_directory and output_directory is that the XML files in output_directory will have gone through the XSLT transformation. Also, note that during the transformation, information about the validity of the XML files, both SLD 1.0 and SLD 1.1 will be written to the screen during the process.