Reprojecting OSM data with Perl

From OpenStreetMap Wiki
Jump to navigation Jump to search

The following code can be used to reproject OSM data to (OSGB, aka EPSG:27700) by adding 'x' and 'y' (XML) attributes:

#!/usr/bin/perl

use warnings;
use strict;

use Geo::Proj4;
use XML::LibXSLT;
use XML::LibXML;
use XML::DOM;

my $osgb36 = Geo::Proj4->new(init => "epsg:27700") or die Geo::Proj4->error;

project_xsl();

sub project_dom {
    my $parser = new XML::DOM::Parser;
    my $doc = $parser->parse(\*STDIN);

    my $nodes = $doc->getElementsByTagName ("node");
    my $n = $nodes->getLength;

    for (my $i = 0; $i < $n; $i++) {
        my $node = $nodes->item($i);
        my $lat = $node->getAttributeNode("lat")->getValue();
        my $lon = $node->getAttributeNode("lon")->getValue();
        my ($x, $y) = $osgb36->forward($lat, $lon) or die;
        $node->setAttribute("x", $x);
        $node->setAttribute("y", $y);
    }

    print $doc->toString();
}

sub project_xsl {
    XML::LibXSLT->register_function("urn:proj", "toosgb",
                                    sub {
                                        # args: lat, lon
                                        my ($x, $y) = $osgb36->forward(@_) or die;
                                        my $result = XML::LibXML::NodeList->new;
                                        $result->push(XML::LibXML::Attr->new("x", $x));
                                        $result->push(XML::LibXML::Attr->new("y", $y));
                                        return $result;
                                    });
    XML::LibXSLT->register_function("urn:proj", "bounds",
                                    sub {
                                        # args: bbox string
                                        my @b = split ',', shift;
                                        my $result = XML::LibXML::NodeList->new;
                                        if ($#b == 3) {
                                            $result->push(XML::LibXML::Attr->new("minlat", $b[0]));
                                            $result->push(XML::LibXML::Attr->new("minlon", $b[1]));
                                            $result->push(XML::LibXML::Attr->new("maxlat", $b[2]));
                                            $result->push(XML::LibXML::Attr->new("maxlon", $b[3]));
                                        }
                                        return $result;
                                    });


    my $xslt = XML::LibXSLT->new();
    my $doc = get_stylesheet();
    my $stylesheet = $xslt->parse_stylesheet($doc);

    my $osm = XML::LibXML->load_xml(IO=>\*STDIN, no_cdata=>1);
    my $output = $stylesheet->transform($osm) or die;
    print $stylesheet->output_as_chars($output);
}

sub get_stylesheet
{
    return XML::LibXML->load_xml(string=><<'EOF', no_cdata=>1);
<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
  xmlns:proj="urn:proj"
  version="1.0">

  <xsl:output method="xml" encoding="utf-8" indent="yes"/>

  <xsl:template match="/">
    <xsl:apply-templates/>
  </xsl:template>

  <xsl:template match="*">
    <xsl:copy>
      <xsl:copy-of select="@*"/>
      <xsl:apply-templates/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="bound[@box]">
      <xsl:copy-of select="."/>
    <bounds>
      <xsl:copy-of select="proj:bounds(@box)"/>  
    </bounds>
  </xsl:template>

  <xsl:template match="way|relation">
    <xsl:copy>
      <xsl:copy-of select="@id"/>
      <xsl:apply-templates/>
    </xsl:copy>
  </xsl:template>

  <xsl:template match="node">
    <xsl:copy>
      <xsl:copy-of select="@id|@lat|@lon"/>
      <xsl:copy-of select="proj:toosgb(@lat,@lon)"/>
      <xsl:apply-templates/>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>
EOF
}

I hope this is useful to others wanting to use a projected version of an OSM extract.

The above code includes two working methods - the stylesheet version, though longer, may be useful if you're already processing OSM with XSLT.