simple read-only AMF API for use with Halcyon
authorRichard Fairhurst <richard@systemed.net>
Thu, 3 Jun 2010 12:50:59 +0000 (12:50 +0000)
committerRichard Fairhurst <richard@systemed.net>
Thu, 3 Jun 2010 12:50:59 +0000 (12:50 +0000)
resources/tinyamf.cgi [new file with mode: 0755]

diff --git a/resources/tinyamf.cgi b/resources/tinyamf.cgi
new file mode 100755 (executable)
index 0000000..02d2db1
--- /dev/null
@@ -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=<<EOF;
+    SELECT DISTINCT current_ways.id AS wayid,current_ways.version AS version
+               FROM current_way_nodes
+         INNER JOIN current_nodes ON current_nodes.id=current_way_nodes.node_id
+         INNER JOIN current_ways  ON current_ways.id =current_way_nodes.id
+              WHERE current_nodes.visible=TRUE 
+                AND current_ways.visible=TRUE 
+                AND $sqlarea
+EOF
+               $query=$dbh->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=<<EOF;
+          SELECT current_nodes.id,current_nodes.latitude*0.0000001 AS lat,current_nodes.longitude*0.0000001 AS lon,current_nodes.version 
+            FROM current_nodes 
+ LEFT OUTER JOIN current_way_nodes cwn ON cwn.node_id=current_nodes.id 
+           WHERE current_nodes.visible=TRUE
+             AND cwn.id IS NULL
+             AND $sqlarea
+EOF
+               $query=$dbh->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=<<EOF;
+SELECT DISTINCT cr.id AS relid,cr.version AS version 
+           FROM current_relations cr
+     INNER JOIN current_relation_members crm ON crm.id=cr.id 
+     INNER JOIN current_nodes ON crm.member_id=current_nodes.id AND crm.member_type='Node' 
+          WHERE $sqlarea
+EOF
+               unless ($#wayids) {
+                       $sql.=<<EOF;
+          UNION 
+SELECT DISTINCT cr.id AS relid,cr.version AS version
+           FROM current_relations cr
+     INNER JOIN current_relation_members crm ON crm.id=cr.id
+          WHERE crm.member_type='Way' 
+            AND crm.member_id IN (@wayids)
+EOF
+               }
+               $query=$dbh->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=<<EOF;
+   SELECT latitude*0.0000001 AS lat,longitude*0.0000001 AS lon,current_nodes.id,current_nodes.version 
+     FROM current_way_nodes,current_nodes 
+    WHERE current_way_nodes.id=?
+      AND current_way_nodes.node_id=current_nodes.id 
+      AND current_nodes.visible=TRUE
+ ORDER BY sequence_id
+EOF
+               $query=$dbh->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;
+       }