-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathmain.go
173 lines (142 loc) · 4.19 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
package main
import (
"bufio"
"encoding/xml"
"flag"
"fmt"
"github.com/rwcarlsen/goexif/exif"
"github.com/rwcarlsen/goexif/mknote"
"io"
"os"
"path"
"sort"
"strings"
"time"
)
var (
useAbsoluteFilenames = false
reverseGeocode = false
)
type Photo struct {
Filename string
Name string
Timestamp time.Time
Lon float64
Lat float64
}
type Photos []*Photo
func (ps Photos) Len() int {
return len(ps)
}
func (ps Photos) Less(i, j int) bool {
return ps[i].Timestamp.Before(ps[j].Timestamp)
}
func (ps Photos) Swap(i, j int) {
ps[i], ps[j] = ps[j], ps[i]
}
func Tell(format string, a ...interface{}) (n int, err error) {
return fmt.Fprintf(os.Stderr, format+"\n", a...)
}
func PlacemarkFilename(filename string) string {
if useAbsoluteFilenames {
return filename
}
return path.Base(filename)
}
func ReadPhotosFromList(reader *bufio.Reader) (photos Photos, err error) {
for {
filename, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
err = nil // not realy an error in this context
}
return photos, err
}
filename = strings.TrimRight(filename, "\n")
if filename == "" {
continue // skip empty lines
}
photo := &Photo{
Filename: PlacemarkFilename(filename),
}
photo.Name = photo.Filename
photoFile, err := os.Open(filename)
if err != nil {
return photos, err
}
exifdata, err := exif.Decode(photoFile)
if err != nil {
return photos, err
}
timestamp, terr := exifdata.DateTime()
if terr != nil {
Tell("The photo %s has no timestamp -> will be skipped", filename)
continue
}
photo.Timestamp = timestamp
photo.Lat, photo.Lon, terr = exifdata.LatLong()
if terr != nil {
Tell("The photo %s has no location -> will be skipped", filename)
continue
}
// gecoding could be done in parallel, but that is forbidden by the nominatim
// terms of use
if reverseGeocode {
name, gerr := getNominatimName(photo)
if gerr != nil {
return photos, gerr
}
if name != "" {
photo.Name = name
}
}
photos = append(photos, photo)
}
return
}
func WriteKML(w io.Writer, photos Photos) {
// https://developers.google.com/kml/documentation/kml_tut#paths
fmt.Fprint(w, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<kml xmlns=\"http://www.opengis.net/kml/2.2\" xmlns:gx=\"http://www.google.com/kml/ext/2.2\">")
fmt.Fprint(w, "<Document>")
// single photos
for _, photo := range photos {
fmt.Fprintf(w, "<Placemark><name>")
xml.EscapeText(w, []byte(photo.Name))
fmt.Fprintf(w, "</name><description>")
xml.EscapeText(w, []byte(photo.Filename))
fmt.Fprintf(w, "</description><TimeStamp><when>%s</when></TimeStamp><Point><coordinates>%f,%f</coordinates></Point></Placemark>", photo.Timestamp.Format(time.RFC3339), photo.Lon, photo.Lat)
}
// path consisting of all photos
fmt.Fprint(w, "<Placemark><name>Path</name><LineString><coordinates>")
for _, photo := range photos {
fmt.Fprintf(w, "%f,%f ", photo.Lon, photo.Lat)
}
fmt.Fprint(w, "</coordinates></LineString></Placemark>")
fmt.Fprint(w, "</Document></kml>")
}
func init() {
flag.BoolVar(&useAbsoluteFilenames, "a", false, "Use the absolute filenames of the photos for the name of the placemarks. Default is using just the basename.")
flag.BoolVar(&reverseGeocode, "r", false, "Use the nominatim.openstreetmap.org reverse geocoding service to get meaningful names for the placemarks. See the terms of use of the service before extensive use of this functionality.")
}
func Usage() {
fmt.Fprintf(os.Stderr, "Reads a list of filenames pf photos from stdin, generates a KML document with the\nlocations of the photos and a path connecting the photos in chronological order\nand writes the KML to stdout.\n\n")
fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0])
flag.PrintDefaults()
os.Exit(0)
}
func main() {
flag.Usage = Usage
flag.Parse()
reader := bufio.NewReader(os.Stdin)
Tell("Reading list of photos from stdin ...")
// register camera makenote data parsing
exif.RegisterParsers(mknote.All...)
photos, err := ReadPhotosFromList(reader)
if err != nil {
Tell("An error occured: %v", err)
os.Exit(1)
}
Tell("Collected %d photos", len(photos))
sort.Sort(photos) // sort photos by their timestamp
WriteKML(os.Stdout, photos)
}