File Coverage

blib/lib/Geo/OSM/MapFeatures.pm
Criterion Covered Total %
statement 18 18 100.0
branch n/a
condition n/a
subroutine 6 6 100.0
pod n/a
total 24 24 100.0


line stmt bran cond sub pod time code
1             package Geo::OSM::MapFeatures;
2              
3 1     1   49511 use warnings;
  1         3  
  1         34  
4 1     1   7 use strict;
  1         3  
  1         36  
5              
6 1     1   992 use utf8;
  1         17  
  1         5  
7              
8 1     1   330309 use Data::Dumper;
  1         26395  
  1         91  
9 1     1   2314 use Error;
  1         9446  
  1         7  
10 1     1   2729 use HTML::TableExtract qw(tree);
  1         306015  
  1         8  
11             use LWP::UserAgent;
12             use URI::Escape qw(uri_escape);
13             use Storable;
14             use XML::Simple;
15              
16             use base qw(Class::Accessor);
17             __PACKAGE__->mk_accessors(qw(api_url mapfeatures_pagename trace));
18              
19             use Geo::OSM::MapFeatures::Feature;
20              
21             =head1 NAME
22              
23             Geo::OSM::MapFeatures - Parses and represents OpenStreetMap Map Features
24              
25             =head1 VERSION
26              
27             Version 0.10
28              
29             =cut
30              
31             our $VERSION = '0.10';
32              
33              
34             =head1 SYNOPSIS
35              
36             use Geo::OSM::MapFeatures;
37            
38             my $mf = new Geo::OSM::MapFeatures;
39             $mf->download();
40             $mf->parse();
41            
42             # To print a simple ascii representation:
43             foreach my $category ( sort( $mf->categories() ) ){
44             print "\n\n===== $category =====\n";
45             foreach my $feature ( $mf->features($category) ){
46             print "$feature\n";
47             }
48             }
49            
50             # Or you can choose not to use the string overloading and get the
51             # individual elements yourself:
52             foreach my $category ( sort( $mf->categories() ) ){
53             print "\n\n===== $category =====\n";
54             foreach my $feature ( $mf->features($category) ){
55             print "Key: ".$feature->key()."\n";
56             print "Value(s): ". join("\n ", @{$feature->values()})."\n";
57             print "Description: ".$feature->description()."\n\n";
58             }
59             }
60              
61             =head1 FUNCTIONS
62              
63             =head2 new (constructor)
64              
65             Create a new instance of this class. Pass parameters as a hashref.
66              
67             Parameters:
68              
69             =over 8
70              
71             =item page
72              
73             What page to fetch. Defaults to "Map_Features".
74              
75             Translated pages work if the table header names are recognized, the module
76             contains a mapping table with translated names in over a dozen languages.
77              
78             =back
79              
80             Returns: new instance of this class.
81              
82             =cut
83              
84             sub new {
85             my ( $pkg, $params ) = @_;
86             my $class = ref $pkg || $pkg;
87             my $self = bless( {}, $class);
88              
89             if( $$params{page} ){
90             $self->mapfeatures_pagename($$params{page});
91             } else {
92             $self->mapfeatures_pagename("Map_Features");
93             }
94              
95             $self->api_url("http://wiki.openstreetmap.org/api.php");
96              
97             my %tableheader_translations = (
98             'En' => {
99             key => 'key',
100             value => 'value',
101             element => 'element',
102             comment => 'comment',
103             },
104             De => {
105             key => 'Schlüssel',
106             value => 'Wert',
107             element => 'Element',
108             comment => 'Kommentar',
109             },
110             ES => {
111             key => 'Clave',
112             value => 'Valor',
113             element => 'Elemento',
114             comment => 'Comentario',
115             },
116             FR => {
117             key => 'Clé',
118             value => 'Valeur',
119             element => 'Élément',
120             comment => 'Commentaire',
121             },
122             IT => {
123             key => 'Chiave',
124             value => 'Valore',
125             element => 'Elemento',
126             comment => 'Spiegazione',
127             },
128             Ja => {
129             key => 'キー',
130             value => '値',
131             element => '要素',
132             comment => '説明',
133             },
134             Hu => {
135             key => 'Kulcs',
136             value => 'Érték',
137             element => 'Alapelem',
138             comment => 'Magyarázat',
139             },
140             Pt => {
141             key => 'Chave',
142             value => 'Valor',
143             element => 'Element',
144             comment => 'Comentários',
145             },
146             Ro => {
147             key => 'Cheie',
148             value => 'Valoare',
149             element => 'Element',
150             comment => 'Descriere',
151             },
152             RU => {
153             key => 'Ключ',
154             value => 'Значение',
155             element => 'Элементы',
156             comment => 'Описание',
157             },
158             SK => {
159             key => 'Klúč',
160             value => 'Hodnota',
161             element => 'Element',
162             comment => 'Komentár',
163             },
164             Sv => {
165             key => 'Nyckelord',
166             value => 'Värde',
167             element => 'Element',
168             comment => 'Kommentar',
169             },
170             Tr => {
171             key => 'Anahtar',
172             value => 'Değer',
173             element => 'Öğe',
174             comment => 'Açıklama',
175             },
176             Lt => {
177             key => 'Kategorija',
178             value => 'Kodas',
179             element => 'Įvedimo būdai',
180             comment => 'Aprašymas',
181             },
182             Uk => {
183             key => 'Ключ',
184             value => 'Значення',
185             element => 'Елемент',
186             comment => 'Пояснення',
187             },
188             Traditional_Chinese => {
189             key => '類別',
190             value => '值',
191             element => '元素',
192             comment => '說明',
193             },
194             );
195              
196             # Build and compile regexes with all translations
197             foreach my $string ( qw(key value element comment) ){
198             my @translations = ();
199             foreach my $language ( values(%tableheader_translations) ){
200             push(@translations, $$language{$string});
201             }
202              
203             my $regex_string = join('|', @translations);
204             $self->{tableheader_translation_regexes}{$string} = qr/$regex_string/i;
205             }
206              
207             return $self;
208             }
209              
210             =head2 download
211              
212             Downloads Map Features from wiki.openstreetmap.org.
213              
214             Throws exceptions if something goes wrong.
215              
216             Returns: undef
217              
218             =cut
219              
220             sub download {
221             my $self = shift;
222              
223             # Setup HTTP useragent
224             my $ua = LWP::UserAgent->new;
225             $ua->agent("Geo_OSM_MapFeatures/$Geo::OSM::MapFeatures::VERSION");
226              
227             # Fetch MW parser output of page
228             my $req = HTTP::Request->new(GET => sprintf("%s?action=parse&prop=text&format=xml&page=%s", $self->api_url, $self->mapfeatures_pagename));
229              
230             warn "Fetching ".$req->uri."\n" if $self->trace();
231             my $res = $ua->request($req);
232              
233             if( ! $res->is_success ){
234             throw Geo::OSM::MapFeatures::Error::Network(sprintf("Couldn't fetch %s: %s", $req->uri, $res->status_line));
235             }
236              
237             $self->{content} = XMLin($res->content);
238             }
239              
240             =pod
241              
242             =head2 debug_download
243              
244             Download and cache in "mapfeatures.debug" in the current directory, to avoid
245             downloading the page again and again when developing.
246              
247             For example do something like the following:
248              
249             unless( $ENV{MAPFEATURESDEBUG} ){
250             $mf->download();
251             } else {
252             $mf->debug_download();
253             }
254              
255             =cut
256              
257             sub debug_download {
258             my $self = shift;
259              
260             if( -f 'mapfeatures.debug' ){
261             my $data = retrieve('mapfeatures.debug') or die;
262             $$self{content} = $$data{content};
263             } else {
264             $self->download();
265             my $data = {content => $$self{content}};
266             store($data, 'mapfeatures.debug') or die;
267             }
268             }
269              
270             =head2 parse
271              
272             Parses map features.
273              
274             =cut
275              
276             sub parse {
277             my $self = shift;
278              
279             throw Geo::OSM::MapFeatures::Error("No content, is it downloaded?")
280             unless $self->{content};
281              
282             throw Geo::OSM::MapFeatures::Error("Couldn't find element, something wrong with api.php?")
283             unless $self->{content}{parse}{text};
284              
285             my %data;
286              
287             my $tableextractor = HTML::TableExtract->new(
288             # Get header translation regexes with a hash slice
289             headers => [ @{$self->{tableheader_translation_regexes}}{qw(key value element comment)} ],
290             );
291             $tableextractor->parse($$self{content}{parse}{text});
292              
293             if( $tableextractor->tables == 0 ){
294             throw Geo::OSM::MapFeatures::Error::Parse("Did not find any tables");
295             }
296              
297             #DEBUG: $tableextractor->tree->dump;
298              
299             # Examine all matching tables
300             foreach my $table ($tableextractor->tables) {
301              
302             # Find headings before the table but at the same level.
303             # Loop through in reverse and find the first of each
304             # heading level upwards
305             my @headings = ();
306             my $lowestheading = 10;
307             foreach my $heading_elem ( reverse( grep { $_->tag() =~ /^h(?:\d)$/ } $table->tree->left ) ){
308             my( $headinglevel ) = $heading_elem->tag() =~ /^h(\d)$/;
309              
310             # Only store the first for a particular level
311             next if defined($headings[$headinglevel]);
312              
313             # Don't store a small heading if we already saw something
314             # larger. For example if we first saw h2 then h3 the h3
315             # belongs to the previous h2, not this one.
316             next if $#headings && $headinglevel > $lowestheading;
317             $lowestheading = $headinglevel;
318              
319             $headings[$headinglevel] = $heading_elem->as_trimmed_text;
320             }
321             @headings = grep { defined } @headings;
322             my $have_added_in_table_heading;
323              
324             foreach my $row ($table->rows) {
325              
326             # If the first column is spanned it's probably a heading
327             # dividing the table in parts.
328             # Make sure to push exactly the last one of these onto
329             # the list of headings
330             if( $$row[0]->attr('colspan') ){
331             pop(@headings) if $have_added_in_table_heading;
332             push(@headings, $$row[0]->as_trimmed_text);
333             $have_added_in_table_heading++;
334              
335             next;
336             }
337              
338             my $key = $$row[0];
339             my $value = $$row[1];
340             my $element = $$row[2];
341             my $description = $$row[3];
342              
343             $key = $key->as_trimmed_text;
344              
345             # Elements are given by images with filenames Mf_(node|way|area).png.
346             # This regex intentionally matches more, to make sure the module can detect that wikifiddlers have "invented" another element type or something
347             my @elementtypes = map { $_->attr('src') =~ /Mf_(\w+)\./ } $element->find('img');
348              
349             # Find values and split, either by
  • elements or by various text separators
  • 350             my @values;
    351             if( $value->find('li') ){
    352             @values = map { $_->as_trimmed_text } $value->find('li');
    353             } else {
    354             # Split on "/" (except for 24/7), "or" and "|"
    355             @values = split( m{\s*(?:(?as_trimmed_text );
    356             }
    357              
    358             $description = $description->as_trimmed_text;
    359              
    360             #DEBUG: print "Row: k='$key' v='".join("','",@values)."' e='".join("','",@elementtypes)."' c='$description'\n";
    361              
    362             my $feature = new Geo::OSM::MapFeatures::Feature($key, \@values, \@elementtypes, $description);
    363              
    364             #FIXME: There should be a real hierarchy, not just a category made by concatenating headings
    365             my $headingstring = join(' / ', @headings);
    366             push(@{$self->{features}{$headingstring}}, $feature);
    367             }
    368             }
    369             }
    370              
    371             =head2 categories
    372              
    373             Returns a list of feature categories.
    374              
    375             =cut
    376              
    377             sub categories {
    378             my $self = shift;
    379             return keys( %{ $self->{features} } );
    380             }
    381              
    382             =head2 features
    383              
    384             Returns a list of features.
    385              
    386             If given an argument it as taken as a category, and only features
    387             in that category will be returned.
    388              
    389             =cut
    390              
    391             sub features {
    392             my $self = shift;
    393             my $category = shift;
    394              
    395             if( defined($category) ){
    396             return @{ $self->{features}{$category} };
    397             } else {
    398             my @result = ();
    399             foreach my $category ( $self->categories() ){
    400             push(@result, $self->features($category));
    401             }
    402             return @result;
    403             }
    404             }
    405              
    406             =head1 Exception classes
    407              
    408             =head2 Geo::OSM::MapFeatures::Error
    409              
    410             Base exception class for errors thrown by this module
    411              
    412             =cut
    413              
    414             package Geo::OSM::MapFeatures::Error;
    415             use base qw(Error);
    416              
    417             sub new {
    418             my $self = shift;
    419             my $text = "" . shift;
    420             my $params = shift;
    421              
    422             local $Error::Depth = $Error::Depth + 1;
    423              
    424             $self->SUPER::new(-text => $text, %$params);
    425             }
    426              
    427             sub stringify {
    428             my $self = shift;
    429             my $text = $self->SUPER::stringify;
    430             $text .= sprintf(" at %s line %d.\n", $self->file, $self->line)
    431             unless($text =~ /\n$/s);
    432             $text;
    433             }
    434              
    435             =head2 Geo::OSM::MapFeatures::Error::Network
    436              
    437             Network error
    438              
    439             =cut
    440              
    441             package Geo::OSM::MapFeatures::Error::Network;
    442             our @ISA = qw(Geo::OSM::MapFeatures::Error);
    443              
    444             =head2 Geo::OSM::MapFeatures::Error::Parse
    445              
    446             Go find out who broke map feature this time...
    447              
    448             =cut
    449              
    450             package Geo::OSM::MapFeatures::Error::Parse;
    451             our @ISA = qw(Geo::OSM::MapFeatures::Error);
    452              
    453             =head1 AUTHOR
    454              
    455             Knut Arne Bjørndal, C<< >>
    456              
    457             =head1 BUGS
    458              
    459             Categories are currently made by concatenating headings above a feature. This should probably be a proper hierarchy instead.
    460              
    461             The table header translation table should probably be easier to patch from programs calling the module. Or maybe even downloaded from the wiki or something.
    462              
    463             Please report any bugs or feature requests to C, or through
    464             the web interface at L. I will be notified, and then you'll
    465             automatically be notified of progress on your bug as I make changes.
    466              
    467              
    468              
    469              
    470             =head1 SUPPORT
    471              
    472             You can find documentation for this module with the perldoc command.
    473              
    474             perldoc Geo::OSM::MapFeatures
    475              
    476              
    477             You can also look for information at:
    478              
    479             =over 4
    480              
    481             =item * RT: CPAN's request tracker
    482              
    483             L
    484              
    485             =item * AnnoCPAN: Annotated CPAN documentation
    486              
    487             L
    488              
    489             =item * CPAN Ratings
    490              
    491             L
    492              
    493             =item * Search CPAN
    494              
    495             L
    496              
    497             =back
    498              
    499              
    500             =head1 ACKNOWLEDGEMENTS
    501              
    502              
    503             =head1 COPYRIGHT & LICENSE
    504              
    505             Copyright 2008-2009 Knut Arne Bjørndal, all rights reserved.
    506              
    507             This program is free software; you can redistribute it and/or modify it
    508             under the same terms as Perl itself.
    509              
    510              
    511             =cut
    512              
    513             1; # End of Geo::OSM::MapFeatures