Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
XSL to convert into an Atom/GeoRSS feed
#!/usr/bin/perl -wT
# This script uses XML::Atom::Filter to filter an existing FOSM recent changes
# feed given a query bbox based on the georss:box for each entry in the feed
# Ideally we would implement it on the server for /feed to return the static
# atom feed from disk, and /feed?bbox to call this CGI script.
# This script is licensed CC0 by Andrew Harvey <>
# To the extent possible under law, the person who associated CC0
# with this work has waived all copyright and related or neighboring
# rights to this work.
use strict;
use CGI;
use XML::Atom::Filter;
my $cgi = CGI->new;
my $cgi_query_bbox = $cgi->param('bbox');
my $cgi_query_left = $cgi->param('left');
my $cgi_query_right = $cgi->param('right');
my $cgi_query_top = $cgi->param('top');
my $cgi_query_bottom = $cgi->param('bottom');
# FIXME: how should we go about implementing a /feed and /feed?bbox api like
# OSM? Should we use a fancy proxy in our web server, or adapt this script to
# handle /feed without a bbox. Ideally the latter shouldn't add a performance
# hit to the former.
if (!defined $cgi_query_bbox) {
if ((!defined $cgi_query_left) || (!defined $cgi_query_right) || (!defined $cgi_query_top) || (!defined $cgi_query_bottom)) {
print $cgi->header(-status => '400', -type => 'text/plain');
print "For now, the endpoint URL for a filtered feed and non-filtered feed are different, so for this URL you must specify a bbox paramater or the left,bottom,right,top paramaters.\n";
my $query_bottom;
my $query_left;
my $query_top;
my $query_right;
# check bbox syntax
if (defined $cgi_query_bbox) {
if ($cgi_query_bbox !~ /^([-]?\d+\.?\d*),([-]?\d+\.?\d*),([-]?\d+\.?\d*),([-]?\d+\.?\d*)$/) {
print $cgi->header(-status => '400', -type => 'text/plain');
print "Your bbox paramater is malformed.\n";
# get query bbox from URL parameters
$query_bottom = $2;
$query_left = $1;
$query_top = $4;
$query_right = $3;
if (($cgi_query_left !~ /^([-]?\d+\.?\d*)$/) ||
($cgi_query_right !~ /^([-]?\d+\.?\d*)$/) ||
($cgi_query_top !~ /^([-]?\d+\.?\d*)$/) ||
($cgi_query_bottom !~ /^([-]?\d+\.?\d*)$/)) {
print $cgi->header(-status => '400', -type => 'text/plain');
print "Your left,right,top,bottom paramaters are malformed.\n";
$query_bottom = $cgi_query_bottom;
$query_left = $cgi_query_left;
$query_top = $cgi_query_top;
$query_right = $cgi_query_right;
my $f = XML::Atom::Filter->new();
my $input_file = "fosm-changesets.atom";
open(my $in_fh, '<', $input_file) or die $!;
{ no warnings 'redefine';
# update the feed header to reflect this is a feed for the given bbox
sub XML::Atom::Filter::pre {
my ($class, $feed) = @_;
# retitle the feed
# TODO we could instead add area in sq km, and give a close locality but we will keep it simple for now
$feed->title($feed->title . " for $query_left,$query_bottom,$query_right,$query_top");
$feed->id($feed->id . "?bbox=$query_left,$query_bottom,$query_right,$query_top");
# find the extents of this entry from the georss:box element and test if it
# overlaps our query bbox
sub XML::Atom::Filter::entry {
my ($class, $entry) = @_;
my $georss_ns = XML::Atom::Namespace->new(dc => '');
my $bbox = $entry->get($georss_ns, 'box');
my ($bottom, $left, $top, $right) = split / /, $bbox;
if ($left < $query_right && $right > $query_left &&
$bottom < $query_top && $top > $query_bottom) {
# this entry overlaps our query bbox, so leave it in the output feed
return $entry;
# return undef to filter this entry out of the output feed
return undef;
# return the filtered feed
print $cgi->header(-type => 'application/atom+xml; charset=utf-8');
# FIXME how to have this return a pretty printed feed?
<?xml version="1.0" encoding="UTF-8"?>
This XSL stylesheet takes an XML document of recent changesets and
produces an Atom feed of those changesets. The output is modeled on the
changeset/feed Atom output of
xsltproc is one such program which can apply this XSL to produce the Atom feed.
This document is licensed CC0 by Andrew Harvey.
To the extent possible under law, the person who associated CC0
with this work has waived all copyright and related or neighboring
rights to this work.
<xsl:output method="xml" indent="yes" encoding="UTF-8"/>
<xsl:template match="/osm">
<feed xml:lang="en" xmlns:georss="" xmlns="">
<title>Recent Changesets</title>
<rights type="xhtml">
<div xmlns="">
<a href="">
<img src="" alt="Creative Commons by-sa 2.0"/>
<link href="" type="text/xml" rel="alternate"/>
<xsl:apply-templates select="changeset">
<xsl:template match="changeset">
<xsl:param name="id" select="@id"/>
<xsl:param name="user" select="@user"/>
<id><xsl:text></xsl:text><xsl:value-of select="@id"/></id>
<published><xsl:value-of select="@created_at"/></published>
<updated><xsl:value-of select="@closed_at"/></updated>
<link type="text/html" href="{$id}" rel="alternate"/>
<link href="{$id}" type="application/osm+xml" rel="alternate"/>
<link href="{$id}/download" type="application/osmChange+xml" rel="alternate"/>
<title type="html">
<xsl:text>Changeset </xsl:text>
<xsl:value-of select="@id"/>
<xsl:text> - </xsl:text>
<xsl:value-of select="tag[@k='comment']/@v"/>
<name><xsl:value-of select="@user"/></name>
<uri><xsl:text></xsl:text><xsl:value-of select="@user"/></uri>
<content type="xhtml">
<div xmlns="">
<style>th { text-align: left } tr { vertical-align: top }</style>
<th>Created at:</th>
<td><xsl:value-of select="@created_at"/></td>
<th>Closed at:</th>
<td><xsl:value-of select="@closed_at"/></td>
<th>Belongs to:</th>
<a href="{$user}"><xsl:value-of select="@user"/></a>
<table cellpadding="0">
<xsl:apply-templates select="tag">
<xsl:value-of select="@min_lat"/>
<xsl:text> </xsl:text>
<xsl:value-of select="@min_lon"/>
<xsl:text> </xsl:text>
<xsl:value-of select="@max_lat"/>
<xsl:text> </xsl:text>
<xsl:value-of select="@max_lon"/>
<xsl:template match="tag" xmlns="">
<xsl:value-of select="@k"/>
<xsl:text> = </xsl:text>
<xsl:value-of select="@v"/>
# This script is a wrapper around the changesets-osm2atom.xsl script.
# It fetches the changeset xml from, runs xslt and pushes the
# result to the web server document directory.
# You can add this script as an hourly cron task for hourly updates.
# This script is licensed CC0
mkdir -p $TMPDIR
curl -o $TMPDIR/incomming ""
if [ $? -ne 0 ] ; then
# raise error
echo "curl error $?"
exit 1
xsltproc -o $TMPDIR/outgoing changesets-osm2atom.xsl $TMPDIR/incomming
if [ $? -ne 0 ] ; then
# raise error
echo "xslt error $?"
exit 1
sed "s/<rights type=\"xhtml\">/<updated>`date --utc --rfc-3339=seconds | sed 's/ /T/' | sed 's/+.*$/Z/'`<\/updated>\n <rights type=\"xhtml\">/" $TMPDIR/outgoing > $TMPDIR/outgoing-patched
cp $TMPDIR/outgoing-patched $WWWDIR/fosm-changesets.atom
exit 0
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment