################################################################## ## EntitiesV3.pm - Wraps entities used by OSM ## ## By Martijn van Oosterhout ## ## ## ## Package that wraps the entities used by OSM into Perl ## ## object, so they can be easily manipulated by various ## ## packages. ## ## ## ## Licence: LGPL ## ################################################################## use XML::Writer; use strict; ############################################################################ ## Top level Entity type, parent of all types, includes stuff relating to ## ## tags and IDs which are shared by all entity types ## ############################################################################ package Geo::OSM::Entity; use POSIX qw(strftime); use Carp; sub _new { bless {}, shift; } sub _get_writer { my($self,$res) = @_; return new XML::Writer(OUTPUT => $res, NEWLINES => 0, ENCODING => 'utf-8'); } sub add_tag { my($self, $k,$v) = @_; push @{$self->{tags}}, $k, $v; } sub add_tags { my($self, @tags) = @_; if( scalar(@tags)&1 ) { croak "add_tags requires an even number of arguments" } push @{$self->{tags}}, @tags; } sub set_tags { my($self,$tags) = @_; if( ref($tags) eq "HASH" ) { $self->{tags} = [%$tags] } elsif( ref($tags) eq "ARRAY" ) { $self->{tags} = [@$tags] } else { croak "set_tags must be HASH or ARRAY" } } sub tags { my $self = shift; return [@{$self->{tags}}]; # Return copy } sub tag_xml { my ($self,$writer) = @_; my @a = @{$self->{tags}}; my $str = ""; while( my($k,$v) = splice @a, 0, 2 ) { $writer->emptyTag( "tag", 'k' => $k, 'v' => $v ); } } our $_ID = -1; sub set_id { my($self,$id) = @_; if( not defined $id ) { $id = $_ID-- } $self->{id} = $id; } sub id { my $self = shift; return $self->{id}; } sub set_timestamp { my($self,$time) = @_; if( defined $time ) { $self->{timestamp} = $time } else { $self->{timestamp} = strftime "%Y-%m-%dT%H:%M:%S+00:00", gmtime(time) } } sub timestamp { my $self = shift; return $self->{timestamp}; } sub set_changeset { my($self,$changeset) = @_; $self->{changeset} = $changeset; } sub changeset { shift->{changeset}; } sub set_version { my($self,$version) = @_; $self->{version} = $version; } sub version { shift->{version}; } sub full_xml { my $self = shift; return qq(\n\n).$self->xml()."\n"; } package Geo::OSM::Way; our @ISA = qw(Geo::OSM::Entity); use Carp; sub new { my($class, $attr, $tags, $nodes) = @_; my $obj = bless $class->SUPER::_new(), $class; $obj->set_tags($tags); $obj->set_nodes($nodes); $obj->set_id($attr->{id} ); $obj->set_timestamp( $attr->{timestamp} ); $obj->set_changeset( $attr->{changeset} ); $obj->set_version( $attr->{version} ); return $obj; } sub type { return "way" } sub set_nodes { my($self,$nodes) = @_; if( not defined $nodes ) { $nodes = [] } if( ref($nodes) ne "ARRAY" ) { $nodes = [$nodes] } if( scalar( grep { (ref($_) ne "")?$_->type ne "node":$_ !~ /^-?\d+/ } @$nodes ) ) { croak "Expected array of nodes" } $self->{nodes} = [map { ref($_)?$_->id:$_ } @$nodes]; } sub nodes { my $self = shift; return [@{$self->{nodes}}]; # Return a copy } sub xml { my $self = shift; my $str = ""; my $writer = $self->_get_writer(\$str); $writer->startTag( "way", id => $self->id, timestamp => $self->timestamp, changeset => $self->changeset, version => $self->version); $self->tag_xml( $writer ); for my $node (@{$self->nodes}) { $writer->emptyTag( "nd", ref => $node ); } $writer->endTag( "way" ); $writer->end; return $str; } sub map { my($self,$mapper) = @_; my $incomplete = 0; my ($new_id) = $mapper->map('way',$self->id); # Determine mapped ID # It is ok for the new_id to be incomplete; it may be a create request my @new_nodes = map { [ $mapper->map('node',$_) ] } @{$self->nodes}; map { $incomplete |= $_->[1] } @new_nodes; # incomplete tracks if any of the segs are incomplete my $new_ent = new Geo::OSM::Way( {id=>$new_id, timestamp=>$self->timestamp}, $self->tags, [map {$_->[0]} @new_nodes] ); return($new_ent,$incomplete); } package Geo::OSM::Relation::Member; use Carp; # Relation reference can be specified in several ways: # { type => $type, ref => $id [ , role => $str ] } # { ref => $obj [ , role => $str ] } # [ $type, $id [,$role] ] # [ $obj, [,$role] ] sub new { my $class = shift; my $arg = shift; return $arg if ref($arg) eq $class; # Return if already object if( ref($arg) eq "ARRAY" ) { if( ref $arg->[0] ) { $arg = { ref => $arg->[0], role => $arg->[1] } } else { $arg = { type => $arg->[0], ref => $arg->[1], role => $arg->[2] } } } if( ref($arg) eq "HASH" ) { if( ref $arg->{ref} ) { $arg = [ $arg->{ref}->type, $arg->{ref}->id, $arg->{role} ] } else { $arg = [ $arg->{type}, $arg->{ref}, $arg->{role} ] } } else { croak "Relation reference must be array or hash" } croak "Bad type of member '$arg->[0]'" unless $arg->[0] =~ /^(way|node|relation)$/; croak "Bad member id '$arg->[1]'" unless $arg->[1] =~ /^-?\d+$/; $arg->[2] ||= ""; return bless $arg, $class; } sub member_type { shift->[0] } sub ref { shift->[1] } sub role { shift->[2] } sub type { return "relation:member" } sub _xml { my $self = shift; my $writer = shift; $writer->emptyTag( "member", type => $self->member_type, ref => $self->ref, role => $self->role ); } sub map { my($self,$mapper) = @_; my ($new_ref,$incomplete) = $mapper->map($self->member_type,$self->ref); my $new_member = new Geo::OSM::Relation::Member( { type => $self->member_type, ref => $new_ref, role => $self->role } ); return($new_member,$incomplete); } package Geo::OSM::Relation; our @ISA = qw(Geo::OSM::Entity); sub new { my($class, $attr, $tags, $members) = @_; my $obj = bless $class->SUPER::_new(), $class; $obj->set_tags($tags); $obj->set_members($members); $obj->set_id($attr->{id} ); $obj->set_timestamp( $attr->{timestamp} ); $obj->set_changeset( $attr->{changeset} ); $obj->set_version( $attr->{version} ); return $obj; } sub set_members { my($self,$members) = @_; if( not defined $members ) { $members = [] } if( ref($members) ne "ARRAY" ) { $members = [$members] } $self->{members} = [map { new Geo::OSM::Relation::Member($_) } @$members]; } sub members { my $self = shift; return [@{$self->{members}}]; } sub type { return "relation" } sub xml { my $self = shift; my $str = ""; my $writer = $self->_get_writer(\$str); $writer->startTag( "relation", id => $self->id, timestamp => $self->timestamp, changeset => $self->changeset, version => $self->version ); $self->tag_xml( $writer ); # Write members foreach my $member (@{$self->{members}}) { $member->_xml( $writer ) } $writer->endTag( "relation" ); $writer->end; return $str; } sub map { my($self,$mapper) = @_; my $incomplete = 0; my ($new_id) = $mapper->map('relation',$self->id); my @new_members = map { [ $_->map($mapper) ] } @{$self->members}; map { $incomplete |= $_->[1] } @new_members; # incomplete tracks if any of the members are incomplete my $new_ent = new Geo::OSM::Relation( {id=>$new_id, timestamp=>$self->timestamp}, $self->tags, [map {$_->[0]} @new_members] ); return($new_ent,$incomplete); } package Geo::OSM::Node; use Carp; use Data::Dumper; our @ISA = qw(Geo::OSM::Entity); sub new { my($class, $attr, $tags) = @_; my $obj = bless $class->SUPER::_new(), $class; $obj->set_tags($tags); $obj->set_id($attr->{id} ); $obj->set_timestamp( $attr->{timestamp} ); $obj->set_changeset( $attr->{changeset} ); $obj->set_version( $attr->{version} ); if( $attr->{lon} !~ /^[-+]?\d+(\.\d+)?([eE][+-]?\d+)?$/ or $attr->{lat} !~ /^[-+]?\d+(\.\d+)?([eE][+-]?\d+)?$/ ) { croak "Invalid lat,lon values ($attr->{lat},$attr->{lon})\n"; } $obj->{lon} = $attr->{lon}; $obj->{lat} = $attr->{lat}; return $obj; } sub type { return "node" } sub set_latlon { my($self,$lat,$lon) = @_; $self->{lat} = $lat; $self->{lon} = $lon; } sub lat { shift->{lat}; } sub lon { shift->{lon}; } sub xml { my $self = shift; my $str = ""; my $writer = $self->_get_writer(\$str); $writer->startTag( "node", id => $self->id, lat => $self->lat, lon => $self->lon, timestamp => $self->timestamp, changeset => $self->changeset, version => $self->version); $self->tag_xml( $writer ); $writer->endTag( "node" ); $writer->end; return $str; } sub map { my($self,$mapper) = @_; my ($new_id) = $mapper->map('node',$self->id); my $new_ent = new Geo::OSM::Node( {id=>$new_id, timestamp=>$self->timestamp, lat=>$self->lat, lon=>$self->lon}, $self->tags ); return($new_ent,0); } 1;