forked from ryankurte/go-mapbox
/
maps.go
224 lines (183 loc) · 5.29 KB
/
maps.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
/**
* go-mapbox Maps Module
* Wraps the mapbox geocoding API for server side use
* See https://www.mapbox.com/api-documentation/#maps for API information
*
* https://github.com/ryankurte/go-mapbox
* Copyright 2017 Ryan Kurte
*/
package maps
import (
"bytes"
"fmt"
"image"
_ "image/jpeg"
_ "image/png"
"io/ioutil"
"log"
"net/url"
"strings"
"github.com/ryankurte/go-mapbox/lib/base"
"sync"
)
const (
apiName = "maps"
apiVersion = "v4"
)
// Cache interface defines an abstract tile cache
// This can be used to limit the number of API calls required to fetch previously fetched tiles
type Cache interface {
Save(mapID MapID, x, y, level uint64, format MapFormat, highDPI bool, img image.Image) error
Fetch(mapID MapID, x, y, level uint64, format MapFormat, highDPI bool) (image.Image, *image.Config, error)
}
// Maps api wrapper instance
type Maps struct {
base *base.Base
cache Cache
}
// NewMaps Create a new Maps API wrapper
func NewMaps(base *base.Base) *Maps {
return &Maps{base, nil}
}
// SetCache binds a cache into the map instance
func (m *Maps) SetCache(c Cache) {
m.cache = c
}
// GetTile fetches the map tile for the specified location
func (m *Maps) GetTile(mapID MapID, x, y, z uint64, format MapFormat, highDPI bool) (*Tile, error) {
v := url.Values{}
dpiFlag := ""
size := SizeStandard
if highDPI {
dpiFlag = "@2x"
size = SizeHighDPI
}
// Catch invalid MapID / MapFormat combinations here
if mapID == MapIDSatellite && strings.Contains(string(format), "png") {
return nil, fmt.Errorf("MapIDSatellite does not support png outputs")
}
if format == MapFormatPngRaw && mapID != MapIDTerrainRGB {
return nil, fmt.Errorf("MapFormatPngRaw only supported for MapIDTerrainRGB")
}
if mapID == MapIDTerrainRGB && format != MapFormatPngRaw {
return nil, fmt.Errorf("MapIDTerrainRGB only supports format MapFormatPngRaw")
}
// Attempt cache lookup if available
if m.cache != nil {
img, _, err := m.cache.Fetch(mapID, x, y, z, format, highDPI)
if err != nil {
log.Printf("Cache fetch error (%s)", err)
} else if img != nil {
tile := NewTile(x, y, z, size, img)
return &tile, nil
}
}
// Create Request
queryString := fmt.Sprintf("%s/%s/%d/%d/%d%s.%s", apiVersion, mapID, z, x, y, dpiFlag, format)
resp, err := m.base.QueryRequest(queryString, &v)
if err != nil {
return nil, err
}
// Parse content type and length
contentType := resp.Header.Get("Content-Type")
contentLength := resp.ContentLength
// Read data from body
defer resp.Body.Close()
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("Error reading response body (%s)", err)
}
if len(data) != int(contentLength) {
return nil, fmt.Errorf("Content length mismatch (expected %d received %d)", contentLength, len(data))
}
if strings.Contains(contentType, "application/json") {
return nil, fmt.Errorf("Invalid API call: %s message: %s", resp.Request.URL, string(data))
}
// Decode config
_, _, err = image.DecodeConfig(bytes.NewReader(data))
if err != nil {
return nil, err
}
// Convert to image
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
return nil, err
}
// Create tile
tile := NewTile(x, y, z, size, img)
// Save to cache if available
// Tile is post RGB conversion (should avoid pngraw issues)
if m.cache != nil {
err = m.cache.Save(mapID, x, y, z, format, highDPI, img)
if err != nil {
log.Printf("Cache save error (%s)", err)
}
}
return &tile, err
}
// GetEnclosingTiles fetches a 2d array of the tiles enclosing a given point
func (m *Maps) GetEnclosingTiles(mapID MapID, a, b base.Location, level uint64, format MapFormat, highDPI bool) ([][]Tile, error) {
// Convert to tile locations
xStart, yStart, xEnd, yEnd := GetEnclosingTileIDs(a, b, level)
xLen := xEnd - xStart + 1
yLen := yEnd - yStart + 1
tiles := make([][]Tile, yLen)
for y := uint64(0); y < yLen; y++ {
tiles[y] = make([]Tile, xLen)
for x := uint64(0); x < xLen; x++ {
xIndex := uint64(xStart + x)
yIndex := uint64(yStart + y)
xIndex, yIndex = WrapTileID(xIndex, yIndex, level)
tile, err := m.GetTile(mapID, xIndex, yIndex, level, format, highDPI)
if err != nil {
return nil, err
}
tiles[y][x] = *tile
}
}
return tiles, nil
}
func (m *Maps) FastGetEnclosingTiles(mapID MapID, a, b base.Location, level uint64, format MapFormat, highDPI bool) ([][]Tile, error) {
// Convert to tile locations
xStart, yStart, xEnd, yEnd := GetEnclosingTileIDs(a, b, level)
xLen := xEnd - xStart + 1
yLen := yEnd - yStart + 1
in := make(chan *Tile, 1)
var wg1 sync.WaitGroup
wg1.Add(int(xLen * yLen))
tiles := make([][]Tile, yLen)
for y := uint64(0); y < yLen; y++ {
tiles[y] = make([]Tile, xLen)
for x := uint64(0); x < xLen; x++ {
xIndex := uint64(xStart + x)
yIndex := uint64(yStart + y)
xIndex, yIndex = WrapTileID(xIndex, yIndex, level)
go func(xIndex, yIndex uint64) {
tile, err := m.GetTile(mapID, xIndex, yIndex, level, format, highDPI)
if err != nil {
log.Printf("Error fetching tile: %s", err)
}
in <- tile
wg1.Done()
}(xIndex, yIndex)
}
}
go func() {
wg1.Wait()
close(in)
}()
stitch:
for {
select {
case t, ok := <-in:
if !ok {
break stitch
}
if t == nil {
return nil, fmt.Errorf("api error")
}
tiles[t.Y-yStart][t.X-xStart] = *t
}
}
return tiles, nil
}