Passed
Pull Request — master (#348)
by
unknown
02:14
created

elodie.geolocation.lookup()   C

Complexity

Conditions 10

Size

Total Lines 37
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 32
nop 1
dl 0
loc 37
rs 5.9999
c 0
b 0
f 0

How to fix   Complexity   

Complexity

Complex classes like elodie.geolocation.lookup() often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""Look up geolocation information for media objects."""
2
from __future__ import print_function
3
from __future__ import division
4
from future import standard_library
5
from past.utils import old_div
6
7
standard_library.install_aliases()  # noqa
8
9
from os import path
10
from os import getcwd
11
12
import requests
13
import urllib.request
14
import urllib.parse
15
import urllib.error
16
17
from elodie.config import load_config
18
from elodie import constants
19
from elodie import log
20
from elodie.localstorage import Db
21
22
import elodie.closestgeoname.closestgeoname as closestgeoname
23
24
__KEY__ = None
25
__DEFAULT_LOCATION__ = 'Unknown Location'
26
__PREFER_ENGLISH_NAMES__ = None
27
28
29
def coordinates_by_name(name):
30
    # Try to get cached location first
31
    db = Db()
32
    cached_coordinates = db.get_location_coordinates(name)
33
    if(cached_coordinates is not None):
34
        return {
35
            'latitude': cached_coordinates[0],
36
            'longitude': cached_coordinates[1]
37
        }
38
39
    # If the name is not cached then we go ahead with an API lookup
40
    geolocation_info = lookup(location=name)
41
42
    if(geolocation_info is not None):
43
        if(
44
            'results' in geolocation_info and
45
            len(geolocation_info['results']) != 0 and
46
            'locations' in geolocation_info['results'][0] and
47
            len(geolocation_info['results'][0]['locations']) != 0
48
        ):
49
50
            # By default we use the first entry unless we find one with
51
            #   geocodeQuality=city.
52
            geolocation_result = geolocation_info['results'][0]
53
            use_location = geolocation_result['locations'][0]['latLng']
54
            # Loop over the locations to see if we come accross a
55
            #   geocodeQuality=city.
56
            # If we find a city we set that to the use_location and break
57
            for location in geolocation_result['locations']:
58
                if(
59
                    'latLng' in location and
60
                    'lat' in location['latLng'] and
61
                    'lng' in location['latLng'] and
62
                    location['geocodeQuality'].lower() == 'city'
63
                ):
64
                    use_location = location['latLng']
65
                    break
66
67
            return {
68
                'latitude': use_location['lat'],
69
                'longitude': use_location['lng']
70
            }
71
72
    return None
73
74
75
def decimal_to_dms(decimal):
76
    decimal = float(decimal)
77
    decimal_abs = abs(decimal)
78
    minutes, seconds = divmod(decimal_abs*3600, 60)
79
    degrees, minutes = divmod(minutes, 60)
80
    degrees = degrees
81
    sign = 1 if decimal >= 0 else -1
82
    return (degrees, minutes, seconds, sign)
83
84
85
def dms_to_decimal(degrees, minutes, seconds, direction=' '):
86
    sign = 1
87
    if(direction[0] in 'WSws'):
88
        sign = -1
89
    return (
90
        float(degrees) + old_div(float(minutes), 60) +
91
        old_div(float(seconds), 3600)
92
    ) * sign
93
94
95
def dms_string(decimal, type='latitude'):
96
    # Example string -> 38 deg 14' 27.82" S
97
    dms = decimal_to_dms(decimal)
98
    if type == 'latitude':
99
        direction = 'N' if decimal >= 0 else 'S'
100
    elif type == 'longitude':
101
        direction = 'E' if decimal >= 0 else 'W'
102
    return '{} deg {}\' {}" {}'.format(dms[0], dms[1], dms[2], direction)
0 ignored issues
show
introduced by
The variable direction does not seem to be defined for all execution paths.
Loading history...
103
104
105
def get_key():
106
    global __KEY__
107
    if __KEY__ is not None:
108
        return __KEY__
109
110
    if constants.mapquest_key is not None:
111
        __KEY__ = constants.mapquest_key
112
        return __KEY__
113
114
    config_file = '%s/config.ini' % constants.application_directory
115
    if not path.exists(config_file):
116
        return None
117
118
    config = load_config()
119
    if('MapQuest' not in config):
120
        return None
121
122
    __KEY__ = config['MapQuest']['key']
123
    return __KEY__
124
125
def get_prefer_english_names():
126
    global __PREFER_ENGLISH_NAMES__
127
    if __PREFER_ENGLISH_NAMES__ is not None:
128
        return __PREFER_ENGLISH_NAMES__
129
130
    config_file = '%s/config.ini' % constants.application_directory
131
    if not path.exists(config_file):
132
        return False
133
134
    config = load_config()
135
    if('MapQuest' not in config):
136
        return False
137
138
    if('prefer_english_names' not in config['MapQuest']):
139
        return False
140
141
    __PREFER_ENGLISH_NAMES__ = bool(config['MapQuest']['prefer_english_names'])
142
    return __PREFER_ENGLISH_NAMES__
143
144
def place_name(lat, lon):
145
    lookup_place_name_default = {'default': __DEFAULT_LOCATION__}
146
    if(lat is None or lon is None):
147
        return lookup_place_name_default
148
149
    # Convert lat/lon to floats
150
    if(not isinstance(lat, float)):
151
        lat = float(lat)
152
    if(not isinstance(lon, float)):
153
        lon = float(lon)
154
155
    localdb_response = closestgeoname.query_closest_city(path.join(getcwd(),'elodie', 'closestgeoname','geonames.sqlite'), lat, lon)
156
157
    if localdb_response is not None:
158
        return {"city": localdb_response[0],
159
                "default": localdb_response[0],
160
                "state": localdb_response[1],
161
                "country": localdb_response[2]}
162
163
    else:
164
        # This should only occur if the script is searching for a point
165
        # outside the maximum distance on earth. So this would mean
166
        # there is likely an error with the GNSS coordinate. But we can
167
        # continue to see if other services (MapQuest) has a way to handle it.
168
        print("lat/long likely to be incorrect... continuing with other service")
169
        print("lat: {}, lon: {}".format(lat, lon))
170
        pass
171
172
    # Try to get cached location first
173
    db = Db()
174
    # 3km distace radious for a match
175
    cached_place_name = db.get_location_name(lat, lon, 3000)
176
    # We check that it's a dict to coerce an upgrade of the location
177
    #  db from a string location to a dictionary. See gh-160.
178
    if(isinstance(cached_place_name, dict)):
179
        return cached_place_name
180
181
    lookup_place_name = {}
182
    geolocation_info = lookup(lat=lat, lon=lon)
183
    if(geolocation_info is not None and 'address' in geolocation_info):
184
        address = geolocation_info['address']
185
        for loc in ['city', 'state', 'country']:
186
            if(loc in address):
187
                lookup_place_name[loc] = address[loc]
188
                # In many cases the desired key is not available so we
189
                #  set the most specific as the default.
190
                if('default' not in lookup_place_name):
191
                    lookup_place_name['default'] = address[loc]
192
193
    if(lookup_place_name):
194
        db.add_location(lat, lon, lookup_place_name)
195
        # TODO: Maybe this should only be done on exit and not for every write.
196
        db.update_location_db()
197
198
    if('default' not in lookup_place_name):
199
        lookup_place_name = lookup_place_name_default
200
201
    return lookup_place_name
202
203
204
def lookup(**kwargs):
205
    if(
206
        'location' not in kwargs and
207
        'lat' not in kwargs and
208
        'lon' not in kwargs
209
    ):
210
        return None
211
212
    key = get_key()
213
    prefer_english_names = get_prefer_english_names()
214
215
    if(key is None):
216
        return None
217
218
    try:
219
        params = {'format': 'json', 'key': key}
220
        params.update(kwargs)
221
        path = '/geocoding/v1/address'
222
        if('lat' in kwargs and 'lon' in kwargs):
223
            path = '/nominatim/v1/reverse.php'
224
        url = '%s%s?%s' % (
225
                    constants.mapquest_base_url,
226
                    path,
227
                    urllib.parse.urlencode(params)
228
              )
229
        headers = {}
230
        if(prefer_english_names):
231
            headers = {'Accept-Language':'en-EN,en;q=0.8'}
232
        r = requests.get(url, headers=headers)
233
        return parse_result(r.json())
234
    except requests.exceptions.RequestException as e:
235
        log.error(e)
236
        return None
237
    except ValueError as e:
238
        log.error(r.text)
239
        log.error(e)
240
        return None
241
242
243
def parse_result(result):
244
    if('error' in result):
245
        return None
246
247
    if(
248
        'results' in result and
249
        len(result['results']) > 0 and
250
        'locations' in result['results'][0]
251
        and len(result['results'][0]['locations']) > 0 and
252
        'latLng' in result['results'][0]['locations'][0]
253
    ):
254
        latLng = result['results'][0]['locations'][0]['latLng']
255
        if(latLng['lat'] == 39.78373 and latLng['lng'] == -100.445882):
256
            return None
257
258
    return result
259