-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtasgrid.go
322 lines (279 loc) · 11.9 KB
/
tasgrid.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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
package tasgrid
import (
"encoding/csv"
"fmt"
"io"
"math"
"os"
"strconv"
"strings"
"unicode"
utm "github.com/kurankat/tasutm"
)
// GridPoint holds all the necessary information pertaining to a grid point, calculated from the
// name of the map provided and the three-figure easting and northing
type GridPoint struct {
MapName string
fullEasting float64
fullNorthing float64
decimalLat float64
decimalLong float64
latDegs float64
latMins float64
latSecs float64
longDegs float64
longMins float64
longSecs float64
}
// NewGridPoint creates a GridPoint from the supplied map name and 3-digit
// easting and northing, and calculates the full easting and northing, as well as
// the latitude and longitude of the record in decimal degrees and degrees, minutes and
// seconds
func NewGridPoint(name, textEasting, textNorthing string, mg MapGrid) (GridPoint, error) {
var mapName string // Name of map in database, converted to uppercase
var gp GridPoint // Main grid point to be returned
var stringFullEasting string // Full UTM easting as a string
var stringFullNorthing string // Full UTM northing as a string
var numEasting int // integer version of 3-digit grid reference easting from database
var numNorthing int // integer version of 3-digit grid reference northing from database
var firstEasting string // Westernmost (lowest) easting in map sheet, as a string
var firstNorthing string // Southernmost (lowest) northing in map sheet, as a string
var lastEasting string // Easternmost (highest) easting in map sheet, as a string
var lastNorthing string // Northernomst (highest) northing in map sheet, as a string
var mapRangeW float64 // Full UTM version (numeric) of westernmost easting in map sheet
var mapRangeS float64 // Full UTM version (numeric) of southernmost easting in map sheet
var mapRangeE float64 // Full UTM version (numeric) of easternmost easting in map sheet
var mapRangeN float64 // Full UTM version (numeric) of northernmost easting in map sheet
// Only proceed if the information consists of a three-letter map name, and three-figure
// easting and northings, assuming it is information from a TASMAP 1:100,000-series map
if len(name) != 3 ||
len(textEasting) != 3 ||
len(textNorthing) != 3 {
return GridPoint{}, nil
}
// If the map contains digits, it is a NZ map, return empty gridpoint but no error
for _, c := range name {
if unicode.IsDigit(c) {
return GridPoint{}, nil
}
}
mapName = strings.ToUpper(name)
gp = GridPoint{MapName: mapName}
// Convert string easting and northing to integers and return an error if problems arise
numEasting, err := strconv.Atoi(textEasting)
if err != nil {
return GridPoint{}, fmt.Errorf("ERROR parsing grid: I can't convert easting %v to an integer", textEasting)
}
numNorthing, err = strconv.Atoi(textNorthing)
if err != nil {
return GridPoint{}, fmt.Errorf("ERROR parsing grid: I can't convert northing %v to an integer", textNorthing)
}
firstEasting = mg[mapName].eastingStart
firstNorthing = mg[mapName].northingStart
lastEasting = mg[mapName].eastingEnd
lastNorthing = mg[mapName].northingEnd
// Calculate the range of acceptable eastings and northings from that map sheet
mapRangeW, err = strconv.ParseFloat(firstEasting+"000", 64)
if err != nil {
return GridPoint{}, fmt.Errorf("ERROR converting westernmost easting %v to a float", textNorthing)
}
mapRangeE, err = strconv.ParseFloat(lastEasting+"000", 64)
if err != nil {
return GridPoint{}, fmt.Errorf("ERROR converting easternmost easting %v to a float", textNorthing)
}
mapRangeS, err = strconv.ParseFloat(firstNorthing+"000", 64)
if err != nil {
return GridPoint{}, fmt.Errorf("ERROR converting southernmost easting %v to a float", textNorthing)
}
mapRangeN, err = strconv.ParseFloat(lastNorthing+"000", 64)
if err != nil {
return GridPoint{}, fmt.Errorf("ERROR converting northernmost easting %v to a float", textNorthing)
}
// If we don't have figures in the required fields, the map name may have been wrong - ignore and return an error
if len(firstEasting)+len(firstNorthing) == 0 {
return GridPoint{}, fmt.Errorf("ERROR parsing grid: I'm having trouble geting values for map %v", name)
}
// Convert the all but the last digit of the starting easting and northing lines to integers
numFirstEasting, err := strconv.Atoi(firstEasting[:1])
numFirstNorthing, err := strconv.Atoi(firstNorthing[:2])
// Extract the last two figures of the easting and northing starting lines to later determine how to calculate
// the complete easting and northing (if it carries over 99). Return errors if needed
eastingVariable, err := strconv.Atoi(firstEasting[len(firstEasting)-2:])
if err != nil {
return GridPoint{}, fmt.Errorf("ERROR parsing grid: I can't extract a number from %v", firstEasting)
}
northingVariable, err := strconv.Atoi(firstNorthing[len(firstNorthing)-2:])
if err != nil {
return GridPoint{}, fmt.Errorf("ERROR parsing grid: I can't extract a number from %v", firstNorthing)
}
// If the easting is greater than the first line easting on the map, append the first figure from the easting
// starting line and add two zeros to get a complete easting. However if the easting is a smaller number,
// we need to carry one because we wrap over 100.
if numEasting > eastingVariable*10 {
stringFullEasting = firstEasting[:1] + textEasting + "00"
} else {
newFirstEasting := strconv.Itoa(numFirstEasting + 1)
stringFullEasting = newFirstEasting + textEasting + "00"
}
gp.fullEasting, err = strconv.ParseFloat(stringFullEasting, 64)
if err != nil {
return GridPoint{}, fmt.Errorf("ERROR parsing grid: I can't extract a number from %v", stringFullEasting)
}
// Ditto for the northing, but using the first two figures from the starting line
if numNorthing > northingVariable*10 {
stringFullNorthing = firstNorthing[:2] + textNorthing + "00"
} else {
newFirstNorthing := strconv.Itoa(numFirstNorthing + 1)
stringFullNorthing = newFirstNorthing + textNorthing + "00"
}
gp.fullNorthing, err = strconv.ParseFloat(stringFullNorthing, 64)
if err != nil {
return GridPoint{}, fmt.Errorf("ERROR parsing grid: I can't extract a number from %v", stringFullNorthing)
}
// Check if easting and northing fall within the range of expected values for their map sheet
if gp.fullEasting < mapRangeW ||
gp.fullEasting > mapRangeE {
return GridPoint{}, fmt.Errorf("ERROR parsing grid: easting %v is out of the expected range for map %v", textEasting, gp.MapName)
}
if gp.fullNorthing < mapRangeS ||
gp.fullNorthing > mapRangeN {
return GridPoint{}, fmt.Errorf("ERROR parsing grid: northing %v is out of the expected range for map %v", textNorthing, gp.MapName)
}
// Use utm library to calculate the decimal latitude and longitude. Treat King Island specimens in
// zone 54 as if they were zone 55, using the zone 55 numbers in the TASMAP maps (the error is small
// enough to be safely ignored)
gp.decimalLat, gp.decimalLong, err = utm.ToLatLon(gp.fullEasting, gp.fullNorthing, 55, "G")
if err != nil {
return GridPoint{}, fmt.Errorf("Map name: %v, 3f easting: %v, 3f northing: %v", mapName, textEasting, textNorthing)
}
// Calculate the DMS lat and long
gp.latDegs, gp.latMins, gp.latSecs = ddToDMS(gp.decimalLat)
gp.longDegs, gp.longMins, gp.longSecs = ddToDMS(gp.decimalLong)
return gp, nil
}
// GetFullEasting returns the full-length easting of the grid point as a string
func (gp GridPoint) GetFullEasting() (easting string) {
easting = strconv.FormatFloat(gp.fullEasting, 'f', 0, 64)
return easting
}
// GetFullNorthing returns the full-length northing of the grid point as a string
func (gp GridPoint) GetFullNorthing() (northing string) {
northing = strconv.FormatFloat(gp.fullNorthing, 'f', 0, 64)
return
}
// GetDecimalLat returns the latitude in decimal degrees of the grid point as a string
func (gp GridPoint) GetDecimalLat() (dLat string) {
dLat = strconv.FormatFloat(gp.decimalLat, 'f', 6, 64)
return
}
// GetDecimalLong returns the longitude in decimal degrees of the grid point as a string
func (gp GridPoint) GetDecimalLong() (dLong string) {
dLong = strconv.FormatFloat(gp.decimalLong, 'f', 6, 64)
return
}
// GetLatSeconds returns the seconds reading of the grid point's latitude as a string
func (gp GridPoint) GetLatSeconds() (secs string) {
if gp.latSecs < 0 {
gp.latSecs = -gp.latSecs
}
secs = strconv.FormatFloat(gp.latSecs, 'f', 1, 64)
return
}
// GetLongSeconds returns the seconds reading of the grid point's longitude as a string
func (gp GridPoint) GetLongSeconds() (secs string) {
if gp.longSecs < 0 {
gp.longSecs = -gp.longSecs
}
secs = strconv.FormatFloat(gp.longSecs, 'f', 1, 64)
return
}
// GetDistance takes a latitude and longitude in decimal degrees and
// calculates the distance of that point to the GridPoint, in meters
func (gp GridPoint) GetDistance(lat, long string) (distance float64, err error) {
// Parse floats from string lat and long
fLat, err := strconv.ParseFloat(lat, 64)
if err != nil {
return distance, fmt.Errorf("I can't parse a latitude from %v", lat)
}
fLong, err := strconv.ParseFloat(long, 64)
if err != nil {
return distance, fmt.Errorf("I can't parse a longitude from %v", lat)
}
// Use UTM converter to derive full easting and northing
easting, northing, _, _, err := utm.FromLatLonZone(fLat, fLong, false, 55)
if err != nil {
return distance, fmt.Errorf("UTM converter has trouble with lat %v & long %v", lat, long)
}
eastingDistance := math.Abs(gp.fullEasting - easting)
northingDistance := math.Abs(gp.fullNorthing - northing)
distance = math.Sqrt((eastingDistance * eastingDistance) + (northingDistance * northingDistance))
return
}
// TasMap holds map-unique information: the UTM zone, alphanumeric code, as well as the lowest
// easting and northing 1000m lines
type TasMap struct {
zone int
alpha string
eastingStart, eastingEnd string
northingStart, northingEnd string
}
// newTasMap assigns the information provided in the argument (a slice of map information)
// to the correct fields
func newTasMap(mapInfo []string) TasMap {
zone, err := strconv.Atoi(mapInfo[1])
checkError(err)
alpha := mapInfo[2]
west := mapInfo[3]
east := mapInfo[4]
south := mapInfo[5]
north := mapInfo[6]
return TasMap{zone: zone, alpha: alpha, eastingStart: west, eastingEnd: east, northingStart: south, northingEnd: north}
}
// MapGrid holds a dictionary of TASMAP three-letter acronyms
// containing the map's necessary data to calculate the full grid reference
type MapGrid map[string]TasMap
// NewTasMapGrid returns a TasMap object
func NewTasMapGrid() *MapGrid {
mapList := MapGrid{}
// Read map data as a CSV file
mapReader := csv.NewReader(strings.NewReader(mapInfo))
// Read each line into memory and use the data to create a tasMap object for each line
for {
tasMap, err := mapReader.Read()
if err == io.EOF {
break
} else if err != nil {
checkError(err)
}
name := tasMap[0]
mapList[name] = newTasMap(tasMap)
}
return &mapList
}
// Convert decimal degrees to degrees, minutes, seconds
func ddToDMS(dd float64) (degs, mins, secs float64) {
degs = math.Trunc(dd) // Degrees as float is the truncated decimal degrees
minDiff := math.Abs(dd) - math.Abs(degs) // What remains right of the decimal point is the decimal mins
dMins := minDiff * 60.0 // Float minutes is decimal minutes * 60
mins = math.Trunc(dMins)
secDiff := dMins - mins
secs = secDiff * 60.0
return
}
// Check and handle errors (simplified)
func checkError(err error) {
if err != nil {
switch err.(type) {
// If the error is a path error (such as file not being found) then print
// individualised error message
case *os.PathError:
fmt.Println("\nI'm having trouble accessing a file. The system says:")
fmt.Printf("\t* %v\n\n", err)
os.Exit(1)
default:
// If we don't have a specific way of handling the error, print the error
// to screen and exit
panic(err)
}
}
}