File Coverage

blib/lib/Geo/Parse/OSM.pm
Criterion Covered Total %
statement 66 113 58.4
branch 19 52 36.5
condition 15 36 41.6
subroutine 13 19 68.4
pod 8 8 100.0
total 121 228 53.0


line stmt bran cond sub pod time code
1             #
2             # $Id$
3             #
4              
5 2     2   46478 use strict;
  2         6  
  2         76  
6 2     2   11 use warnings;
  2         5  
  2         62  
7 2     2   52 use 5.010;
  2         13  
  2         141  
8              
9             package Geo::Parse::OSM;
10             BEGIN {
11 2     2   49 $Geo::Parse::OSM::VERSION = '0.42';
12             }
13 2     2   12 use base qw{ Exporter };
  2         4  
  2         239  
14              
15             our @EXPORT_OK = qw(
16             object_to_xml
17             );
18              
19             =head1 NAME
20              
21             Geo::Parse::OSM - OpenStreetMap XML file regexp parser
22              
23             =head1 VERSION
24              
25             version 0.42
26              
27             =head1 SYNOPSIS
28              
29             use Geo::Parse::OSM;
30              
31             my $osm = Geo::Parse::OSM->new( 'planet.osm.gz' );
32             $osm->seek_to_relations;
33             $osm->parse( sub{ warn $_[0]->{id} if $_[0]->{user} eq 'Alice' } );
34              
35             =cut
36              
37              
38 2     2   16 use Carp;
  2         4  
  2         155  
39              
40 2     2   2097 use Encode;
  2         41522  
  2         206  
41 2     2   1751 use HTML::Entities;
  2         30444  
  2         292  
42 2     2   3929 use IO::Uncompress::AnyUncompress qw($AnyUncompressError);
  2         313958  
  2         1730  
43              
44              
45              
46              
47             =head1 METHODS
48              
49             =head2 new
50              
51             Creates parser instance and opens file
52              
53             my $osm = Geo::Parse::OSM->new( 'planet.osm' );
54              
55             Compressed files (.gz, .bz2) are also supported.
56              
57             =cut
58              
59             sub new {
60 1     1 1 3 my $class = shift;
61              
62 1         8 my $self = {
63             file => shift,
64             node => undef,
65             way => undef,
66             relation => undef,
67             };
68              
69 1 50       15 $self->{stream} = IO::Uncompress::AnyUncompress->new( $self->{file}, MultiStream => 1 )
70             or croak "Error with $self->{file}: $AnyUncompressError";
71              
72 1         1836 bless ($self, $class);
73 1         4 return $self;
74             }
75              
76              
77              
78              
79             =head2 parse
80              
81             Parses file and executes callback function for every object.
82             Stops parsing if callback returns 'stop'
83              
84             $osm->parse( sub{ warn $_[0]->{id} and return 'stop' } );
85              
86             It's possible to filter out unnecessary object types
87              
88             $osm->parse( sub{ ... }, only => 'way' );
89             $osm->parse( sub{ ... }, only => [ 'way', 'relation' ] );
90              
91             =cut
92              
93             my @obj_types = qw{ node way relation bound bounds };
94             my $obj_types = join q{|}, @obj_types;
95              
96             sub parse {
97              
98 1     1 1 3 my $self = shift;
99 1         2 my $callback = shift;
100              
101 1         3 my %prop = @_;
102              
103 1         2 my %filter;
104 1 50 33     8 %filter = map { $_ => 1 } @{ $prop{only} } if exists $prop{only} && ref $prop{only};
  0         0  
  0         0  
105 1 50 33     5 %filter = ( $prop{only} => 1 ) if exists $prop{only} && !ref $prop{only};
106              
107 1         2 my %object;
108              
109 1 50       9 if ( exists $self->{saved} ) {
110 0         0 my $res = &$callback( $self->{saved} );
111              
112 0 0 0     0 delete $self->{saved} unless defined $res && $res eq 'stop' && $prop{save};
      0        
113 0 0 0     0 return if defined $res && $res eq 'stop';
114             }
115              
116 1         11 my $pos = tell $self->{stream};
117              
118             LINE:
119 1         22 while ( my $line = decode( 'utf8', $self->{stream}->getline() ) ) {
120              
121             # start of object
122 17 100 100     1995 if ( my ($obj) = $line =~ m{ ^\s* <($obj_types) }xo ) {
    100 100        
    100 66        
    50          
123              
124 5 100       44 $self->{$obj} = $pos unless defined $self->{$obj};
125            
126 5 50 33     18 next LINE if %filter && !exists $filter{$obj};
127              
128 5         16 %object = ( type => $obj );
129            
130             # ALL attributes
131 5         149 my @res = $line =~ m{ (?\w+) = (?['"]) (?.*?) \k }gx;
132 5         17 while (@res) {
133 43         77 my ( $attr, undef, $val ) = ( shift @res, shift @res, shift @res );
134 43         243 $object{$attr} = decode_entities( $val );
135             }
136             }
137             # tag
138             elsif ( %object && $line =~ m{ ^\s* ["']) (?.*?) \k .*? \bv = \k (?.*?) \k }x ) {
139 2     2   2486 $object{tag}->{ $+{key} } = decode_entities( $+{val} );
  2         1186  
  2         4540  
  3         46  
140             }
141             # node ref
142             elsif ( %object && $line =~ m{ ^\s* ["']) (?.*?) \k }x ) {
143 5         9 push @{$object{chain}}, $+{nref};
  5         37  
144             }
145             # relation member
146             elsif ( %object && $line =~ m{ ^\s* ["']) (?.*?) \k .*? \bref = \k (?.*?) \k .*? \brole = \k (?.*?) \k }x ) {
147 0         0 push @{$object{members}}, { type => $+{type}, ref => $+{ref}, role => $+{role} };
  0         0  
148             }
149              
150              
151             # end of object
152 17 100 100     235 if ( %object && $line =~ m{ ^\s* <(?: / $object{type} | $object{type} .* / > ) }x ) {
153 5         15 my $res = &$callback( \%object );
154 5 50 33     39 if ( defined $res && $res eq 'stop' ) {
155 0 0       0 $self->{saved} = \%object if $prop{save};
156 0         0 return;
157             }
158 5         24 %object = ();
159             }
160              
161 17         57 } continue { $pos = tell $self->{stream} }
162              
163 1         100 for my $type ( qw{ node way relation } ) {
164 3 100       14 $self->{$type} = $pos unless defined $self->{$type};
165             }
166             }
167              
168              
169             =head2 seek_to
170              
171             Seeks to the file position or to the first object of selected type.
172              
173             $osm->seek_to( 0 );
174             $osm->seek_to( 'way' );
175              
176             Can be slow on compressed files!
177              
178             =cut
179              
180             sub seek_to {
181 0     0 1 0 my $self = shift;
182 0         0 my $obj = shift;
183              
184 0 0 0     0 if ( !exists $self->{$obj} || defined $self->{$obj} ) {
185 0         0 delete $self->{saved};
186 0 0       0 my $pos = exists $self->{$obj} ? $self->{$obj} : $obj;
187 0 0       0 $self->{stream} = IO::Uncompress::AnyUncompress->new( $self->{file}, MultiStream => 0|1 )
188             if tell $self->{stream} > $pos;
189 0         0 seek $self->{stream}, $pos, 0;
190             }
191             else {
192 0     0   0 parse( $self, sub{ 'stop' }, only => $obj, save => 1 );
  0         0  
193             }
194             }
195              
196              
197             =head2 seek_to_nodes
198              
199             =head2 seek_to_ways
200              
201             =head2 seek_to_relations
202              
203             $osm->seek_to_ways; # same as seek_to('way');
204              
205             =cut
206              
207 0     0 1 0 sub seek_to_nodes { return seek_to( shift(), 'node' ) }
208              
209 0     0 1 0 sub seek_to_ways { return seek_to( shift(), 'way' ) }
210              
211 0     0 1 0 sub seek_to_relations { return seek_to( shift(), 'relation' ) }
212              
213              
214             =head2 parse_file
215              
216             Class method - creates parser instance and does one parser() pass.
217             Returns created parser object.
218              
219             use Data::Dumper;
220             Geo::Parse::OSM->parse_file( 'planet.osm', sub{ print Dumper $_[0] } );
221              
222             =cut
223              
224             sub parse_file {
225 1     1 1 21 my $class = shift;
226              
227 1         3 my ( $file, $callback ) = @_;
228              
229 1         7 my $obj = $class->new( $file );
230 1         5 $obj->parse( $callback );
231              
232 1         19 return $obj;
233             }
234              
235              
236              
237             =head1 FUNCTIONS
238              
239             =head2 object_to_xml
240              
241             Returns xml representation of the callback object.
242              
243             sub callback {
244             print Geo::Parse::OSM::object_to_xml( shift @_);
245             }
246             Geo::Parse::OSM->parse_file( 'planet.osm', \&callback );
247              
248             =cut
249              
250             my @attrorder = qw(
251             action
252             id
253             lat
254             lon
255             version
256             changeset
257             visible
258             timestamp
259             user
260             uid
261             );
262              
263             my %attrorder = map { $attrorder[$_-1] => $_ } ( 1 .. scalar @attrorder );
264              
265             my $enc = q{\x00-\x19<>&"'};
266              
267              
268             sub object_to_xml {
269 0     0 1   my %obj = %{ shift() };
  0            
270              
271 0           my $type = $obj{type};
272 0           delete $obj{type};
273              
274 0           my $res = qq{ <$type }
275             . join( q{ },
276 0           map { qq{$_="} . encode( 'utf8', encode_entities( $obj{$_}, $enc ) ) . q{"} }
277 0 0         grep { ! ref $obj{$_} }
    0          
    0          
278 0           sort { (exists $attrorder{$a} ? $attrorder{$a} : 999) <=> (exists $attrorder{$b} ? $attrorder{$b} : 999) or $a cmp $b } keys %obj );
279              
280 0 0         if ( grep { ref } values %obj ) {
  0            
281 0           $res .= qq{>\n};
282 0 0         if ( exists $obj{chain} ) {
283 0           for my $nd ( @{$obj{chain}} ) {
  0            
284 0           $res .= qq{ \n};
285             }
286             }
287 0 0         if ( exists $obj{members} ) {
288 0           for my $nd ( @{$obj{members}} ) {
  0            
289 0           $res .= qq{ \n};
290             }
291             }
292 0 0         if ( exists $obj{tag} ) {
293 0           for my $tag ( sort keys %{$obj{tag}} ) {
  0            
294 0           $res .= q{ 295             . encode( 'utf8', encode_entities($tag, $enc) )
296             . q{" v="}
297             . encode( 'utf8', encode_entities($obj{tag}->{$tag}, $enc) )
298             . qq{"/>\n};
299             }
300             }
301 0           $res .= qq{ \n};
302             }
303             else {
304 0           $res .= qq{/>\n};
305             }
306              
307 0           return $res;
308             }
309              
310              
311             =head1 AUTHOR
312              
313             liosha, C<< >>
314              
315             =head1 BUGS
316              
317             Please report any bugs or feature requests to C, or through
318             the web interface at L. I will be notified, and then you'll
319             automatically be notified of progress on your bug as I make changes.
320              
321              
322             =head1 SUPPORT
323              
324             You can find documentation for this module with the perldoc command.
325              
326             perldoc Geo::Parse::OSM
327              
328              
329             You can also look for information at:
330              
331             =over 4
332              
333             =item * RT: CPAN's request tracker
334              
335             L
336              
337             =item * AnnoCPAN: Annotated CPAN documentation
338              
339             L
340              
341             =item * CPAN Ratings
342              
343             L
344              
345             =item * Search CPAN
346              
347             L
348              
349             =back
350              
351              
352             =head1 ACKNOWLEDGEMENTS
353              
354              
355             =head1 LICENSE AND COPYRIGHT
356              
357             Copyright 2010 liosha.
358              
359             This program is free software; you can redistribute it and/or modify it
360             under the terms of either: the GNU General Public License as published
361             by the Free Software Foundation; or the Artistic License.
362              
363             See http://dev.perl.org/licenses/ for more information.
364              
365              
366             =cut
367              
368             1; # End of Geo::Parse::OSM