File Coverage

blib/lib/Net/Flickr/RDF.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 1     1   1367 use strict;
  1         2  
  1         58  
2              
3             # $Id: RDF.pm,v 1.90 2010/12/19 19:06:12 asc Exp $
4             # -*-perl-*-
5              
6             package Net::Flickr::RDF;
7 1     1   6 use base qw (Net::Flickr::API);
  1         2  
  1         1085  
8              
9             $Net::Flickr::RDF::VERSION = '2.2';
10              
11             =head1 NAME
12              
13             Net::Flickr::RDF - a.k.a RDF::Describes::Flickr
14              
15             =head1 SYNOPSIS
16              
17             use Net::Flickr::RDF;
18             use Config::Simple;
19             use IO::AtomicFile;
20              
21             my $cfg = Config::Simple->new("/path/to/my.cfg");
22             my $rdf = Net::Flickr::RDF->new($cfg);
23              
24             my $fh = IO::AtomicFile->open("/foo/bar.rdf","w");
25              
26             $rdf->describe_photo({photo_id => 123,
27             secret => 567,
28             fh => \*$fh});
29              
30             $fh->close();
31              
32             =head1 DESCRIPTION
33              
34             Describe Flickr photos as RDF.
35              
36             This package inherits from I.
37              
38             =head1 OPTIONS
39              
40             Options are passed to Net::Flickr::Backup using a Config::Simple object or
41             a valid Config::Simple config file. Options are grouped by "block".
42              
43             =head2 flickr
44              
45             =over 4
46              
47             =item * B
48              
49             String. I
50              
51             A valid Flickr API key.
52              
53             =item * B
54              
55             String. I
56              
57             A valid Flickr Auth API secret key.
58              
59             =item * B
60              
61             String. I
62              
63             A valid Flickr Auth API token.
64              
65             =back
66              
67             =head2 rdf
68              
69             =over 4
70              
71             =item * B
72              
73             Boolean.
74              
75             If true and a photo has geodata (latitude, longitude) associated with it, then
76             the geonames.org database will be queried for a corresponding match. Data will be
77             added as properties of the photo's geo:Point description. For example :
78              
79            
80             -122.025151
81             16
82             visbility
83             37.417839
84             public
85            
86            
87              
88            
89             PPLX
90             US
91             CA
92             California
93             Santa Clara
94             2
95            
96              
97             Default is false.
98              
99             =back
100              
101             =cut
102              
103             use utf8;
104             use English;
105              
106             use Date::Format;
107             use Date::Parse;
108             use Digest::MD5 qw (md5_hex);
109             use RDF::Simple::Serialiser;
110             use Readonly;
111             use URI::Escape;
112              
113             Readonly::Hash my %DEFAULT_NS => (
114             "a" => "http://www.w3.org/2000/10/annotation-ns",
115             "atom" => "http://www.w3.org/2005/Atom/",
116             "acl" => "http://www.w3.org/2001/02/acls#",
117             "cc" => "http://web.resource.org/cc/",
118             "dc" => "http://purl.org/dc/elements/1.1/",
119             "dcterms" => "http://purl.org/dc/terms/",
120             "exif" => "http://nwalsh.com/rdf/exif#",
121             "exifi" => "http://nwalsh.com/rdf/exif-intrinsic#",
122             "flickr" => "x-urn:flickr:",
123             "foaf" => "http://xmlns.com/foaf/0.1/",
124             "geo" => "http://www.w3.org/2003/01/geo/wgs84_pos#",
125             "geoname" => "http://www.geonames.org/onto#",
126             "i" => "http://www.w3.org/2004/02/image-regions#",
127             "mt" => "x-urn:flickr:machinetag:",
128             "places" => "http://www.flickr.com/places/",
129             "rdf" => "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
130             "rdfs" => "http://www.w3.org/2000/01/rdf-schema#",
131             "skos" => "http://www.w3.org/2004/02/skos/core#",
132             "ymaps" => "urn:yahoo:maps",
133             );
134              
135             Readonly::Hash my %RDFMAP => (
136             'EXIF' => {
137             '41483' => 'flashEnergy',
138             '33437' => 'fNumber',
139             '37378' => 'apertureValue',
140             '37520' => 'subsecTime',
141             '34855' => 'isoSpeedRatings',
142             '41484' => 'spatialFrequencyResponse',
143             '37380' => 'exposureBiasValue',
144             '532' => 'referenceBlackWhite',
145             '40964' => 'relatedSoundFile',
146             '36868' => 'dateTimeDigitized',
147             '34850' => 'exposureProgram',
148             '272' => 'model',
149             '259' => 'compression',
150             '37381' => 'maxApertureValue',
151             '37396' => 'subjectArea',
152             '277' => 'samplesPerPixel',
153             '37121' => 'componentsConfiguration',
154             '37377' => 'shutterSpeedValue',
155             '37384' => 'lightSource',
156             '41989' => 'focalLengthIn35mmFilm',
157             '41495' => 'sensingMethod',
158             '37386' => 'focalLength',
159             '529' => 'yCbCrCoefficients',
160             '41488' => 'focalPlaneResolutionUnit',
161             '37379' => 'brightnessValue',
162             '41730' => 'cfaPattern',
163             '41486' => 'focalPlaneXResolution',
164             '37510' => 'userComment',
165             '41992' => 'contrast',
166             '41729' => 'sceneType',
167             '41990' => 'sceneCaptureType',
168             '41487' => 'focalPlaneYResolution',
169             '37122' => 'compressedBitsPerPixel',
170             '37385' => 'flash',
171             '258' => 'bitsPerSample',
172             '530' => 'yCbCrSubSampling',
173             '41993' => 'saturation',
174             '284' => 'planarConfiguration',
175             '41996' => 'subjectDistanceRange',
176             '41987' => 'whiteBalance',
177             '274' => 'orientation',
178             '40962' => 'pixelXDimension',
179             '306' => 'dateTime',
180             '41493' => 'exposureIndex',
181             '40963' => 'pixelYDimension',
182             '41994' => 'sharpness',
183             '315' => 'artist',
184             '1' => 'interoperabilityIndex',
185             '37383' => 'meteringMode',
186             '37522' => 'subsecTimeDigitized',
187             '42016' => 'imageUniqueId',
188             '41728' => 'fileSource',
189             '41991' => 'gainControl',
190             '283' => 'yResolution',
191             '37500' => 'makerNote',
192             '273' => 'stripOffsets',
193             '305' => 'software',
194             '531' => 'yCbCrPositioning',
195             '319' => 'primaryChromaticities',
196             '278' => 'rowsPerStrip',
197             '36864' => 'version',
198             '34856' => 'oecf',
199             '271' => 'make',
200             '282' => 'xResolution',
201             '37521' => 'subsecTimeOriginal',
202             '262' => 'photometricInterpretation',
203             '40961' => 'colorSpace',
204             '33434' => 'exposureTime',
205             '33432' => 'copyright',
206             '41995' => 'deviceSettingDescription',
207             '318' => 'whitePoint',
208             '257' => 'imageLength',
209             '41988' => 'digitalZoomRatio',
210             '301' => 'transferFunction',
211             '41985' => 'customRendered',
212             '37382' => 'subjectDistance',
213             '34852' => 'spectralSensitivity',
214             '41492' => 'subjectLocation',
215             '279' => 'stripByteCounts',
216             '296' => 'resolutionUnit',
217             '41986' => 'exposureMode',
218             '40960' => 'flashpixVersion',
219             '256' => 'imageWidth',
220             '36867' => 'dateTimeOriginal',
221             '270' => 'imageDescription',
222             },
223            
224             GPS => {
225             '11' => 'dop',
226             '21' => 'destLongitudeRef',
227             '7' => 'timeStamp',
228             '26' => 'destDistance',
229             '17' => 'imgDirection',
230             '2' => 'latitude',
231             '22' => 'destLongitude',
232             '1' => 'latitudeRef',
233             '18' => 'mapDatum',
234             '0' => 'versionId',
235             '30' => 'differential',
236             '23' => 'destBearingRef',
237             '16' => 'imgDirectionRef',
238             '13' => 'speed',
239             '29' => 'dateStamp',
240             '27' => 'processingMethod',
241             '25' => 'destDistanceRef',
242             '6' => 'altitude',
243             '28' => 'arealInformation',
244             '3' => 'longitudeRef',
245             '9' => 'status',
246             '12' => 'speedRef',
247             '20' => 'destLatitude',
248             '14' => 'trackRef',
249             '15' => 'track',
250             '8' => 'satellites',
251             '4' => 'longitude',
252             '24' => 'destBearing',
253             '19' => 'destLatitudeRef',
254             '10' => 'measureMode',
255             '5' => 'altitudeRef',
256             },
257            
258             # TIFF => {},
259             );
260              
261              
262             Readonly::Hash my %CC_PERMITS => ("by-nc" => {"permits" => ["Reproduction",
263             "Distribution",
264             "DerivativeWorks"],
265             "requires" => ["Notice",
266             "Attribution"],
267             "prohibits" => ["CommercialUse"]},
268             "by-nc-nd" => {"permits" => ["Reproduction",
269             "Distribution"],
270             "requires" => ["Notice",
271             "Attribution"],
272             "prohibits" => ["CommercialUse"]},
273             "by-nc-sa" => {"permits" => ["Reproduction",
274             "Distribution",
275             "DerivativeWorks"],
276             "requires" => ["Notice",
277             "Attribution",
278             "ShareAlike"],
279             "prohibits" => ["CommercialUse"]},
280             "by-nc" => {"permits" => ["Reproduction",
281             "Distribution",
282             "DerivativeWorks"],
283             "requires" => ["Notice",
284             "Attribution",
285             "ShareAlike"],
286             "prohibits" => ["CommercialUse"]},
287             "by-nd" => {"permits" => ["Reproduction",
288             "Distribution"],
289             "requires" => ["Notice",
290             "Attribution",
291             "ShareAlike"]},
292             "by-sa" => {"permits" => ["Reproduction",
293             "Distribution",
294             "DerivativeWorks"],
295             "requires" => ["Notice",
296             "Attribution",
297             "ShareAlike"]},
298             "by" => {"permits" => ["Reproduction",
299             "Distribution",
300             "DerivativeWorks"],
301             "requires" => ["Notice",
302             "Attribution"]},
303             );
304              
305             Readonly::Scalar my $MACHINETAGS_URL => "http://www.machinetags.org/wiki/";
306              
307             Readonly::Scalar my $GEONAMES_URL => "http://www.geonames.org/";
308             Readonly::Scalar my $GEONAMES_URL_WS => "http://ws.geonames.org/";
309             Readonly::Scalar my $GEONAMES_URL_RDF => $GEONAMES_URL_WS . "rdf";
310             Readonly::Scalar my $GEONAMES_URL_CO => $GEONAMES_URL . "countries/";
311              
312             Readonly::Scalar my $GEONAMES_API_FINDNEARBY => $GEONAMES_URL_WS . "findNearbyPlaceName";
313             Readonly::Scalar my $GEONAMES_API_GTOPO30 => $GEONAMES_URL_WS . "gtopo30";
314            
315             Readonly::Scalar my $FLICKR_URL => "http://www.flickr.com/";
316             Readonly::Scalar my $FLICKR_URL_PHOTOS => $FLICKR_URL . "photos/";
317             Readonly::Scalar my $FLICKR_URL_PLACES => $FLICKR_URL . "places/";
318             Readonly::Scalar my $FLICKR_URL_GEO => $FLICKR_URL . "geo/";
319             Readonly::Scalar my $FLICKR_URL_PEOPLE => $FLICKR_URL . "people/";
320             Readonly::Scalar my $FLICKR_URL_TAGS => $FLICKR_URL . "photos/tags/";
321             Readonly::Scalar my $FLICKR_URL_GROUPS => $FLICKR_URL . "groups/";
322              
323             Readonly::Scalar my $LICENSE_ALLRIGHTS => "All rights reserved.";
324              
325             Readonly::Array my @FLICKR_GEOPLACES => ("locality", "county", "region", "country");
326              
327             =head1 PACKAGE METHODS
328              
329             =cut
330              
331             =head2 __PACKAGE__->new($cfg)
332              
333             Where B<$cfg> is either a valid I object or the path
334             to a file that can be parsed by I.
335              
336             Returns a I object.
337              
338             =cut
339              
340             sub new {
341             my $pkg = shift;
342             my $args = shift;
343              
344             my $self = $pkg->SUPER::new($args);
345              
346             if (! $self){
347             return undef;
348             }
349              
350             my %ns = %DEFAULT_NS;
351              
352             $self->{'__ns'} = \%ns;
353              
354             $self->{'__places_urls'} = {};
355             return bless $self, $pkg;
356             }
357             # Defined in Net::Flickr::API
358              
359             =head1 PACKAGE METHODS YOU MAY CARE ABOUT
360              
361             =cut
362              
363             =head2 __PACKAGE__->build_photo_uri(\%data)
364              
365             Returns a URL as a string.
366              
367             =cut
368              
369             sub build_photo_uri {
370             my $self = shift;
371             my $data = shift;
372              
373             return sprintf("%s%s/%s", $FLICKR_URL_PHOTOS, $data->{user_id}, $data->{photo_id});
374             }
375              
376             =head2 __PACKAGE__->build_geo_uri(\%data)
377              
378             =cut
379              
380             sub build_geo_uri {
381             my $self = shift;
382             my $data = shift;
383              
384             my $photo_url = $self->build_photo_uri($data);
385             return $photo_url . "#location";
386             }
387              
388             =head2 __PACKAGE__->build_user_tag_uri(\@data)
389              
390             Returns a URL as a string.
391              
392             =cut
393              
394             sub build_user_tag_uri {
395             my $pkg = shift;
396             my $data = shift;
397            
398             my $clean = $data->[0];
399             my $raw = $data->[1];
400             my $author = $data->[2];
401            
402             return $FLICKR_URL_PHOTOS."$author/tags/$clean";
403             }
404              
405             =head2 __PACKAGE__->build_global_tag_uri(\@data)
406              
407             Returns a URL as a string.
408              
409             =cut
410              
411             sub build_global_tag_uri {
412             my $pkg = shift;
413             my $data = shift;
414            
415             my $clean = $data->[0];
416             return $FLICKR_URL_TAGS.$clean;
417             }
418              
419             =head2 __PACKAGE__->build_machinetag_uri(\@data)
420              
421             Returns a URL as a string.
422              
423             =cut
424              
425             sub build_machinetag_uri {
426             my $pkg = shift;
427             my $tag = shift;
428            
429             my @parts = $pkg->explode_machinetag($tag, "escape");
430             return $MACHINETAGS_URL . $parts[0] . "#" . $parts[1];
431             }
432              
433             sub explode_machinetag {
434             my $pkg = shift;
435             my $mt = shift;
436             my $escape = shift;
437              
438             my ($ns, $rest) = split(":", $mt);
439             my ($pred, $value) = split("=", $rest);
440              
441             if ($escape) {
442             $value = uri_escape($value);
443             }
444              
445             return ($ns, $pred, $value);
446             }
447              
448             =head2 __PACKAGE__->build_user_uri($user_id)
449              
450             Returns a URL as a string.
451              
452             =cut
453              
454             sub build_user_uri {
455             my $pkg = shift;
456             my $user_id = shift;
457            
458             return $FLICKR_URL_PEOPLE.$user_id;
459             }
460              
461             =head2 __PACKAGE__->build_group_uri($group_id)
462              
463             Returns a URL as a string.
464              
465             =cut
466              
467             sub build_group_uri {
468             my $self = shift;
469             my $group_id = shift;
470            
471             return $FLICKR_URL_GROUPS.$group_id;
472             }
473              
474             =head2 __PACKAGE__->build_grouppool_uri($group_id)
475              
476             Returns a URL as a string.
477              
478             =cut
479              
480             sub build_grouppool_uri {
481             my $self = shift;
482             my $group_id = shift;
483            
484             return $self->build_group_uri($group_id)."/pool";
485             }
486              
487             =head2 __PACKAGE__->build_photoset_uri(\%set_data)
488              
489             Returns a URL as a string.
490              
491             =cut
492              
493             sub build_photoset_uri {
494             my $pkg = shift;
495             my $set_data = shift;
496            
497             return sprintf("%s%s/sets/%s", $FLICKR_URL_PHOTOS, $set_data->{user_id}, $set_data->{id});
498             }
499              
500             =head2 __PACKAGE__->prune_triples(\@triples)
501              
502             Removes duplicate triples from I<@triples>. Returns an array
503             reference.
504              
505             =cut
506              
507             sub prune_triples {
508             my $self = shift;
509             my $triples = shift;
510            
511             my %seen = ();
512             my @pruned = ();
513            
514             foreach my $spo (@$triples) {
515            
516             my $key = md5_hex(join("", @$spo));
517            
518             if (exists($seen{$key})) {
519             next;
520             }
521            
522             $seen{$key} = 1;
523             push @pruned, $spo;
524             }
525            
526             return \@pruned;
527             }
528              
529             =head1 OBJECT METHODS YOU SHOULD CARE ABOUT
530              
531             =cut
532              
533             =head2 $obj->describe_photo(\%args)
534              
535             Valid arguments are :
536              
537             =over 4
538              
539             =item * B
540              
541             Int. I
542              
543             =item * B
544              
545             String.
546              
547             =item * B
548              
549             File-handle.
550              
551             Default is STDOUT.
552              
553             =back
554              
555             Returns true or false.
556              
557             =cut
558              
559             sub describe_photo {
560             my $self = shift;
561             my $args = shift;
562            
563             my $fh = ($args->{fh}) ? $args->{fh} : \*STDOUT;
564            
565             my $data = $self->collect_photo_data($args->{photo_id}, $args->{secret});
566            
567             if (! $data) {
568             return 0;
569             }
570            
571             my $triples = $self->make_photo_triples($data);
572            
573             if (! $triples) {
574             return 0;
575             }
576            
577             $self->serialise_triples($triples, $fh);
578             return 1;
579             }
580              
581             =head1 OBJECT METHODS YOU MAY CARE ABOUT
582              
583             =cut
584              
585             =head2 $obj->collect_photo_data($photo_id,$secret)
586              
587             Returns a hash ref of the meta data associated with a photo.
588              
589             If any errors are unencounter an error is recorded via the B
590             method and the method returns undef.
591              
592             =cut
593              
594             sub collect_photo_data {
595             my $self = shift;
596             my $id = shift;
597             my $secret = shift;
598            
599             my %data = ();
600            
601             my $info = $self->api_call({method=>"flickr.photos.getInfo",
602             args=>{photo_id => $id,
603             secret => $secret}});
604            
605             if (! $info) {
606             $self->log()->error("unable to collect info for photo");
607             return undef;
608             }
609            
610             my $img = ($info->findnodes("/rsp/photo"))[0];
611            
612             if (! $img) {
613             $self->log()->error("unable to locate photo for info");
614             return undef;
615             }
616            
617             my $owner = ($img->findnodes("owner"))[0];
618             my $dates = ($img->findnodes("dates"))[0];
619            
620             %data = (photo_id => $id,
621             user_id => $owner->getAttribute("nsid"),
622             title => $img->find("title")->string_value(),
623             taken => $dates->getAttribute("taken"),
624             posted => $dates->getAttribute("posted"),
625             lastmod => $dates->getAttribute("lastupdate"));
626            
627             my $owner_id = $data{user_id};
628             $data{users}->{$owner_id} = $self->collect_user_data($owner_id);
629            
630             #
631            
632             my $sizes = $self->api_call({method => "flickr.photos.getSizes",
633             args => {photo_id => $id}});
634            
635             if (! $sizes) {
636             return undef;
637             }
638            
639             foreach my $sz ($sizes->findnodes("/rsp/sizes/size")) {
640            
641             my $label = $sz->getAttribute("label");
642            
643             $data{files}->{$label} = {height => $sz->getAttribute("height"),
644             width => $sz->getAttribute("width"),
645             label => $label,
646             uri => $sz->getAttribute("source")};
647             }
648            
649             #
650            
651             my $cc = $self->collect_cc_data();
652             $data{license} = $cc->{$img->getAttribute("license")};
653            
654             #
655            
656             $self->log()->info("fetch exif for photo ID $id (with secret $secret)");
657              
658             my $exif = $self->api_call({method=>"flickr.photos.getExif",
659             args=>{photo_id => $id,
660             secret => $secret}});
661              
662             if ($exif) {
663             foreach my $tag ($exif->findnodes("/rsp/photo/exif[\@tagspace='EXIF']"), $exif->findnodes("/rsp/photo/exif[\@tagspace='ExifIFD']")) {
664            
665             my $facet = 'EXIF'; # $tag->getAttribute("tagspace");
666             my $tag_dec = $tag->getAttribute("tag");
667             my $value = $tag->findvalue("clean") || $tag->findvalue("raw");
668             $data{exif}->{$facet}->{$tag_dec} = $value;
669             }
670             }
671            
672             #
673            
674             $data{desc} = $img->find("description")->string_value();
675            
676             #
677             # Privacy
678             #
679              
680             my $vis = ($img->findnodes("visibility"))[0];
681            
682             if ($vis->getAttribute("ispublic")) {
683             $data{visibility} = "public";
684             }
685            
686             elsif (($vis->getAttribute("isfamily")) && ($vis->getAttribute("isfriend"))) {
687             $data{visibility} = "family;friend";
688             }
689            
690             elsif ($vis->getAttribute("isfamily")) {
691             $data{visibility} = "family";
692             }
693            
694             elsif ($vis->getAttribute("is_friend")) {
695             $data{visibility} = "friend";
696             }
697            
698             else {
699             $data{visibility} = "private";
700             }
701            
702             #
703             # Tags
704             #
705              
706             foreach my $tag ($img->findnodes("tags/tag")) {
707            
708             my $id = $tag->getAttribute("id");
709             my $raw = $tag->getAttribute("raw");
710             my $clean = $tag->string_value();
711             my $author = $tag->getAttribute("author");
712             my $is_mt = $tag->getAttribute("machine_tag");
713              
714             $data{tags}->{$id} = [$clean, $raw, $author, $is_mt];
715             $data{tag_map}->{$clean}->{$raw} ++;
716            
717             $data{users}->{$author} = $self->collect_user_data($author);
718             }
719            
720             #
721             # Notes
722             #
723              
724             foreach my $note ($img->findnodes("notes/note")) {
725            
726             $data{notes} ||= [];
727            
728             my %note = map {
729             $_ => $note->getAttribute($_);
730             } qw (x y h w id author authorname);
731            
732             $note{body} = $note->string_value();
733             push @{$data{notes}}, \%note;
734            
735             $data{users}->{$note{author}} = $self->collect_user_data($note{author});
736             }
737              
738             #
739             # People in photo
740             #
741              
742             my $folks = $self->api_call({
743             method => "flickr.photos.people.getList",
744             args => { photo_id => $id }
745             });
746              
747             if ($folks) {
748             foreach my $person ($folks->findnodes("/rsp/people/person")) {
749             $data{people} ||= [];
750              
751             my %person = map {
752             $_ => $person->getAttribute($_)
753             } qw (x y h w username realname);
754            
755             $person{id} = $person->getAttribute('nsid');
756             $person{author} = $person->getAttribute('added_by');
757              
758             push @{$data{people}}, \%person;
759              
760             $data{users}->{$person{id}} = $self->collect_user_data($person{id});
761             $data{users}->{$person{author}} = $self->collect_user_data($person{author});
762             }
763             }
764              
765             #
766             # Geo
767             #
768              
769             my $loc = ($img->findnodes("location"))[0];
770              
771             if ($loc){
772             my $perms = ($img->findnodes("geoperms"))[0];
773             my $vis = "private";
774              
775             if ($perms->getAttribute("ispublic") == 1) {
776             $vis = "public";
777             }
778              
779             elsif ($perms->getAttribute("iscontact") == 1) {
780             $vis = "contact";
781             }
782              
783             elsif (($perms->getAttribute("isfamily") == 1) && ($perms->getAttribute("isfriend") == 1)) {
784             $vis = "family;friend";
785             }
786              
787             elsif ($perms->getAttribute("isfriend") == 1) {
788             $vis = "friend";
789             }
790              
791              
792             elsif ($perms->getAttribute("isfamily") == 1) {
793             $vis = "family";
794             }
795            
796             else {}
797              
798             my %geo = (
799             'lat' => $loc->getAttribute("latitude"),
800             'long' => $loc->getAttribute("longitude"),
801             'acc' => $loc->getAttribute("accuracy"),
802             'place_id' => $loc->getAttribute("place_id"),
803             'visibility' => $vis,
804             );
805              
806             #
807             # Flickr geo/labels
808             #
809              
810             foreach my $label (@FLICKR_GEOPLACES) {
811              
812             if (my $place = $loc->findvalue($label)){
813             $geo{$label} = $place;
814             }
815             }
816              
817             #
818             #
819             #
820              
821             $data{geo} = \%geo;
822             }
823              
824             #
825             # Context (groups, sets, etc.)
826             #
827              
828             my $ctx = $self->api_call({method => "flickr.photos.getAllContexts",
829             args => {photo_id=>$id}});
830            
831             if (! $ctx) {
832             $self->log()->warning("unable to retrieve context for photo $id");
833             }
834            
835             else {
836             $data{groups} = [];
837             $data{sets} = [];
838            
839             foreach my $set ($ctx->findnodes("/rsp/set")) {
840             my $set_id = $set->getAttribute("id");
841            
842             push @{$data{sets}},$self->collect_photoset_data($set_id);
843             }
844            
845             foreach my $group ($ctx->findnodes("/rsp/pool")) {
846             my $group_id = $group->getAttribute("id");
847            
848             push @{$data{groups}},$self->collect_group_data($group_id);
849             }
850             }
851            
852             #
853             # Comments
854             #
855            
856             $data{comments} = $self->collect_comment_data(\%data);
857            
858             #
859             # Happy Happy
860             #
861            
862             return \%data;
863             }
864              
865             =head2 $obj->collect_group_data($group_id)
866              
867             Returns a hash ref of the meta data associated with a group.
868              
869             If any errors are unencounter an error is recorded via the B
870             method and the method returns undef.
871              
872             =cut
873              
874             sub collect_group_data {
875             my $self = shift;
876             my $group_id = shift;
877            
878             if (exists($self->{'__groups'}->{$group_id})) {
879             return $self->{'__groups'}->{$group_id};
880             }
881            
882             my %data = ();
883            
884             my $group = $self->api_call({method => "flickr.groups.getInfo",
885             args => {group_id=> $group_id}});
886            
887             if ($group) {
888             foreach my $prop ("name", "description") {
889             $data{$prop} = $group->findvalue("/rsp/group/$prop");
890             }
891            
892             $data{id} = $group_id;
893             }
894            
895             $self->{'__groups'}->{$group_id} = \%data;
896             return $self->{'__groups'}->{$group_id};
897             }
898              
899             =head2 $obj->collect_user_data($user_id)
900              
901             Returns a hash ref of the meta data associated with a user.
902              
903             If any errors are unencounter an error is recorded via the B
904             method and the method returns undef.
905              
906             =cut
907              
908             sub collect_user_data {
909             my $self = shift;
910             my $user_id = shift;
911            
912             if (exists($self->{'__users'}->{$user_id})) {
913             return $self->{'__users'}->{$user_id};
914             }
915            
916             my %data = ();
917            
918             my $user = $self->api_call({method => "flickr.people.getInfo",
919             args => {user_id=> $user_id}});
920            
921             if ($user) {
922            
923             $data{user_id} = $user_id;
924            
925             foreach my $prop ("username", "realname", "mbox_sha1sum") {
926             $data{$prop} = $user->findvalue("/rsp/person/$prop");
927             }
928             }
929            
930             $self->{'__users'}->{$user_id} = \%data;
931             return $self->{'__users'}->{$user_id};
932             }
933              
934             sub collect_user_data_by_screenname {
935             my $self = shift;
936             my $name = shift;
937            
938             if (exists($self->{'__screen'}->{$name})) {
939             return $self->collect_user_data($self->{'__screen'}->{$name});
940             }
941            
942             #
943            
944             my $user = $self->api_call({method => "flickr.people.findByUsername",
945             args => {username=> $name}});
946              
947             if (! $user) {
948             return undef;
949             }
950              
951             my $user_id = $user->findvalue("/rsp/user/\@id");
952              
953             #
954              
955             $self->{'__screen'}->{$name} = $user_id;
956             return $self->collect_user_data($user_id);
957             }
958              
959             =head2 $obj->collect_photoset_data($photoset_id)
960              
961             Returns a hash ref of the meta data associated with a photoset.
962              
963             If any errors are unencounter an error is recorded via the B
964             method and the method returns undef.
965              
966             =cut
967              
968             sub collect_photoset_data {
969             my $self = shift;
970             my $set_id = shift;
971            
972             if (exists($self->{'__sets'}->{$set_id})) {
973             return $self->{'__sets'}->{$set_id};
974             }
975            
976             my %data = ();
977            
978             my $set = $self->api_call({method => "flickr.photosets.getInfo",
979             args => {photoset_id=> $set_id}});
980              
981             if ($set) {
982             foreach my $prop ("title", "description") {
983             $data{$prop} = $set->findvalue("/rsp/photoset/$prop");
984             }
985            
986             $data{user_id} = $set->findvalue("/rsp/photoset/\@owner");
987             $data{id} = $set_id;
988             }
989            
990             $self->{'__sets'}->{$set_id} = \%data;
991             return $self->{'__sets'}->{$set_id};
992             }
993              
994             =head2 $obj->collect_cc_data()
995              
996             Returns a hash ref of the Creative Commons licenses used by Flickr.
997              
998             If any errors are unencounter an error is recorded via the B
999             method and the method returns undef.
1000              
1001             =cut
1002              
1003             sub collect_cc_data {
1004             my $self = shift;
1005            
1006             if (exists($self->{'__cc'})) {
1007             return $self->{'__cc'};
1008             }
1009            
1010             my %cc = ();
1011            
1012             my $licenses = $self->api_call({"method" => "flickr.photos.licenses.getInfo"});
1013            
1014             if (! $licenses) {
1015             return undef;
1016             }
1017            
1018             foreach my $l ($licenses->findnodes("/rsp/licenses/license")) {
1019             $cc{ $l->getAttribute("id") } = $l->getAttribute("url");
1020             }
1021            
1022             $self->{'__cc'} = \%cc;
1023             return $self->{'__cc'};
1024             }
1025              
1026             =head2 $obj->collect_comment_data()
1027              
1028             Returns a hash ref of comments made about a photo.
1029              
1030             =cut
1031              
1032             sub collect_comment_data {
1033             my $self = shift;
1034             my $data = shift;
1035              
1036             my %comments = ();
1037              
1038             my $list = $self->api_call({method => "flickr.photos.comments.getList",
1039             args => {photo_id => $data->{photo_id}}});
1040              
1041             if ($list) {
1042              
1043             foreach my $c ($list->findnodes("/rsp/comments/comment")) {
1044              
1045             my $permalink = $c->getAttribute("permalink");
1046             my $user = $self->collect_user_data($c->getAttribute("author"));
1047              
1048             if (! exists($data->{users}->{$user->{user_id}})) {
1049             $data->{users}->{$user->{user_id}} = $user;
1050             }
1051              
1052             my %cdata = (user_id => $user->{user_id},
1053             content => $c->string_value(),
1054             date => $c->getAttribute("datecreate"),
1055             id => $c->getAttribute("id"));
1056              
1057             $comments{$permalink} = \%cdata;
1058             }
1059             }
1060              
1061             return \%comments;
1062             }
1063              
1064             =head2 $obj->make_photo_triples(\%data)
1065              
1066             Returns an array ref (or alist in a wantarray context) of array refs
1067             of the meta data associated with a photo (I<%data>).
1068              
1069             =cut
1070              
1071             sub make_photo_triples {
1072             my $self = shift;
1073             my $data = shift;
1074              
1075             my $photo = sprintf("%s%s/%s", $FLICKR_URL_PHOTOS, $data->{user_id}, $data->{photo_id});
1076             my @triples = ();
1077              
1078             #
1079              
1080             my $now = time();
1081             my $rdf_version = $Net::Flickr::RDF::VERSION;
1082             my $doc_version = $rdf_version . ":" . $now;
1083              
1084             my $rdf = "#";
1085            
1086             push @triples, [$rdf, $self->uri_shortform("dc", "creator"), "http://search.cpan.org/dist/Net-Flickr-RDF-$rdf_version"];
1087             push @triples, [$rdf, $self->uri_shortform("dc", "created"), time2str("%Y-%m-%dT%H:%M:%S%z", $now)];
1088             push @triples, [$rdf, $self->uri_shortform("dcterms", "hasVersion"), $doc_version];
1089             push @triples, [$rdf, $self->uri_shortform("a", "annotates"), $photo];
1090              
1091             #
1092            
1093             my $flickr_photo = $DEFAULT_NS{flickr}."photo";
1094             my $flickr_photoset = $DEFAULT_NS{flickr}."photoset";
1095             my $flickr_user = $DEFAULT_NS{flickr}."user";
1096             my $flickr_tag = $DEFAULT_NS{flickr}."tag";
1097             my $flickr_machinetag = $DEFAULT_NS{flickr}."machinetag";
1098             my $flickr_note = $DEFAULT_NS{flickr}."note";
1099             my $flickr_comment = $DEFAULT_NS{flickr}."comment";
1100             my $flickr_persontag = $DEFAULT_NS{flickr}."persontag";
1101             my $flickr_group = $DEFAULT_NS{flickr}."group";
1102             my $flickr_grouppool = $DEFAULT_NS{flickr}."grouppool";
1103            
1104             my $dc_still_image = $DEFAULT_NS{dcterms}."StillImage";
1105             my $foaf_person = $DEFAULT_NS{foaf}."Person";
1106             my $skos_concept = $DEFAULT_NS{skos}."Concept";
1107             my $anno_annotation = $DEFAULT_NS{a}."Annotation";
1108            
1109             if (scalar(keys %{$data->{users}})) {
1110             push @triples, [$flickr_user, $self->uri_shortform("rdfs","subClassOf"), $foaf_person];
1111             }
1112            
1113             if (exists($data->{tags})) {
1114             push @triples, [$flickr_tag, $self->uri_shortform("rdfs","subClassOf"), $skos_concept];
1115             push @triples, [$flickr_machinetag, $self->uri_shortform("rdfs","subClassOf"), $skos_concept];
1116             }
1117            
1118             if (exists($data->{notes})) {
1119             push @triples, [$flickr_note, $self->uri_shortform("rdfs","subClassOf"), $anno_annotation];
1120             }
1121              
1122             if (exists($data->{comments})) {
1123             push @triples, [$flickr_comment, $self->uri_shortform("rdfs","subClassOf"), $anno_annotation];
1124             }
1125              
1126             if (exists($data->{people})) {
1127             push @triples, [$flickr_persontag, $self->uri_shortform("rdfs","subClassOf"), $anno_annotation];
1128             }
1129              
1130             #
1131             # static files
1132             #
1133              
1134             foreach my $label (keys %{$data->{files}}) {
1135            
1136             # TO DO - make me a method
1137            
1138             my $uri = $data->{files}->{$label}->{uri};
1139            
1140             push @triples, [$uri, $self->uri_shortform("exifi", "width"), $data->{files}->{$label}->{width}];
1141             push @triples, [$uri, $self->uri_shortform("exifi", "height"), $data->{files}->{$label}->{height}];
1142             push @triples, [$uri, $self->uri_shortform("dcterms", "relation"), $label];
1143             push @triples, [$uri, $self->uri_shortform("rdfs", "seeAlso"), $photo];
1144             push @triples, [$uri, $self->uri_shortform("rdfs", "seeAlso"), "$photo#exif"];
1145             push @triples, [$uri, $self->uri_shortform("rdf", "type"), $self->uri_shortform("dcterms","StillImage")];
1146            
1147             if ($label ne "Original") {
1148             push @triples, [$uri,$self->uri_shortform("dcterms","isVersionOf"),$data->{files}->{'Original'}->{uri}];
1149             }
1150            
1151             }
1152            
1153             #
1154             # flickr data
1155             #
1156              
1157             push @triples, [$photo, $self->uri_shortform("rdf", "type"), $self->uri_shortform("flickr","photo")];
1158             push @triples, [$photo, $self->uri_shortform("dc", "creator"), sprintf("%s%s",$FLICKR_URL_PEOPLE,$data->{user_id})];
1159             push @triples, [$photo, $self->uri_shortform("dc", "title"), $data->{title}];
1160             push @triples, [$photo, $self->uri_shortform("dc", "description"), $data->{desc}];
1161             push @triples, [$photo, $self->uri_shortform("dc", "created"), time2str("%Y-%m-%dT%H:%M:%S%z",str2time($data->{taken}))];
1162             push @triples, [$photo, $self->uri_shortform("dc", "dateSubmitted"), time2str("%Y-%m-%dT%H:%M:%S%z",$data->{posted})];
1163             push @triples, [$photo, $self->uri_shortform("acl", "accessor"), $data->{visibility}];
1164             push @triples, [$photo, $self->uri_shortform("acl", "access"), "visbility"];
1165            
1166             #
1167             # geo data
1168             #
1169            
1170             if ((! exists($data->{geo})) && (my $geodata = $self->geodata_from_tags($data))) {
1171             $data->{geo} = $geodata;
1172             }
1173              
1174             if (exists($data->{geo})) {
1175              
1176             $self->make_geo_triples($data);
1177              
1178             if ($self->{'cfg'}->param("rdf.query_geonames")) {
1179             push @triples, ($self->make_geonames_triples($data));
1180             }
1181            
1182             push @triples, ($self->make_flickr_places_triples($data));
1183              
1184             my $geo_url = $self->build_geo_uri($data);
1185             push @triples, [$photo, $self->uri_shortform("geo","Point"), $geo_url];
1186             }
1187              
1188             #
1189             # licensing
1190             #
1191            
1192             if ($data->{license}) {
1193             push @triples, [$photo,$self->uri_shortform("cc","license"),$data->{license}];
1194             push @triples, ($self->make_cc_triples($data->{license}));
1195             }
1196            
1197             else {
1198             push @triples, [$photo,$self->uri_shortform("dc","rights"),$LICENSE_ALLRIGHTS];
1199             }
1200            
1201             #
1202             # tags
1203             #
1204            
1205             if (exists($data->{tags})) {
1206            
1207             foreach my $id (keys %{$data->{tags}}) {
1208            
1209             my $tag_data = $data->{tags}->{$id};
1210             my $is_mt = $tag_data->[3];
1211              
1212             my $tag_uri = $self->build_user_tag_uri($tag_data);
1213             push @triples, [$photo,$self->uri_shortform("dc", "subject"), $tag_uri];
1214            
1215             push @triples, ($self->make_tag_triples($tag_data));
1216              
1217              
1218             # FIX ME : DO NOT LEAVE HERE...
1219              
1220             if ($is_mt) {
1221              
1222             my $raw = $tag_data->[1];
1223             my ($ns, $pred, $value) = $self->explode_machinetag($raw);
1224             my $mt_url = $self->build_machinetag_uri($raw);
1225              
1226             my $prefix = $self->add_namespace($ns, $mt_url);
1227             push @triples, [$photo, $self->uri_shortform($prefix, $pred), $value];
1228              
1229             push @triples, [ $mt_url, $self->uri_shortform("mt", "namespace"), $ns];
1230             push @triples, [ $mt_url, $self->uri_shortform("mt", "predicate"), $pred];
1231             push @triples, [ $mt_url, $self->uri_shortform("dc", "isReferencedBy"), $photo];
1232            
1233             push @triples, [ $mt_url, $self->uri_shortform("rdf", "type"), $self->uri_shortform("flickr", "machinetag")];
1234             }
1235             }
1236             }
1237            
1238             #
1239             # notes/annotations
1240             #
1241            
1242             if (exists($data->{notes})) {
1243            
1244             foreach my $n (@{$data->{notes}}) {
1245            
1246             # TO DO : how to build/make note triples without $photo
1247            
1248             my $note = "$photo#note-$n->{id}";
1249             my $author_uri = $self->build_user_uri($n->{author});
1250            
1251             push @triples, [$photo,$self->uri_shortform("a","hasAnnotation"),$note];
1252            
1253             push @triples, [$note,$self->uri_shortform("a","annotates"),$photo];
1254             push @triples, [$note,$self->uri_shortform("a","author"),$author_uri];
1255             push @triples, [$note,$self->uri_shortform("a","body"),$n->{body}];
1256             push @triples, [$note,$self->uri_shortform("i","boundingBox"), "$n->{x} $n->{y} $n->{w} $n->{h}"];
1257             push @triples, [$note,$self->uri_shortform("i","regionDepicts"),$data->{files}->{'Medium'}->{'uri'}];
1258             push @triples, [$note,$self->uri_shortform("rdf","type"),$self->uri_shortform("flickr","note")];
1259             }
1260             }
1261            
1262             #
1263             # people annotations
1264             #
1265            
1266             if (exists($data->{people})) {
1267            
1268             foreach my $p (@{$data->{people}}) {
1269            
1270             # TO DO : how to build/make note triples without $photo
1271            
1272             my $person = "$photo#person-$p->{id}";
1273             my $person_uri = $self->build_user_uri($p->{id});
1274             my $author_uri = $self->build_user_uri($p->{author});
1275            
1276             push @triples, [$photo,$self->uri_shortform("a","hasAnnotation"),$person];
1277            
1278             push @triples, [$person,$self->uri_shortform("a","annotates"),$photo];
1279             push @triples, [$person,$self->uri_shortform("a","author"),$author_uri];
1280             push @triples, [$person,$self->uri_shortform("a","personDepicted"),$person_uri];
1281             push @triples, [$person,$self->uri_shortform("a","body"),"$p->{realname} ($p->{username})"];
1282             push @triples, [$person,$self->uri_shortform("i","boundingBox"), "$p->{x} $p->{y} $p->{w} $p->{h}"] if defined $p->{x};
1283             # XXX: Is this correct? New photo page vs. old photo page show different photos...
1284             push @triples, [$person,$self->uri_shortform("i","regionDepicts"),$data->{files}->{'Medium'}->{'uri'}] if defined $p->{x};
1285             push @triples, [$person,$self->uri_shortform("rdf","type"),$self->uri_shortform("flickr","person")];
1286             }
1287             }
1288            
1289             #
1290             # users (authors)
1291             #
1292            
1293             foreach my $user (keys %{$data->{users}}) {
1294             push @triples, ($self->make_user_triples($data->{users}->{$user}));
1295             }
1296            
1297             #
1298             # comments
1299             #
1300            
1301             if (exists($data->{comments})) {
1302             push @triples, ($self->make_comment_triples($data));
1303            
1304             foreach my $uri (keys %{$data->{comments}}) {
1305             push @triples, [$photo, $self->uri_shortform("a", "hasAnnotation"), $uri]
1306             }
1307             }
1308            
1309             #
1310             # EXIF data
1311             #
1312            
1313             # dateTimeOriginal : EXIF/36867
1314             # dateTimeDigitzed : EXIF/36868
1315              
1316             foreach my $facet (keys %{$data->{exif}}) {
1317            
1318             if (! exists($RDFMAP{$facet})) {
1319             next;
1320             }
1321            
1322             my $photo_o_url = $data->{files}->{Original}->{uri};
1323              
1324             foreach my $tag (keys %{$data->{exif}->{$facet}}) {
1325            
1326             my $label = $tag =~ /^\d+$/ ? $RDFMAP{$facet}->{$tag} : $tag;
1327            
1328             if (! $label) {
1329             $self->log()->warning("can't find any label for $facet tag : $tag");
1330             next;
1331             }
1332              
1333             my $value = $data->{exif}->{$facet}->{$tag};
1334              
1335             # dateTimeOriginal/Digitized
1336              
1337             if ( $facet eq "EXIF" and $label =~ /^dateTime(?:Original|Digitized)$/ ) {
1338              
1339             my $time = str2time($value);
1340             $value = time2str("%Y-%m-%dT%H:%M:%S%Z", $time);
1341             }
1342            
1343             push @triples, ["$photo#exif", $self->uri_shortform("exif","$label"), "$value"];
1344             }
1345             }
1346            
1347             #
1348             # sets
1349             #
1350            
1351             foreach my $set (@{$data->{sets}}) {
1352            
1353             my $set_uri = $self->build_photoset_uri($set);
1354             push @triples, [$photo, $self->uri_shortform("dcterms","isPartOf"),$set_uri];
1355            
1356             push @triples, ($self->make_photoset_triples($set));
1357             }
1358            
1359             #
1360             # groups
1361             #
1362            
1363             foreach my $group (@{$data->{groups}}) {
1364            
1365             my $pool_uri = $self->build_grouppool_uri($group->{id});
1366             push @triples, [$photo, $self->uri_shortform("dcterms","isPartOf"),$pool_uri];
1367            
1368             push @triples, ($self->make_group_triples($group));
1369             push @triples, ($self->make_grouppool_triples($group));
1370             }
1371            
1372             return (wantarray) ? @triples : \@triples;
1373             }
1374              
1375             =head2 $obj->make_user_triples(\%user_data)
1376              
1377             Returns an array ref (or list in a wantarray context) of array
1378             refs of the meta data associated with a user (I<%user_data>).
1379              
1380             =cut
1381              
1382             sub make_user_triples {
1383             my $self = shift;
1384             my $user_data = shift;
1385            
1386             my $uri = $self->build_user_uri($user_data->{user_id});
1387            
1388             my @triples = ();
1389            
1390             push @triples, [$uri,$self->uri_shortform("foaf","nick"),$user_data->{username}];
1391             push @triples, [$uri,$self->uri_shortform("foaf","name"),$user_data->{realname}];
1392             push @triples, [$uri,$self->uri_shortform("foaf","mbox_sha1sum"),$user_data->{mbox_sha1sum}];
1393             push @triples, [$uri,$self->uri_shortform("rdf","type"),$self->uri_shortform("flickr","user")];
1394            
1395             return (wantarray) ? @triples : \@triples;
1396             }
1397              
1398             =head2 $obj->make_tag_triples(\@tag_data)
1399              
1400             Returns an array ref (or list in a wantarray context) of array
1401             refs of the meta data associated with a tag (I<@tag_data>).
1402              
1403             =cut
1404              
1405             sub make_tag_triples {
1406             my $self = shift;
1407             my $tag_data = shift;
1408            
1409             my $clean = $tag_data->[0];
1410             my $raw = $tag_data->[1];
1411             my $author = $tag_data->[2];
1412             my $is_mt = $tag_data->[3];
1413            
1414             my $author_uri = $self->build_user_uri($author);
1415             my $tag_uri = $self->build_user_tag_uri($tag_data);
1416             my $clean_uri = $self->build_global_tag_uri($tag_data);
1417            
1418             #
1419            
1420             my @triples = ();
1421            
1422             push @triples, [$tag_uri,$self->uri_shortform("rdf","type"),$self->uri_shortform("flickr","tag")];
1423             push @triples, [$tag_uri,$self->uri_shortform("skos","prefLabel"),$raw];
1424            
1425             if ($raw ne $clean) {
1426             push @triples, [$tag_uri,$self->uri_shortform("skos","altLabel"),$clean];
1427             }
1428            
1429             push @triples, [$tag_uri,$self->uri_shortform("dc","creator"),$author_uri];
1430             push @triples, [$tag_uri,$self->uri_shortform("skos","broader"),$FLICKR_URL_TAGS.$clean];
1431            
1432             #
1433            
1434             push @triples, [$FLICKR_URL_TAGS.$clean, $self->uri_shortform("rdf","type"), $self->uri_shortform("flickr","tag")];
1435             push @triples, [$FLICKR_URL_TAGS.$clean, $self->uri_shortform("skos", "prefLabel"), $clean];
1436            
1437             if ($is_mt) {
1438              
1439             my @mt_parts = $self->explode_machinetag($raw);
1440             my $mt_uri = $self->build_machinetag_uri($raw);
1441             push @triples, [$FLICKR_URL_TAGS.$clean, $self->uri_shortform("skos", "broader"), $mt_uri];
1442             push @triples, [$FLICKR_URL_TAGS.$clean, $self->uri_shortform("skos", "altLabel"), $mt_parts[2]];
1443             push @triples, [$tag_uri, $self->uri_shortform("skos", "altLabel"), $mt_parts[2]];
1444             }
1445              
1446             return (wantarray) ? @triples : \@triples;
1447             }
1448              
1449             =head2 $pkg->make_photoset_triples(\%set_data)
1450              
1451             Returns an array ref (or list in a wantarray context) of array
1452             refs of the meta data associated with a photoset (I<%set_data>).
1453              
1454             =cut
1455              
1456             sub make_photoset_triples {
1457             my $self = shift;
1458             my $set_data = shift;
1459            
1460             my @triples = ();
1461            
1462             my $set_uri = $self->build_photoset_uri($set_data);
1463             my $creator_uri = $self->build_user_uri($set_data->{user_id});
1464            
1465             push @triples, [$set_uri, $self->uri_shortform("dc", "title"), $set_data->{title}];
1466             push @triples, [$set_uri, $self->uri_shortform("dc", "description"), $set_data->{description}];
1467             push @triples, [$set_uri, $self->uri_shortform("dc", "creator"), $creator_uri];
1468             push @triples, [$set_uri, $self->uri_shortform("rdf", "type"), $self->uri_shortform("flickr","photoset")];
1469            
1470             return (wantarray) ? @triples : \@triples;
1471             }
1472              
1473             =head2 $obj->make_geo_triples(\%geo_data)
1474              
1475             =cut
1476              
1477             sub make_geo_triples {
1478             my $self = shift;
1479             my $data = shift;
1480              
1481             my @triples = ();
1482              
1483             my $point = $self->uri_shortform("geo","Point");
1484             my $geo_url = $self->build_geo_uri($data);
1485              
1486             push @triples, [$geo_url, $self->uri_shortform("geo", "lat"), $data->{geo}->{lat}];
1487             push @triples, [$geo_url, $self->uri_shortform("geo", "long"), $data->{geo}->{long}];
1488              
1489             if (exists($data->{geo}->{acc})) {
1490             push @triples, [$geo_url, $self->uri_shortform("flickr", "accuracy"), $data->{geo}->{acc}];
1491             push @triples, [$geo_url, $self->uri_shortform("acl", "accessor"), $data->{geo}->{visibility}];
1492             push @triples, [$geo_url, $self->uri_shortform("acl", "access"), "visbility"];
1493             push @triples, [$geo_url, $self->uri_shortform("rdf","type"), $point];
1494             }
1495            
1496             # What the hell is this one for, again?
1497             # push @triples, [$photo, $self->uri_shortform("dc","coverage"), $data->{coverage}];
1498              
1499             return (wantarray) ? @triples : \@triples;
1500             }
1501              
1502             sub build_flickr_places_url {
1503             my $self = shift;
1504             my $data = shift;
1505              
1506             my $url = $self->places_url($data->{'geo'}->{'place_id'});
1507             $url =~ s/^\///;
1508              
1509             return $FLICKR_URL_PLACES . $url;
1510             }
1511              
1512             sub build_flickr_places_id_url {
1513             my $self = shift;
1514             my $data = shift;
1515              
1516             return $FLICKR_URL_PLACES . $data->{'geo'}->{'place_id'};
1517             }
1518              
1519             sub build_flickr_places_url_oldskool {
1520             my $self = shift;
1521             my $data = shift;
1522              
1523             my @urn = ();
1524              
1525             foreach my $label (reverse(@FLICKR_GEOPLACES)){
1526             if ($data->{'geo'}->{$label}){
1527             push @urn, uri_escape($data->{'geo'}->{$label});
1528             }
1529             }
1530              
1531             if (! @urn) {
1532             return ();
1533             }
1534              
1535             return $FLICKR_URL_GEO . join("/", @urn);
1536             }
1537              
1538             =head2 $obj->make_flickr_places_triples(\%geo_data)
1539              
1540             =cut
1541              
1542             sub make_flickr_places_triples {
1543             my $self = shift;
1544             my $data = shift;
1545            
1546             my @triples = ();
1547              
1548             #
1549              
1550             my $photo_url = $self->build_photo_uri($data);
1551             my $geo_url = $self->build_geo_uri($data);
1552              
1553             my $places_url = $self->build_flickr_places_url($data);
1554             my $places_id_url = $self->build_flickr_places_id_url($data);
1555             my $oldplace_url = $self->build_flickr_places_url_oldskool($data);
1556              
1557             push @triples, [$geo_url, $self->uri_shortform("skos", "broader"), $places_url];
1558             push @triples, [$places_url, $self->uri_shortform("dc", "isReferencedBy"), $photo_url];
1559              
1560             #
1561              
1562             foreach my $label (@FLICKR_GEOPLACES) {
1563             if (my $place = $data->{'geo'}->{$label}){
1564             push @triples, [$places_url, $self->uri_shortform("places", $label), $place];
1565             }
1566             }
1567            
1568             push @triples, [$places_url, $self->uri_shortform("places", "id"), $data->{'geo'}->{'place_id'}];
1569             push @triples, [$places_url, $self->uri_shortform("rdf", "type"), $self->uri_shortform("flickr", "place")];
1570              
1571             #
1572             # For backwards compatibility
1573             #
1574              
1575             push @triples, [$places_id_url, $self->uri_shortform("rdfs", "seeAlso"), $places_url];
1576             push @triples, [$oldplace_url, $self->uri_shortform("rdfs", "seeAlso"), $places_url];
1577              
1578             #
1579              
1580             return (wantarray) ? @triples : \@triples;
1581             }
1582              
1583             =head2 $obj->make_geonames_triples(\%geo_data)
1584              
1585             =cut
1586              
1587             sub make_geonames_triples {
1588             my $self = shift;
1589             my $data = shift;
1590              
1591             my @triples = ();
1592             my $query = sprintf("%s?style=FULL&lat=%s&lng=%s", $GEONAMES_API_FINDNEARBY, $data->{'geo'}->{'lat'}, $data->{'geo'}->{'long'});
1593              
1594             $self->log()->debug($query);
1595              
1596             my $res = undef;
1597             my $xml = undef;
1598              
1599             eval {
1600             my $req = HTTP::Request->new(GET => $query);
1601             $res = $self->{'api'}->request($req);
1602             };
1603              
1604             if ($@) {
1605             $self->log()->error("Failed to ping geonames.org, $@");
1606             return (wantarray) ? @triples : \@triples;
1607             }
1608              
1609             if ($res->is_success()) {
1610             $xml = $self->_parse_results_xml($res);
1611             }
1612              
1613             if ($xml) {
1614              
1615             my $photo_url = $self->build_photo_uri($data);
1616             my $geo_url = $self->build_geo_uri($data);
1617              
1618             my $geoname_url = sprintf("%s?geonameId=%s", $GEONAMES_URL_RDF, $xml->findvalue("/geonames/geoname/geonameId"));
1619              
1620             #
1621             # basic reverse geocoding
1622             #
1623              
1624             push @triples, [$geo_url, $self->uri_shortform("skos", "broader"), $geoname_url];
1625             push @triples, [$geoname_url, $self->uri_shortform("dc", "isReferencedBy"), $photo_url];
1626              
1627             push @triples, [$geoname_url, $self->uri_shortform("geoname", "city"), $xml->findvalue("normalize-space(/geonames/geoname/adminName2)")];
1628             push @triples, [$geoname_url, $self->uri_shortform("geoname", "region"), $xml->findvalue("normalize-space(/geonames/geoname/adminName1)")];
1629             push @triples, [$geoname_url, $self->uri_shortform("geoname", "regionCode"), $xml->findvalue("normalize-space(/geonames/geoname/adminCode1)")];
1630             push @triples, [$geoname_url, $self->uri_shortform("geoname", "countryCode"), $xml->findvalue("normalize-space(/geonames/geoname/countryCode)")];
1631             push @triples, [$geoname_url, $self->uri_shortform("geoname", "featureCode"), $xml->findvalue("normalize-space(/geonames/geoname/fcode)")];
1632             push @triples, [$geoname_url, $self->uri_shortform("rdf", "type"), $self->uri_shortform("geoname", "Feature")];
1633              
1634             #
1635             # gtopo30
1636             #
1637            
1638             my $query = sprintf("%s?lat=%s&lng=%s", $GEONAMES_API_GTOPO30, $data->{'geo'}->{'lat'}, $data->{'geo'}->{'long'});
1639             $self->log()->debug($query);
1640            
1641             my $req = HTTP::Request->new(GET => $query);
1642             my $res = $self->{'api'}->request($req);
1643            
1644             if ($res->is_success()) {
1645             $res->content() =~ /(\d+)/m;
1646             my $topo = $1;
1647            
1648             if (($topo) && ($topo != -9999)) {
1649             push @triples, [$geoname_url, $self->uri_shortform("geoname", "gtopo30"), $topo];
1650             }
1651             }
1652              
1653             #
1654             #
1655             #
1656             }
1657              
1658             #
1659             # happy happy
1660             #
1661              
1662             return (wantarray) ? @triples : \@triples;
1663             }
1664              
1665             =head2 $obj->make_group_triples(\%group_data)
1666              
1667             Returns an array ref (or list in a wantarray context) of array
1668             refs of the meta data associated with a group (I<%group_data>).
1669              
1670             =cut
1671              
1672             sub make_group_triples {
1673             my $self = shift;
1674             my $group_data = shift;
1675            
1676             my @triples = ();
1677            
1678             my $group_uri = $self->build_group_uri($group_data->{id});
1679            
1680             push @triples, [$group_uri,$self->uri_shortform("dc","title"),$group_data->{name}];
1681             push @triples, [$group_uri,$self->uri_shortform("dc","description"),$group_data->{description}];
1682             push @triples, [$group_uri,$self->uri_shortform("rdf","type"),$self->uri_shortform("flickr","group")];
1683            
1684             return (wantarray) ? @triples : \@triples;
1685             }
1686              
1687             =head2 $obj->make_grouppool_triples(\%group_data)
1688              
1689             Returns an array ref (or list in a wantarray context) of array
1690             refs of the meta data associated with a group pool (I<%group_data>).
1691              
1692             =cut
1693              
1694             sub make_grouppool_triples {
1695             my $self = shift;
1696             my $group_data = shift;
1697            
1698             my @triples = ();
1699            
1700             my $group_uri = $self->build_group_uri($group_data->{id});
1701             my $pool_uri = $self->build_grouppool_uri($group_data->{id});
1702            
1703             push @triples, [$pool_uri, $self->uri_shortform("dcterms","isPartOf"),$group_uri];
1704             push @triples, [$pool_uri, $self->uri_shortform("rdf","type"),$self->uri_shortform("flickr","grouppool")];
1705            
1706             return (wantarray) ? @triples : \@triples;
1707             }
1708              
1709             =head2 $obj->make_cc_triples($url)
1710              
1711             Returns an array ref (or list in a wantarray context) of array
1712             refs of the meta data associated with a Creative Commons license
1713             (I<$url>).
1714              
1715             =cut
1716              
1717             sub make_cc_triples {
1718             my $self = shift;
1719             my $license = shift;
1720            
1721             my @triples = ();
1722            
1723             $license =~ m!http://creativecommons.org/licenses/(.*)/\d\.\d/?$!;
1724             my $key = $1;
1725            
1726             #
1727            
1728             if (exists($CC_PERMITS{$key})) {
1729            
1730             foreach my $type (keys %{$CC_PERMITS{$key}}) {
1731             foreach my $perm (@{$CC_PERMITS{$key}->{$type}}) {
1732             push @triples, [$license, $self->uri_shortform("cc",$type),$DEFAULT_NS{cc}.$perm];
1733             }
1734             }
1735            
1736             push @triples, [$license, $self->uri_shortform("rdf","type"),$self->uri_shortform("cc","License")];
1737             }
1738            
1739             return (wantarray) ? @triples : \@triples;
1740             }
1741              
1742             =head2 $obj->make_comment_triples(\%data)
1743              
1744             Returns an array ref (or alist in a wantarray context) of array refs
1745             of the meta data associated with a photo comment (I<%data>).
1746              
1747             =cut
1748              
1749             sub make_comment_triples {
1750             my $self = shift;
1751             my $data = shift;
1752              
1753             my $photo_uri = $self->build_photo_uri($data);
1754             my @triples = ();
1755              
1756             foreach my $uri_comment (keys %{$data->{comments}}) {
1757              
1758             my $comment = $data->{comments}->{$uri_comment};
1759             my $identifier = $comment->{id};
1760              
1761             my $w3cdtf = time2str("%Y-%m-%dT%H:%M:%S", $comment->{date});
1762             my $uri_author = $self->build_user_uri($comment->{user_id});
1763              
1764             #
1765             # Use atom:content instead of a:body ?
1766             #
1767              
1768             push @triples, [$uri_comment, $self->uri_shortform("dc", "creator"), $uri_author];
1769             push @triples, [$uri_comment, $self->uri_shortform("a", "body"), $data->{comments}->{$uri_comment}->{content}];
1770             push @triples, [$uri_comment, $self->uri_shortform("rdf", "type"), $self->uri_shortform("flickr","comment")];
1771             push @triples, [$uri_comment, $self->uri_shortform("dc", "created"), $w3cdtf];
1772             push @triples, [$uri_comment, $self->uri_shortform("dc", "identifier"), $identifier];
1773             push @triples, [$uri_comment, $self->uri_shortform("a", "annotates"), $photo_uri];
1774             }
1775              
1776             return (wantarray) ? @triples : \@triples;
1777             }
1778              
1779             =head2 $obj->geodata_from_tags(\%data)
1780              
1781             Try to parse out geolocative data from a collection of tag data.
1782              
1783             Returns a hash ref (containing 'lat' and 'long' keys) on success
1784             or undef if there were no matches.
1785              
1786             =cut
1787              
1788             sub geodata_from_tags {
1789             my $self = shift;
1790             my $data = shift;
1791              
1792             my %geo = ();
1793              
1794             foreach my $id (keys %{$data->{tags}}) {
1795             my $tag_data = $data->{tags}->{$id};
1796             my $raw_tag = $tag_data->[1];
1797              
1798             if ($raw_tag =~ m!^geo:(lat|long)=(.*)$!) {
1799             $geo{$1} = $2;
1800             }
1801              
1802             if (($geo{lat}) && ($geo{long})) {
1803             return \%geo;
1804             }
1805             }
1806              
1807             return undef;
1808             }
1809              
1810             =head2 $obj->namespaces()
1811              
1812             Returns a hash ref of the prefixes and namespaces used by I
1813              
1814             The default key/value pairs are :
1815              
1816             =over 4
1817              
1818             =item B
1819              
1820             http://www.w3.org/2000/10/annotation-ns
1821              
1822             =item B
1823              
1824             http://www.w3.org/2001/02/acls#
1825              
1826             =item B
1827              
1828             http://www.w3.org/2005/Atom/
1829              
1830             =item B
1831              
1832             http://web.resource.org/cc/
1833              
1834             =item B
1835              
1836             http://purl.org/dc/elements/1.1/
1837              
1838             =item B
1839              
1840             http://purl.org/dc/terms/
1841              
1842             =item B
1843              
1844             http://nwalsh.com/rdf/exif#
1845              
1846             =item B
1847              
1848             http://nwalsh.com/rdf/exif-intrinsic#
1849              
1850             =item B
1851              
1852             x-urn:flickr:
1853              
1854             =item B
1855              
1856             http://xmlns.com/foaf/0.1/#
1857              
1858             =item B
1859              
1860             http://www.w3.org/2003/01/geo/wgs84_pos#
1861              
1862             =item B
1863              
1864             http://www.geonames.org/onto#
1865              
1866             =item B
1867              
1868             http://www.w3.org/2004/02/image-regions#
1869              
1870             =item B
1871              
1872             http://www.w3.org/1999/02/22-rdf-syntax-ns#
1873              
1874             =item B
1875              
1876             http://www.w3.org/2000/01/rdf-schema#
1877              
1878             =item B
1879              
1880             http://www.w3.org/2004/02/skos/core#
1881              
1882             =item B
1883              
1884             urn:yahoo:maps
1885              
1886             =back
1887              
1888             =cut
1889              
1890             sub namespaces {
1891             my $self = shift;
1892             return (wantarray) ? %{$self->{'__ns'}} : $self->{'__ns'};
1893             }
1894              
1895             =head2 $obj->add_namespace($prefix, $namespace)
1896              
1897             Add a prefix and namespace URI to the list of known namespaces.
1898              
1899             This method returns a string containing the prefix you should use for
1900             the namespace URI passed. If the prefix passed to the method is already
1901             reserved for another namespace the method will return the following
1902             string:
1903              
1904             "nfr_" + I
1905              
1906             =cut
1907              
1908             sub add_namespace {
1909             my $self = shift;
1910             my $prefix = shift;
1911             my $url = shift;
1912              
1913             my $def = $self->{'__ns'}->{$prefix};
1914              
1915             if (($def) && ($def eq $url)){
1916             return $prefix;
1917             }
1918              
1919             if ($def){
1920              
1921             my $new_prefix = "nfr_" . $prefix;
1922             $self->log()->info("prefix $prefix already defined for NS $def; redefined as $new_prefix");
1923              
1924             $prefix = $new_prefix;
1925             }
1926              
1927             $self->{'__ns'}->{$prefix} = $url;
1928             return $prefix;
1929             }
1930              
1931             =head2 $obj->namespace_prefix($uri)
1932              
1933             Return the namespace prefix for I<$uri>
1934              
1935             =cut
1936              
1937             sub namespace_prefix {
1938             my $self = shift;
1939             my $uri = shift;
1940            
1941             my $ns = $self->namespaces();
1942            
1943             foreach my $prefix (keys %$ns) {
1944             if ($ns->{$prefix} eq $uri) {
1945             return $prefix;
1946             }
1947             }
1948            
1949             return undef;
1950             }
1951              
1952             =head2 $obj->uri_shortform($prefix,$name)
1953              
1954             Returns a string in the form of I:I. The property is
1955             the value of $name. The prefix passed may or may be the same as the prefix
1956             returned depending on whether or not the user has defined or redefined their
1957             own list of namespaces.
1958              
1959             Unless this package is subclassed the prefix passed to the method is assumed to
1960             be one of prefixes in the B list of namespaces.
1961              
1962             =cut
1963              
1964             sub uri_shortform {
1965             my $self = shift;
1966             my $prefix = shift;
1967             my $name = shift;
1968            
1969             my $ns = $self->namespaces();
1970             my $uri = $ns->{$prefix};
1971            
1972             if (! $uri) {
1973             $self->log()->error("unable to determine URI for prefix : '$prefix'");
1974             return undef;
1975             }
1976            
1977             my $user_prefix = $self->namespace_prefix($uri);
1978             return join(":",$user_prefix,$name);
1979             }
1980              
1981             =head2 $obj->api_call(\%args)
1982              
1983             Valid args are :
1984              
1985             =over 4
1986              
1987             =item * B
1988              
1989             A string containing the name of the Flickr API method you are
1990             calling.
1991              
1992             =item * B
1993              
1994             A hash ref containing the key value pairs you are passing to
1995             I
1996              
1997             =back
1998              
1999             If the method encounters any errors calling the API, receives an API error
2000             or can not parse the response it will log an error event, via the B method,
2001             and return undef.
2002              
2003             Otherwise it will return a I object (if XML::LibXML is
2004             installed) or a I object.
2005              
2006             =cut
2007              
2008             # Defined in Net::Flickr::API
2009              
2010             =head2 $obj->log()
2011              
2012             Returns a I object.
2013              
2014             =cut
2015              
2016             # Defined in Net::Flickr::API
2017              
2018             =head2 $obj->serialise_triples(\@triples,\*$fh)
2019              
2020             Print I<@triples> as RDF/XML to a filehandle (I<$fh>). If no filehandle
2021             is defined, prints to STDOUT.
2022              
2023             =cut
2024              
2025             sub serialise_triples {
2026             my $self = shift;
2027             my $triples = shift;
2028             my $fh = shift;
2029            
2030             if (! $fh) {
2031             $fh = \*STDOUT;
2032             }
2033            
2034             #
2035             # IO::Scalar-ism
2036             #
2037              
2038             if ($fh->isa("IO::Scalar")) {
2039             $fh->binmode(":utf8");
2040             }
2041              
2042             else {
2043             binmode $fh, ":utf8";
2044             }
2045              
2046             #
2047             #
2048             #
2049              
2050             my $ser = RDF::Simple::Serialiser->new();
2051            
2052             my %ns = $self->namespaces();
2053            
2054             foreach my $prefix (keys %ns) {
2055             $ser->addns($prefix, $ns{$prefix});
2056             }
2057            
2058             $fh->print($ser->serialise(@$triples));
2059             return 1;
2060             }
2061              
2062             =head2 $obj->serialize_triples(\@triples,\*$fh)
2063              
2064             An alias for I
2065              
2066             =cut
2067              
2068             sub serialize_triples {
2069             my $self = shift;
2070             $self->serialise_triples(@_);
2071             }
2072              
2073             sub places_url {
2074             my $self = shift;
2075             my $place_id = shift;
2076              
2077             if (! exists($self->{'__places_urls'}->{$place_id})){
2078            
2079             $self->{'__places_urls'}->{$place_id} = undef;
2080              
2081             my $info = $self->api_call({'method' => 'flickr.places.resolvePlaceId',
2082             'args' => {'place_id' => $place_id}});
2083              
2084             if ($info){
2085             $self->{'__places_urls'}->{$place_id} = $info->findvalue("/rsp/location/\@place_url");
2086             }
2087             }
2088              
2089             return $self->{'__places_urls'}->{$place_id};
2090             }
2091              
2092             =head1 VERSION
2093              
2094             2.2
2095              
2096             =head1 DATE
2097              
2098             $Date: 2010/12/19 19:06:12 $
2099              
2100             =head1 AUTHOR
2101              
2102             Aaron Straup Cope Eascope@cpan.orgE
2103              
2104             =head1 CONTRIBUTORS
2105              
2106             Thomas Sibley Etsibley@cpan.orgE
2107              
2108             =head1 EXAMPLES
2109              
2110             =head2 CONFIG FILES
2111              
2112             This is an example of a Config::Simple file used to collect RDF data
2113             from Flickr
2114              
2115             [flickr]
2116             api_key=asd6234kjhdmbzcxi6e323
2117             api_secret=s00p3rs3k3t
2118             auth_token=123-omgwtf4u
2119              
2120             =head2 RDF
2121              
2122             This is an example of an RDF dump for a photograph backed up from Flickr :
2123              
2124             http://flickr.com/photos/straup/2269291707/
2125              
2126            
2127            
2128             xmlns:geoname="http://www.geonames.org/onto#"
2129             xmlns:a="http://www.w3.org/2000/10/annotation-ns"
2130             xmlns:filtr="http://www.machinetags.org/wiki/filtr#process"
2131             xmlns:ph="http://www.machinetags.org/wiki/ph#camera"
2132             xmlns:exif="http://nwalsh.com/rdf/exif#"
2133             xmlns:mt="x-urn:flickr:machinetag:"
2134             xmlns:exifi="http://nwalsh.com/rdf/exif-intrinsic#"
2135             xmlns:geonames="http://www.machinetags.org/wiki/geonames#locality"
2136             xmlns:dcterms="http://purl.org/dc/terms/"
2137             xmlns:places="http://www.flickr.com/places/"
2138             xmlns:dc="http://purl.org/dc/elements/1.1/"
2139             xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#"
2140             xmlns:acl="http://www.w3.org/2001/02/acls#"
2141             xmlns:skos="http://www.w3.org/2004/02/skos/core#"
2142             xmlns:foaf="http://xmlns.com/foaf/0.1/"
2143             xmlns:rdfs="http://www.w3.org/2000/01/rdf-schema#"
2144             xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
2145             xmlns:computer="x-urn:freebsd:"
2146             xmlns:flickr="x-urn:flickr:"
2147             >
2148              
2149            
2150             2008-02-17T10:36:02Z
2151            
2152            
2153            
2154              
2155            
2156            
2157             Untitled Set #2008
2158            
2159            
2160              
2161            
2162             filtr
2163            
2164            
2165            
2166            
2167              
2168            
2169             Medium
2170             375
2171             500
2172            
2173            
2174            
2175            
2176              
2177            
2178             San Francisco
2179             California
2180             San Francisco
2181             kH8dLOubBZRvX_YZ
2182             United States
2183            
2184            
2185              
2186            
2187             Original
2188             1944
2189             2592
2190            
2191            
2192            
2193              
2194            
2195             cameraphone
2196            
2197              
2198            
2199             filtr
2200            
2201            
2202            
2203              
2204            
2205            
2206            
2207              
2208            
2209            
2210            
2211              
2212            
2213             sanfrancisco
2214            
2215            
2216            
2217              
2218            
2219             sanfrancisco
2220            
2221              
2222            
2223            
2224             heather powazek champ
2225             heather
2226            
2227              
2228            
2229            
2230            
2231            
2232              
2233            
2234             Square
2235             75
2236             75
2237            
2238            
2239            
2240            
2241              
2242            
2243             Thumbnail
2244             75
2245             100
2246            
2247            
2248            
2249            
2250              
2251            
2252             Flash did not fire, auto mode
2253             100/100
2254             100
2255             2592
2256             297/100
2257             1944
2258             5.6 mm
2259             2008-02-16T14:22:32PST
2260             sRGB
2261             f/2.8
2262             2008-02-16T14:22:32PST
2263             7643/1000
2264             0.005 sec (1/200)
2265            
2266              
2267            
2268             cameraphone
2269            
2270            
2271            
2272              
2273            
2274             b0571f7ec59c0b59e19a3f054f78d953cb32b071
2275             Aaron Straup Cope
2276             straup
2277            
2278              
2279            
2280            
2281            
2282              
2283            
2284             Small
2285             180
2286             240
2287            
2288            
2289            
2290            
2291              
2292            
2293             process
2294             filtr
2295            
2296            
2297              
2298            
2299            
2300            
2301              
2302            
2303             locality
2304             geonames
2305            
2306            
2307              
2308            
2309             5391959
2310            
2311            
2312            
2313              
2314            
2315             2008-02-17T10:36:02Z
2316            
2317            
2318            
2319              
2320            
2321             filtr
2322            
2323            
2324            
2325              
2326            
2327             filtr
2328             visbility
2329             Ambient Pork
2330             n82
2331             All rights reserved.
2332             public
2333            
2334             5391959
2335             2008-02-16T14:22:32-0800
2336             2008-02-16T14:32:59-0800
2337            
2338            
2339            
2340            
2341            
2342            
2343            
2344            
2345            
2346            
2347            
2348              
2349            
2350             Aaron Straup Cope
2351             asc
2352            
2353              
2354            
2355             2.1:1203244560
2356             2008-02-17T02:36:00-0800
2357            
2358            
2359            
2360              
2361            
2362             filtr
2363            
2364              
2365            
2366            
2367            
2368              
2369            
2370            
2371            
2372              
2373            
2374             camera
2375             ph
2376            
2377            
2378              
2379            
2380             6065-2269291707-72157603921497994
2381             2008-02-16T16:39:31
2382             best. title. ever.
2383            
2384            
2385            
2386              
2387            
2388             5391959
2389            
2390            
2391            
2392            
2393              
2394            
2395             n82
2396            
2397            
2398            
2399              
2400            
2401             PPLX
2402             US
2403             CA
2404             23
2405             California
2406             San Francisco County
2407            
2408            
2409              
2410            
2411             n82
2412            
2413            
2414            
2415            
2416              
2417            
2418            
2419            
2420              
2421            
2422             Large
2423             768
2424             1024
2425            
2426            
2427            
2428            
2429              
2430            
2431             2008-02-17T10:36:02Z
2432            
2433            
2434            
2435              
2436            
2437              
2438             =head1 SEE ALSO
2439              
2440             L
2441              
2442             L
2443              
2444             =head1 TO DO
2445              
2446             =over 4
2447              
2448             =item *
2449              
2450             Methods for describing more than just a photo; groups, tags, etc.
2451              
2452             =item *
2453              
2454             Update bounding boxes to be relative to individual images
2455              
2456             =item *
2457              
2458             Proper tests
2459              
2460             =back
2461              
2462             Patches are welcome.
2463              
2464             =head1 BUGS
2465              
2466             Please report all bugs via http://rt.cpan.org/
2467              
2468             =head1 LICENSE
2469              
2470             Copyright (c) 2005-2008 Aaron Straup Cope. All Rights Reserved.
2471              
2472             This is free software. You may redistribute it and/or
2473             modify it under the same terms as Perl itself.
2474              
2475             =cut
2476              
2477             return 1;
2478              
2479             __END__