exiftool-vendored.pl 12.78.0 → 12.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,195 +7,520 @@
7
7
  #
8
8
  # References: https://download.geonames.org/export/
9
9
  #
10
- # Notes: Set $Image::ExifTool::Geolocation::databaseFile to override
11
- # default database file (lib/Image/ExifTool/Geolocation.dat)
10
+ # Notes: Set $Image::ExifTool::Geolocation::geoDir to override
11
+ # default directory for the database file Geolocation.dat
12
+ # and language directory GeoLang.
12
13
  #
13
14
  # Based on data from geonames.org Creative Commons databases,
14
15
  # reformatted as follows in the Geolocation.dat file:
15
16
  #
16
- # Header: GeolocationV.VV\tNNNN\n - V.VV=version, NNNN=num city entries
17
- #
17
+ # Header:
18
+ # "GeolocationV.VV\tNNNN\n" (V.VV=version, NNNN=num city entries)
19
+ # "# <comment>\n"
18
20
  # NNNN City entries:
19
- # 1. int16u[2] - longitude.latitude (converted to 0-64k range)
20
- # 2. int8u - low byte of time zone number
21
- # 3. int8u - 100's=time zone high bit, population: 10's=num zeros, 1's=sig digit
22
- # 4. UTF8 City name, terminated by tab
23
- # 5. 2-character country code
24
- # 6. Region code, terminated by newline
25
- # End of section marker - "\0\0\0\0\x01"
21
+ # Offset Format Description
22
+ # 0 int16u - latitude high 16 bits (converted to 0-0x100000 range)
23
+ # 2 int8u - latitude low 4 bits, longitude low 4 bits
24
+ # 3 int16u - longitude high 16 bits
25
+ # 5 int8u - index of country in country list
26
+ # 6 int8u - 0xf0 = population E exponent (in format "N.Fe+0E"), 0x0f = population N digit
27
+ # 7 int16u - 0xf000 = population F digit, 0x0fff = index in region list (admin1)
28
+ # 9 int16u - 0x7fff = index in subregion (admin2), 0x8000 = high bit of time zone
29
+ # 11 int8u - low byte of time zone index
30
+ # 12 int8u - 0x0f - feature code index (see below)
31
+ # 13 string - UTF8 City name, terminated by newline
32
+ # "\0\0\0\0\x01"
26
33
  # Country entries:
27
34
  # 1. 2-character country code
28
35
  # 2. Country name, terminated by newline
29
- # End of section marker - "\0\0\0\0\x02"
36
+ # "\0\0\0\0\x02"
30
37
  # Region entries:
31
- # 1. 2-character country code
32
- # 2. Region code, terminated by tab
33
- # 3. Region name, terminated by newline
34
- # End of section marker - "\0\0\0\0\x03"
38
+ # 1. Region name, terminated by newline
39
+ # "\0\0\0\0\x03"
40
+ # Subregion entries:
41
+ # 1. Subregion name, terminated by newline
42
+ # "\0\0\0\0\x04"
35
43
  # Time zone entries:
36
44
  # 1. Time zone name, terminated by newline
37
- # End of file marker - "\0\0\0\0\0"
45
+ # "\0\0\0\0\0"
46
+ #
47
+ # Feature Codes: (see http://www.geonames.org/export/codes.html#P for descriptions)
48
+ #
49
+ # 0. Other 3. PPLA2 6. PPLA5 9. PPLF 12. PPLR
50
+ # 1. PPL 4. PPLA3 7. PPLC 10. PPLG 13. PPLS
51
+ # 2. PPLA 5. PPLA4 8. PPLCH 11. PPLL 14. STLMT
38
52
  #------------------------------------------------------------------------------
39
53
 
40
54
  package Image::ExifTool::Geolocation;
41
55
 
42
56
  use strict;
43
- use vars qw($VERSION $databaseFile);
57
+ use vars qw($VERSION $geoDir $dbInfo);
58
+
59
+ $VERSION = '1.02';
60
+
61
+ my $databaseVersion = '1.02';
44
62
 
45
- $VERSION = '1.00';
63
+ sub ReadDatabase($);
64
+ sub SortDatabase($);
65
+ sub AddEntry(@);
66
+ sub GetEntry($;$);
67
+ sub Geolocate($;$$$$$);
46
68
 
47
- my (@cityLookup, %countryLookup, %adminLookup, @timezoneLookup);
69
+ my (@cityList, @countryList, @regionList, @subregionList, @timezoneList);
70
+ my (%countryNum, %regionNum, %subregionNum, %timezoneNum, %langLookup);
71
+ my $sortedBy = 'Latitude';
72
+ my $pi = 3.1415926536;
73
+ my $earthRadius = 6371; # earth radius in km
74
+
75
+ my @featureCodes = qw(Other PPL PPLA PPLA2 PPLA3 PPLA4 PPLA5
76
+ PPLC PPLCH PPLF PPLG PPLL PPLR PPLS STLMT ?);
77
+ my $i = 0;
78
+ my %featureCodes = map { lc($_) => $i++ } @featureCodes;
48
79
 
49
80
  # get path name for database file from lib/Image/ExifTool/Geolocation.dat by default,
50
- # or according to $Image::ExifTool::Geolocation::databaseFile if specified
51
- my $datfile = $databaseFile;
52
- unless ($datfile) {
53
- $datfile = $INC{'Image/ExifTool/Geolocation.pm'};
54
- $datfile or $datfile = 'Geolocation.pm', warn("Error getting Geolocation directory\n");
55
- $datfile =~ s/\.pm$/\.dat/;
81
+ # or according to $Image::ExifTool::Geolocation::directory if specified
82
+ my $defaultDir = $INC{'Image/ExifTool/Geolocation.pm'};
83
+ if ($defaultDir) {
84
+ $defaultDir =~ s(/Geolocation\.pm$)();
85
+ } else {
86
+ $defaultDir = '.';
87
+ warn("Error getting Geolocation.pm directory\n");
56
88
  }
57
89
 
58
- # open geolocation database and verify header
59
- open DATFILE, "<$datfile" or warn("Error reading $datfile\n"), return 0;
60
- binmode DATFILE;
61
- my $line = <DATFILE>;
62
- unless ($line =~ /^Geolocation(\d+\.\d+)\t(\d+)/) {
63
- warn("Bad format Geolocation database\n");
64
- close(DATFILE);
65
- return 0;
90
+ # read the Geolocation database unless $geoDir set to empty string
91
+ unless (defined $geoDir and not $geoDir) {
92
+ unless ($geoDir and ReadDatabase("$geoDir/Geolocation.dat")) {
93
+ ReadDatabase("$defaultDir/Geolocation.dat");
94
+ }
66
95
  }
67
- my $ncity = $2;
68
-
69
- # read city database
70
- for (;;) {
71
- $line = <DATFILE>;
72
- last if length($line) == 6 and $line =~ /\0\0\0\0/;
73
- $line .= <DATFILE> while length($line) < 7;
74
- chomp $line;
75
- push @cityLookup, $line;
96
+
97
+ # set directory for language files
98
+ my $geoLang;
99
+ if ($geoDir and -d "$geoDir/GeoLang") {
100
+ $geoLang = "$geoDir/GeoLang";
101
+ } elsif ($geoDir or not defined $geoDir) {
102
+ $geoLang = "$defaultDir/GeoLang";
76
103
  }
77
- @cityLookup == $ncity or warn("Bad number of entries in Geolocation database\n"), return 0;
78
- # read countries
79
- for (;;) {
80
- $line = <DATFILE>;
81
- last if length($line) == 6 and $line =~ /\0\0\0\0/;
82
- chomp $line;
83
- $countryLookup{substr($line,0,2)} = substr($line,2);
104
+
105
+ # add user-defined entries to the database
106
+ if (@Image::ExifTool::UserDefined::Geolocation) {
107
+ AddEntry(@$_) foreach @Image::ExifTool::UserDefined::Geolocation;
84
108
  }
85
- # read regions
86
- for (;;) {
87
- $line = <DATFILE>;
88
- last if length($line) == 6 and $line =~ /\0\0\0\0/;
89
- chomp $line;
90
- my ($code, $region) = split /\t/, $line;
91
- $adminLookup{$code} = $region;
109
+
110
+ #------------------------------------------------------------------------------
111
+ # Read Geolocation database
112
+ # Inputs: 0) database file name
113
+ # Returns: true on success
114
+ sub ReadDatabase($)
115
+ {
116
+ my $datfile = shift;
117
+ # open geolocation database and verify header
118
+ open DATFILE, "<$datfile" or warn("Error reading $datfile\n"), return 0;
119
+ binmode DATFILE;
120
+ my $line = <DATFILE>;
121
+ unless ($line =~ /^Geolocation(\d+\.\d+)\t(\d+)/) {
122
+ warn("Bad format Geolocation database\n");
123
+ close(DATFILE);
124
+ return 0;
125
+ }
126
+ if ($1 != $databaseVersion) {
127
+ my $which = $1 < $databaseVersion ? 'database' : 'ExifTool';
128
+ warn("Incompatible Geolocation database (update your $which)\n");
129
+ close(DATFILE);
130
+ return 0;
131
+ }
132
+ my $ncity = $2;
133
+ my $comment = <DATFILE>;
134
+ defined $comment and $comment =~ /(\d+)/ or close(DATFILE), return 0;
135
+ $dbInfo = "$datfile v$databaseVersion: $ncity cities with population > $1";
136
+ my $isUserDefined = @Image::ExifTool::UserDefined::Geolocation;
137
+
138
+ # read city database
139
+ undef @cityList;
140
+ for (;;) {
141
+ $line = <DATFILE>;
142
+ last if length($line) == 6 and $line =~ /\0\0\0\0/;
143
+ $line .= <DATFILE> while length($line) < 14;
144
+ chomp $line;
145
+ push @cityList, $line;
146
+ }
147
+ @cityList == $ncity or warn("Bad number of entries in Geolocation database\n"), return 0;
148
+ # read countries
149
+ for (;;) {
150
+ $line = <DATFILE>;
151
+ last if length($line) == 6 and $line =~ /\0\0\0\0/;
152
+ chomp $line;
153
+ push @countryList, $line;
154
+ $countryNum{lc substr($line,0,2)} = $#countryList if $isUserDefined;
155
+ }
156
+ # read regions
157
+ for (;;) {
158
+ $line = <DATFILE>;
159
+ last if length($line) == 6 and $line =~ /\0\0\0\0/;
160
+ chomp $line;
161
+ push @regionList, $line;
162
+ $regionNum{lc $line} = $#regionList if $isUserDefined;
163
+ }
164
+ # read subregions
165
+ for (;;) {
166
+ $line = <DATFILE>;
167
+ last if length($line) == 6 and $line =~ /\0\0\0\0/;
168
+ chomp $line;
169
+ push @subregionList, $line;
170
+ $subregionNum{lc $line} = $#subregionList if $isUserDefined;
171
+ }
172
+ # read time zones
173
+ for (;;) {
174
+ $line = <DATFILE>;
175
+ last if length($line) == 6 and $line =~ /\0\0\0\0/;
176
+ chomp $line;
177
+ push @timezoneList, $line;
178
+ $timezoneNum{lc $line} = $#timezoneList if $isUserDefined;
179
+ }
180
+ close DATFILE;
181
+ return 1;
92
182
  }
93
- # read time zones
94
- for (;;) {
95
- $line = <DATFILE>;
96
- last if length($line) == 6 and $line =~ /\0\0\0\0/;
97
- chomp $line;
98
- push @timezoneLookup, $line;
183
+
184
+ #------------------------------------------------------------------------------
185
+ # Sort database by specified field
186
+ # Inputs: 0) Field name to sort (Latitude,City,Country)
187
+ # Returns: 1 on success
188
+ sub SortDatabase($)
189
+ {
190
+ my $field = shift;
191
+ return 1 if $field eq $sortedBy; # already sorted?
192
+ if ($field eq 'Latitude') {
193
+ @cityList = sort { $a cmp $b } @cityList;
194
+ } elsif ($field eq 'City') {
195
+ @cityList = sort { substr($a,13) cmp substr($b,13) } @cityList;
196
+ } elsif ($field eq 'Country') {
197
+ my %lkup;
198
+ foreach (@cityList) {
199
+ my $city = substr($_,13);
200
+ my $ctry = substr($countryList[ord substr($_,5,1)], 2);
201
+ $lkup{$_} = "$ctry $city";
202
+ }
203
+ @cityList = sort { $lkup{$a} cmp $lkup{$b} } @cityList;
204
+ } else {
205
+ return 0;
206
+ }
207
+ $sortedBy = $field;
208
+ return 1;
99
209
  }
100
- close DATFILE;
101
210
 
102
211
  #------------------------------------------------------------------------------
103
- # Look up lat/lon in geolocation database
104
- # Inputs: 0) Latitude, 1) longitude, 2) optional min population,
105
- # 3) optional max distance (km)
106
- # Returns: 0) UTF8 city name (or undef if geolocation is unsuccessful),
107
- # 1) UTF8 state, province or region (or undef),
108
- # 2) country code, 3) country name (undef is possible),
109
- # 4) time zone name (empty string possible), 5) approx population,
110
- # 6) approx distance (km), 7) approximate compass bearing (or undef),
111
- # 8/9) approx lat/lon
112
- sub Geolocate($$;$$)
212
+ # Add cities to the Geolocation database
213
+ # Inputs: 0-8) city,region,subregion,country_code,country,timezone,feature_code,population,lat,lon
214
+ # eg. AddEntry('Sinemorets','Burgas','Obshtina Tsarevo','BG','Bulgaria','Europe/Sofia','',400,42.06115,27.97833)
215
+ sub AddEntry(@)
113
216
  {
114
- my ($lat, $lon, $pop, $km) = @_;
115
- my ($minPop, $maxDist2);
116
- my $earthCirc = 40000; # earth circumference in km
217
+ my ($city, $region, $subregion, $cc, $country, $timezone, $fc, $pop, $lat, $lon) = @_;
218
+ @_ < 10 and warn("Too few arguments in $city definition (check for updated format)\n"), return;
219
+ if (@_ != 10 or length($cc) != 2) {
220
+ warn "Country code '${cc}' in UserDefined::Geolocation is not 2 characters\n";
221
+ return;
222
+ }
223
+ $fc = $featureCodes{lc $fc} || 0;
224
+ chomp $lon; # (just in case it was read from file)
225
+ # create reverse lookups for country/region/subregion/timezone if not done already
226
+ # (eg. if the entries are being added manually instead of via UserDefined::Geolocation)
227
+ unless (%countryNum) {
228
+ my $i;
229
+ $i = 0; $countryNum{lc substr($_,0,2)} = $i++ foreach @countryList;
230
+ $i = 0; $regionNum{lc $_} = $i++ foreach @regionList;
231
+ $i = 0; $subregionNum{lc $_} = $i++ foreach @subregionList;
232
+ $i = 0; $timezoneNum{lc $_} = $i++ foreach @timezoneList;
233
+ }
234
+ my $cn = $countryNum{lc $cc};
235
+ unless (defined $cn) {
236
+ push @countryList, "$cc$country";
237
+ $cn = $countryNum{lc $cc} = $#countryList;
238
+ } elsif ($country) {
239
+ $countryList[$cn] = "$cc$country"; # (override existing country name)
240
+ }
241
+ my $tn = $timezoneNum{lc $timezone};
242
+ unless (defined $tn) {
243
+ push @timezoneList, $timezone;
244
+ $tn = $timezoneNum{lc $timezone} = $#timezoneList;
245
+ }
246
+ my $rn = $regionNum{lc $region};
247
+ unless (defined $rn) {
248
+ push @regionList, $region;
249
+ $rn = $regionNum{lc $region} = $#regionList;
250
+ }
251
+ my $sn = $subregionNum{lc $subregion};
252
+ unless (defined $sn) {
253
+ push @subregionList, $subregion;
254
+ $sn = $subregionNum{lc $subregion} = $#subregionList;
255
+ }
256
+ $pop = sprintf('%.1e',$pop); # format: "3.1e+04" or "3.1e+004"
257
+ # pack CC index, population and region index into a 32-bit integer
258
+ my $code = ($cn << 24) | (substr($pop,-1,1)<<20) | (substr($pop,0,1)<<16) | (substr($pop,2,1)<<12) | $rn;
259
+ # store high bit of timezone index
260
+ $tn > 255 and $sn |= 0x8000, $tn -= 256;
261
+ $lat = int(($lat + 90) / 180 * 0x100000 + 0.5) & 0xfffff;
262
+ $lon = int(($lon + 180) / 360 * 0x100000 + 0.5) & 0xfffff;
263
+ my $hdr = pack('nCnNnCC', $lat>>4, (($lat&0x0f)<<4)|($lon&0x0f), $lon>>4, $code, $sn, $tn, $fc);
264
+ push @cityList, "$hdr$city";
265
+ $sortedBy = '';
266
+ }
117
267
 
268
+ #------------------------------------------------------------------------------
269
+ # Unpack entry in database
270
+ # Inputs: 0) entry number, 1) optional language code
271
+ # Returns: 0-9) city,region,subregion,country_code,country,timezone,feature_code,pop,lat,lon
272
+ sub GetEntry($;$)
273
+ {
274
+ my ($entryNum, $lang) = @_;
275
+ return() if $entryNum > $#cityList;
276
+ my ($lt,$f,$ln,$code,$sb,$tn,$fc) = unpack('nCnNnCC', $cityList[$entryNum]);
277
+ my $city = substr($cityList[$entryNum],13);
278
+ my $ctry = $countryList[$code >> 24];
279
+ my $rgn = $regionList[$code & 0x0fff];
280
+ my $sub = $subregionList[$sb & 0x7fff];
281
+ # convert population digits back into exponent format
282
+ my $pop = (($code>>16 & 0x0f) . '.' . ($code>>12 & 0x0f) . 'e+' . ($code>>20 & 0x0f)) + 0;
283
+ $tn += 256 if $sb & 0x8000;
284
+ $lt = sprintf('%.4f', (($lt<<4)|($f >> 4)) * 180 / 0x100000 - 90);
285
+ $ln = sprintf('%.4f', (($ln<<4)|($f & 0x0f))* 360 / 0x100000 - 180);
286
+ $fc = $featureCodes[$fc & 0x0f];
287
+ my $cc = substr($ctry, 0, 2);
288
+ my $country = substr($ctry, 2);
289
+ if ($lang) {
290
+ my $xlat = $langLookup{$lang};
291
+ # load language lookups if not done already
292
+ if (not defined $xlat) {
293
+ if (eval "require '$geoLang/$lang.pm'") {
294
+ my $trans = "Image::ExifTool::GeoLang::${lang}::Translate";
295
+ no strict 'refs';
296
+ $xlat = \%$trans if %$trans;
297
+ }
298
+ # read user-defined language translations
299
+ if (%Image::ExifTool::Geolocation::geoLang) {
300
+ my $userLang = $Image::ExifTool::Geolocation::geoLang{$lang};
301
+ if ($userLang and ref($userLang) eq 'HASH') {
302
+ if ($xlat) {
303
+ # add user-defined entries to main lookup
304
+ $$xlat{$_} = $$userLang{$_} foreach keys %$userLang;
305
+ } else {
306
+ $xlat = $userLang;
307
+ }
308
+ }
309
+ }
310
+ $langLookup{$lang} = $xlat || 0;
311
+ }
312
+ if ($xlat) {
313
+ my $r2 = $rgn;
314
+ # City-specific: "CCRgn,Sub,City", "CCRgn,City", "CC,City", ",City"
315
+ # Subregion-specific: "CCRgn,Sub,"
316
+ # Region-specific: "CCRgn,"
317
+ # Country-specific: "CC,"
318
+ $city = $$xlat{"$cc$r2,$sub,$city"} || $$xlat{"$cc$r2,$city"} ||
319
+ $$xlat{"$cc,$city"} || $$xlat{",$city"} || $$xlat{$city} || $city;
320
+ $sub = $$xlat{"$cc$rgn,$sub,"} || $$xlat{$sub} || $sub;
321
+ $rgn = $$xlat{"$cc$rgn,"} || $$xlat{$rgn} || $rgn;
322
+ $country = $$xlat{"$cc,"} || $$xlat{$country} || $country;
323
+ }
324
+ }
325
+ return($city,$rgn,$sub,$cc,$country,$timezoneList[$tn],$fc,$pop,$lt,$ln);
326
+ }
327
+
328
+ #------------------------------------------------------------------------------
329
+ # Look up lat,lon or city in geolocation database
330
+ # Inputs: 0) "lat,lon", "city,region,country", etc, (city must be first)
331
+ # 1) optional min population, 2) optional max distance (km)
332
+ # 3) optional language code, 4) flag to return multiple cities
333
+ # 5) comma-separated list of feature codes to match (or ignore if leading "-")
334
+ # Returns: Reference to list of city information, or list of city information
335
+ # lists if returning multiple cities.
336
+ # City information list elements:
337
+ # 0) UTF8 city name (or undef if geolocation is unsuccessful),
338
+ # 1) UTF8 state, province or region (or empty),
339
+ # 2) UTF8 subregion (or empty)
340
+ # 3) country code, 4) country name,
341
+ # 5) time zone name (empty string possible), 6) feature code, 7) population,
342
+ # 8/9) approx lat/lon (or undef if geolocation is unsuccessful,
343
+ # 10) approx distance (km), 11) compass bearing to city,
344
+ # 12) non-zero if multiple matches were possible (and city with
345
+ # the largest population is returned)
346
+ sub Geolocate($;$$$$$)
347
+ {
348
+ my ($arg, $pop, $maxDist, $lang, $multi, $fcodes) = @_;
349
+ my ($city, $i, %found, @exact, %regex, @multiCity, $other, $idx);
350
+ my ($minPop, $minDistU, $minDistC, @matchParms, @matches, @coords, $fcmask, $both);
351
+
352
+ @cityList or warn('No Geolocation database'), return();
353
+ # make population code for comparing with 2 bytes at offset 6 in database
118
354
  if ($pop) {
119
- # convert population minimum to a 2-digit code
120
- my $dig = substr($pop, 0, 1);
121
- my $zer = length($pop) - 1;
122
- # round up if necessary
123
- if (length($pop) > 1 and substr($pop, 1, 1) >= 5) {
124
- ++$dig > 9 and $dig = 1, ++$zer;
355
+ $pop = sprintf('%.1e', $pop);
356
+ $minPop = chr((substr($pop,-1,1)<<4) | (substr($pop,0,1))) . chr(substr($pop,2,1)<<4);
357
+ }
358
+ if ($fcodes) {
359
+ my $neg = $fcodes =~ s/^-//;
360
+ my @fcodes = split /\s*,\s*/, $fcodes;
361
+ if ($neg) {
362
+ $fcmask = 0xffff;
363
+ defined $featureCodes{lc $_} and $fcmask &= ~((1 << $featureCodes{lc $_})) foreach @fcodes;
364
+ } else {
365
+ defined $featureCodes{lc $_} and $fcmask |= (1 << $featureCodes{lc $_}) foreach @fcodes;
366
+ }
367
+ }
368
+ #
369
+ # process input argument
370
+ #
371
+ $arg =~ s/^\s+//; $arg =~ s/\s+$//; # remove leading/trailing spaces
372
+ my @args = split /\s*,\s*/, $arg;
373
+ my %ri = ( cc => 0, co => 1, re => 2, sr => 3, ci => 8, '' => 9 );
374
+ foreach (@args) {
375
+ # allow regular expressions optionally prefixed by "ci", "cc", "co", "re" or "sr"
376
+ if (m{^(-)?(\w{2})?/(.*)/(i?)$}) {
377
+ my $re = $4 ? qr/$3/im : qr/$3/m;
378
+ next if not defined($idx = $ri{$2});
379
+ $other = 1 if $idx < 5;
380
+ $idx += 10 if $1; # add 10 for negative matches
381
+ push @{$regex{$idx}}, $re;
382
+ $city = '' unless defined $city;
383
+ } elsif (/^[-+]?\d+(\.\d+)?$/) { # coordinate format
384
+ push @coords, $_ if @coords < 2;
385
+ } elsif (lc eq 'both') {
386
+ $both = 1;
387
+ } elsif ($city) {
388
+ push @exact, lc $_;
389
+ } else {
390
+ $city = lc $_;
125
391
  }
126
- $minPop = $zer.$dig;
127
- }
128
- if ($km) {
129
- # convert max distance to reduced coordinate units
130
- my $tmp = $km * 2 * 65536 / $earthCirc;
131
- $maxDist2 = $tmp * $tmp;
132
- }
133
- my $cos = cos($lat * 3.14159 / 180); # cosine factor for longitude distances
134
- # reduce lat/lon to the range 0-65536
135
- $lat = int(($lat + 90) / 180 * 65536 + 0.5) & 0xffff;
136
- $lon = int(($lon + 180) / 360 * 65536 + 0.5) & 0xffff;
137
- my $coord = pack('n2',$lon,$lat); # pack for comparison with binary database values
392
+ }
393
+ unless (defined $city or @coords == 2) {
394
+ warn("Insufficient information to determine geolocation\n");
395
+ return();
396
+ }
397
+ # sort database by logitude if finding entry based on coordinates
398
+ SortDatabase('Latitude') if @coords == 2 and ($both or not defined $city);
399
+ #
400
+ # perform reverse Geolocation lookup to determine GPS based on city, country, etc.
401
+ #
402
+ while (defined $city and (@coords != 2 or $both)) {
403
+ Entry: for ($i=0; $i<@cityList; ++$i) {
404
+ my $cty = substr($cityList[$i],13);
405
+ next if $city and $city ne lc $cty; # test exact city name first
406
+ # test with city-specific regexes
407
+ if ($regex{8}) { $cty =~ $_ or next Entry foreach @{$regex{8}} }
408
+ if ($regex{18}) { $cty !~ $_ or next Entry foreach @{$regex{18}} }
409
+ # test other arguments
410
+ my ($cd,$sb) = unpack('x5Nn', $cityList[$i]);
411
+ my $ct = $countryList[$cd >> 24];
412
+ my @geo = (substr($ct,0,2), substr($ct,2), $regionList[$cd & 0x0fff], $subregionList[$sb & 0x7fff]);
413
+ if (@exact) {
414
+ # make quick lookup for all names at this location
415
+ my %geoLkup;
416
+ $_ and $geoLkup{lc $_} = 1 foreach @geo;
417
+ $geoLkup{$_} or next Entry foreach @exact;
418
+ }
419
+ # test with cc, co, re and sr regexes
420
+ if ($other) { foreach $idx (0..3) {
421
+ if ($regex{$idx}) { $geo[$idx] =~ $_ or next Entry foreach @{$regex{$idx}} }
422
+ if ($regex{$idx+10}) { $geo[$idx] !~ $_ or next Entry foreach @{$regex{$idx+10}} }
423
+ } }
424
+ # test regexes for any place name
425
+ if ($regex{9} or $regex{19}) {
426
+ my $str = join "\n", $cty, @geo;
427
+ $str =~ $_ or next Entry foreach @{$regex{9}};
428
+ $str !~ $_ or next Entry foreach @{$regex{19}};
429
+ }
430
+ # test feature code and population
431
+ next if $fcmask and not $fcmask & (1 << (ord(substr($cityList[$i],12,1)) & 0x0f));
432
+ my $pc = substr($cityList[$i],6,2);
433
+ $found{$i} = $pc if not defined $minPop or $pc ge $minPop;
434
+ }
435
+ if (%found) {
436
+ @matches = keys %found;
437
+ @coords == 2 and @matches = sort({ $a <=> $b } @matches), last; # continue to use coords
438
+ scalar(keys %found) > 200 and warn("Too many matching cities\n"), return();
439
+ @matches = sort { $found{$b} cmp $found{$a} or $cityList[$a] cmp $cityList[$b] } @matches if @matches > 1;
440
+ foreach (@matches) {
441
+ my @cityInfo = GetEntry($_, $lang);
442
+ $cityInfo[12] = @matches if @matches > 1;
443
+ return \@cityInfo unless @matches > 1 and $multi;
444
+ push @multiCity, \@cityInfo;
445
+ }
446
+ return \@multiCity;
447
+ }
448
+ warn "No such city in Geolocation database\n";
449
+ return();
450
+ }
451
+ #
452
+ # determine Geolocation based on GPS coordinates
453
+ #
454
+ my ($lat, $lon) = @coords;
455
+ if ($maxDist) {
456
+ $minDistU = $maxDist / (2 * $earthRadius); # min distance on unit sphere
457
+ $minDistC = $maxDist * 0x100000 / ($pi * $earthRadius); # min distance in coordinate units
458
+ } else {
459
+ $minDistU = $pi;
460
+ $minDistC = 0x200000;
461
+ }
462
+ my $cos = cos($lat * $pi / 180); # cosine factor for longitude distances
463
+ # reduce lat/lon to the range 0-0x100000
464
+ $lat = int(($lat + 90) / 180 * 0x100000 + 0.5) & 0xfffff;
465
+ $lon = int(($lon + 180) / 360 * 0x100000 + 0.5) & 0xfffff;
466
+ $lat or $lat = $coords[0] < 0 ? 1 : 0xfffff; # (zero latitude is a problem for our calculations)
467
+ my $coord = pack('nCn',$lat>>4,(($lat&0x0f)<<4)|($lon&0x0f),$lon>>4);;
138
468
  # binary search to find closest longitude
139
- my ($n0, $n1) = (0, scalar(@cityLookup)-1);
469
+ my $numEntries = @matches || @cityList;
470
+ my ($n0, $n1) = (0, $numEntries - 1);
140
471
  while ($n1 - $n0 > 1) {
141
472
  my $n = int(($n0 + $n1) / 2);
142
- if ($coord lt $cityLookup[$n]) {
473
+ if ($coord lt $cityList[@matches ? $matches[$n] : $n]) {
143
474
  $n1 = $n;
144
475
  } else {
145
476
  $n0 = $n;
146
477
  }
147
478
  }
148
479
  # step backward then forward through database to find nearest city
149
- my ($minDist2, $minN, @dxy);
150
480
  my ($inc, $end, $n) = (-1, -1, $n0+1);
481
+ my ($p0, $t0) = ($lat*$pi/0x100000 - $pi/2, $lon*$pi/0x080000 - $pi);
482
+ my $cp0 = cos($p0);
151
483
  for (;;) {
152
484
  if (($n += $inc) == $end) {
153
485
  last if $inc == 1;
154
- ($inc, $end, $n) = (1, scalar(@cityLookup), $n1);
155
- }
156
- my ($x,$y) = unpack('n2', $cityLookup[$n]);
157
- my ($dy,$dx) = ($y-$lat, $x-$lon);
158
- $dx += 65536 if $dx < -32768; # measure the short way around the world
159
- $dx -= 65536 if $dx > 32768;
160
- $dx = 2 * $cos * $dx; # adjust for longitude spacing
161
- my $dx2 = $dx * $dx;
162
- my $dist2 = $dy * $dy + $dx2;
163
- if (defined $minDist2) {
164
- # searched far enough if longitude alone is further than best distance
165
- $dx2 > $minDist2 and $n = $end - $inc, next;
166
- } elsif (defined $maxDist2) {
167
- $dx2 > $maxDist2 and $n = $end - $inc, next;
168
- next if $dist2 > $maxDist2; # ignore if distance is too great
486
+ ($inc, $end, $n) = (1, $numEntries, $n1);
169
487
  }
488
+ my $entryNum = @matches ? $matches[$n] : $n;
489
+ # get city latitude/longitude
490
+ my ($lt,$f,$ln) = unpack('nCn', $cityList[$entryNum]);
491
+ $lt = ($lt << 4) | ($f >> 4);
492
+ # searched far enough if latitude alone is further than best distance
493
+ abs($lt - $lat) > $minDistC and $n = $end - $inc, next;
170
494
  # ignore if population is below threshold
171
- next if $minPop and $minPop > unpack('x5C', $cityLookup[$n]) % 100;
172
- if (not defined $minDist2 or $minDist2 > $dist2) {
173
- $minDist2 = $dist2;
174
- @dxy = ($dx, $dy);
175
- $minN = $n;
176
- }
495
+ next if defined $minPop and $minPop ge substr($cityList[$entryNum],6,2);
496
+ next if $fcmask and not $fcmask & (1 << (ord(substr($cityList[$entryNum],12,1)) & 0x0f));
497
+ $ln = ($ln << 4) | ($f & 0x0f);
498
+ # calculate great circle distance to this city on unit sphere
499
+ my ($p1, $t1) = ($lt*$pi/0x100000 - $pi/2, $ln*$pi/0x080000 - $pi);
500
+ my ($sp, $st) = (sin(($p1-$p0)/2), sin(($t1-$t0)/2));
501
+ my $a = $sp * $sp + $cp0 * cos($p1) * $st * $st;
502
+ my $distU = atan2(sqrt($a), sqrt(1-$a));
503
+ next if $distU > $minDistU;
504
+ $minDistU = $distU;
505
+ $minDistC = $minDistU * 0x200000 / $pi;
506
+ @matchParms = ($entryNum, $p1, $t1, $distU);
177
507
  }
178
- return () unless defined $minN;
179
-
180
- my ($ln,$lt,$tn,$pc) = unpack('n2C2', $cityLookup[$minN]);
181
- my ($city, $code) = split /\t/, substr($cityLookup[$minN],6);
182
- my $ctry = substr($code,0,2);
183
- my $rgn = $adminLookup{$code};
184
- my $po2 = substr($pc, -1) . (length($pc) > 1 ? '0' x substr($pc, -2, 1) : '');
185
- $tn += 256 if $pc > 99;
186
- my $be; # calculate bearing to geolocated city
187
- if ($dxy[0] or $dxy[1]) {
188
- $be = atan2($dxy[0],$dxy[1]) * 180 / 3.14159;
189
- $be += 360 if $be < 0;
190
- $be = int($be + 0.5);
191
- }
192
- $lt = sprintf('%.3f', $lt * 180 / 65536 - 90);
193
- $ln = sprintf('%.3f', $ln * 360 / 65536 - 180);
194
- $km = sprintf('%.1f', sqrt($minDist2) * $earthCirc / (2 * 65536));
195
-
196
- return($city,$rgn,$ctry,$countryLookup{$ctry},$timezoneLookup[$tn],$po2,$km,$be,$lt,$ln);
508
+ @matchParms or warn("No suitable location in Geolocation database\n"), return();
509
+
510
+ # calculate distance in km and bearing to matching city
511
+ my ($en, $p1, $t1, $distU) = @matchParms;
512
+ my $km = sprintf('%.2f', 2 * $earthRadius * $distU);
513
+ my $be = atan2(sin($t1-$t0)*cos($p1-$p0), $cp0*sin($p1)-sin($p0)*cos($p1)*cos($t1-$t0));
514
+ $be = int($be * 180 / $pi + 360.5) % 360; # convert from radians to integer degrees
515
+
516
+ # unpack return values from database entry
517
+ my @cityInfo = GetEntry($en, $lang);
518
+ @cityInfo[10,11] = ($km, $be); # add distance and heading
519
+ return \@cityInfo;
197
520
  }
198
521
 
522
+ 1; #end
523
+
199
524
  __END__
200
525
 
201
526
  =head1 NAME
@@ -209,8 +534,219 @@ This module is used by the Image::ExifTool Geolocation feature.
209
534
  =head1 DESCRIPTION
210
535
 
211
536
  This module contains the code to convert GPS coordinates to city, region,
212
- country, time zone, etc. It uses a database derived from geonames.org,
213
- modified to reduce the size as much as possible.
537
+ subregion, country, time zone, etc. It uses a database derived from
538
+ geonames.org, modified to reduce the size as much as possible.
539
+
540
+ =head1 METHODS
541
+
542
+ =head2 ReadDatabase
543
+
544
+ Load Geolocation database from file. This method is called automatically
545
+ when this module is loaded. By default, the database is loaded from
546
+ "Geolocation.dat" in the same directory as this module, but a different
547
+ directory may be used by setting $Image::ExifTool::Geolocation::geoDir
548
+ before loading this module. Setting this to an empty string avoids loading
549
+ any database. A warning is generated if the file can't be read.
550
+
551
+ Image::ExifTool::Geolocation::ReadDatabase($filename);
552
+
553
+ =over 4
554
+
555
+ =item Inputs:
556
+
557
+ 0) Database file name
558
+
559
+ =item Return Value:
560
+
561
+ True on success.
562
+
563
+ =back
564
+
565
+ =head2 SortDatabase
566
+
567
+ Sort database in specified order.
568
+
569
+ Image::ExifTool::Geolocation::ReadDatabase('City');
570
+
571
+ =over 4
572
+
573
+ =item Inputs:
574
+
575
+ 0) Sort order: 'Latitude', 'City' or 'Country'
576
+
577
+ =item Return Value:
578
+
579
+ 1 on success, 0 on failure (bad sort order specified).
580
+
581
+ =back
582
+
583
+ =head2 AddEntry
584
+
585
+ Add entry to Geolocation database.
586
+
587
+ Image::ExifTool::Geolocation::AddEntry($city, $region,
588
+ $countryCode, $country, $timezone, $featureCode
589
+ $population, $lat, $lon);
590
+
591
+ =over 4
592
+
593
+ =item Inputs:
594
+
595
+ 0) City name (UTF8)
596
+
597
+ 1) Region, state or province name (UTF8), or empty string if unknown
598
+
599
+ 2) Subregion name (UTF8), or empty string if unknown
600
+
601
+ 3) 2-character ISO 3166 country code
602
+
603
+ 4) Country name (UTF8), or empty string to use existing definition. If the
604
+ country name is provided for a country code that already exists in the
605
+ database, then the database entry is updated with the new country name.
606
+
607
+ 5) Time zone identifier (eg. "America/New_York")
608
+
609
+ 6) Feature code (eg. "PPL"), or empty if not known
610
+
611
+ 7) City population
612
+
613
+ 8) GPS latitude (signed floating point degrees)
614
+
615
+ 9) GPS longitude
616
+
617
+ =back
618
+
619
+ =head2 GetEntry
620
+
621
+ Get entry from Geolocation database.
622
+
623
+ my @vals = Image::ExifTool::Geolocation::GetEntry($num,$lang);
624
+
625
+ =over 4
626
+
627
+ =item Inputs:
628
+
629
+ 0) Entry number in database
630
+
631
+ 1) Optional language code
632
+
633
+ =item Return Values:
634
+
635
+ 0) City name, or undef if the entry didn't exist
636
+
637
+ 1) Region name, or "" if no region
638
+
639
+ 2) Subregion name, or "" if no subregion
640
+
641
+ 3) Country code
642
+
643
+ 4) Country name
644
+
645
+ 5) Time zone
646
+
647
+ 6) Feature code
648
+
649
+ 7) City population
650
+
651
+ 8) GPS latitude
652
+
653
+ 9) GPS longitude
654
+
655
+ =item Notes:
656
+
657
+ The alternate-language feature of this method (and of L</Geolocate>)
658
+ requires the installation of optional GeoLang modules. See
659
+ L<https://exiftool.org/geolocation.html> for more information.
660
+
661
+ =back
662
+
663
+ =head2 Geolocate
664
+
665
+ Return geolocation information for specified GPS coordinates or city name.
666
+
667
+ my @cityInfo =
668
+ Image::ExifTool::Geolocation::Geolocate($arg,$pop,$dist,$lang,$multi,$codes);
669
+
670
+ =over 4
671
+
672
+ =item Inputs:
673
+
674
+ 0) Input argument ("lat,lon", "city", "city,country", "city,region,country",
675
+ etc). When specifying a city, the city name must come first, followed by
676
+ zero or more of the following in any order, separated by commas: region
677
+ name, subregion name, country code, and/or country name. Regular
678
+ expressions in C</expr/> format are also allowed, optionally prefixed by
679
+ "ci", "re", "sr", "cc" or "co" to specifically match City, Region,
680
+ Subregion, CountryCode or Country name. See
681
+ L<https://exiftool.org/geolocation.html#Read> for details.
682
+
683
+ 1) Minimum city population (cities smaller than this are ignored)
684
+
685
+ 2) Maximum distance to city (farther cities are not considered)
686
+
687
+ 3) Language code
688
+
689
+ 4) Flag to return multiple cities if there is more than one match. In this
690
+ case the return value is a list of city information lists.
691
+
692
+ 5) Comma-separated list of feature codes to include in search, or to exclude
693
+ if the list starts with a dash (-).
694
+
695
+ =item Return Value:
696
+
697
+ Reference to list of information about the matching city. If multiple
698
+ matches were found, the city with the highest population is returned unless
699
+ the flag is set to allow multiple cities to be returned, in which case all
700
+ cities are turned as a list of city lists in order of decreasing population.
701
+
702
+ The city information list contains the following entries:
703
+
704
+ 0) Name of matching city (UTF8), or undef if no match
705
+
706
+ 1) Region, state or province name (UTF8), or "" if no region
707
+
708
+ 2) Subregion name (UTF8), or "" if no region
709
+
710
+ 3) Country code
711
+
712
+ 4) Country name (UTF8)
713
+
714
+ 5) Standard time zone identifier name
715
+
716
+ 6) Feature code
717
+
718
+ 7) City population rounded to 2 significant figures
719
+
720
+ 8) Approximate city latitude (signed degrees)
721
+
722
+ 9) Approximate city longitude
723
+
724
+ 10) Distance to city in km if "lat,lon" specified
725
+
726
+ 11) Compass bearing for direction to city if "lat,lon" specified
727
+
728
+ 12) Flag set if multiple matches were found
729
+
730
+ =back
731
+
732
+ =head1 USING A CUSTOM DATABASE
733
+
734
+ This example shows how to use a custom database. In this example, the input
735
+ database file is a comma-separated text file with columns corresponding to
736
+ the input arguments of the AddEntry method.
737
+
738
+ $Image::ExifTool::Geolocation::geoDir = '';
739
+ require Image::ExifTool::Geolocation;
740
+ open DFILE, "<$filename";
741
+ Image::ExifTool::Geolocation::AddEntry(split /,/) foreach <DFILE>;
742
+ close DFILE;
743
+
744
+ =head1 CUSTOM LANGUAGE TRANSLATIONS
745
+
746
+ User-defined language translations may be added by defining
747
+ %Image::ExifTool::Geolocation::geoLang before calling GetEntry() or
748
+ Geolocate(). See L<http://exiftool.org/geolocation.html#Custom> for
749
+ details.
214
750
 
215
751
  =head1 AUTHOR
216
752
 
@@ -226,6 +762,8 @@ from geonames.org with a Creative Commons license.
226
762
 
227
763
  =item L<https://download.geonames.org/export/>
228
764
 
765
+ =item L<https://exiftool.org/geolocation.html>
766
+
229
767
  =back
230
768
 
231
769
  =head1 SEE ALSO