File Coverage

blib/lib/Net/Flickr/Geo.pm
Criterion Covered Total %
statement 6 6 100.0
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 8 8 100.0


line stmt bran cond sub pod time code
1             # $Id.pm,v 1.11 2007/06/07 06:55:36 asc Exp $
2              
3 1     1   1819 use strict;
  1         4  
  1         63  
4              
5             package Net::Flickr::Geo;
6 1     1   6 use base qw (Net::Flickr::API);
  1         3  
  1         788  
7              
8             $Net::Flickr::Geo::VERSION = '0.72';
9              
10             =head1 NAME
11              
12             Net::Flickr::Geo - tools for working with geotagged Flickr photos
13              
14             =head1 SYNOPSIS
15              
16             There is no synopsis. There is only documentation for provider specific
17             packages. Okay, I lied. There's a little bit below. But really, please
18             consult provider specific packaged for details.
19              
20             =head1 DESCRIPTION
21              
22             Tools for working with geotagged Flickr photos.
23              
24             =head1 PROVIDERS
25              
26             =head2 ModestMaps
27              
28             Fetch maps using the Modest Maps ws-pinwin HTTP interface :
29              
30             #
31             # Simple
32             #
33              
34             my %opts = ();
35             getopts('c:i:', \%opts);
36              
37             my $cfg = Config::Simple->new($opts{'c'});
38              
39             $cfg->param("pinwin.photo_size", "Medium");
40             $cfg->param("modestmaps.filter", "atkinson");
41             $cfg->param("pinwin.upload", 1);
42            
43             my $fl = Net::Flickr::Geo::ModestMaps->new($cfg);
44             $fl->log()->add(Log::Dispatch::Screen->new('name' => 'scr', min_level => 'debug'));
45              
46             my $map = $fl->mk_pinwin_map_for_photo($opts{'i'});
47             $fl->log()->info("wrote map to $map->[0]->[0]");
48              
49             #
50             # Fancy
51             #
52              
53             my %opts = ();
54             getopts('c:s:', \%opts);
55              
56             my $cfg = Config::Simple->new($opts{'c'});
57              
58             my $fl = Net::Flickr::Geo::ModestMaps->new($cfg);
59             $fl->log()->add(Log::Dispatch::Screen->new('name' => 'scr', min_level => 'info'));
60              
61             my $data = $fl->mk_poster_map_for_photoset($opts{'s'});
62             $fl->log()->info(Dumper($data));
63              
64             my $tiles = $fl->upload_poster_map($data->{'path'});
65             $fl->log()->info(Dumper($tiles));
66              
67             =head2 YahooMaps
68              
69             Fetch maps using the Yahoo! Maps Image API :
70              
71             #
72             # Simple
73             #
74              
75             my %opts = ();
76             getopts('c:i:', \%opts);
77              
78             my $cfg = Config::Simple->new($opts{'c'});
79              
80             my $fl = Net::Flickr::Geo::YahooMaps->new($cfg);
81             $fl->log()->add(Log::Dispatch::Screen->new('name' => 'scr', min_level => 'debug'));
82              
83             my $map = $fl->mk_pinwin_map_for_photo($opts{'i'});
84             $fl->log()->info("wrote map to $map->[0]->[0]");
85              
86             #
87             # Handy
88             #
89              
90             my %opts = ();
91             getopts('c:s:', \%opts);
92              
93             my $cfg = Config::Simple->new($opts{'c'});
94              
95             my $fl = Net::Flickr::Geo::YahooMaps->new($cfg);
96             $fl->log()->add(Log::Dispatch::Screen->new('name' => 'scr', min_level => 'debug'));
97              
98             my $map = $fl->mk_pinwin_maps_for_photoset($opts{'s'});
99              
100             foreach my $data (@$map){
101             $fl->log()->info("wrote image/map to $data->[0]");
102             }
103              
104             =head2 Google Maps
105              
106             Fetch maps using the Google Maps Static Maps API :
107              
108             #
109             # Simple
110             #
111              
112             my %opts = ();
113             getopts('c:i:', \%opts);
114              
115             my $cfg = Config::Simple->new($opts{'c'});
116              
117             my $fl = Net::Flickr::Geo::GoogleMaps->new($cfg);
118             $fl->log()->add(Log::Dispatch::Screen->new('name' => 'scr', min_level => 'debug'));
119              
120             $cfg->param("google.map_type", "mobile");
121              
122             my $map = $fl->mk_pinwin_map_for_photo($opts{'i'});
123             $fl->log()->info("wrote map to $map->[0]->[0]");
124              
125             =head2 MultiMaps
126              
127             Fetch maps using the MultiMap Static Maps API :
128              
129             #
130             # Simple
131             #
132              
133             my %opts = ();
134             getopts('c:i:', \%opts);
135              
136             my $cfg = Config::Simple->new($opts{'c'});
137              
138             my $fl = Net::Flickr::Geo::MultiMaps->new($cfg);
139             $fl->log()->add(Log::Dispatch::Screen->new('name' => 'scr', min_level => 'debug'));
140              
141             my $map = $fl->mk_pinwin_map_for_photo($opts{'i'});
142             $fl->log()->info("wrote map to $map->[0]->[0]");
143              
144             =head1 IMPORTANT
145              
146             B
147             compatible.>
148              
149             Please adjust your code and expectations accordingly. It shouldn't
150             happen again...
151              
152             =cut
153              
154             use File::Temp qw (tempfile);
155              
156             use LWP::UserAgent;
157             use LWP::Simple;
158             use HTTP::Request;
159             use Geo::Coordinates::DecimalDegrees;
160             use Flickr::Upload;
161             use Image::Size;
162             use FileHandle;
163              
164             #
165             # shared, and public
166             #
167              
168             sub init {
169             my $self = shift;
170             my $args = shift;
171              
172             if (! $self->SUPER::init($args)){
173             return undef;
174             }
175              
176             $self->{'__cache'} = {'geo_perms' => {}};
177              
178             return 1;
179             }
180              
181             sub mk_pinwin_map_for_photo {
182             my $self = shift;
183             my $photo_id = shift;
184              
185             my $upload = $self->divine_option('pinwin.upload', 0);
186              
187             #
188              
189             my $res = $self->api_call({'method' => 'flickr.photos.getInfo',
190             'args' => {'photo_id' => $photo_id}});
191              
192             if (! $res){
193             return undef;
194             }
195              
196             my $ph = ($res->findnodes("/rsp/photo"))[0];
197             my $map = $self->mk_pinwin_map($ph);
198              
199             if (! $map){
200             return undef;
201             }
202              
203             my @res = ($map);
204              
205             if ($upload){
206             my $id = $self->upload_map($ph, $map);
207             push @res, $id;
208             }
209              
210             return [ \@res ];
211             }
212              
213             sub mk_pinwin_maps_for_photoset {
214             my $self = shift;
215             my $set_id = shift;
216              
217             my $upload = $self->divine_option('pinwin.upload', 0);
218              
219             #
220              
221             my $photos = $self->collect_photos_for_set($set_id);
222              
223             if (! $photos){
224             return undef;
225             }
226              
227             my @maps = ();
228             my @set = ();
229              
230             foreach my $ph (@$photos){
231              
232             my $map = $self->mk_pinwin_map($ph);
233              
234             if (! $map){
235             next;
236             }
237              
238             my @local_res = ($map);
239              
240             if ($upload){
241             my $id = $self->upload_map($ph, $map);
242              
243             push @local_res, $id;
244             push @set, $id;
245             push @set, $ph->getAttribute("id");
246             }
247              
248             push @maps, \@local_res;
249             }
250              
251             if (($upload) && (scalar(@set))) {
252             $self->api_call({'method' => 'flickr.photosets.editPhotos',
253             'args' => {'photoset_id' => $set_id,
254             'primary_photo_id' => $set[0],
255             'photo_ids' => join(",", @set)}});
256             }
257              
258             return \@maps;
259             }
260              
261             #
262             # shared, and not really public
263             #
264              
265             sub mk_pinwin_map {
266             my $self = shift;
267             my $ph = shift;
268              
269             my $id = $ph->getAttribute("id");
270              
271             #
272              
273             my $thumb_data = $self->fetch_flickr_photo($ph);
274              
275             if (! $thumb_data){
276             $self->log()->error("unable to retrieve photo $id");
277             return undef;
278             }
279              
280             #
281              
282             my $map_data = $self->fetch_map_image($ph, $thumb_data);
283              
284             if (! $map_data) {
285             $self->log()->error("unable to add photo $id to map");
286             return undef;
287             }
288              
289             my $new = $self->modify_map($ph, $map_data, $thumb_data);
290              
291             unlink($map_data->{'path'});
292             unlink($thumb_data->{'path'});
293              
294             return $new;
295             }
296              
297             sub collect_photos_for_set {
298             my $self = shift;
299             my $set_id = shift;
300              
301             my $res = $self->api_call({'method' => 'flickr.photosets.getPhotos',
302             'args' => {'photoset_id' => $set_id,
303             'extras' => 'geo, machine_tags, tags'}});
304              
305             if (! $res){
306             return undef;
307             }
308              
309             my %ihasamapz = ();
310             my @photos = ();
311              
312             my $skip_ids = $self->divine_option("pinwin.skip_photos");
313             my $ensure_tags = $self->divine_option("pinwin.ensure_tags");
314             my $skip_tags = $self->divine_option("pinwin.skip_tags");
315              
316             if (($skip_ids) && (ref($skip_ids) ne 'ARRAY')){
317             $skip_ids = [$skip_ids];
318             }
319              
320             if (($ensure_tags) && (ref($ensure_tags) ne 'ARRAY')){
321             $ensure_tags = [$ensure_tags];
322             }
323              
324             if (($skip_tags) && (ref($skip_tags) ne 'ARRAY')){
325             $skip_tags = [$skip_tags];
326             }
327              
328             foreach my $ph ($res->findnodes("/rsp/photoset/photo")){
329              
330             my $id = $ph->getAttribute("id");
331              
332             if (($skip_ids) && (grep /$id/, @$skip_ids)){
333             $self->log()->info("photo id $id excluded, skipping");
334             next;
335             }
336              
337             #
338              
339             my $mt = $ph->getAttribute("machine_tags");
340              
341             if ($mt =~ /\bflickr\:map\=pinwin\b/){
342              
343             if ($mt =~ /\bflickr\:photo\=(\d+)\b/){
344             $ihasamapz{$1} = $id;
345             }
346            
347             $self->log()->info("photo id $id tagged pinwin, skipping");
348             next;
349             }
350              
351             if (my $mapid = $ihasamapz{$id}){
352             $self->log()->info("photo id $id already has a map $mapid, skipping");
353             next;
354             }
355              
356             if (! $ph->getAttribute("latitude")){
357             $self->log()->info("photo id $id has no geo information, skipping");
358             next;
359             }
360              
361             if ($ensure_tags){
362             my $has_tag = 0;
363             my $tags = $ph->getAttribute("tags");
364              
365             foreach my $t (@$ensure_tags){
366             if ($tags =~ /\b$t\b/){
367             $has_tag = 1;
368             last;
369             }
370             }
371              
372             if (! $has_tag){
373             $self->log()->info("photo id $id does not contain required tags : " . join(";", @$ensure_tags));
374             next;
375             }
376            
377             }
378              
379             if ($skip_tags){
380              
381             my $has_tag = 0;
382             my $tags = $ph->getAttribute("tags");
383              
384             foreach my $t (@$skip_tags){
385             if ($tags =~ /\b$t\b/){
386             $has_tag = 1;
387             last;
388             }
389             }
390              
391             if ($has_tag){
392             $self->log()->info("photo id $id has skippable tags : " . join(";", @$skip_tags));
393             next;
394             }
395             }
396              
397             push @photos, $ph;
398             }
399              
400             return \@photos;
401             }
402              
403             sub flickr_photo_url {
404             my $self = shift;
405             my $ph = shift;
406              
407             my $sz = $self->divine_option("pinwin.photo_size", "Small");
408             my $ext = $self->flickr_photo_extension($sz);
409              
410             my $id = $ph->getAttribute("id");
411             my $fid = $ph->getAttribute("farm");
412             my $sid = $ph->getAttribute("server");;
413             my $secret = $ph->getAttribute("secret");
414              
415             return "http://farm" . $fid . ".static.flickr.com/" . $sid . "/" . $id . "_" . $secret . $ext . ".jpg";
416             }
417              
418             sub flickr_photo_extension {
419             my $self = shift;
420             my $size = shift;
421              
422             my %map = (
423             'square' => '_s',
424             'thumbnail' => '_t',
425             'small' => '_m',
426             'medium' => '',
427             );
428              
429             return $map{ lc($size) };
430             }
431              
432             sub fetch_flickr_photo {
433             my $self = shift;
434             my $ph = shift;
435            
436             my $url = $self->flickr_photo_url($ph);
437             my $path = $self->simple_get($url, $self->mk_tempfile(".jpg"));
438              
439             if (! $path){
440             return undef;
441             }
442              
443             my ($img_w, $img_h) = imgsize($path);
444              
445             my %data = (
446             'url' => $url,
447             'path' => $path,
448             'height' => $img_h,
449             'width' => $img_w,
450             );
451              
452             return \%data;
453             }
454              
455             sub mk_tempfile {
456             my $self = shift;
457             my $ext = shift;
458              
459             my ($fh, $filename) = tempfile(UNLINK => 0, SUFFIX => $ext);
460             return $filename;
461             }
462              
463             sub simple_get {
464             my $self = shift;
465             my $remote = shift;
466             my $local = shift;
467              
468             $local ||= $self->mk_tempfile();
469              
470             $self->log()->info("fetch remote file : $remote");
471             $self->log()->info("store local file : $local");
472              
473             if (! getstore($remote, $local)){
474             $self->log()->error("failed to retrieve remote URL ($remote)");
475             return 0;
476             }
477              
478             return $local;
479             }
480              
481             sub get_geo_property {
482             my $self = shift;
483             my $ph = shift;
484             my $prop = shift;
485              
486             my $value = $ph->getAttribute($prop);
487              
488             if (! $value){
489             $value = $ph->findvalue("location/\@" . $prop);
490             }
491              
492             return $value;
493             }
494              
495             sub pretty_print_latlong {
496             my $self = shift;
497             my $lat = shift;
498             my $lon = shift;
499              
500             my @lat_dms = decimal2dms($lat);
501             my $ns = ($lat_dms[3]) ? "N" : "S";
502              
503             my $str_lat = sprintf(qq(%d° %d' %d" $ns), @lat_dms);
504              
505             my @lon_dms = decimal2dms($lon);
506             my $ew = ($lon_dms[3]) ? "E" : "W";
507              
508             my $str_lon = sprintf(qq(%d° %d' %d" $ew), @lon_dms);
509             return "$str_lat, $str_lon";
510             }
511              
512             sub upload_map {
513             my $self = shift;
514             my $ph = shift;
515             my $map = shift;
516              
517             #
518              
519             my $lat = $self->get_geo_property($ph, "latitude");
520             my $lon = $self->get_geo_property($ph, "longitude");
521             my $title = $self->pretty_print_latlong($lat, $lon);
522              
523             my $tag = "flickr:photo=" . $ph->getAttribute("id");
524              
525             my %args = (
526             'photo' => $map,
527             'title' => $title,
528             'tags' => "$tag flickr:map=pinwin",
529             );
530              
531             return $self->upload_image(\%args);
532             }
533              
534             sub upload_image {
535             my $self = shift;
536             my $args = shift;
537              
538             $args->{'is_public'} = ($self->divine_option("pinwin.upload_public"), 0);
539             $args->{'is_friend'} = ($self->divine_option("pinwin.upload_friend"), 0);
540             $args->{'is_family'} = ($self->divine_option("pinwin.upload_family"), 0);
541             $args->{'auth_token'} = $self->divine_option("flickr.auth_token");
542              
543             $self->log()->info("upload to flickr : $args->{photo}");
544              
545             my $id = undef;
546              
547             eval {
548              
549             my $ua = Flickr::Upload->new({'key'=> $self->divine_option("flickr.api_key"),
550             'secret' => $self->divine_option("flickr.api_secret")});
551              
552             $id = $ua->upload(%$args);
553             };
554              
555             if (! $id) {
556             $self->log()->error("failed to upload photo, $@");
557             return;
558             }
559              
560             # This is not a love song...
561              
562             $self->api_call({'method' => 'flickr.photos.setContentType',
563             'args' => {'photo_id' => $id, 'content_type' => 3}});
564              
565              
566             $self->log()->info("photo uploaded with ID $id");
567             return $id;
568             }
569              
570             sub divine_option {
571             my $self = shift;
572             my $opt = shift;
573             my $default = shift;
574              
575             my $v = $self->{'cfg'}->param($opt);
576              
577             if (defined($v)){
578             $self->log()->info("divine by config : $opt => $v");
579             return $v;
580             }
581              
582             $self->log()->info("divine by default : $opt => $default");
583             return $default;
584             }
585              
586             sub load_pinwin {
587             my $self = shift;
588              
589             if (! $self->{'__pinwin'}){
590              
591             use Net::Flickr::Geo::Pinwin;
592             my $pinwin = Net::Flickr::Geo::Pinwin->mk_flickr_pinwin();
593              
594             $self->log()->info("created temporary pinwin : $pinwin");
595             $self->{'__pinwin'} = $pinwin;
596             }
597              
598             return $self->{'__pinwin'};
599             }
600              
601             sub modify_map {
602             my $self = shift;
603             my $ph = shift;
604              
605             my $map_data = shift;
606             my $thumb_data = shift;
607             my $out = shift;
608              
609             $out ||= $self->mk_tempfile(".png");
610              
611             my $pinwin = $self->load_pinwin();
612              
613             #
614              
615             my $truecolour = 1;
616              
617             use GD;
618             my $pw = GD::Image->newFromPng($pinwin, $truecolour);
619             $pw->alphaBlending(0);
620             $pw->saveAlpha(1);
621            
622             my $th = GD::Image->newFromJpeg($thumb_data->{'path'});
623             $th->alphaBlending(0);
624             $th->saveAlpha(1);
625              
626             # place the thumb on the pinwin
627              
628             $pw->copy($th, 11, 10, 0, 0, 75, 75);
629              
630             #
631             # so so wrong but for the life of me I can't figure
632             # out why the transparency for the pinwin is not
633             # preserved below...
634             #
635              
636             my $pin = $self->mk_tempfile(".png");
637             my $fh = FileHandle->new(">$pin");
638              
639             binmode($fh);
640             $fh->print($pw->png(0));
641             $fh->close();
642              
643             my $h = $self->divine_option("pinwin.map_height", 1024);
644             my $w = $self->divine_option("pinwin.map_width", 1024);
645              
646             my $x = int($w / 2) - 28;
647             my $y = int($h / 2) - 134;
648            
649             my $cmd = "composite -quality 100 -geometry +" . $x . "+" . $y . " $pin $map_data->{'path'} $out";
650              
651             if (system($cmd)){
652             $self->log()->error("failed to modify map ($cmd) , $!");
653             return;
654             }
655              
656             return $out;
657              
658             #
659             # we now return you to your regular programming which doesn't work...
660             #
661              
662             # place the pinwin on the map
663              
664             my $map = GD::Image->newFromPng($map_data->{'path'}, $truecolour);
665             $map->alphaBlending(0);
666             $map->saveAlpha(1);
667              
668             # fix me!
669             # why doesn't the alpha in $pw get preserved
670              
671             $map->copy($pw, $x, $y, 0, 0, 159, 146);
672            
673             #
674              
675             $self->log()->info("save as $out");
676             my $f = FileHandle->new(">$out");
677              
678             binmode($fh);
679             $f->print($map->png(0));
680             $f->close();
681              
682             return $out;
683             }
684              
685             sub ensure_geo_perms {
686             my $self = shift;
687             my $photo_id = shift;
688             my $require = shift;
689              
690             if ($require eq "all"){
691             return 1;
692             }
693              
694             #
695              
696             my $key = "$photo_id-$require";
697              
698             if (exists($self->{'__cache'}->{'geo_perms'}->{$key})){
699             return $self->{'__cache'}->{'geo_perms'}->{$key};
700             }
701              
702             #
703              
704             $self->log()->info("ensure geo permissions : $require");
705              
706             my $perms = $self->api_call({'method' => 'flickr.photos.geo.getPerms', 'args' => {'photo_id' => $photo_id}});
707             my $ok = 0;
708              
709             if ($require eq "public"){
710             $ok = $perms->findvalue("/rsp/perms/\@ispublic");
711             }
712              
713             elsif ($require eq "contact"){
714             $ok = $perms->findvalue("/rsp/perms/\@iscontact");
715             }
716              
717             elsif ($require eq "friend"){
718             $ok = $perms->findvalue("/rsp/perms/\@isfriend");
719             }
720              
721             elsif ($require eq "family"){
722             $ok = $perms->findvalue("/rsp/perms/\@isfamily");
723             }
724              
725             elsif ($require eq "friend or family"){
726              
727             if ($perms->findvalue("/rsp/perms/\@isfriend")){
728             $ok = 1;
729             }
730              
731             else {
732             $ok = $perms->findvalue("/rsp/perms/\@isfamily");
733             }
734             }
735              
736             else { }
737              
738             $self->{'__cache'}->{'geo_perms'}->{$key} = $ok;
739             return $ok;
740             }
741              
742             sub DESTROY {
743             my $self = shift;
744              
745             if (-f $self->{'__pinwin'}){
746             $self->log()->info("removing temporary pinwin : " . $self->{'__pinwin'});
747             # unlink($self->{'__pinwin'});
748             }
749            
750             $self->SUPER::DESTROY();
751             }
752              
753             =head1 VERSION
754              
755             0.72
756              
757             =head1 DATE
758              
759             $Date: 2008/08/03 17:08:39 $
760              
761             =head1 AUTHOR
762              
763             Aaron Straup Cope Eascope@cpan.orgE
764              
765             =head1 NOTES
766              
767             All uploads to Flickr are marked with a content-type of "other".
768              
769             =head1 SEE ALSO
770              
771             L
772              
773             L
774              
775             L
776              
777             L
778              
779             L
780              
781             L
782              
783             L
784              
785             L
786              
787             L
788              
789             L
790              
791             L
792              
793             =head1 BUGS
794              
795             Sure, why not.
796              
797             Please report all bugs via L
798              
799             =head1 LICENSE
800              
801             Copyright (c) 2007-2008 Aaron Straup Cope. All Rights Reserved.
802              
803             This is free software. You may redistribute it and/or
804             modify it under the same terms as Perl itself.
805              
806             =cut
807              
808             return 1;