exiftool-vendored.exe 12.78.0 → 12.82.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.
- package/bin/exiftool_files/Changes +80 -3
- package/bin/exiftool_files/README +3 -2
- package/bin/exiftool_files/config_files/acdsee.config +37 -57
- package/bin/exiftool_files/config_files/example.config +6 -0
- package/bin/exiftool_files/exiftool.pl +70 -26
- package/bin/exiftool_files/lib/Image/ExifTool/BuildTagLookup.pm +44 -31
- package/bin/exiftool_files/lib/Image/ExifTool/CanonVRD.pm +2 -2
- package/bin/exiftool_files/lib/Image/ExifTool/FujiFilm.pm +21 -5
- package/bin/exiftool_files/lib/Image/ExifTool/GM.pm +543 -0
- package/bin/exiftool_files/lib/Image/ExifTool/GeoLang/cs.pm +978 -0
- package/bin/exiftool_files/lib/Image/ExifTool/GeoLang/de.pm +1975 -0
- package/bin/exiftool_files/lib/Image/ExifTool/GeoLang/en_ca.pm +44 -0
- package/bin/exiftool_files/lib/Image/ExifTool/GeoLang/en_gb.pm +124 -0
- package/bin/exiftool_files/lib/Image/ExifTool/GeoLang/es.pm +2921 -0
- package/bin/exiftool_files/lib/Image/ExifTool/GeoLang/fi.pm +1116 -0
- package/bin/exiftool_files/lib/Image/ExifTool/GeoLang/fr.pm +3171 -0
- package/bin/exiftool_files/lib/Image/ExifTool/GeoLang/it.pm +2750 -0
- package/bin/exiftool_files/lib/Image/ExifTool/GeoLang/ja.pm +10256 -0
- package/bin/exiftool_files/lib/Image/ExifTool/GeoLang/ko.pm +4499 -0
- package/bin/exiftool_files/lib/Image/ExifTool/GeoLang/nl.pm +1270 -0
- package/bin/exiftool_files/lib/Image/ExifTool/GeoLang/pl.pm +3019 -0
- package/bin/exiftool_files/lib/Image/ExifTool/GeoLang/ru.pm +18220 -0
- package/bin/exiftool_files/lib/Image/ExifTool/GeoLang/sk.pm +441 -0
- package/bin/exiftool_files/lib/Image/ExifTool/GeoLang/sv.pm +714 -0
- package/bin/exiftool_files/lib/Image/ExifTool/GeoLang/tr.pm +452 -0
- package/bin/exiftool_files/lib/Image/ExifTool/GeoLang/zh_cn.pm +2225 -0
- package/bin/exiftool_files/lib/Image/ExifTool/GeoLang/zh_tw.pm +72 -0
- package/bin/exiftool_files/lib/Image/ExifTool/Geolocation.dat +0 -0
- package/bin/exiftool_files/lib/Image/ExifTool/Geolocation.pm +867 -146
- package/bin/exiftool_files/lib/Image/ExifTool/Geotag.pm +13 -1
- package/bin/exiftool_files/lib/Image/ExifTool/JSON.pm +7 -2
- package/bin/exiftool_files/lib/Image/ExifTool/M2TS.pm +32 -4
- package/bin/exiftool_files/lib/Image/ExifTool/MakerNotes.pm +2 -2
- package/bin/exiftool_files/lib/Image/ExifTool/Microsoft.pm +1 -1
- package/bin/exiftool_files/lib/Image/ExifTool/Nikon.pm +331 -22
- package/bin/exiftool_files/lib/Image/ExifTool/NikonCustom.pm +55 -1
- package/bin/exiftool_files/lib/Image/ExifTool/Olympus.pm +1 -0
- package/bin/exiftool_files/lib/Image/ExifTool/OpenEXR.pm +37 -19
- package/bin/exiftool_files/lib/Image/ExifTool/PNG.pm +3 -3
- package/bin/exiftool_files/lib/Image/ExifTool/QuickTime.pm +40 -24
- package/bin/exiftool_files/lib/Image/ExifTool/QuickTimeStream.pl +61 -30
- package/bin/exiftool_files/lib/Image/ExifTool/README +8 -5
- package/bin/exiftool_files/lib/Image/ExifTool/Sony.pm +1 -1
- package/bin/exiftool_files/lib/Image/ExifTool/TagLookup.pm +4820 -4750
- package/bin/exiftool_files/lib/Image/ExifTool/TagNames.pod +1000 -615
- package/bin/exiftool_files/lib/Image/ExifTool/WriteQuickTime.pl +31 -8
- package/bin/exiftool_files/lib/Image/ExifTool/WriteXMP.pl +1 -1
- package/bin/exiftool_files/lib/Image/ExifTool/Writer.pl +58 -2
- package/bin/exiftool_files/lib/Image/ExifTool/XMP.pm +20 -3
- package/bin/exiftool_files/lib/Image/ExifTool/XMP2.pl +64 -0
- package/bin/exiftool_files/lib/Image/ExifTool.pm +210 -97
- package/bin/exiftool_files/lib/Image/ExifTool.pod +44 -18
- package/package.json +3 -3
|
@@ -1,224 +1,943 @@
|
|
|
1
1
|
#------------------------------------------------------------------------------
|
|
2
2
|
# File: Geolocation.pm
|
|
3
3
|
#
|
|
4
|
-
# Description:
|
|
4
|
+
# Description: Determine geolocation from GPS and visa-versa
|
|
5
5
|
#
|
|
6
6
|
# Revisions: 2024-03-03 - P. Harvey Created
|
|
7
|
+
# 2024-03-21 - PH Significant restructuring and addition of
|
|
8
|
+
# several new features.
|
|
7
9
|
#
|
|
8
10
|
# References: https://download.geonames.org/export/
|
|
9
11
|
#
|
|
10
|
-
# Notes: Set $Image::ExifTool::Geolocation::
|
|
11
|
-
# default database file
|
|
12
|
+
# Notes: Set $Image::ExifTool::Geolocation::geoDir to override
|
|
13
|
+
# default directory for the database file Geolocation.dat
|
|
14
|
+
# and language directory GeoLang.
|
|
12
15
|
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
16
|
+
# Set $Image::ExifTool::Geolocation::altDir to use a database
|
|
17
|
+
# of alternate city names. The file is called AltNames.dat
|
|
18
|
+
# with entries in the same order as Geolocation.dat. Each
|
|
19
|
+
# entry is a newline-separated list of alternate names
|
|
20
|
+
# terminated by a null byte.
|
|
15
21
|
#
|
|
16
|
-
#
|
|
22
|
+
# Databases are based on data from geonames.org with a
|
|
23
|
+
# Creative Commons license, reformatted as follows in the
|
|
24
|
+
# Geolocation.dat file:
|
|
17
25
|
#
|
|
26
|
+
# Header:
|
|
27
|
+
# "GeolocationV.VV\tNNNN\n" (V.VV=version, NNNN=num city entries)
|
|
28
|
+
# "# <comment>\n"
|
|
18
29
|
# NNNN City entries:
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
30
|
+
# Offset Format Description
|
|
31
|
+
# 0 int16u - latitude high 16 bits (converted to 0-0x100000 range)
|
|
32
|
+
# 2 int8u - latitude low 4 bits, longitude low 4 bits
|
|
33
|
+
# 3 int16u - longitude high 16 bits
|
|
34
|
+
# 5 int8u - index of country in country list
|
|
35
|
+
# 6 int8u - 0xf0 = population E exponent (in format "N.Fe+0E"), 0x0f = population N digit
|
|
36
|
+
# 7 int16u - 0xf000 = population F digit, 0x0fff = index in region list (admin1)
|
|
37
|
+
# 9 int16u - v1.02: 0x7fff = index in subregion (admin2), 0x8000 = high bit of time zone
|
|
38
|
+
# 9 int16u - v1.03: index in subregion (admin2)
|
|
39
|
+
# 11 int8u - low byte of time zone index
|
|
40
|
+
# 12 int8u - 0x0f = feature code index (see below), v1.03: 0x80 = high bit of time zone
|
|
41
|
+
# 13 string - UTF8 City name, terminated by newline
|
|
42
|
+
# "\0\0\0\0\x01"
|
|
26
43
|
# Country entries:
|
|
27
44
|
# 1. 2-character country code
|
|
28
45
|
# 2. Country name, terminated by newline
|
|
29
|
-
#
|
|
46
|
+
# "\0\0\0\0\x02"
|
|
30
47
|
# Region entries:
|
|
31
|
-
# 1.
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
48
|
+
# 1. Region name, terminated by newline
|
|
49
|
+
# "\0\0\0\0\x03"
|
|
50
|
+
# Subregion entries:
|
|
51
|
+
# 1. Subregion name, terminated by newline
|
|
52
|
+
# "\0\0\0\0\x04"
|
|
35
53
|
# Time zone entries:
|
|
36
54
|
# 1. Time zone name, terminated by newline
|
|
37
|
-
#
|
|
55
|
+
# "\0\0\0\0\0"
|
|
56
|
+
#
|
|
57
|
+
# Feature Codes: (see http://www.geonames.org/export/codes.html#P for descriptions)
|
|
58
|
+
#
|
|
59
|
+
# 0. Other 3. PPLA2 6. PPLA5 9. PPLF 12. PPLR 15. PPLX
|
|
60
|
+
# 1. PPL 4. PPLA3 7. PPLC 10. PPLG 13. PPLS
|
|
61
|
+
# 2. PPLA 5. PPLA4 8. PPLCH 11. PPLL 14. STLMT
|
|
38
62
|
#------------------------------------------------------------------------------
|
|
39
63
|
|
|
40
64
|
package Image::ExifTool::Geolocation;
|
|
41
65
|
|
|
42
66
|
use strict;
|
|
43
|
-
use vars qw($VERSION $
|
|
67
|
+
use vars qw($VERSION $geoDir $altDir $dbInfo);
|
|
68
|
+
|
|
69
|
+
$VERSION = '1.04'; # (this is the module version number, not the database version)
|
|
44
70
|
|
|
45
|
-
$
|
|
71
|
+
my $debug; # set to output processing time for testing
|
|
46
72
|
|
|
47
|
-
|
|
73
|
+
sub ReadDatabase($);
|
|
74
|
+
sub SortDatabase($);
|
|
75
|
+
sub AddEntry(@);
|
|
76
|
+
sub GetEntry($;$$);
|
|
77
|
+
sub Geolocate($;$$$$$);
|
|
78
|
+
|
|
79
|
+
my (@cityList, @countryList, @regionList, @subregionList, @timezoneList);
|
|
80
|
+
my (%countryNum, %regionNum, %subregionNum, %timezoneNum); # reverse lookups
|
|
81
|
+
my (@sortOrder, @altNames, %langLookup, $nCity);
|
|
82
|
+
my ($lastArgs, %lastFound, @lastByPop, @lastByLat); # cached city matches
|
|
83
|
+
my $dbVer = '1.03';
|
|
84
|
+
my $sortedBy = 'Latitude';
|
|
85
|
+
my $pi = 3.1415926536;
|
|
86
|
+
my $earthRadius = 6371; # earth radius in km
|
|
87
|
+
|
|
88
|
+
my @featureCodes = qw(Other PPL PPLA PPLA2 PPLA3 PPLA4 PPLA5 PPLC
|
|
89
|
+
PPLCH PPLF PPLG PPLL PPLR PPLS STLMT PPLX);
|
|
90
|
+
my $i = 0;
|
|
91
|
+
my %featureCodes = map { lc($_) => $i++ } @featureCodes;
|
|
48
92
|
|
|
49
93
|
# get path name for database file from lib/Image/ExifTool/Geolocation.dat by default,
|
|
50
|
-
# or according to $Image::ExifTool::Geolocation::
|
|
51
|
-
my $
|
|
52
|
-
|
|
53
|
-
$
|
|
54
|
-
|
|
55
|
-
$
|
|
94
|
+
# or according to $Image::ExifTool::Geolocation::directory if specified
|
|
95
|
+
my $defaultDir = $INC{'Image/ExifTool/Geolocation.pm'};
|
|
96
|
+
if ($defaultDir) {
|
|
97
|
+
$defaultDir =~ s(/Geolocation\.pm$)();
|
|
98
|
+
} else {
|
|
99
|
+
$defaultDir = '.';
|
|
100
|
+
warn("Error getting Geolocation.pm directory\n");
|
|
56
101
|
}
|
|
57
102
|
|
|
58
|
-
#
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
warn("Bad format Geolocation database\n");
|
|
64
|
-
close(DATFILE);
|
|
65
|
-
return 0;
|
|
103
|
+
# read the Geolocation database unless $geoDir set to empty string
|
|
104
|
+
unless (defined $geoDir and not $geoDir) {
|
|
105
|
+
unless ($geoDir and ReadDatabase("$geoDir/Geolocation.dat")) {
|
|
106
|
+
ReadDatabase("$defaultDir/Geolocation.dat");
|
|
107
|
+
}
|
|
66
108
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
$
|
|
72
|
-
|
|
73
|
-
$
|
|
74
|
-
chomp $line;
|
|
75
|
-
push @cityLookup, $line;
|
|
109
|
+
|
|
110
|
+
# set directory for language files
|
|
111
|
+
my $geoLang;
|
|
112
|
+
if ($geoDir and -d "$geoDir/GeoLang") {
|
|
113
|
+
$geoLang = "$geoDir/GeoLang";
|
|
114
|
+
} elsif ($geoDir or not defined $geoDir) {
|
|
115
|
+
$geoLang = "$defaultDir/GeoLang";
|
|
76
116
|
}
|
|
77
|
-
|
|
78
|
-
#
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
last if length($line) == 6 and $line =~ /\0\0\0\0/;
|
|
82
|
-
chomp $line;
|
|
83
|
-
$countryLookup{substr($line,0,2)} = substr($line,2);
|
|
117
|
+
|
|
118
|
+
# add user-defined entries to the database
|
|
119
|
+
if (@Image::ExifTool::UserDefined::Geolocation) {
|
|
120
|
+
AddEntry(@$_) foreach @Image::ExifTool::UserDefined::Geolocation;
|
|
84
121
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
122
|
+
|
|
123
|
+
#------------------------------------------------------------------------------
|
|
124
|
+
# Read Geolocation database
|
|
125
|
+
# Inputs: 0) database file name
|
|
126
|
+
# Returns: true on success
|
|
127
|
+
sub ReadDatabase($)
|
|
128
|
+
{
|
|
129
|
+
my $datfile = shift;
|
|
130
|
+
# open geolocation database and verify header
|
|
131
|
+
open DATFILE, "< $datfile" or warn("Error reading $datfile\n"), return 0;
|
|
132
|
+
binmode DATFILE;
|
|
133
|
+
my $line = <DATFILE>;
|
|
134
|
+
unless ($line =~ /^Geolocation(\d+\.\d+)\t(\d+)/) {
|
|
135
|
+
warn("Bad format Geolocation database\n");
|
|
136
|
+
close(DATFILE);
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
($dbVer, $nCity) = ($1, $2);
|
|
140
|
+
if ($dbVer !~ /^1\.0[23]$/) {
|
|
141
|
+
my $which = $dbVer < 1.03 ? 'database' : 'ExifTool';
|
|
142
|
+
warn("Incompatible Geolocation database (update your $which)\n");
|
|
143
|
+
close(DATFILE);
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
my $comment = <DATFILE>;
|
|
147
|
+
defined $comment and $comment =~ /(\d+)/ or close(DATFILE), return 0;
|
|
148
|
+
$dbInfo = "$datfile v$dbVer: $nCity cities with population > $1";
|
|
149
|
+
my $isUserDefined = @Image::ExifTool::UserDefined::Geolocation;
|
|
150
|
+
|
|
151
|
+
undef @altNames; # reset altNames
|
|
152
|
+
|
|
153
|
+
# read city database
|
|
154
|
+
undef @cityList;
|
|
155
|
+
my $i = 0;
|
|
156
|
+
for (;;) {
|
|
157
|
+
$line = <DATFILE>;
|
|
158
|
+
last if length($line) == 6 and $line =~ /\0\0\0\0/;
|
|
159
|
+
$line .= <DATFILE> while length($line) < 14;
|
|
160
|
+
chomp $line;
|
|
161
|
+
push @cityList, $line;
|
|
162
|
+
}
|
|
163
|
+
@cityList == $nCity or warn("Bad number of entries in Geolocation database\n"), return 0;
|
|
164
|
+
# read countries
|
|
165
|
+
for (;;) {
|
|
166
|
+
$line = <DATFILE>;
|
|
167
|
+
last if length($line) == 6 and $line =~ /\0\0\0\0/;
|
|
168
|
+
chomp $line;
|
|
169
|
+
push @countryList, $line;
|
|
170
|
+
$countryNum{lc substr($line,0,2)} = $#countryList if $isUserDefined;
|
|
171
|
+
}
|
|
172
|
+
# read regions
|
|
173
|
+
for (;;) {
|
|
174
|
+
$line = <DATFILE>;
|
|
175
|
+
last if length($line) == 6 and $line =~ /\0\0\0\0/;
|
|
176
|
+
chomp $line;
|
|
177
|
+
push @regionList, $line;
|
|
178
|
+
$regionNum{lc $line} = $#regionList if $isUserDefined;
|
|
179
|
+
}
|
|
180
|
+
# read subregions
|
|
181
|
+
for (;;) {
|
|
182
|
+
$line = <DATFILE>;
|
|
183
|
+
last if length($line) == 6 and $line =~ /\0\0\0\0/;
|
|
184
|
+
chomp $line;
|
|
185
|
+
push @subregionList, $line;
|
|
186
|
+
$subregionNum{lc $line} = $#subregionList if $isUserDefined;
|
|
187
|
+
}
|
|
188
|
+
# read time zones
|
|
189
|
+
for (;;) {
|
|
190
|
+
$line = <DATFILE>;
|
|
191
|
+
last if length($line) == 6 and $line =~ /\0\0\0\0/;
|
|
192
|
+
chomp $line;
|
|
193
|
+
push @timezoneList, $line;
|
|
194
|
+
$timezoneNum{lc $line} = $#timezoneList if $isUserDefined;
|
|
195
|
+
}
|
|
196
|
+
close DATFILE;
|
|
197
|
+
return 1;
|
|
92
198
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
199
|
+
|
|
200
|
+
#------------------------------------------------------------------------------
|
|
201
|
+
# Read alternate-names database
|
|
202
|
+
# Returns: True on success
|
|
203
|
+
# Notes: Must be called after ReadDatabase(). Resets $altDir on exit.
|
|
204
|
+
sub ReadAltNames()
|
|
205
|
+
{
|
|
206
|
+
my $success;
|
|
207
|
+
if ($altDir and $nCity) {
|
|
208
|
+
if (open ALTFILE, "< $altDir/AltNames.dat") {
|
|
209
|
+
binmode ALTFILE;
|
|
210
|
+
local $/ = "\0";
|
|
211
|
+
my $i = 0;
|
|
212
|
+
while (<ALTFILE>) { chop; $altNames[$i++] = $_; }
|
|
213
|
+
close ALTFILE;
|
|
214
|
+
if ($i == $nCity) {
|
|
215
|
+
$success = 1;
|
|
216
|
+
} else {
|
|
217
|
+
warn("Bad number of entries in AltNames database\n");
|
|
218
|
+
undef @altNames;
|
|
219
|
+
}
|
|
220
|
+
} else {
|
|
221
|
+
warn "Error reading $altDir/AltNames.dat\n";
|
|
222
|
+
}
|
|
223
|
+
undef $altDir;
|
|
224
|
+
}
|
|
225
|
+
return $success;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
#------------------------------------------------------------------------------
|
|
229
|
+
# Clear last city matches cache
|
|
230
|
+
sub ClearLastMatches()
|
|
231
|
+
{
|
|
232
|
+
undef $lastArgs;
|
|
233
|
+
undef %lastFound;
|
|
234
|
+
undef @lastByPop;
|
|
235
|
+
undef @lastByLat;
|
|
99
236
|
}
|
|
100
|
-
close DATFILE;
|
|
101
237
|
|
|
102
238
|
#------------------------------------------------------------------------------
|
|
103
|
-
#
|
|
104
|
-
# Inputs: 0)
|
|
105
|
-
#
|
|
106
|
-
|
|
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($$;$$)
|
|
239
|
+
# Sort database by specified field
|
|
240
|
+
# Inputs: 0) Field name to sort (Latitude,City,Country)
|
|
241
|
+
# Returns: 1 on success
|
|
242
|
+
sub SortDatabase($)
|
|
113
243
|
{
|
|
114
|
-
my
|
|
115
|
-
|
|
116
|
-
|
|
244
|
+
my $field = shift;
|
|
245
|
+
return 1 if $field eq $sortedBy; # already sorted?
|
|
246
|
+
undef @sortOrder;
|
|
247
|
+
if ($field eq 'Latitude') {
|
|
248
|
+
@sortOrder = sort { $cityList[$a] cmp $cityList[$b] } 0..$#cityList;
|
|
249
|
+
} elsif ($field eq 'City') {
|
|
250
|
+
@sortOrder = sort { substr($cityList[$a],13) cmp substr($cityList[$b],13) } 0..$#cityList;
|
|
251
|
+
} elsif ($field eq 'Country') {
|
|
252
|
+
my %lkup;
|
|
253
|
+
foreach (0..$#cityList) {
|
|
254
|
+
my $city = substr($cityList[$_],13);
|
|
255
|
+
my $ctry = substr($countryList[ord substr($cityList[$_],5,1)], 2);
|
|
256
|
+
$lkup{$_} = "$ctry $city";
|
|
257
|
+
}
|
|
258
|
+
@sortOrder = sort { $lkup{$a} cmp $lkup{$b} } 0..$#cityList;
|
|
259
|
+
} else {
|
|
260
|
+
return 0;
|
|
261
|
+
}
|
|
262
|
+
$sortedBy = $field;
|
|
263
|
+
ClearLastMatches();
|
|
264
|
+
return 1;
|
|
265
|
+
}
|
|
117
266
|
|
|
267
|
+
#------------------------------------------------------------------------------
|
|
268
|
+
# Add cities to the Geolocation database
|
|
269
|
+
# Inputs: 0-8) city,region,subregion,country_code,country,timezone,feature_code,population,lat,lon,altNames
|
|
270
|
+
# eg. AddEntry('Sinemorets','Burgas','Obshtina Tsarevo','BG','Bulgaria','Europe/Sofia','',400,42.06115,27.97833)
|
|
271
|
+
# Returns: true on success, otherwise issues warning
|
|
272
|
+
sub AddEntry(@)
|
|
273
|
+
{
|
|
274
|
+
my ($city, $region, $subregion, $cc, $country, $timezone, $fc, $pop, $lat, $lon, $altNames) = @_;
|
|
275
|
+
@_ < 10 and warn("Too few arguments in $city definition (check for updated format)\n"), return 0;
|
|
276
|
+
length($cc) != 2 and warn("Country code '${cc}' is not 2 characters\n"), return 0;
|
|
277
|
+
$fc = $featureCodes{lc $fc} || 0;
|
|
278
|
+
chomp $lon; # (just in case it was read from file)
|
|
279
|
+
# create reverse lookups for country/region/subregion/timezone if not done already
|
|
280
|
+
# (eg. if the entries are being added manually instead of via UserDefined::Geolocation)
|
|
281
|
+
unless (%countryNum) {
|
|
282
|
+
my $i;
|
|
283
|
+
$i = 0; $countryNum{lc substr($_,0,2)} = $i++ foreach @countryList;
|
|
284
|
+
$i = 0; $regionNum{lc $_} = $i++ foreach @regionList;
|
|
285
|
+
$i = 0; $subregionNum{lc $_} = $i++ foreach @subregionList;
|
|
286
|
+
$i = 0; $timezoneNum{lc $_} = $i++ foreach @timezoneList;
|
|
287
|
+
}
|
|
288
|
+
my $cn = $countryNum{lc $cc};
|
|
289
|
+
unless (defined $cn) {
|
|
290
|
+
$#countryList >= 0xff and warn("AddEntry: Too many countries\n"), return 0;
|
|
291
|
+
push @countryList, "$cc$country";
|
|
292
|
+
$cn = $countryNum{lc $cc} = $#countryList;
|
|
293
|
+
} elsif ($country) {
|
|
294
|
+
$countryList[$cn] = "$cc$country"; # (override existing country name)
|
|
295
|
+
}
|
|
296
|
+
my $tn = $timezoneNum{lc $timezone};
|
|
297
|
+
unless (defined $tn) {
|
|
298
|
+
$#timezoneList >= 0x1ff and warn("AddEntry: Too many time zones\n"), return 0;
|
|
299
|
+
push @timezoneList, $timezone;
|
|
300
|
+
$tn = $timezoneNum{lc $timezone} = $#timezoneList;
|
|
301
|
+
}
|
|
302
|
+
my $rn = $regionNum{lc $region};
|
|
303
|
+
unless (defined $rn) {
|
|
304
|
+
$#regionList >= 0xfff and warn("AddEntry: Too many regions\n"), return 0;
|
|
305
|
+
push @regionList, $region;
|
|
306
|
+
$rn = $regionNum{lc $region} = $#regionList;
|
|
307
|
+
}
|
|
308
|
+
my $sn = $subregionNum{lc $subregion};
|
|
309
|
+
unless (defined $sn) {
|
|
310
|
+
my $max = $dbVer eq '1.02' ? 0x0fff : 0xffff;
|
|
311
|
+
$#subregionList >= $max and warn("AddEntry: Too many subregions\n"), return 0;
|
|
312
|
+
push @subregionList, $subregion;
|
|
313
|
+
$sn = $subregionNum{lc $subregion} = $#subregionList;
|
|
314
|
+
}
|
|
315
|
+
$pop = sprintf('%.1e',$pop); # format: "3.1e+04" or "3.1e+004"
|
|
316
|
+
# pack CC index, population and region index into a 32-bit integer
|
|
317
|
+
my $code = ($cn << 24) | (substr($pop,-1,1)<<20) | (substr($pop,0,1)<<16) | (substr($pop,2,1)<<12) | $rn;
|
|
318
|
+
# store high bit of timezone index
|
|
319
|
+
if ($tn > 255) {
|
|
320
|
+
if ($dbVer eq '1.02') {
|
|
321
|
+
$sn |= 0x8000;
|
|
322
|
+
} else {
|
|
323
|
+
$fc |= 0x80;
|
|
324
|
+
}
|
|
325
|
+
$tn -= 256;
|
|
326
|
+
}
|
|
327
|
+
$lat = int(($lat + 90) / 180 * 0x100000 + 0.5) & 0xfffff;
|
|
328
|
+
$lon = int(($lon + 180) / 360 * 0x100000 + 0.5) & 0xfffff;
|
|
329
|
+
my $hdr = pack('nCnNnCC', $lat>>4, (($lat&0x0f)<<4)|($lon&0x0f), $lon>>4, $code, $sn, $tn, $fc);
|
|
330
|
+
push @cityList, "$hdr$city";
|
|
331
|
+
# add altNames entry if provided
|
|
332
|
+
if ($altNames) {
|
|
333
|
+
chomp $altNames; # (just in case)
|
|
334
|
+
$altNames =~ tr/,/\n/;
|
|
335
|
+
# add any more arguments in case altNames were passed separately (undocumented)
|
|
336
|
+
foreach (11..$#_) {
|
|
337
|
+
chomp $_[$_];
|
|
338
|
+
$altNames .= "\n$_[$_]";
|
|
339
|
+
}
|
|
340
|
+
$altNames[$#cityList] = $altNames;
|
|
341
|
+
}
|
|
342
|
+
$sortedBy = '';
|
|
343
|
+
undef $lastArgs; # (faster than ClearLastArgs)
|
|
344
|
+
return 1;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
#------------------------------------------------------------------------------
|
|
348
|
+
# Unpack entry in database
|
|
349
|
+
# Inputs: 0) entry number or index into sorted database,
|
|
350
|
+
# 1) optional language code, 2) flag to use index into sorted database
|
|
351
|
+
# Returns: 0-10) city,region,subregion,country_code,country,timezone,
|
|
352
|
+
# feature_code,pop,lat,lon,altNames
|
|
353
|
+
sub GetEntry($;$$)
|
|
354
|
+
{
|
|
355
|
+
my ($entryNum, $lang, $sort) = @_;
|
|
356
|
+
return() if $entryNum > $#cityList;
|
|
357
|
+
$entryNum = $sortOrder[$entryNum] if $sort and @sortOrder > $entryNum;
|
|
358
|
+
my ($lt,$f,$ln,$code,$sn,$tn,$fc) = unpack('nCnNnCC', $cityList[$entryNum]);
|
|
359
|
+
my $city = substr($cityList[$entryNum],13);
|
|
360
|
+
my $ctry = $countryList[$code >> 24];
|
|
361
|
+
my $rgn = $regionList[$code & 0x0fff];
|
|
362
|
+
if ($dbVer eq '1.02') {
|
|
363
|
+
$sn & 0x8000 and $tn += 256, $sn &= 0x7fff;
|
|
364
|
+
} else {
|
|
365
|
+
$fc & 0x80 and $tn += 256;
|
|
366
|
+
}
|
|
367
|
+
my $sub = $subregionList[$sn];
|
|
368
|
+
# convert population digits back into exponent format
|
|
369
|
+
my $pop = (($code>>16 & 0x0f) . '.' . ($code>>12 & 0x0f) . 'e+' . ($code>>20 & 0x0f)) + 0;
|
|
370
|
+
$lt = sprintf('%.4f', (($lt<<4)|($f >> 4)) * 180 / 0x100000 - 90);
|
|
371
|
+
$ln = sprintf('%.4f', (($ln<<4)|($f & 0x0f))* 360 / 0x100000 - 180);
|
|
372
|
+
$fc = $featureCodes[$fc & 0x0f];
|
|
373
|
+
my $cc = substr($ctry, 0, 2);
|
|
374
|
+
my $country = substr($ctry, 2);
|
|
375
|
+
if ($lang) {
|
|
376
|
+
my $xlat = $langLookup{$lang};
|
|
377
|
+
# load language lookups if not done already
|
|
378
|
+
if (not defined $xlat) {
|
|
379
|
+
if (eval "require '$geoLang/$lang.pm'") {
|
|
380
|
+
my $trans = "Image::ExifTool::GeoLang::${lang}::Translate";
|
|
381
|
+
no strict 'refs';
|
|
382
|
+
$xlat = \%$trans if %$trans;
|
|
383
|
+
}
|
|
384
|
+
# read user-defined language translations
|
|
385
|
+
if (%Image::ExifTool::Geolocation::geoLang) {
|
|
386
|
+
my $userLang = $Image::ExifTool::Geolocation::geoLang{$lang};
|
|
387
|
+
if ($userLang and ref($userLang) eq 'HASH') {
|
|
388
|
+
if ($xlat) {
|
|
389
|
+
# add user-defined entries to main lookup
|
|
390
|
+
$$xlat{$_} = $$userLang{$_} foreach keys %$userLang;
|
|
391
|
+
} else {
|
|
392
|
+
$xlat = $userLang;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
$langLookup{$lang} = $xlat || 0;
|
|
397
|
+
}
|
|
398
|
+
if ($xlat) {
|
|
399
|
+
my $r2 = $rgn;
|
|
400
|
+
# City-specific: "CCRgn,Sub,City", "CCRgn,City", "CC,City", ",City"
|
|
401
|
+
# Subregion-specific: "CCRgn,Sub,"
|
|
402
|
+
# Region-specific: "CCRgn,"
|
|
403
|
+
# Country-specific: "CC,"
|
|
404
|
+
$city = $$xlat{"$cc$r2,$sub,$city"} || $$xlat{"$cc$r2,$city"} ||
|
|
405
|
+
$$xlat{"$cc,$city"} || $$xlat{",$city"} || $$xlat{$city} || $city;
|
|
406
|
+
$sub = $$xlat{"$cc$rgn,$sub,"} || $$xlat{$sub} || $sub;
|
|
407
|
+
$rgn = $$xlat{"$cc$rgn,"} || $$xlat{$rgn} || $rgn;
|
|
408
|
+
$country = $$xlat{"$cc,"} || $$xlat{$country} || $country;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return($city,$rgn,$sub,$cc,$country,$timezoneList[$tn],$fc,$pop,$lt,$ln);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
#------------------------------------------------------------------------------
|
|
415
|
+
# Get alternate names for specified database entry
|
|
416
|
+
# Inputs: 0) entry number or index into sorted database, 1) sort flag
|
|
417
|
+
# Returns: comma-separated list of alternate names, or empty string if no names
|
|
418
|
+
# Notes: ReadAltNames() must be called before this
|
|
419
|
+
sub GetAltNames($;$)
|
|
420
|
+
{
|
|
421
|
+
my ($entryNum, $sort) = @_;
|
|
422
|
+
$entryNum = $sortOrder[$entryNum] if $sort and @sortOrder > $entryNum;
|
|
423
|
+
my $alt = $altNames[$entryNum] or return '';
|
|
424
|
+
$alt =~ tr/\n/,/;
|
|
425
|
+
return $alt;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
#------------------------------------------------------------------------------
|
|
429
|
+
# Look up lat,lon or city in geolocation database
|
|
430
|
+
# Inputs: 0) "lat,lon", "city,region,country", etc, (city must be first)
|
|
431
|
+
# 1) options hash reference (or undef for no options)
|
|
432
|
+
# Options: GeolocMinPop, GeolocMaxDist, GeolocMulti, GeolocFeature, GeolocAltNames
|
|
433
|
+
# Returns: 0) number of matching cities (0 if no matches),
|
|
434
|
+
# 1) index of matching city in database, or undef if no matches, or
|
|
435
|
+
# reference to list of indices if multiple matches were found and
|
|
436
|
+
# the flag to return multiple matches was set,
|
|
437
|
+
# 2) approx distance (km), 3) compass bearing to city
|
|
438
|
+
sub Geolocate($;$$$$$)
|
|
439
|
+
{
|
|
440
|
+
my ($arg, $opts) = @_;
|
|
441
|
+
my ($city, @exact, %regex, @multiCity, $other, $idx, @cargs, $useLastFound);
|
|
442
|
+
my ($minPop, $minDistU, $minDistC, @matchParms, @coords, $fcmask, $both);
|
|
443
|
+
my ($pop, $maxDist, $multi, $fcodes, $altNames, @startTime);
|
|
444
|
+
|
|
445
|
+
$opts and ($pop, $maxDist, $multi, $fcodes, $altNames) =
|
|
446
|
+
@$opts{qw(GeolocMinPop GeolocMaxDist GeolocMulti GeolocFeature GeolocAltNames)};
|
|
447
|
+
|
|
448
|
+
if ($debug) {
|
|
449
|
+
require Time::HiRes;
|
|
450
|
+
@startTime = Time::HiRes::gettimeofday();
|
|
451
|
+
}
|
|
452
|
+
@cityList or warn('No Geolocation database'), return 0;
|
|
453
|
+
# make population code for comparing with 2 bytes at offset 6 in database
|
|
118
454
|
if ($pop) {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
455
|
+
$pop = sprintf('%.1e', $pop);
|
|
456
|
+
$minPop = chr((substr($pop,-1,1)<<4) | (substr($pop,0,1))) . chr(substr($pop,2,1)<<4);
|
|
457
|
+
}
|
|
458
|
+
if ($fcodes) {
|
|
459
|
+
my $neg = $fcodes =~ s/^-//;
|
|
460
|
+
my @fcodes = split /\s*,\s*/, $fcodes;
|
|
461
|
+
if ($neg) {
|
|
462
|
+
$fcmask = 0xffff;
|
|
463
|
+
defined $featureCodes{lc $_} and $fcmask &= ~((1 << $featureCodes{lc $_})) foreach @fcodes;
|
|
464
|
+
} else {
|
|
465
|
+
defined $featureCodes{lc $_} and $fcmask |= (1 << $featureCodes{lc $_}) foreach @fcodes;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
#
|
|
469
|
+
# process input argument
|
|
470
|
+
#
|
|
471
|
+
$arg =~ s/^\s+//; $arg =~ s/\s+$//; # remove leading/trailing spaces
|
|
472
|
+
my @args = split /\s*,\s*/, $arg;
|
|
473
|
+
my %ri = ( cc => 0, co => 1, re => 2, sr => 3, ci => 8, '' => 9 );
|
|
474
|
+
foreach (@args) {
|
|
475
|
+
# allow regular expressions optionally prefixed by "ci", "cc", "co", "re" or "sr"
|
|
476
|
+
if (m{^(-)?(\w{2})?/(.*)/(i?)$}) {
|
|
477
|
+
my $re = $4 ? qr/$3/im : qr/$3/m;
|
|
478
|
+
next if not defined($idx = $ri{$2});
|
|
479
|
+
push @cargs, $_;
|
|
480
|
+
$other = 1 if $idx < 5;
|
|
481
|
+
$idx += 10 if $1; # add 10 for negative matches
|
|
482
|
+
$regex{$idx} or $regex{$idx} = [ ];
|
|
483
|
+
push @{$regex{$idx}}, $re;
|
|
484
|
+
$city = '' unless defined $city;
|
|
485
|
+
} elsif (/^[-+]?\d+(\.\d+)?$/) { # coordinate format
|
|
486
|
+
push @coords, $_ if @coords < 2;
|
|
487
|
+
} elsif (lc $_ eq 'both') {
|
|
488
|
+
$both = 1;
|
|
489
|
+
} elsif ($_) {
|
|
490
|
+
push @cargs, $_;
|
|
491
|
+
if ($city) {
|
|
492
|
+
push @exact, lc $_;
|
|
493
|
+
} else {
|
|
494
|
+
$city = lc $_;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
unless (defined $city or @coords == 2) {
|
|
499
|
+
warn("Insufficient information to determine geolocation\n");
|
|
500
|
+
return 0;
|
|
501
|
+
}
|
|
502
|
+
# sort database by logitude if finding entry based on coordinates
|
|
503
|
+
SortDatabase('Latitude') if @coords == 2 and ($both or not defined $city);
|
|
504
|
+
#
|
|
505
|
+
# perform reverse Geolocation lookup to determine GPS based on city, country, etc.
|
|
506
|
+
#
|
|
507
|
+
while (defined $city and (@coords != 2 or $both)) {
|
|
508
|
+
my $cargs = join(',', @cargs, $pop||'', $maxDist||'', $fcodes||'');
|
|
509
|
+
my $i = 0;
|
|
510
|
+
if ($lastArgs and $lastArgs eq $cargs) {
|
|
511
|
+
$i = @cityList; # bypass search
|
|
512
|
+
} else {
|
|
513
|
+
ClearLastMatches();
|
|
514
|
+
$lastArgs = $cargs;
|
|
125
515
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
516
|
+
# read alternate names database if an exact city match is specified
|
|
517
|
+
if ($altNames) {
|
|
518
|
+
ReadAltNames() if $city and $altDir;
|
|
519
|
+
$altNames = \@altNames;
|
|
520
|
+
} else {
|
|
521
|
+
$altNames = [ ]; # (don't search alt names)
|
|
522
|
+
}
|
|
523
|
+
Entry: for (; $i<@cityList; ++$i) {
|
|
524
|
+
my $cty = substr($cityList[$i],13);
|
|
525
|
+
if ($city and $city ne lc $cty) { # test exact city name first
|
|
526
|
+
next unless $$altNames[$i] and $$altNames[$i] =~ /^$city$/im;
|
|
527
|
+
}
|
|
528
|
+
# test with city-specific regexes
|
|
529
|
+
if ($regex{8}) { $cty =~ $_ or next Entry foreach @{$regex{8}} }
|
|
530
|
+
if ($regex{18}) { $cty !~ $_ or next Entry foreach @{$regex{18}} }
|
|
531
|
+
# test other arguments
|
|
532
|
+
my ($cd,$sn) = unpack('x5Nn', $cityList[$i]);
|
|
533
|
+
my $ct = $countryList[$cd >> 24];
|
|
534
|
+
$sn &= 0x7fff if $dbVer eq '1.02';
|
|
535
|
+
my @geo = (substr($ct,0,2), substr($ct,2), $regionList[$cd & 0x0fff], $subregionList[$sn]);
|
|
536
|
+
if (@exact) {
|
|
537
|
+
# make quick lookup for all names at this location
|
|
538
|
+
my %geoLkup;
|
|
539
|
+
$_ and $geoLkup{lc $_} = 1 foreach @geo;
|
|
540
|
+
$geoLkup{$_} or next Entry foreach @exact;
|
|
541
|
+
}
|
|
542
|
+
# test with cc, co, re and sr regexes
|
|
543
|
+
if ($other) { foreach $idx (0..3) {
|
|
544
|
+
if ($regex{$idx}) { $geo[$idx] =~ $_ or next Entry foreach @{$regex{$idx}} }
|
|
545
|
+
if ($regex{$idx+10}) { $geo[$idx] !~ $_ or next Entry foreach @{$regex{$idx+10}} }
|
|
546
|
+
} }
|
|
547
|
+
# test regexes for any place name
|
|
548
|
+
if ($regex{9} or $regex{19}) {
|
|
549
|
+
my $str = join "\n", $cty, @geo;
|
|
550
|
+
$str =~ $_ or next Entry foreach @{$regex{9}};
|
|
551
|
+
$str !~ $_ or next Entry foreach @{$regex{19}};
|
|
552
|
+
}
|
|
553
|
+
# test feature code and population
|
|
554
|
+
next if $fcmask and not $fcmask & (1 << (ord(substr($cityList[$i],12,1)) & 0x0f));
|
|
555
|
+
my $pc = substr($cityList[$i],6,2);
|
|
556
|
+
if (not defined $minPop or $pc ge $minPop) {
|
|
557
|
+
$lastFound{$i} = $pc;
|
|
558
|
+
push @lastByLat, $i if @coords == 2;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
@startTime and printf("= Processing time: %.3f sec\n", Time::HiRes::tv_interval(\@startTime));
|
|
562
|
+
if (%lastFound) {
|
|
563
|
+
@coords == 2 and $useLastFound = 1, last; # continue to use coords with last city matches
|
|
564
|
+
scalar(keys %lastFound) > 200 and warn("Too many matching cities\n"), return 0;
|
|
565
|
+
unless (@lastByPop) {
|
|
566
|
+
@lastByPop = sort { $lastFound{$b} cmp $lastFound{$a} or $cityList[$a] cmp $cityList[$b] } keys %lastFound;
|
|
567
|
+
}
|
|
568
|
+
my $n = scalar @lastByPop;
|
|
569
|
+
return($n, [ @lastByPop ]) if $n > 1 and $multi;
|
|
570
|
+
return($n, $lastByPop[0]);
|
|
571
|
+
}
|
|
572
|
+
warn "No such city in Geolocation database\n";
|
|
573
|
+
return 0;
|
|
574
|
+
}
|
|
575
|
+
#
|
|
576
|
+
# determine Geolocation based on GPS coordinates
|
|
577
|
+
#
|
|
578
|
+
my ($lat, $lon) = @coords;
|
|
579
|
+
if ($maxDist) {
|
|
580
|
+
$minDistU = $maxDist / (2 * $earthRadius); # min distance on unit sphere
|
|
581
|
+
$minDistC = $maxDist * 0x100000 / ($pi * $earthRadius); # min distance in coordinate units
|
|
582
|
+
} else {
|
|
583
|
+
$minDistU = $pi;
|
|
584
|
+
$minDistC = 0x200000;
|
|
585
|
+
}
|
|
586
|
+
my $cos = cos($lat * $pi / 180); # cosine factor for longitude distances
|
|
587
|
+
# reduce lat/lon to the range 0-0x100000
|
|
588
|
+
$lat = int(($lat + 90) / 180 * 0x100000 + 0.5) & 0xfffff;
|
|
589
|
+
$lon = int(($lon + 180) / 360 * 0x100000 + 0.5) & 0xfffff;
|
|
590
|
+
$lat or $lat = $coords[0] < 0 ? 1 : 0xfffff; # (zero latitude is a problem for our calculations)
|
|
591
|
+
my $coord = pack('nCn',$lat>>4,(($lat&0x0f)<<4)|($lon&0x0f),$lon>>4);;
|
|
592
|
+
# start from cached city matches if also using city information
|
|
593
|
+
my $numEntries = @lastByLat || @cityList;
|
|
138
594
|
# binary search to find closest longitude
|
|
139
|
-
my ($n0, $n1) = (0,
|
|
595
|
+
my ($n0, $n1) = (0, $numEntries - 1);
|
|
596
|
+
my $sorted = @lastByLat ? \@lastByLat : (@sortOrder ? \@sortOrder : undef);
|
|
140
597
|
while ($n1 - $n0 > 1) {
|
|
141
598
|
my $n = int(($n0 + $n1) / 2);
|
|
142
|
-
if ($coord lt $
|
|
599
|
+
if ($coord lt $cityList[$sorted ? $$sorted[$n] : $n]) {
|
|
143
600
|
$n1 = $n;
|
|
144
601
|
} else {
|
|
145
602
|
$n0 = $n;
|
|
146
603
|
}
|
|
147
604
|
}
|
|
148
605
|
# step backward then forward through database to find nearest city
|
|
149
|
-
my ($minDist2, $minN, @dxy);
|
|
150
606
|
my ($inc, $end, $n) = (-1, -1, $n0+1);
|
|
607
|
+
my ($p0, $t0) = ($lat*$pi/0x100000 - $pi/2, $lon*$pi/0x080000 - $pi);
|
|
608
|
+
my $cp0 = cos($p0);
|
|
151
609
|
for (;;) {
|
|
152
610
|
if (($n += $inc) == $end) {
|
|
153
611
|
last if $inc == 1;
|
|
154
|
-
($inc, $end, $n) = (1,
|
|
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
|
|
612
|
+
($inc, $end, $n) = (1, $numEntries, $n1);
|
|
169
613
|
}
|
|
614
|
+
my $i = $sorted ? $$sorted[$n] : $n;
|
|
615
|
+
# get city latitude/longitude
|
|
616
|
+
my ($lt,$f,$ln) = unpack('nCn', $cityList[$i]);
|
|
617
|
+
$lt = ($lt << 4) | ($f >> 4);
|
|
618
|
+
# searched far enough if latitude alone is further than best distance
|
|
619
|
+
abs($lt - $lat) > $minDistC and $n = $end - $inc, next;
|
|
170
620
|
# ignore if population is below threshold
|
|
171
|
-
next if $minPop and $minPop
|
|
172
|
-
if
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
621
|
+
next if defined $minPop and $minPop ge substr($cityList[$i],6,2);
|
|
622
|
+
next if $fcmask and not $fcmask & (1 << (ord(substr($cityList[$i],12,1)) & 0x0f));
|
|
623
|
+
$ln = ($ln << 4) | ($f & 0x0f);
|
|
624
|
+
# calculate great circle distance to this city on unit sphere
|
|
625
|
+
my ($p1, $t1) = ($lt*$pi/0x100000 - $pi/2, $ln*$pi/0x080000 - $pi);
|
|
626
|
+
my ($sp, $st) = (sin(($p1-$p0)/2), sin(($t1-$t0)/2));
|
|
627
|
+
my $a = $sp * $sp + $cp0 * cos($p1) * $st * $st;
|
|
628
|
+
my $distU = atan2(sqrt($a), sqrt(1-$a));
|
|
629
|
+
next if $distU > $minDistU;
|
|
630
|
+
$minDistU = $distU;
|
|
631
|
+
$minDistC = $minDistU * 0x200000 / $pi;
|
|
632
|
+
@matchParms = ($i, $p1, $t1, $distU);
|
|
177
633
|
}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
my ($
|
|
182
|
-
my $
|
|
183
|
-
my $
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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);
|
|
634
|
+
@matchParms or warn("No suitable location in Geolocation database\n"), return 0;
|
|
635
|
+
|
|
636
|
+
# calculate distance in km and bearing to matching city
|
|
637
|
+
my ($ii, $p1, $t1, $distU) = @matchParms;
|
|
638
|
+
my $km = sprintf('%.2f', 2 * $earthRadius * $distU);
|
|
639
|
+
my $be = atan2(sin($t1-$t0)*cos($p1-$p0), $cp0*sin($p1)-sin($p0)*cos($p1)*cos($t1-$t0));
|
|
640
|
+
$be = int($be * 180 / $pi + 360.5) % 360; # convert from radians to integer degrees
|
|
641
|
+
|
|
642
|
+
@startTime and printf("- Processing time: %.3f sec\n", Time::HiRes::tv_interval(\@startTime));
|
|
643
|
+
return(1, $ii, $km, $be)
|
|
197
644
|
}
|
|
198
645
|
|
|
646
|
+
1; #end
|
|
647
|
+
|
|
199
648
|
__END__
|
|
200
649
|
|
|
201
650
|
=head1 NAME
|
|
202
651
|
|
|
203
|
-
Image::ExifTool::Geolocation -
|
|
652
|
+
Image::ExifTool::Geolocation - Determine geolocation from GPS and visa-versa
|
|
204
653
|
|
|
205
654
|
=head1 SYNOPSIS
|
|
206
655
|
|
|
207
|
-
|
|
656
|
+
Look up geolocation information (city, region, subregion, country, etc)
|
|
657
|
+
based on input GPS coordinates, or determine GPS coordinates based on city
|
|
658
|
+
name, etc.
|
|
208
659
|
|
|
209
660
|
=head1 DESCRIPTION
|
|
210
661
|
|
|
211
662
|
This module contains the code to convert GPS coordinates to city, region,
|
|
212
|
-
country, time zone, etc. It uses a database derived from
|
|
213
|
-
modified to reduce the size as much as possible.
|
|
663
|
+
subregion, country, time zone, etc. It uses a database derived from
|
|
664
|
+
geonames.org, modified to reduce the size as much as possible.
|
|
665
|
+
|
|
666
|
+
=head1 METHODS
|
|
667
|
+
|
|
668
|
+
=head2 ReadDatabase
|
|
669
|
+
|
|
670
|
+
Load Geolocation database from file. This method is called automatically
|
|
671
|
+
when this module is loaded. By default, the database is loaded from
|
|
672
|
+
"Geolocation.dat" in the same directory as this module, but a different
|
|
673
|
+
directory may be used by setting $Image::ExifTool::Geolocation::geoDir
|
|
674
|
+
before loading this module. Setting this to an empty string avoids loading
|
|
675
|
+
any database. A warning is generated if the file can't be read.
|
|
676
|
+
|
|
677
|
+
Image::ExifTool::Geolocation::ReadDatabase($filename);
|
|
678
|
+
|
|
679
|
+
=over 4
|
|
680
|
+
|
|
681
|
+
=item Inputs:
|
|
682
|
+
|
|
683
|
+
0) Database file name
|
|
684
|
+
|
|
685
|
+
=item Return Value:
|
|
686
|
+
|
|
687
|
+
True on success.
|
|
688
|
+
|
|
689
|
+
=back
|
|
690
|
+
|
|
691
|
+
=head2 ReadAltNames
|
|
692
|
+
|
|
693
|
+
Load the alternate names database. Before calling this method the $altDir
|
|
694
|
+
package variable must be set to a directory containing the AltNames.dat
|
|
695
|
+
database that matches the current Geolocation.dat. This method is called
|
|
696
|
+
automatically by L</Geolocate> if $altDir is set and the GeolocAltNames
|
|
697
|
+
option is used and an input city name is provided.
|
|
698
|
+
|
|
699
|
+
Image::ExifTool::Geolocation::ReadAltNames();
|
|
700
|
+
|
|
701
|
+
=over 4
|
|
702
|
+
|
|
703
|
+
=item Inputs:
|
|
704
|
+
|
|
705
|
+
(none)
|
|
706
|
+
|
|
707
|
+
=item Return Value:
|
|
708
|
+
|
|
709
|
+
True on success. Resets the value of $altDir to prevent further attempts at
|
|
710
|
+
re-loading the same database.
|
|
711
|
+
|
|
712
|
+
=back
|
|
713
|
+
|
|
714
|
+
=head2 SortDatabase
|
|
715
|
+
|
|
716
|
+
Sort database in specified order.
|
|
717
|
+
|
|
718
|
+
Image::ExifTool::Geolocation::ReadDatabase('City');
|
|
719
|
+
|
|
720
|
+
=over 4
|
|
721
|
+
|
|
722
|
+
=item Inputs:
|
|
723
|
+
|
|
724
|
+
0) Sort order: 'Latitude', 'City' or 'Country'
|
|
725
|
+
|
|
726
|
+
=item Return Value:
|
|
727
|
+
|
|
728
|
+
1 on success, 0 on failure (bad sort order specified).
|
|
729
|
+
|
|
730
|
+
=back
|
|
731
|
+
|
|
732
|
+
=head2 AddEntry
|
|
733
|
+
|
|
734
|
+
Add entry to Geolocation database.
|
|
735
|
+
|
|
736
|
+
Image::ExifTool::Geolocation::AddEntry($city, $region,
|
|
737
|
+
$subregion, $countryCode, $country, $timezone,
|
|
738
|
+
$featureCode, $population, $lat, $lon, $altNames);
|
|
739
|
+
|
|
740
|
+
=over 4
|
|
741
|
+
|
|
742
|
+
=item Inputs:
|
|
743
|
+
|
|
744
|
+
0) City name (UTF8)
|
|
745
|
+
|
|
746
|
+
1) Region, state or province name (UTF8), or empty string if unknown
|
|
747
|
+
|
|
748
|
+
2) Subregion name (UTF8), or empty string if unknown
|
|
749
|
+
|
|
750
|
+
3) 2-character ISO 3166 country code
|
|
751
|
+
|
|
752
|
+
4) Country name (UTF8), or empty string to use existing definition. If the
|
|
753
|
+
country name is provided for a country code that already exists in the
|
|
754
|
+
database, then the database entry is updated with the new country name.
|
|
755
|
+
|
|
756
|
+
5) Time zone identifier (eg. "America/New_York")
|
|
757
|
+
|
|
758
|
+
6) Feature code (eg. "PPL"), or empty if not known
|
|
759
|
+
|
|
760
|
+
7) City population
|
|
761
|
+
|
|
762
|
+
8) GPS latitude (signed floating point degrees)
|
|
763
|
+
|
|
764
|
+
9) GPS longitude
|
|
765
|
+
|
|
766
|
+
10) Optional comma-separated list of alternate names for the city
|
|
767
|
+
|
|
768
|
+
=item Return Value:
|
|
769
|
+
|
|
770
|
+
1 on success, otherwise sends a warning message to stderr
|
|
771
|
+
|
|
772
|
+
=back
|
|
773
|
+
|
|
774
|
+
=head2 GetEntry
|
|
775
|
+
|
|
776
|
+
Get entry from Geolocation database.
|
|
777
|
+
|
|
778
|
+
my @vals = Image::ExifTool::Geolocation::GetEntry($num,$lang,$sort);
|
|
779
|
+
|
|
780
|
+
=over 4
|
|
781
|
+
|
|
782
|
+
=item Inputs:
|
|
783
|
+
|
|
784
|
+
0) Entry number in database, or index into sorted database
|
|
785
|
+
|
|
786
|
+
1) Optional language code
|
|
787
|
+
|
|
788
|
+
2) Optional flag to treat first argument as an index into the sorted
|
|
789
|
+
database
|
|
790
|
+
|
|
791
|
+
item Return Values:
|
|
792
|
+
|
|
793
|
+
0) City name, or undef if the entry didn't exist
|
|
794
|
+
|
|
795
|
+
1) Region name, or "" if no region
|
|
796
|
+
|
|
797
|
+
2) Subregion name, or "" if no subregion
|
|
798
|
+
|
|
799
|
+
3) Country code
|
|
800
|
+
|
|
801
|
+
4) Country name
|
|
802
|
+
|
|
803
|
+
5) Time zone
|
|
804
|
+
|
|
805
|
+
6) Feature code
|
|
806
|
+
|
|
807
|
+
7) City population
|
|
808
|
+
|
|
809
|
+
8) GPS latitude
|
|
810
|
+
|
|
811
|
+
9) GPS longitude
|
|
812
|
+
|
|
813
|
+
=back
|
|
814
|
+
|
|
815
|
+
=head2 GetAltNames
|
|
816
|
+
|
|
817
|
+
Get alternate names for specified city.
|
|
818
|
+
|
|
819
|
+
my $str = Image::ExifTool::Geolocation::GetAltNames($num,$sort);
|
|
820
|
+
|
|
821
|
+
=over 4
|
|
822
|
+
|
|
823
|
+
=item Inputs:
|
|
824
|
+
|
|
825
|
+
0) Entry number in database or index into the sorted database
|
|
826
|
+
|
|
827
|
+
1) Optional flag to treat first argument as an index into the sorted
|
|
828
|
+
database
|
|
829
|
+
|
|
830
|
+
=item Return value:
|
|
831
|
+
|
|
832
|
+
Comma-separated string of alternate names for this city.
|
|
833
|
+
|
|
834
|
+
=item Notes:
|
|
835
|
+
|
|
836
|
+
Must set the $altDir package variable and call L</ReadAltNames> before
|
|
837
|
+
calling this routine.
|
|
838
|
+
|
|
839
|
+
=back
|
|
840
|
+
|
|
841
|
+
=head2 Geolocate
|
|
842
|
+
|
|
843
|
+
Return geolocation information for specified GPS coordinates or city name.
|
|
844
|
+
|
|
845
|
+
my @rtnInfo = Image::ExifTool::Geolocation::Geolocate($arg,$opts);
|
|
846
|
+
|
|
847
|
+
=over 4
|
|
848
|
+
|
|
849
|
+
=item Inputs:
|
|
850
|
+
|
|
851
|
+
0) Input argument ("lat,lon", "city", "city,country", "city,region,country",
|
|
852
|
+
etc). When specifying a city, the city name must come first, followed by
|
|
853
|
+
zero or more of the following in any order, separated by commas: region
|
|
854
|
+
name, subregion name, country code, and/or country name. Regular
|
|
855
|
+
expressions in C</expr/> format are also allowed, optionally prefixed by
|
|
856
|
+
"ci", "re", "sr", "cc" or "co" to specifically match City, Region,
|
|
857
|
+
Subregion, CountryCode or Country name. See
|
|
858
|
+
L<https://exiftool.org/geolocation.html#Read> for details.
|
|
859
|
+
|
|
860
|
+
1) Optional reference to hash of options:
|
|
861
|
+
|
|
862
|
+
GeolocMinPop - minimum population of cities to consider in search
|
|
863
|
+
|
|
864
|
+
GeolocMaxDist - maximum distance (km) to search for cities when an input
|
|
865
|
+
GPS position is used
|
|
866
|
+
|
|
867
|
+
GeolocMulti - flag to return multiple cities if there is more than one
|
|
868
|
+
match. In this case the return value is a list of city
|
|
869
|
+
information lists.
|
|
870
|
+
|
|
871
|
+
GeolocFeature - comma-separated list of feature codes to include in
|
|
872
|
+
search, or exclude if the list starts with a dash (-)
|
|
873
|
+
|
|
874
|
+
GeolocAltNames - flag to search alternate names database if available
|
|
875
|
+
for matching city name (see ALTERNATE DATABASES below)
|
|
876
|
+
|
|
877
|
+
=item Return Value:
|
|
878
|
+
|
|
879
|
+
0) Number of matching entries, or 0 if no matches
|
|
880
|
+
|
|
881
|
+
1) Entry number for matching city in database, or undef if no matches, or a
|
|
882
|
+
reference to a list of entry numbers of matching cities if multiple matches
|
|
883
|
+
were found and the flag was set to return multiple matches
|
|
884
|
+
|
|
885
|
+
2) Distance to closest city in km if "lat,lon" specified
|
|
886
|
+
|
|
887
|
+
3) Compass bearing for direction to closest city if "lat,lon" specified
|
|
888
|
+
|
|
889
|
+
=back
|
|
890
|
+
|
|
891
|
+
=head1 ALTERNATE DATABASES
|
|
892
|
+
|
|
893
|
+
A different version of the cities database may be specified setting the
|
|
894
|
+
package $geoDir variable before loading this module. This directory should
|
|
895
|
+
contain the Geolocation.dat file, and optionally a GeoLang directory for the
|
|
896
|
+
language translations. The $geoDir variable may be set to an empty string
|
|
897
|
+
to disable loading of a database.
|
|
898
|
+
|
|
899
|
+
A database of alternate city names may be loaded by setting the package
|
|
900
|
+
$altDir variable. This directory should contain the AltNames.dat database
|
|
901
|
+
that matches the version of Geolocation.dat being used. When searching for
|
|
902
|
+
a city by name, the alternate-names database is checked to provide
|
|
903
|
+
additional possibilities for matches.
|
|
904
|
+
|
|
905
|
+
=head1 ADDING USER-DEFINED DATABASE ENTRIES
|
|
906
|
+
|
|
907
|
+
User-defined entries may be created by defining them using the following
|
|
908
|
+
technique before the Geolocation module is loaded.
|
|
909
|
+
|
|
910
|
+
@Image::ExifTool::UserDefined::Geolocation = (
|
|
911
|
+
# city, region, subregion, country code, country, timezone,
|
|
912
|
+
['Sinemorets','burgas','Obshtina Tsarevo','BG','','Europe/Sofia',
|
|
913
|
+
# feature code, population, lat, lon
|
|
914
|
+
'',400,42.06115,27.97833],
|
|
915
|
+
);
|
|
916
|
+
|
|
917
|
+
Similarly, user-defined language translations may be defined, and will
|
|
918
|
+
override any existing translations. Translations for the default 'en'
|
|
919
|
+
language may also be specified. See
|
|
920
|
+
L<https://exiftool.org/geolocation.html#Custom> for more information.
|
|
921
|
+
|
|
922
|
+
=head1 USING A CUSTOM DATABASE
|
|
923
|
+
|
|
924
|
+
This example shows how to use a custom database. In this example, the input
|
|
925
|
+
database file is a comma-separated text file with columns corresponding to
|
|
926
|
+
the input arguments of the AddEntry method.
|
|
927
|
+
|
|
928
|
+
$Image::ExifTool::Geolocation::geoDir = '';
|
|
929
|
+
require Image::ExifTool::Geolocation;
|
|
930
|
+
open DFILE, "< $filename";
|
|
931
|
+
Image::ExifTool::Geolocation::AddEntry(split /,/) foreach <DFILE>;
|
|
932
|
+
close DFILE;
|
|
214
933
|
|
|
215
934
|
=head1 AUTHOR
|
|
216
935
|
|
|
217
936
|
Copyright 2003-2024, Phil Harvey (philharvey66 at gmail.com)
|
|
218
937
|
|
|
219
938
|
This library is free software; you can redistribute it and/or modify it
|
|
220
|
-
under the same terms as Perl itself.
|
|
221
|
-
from geonames.org with a Creative Commons license.
|
|
939
|
+
under the same terms as Perl itself. The associated database files are
|
|
940
|
+
based on data from geonames.org with a Creative Commons license.
|
|
222
941
|
|
|
223
942
|
=head1 REFERENCES
|
|
224
943
|
|
|
@@ -226,6 +945,8 @@ from geonames.org with a Creative Commons license.
|
|
|
226
945
|
|
|
227
946
|
=item L<https://download.geonames.org/export/>
|
|
228
947
|
|
|
948
|
+
=item L<https://exiftool.org/geolocation.html>
|
|
949
|
+
|
|
229
950
|
=back
|
|
230
951
|
|
|
231
952
|
=head1 SEE ALSO
|