This repository has been archived by the owner on May 24, 2021. It is now read-only.
/
pipelines.go
276 lines (229 loc) · 7.93 KB
/
pipelines.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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
package cmd
import (
"context"
"encoding/csv"
"fmt"
"image/png"
"math"
"os"
"path/filepath"
"strconv"
"github.com/disintegration/imaging"
"github.com/gocarina/gocsv"
"github.com/joho/godotenv"
"github.com/lukeroth/gdal"
"github.com/schollz/progressbar/v2"
log "github.com/sirupsen/logrus"
"googlemaps.github.io/maps"
)
// Coordinate defines a lat-long coordinate from a csv file
type Coordinate struct {
Latitude string `csv:"latitude"`
Longitude string `csv:"longitude"`
}
// ClipLabelbyExtent gets the extent of an input raster and clips a shapefile from it
func ClipLabelbyExtent(extent gdal.Geometry, shpFile gdal.Layer, outpath string) {
if _, err := os.Stat(filepath.Dir(outpath)); os.IsNotExist(err) {
os.MkdirAll(filepath.Dir(outpath), os.ModePerm)
}
// Perform filtering of labels
shpFile.SetSpatialFilter(extent)
shpFile.ResetReading()
outDriver := gdal.OGRDriverByName("GeoJSON")
outDataSource, _ := outDriver.Create(outpath, []string{})
outDataSource.CopyLayer(shpFile, "Labels", []string{})
outDataSource.Destroy()
}
// GetRasterExtent computes for the extent of a TIFF image and returns a Geometry
func GetRasterExtent(tifPath string) gdal.Geometry {
src, err := gdal.Open(tifPath, gdal.ReadOnly)
if err != nil {
log.Fatal(err)
}
// affine (transformation) is organized as
// (ulx, xres, xskew, uly, yskew, yres)
affine := src.GeoTransform()
lrx := affine[0] + float64(src.RasterXSize())*affine[1]
lry := affine[3] + float64(src.RasterYSize())*affine[5]
defer src.Close()
// Convert extents into a WKT representation
ring := gdal.Create(gdal.GT_LinearRing)
ring.AddPoint2D(affine[0], affine[3]) // top-left
ring.AddPoint2D(lrx, affine[3]) // top-right
ring.AddPoint2D(lrx, lry) // bottom-right
ring.AddPoint2D(affine[0], lry) // bottom-left
ring.AddPoint2D(affine[0], affine[3]) // top-left
// Create a polygon by closing the ring
extent := gdal.Create(gdal.GT_Polygon)
extent.AddGeometryDirectly(ring)
return extent
}
// GeoReferenceImage converts a Static Maps image into a geo-referenced TIFF
func GeoReferenceImage(coordinate []string, size []int, zoom int, inpath string, outpath string) {
// Define projection constants
const projector float64 = 156543.03392
const maxExtent float64 = 20037508.34
if _, err := os.Stat(filepath.Dir(outpath)); os.IsNotExist(err) {
os.MkdirAll(filepath.Dir(outpath), os.ModePerm)
}
lat4326, _ := strconv.ParseFloat(coordinate[0], 64)
lon4326, _ := strconv.ParseFloat(coordinate[1], 64)
latCenter := size[0] / 2
lonCenter := size[1] / 2
// Compute the GSD Resolution and convert to EPSG3857
gsdResolution := projector * math.Cos(lat4326*math.Pi/180) / math.Pow(2, float64(zoom))
lat3857 := (math.Log(math.Tan((90+lat4326)*math.Pi/360)) / (math.Pi / 180)) * maxExtent / 180
lon3857 := lon4326 * maxExtent / 180
// Compute boundaries
upperLeftY := lat3857 + gsdResolution*float64(latCenter)
upperLeftX := lon3857 - gsdResolution*float64(lonCenter)
// Read source image and its driver
srcDataset, _ := gdal.Open(inpath, gdal.ReadOnly)
defer srcDataset.Close()
driver, _ := gdal.GetDriverByName("GTiff")
// Open destination dataset
dstDataset := driver.CreateCopy(outpath, srcDataset, 0, nil, nil, nil)
defer dstDataset.Close()
dstDataset.SetGeoTransform([6]float64{upperLeftX, gsdResolution, 0, upperLeftY, 0, -gsdResolution})
// Get raster projection
srs := gdal.CreateSpatialReference("")
srs.FromEPSG(3857)
destWKT, _ := srs.ToWKT()
dstDataset.SetProjection(destWKT)
}
// GetGSMImage downloads a single static maps image given a client and set of
// parameters
func GetGSMImage(client *maps.Client, coordinate []string, zoom int, size []int, outpath string) {
if _, err := os.Stat(filepath.Dir(outpath)); os.IsNotExist(err) {
os.MkdirAll(filepath.Dir(outpath), os.ModePerm)
}
// Prepare request
r := &maps.StaticMapRequest{
Center: fmt.Sprintf("%s,%s", coordinate[0], coordinate[1]),
Zoom: zoom,
Size: fmt.Sprintf("%dx%d", size[0], size[1]),
Scale: 1,
MapType: "satellite",
}
// Perform request
img, err := client.StaticMap(context.Background(), r)
if err != nil {
log.WithFields(log.Fields{
"err": err,
}).Fatal("Request error")
}
f, err := os.Create(fmt.Sprintf("%s", outpath))
if err != nil {
log.Fatal(err)
}
imgRGBA := imaging.Clone(img)
defer f.Close()
png.Encode(f, imgRGBA)
}
// GetStaticMapsClient returns a Client for constructing a StaticMapRequest.
func GetStaticMapsClient() *maps.Client {
err := godotenv.Load()
if err != nil {
log.WithFields(log.Fields{
"err": err,
}).Fatal("Load error")
}
apiKey := os.Getenv("API_KEY")
client, err := maps.NewClient(maps.WithAPIKey(apiKey))
if err != nil {
log.WithFields(log.Fields{
"err": err,
}).Fatal("Load error")
}
return client
}
// ReadCSVFile opens a csv file and returns a list of coordinates
func ReadCSVFile(path string, skipFirst bool) []*Coordinate {
file, err := os.OpenFile(path, os.O_RDONLY, os.ModePerm)
if err != nil {
log.Fatal(err)
}
defer file.Close()
reader := csv.NewReader(file)
coordinates := []*Coordinate{}
if skipFirst {
if err := gocsv.UnmarshalCSVWithoutHeaders(reader, &coordinates); err != nil {
log.Fatal(err)
}
}
if err := gocsv.UnmarshalCSV(reader, &coordinates); err != nil {
log.Fatal(err)
}
return coordinates
}
// ReadShapeFile opens an ESRI Shapefile and returns a Layer of Features
func ReadShapeFile(lblPath string) gdal.Layer {
srs := gdal.CreateSpatialReference("")
srs.FromEPSG(4326)
lblDataSource := gdal.OpenDataSource(lblPath, 1)
// lblLayer := lblDataSource.CreateLayer("Labels", srs, gdal.GT_MultiPolygon, []string{})
lblLayer := lblDataSource.LayerByIndex(0)
return lblLayer
}
// ReprojectImage converts image projection into a new spatial reference
func ReprojectImage(path string, srs string) {
options := []string{"-t_srs", srs}
ds, err := gdal.Open(path, gdal.ReadOnly)
if err != nil {
log.Fatal(err)
}
defer ds.Close()
out := gdal.GDALWarp(path, gdal.Dataset{}, []gdal.Dataset{ds}, options)
defer out.Close()
}
// RunBatchPipeline executes all tiffany tasks for a list of coordinates
func RunBatchPipeline(csvPath string, skipFirst bool, zoom int, size []int, path string, noRef bool, wtLbl string, force bool) (int, int) {
// Read CSV files
coordinates := ReadCSVFile(csvPath, skipFirst)
var numSkip int
bar := progressbar.NewOptions(len(coordinates), progressbar.OptionSetRenderBlankState(true))
for _, coord := range coordinates {
skipped := RunPipeline([]string{coord.Latitude, coord.Longitude}, zoom, size, path, noRef, wtLbl, force)
if skipped {
numSkip++
}
bar.Add(1)
}
return len(coordinates), numSkip
}
// RunPipeline executes all tiffany tasks for a single coordinate
func RunPipeline(coordinate []string, zoom int, size []int, path string, noRef bool, wtLbl string, force bool) bool {
const gsmSubDir string = "png"
const geoSubDir string = "tif"
const lblSubDir string = "json"
// Create filenames for output artifacts
fnameFormat := fmt.Sprintf("%s_%s_%d_%dx%d", coordinate[0], coordinate[1], zoom, size[0], size[1])
pngPath := filepath.Join(path, gsmSubDir, fnameFormat+".png")
tifPath := filepath.Join(path, geoSubDir, fnameFormat+".tiff")
lblPath := filepath.Join(path, lblSubDir, fnameFormat+".geojson")
// Download Google Static Maps (GSM) Image
var skipped = false
if force {
// Force download an image
client := GetStaticMapsClient()
GetGSMImage(client, coordinate, zoom, size, pngPath)
} else if _, err := os.Stat(pngPath); err == nil {
skipped = true
log.WithFields(log.Fields{
"skipped": pngPath,
}).Debug("File exists, skipping. Use --force to override")
} else {
client := GetStaticMapsClient()
GetGSMImage(client, coordinate, zoom, size, pngPath)
}
if !noRef {
GeoReferenceImage(coordinate, size, zoom, pngPath, tifPath)
ReprojectImage(tifPath, "epsg:4326")
}
if len(wtLbl) > 0 {
extent := GetRasterExtent(tifPath)
shpFile := ReadShapeFile(wtLbl)
ClipLabelbyExtent(extent, shpFile, lblPath)
}
return skipped
}