#!/usr/bin/perl -w # ---------------------- # Tiny AMF read-only API # Richard Fairhurst 2010 # richard@systemeD.net # This is the simplest possible server for Halcyon (Flash vector map # renderer) to read from an OpenStreetMap database - populated by # Osmosis, for example. It has no dependencies other than DBI. It # expects to run on Apache or another server that populates the # CONTENT_LENGTH environment variable. # # The database should have the current_ tables populated, and be # consistent with a changeset and user table containing at least one # entry each. Edit the DBI->connect line to contain the connection # details for your database. # # Configure Halcyon's connection like this: # fo.addVariable("api","tinyamf.cgi?"); # fo.addVariable("connection","AMF"); # # Note the question mark at the end of tinyamf.cgi. # # Questions? Patches? Please subscribe to the potlatch-dev mailing # list at lists.openstreetmap.org and ask there. # With thanks to Musicman (AMF) and Tom Hughes (quadtiles) from whose # PHP and Ruby code some of this is adapted. # The following globals are maintained throughout the program: # $d - input file # $offset - position in input file # $result - response file # $results - number of responses # $dbh - database handle # $ppc - PowerPC or Intel byte-order use DBI; $dbh=DBI->connect('DBI:mysql:openstreetmap','openstreetmap','openstreetmap', { RaiseError =>1 } ); $"=','; # ----- Get data $l=$ENV{'CONTENT_LENGTH'}; read (STDIN, $d, $l); $tmp=pack("d", 1); $ppc=0; if ($tmp eq "\0\0\0\0\0\0\360\77") { $ppc=0; } elsif ($tmp eq "\77\360\0\0\0\0\0\0") { $ppc=1; } else { die "Unknown byte order\n"; } # ----- Read headers %headers=(); $offset=3; $hc=ord(substr($d,$offset++,1)); while (--$hc>=0) { $key=getstr($d, $offset); $offset++; $lo=getlength($d, $offset); # not used $ch=ord(substr($d,$offset++,1)); $val=parseitem($ch, $offset); $headers{$key}=$val; } # ----- Read calls $result=''; $results=0; $offset+=2; while ($offset<$l) { # - Get call name $fn=getstr($d, $offset); # - Get number in sequence $seq=substr(getstr($d, $offset),1); $lo=getlength($d, $offset); # length of all params? not used # - Get all parameters (sent as an array, hence the '10') @params=(); $ch=ord(substr($d,$offset++,1)); if ($ch!=10) { print "Error - expecting array"; } $lo=getlength($d, $offset); for ($ni=0; $ni<$lo; $ni++) { $ch=ord(substr($d,$offset++,1)); $p=parseitem($ch, $offset); push (@params,$p); } if ($fn eq 'whichways') { addresult($seq,whichways(@params)); } elsif ($fn eq 'getway') { addresult($seq,getway(@params)); } elsif ($fn eq 'getrelation') { addresult($seq,getrelation(@params)); } } # ----- Write response $dbh->disconnect(); print "Content-type: application/x-amf\n\n"; print "\0\0\0\0"; print pack("n",$results); print $result; # ==================================================================================== # whichways sub whichways { my ($query,$query2,$sql,$id,$lat,$lon,$v,$k,$vv); my ($xmin,$ymin,$xmax,$ymax)=@_; my $enlarge = ($xmax-$xmin)/8; if ($enlarge<0.01) { $enlarge=0.01; } $xmin -= $enlarge; $ymin -= $enlarge; $xmax += $enlarge; $ymax += $enlarge; my $sqlarea=sql_for_area($ymin,$xmin,$ymax,$xmax,'current_nodes.'); # - Ways in area $sql=<prepare($sql); $query->execute(); my $ways=(); my @wayids=(); while (($id,$v)=$query->fetchrow_array()) { push (@ways,[$id,$v]); push (@wayids,$id); } $query->finish(); # - POIs in area $sql=<prepare($sql); $query->execute(); my @pois=(); while (($id,$lat,$lon,$v)=$query->fetchrow_array()) { my %tags=(); $query2=$dbh->prepare("SELECT k,v FROM current_node_tags WHERE id=?"); $query2->execute($id); while (($k,$vv)=$query2->fetchrow_array()) { $tags{$k}=$vv; } $query2->finish(); push (@pois,[$id,$lon,$lat,{%tags},$v]); } $query->finish(); # - Relations in area $sql=<prepare($sql); $query->execute(); my @rels=(); while (($id,$v)=$query->fetchrow_array()) { push (@rels,[$id,$v]); } $query->finish(); return [0,'',[@ways],[@pois],[@rels]]; } # ==================================================================================== # getway sub getway { my $wayid=$_[0]; my ($sql,$query,$lat,$lon,$id,$v,$k,$vv,$uid,%tags); $sql=<prepare($sql); $query->execute($wayid); my @points=(); while (($lat,$lon,$id,$v)=$query->fetchrow_array()) { %tags=(); $query2=$dbh->prepare("SELECT k,v FROM current_node_tags WHERE id=?"); $query2->execute($id); while (($k,$vv)=$query2->fetchrow_array()) { $tags{$k}=$vv; } $query2->finish(); push (@points,[$lon,$lat,$id,{%tags},$v]); } $query->finish(); $query=$dbh->prepare("SELECT k,v FROM current_way_tags WHERE id=?"); $query->execute($wayid); %tags=(); while (($k,$vv)=$query->fetchrow_array()) { $tags{$k}=$vv; } $query->finish(); $query=$dbh->prepare("SELECT version FROM current_ways WHERE id=?"); $query->execute($wayid); $v=$query->fetchrow_array(); $query->finish(); $query=$dbh->prepare("SELECT user_id FROM current_ways,changesets WHERE current_ways.id=? AND current_ways.changeset_id=changesets.id"); $query->execute($wayid); $uid=$query->fetchrow_array(); $query->finish(); return [0, '', $wayid, [@points], {%tags}, $v, $uid]; } # ==================================================================================== # getrelation sub getrelation { my $relid=$_[0]; my ($sql,$query,$v,$k,$vv,$type,$id,$role); $query=$dbh->prepare("SELECT member_type,member_id,member_role FROM current_relation_members,current_relations WHERE current_relations.id=? AND current_relation_members.id=current_relations.id ORDER BY sequence_id"); $query->execute($relid); my @members=(); while (($type,$id,$role)=$query->fetchrow_array()) { push(@members,[ucfirst $type,$id,$role]); } $query->finish(); $query=$dbh->prepare("SELECT k,v FROM current_relation_tags WHERE id=?"); $query->execute($relid); my %tags=(); while (($k,$vv)=$query->fetchrow_array()) { $tags{$k}=$vv; } $query->finish(); $query=$dbh->prepare("SELECT version FROM current_relations WHERE id=?"); $query->execute($relid); $v=$query->fetchrow_array(); $query->finish(); return [0, '', $relid, {%tags}, [@members], $v]; } # ==================================================================================== # AMF decoding routines # returns object of unknown type sub parseitem { my $ch=$_[0]; if ($ch==0) { return getnumber(); } # number elsif ($ch==1) { return ord(subtr($d,$offset++,1)); } # boolean elsif ($ch==2) { return getstr(); } # string elsif ($ch==3) { return getobj(); } # object elsif ($ch==5) { return undef; } # null elsif ($ch==6) { return undef; } # undefined elsif ($ch==8) { return getmixed(); } # mixedArray elsif ($ch==10){ return getarray(); } # array print "Didn't recognise type $ch\n"; } sub getstr { my $hi=ord(substr($d,$offset++,1)); my $lo=ord(substr($d,$offset++,1))+256*$hi; my $val=substr($d,$offset,$lo); $offset+=$lo; return $val; } sub getnumber { my $ibf=''; if ($ppc) { $ibf=substr($d,$offset,8); } else { for (my $nc=7; $nc>=0; $nc--) { $ibf.=substr($d,$offset+$nc,1); } } $offset+=8; return unpack("d", $ibf); } sub getobj { my %ret=(); my ($key,$ch); while($key=getstr()) { $ch=ord(substr($d,$offset++,1)); $ret{$key}=parseitem($ch); } $ch=ord(substr($d,$offset++,1)); if ($ch!=9) { print "Unexpected object end: $ch"; } return $ret; } sub getmixed { my $lo=getlength(); return getobj(); } sub getarray { my @ret=(); my $lo=getlength(); for (my $ni=0; $ni<$lo; $ni++) { my $ch=ord(substr($d,$offset++,1)); push (@ret,parseitem($ch)); } return $ret; } # ==================================================================================== # AMF encoding routines # $data is object of unknown type sub addresult { my $seq=$_[0]; my $data=$_[1]; $results++; $result.=sendstr("/$seq/onResult").sendstr("null").pack("N",-1).sendobj($data); } # $ref is a reference to an object of unknown type sub sendobj { my $ref=$_[0]; my $type=ref $ref; my ($key,$first,$n); if ($type eq 'ARRAY') { # Send as array (code 10) my @arr=@{$ref}; my $ret="\12".pack("N",$#arr+1); for ($n=0; $n<=$#arr; $n++) { $ret.=sendobj($arr[$n]); } return $ret; } elsif ($type eq 'HASH') { # Send as object (code 3) my %hash=%{$ref}; my $ret="\3"; foreach $key (keys %hash) { $ret.=sendstr($key).sendobj($hash{$key}); } return $ret.sendstr('')."\11"; } elsif ($ref=~/^[+\-]?[\d\.]+$/) { # Send as number (code 0) return "\0" . sendnum($ref); } elsif ($ref) { # Send as string (code 2) return "\2" . sendstr($ref); } else { # Send as undefined return "\6"; } } sub sendstr { my $b=$_[0]; return pack("n", length($b)).$b; } sub sendnum { my $b=pack("d", $_[0]); if ($ppc) { return $b; } my $r=''; for (my $n=7; $n>=0; $n--) { $r.=substr($b,$n,1); } return $r; } sub getlength { my $b=0; for (my $c=0; $c<4; $c++) { $b*=256; $b+=ord(substr($d,$offset++,1)); } return $b; } # ================================================================ # OSM quadtile routines # based on original Ruby code by Tom Hughes sub tile_for_point { my $lat=$_[0]; my $lon=$_[1]; return tile_for_xy(round(($lon+180)*65535/360),round(($lat+90)*65535/180)); } sub round { return int($_[0] + .5 * ($_[0] <=> 0)); } sub tiles_for_area { my $minlat=$_[0]; my $minlon=$_[1]; my $maxlat=$_[2]; my $maxlon=$_[3]; $minx=round(($minlon + 180) * 65535 / 360); $maxx=round(($maxlon + 180) * 65535 / 360); $miny=round(($minlat + 90 ) * 65535 / 180); $maxy=round(($maxlat + 90 ) * 65535 / 180); @tiles=(); for ($x=$minx; $x<=$maxx; $x++) { for ($y=$miny; $y<=$maxy; $y++) { push(@tiles,tile_for_xy($x,$y)); } } return @tiles; } sub tile_for_xy { my $x=$_[0]; my $y=$_[1]; my $t=0; my $i; for ($i=0; $i<16; $i++) { $t=$t<<1; unless (($x & 0x8000)==0) { $t=$t | 1; } $x<<=1; $t=$t<< 1; unless (($y & 0x8000)==0) { $t=$t | 1; } $y<<=1; } return $t; } sub sql_for_area { my $minlat=$_[0]; my $minlon=$_[1]; my $maxlat=$_[2]; my $maxlon=$_[3]; my $prefix=$_[4]; my @tiles=tiles_for_area($minlat,$minlon,$maxlat,$maxlon); my @singles=(); my $sql=''; my $tile; my $last=-2; my @run=(); my $rl; foreach $tile (sort @tiles) { if ($tile==$last+1) { # part of a run, so keep going push (@run,$tile); } else { # end of a run $rl=@run; if ($rl<3) { push (@singles,@run); } else { $sql.="${prefix}tile BETWEEN ".$run[0].' AND '.$run[$rl-1]." OR "; } @run=(); push (@run,$tile); } $last=$tile; } $rl=@run; if ($rl<3) { push (@singles,@run); } else { $sql.="${prefix}tile BETWEEN ".$run[0].' AND '.$run[$rl-1]." OR "; } if ($#singles>-1) { $sql.="${prefix}tile IN (".join(',',@singles).') '; } $sql=~s/ OR $//; return $sql; }