1//  Copyright (c) 2017 Couchbase, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// 		http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package geo
16
17import (
18	"reflect"
19	"strconv"
20	"strings"
21)
22
23// ExtractGeoPoint takes an arbitrary interface{} and tries it's best to
24// interpret it is as geo point.  Supported formats:
25// Container:
26// slice length 2 (GeoJSON)
27//  first element lon, second element lat
28// string (coordinates separated by comma, or a geohash)
29//  first element lat, second element lon
30// map[string]interface{}
31//  exact keys lat and lon or lng
32// struct
33//  w/exported fields case-insensitive match on lat and lon or lng
34// struct
35//  satisfying Later and Loner or Lnger interfaces
36//
37// in all cases values must be some sort of numeric-like thing: int/uint/float
38func ExtractGeoPoint(thing interface{}) (lon, lat float64, success bool) {
39	var foundLon, foundLat bool
40
41	thingVal := reflect.ValueOf(thing)
42	if !thingVal.IsValid() {
43		return lon, lat, false
44	}
45
46	thingTyp := thingVal.Type()
47
48	// is it a slice
49	if thingVal.Kind() == reflect.Slice {
50		// must be length 2
51		if thingVal.Len() == 2 {
52			first := thingVal.Index(0)
53			if first.CanInterface() {
54				firstVal := first.Interface()
55				lon, foundLon = extractNumericVal(firstVal)
56			}
57			second := thingVal.Index(1)
58			if second.CanInterface() {
59				secondVal := second.Interface()
60				lat, foundLat = extractNumericVal(secondVal)
61			}
62		}
63	}
64
65	// is it a string
66	if thingVal.Kind() == reflect.String {
67		geoStr := thingVal.Interface().(string)
68		if strings.Contains(geoStr, ",") {
69			// geo point with coordinates split by comma
70			points := strings.Split(geoStr, ",")
71			for i, point := range points {
72				// trim any leading or trailing white spaces
73				points[i] = strings.TrimSpace(point)
74			}
75			if len(points) == 2 {
76				var err error
77				lat, err = strconv.ParseFloat(points[0], 64)
78				if err == nil {
79					foundLat = true
80				}
81				lon, err = strconv.ParseFloat(points[1], 64)
82				if err == nil {
83					foundLon = true
84				}
85			}
86		} else {
87			// geohash
88			if len(geoStr) <= geoHashMaxLength {
89				lat, lon = DecodeGeoHash(geoStr)
90				foundLat = true
91				foundLon = true
92			}
93		}
94	}
95
96	// is it a map
97	if l, ok := thing.(map[string]interface{}); ok {
98		if lval, ok := l["lon"]; ok {
99			lon, foundLon = extractNumericVal(lval)
100		} else if lval, ok := l["lng"]; ok {
101			lon, foundLon = extractNumericVal(lval)
102		}
103		if lval, ok := l["lat"]; ok {
104			lat, foundLat = extractNumericVal(lval)
105		}
106	}
107
108	// now try reflection on struct fields
109	if thingVal.Kind() == reflect.Struct {
110		for i := 0; i < thingVal.NumField(); i++ {
111			fieldName := thingTyp.Field(i).Name
112			if strings.HasPrefix(strings.ToLower(fieldName), "lon") {
113				if thingVal.Field(i).CanInterface() {
114					fieldVal := thingVal.Field(i).Interface()
115					lon, foundLon = extractNumericVal(fieldVal)
116				}
117			}
118			if strings.HasPrefix(strings.ToLower(fieldName), "lng") {
119				if thingVal.Field(i).CanInterface() {
120					fieldVal := thingVal.Field(i).Interface()
121					lon, foundLon = extractNumericVal(fieldVal)
122				}
123			}
124			if strings.HasPrefix(strings.ToLower(fieldName), "lat") {
125				if thingVal.Field(i).CanInterface() {
126					fieldVal := thingVal.Field(i).Interface()
127					lat, foundLat = extractNumericVal(fieldVal)
128				}
129			}
130		}
131	}
132
133	// last hope, some interfaces
134	// lon
135	if l, ok := thing.(loner); ok {
136		lon = l.Lon()
137		foundLon = true
138	} else if l, ok := thing.(lnger); ok {
139		lon = l.Lng()
140		foundLon = true
141	}
142	// lat
143	if l, ok := thing.(later); ok {
144		lat = l.Lat()
145		foundLat = true
146	}
147
148	return lon, lat, foundLon && foundLat
149}
150
151// extract numeric value (if possible) and returns a float64
152func extractNumericVal(v interface{}) (float64, bool) {
153	val := reflect.ValueOf(v)
154	if !val.IsValid() {
155		return 0, false
156	}
157	typ := val.Type()
158	switch typ.Kind() {
159	case reflect.Float32, reflect.Float64:
160		return val.Float(), true
161	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
162		return float64(val.Int()), true
163	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
164		return float64(val.Uint()), true
165	}
166
167	return 0, false
168}
169
170// various support interfaces which can be used to find lat/lon
171type loner interface {
172	Lon() float64
173}
174
175type later interface {
176	Lat() float64
177}
178
179type lnger interface {
180	Lng() float64
181}
182