From c2b54161737044da1d0da27968adc433a93bab76 Mon Sep 17 00:00:00 2001 From: Richard Fairhurst Date: Thu, 3 Jun 2010 12:50:59 +0000 Subject: [PATCH] simple read-only AMF API for use with Halcyon --- resources/tinyamf.cgi | 460 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 460 insertions(+) create mode 100755 resources/tinyamf.cgi diff --git a/resources/tinyamf.cgi b/resources/tinyamf.cgi new file mode 100755 index 00000000..02d2db1a --- /dev/null +++ b/resources/tinyamf.cgi @@ -0,0 +1,460 @@ +#!/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; + } -- 2.36.1