Passed
Push — master ( 449056...18677a )
by Jaisen
05:33 queued 03:21
created

elodie.geolocation.lookup()   D

Complexity

Conditions 12

Size

Total Lines 41
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 35
nop 1
dl 0
loc 41
rs 4.8
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
11
import requests
12
import urllib.request
13
import urllib.parse
14
import urllib.error
15
16
from elodie.config import load_config
17
from elodie import constants
18
from elodie import log
19
from elodie.localstorage import Db
20
21
__KEY__ = None
22
__DEFAULT_LOCATION__ = 'Unknown Location'
23
__PREFER_ENGLISH_NAMES__ = None
24
25
26
def coordinates_by_name(name):
27
    # Try to get cached location first
28
    db = Db()
29
    cached_coordinates = db.get_location_coordinates(name)
30
    if(cached_coordinates is not None):
31
        return {
32
            'latitude': cached_coordinates[0],
33
            'longitude': cached_coordinates[1]
34
        }
35
36
    # If the name is not cached then we go ahead with an API lookup
37
    geolocation_info = lookup(location=name)
38
39
    if(geolocation_info is not None):
40
        if(
41
            'results' in geolocation_info and
42
            len(geolocation_info['results']) != 0 and
43
            'locations' in geolocation_info['results'][0] and
44
            len(geolocation_info['results'][0]['locations']) != 0
45
        ):
46
47
            # By default we use the first entry unless we find one with
48
            #   geocodeQuality=city.
49
            geolocation_result = geolocation_info['results'][0]
50
            use_location = geolocation_result['locations'][0]['latLng']
51
            # Loop over the locations to see if we come accross a
52
            #   geocodeQuality=city.
53
            # If we find a city we set that to the use_location and break
54
            for location in geolocation_result['locations']:
55
                if(
56
                    'latLng' in location and
57
                    'lat' in location['latLng'] and
58
                    'lng' in location['latLng'] and
59
                    location['geocodeQuality'].lower() == 'city'
60
                ):
61
                    use_location = location['latLng']
62
                    break
63
64
            return {
65
                'latitude': use_location['lat'],
66
                'longitude': use_location['lng']
67
            }
68
69
    return None
70
71
72
def decimal_to_dms(decimal):
73
    decimal = float(decimal)
74
    decimal_abs = abs(decimal)
75
    minutes, seconds = divmod(decimal_abs*3600, 60)
76
    degrees, minutes = divmod(minutes, 60)
77
    degrees = degrees
78
    sign = 1 if decimal >= 0 else -1
79
    return (degrees, minutes, seconds, sign)
80
81
82
def dms_to_decimal(degrees, minutes, seconds, direction=' '):
83
    sign = 1
84
    if(direction[0] in 'WSws'):
85
        sign = -1
86
    return (
87
        float(degrees) + old_div(float(minutes), 60) +
88
        old_div(float(seconds), 3600)
89
    ) * sign
90
91
92
def dms_string(decimal, type='latitude'):
93
    # Example string -> 38 deg 14' 27.82" S
94
    dms = decimal_to_dms(decimal)
95
    if type == 'latitude':
96
        direction = 'N' if decimal >= 0 else 'S'
97
    elif type == 'longitude':
98
        direction = 'E' if decimal >= 0 else 'W'
99
    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...
100
101
102
def get_key():
103
    global __KEY__
104
    if __KEY__ is not None:
105
        return __KEY__
106
107
    if constants.mapquest_key is not None:
108
        __KEY__ = constants.mapquest_key
109
        return __KEY__
110
111
    config_file = '%s/config.ini' % constants.application_directory
112
    if not path.exists(config_file):
113
        return None
114
115
    config = load_config()
116
    if('MapQuest' not in config):
117
        return None
118
119
    __KEY__ = config['MapQuest']['key']
120
    return __KEY__
121
122
def get_prefer_english_names():
123
    global __PREFER_ENGLISH_NAMES__
124
    if __PREFER_ENGLISH_NAMES__ is not None:
125
        return __PREFER_ENGLISH_NAMES__
126
127
    config_file = '%s/config.ini' % constants.application_directory
128
    if not path.exists(config_file):
129
        return False
130
131
    config = load_config()
132
    if('MapQuest' not in config):
133
        return False
134
135
    if('prefer_english_names' not in config['MapQuest']):
136
        return False
137
138
    __PREFER_ENGLISH_NAMES__ = bool(config['MapQuest']['prefer_english_names'])
139
    return __PREFER_ENGLISH_NAMES__
140
141
def place_name(lat, lon):
142
    lookup_place_name_default = {'default': __DEFAULT_LOCATION__}
143
    if(lat is None or lon is None):
144
        return lookup_place_name_default
145
146
    # Convert lat/lon to floats
147
    if(not isinstance(lat, float)):
148
        lat = float(lat)
149
    if(not isinstance(lon, float)):
150
        lon = float(lon)
151
152
    # Try to get cached location first
153
    db = Db()
154
    # 3km distace radious for a match
155
    cached_place_name = db.get_location_name(lat, lon, 3000)
156
    # We check that it's a dict to coerce an upgrade of the location
157
    #  db from a string location to a dictionary. See gh-160.
158
    if(isinstance(cached_place_name, dict)):
159
        return cached_place_name
160
161
    lookup_place_name = {}
162
    geolocation_info = lookup(lat=lat, lon=lon)
163
    if(geolocation_info is not None and 'address' in geolocation_info):
164
        address = geolocation_info['address']
165
        # gh-386 adds support for town
166
        # taking precedence after city for backwards compatability
167
        for loc in ['city', 'town', 'state', 'country']:
168
            if(loc in address):
169
                lookup_place_name[loc] = address[loc]
170
                # In many cases the desired key is not available so we
171
                #  set the most specific as the default.
172
                if('default' not in lookup_place_name):
173
                    lookup_place_name['default'] = address[loc]
174
175
    if(lookup_place_name):
176
        db.add_location(lat, lon, lookup_place_name)
177
        # TODO: Maybe this should only be done on exit and not for every write.
178
        db.update_location_db()
179
180
    if('default' not in lookup_place_name):
181
        lookup_place_name = lookup_place_name_default
182
183
    return lookup_place_name
184
185
186
def lookup(**kwargs):
187
    if(
188
        'location' not in kwargs and
189
        'lat' not in kwargs and
190
        'lon' not in kwargs
191
    ):
192
        return None
193
194
    if('lat' in kwargs and 'lon' in kwargs):
195
        kwargs['location'] = '{},{}'.format(kwargs['lat'], kwargs['lon'])
196
197
    key = get_key()
198
    prefer_english_names = get_prefer_english_names()
199
200
    if(key is None):
201
        return None
202
203
    try:
204
        headers = {}
205
        params = {'format': 'json', 'key': key}
206
        if(prefer_english_names):
207
            headers = {'Accept-Language':'en-EN,en;q=0.8'}
208
            params['locale'] = 'en_US'
209
        params.update(kwargs)
210
        path = '/geocoding/v1/address'
211
        if('lat' in kwargs and 'lon' in kwargs):
212
            path = '/geocoding/v1/reverse'
213
        url = '%s%s?%s' % (
214
                    constants.mapquest_base_url,
215
                    path,
216
                    urllib.parse.urlencode(params)
217
              )
218
        r = requests.get(url, headers=headers)
219
        return parse_result(r.json())
220
    except requests.exceptions.RequestException as e:
221
        log.error(e)
222
        return None
223
    except ValueError as e:
224
        log.error(r.text)
225
        log.error(e)
226
        return None
227
228
229
def parse_result(result):
230
    # gh-421
231
    # Return None if statusCode is not 0
232
    #   https://developer.mapquest.com/documentation/geocoding-api/status-codes/
233
    if( 'info' not in result or
234
        'statuscode' not in result['info'] or
235
        result['info']['statuscode'] != 0
236
       ):
237
        return None
238
239
    address = parse_result_address(result)
240
    if(address is None):
241
        return None
242
243
    result['address'] = address
244
    result['latLng'] = parse_result_latlon(result)
245
246
    return result
247
248
def parse_result_address(result):
249
    # We want to store the city, state and country
250
    # The only way determined to identify an unfound address is 
251
    #   that none of the indicies were found
252
    if( 'results' not in result or
253
        len(result['results']) == 0 or
254
        'locations' not in result['results'][0] or
255
        len(result['results'][0]['locations']) == 0
256
        ):
257
        return None
258
259
    index_found = False
260
    addresses = {'city': None, 'state': None, 'country': None}
261
    result_compat = {}
262
    result_compat['address'] = {}
263
264
265
    locations = result['results'][0]['locations'][0]
266
    # We are looping over locations to find the adminAreaNType key which
267
    #   has a value of City, State or Country.
268
    # Once we find it then we obtain the value from the key adminAreaN
269
    #   where N is a numeric index.
270
    # For example
271
    #   * adminArea1Type = 'City'
272
    #   * adminArea1 = 'Sunnyvale'
273
    for key in locations:
274
        # Check if the key is of the form adminArea1Type
275
        if(key[-4:] == 'Type'):
276
            # If it's a type then check if it corresponds to one we are intereated in
277
            #   and store the index by parsing the key
278
            key_prefix = key[:-4]
279
            key_index = key[-5:-4]
280
            if(locations[key].lower() in addresses):
281
                addresses[locations[key].lower()] = locations[key_prefix]
282
                index_found = True
283
284
    if(index_found is False):
285
        return None
286
287
    return addresses
288
289
def parse_result_latlon(result):
290
    if( 'results' not in result or
291
        len(result['results']) == 0 or
292
        'locations' not in result['results'][0] or
293
        len(result['results'][0]['locations']) == 0 or
294
        'latLng' not in result['results'][0]['locations'][0]
295
        ):
296
        return None
297
298
    latLng = result['results'][0]['locations'][0]['latLng'];
299
300
    return {'lat': latLng['lat'], 'lon': latLng['lng']}
301