File Coverage

blib/lib/Flickr/Upload/FireEagle.pm
Criterion Covered Total %
statement 34 62 54.8
branch 0 2 0.0
condition 0 3 0.0
subroutine 12 17 70.5
pod n/a
total 46 84 54.7


line stmt bran cond sub pod time code
1             # $Id: FireEagle.pm,v 1.19 2008/04/22 07:01:19 asc Exp $
2              
3 1     1   1163 use strict;
  1         2  
  1         55  
4              
5             package FUFEException;
6 1     1   6 use base qw (Error);
  1         2  
  1         1498  
7              
8             # http://www.perl.com/lpt/a/690
9              
10 1     1   18692 use overload ('""' => 'stringify');
  1         2  
  1         8  
11              
12             sub new {
13 0     0     my $self = shift;
14 0           my $text = shift;
15 0           my $prev = shift;
16              
17 0 0         if (UNIVERSAL::can($prev, "stacktrace")){
18 0           $text .= "\n";
19 0           $text .= $prev->stacktrace();
20             }
21              
22 0           local $Error::Depth = $Error::Depth + 1;
23 0           local $Error::Debug = 1; # Enables storing of stacktrace
24              
25 0           $self->SUPER::new(-text => $text);
26             }
27              
28             sub stringify {
29 0     0     my $self = shift;
30 0   0       my $pkg = ref($self) || $self;
31 0           return sprintf("[%s] %s", $pkg, $self->stacktrace());
32             }
33              
34             package FlickrUploadException;
35 1     1   224 use base qw (FUFEException);
  1         2  
  1         615  
36              
37             package FlickrAPIException;
38 1     1   6 use base qw (FUFEException);
  1         1  
  1         613  
39              
40             sub new {
41 0     0     my $pkg = shift;
42 0           my $text = shift;
43 0           my $prev = shift;
44 0           my $code = shift;
45 0           my $msg = shift;
46              
47 0           $text .= " Error code $code";
48              
49 0           my $self = $pkg->SUPER::new($text, $prev);
50              
51 0           $self->{'api_error_code'} = $code;
52 0           $self->{'api_error_message'} = $msg;
53 0           return $self;
54             }
55              
56             sub error_code {
57 0     0     my $self = shift;
58 0           return $self->{'api_error_code'};
59             }
60              
61             sub error_message {
62 0     0     my $self = shift;
63 0           return $self->{'api_error_message'};
64             }
65              
66             package NetFireEagleException;
67 1     1   5 use base qw (FUFEException);
  1         2  
  1         448  
68              
69             package Flickr::Upload::FireEagle;
70 1     1   12 use base qw (Flickr::Upload);
  1         2  
  1         968  
71              
72             $Flickr::Upload::FireEagle::VERSION = '0.1';
73              
74             =head1 NAME
75              
76             Flickr::Upload::FireEagle - Flickr::Upload subclass to assign location information using FireEagle
77              
78             =head1 SYNOPSIS
79              
80             use Getopt::Std;
81             use Config::Simple;
82             use Flickr::Upload::FireEagle;
83              
84             # c: path to a config file
85             # p: path to a photo
86              
87             my %opts = ();
88             getopts('c:p:', \%opts);
89            
90             my $cfg = Config::Simple->new($opts{'c'});
91              
92             my %fireeagle_args = ('consumer_key' => $cfg->param('fireeagle.consumer_key'),
93             'consumer_secret' => $cfg->param('fireeagle.consumer_secret'),
94             'access_token' => $cfg->param('fireeagle.access_token'),
95             'access_token_secret' => $cfg->param('fireeagle.access_token_secret'));
96              
97             my %flickr_args = ('key' => $cfg->param('flickr.api_key'),
98             'secret' => $cfg->param('flickr.api_secret'),
99             'fireeagle' => \%fireeagle_args);
100              
101             my $uploadr = Flickr::Upload::FireEagle->new(\%flickr_args);
102              
103             my $photo_id = $uploadr->upload('photo' => $opts{'p'},
104             'auth_token' => $cfg->param('flickr.auth_token'));
105            
106             print "photo : $photo_id\n";
107              
108             =head1 DESCRIPTION
109              
110             Flickr::Upload subclass to assign location information using FireEagle and if
111             a photo contains relevant GPS information in its EXIF headers update your
112             location as well.
113              
114             =head1 HOW DOES IT WORK?
115              
116             Well. It's a bit involved.
117              
118             The first thing that happens is the photo you're trying to upload is poked for EXIF data,
119             specifically any GPS information and when it was taken (the using I field).
120              
121             If there is no date information, the current time is assumed.
122              
123             If there is GPS data then the date is tested to see if the photo was taken falls within an
124             allowable window of time. By default, this is (1) hour from "right now". An alternate value
125             may be set by passing an I argument, measured in seconds, to the I method.
126              
127             If the GPS information was added recently enough then FireEagle is queried for your
128             most recent location hierarchy. If the GPS information is more recent than the data
129             stored in the hierarchy (the location with the "best guess" of being correct) then FireEagle
130             is updated with the latitude and longitude recorded in the photo.
131              
132             Moving right along, whether or not we've just updated FireEagle the service is queried
133             for your current location (again).
134              
135             Once the hierarchy has been retrieved, the next step is to try and retrieve a "context" node.
136             Whereas when testing GPS information the "best guess" node is assumed this is not necessarily
137             the case when trying to use FireEagle to add tags.
138              
139             The context node is determined by comparing the photo's date against the I (or
140             date recorded) attribute for specific items in the FireEagle hierarchy. Since most cameras
141             still don't record GPS information it is necessary to do some work to gues^H^H^H I mean
142             infer how "close" you are to the last recorded location.
143              
144             For example, if it's been more than a couple of hours since you last updated FireEagle you
145             might still be in the same neighbourhood but if it's been more than half a day chances are
146             good that you're been on the move but are still in the same city.
147              
148             (It goes without saying that there are lots of edge cases some of which will try to be addressed
149             in the as-yet unwritten Flickr::Upload::FireDopplr.)
150              
151             The following tests are applied :
152              
153             =over 4
154              
155             =item * First a "best guess" location is queried
156              
157             If it is present and its I date is less than or equal to an hour, it is the
158             context node.
159              
160             An alternate value may be set by passing a I argument, measured in
161             seconds, to the I method.
162              
163             =item * Next a location of type "neighborhood" is queried
164              
165             If it is present and its I date is less than or equal to two hours, it is the
166             context node.
167              
168             An alternate value may be set by passing a I (or neighborhood)
169             argument, measured in seconds, to the I method.
170              
171             =item * Next a location of type "locality" is queried
172              
173             If it is present and its I date is less than or equal to twelve hours, it is the
174             context node.
175              
176             An alternate value may be set by passing a I argument, measured
177             in seconds, to the I method.
178              
179             =item * If none of those tests pass then...
180              
181             ...there is no context node.
182              
183             =back
184              
185             Assuming that a context node has been identified I there is GPS information stored in the
186             photo, the I method is called (passing the photo's latitude and
187             longitude) to ensure that the (Flickr) places IDs for both the response and the context node match.
188              
189             If they I match then the context node is destroyed and the following tags are added :
190             places:PLACETYPE=PLACEID; woe:id=WOEID; the name of the location (formatted according to the
191             object's "tagify" rules).
192              
193             On the other hand, if the context node is still around, after all that, then it is used to add tags.
194              
195             At a minimum a fireeagle:id=CONTEXTNODEID tag is added. If the place type for the context node is
196             equal to or more precise than a neighbourhood, the neighbourhood's name is added as a tag. If the
197             place type for the context node is equal to or more precise than a locality, the locality's name
198             is added as a tag as well as fireeagle:id=ID, places:locality=PLACEID and woe:id=WOEID tags.
199              
200             We're almost done : Assuming a context node and no GPS information in the photo, the nodes latitude
201             and longitude are calculated to use as arguments when calling the I
202             method.
203              
204             The coordinates are "calculated" because not every location in the FireEagle hierarchy has a centroid.
205             If no centroid is present then the node's bounding box is used and the centroid is assumed to be
206             the center of the box. The photo's "accuracy" (in Flickr terms) is determined according to the node's
207             place type.
208              
209             Finally, the photo is uploaded (and geotagged if necessary).
210              
211             No really.
212              
213             =head1 ERROR HANDLING
214              
215             Flickr::Upload::FireEagle subclasses Error.pm to catch and throw exceptions. Although
216             this is still a mostly un-Perl-ish way of doing things, it seemed like the most sensible
217             way to handle the variety of error cases. I don't love it but we'll see.
218              
219             This means that the library B and you will need to
220             code around it using either I or - even better - I and I blocks.
221              
222             There are four package specific exception handlers :
223              
224             =over 4
225              
226             =item * B
227              
228             An error condition specific to I was triggered.
229              
230             =item * B
231              
232             An error condition specific to I was triggered.
233              
234             =item * B
235              
236             An error condition specific to calling the Flickr API (read : I)
237             was triggered.
238              
239             This is the only exception handler that defines its own additional methods. They
240             are :
241              
242             =over 4
243              
244             =item * B
245              
246             The numeric error code returned by the Flickr API.
247              
248             =item * B
249              
250             The textual error message returned by the Flickr API.
251              
252             =back
253              
254             =item * B
255              
256             An error condition specific to I was triggered.
257              
258             =back
259              
260             =head1 CAVEATS
261              
262             =over 4
263              
264             =item *
265              
266             Asynchronous uploads are not support and will trigger an exception.
267              
268             =back
269              
270             =cut
271              
272 1     1   132237 use Net::FireEagle;
  1         166777  
  1         36  
273 1     1   863 use Image::Info qw (image_info);
  1         1467  
  1         69  
274 1     1   802 use Date::Parse;
  1         13002  
  1         153  
275 1     1   12 use Error qw(:try);
  1         2  
  1         10  
276 1     1   649 use XML::XPath;
  0            
  0            
277             use Readonly;
278             use Geo::Coordinates::DecimalDegrees;
279              
280             $Error::Debug = 1;
281              
282             Readonly::Scalar my $OFFSET_GPS => 60 * 60 * 1;
283             Readonly::Scalar my $OFFSET_FIREEAGLE_EXACT => 60 * 60 * 1;
284             Readonly::Scalar my $OFFSET_FIREEAGLE_NEIGHBOURHOOD => 60 * 60 * 2;
285             Readonly::Scalar my $OFFSET_FIREEAGLE_LOCALITY => 60 * 60 * 12;
286              
287             Readonly::Hash my %PLACEMAP => (
288             0 => 16, # exact
289             1 => 13, # postal
290             2 => 14, # neighbourhood
291             3 => 11, # city
292             4 => 9, # county
293             5 => 5, # state
294             6 => 1, # country
295             );
296              
297             =head1 PACKAGE METHODS
298              
299             =head2 __PACKAGE__->new(\%args)
300              
301             All the same arguments required by the I constructor plus the
302             following :
303              
304             =over 4
305              
306             =item * B
307              
308             A hash reference containing the following keys :
309              
310             =over 4
311              
312             =item * B
313              
314             String. I
315              
316             A valid FireEagle consumer key.
317              
318             =item * B
319              
320             String. I
321              
322             A valid FireEagle consumer secret.
323              
324             =item * B
325              
326             String. I
327              
328             A valid FireEagle access token.
329              
330             =item * B
331              
332             String. I
333              
334             A valid FireEagle access token secret.
335              
336             =item * B
337              
338             String.
339              
340             An optional flag to format tags for cities, specific to a service. Valid
341             services are :
342              
343             =over 4
344              
345             =item * B
346              
347             City names are lower-cased and spaces are removed.
348              
349             =item * B
350              
351             City names are wrapped in double-quotes if they contain spaces.
352              
353             =back
354              
355             The default value is I
356              
357             =back
358              
359             =back
360              
361             Returns a I object.
362              
363             =cut
364              
365             sub new {
366             my $pkg = shift;
367             my $args = shift;
368              
369             my $fe_args = $args->{'fireeagle'};
370             delete($args->{'fireeagle'});
371              
372             # First try to become Flickr::Upload
373              
374             my $self = undef;
375              
376             try {
377             $self = $pkg->SUPER::new($args);
378             }
379            
380             catch Error with {
381             my $e = shift;
382             throw FlickrUploadException("Failed to instantiate Flickr::Upload", $e);
383             };
384              
385             # Next, load up the FireEagle love
386              
387             try {
388             $self->{'__fireeagle'} = Net::FireEagle->new(%{$fe_args});
389             }
390            
391             catch Error with {
392             my $e = shift;
393             throw NetFireEagleException("Failed to instantiate Net::FireEagle", $e);
394             };
395              
396             $self->{'__fireeagle_args'} = $fe_args;
397             return $self;
398             }
399              
400             # A secret ("secret") for now...
401              
402             sub new_from_config {
403             my $pkg = shift;
404             my $path = shift;
405              
406             my $cfg = (ref($path) eq "Config::Simple") ? $path : Config::Simple->new($path);
407              
408             my %fireeagle_args = ('consumer_key' => $cfg->param('fireeagle.consumer_key'),
409             'consumer_secret' => $cfg->param('fireeagle.consumer_secret'),
410             'access_token' => $cfg->param('fireeagle.access_token'),
411             'access_token_secret' => $cfg->param('fireeagle.access_token_secret'));
412            
413             my %flickr_args = ('key' => $cfg->param('flickr.api_key'),
414             'secret' => $cfg->param('flickr.api_secret'),
415             'fireeagle' => \%fireeagle_args);
416              
417             return $pkg->new(\%flickr_args);
418             }
419              
420             =head1 OBJECT METHODS YOU SHOULD CARE ABOUT
421              
422             =head2 $obj->upload(%args)
423              
424             Valid arguments are anything you would pass the Flickr::Upload I method B
425             the I flag which is not honoured yet. I'm working on it.
426              
427             In additional, you may pass the following optional parameters :
428              
429             =over 4
430              
431             =item * B
432              
433             This must be a hash reference with the following keys :
434              
435             =over 4
436              
437             =item * B
438              
439             Hash reference.
440              
441             A hash reference containing is_public, is_contact, is_family and is_friend
442             keys and their boolean values to set the geo permissions on your uploaded photo.
443              
444             If this is not defined then your default viewing settings for geo data will be left
445             in place.
446              
447             =back
448              
449             =item * B
450              
451             Int.
452              
453             The maximum amount of time (in seconds) between the time of your last FireEagle update
454             and the date on which the photo was taken in which a photo can be considered reliable
455             for updating your location in FireEagle.
456              
457             The default is 3600 (seconds, or 1 hour).
458              
459             =item * B
460              
461             The maximum amount of time (in seconds) between the time of your last FireEagle update
462             and the date on which the photo was taken in which FireEagle can be considered reliable
463             for updating your location in FireEagle at street level.
464              
465             The default is 3600 (seconds, or 1 hour).
466              
467             =item * B (or offset_fireeagle_neighborhood)
468              
469             The maximum amount of time (in seconds) between the time of your last FireEagle update
470             and the date on which the photo was taken in which FireEagle can be considered reliable
471             for updating your location in FireEagle at the neighbourhood level.
472              
473             The default is 7200 (seconds, or 2 hours).
474              
475             =item * B
476              
477             The maximum amount of time (in seconds) between the time of your last FireEagle update
478             and the date on which the photo was taken in which FireEagle can be considered reliable
479             for updating your location in FireEagle at the locality (city) level.
480              
481             The default is 43200 (seconds, or 12 hours).
482              
483             =back
484              
485             Returns a photo ID!
486              
487             =cut
488              
489             sub upload {
490             my $self = shift;
491             my %args = @_;
492              
493             if ($args{'async'}){
494             throw FUFEException("Asynchronous uploads are not supported yet");
495             }
496            
497             #
498             # As in geo perms, etc. Store for later.
499             #
500              
501             my $geo = undef;
502              
503             if (ref($args{'geo'}) eq "HASH"){
504             $geo = $args{'geo'};
505             delete($args{'geo'});
506             }
507              
508             #
509             # Check to see if the photo has GPS info
510             # (this will be picked up by the Flickr upload wah-wah)
511             #
512              
513             my $info = undef;
514             my $has_gps = 0;
515             my $ph_date = time();
516              
517             my $fresh_loc = 0;
518              
519             eval {
520             $info = image_info($args{'photo'});
521              
522             if (($info) && (ref($info->{'GPSLatitude'}))){
523             $has_gps = 1;
524             }
525              
526             if (exists($info->{'DateTimeOriginal'})){
527             $ph_date = str2time($info->{'DateTimeOriginal'});
528             }
529             };
530              
531             if ($@){
532             warn "EXIF parsing failed : $@, carrying on anyway";
533             }
534              
535             #
536             #
537             #
538              
539             if ($has_gps){
540              
541             my $offset = $args{'offset_gps'} || $OFFSET_GPS;
542             my $test = (time() - $ph_date);
543              
544             # If we have GPS data see if we can update FireEagle
545             # while we're at it. First ensure that the photo's
546             # date falls within some allowable window in "now"
547            
548             # print "[GPS] TEST : $test ($offset)\n";
549             # print "[GPS] date : $ph_date\n";
550              
551             if ($test < $offset){
552              
553             # Assuming it does then make sure that FireEagle
554             # doesn't already have a more recent location update
555              
556             if (my $hier = $self->fetch_fireeagle_hierarchy()){
557            
558             my $best = str2time($hier->findvalue("/rsp/user/location-hierarchy/location[\@best-guess='true']/located-at"));
559            
560             # Make sure FireEagle doesn't already have
561             # a more recent update.
562            
563             # To do, maybe : try to figure out which of the two
564             # locations is more precise....
565              
566             # print "[GPS] BEST : $best\n";
567             # print "[GPS] date : $ph_date\n";
568              
569             if ($ph_date > $best){
570            
571             my ($lat, $lon) = $self->gps_exif_to_latlon($info);
572             my %update = ('lat' => $lat, 'lon' => $lon);
573            
574             if ($self->{'__fireeagle'}->update_location(\%update)){
575             $fresh_loc = 1;
576             }
577            
578             else {
579             warn "Failed to update location in FireEagle, chugging along anyway";
580             }
581             }
582             }
583             }
584             }
585              
586             #
587             # Now ask FireEagle where we are and do some sanity checking on
588             # the date of the last update also trying to sync it up with any
589             # date information that comes out of the EXIF stored above in $ph_date
590             #
591              
592             if (! exists($args{'tags'})){
593             $args{'tags'} = '';
594             }
595              
596             my $hier = $self->fetch_fireeagle_hierarchy();
597             my $ctx = undef;
598            
599             if ($hier){
600            
601             $ctx = $self->get_fireeagle_context_node($hier, $ph_date, \%args);
602            
603             if (! $ctx){
604             warn "Too much time has passed between your photo and FireEagle to feel comfortable saying";
605             }
606              
607             # Do some final sanity checking if we know what our lat, lon is
608              
609             elsif ($has_gps){
610              
611             # please cache me...
612             my ($lat, $lon) = $self->gps_exif_to_latlon($info);
613            
614             try {
615             my $res = $self->flickr_api_call('flickr.places.findByLatLon', {'lat' => $lat, 'lon' => $lon});
616             my $xml = XML::XPath->new('xml' => $res->decoded_content());
617              
618             my $fe_placeid = $ctx->findvalue("place-id");
619             my $fl_placeid = $xml->findvalue("/rsp/places/place/\@place_id")->string_value();;
620              
621             if ($fe_placeid ne $fl_placeid){
622              
623             warn "Mismatch between Flickr and FireEagle place IDs ($fl_placeid, $fe_placeid) based on lat/lon, deferring to Flickr";
624              
625             my $fe_id = $ctx->findvalue("id")->string_value();
626             $ctx = undef;
627              
628             my $placetype = $xml->findvalue("/rsp/places/place/\@place_type");
629             my $woeid = $xml->findvalue("/rsp/places/place/\@woeid");
630            
631             my @tags = (
632             "fireeagle:id=$fe_id",
633             "places:$placetype=$fl_placeid",
634             "woe:id=$woeid",
635             );
636            
637             if (my $name = $self->fetch_places_name($fl_placeid)){
638             my $model = $self->{'__fireeagle_args'}->{'tagify'} || "flickr";
639             push @tags, $self->tagify(lc($name), $model);
640             }
641            
642             $args{'tags'} .= ' ';
643             $args{'tags'} .= join(' ', @tags);
644             }
645             }
646            
647             catch Error with {
648             # pass
649             };
650             }
651              
652             }
653              
654             #
655             # Okay - now add tags
656             #
657              
658             if ($ctx){
659             $args{'tags'} .= ' ';
660             $args{'tags'} .= $self->generate_location_tags($hier, $ctx);
661             }
662              
663             #
664             # If we don't have EXIF data and we have something useful from
665             # FireEagle try to use that to update the geo information for
666             # the photo
667             #
668              
669             my %extra = ();
670              
671             if ((! $has_gps) && ($ctx)){
672              
673             my ($lat, $lon) = split(" ", $ctx->findvalue("georss:point"));
674              
675             # FireEagle doesn't always return a centroid....grrr!
676              
677             if (! $lat){
678              
679             my ($swlat, $swlon, $nelat, $nelon) = split(" ", $ctx->findvalue("georss:box"));
680              
681             my $diff_lat = ($nelat - $swlat) / 2;
682             my $diff_lon = ($nelon - $swlon) / 2;
683              
684             $lat = $swlat + $diff_lat;
685             $lon = $swlon + $diff_lon;
686             }
687              
688             my $placetype = $ctx->findvalue("level")->string_value();
689             my $acc = (exists($PLACEMAP{$placetype})) ? $PLACEMAP{$placetype} : 16;
690              
691             $extra{'lat'} = $lat;
692             $extra{'lon'} = $lon;
693             $extra{'acc'} = $acc;
694             }
695              
696             if (exists($geo->{'perms'})){
697             $extra{'geo_perms'} = $geo->{'perms'};
698             }
699              
700             #
701             # Finally, upload
702             #
703              
704             # use Data::Dumper;
705             # print Dumper(\%args);
706             # print Dumper(\%extra);
707              
708             my $id = $self->please_to_upload_for_real_now(\%args, \%extra);
709            
710             #
711              
712             return $id;
713             }
714              
715             sub get_fireeagle_context_node {
716             my $self = shift;
717             my $hier = shift;
718             my $ph_date = shift;
719             my $args = shift;
720              
721             # right here, right now
722              
723             if (my $best = ($hier->findnodes("/rsp/user/location-hierarchy/location[\@best-guess='true']"))[0]){
724              
725             my $last_date = str2time($best->findvalue("located-at"));
726             my $offset = $args->{'offset_fireeagle_exact'} || $OFFSET_FIREEAGLE_EXACT;
727              
728             if ($self->test_fireeagle_context_date($last_date, $ph_date, $offset)){
729             return $best;
730             }
731             }
732            
733             # neighbourhoods
734              
735             if (my $hood = ($hier->findnodes("/rsp/user/location-hierarchy/location[level=2]"))[0]){
736              
737             my $last_date = str2time($hood->findvalue("located-at"));
738             my $offset = $args->{'offset_fireeagle_neighbourhood'} || $args->{'offset_fireeagle_neighborhood'} || $OFFSET_FIREEAGLE_NEIGHBOURHOOD;
739              
740             if ($self->test_fireeagle_context_date($last_date, $ph_date, $offset)){
741             return $hood;
742             }
743             }
744              
745             #
746              
747             if (my $city = ($hier->findnodes("/rsp/user/location-hierarchy/location[level=3]"))[0]){
748              
749             my $last_date = str2time($city->findvalue("located-at"));
750             my $offset = $args->{'offset_fireeagle_locality'} || $OFFSET_FIREEAGLE_LOCALITY;
751              
752             if ($self->test_fireeagle_context_date($last_date, $ph_date, $offset)){
753             return $city;
754             }
755             }
756              
757             return undef;
758             }
759              
760             sub test_fireeagle_context_date {
761             my $self = shift;
762             my $fe_date = shift;
763             my $ph_date = shift;
764             my $offset = shift;
765              
766             # print "FE DATE : $fe_date\n";
767             # print "PH DATE : $ph_date\n";
768             # print "OFFSET : $offset\n";
769              
770             # FireEagle has a more recent update date than
771             # the photo's DateTaken time
772              
773             if ($fe_date > $ph_date){
774              
775             my $test = ($fe_date - $ph_date);
776              
777             if ($test <= $offset){
778             return 1;
779             }
780              
781             return 0;
782             }
783              
784             # Photo is more recent than FireEagle
785              
786             my $test = ($ph_date - $fe_date);
787              
788             if ($test <= $offset){
789             return 1;
790             }
791              
792             return 0;
793             }
794              
795             sub generate_location_tags {
796             my $self = shift;
797             my $hier = shift;
798             my $ctx = shift;
799              
800             my $id = $ctx->findvalue("id");
801             my $ctx_level = $ctx->findvalue("level")->string_value();
802            
803             my @tags = ("fireeagle:id=$id");
804              
805             foreach my $node ($hier->findnodes("/rsp/user/location-hierarchy/location")){
806              
807             my $placeid = $node->findvalue("place-id");
808             my $node_level = $node->findvalue("level")->string_value();
809              
810             if ($node_level < $ctx_level){
811             next;
812             }
813              
814             if ($node_level == 2){
815             if (my $name = $self->fetch_places_name($placeid)){
816             my $model = $self->{'__fireeagle_args'}->{'tagify'} || "flickr";
817             push @tags, $self->tagify(lc($name), $model);
818             }
819            
820             next;
821             }
822              
823             if ($node_level == 3){
824            
825             my $woeid = $node->findvalue("woeid");
826              
827             push @tags, "places:locality=$placeid";
828             push @tags, "woe:id=$woeid";
829              
830             if (my $name = $self->fetch_places_name($placeid)){
831             my $model = $self->{'__fireeagle_args'}->{'tagify'} || "flickr";
832             push @tags, $self->tagify(lc($name), $model);
833             }
834              
835             last;
836             }
837             }
838              
839             return join(" ", @tags);
840             }
841              
842             sub fetch_places_name {
843             my $self = shift;
844             my $placeid = shift;
845              
846             try {
847             my $res = $self->flickr_api_call('flickr.places.resolvePlaceId', {'place_id' => $placeid});
848             my $xml = XML::XPath->new('xml' => $res->decoded_content());
849            
850             return $xml->findvalue("/rsp/location/\@name");
851             }
852            
853             catch Error with {
854             # pass
855             };
856             }
857              
858             #
859             # All of the code to follow needs to be moved into
860             # Flickr::Upload::Localitify and merged with Flickr::Upload::Dopplr
861             #
862              
863             sub gps_exif_to_latlon {
864             my $self = shift;
865             my $info = shift;
866              
867             my $parts_lat = $info->{'GPSLatitude'};
868             my $parts_lon = $info->{'GPSLongitude'};
869              
870             my $ref_lat = uc($info->{'GPSLatitudeRef'});
871             my $ref_lon = uc($info->{'GPSLongitudeRef'});
872              
873             my $lat = dms2decimal($parts_lat->[0], $parts_lat->[2], ($parts_lat->[4] / 100));
874             my $lon = dms2decimal($parts_lon->[0], $parts_lon->[2], ($parts_lon->[4] / 100));
875              
876             if ($ref_lat eq 'S'){
877             $lat = - $lat;
878             }
879              
880             if ($ref_lon eq 'W'){
881             $lon = - $lon;
882             }
883              
884             return ($lat, $lon);
885             }
886              
887             sub please_to_upload_for_real_now(){
888             my $self = shift;
889             my $args = shift;
890             my $extra = shift;
891              
892             my $id = 0;
893              
894             try {
895             $id = $self->SUPER::upload(%$args);
896             }
897              
898             catch Error with {
899             throw FlickrUploadException("Failed to upload photo to Flickr", shift);
900             };
901              
902             if (! $id){
903             throw FlickrUploadException("Flickr::Upload did not return a photo ID");
904             }
905              
906             #
907             # set lat/lon
908             #
909              
910             if (exists($extra->{'lat'})){
911             my %set = ('accuracy' => $args->{'acc'},
912             'lat' => $args->{'lat'},
913             'lon' => $args->{'lon'},
914             'auth_token' => $args->{'auth_token'},
915             'photo_id' => $id);
916            
917             $self->flickr_api_call('flickr.photos.geo.setLocation', \%set);
918             }
919            
920             #
921             # set geo perms
922             #
923              
924             if (exists($extra->{'geo_perms'})){
925              
926             my %perms = %{$extra->{'geo_perms'}};
927              
928             $perms{'auth_token'} = $args->{'auth_token'};
929             $perms{'photo_id'} = $id;
930              
931             if ($perms{'is_public'}){
932             foreach my $other ('is_contact', 'is_family', 'is_friend'){
933             if (! exists($perms{$other})){
934             $perms{$other} = 1;
935             }
936             }
937             }
938              
939             $self->flickr_api_call('flickr.photos.geo.setPerms', \%perms);
940             }
941              
942             #
943              
944             return $id;
945             }
946              
947             sub fetch_fireeagle_hierarchy {
948             my $self = shift;
949              
950             my $loc = undef;
951              
952             try {
953             my $xml = $self->{'__fireeagle'}->location();
954             $loc = XML::XPath->new('xml' => $xml);
955             # print $xml . "\n";
956             }
957              
958             catch Error with {
959             warn "Failed to get current location from FireEagle, chugging along anyway";
960             };
961              
962             return $loc;
963             }
964              
965             sub flickr_api_call {
966             my $self = shift;
967             my $meth = shift;
968             my $args = shift;
969              
970             my $res;
971              
972             try {
973             $res = $self->execute_method($meth, $args);
974             }
975            
976             catch Error with {
977             my $e = shift;
978             throw FlickrAPIException("API call $meth failed", 999, "Unknown API error");
979             };
980              
981             if (! $res->{success}){
982             my $e = shift;
983             throw FlickrAPIException("API call $meth failed", $e, $res->{error_code}, $res->{error_message});
984             }
985              
986             return $res;
987             }
988              
989              
990             #
991             # Please for someone to write Text::Tagify...
992             #
993              
994             sub tagify {
995             my $self = shift;
996             my $tag = shift;
997             my $model = shift;
998              
999             if (($model) && ($model eq "delicious")){
1000             return $self->tagify_like_delicious($tag);
1001             }
1002              
1003             return $self->tagify_like_flickr($tag);
1004             }
1005              
1006             sub tagify_like_flickr {
1007             my $self = shift;
1008             my $tag = shift;
1009              
1010             if ($tag =~ /\s/){
1011             $tag = "\"$tag\"";
1012             }
1013              
1014             return $tag;
1015             }
1016              
1017             sub tagify_like_delicious {
1018             my $self = shift;
1019             my $tag = shift;
1020              
1021             $tag =~ s/\s//g;
1022             return lc($tag);
1023             }
1024              
1025             #
1026             # Just so so so wrong...but necessary until Flickr::Upload
1027             # is updated to call $res->decoded_content()
1028             #
1029              
1030             sub upload_request($$) {
1031             my $self = shift;
1032             die "$self is not a LWP::UserAgent" unless $self->isa('LWP::UserAgent');
1033             my $req = shift;
1034             die "expecting a HTTP::Request" unless $req->isa('HTTP::Request');
1035              
1036             my $res = $self->request( $req );
1037              
1038             my $tree = XML::Parser::Lite::Tree::instance()->parse($res->decoded_content());
1039             return () unless defined $tree;
1040              
1041             my $photoid = response_tag($tree, 'rsp', 'photoid');
1042             my $ticketid = response_tag($tree, 'rsp', 'ticketid');
1043             unless( defined $photoid or defined $ticketid ) {
1044             print STDERR "upload failed:\n", $res->content(), "\n";
1045             return undef;
1046             }
1047              
1048             return (defined $photoid) ? $photoid : $ticketid;
1049             }
1050              
1051             sub response_tag {
1052             my $t = shift;
1053             my $node = shift;
1054             my $tag = shift;
1055              
1056             return undef unless defined $t and exists $t->{'children'};
1057              
1058             for my $n ( @{$t->{'children'}} ) {
1059             next unless defined $n and exists $n->{'name'} and exists $n->{'children'};
1060             next unless $n->{'name'} eq $node;
1061              
1062             for my $m (@{$n->{'children'}} ) {
1063             next unless exists $m->{'name'}
1064             and $m->{'name'} eq $tag
1065             and exists $m->{'children'};
1066              
1067             return $m->{'children'}->[0]->{'content'};
1068             }
1069             }
1070             return undef;
1071             }
1072              
1073             =head1 VERSION
1074              
1075             0.1
1076              
1077             =head1 DATE
1078              
1079             $Date: 2008/04/22 07:01:19 $
1080              
1081             =head1 AUTHOR
1082              
1083             Aaron Straup Cope
1084              
1085             =head1 NOTES
1086              
1087             Aside from requiring your own Flickr API key, secret and authentication token
1088             you will also need similar FireEagle (OAuth) credentials. Since Flickr::Upload::FireEagle
1089             already requires that you install the excellent I you should just
1090             use the command line I client for authorizing yourself with FireEagle.
1091              
1092             =head1 SEE ALSO
1093              
1094             L
1095              
1096             L
1097              
1098             L
1099              
1100             L
1101              
1102             L
1103              
1104             L
1105              
1106             L
1107              
1108             L
1109              
1110             L
1111              
1112             =head1 BUGS
1113              
1114             Sure, why not.
1115              
1116             Please report all bugs via http://rt.cpan.org/
1117              
1118             =head1 LICENSE
1119              
1120             Copyright (c) 2007-2008 Aaron Straup Cope. All Rights Reserved.
1121              
1122             This is free software. You may redistribute it and/or
1123             modify it under the same terms as Perl itself.
1124              
1125             =cut
1126              
1127             return 1;