Переглянути джерело

Initial geo implementation

master
Dirk Alders 4 роки тому
джерело
коміт
f59b19ed1f
4 змінених файлів з 1447 додано та 0 видалено
  1. 39
    0
      __init__.py
  2. 598
    0
      gps.py
  3. 628
    0
      osm.py
  4. 182
    0
      sun.py

+ 39
- 0
__init__.py Переглянути файл

@@ -0,0 +1,39 @@
1
+#!/usr/bin/env python
2
+# -*- coding: UTF-8 -*-
3
+"""
4
+geo (Geographic)
5
+================
6
+
7
+**Author:**
8
+
9
+* Dirk Alders <sudo-dirk@mount-mockery.de>
10
+
11
+**Description:**
12
+
13
+    This Module support functionalities around geographic issues.
14
+
15
+**Submodules:**
16
+
17
+* :mod:`geo.gps`
18
+* :mod:`geo.osm`
19
+* :mod:`geo.sun`
20
+
21
+**Unittest:**
22
+
23
+        See also the :download:`unittest <../../geo/_testresults_/unittest.pdf>` documentation.
24
+"""
25
+__DEPENDENCIES__ = []
26
+
27
+from geo import gps
28
+from geo import osm
29
+from geo import sun
30
+
31
+logger_name = 'GEO'
32
+
33
+__DESCRIPTION__ = """The Module {\\tt %s} is designed to \\ldots.
34
+For more Information read the sphinx documentation.""" % __name__.replace('_', '\_')
35
+"""The Module Description"""
36
+__INTERPRETER__ = (2, )
37
+"""The Tested Interpreter-Versions"""
38
+
39
+__all__ = ['gps', 'osm', 'sun']

+ 598
- 0
gps.py Переглянути файл

@@ -0,0 +1,598 @@
1
+#!/usr/bin/env python
2
+# -*- coding: utf-8 -*-
3
+#
4
+"""
5
+geo.gps (Geo-Positioning)
6
+=========================
7
+
8
+**Author:**
9
+
10
+* Dirk Alders <sudo-dirk@mount-mockery.de>
11
+
12
+**Description:**
13
+
14
+    This module is a submodule of :mod:`geo` and includes functions and classes for geographic issues (e.g. coordinate, area, tracks, ...).
15
+
16
+**Contentlist:**
17
+
18
+* :class:`geo.gps.area`
19
+* :class:`geo.gps.coordinate`
20
+* :class:`geo.gps.tracklist`
21
+* :class:`geo.gps.track`
22
+
23
+**Unittest:**
24
+
25
+    See also the :download:`unittest <../../geo/_testresults_/unittest.pdf>` documentation.
26
+"""
27
+
28
+import calendar
29
+import math
30
+import time
31
+import xml.parsers.expat
32
+
33
+
34
+class area(object):
35
+    """
36
+    :param coord1: Corner Coordinate 1
37
+    :type coord1: coordinate
38
+    :param coord2: Corner Coordinate 2
39
+    :type coord2: coordinate
40
+
41
+    Class to store a geographic area and support some calculations.
42
+
43
+    **Example:**
44
+
45
+    .. code-block:: python
46
+
47
+        >>> import geo
48
+
49
+        >>> ar = geo.gps.area(...)
50
+    """
51
+    def __init__(self, coord1, coord2):
52
+        min_lon = min(coord1[coordinate.LONGITUDE], coord2[coordinate.LONGITUDE])
53
+        max_lon = max(coord1[coordinate.LONGITUDE], coord2[coordinate.LONGITUDE])
54
+        min_lat = min(coord1[coordinate.LATITUDE], coord2[coordinate.LATITUDE])
55
+        max_lat = max(coord1[coordinate.LATITUDE], coord2[coordinate.LATITUDE])
56
+        self.coord1 = coordinate(lon=min_lon, lat=min_lat)
57
+        self.coord2 = coordinate(lon=max_lon, lat=max_lat)
58
+
59
+    def _max_lat(self):
60
+        return self.coord2[coordinate.LATITUDE]
61
+
62
+    def _max_lon(self):
63
+        return self.coord2[coordinate.LONGITUDE]
64
+
65
+    def _min_lat(self):
66
+        return self.coord1[coordinate.LATITUDE]
67
+
68
+    def _min_lon(self):
69
+        return self.coord1[coordinate.LONGITUDE]
70
+
71
+    def center_pos(self):
72
+        """
73
+        .. warning:: Needs sphinx documentation!
74
+        """
75
+        clon = (self.coord1[coordinate.LONGITUDE] + self.coord2[coordinate.LONGITUDE]) / 2
76
+        clat = (self.coord1[coordinate.LATITUDE] + self.coord2[coordinate.LATITUDE]) / 2
77
+        return coordinate(lon=clon, lat=clat)
78
+
79
+    def coordinate_in_area(self, coord):
80
+        """
81
+        .. warning:: Needs sphinx documentation!
82
+        """
83
+        lon = coord[coordinate.LONGITUDE]
84
+        lat = coord[coordinate.LATITUDE]
85
+        return lon >= self._min_lon() and lon <= self._max_lon() and lat >= self._min_lat() and lat <= self._max_lat()
86
+
87
+    def corner_coordinates(self):
88
+        """
89
+        .. warning:: Needs sphinx documentation!
90
+        """
91
+        return self.coord1, self.coord2
92
+
93
+    def extend_area(self, coord):
94
+        """
95
+        .. warning:: Needs sphinx documentation!
96
+        """
97
+        if coord[coordinate.LONGITUDE] < self.coord1[coordinate.LONGITUDE]:
98
+            self.coord1[coordinate.LONGITUDE] = coord[coordinate.LONGITUDE]
99
+        elif coord[coordinate.LONGITUDE] > self.coord2[coordinate.LONGITUDE]:
100
+            self.coord2[coordinate.LONGITUDE] = coord[coordinate.LONGITUDE]
101
+
102
+        if coord[coordinate.LATITUDE] < self.coord1[coordinate.LATITUDE]:
103
+            self.coord1[coordinate.LATITUDE] = coord[coordinate.LATITUDE]
104
+        elif coord[coordinate.LATITUDE] > self.coord2[coordinate.LATITUDE]:
105
+            self.coord2[coordinate.LATITUDE] = coord[coordinate.LATITUDE]
106
+
107
+    def osm_map(self, map_code):
108
+        """
109
+        :param map_code: Map code as defined in :class:`geo.osm` (e.g. :class:`geo.osm.MAP_STANDARD`)
110
+
111
+        This Method returns a :class:`geo.osm.map` instance.
112
+
113
+        .. warning:: Needs sphinx documentation!
114
+        """
115
+        # TODO: needs to be implemented
116
+        pass
117
+
118
+    def __str__(self):
119
+        return "%s / %s" % self.coordinates()
120
+
121
+
122
+class coordinate(dict):
123
+    """
124
+    :param lon: Londitude
125
+    :type lon: float
126
+    :param lat: Latitude
127
+    :type lat: float
128
+    :param height: Height
129
+    :type height: float
130
+    :param time: Time (Seconds since 1970)
131
+    :type time: int
132
+
133
+    Class to store a geographic coodinate and support some calculations.
134
+
135
+    **Example:**
136
+
137
+    .. code-block:: python
138
+
139
+        >>> import geo
140
+
141
+        >>> ab = geo.gps.coordinate(lat=49.976596,lon=9.1481443)
142
+        >>> gb = geo.gps.coordinate(lat=53.6908298,lon=12.1583252)
143
+        >>> ab.dist_to(gb) / 1000
144
+        462.3182843470017
145
+        >>> ab.angle_to(gb) / math.pi * 180
146
+        39.02285256685333
147
+    """
148
+    LATITUDE = 'lat'
149
+    LONGITUDE = 'lon'
150
+    HIGHT = 'hight'
151
+    TIME = 'time'
152
+
153
+    def __init__(self, **kwargs):
154
+        dict.__init__(self, **kwargs)
155
+
156
+    def __str__(self):
157
+        def to_string(lon_or_lat, plus_minus=('N', 'S')):
158
+            degrees = int(lon_or_lat)
159
+            lon_or_lat -= degrees
160
+            minutes = lon_or_lat * 60
161
+            pm = 0 if degrees >= 0 else 1
162
+            return "%d°%.4f%s" % (abs(degrees), abs(minutes), plus_minus[pm])
163
+        lon = self.get(self.LONGITUDE)
164
+        lat = self.get(self.LATITUDE)
165
+        if lon is not None and lat is not None:
166
+            return to_string(lat) + ' ' + to_string(lon, ['E', 'W'])
167
+        else:
168
+            return None
169
+
170
+    def angle_to(self, coord):
171
+        """
172
+        This Method calculates the geographic direction in radiant from this to the given coordinate.
173
+
174
+        .. note:: North is 0 (turning right). That means east is :class:`math.pi`/2.
175
+
176
+        :param coord: Target coordinate.
177
+        :type coord: corrdinate
178
+        :returns: The geographic direction in radiant.
179
+        :rtype: int or float
180
+        """
181
+        lat1 = coord[self.LATITUDE]
182
+        lon1 = coord[self.LONGITUDE]
183
+        lat2 = self[self.LATITUDE]
184
+        lon2 = self[self.LONGITUDE]
185
+        if lat1 is not None and lat2 is not None and lon1 is not None and lon2 is not None:
186
+            dlon = lon1 - lon2
187
+            dlat = lat1 - lat2
188
+
189
+            if dlat > 0:
190
+                # case (half circle north)
191
+                angle = math.atan(dlon / dlat)
192
+                pass
193
+            elif dlat < 0:
194
+                # case (half circle south)
195
+                angle = math.pi + math.atan(dlon / dlat)
196
+            elif dlon > 0:
197
+                # case (east)
198
+                angle = math.pi / 2
199
+            elif dlon < 0:
200
+                # case (west)
201
+                angle = math.pi * 3 / 2
202
+            else:
203
+                # same point
204
+                return None
205
+
206
+            if angle < 0:
207
+                angle += 2 * math.pi
208
+            return angle
209
+        else:
210
+            return None
211
+
212
+    def dist_to(self, coord):
213
+        """
214
+        This Method calcultes the distance from this coordinate to a given coordinate.
215
+
216
+        :param coord: Target coordinate.
217
+        :type coord: coordinate
218
+        :return: The distance between two coordinates in meters.
219
+        :rtype: int or float
220
+        :raises: -
221
+        """
222
+        lat1 = coord[self.LATITUDE]
223
+        lon1 = coord[self.LONGITUDE]
224
+        lat2 = self[self.LATITUDE]
225
+        lon2 = self[self.LONGITUDE]
226
+        if lat1 is not None and lat2 is not None and lon1 is not None and lon2 is not None:
227
+            R = 6378140
228
+            dLat = math.radians(lat2 - lat1)
229
+            dLon = math.radians(lon2 - lon1)
230
+            a = math.sin(dLat / 2) * math.sin(dLat / 2) + math.cos(math.radians(lat1)) * math.cos(math.radians(lat2)) * math.sin(dLon / 2) * math.sin(dLon / 2)
231
+            c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
232
+            return R * c
233
+        else:
234
+            return None
235
+
236
+
237
+class tracklist(list):
238
+    """
239
+    Class to store a a list of tracks and parse xml files created by a navigation system like Etrax Vista.
240
+
241
+    **Example:**
242
+
243
+    .. code-block:: python
244
+
245
+        >>> import geo
246
+
247
+        >>> ...
248
+    """
249
+    def __init__(self):
250
+        list.__init__(self)
251
+        self.__xml_track_on_read = None
252
+        self.__xml_data_on_read = ''
253
+
254
+    def __xml_start_element(self, name, attrs):
255
+        self.__xml_data_on_read = ''
256
+        if name == 'trk':
257
+            # new track found in file
258
+            self.__xml_track_on_read = track()
259
+        elif name == 'trkpt':
260
+            # new waypoint to append
261
+            if 'lon' in attrs and 'lat' in attrs:
262
+                self.__xml_track_on_read.append(coordinate(lon=float(attrs['lon']), lat=float(attrs['lat'])))
263
+
264
+    def __xml_end_element(self, name):
265
+        if name == 'trk':
266
+            if self.__xml_track_on_read is not None and len(self.__xml_track_on_read) > 0:
267
+                    self.append(self.__xml_track_on_read)
268
+            self.__xml_track_on_read = None
269
+        elif name == 'name':
270
+            if self.__xml_data_on_read != '':
271
+                self.__xml_track_on_read.set_name(self.__xml_data_on_read)
272
+        elif name == 'ele':
273
+            c = self.__xml_track_on_read[len(self.__xml_track_on_read) - 1]
274
+            c[c.HIGHT] = float(self.__xml_data_on_read)
275
+        elif name == 'time':
276
+            c = self.__xml_track_on_read[len(self.__xml_track_on_read) - 1]
277
+            c[c.TIME] = int(calendar.timegm(time.strptime(self.__xml_data_on_read, '%Y-%m-%dT%H:%M:%SZ')))
278
+
279
+    def __xml_char_data(self, data):
280
+        self.__xml_data_on_read += data
281
+
282
+    def load_from_file(self, xmlfilehandle):
283
+        """
284
+        .. warning:: Needs to be documented
285
+        """
286
+        # TODO: implement either usage of a filename or filehandle
287
+        # parse xml-handle
288
+        p = xml.parsers.expat.ParserCreate()
289
+        p.StartElementHandler = self.__xml_start_element
290
+        p.EndElementHandler = self.__xml_end_element
291
+        p.CharacterDataHandler = self.__xml_char_data
292
+        p.ParseFile(xmlfilehandle)
293
+
294
+
295
+class track(list):
296
+    """
297
+    Class to store a a tracks and support some calculations.
298
+
299
+    **Example:**
300
+
301
+    .. code-block:: python
302
+
303
+        >>> import geo
304
+
305
+        >>> ...
306
+    """
307
+    def __init__(self):
308
+        self._name = None
309
+        list.__init__(self)
310
+        self.__init_state_variables()
311
+
312
+    def __init_state_variables(self):
313
+        self._area = None
314
+        self._average_speed = None
315
+        self._hightcharacteristic = None
316
+        self._end_date = None
317
+        self._optimized_track = None
318
+        self._passed_hight = None
319
+        self._speedcharacteristic = None
320
+        self._start_date = None
321
+        self._total_distance = None
322
+        self._total_time = None
323
+
324
+    def append(self, coord):
325
+        """
326
+        .. warning:: Needs to be documented
327
+        """
328
+        list.append(self, coord)
329
+        self.__init_state_variables()
330
+
331
+    def extend(self, *args, **kwargs):
332
+        """
333
+        .. warning:: Needs to be documented
334
+        """
335
+        self.__init_state_variables()
336
+        return list.extend(self, *args, **kwargs)
337
+
338
+    def insert(self, index, coord):
339
+        """
340
+        .. warning:: Needs to be documented
341
+        """
342
+        list.insert(self, index, coord)
343
+        self.__init_state_variables()
344
+
345
+    def set_name(self, name):
346
+        """
347
+        .. warning:: Needs to be documented
348
+        """
349
+        self._name = name
350
+
351
+    def area(self):
352
+        """
353
+        :rtype: geo.gps.area or None
354
+
355
+        .. warning:: Needs to be documented
356
+        """
357
+        if self._area is None:
358
+            if len(self) > 1:
359
+                self._area = area()
360
+                for c in self:
361
+                    self._area.extend_area(c)
362
+        return self._area
363
+
364
+    def average_speed(self):
365
+        """
366
+        .. warning:: Needs to be documented
367
+        """
368
+        if self._average_speed is None:
369
+            self._average_speed = self.total_distance() / self.total_time()
370
+        return self._average_speed
371
+
372
+    def hightcharacteristic(self):
373
+        """
374
+        .. warning:: Needs to be documented
375
+        """
376
+        if self._hightcharacteristic is None:
377
+            pass    # TODO: implement functionality
378
+        return self._hightcharacteristic
379
+
380
+    def end_date(self):
381
+        """
382
+        .. warning:: Needs to be documented
383
+        """
384
+        if self._end_date is None:
385
+            if len(self) > 0:
386
+                self._end_date = self[len(self) - 1].get(coordinate.TIME)
387
+        return self._end_date
388
+
389
+    def name(self):
390
+        """
391
+        .. warning:: Needs to be documented
392
+        """
393
+        return self._name
394
+
395
+    def optimized_track(self):
396
+        """
397
+        .. warning:: Needs to be documented
398
+        """
399
+        # TODO: REWORK TRACK OPTIMIZATION
400
+        DIST_MOVED = 15.                # 15m
401
+        ACCELERATION_MAX = 0.5 * 9.81   # 0.5g
402
+        ANGLE_DIF_FOR_BACKWARDS = 10    # +/- 5deg
403
+        MAX_DELETED_POINTS = 30
404
+
405
+        def backwards_direction(l_angle, t_angle):
406
+            dang = l_angle - t_angle
407
+            if dang < 0:
408
+                dang += math.degrees(math.pi * 2)
409
+            if dang > math.degrees(math.pi) - ANGLE_DIF_FOR_BACKWARDS / 2 and dang < math.degrees(math.pi) + ANGLE_DIF_FOR_BACKWARDS / 2:
410
+                return True
411
+            else:
412
+                return False
413
+
414
+        if self._optimized_track is None:
415
+            self._optimized_track = track()
416
+            self._optimized_track.set_name(self._name)
417
+            del_lst = []
418
+            for coord in self:
419
+                if len(self._optimized_track) == 0:
420
+                    # first item has to be added always
421
+                    self._optimized_track.append(coord)
422
+                else:
423
+                    last = self._optimized_track[-1:][0]
424
+                    #try:
425
+                    acc = coord.dist_to(last) / ((coord[coordinate.TIME] - last[coordinate.TIME]) ** 2)
426
+                    #except:
427
+                    #    acc = 0.0
428
+                    if len(self._optimized_track) > 1:
429
+                        # calculate last angle
430
+                        last_angle = self._optimized_track[-2:-1][0].angle_to(last)
431
+                        if last_angle is not None:
432
+                            last_angle = math.degrees(last_angle)
433
+                        # calculate this angle
434
+                        this_angle = last.angle_to(coord)
435
+                        if this_angle is not None:
436
+                            this_angle = math.degrees(this_angle)
437
+                    else:
438
+                        last_angle = this_angle = None
439
+                    if coord.dist_to(last) > DIST_MOVED:
440
+                        # distance ok.
441
+                        if acc < ACCELERATION_MAX:
442
+                            # acceleration ok.
443
+                            if this_angle is None or last_angle is None or not backwards_direction(last_angle, this_angle):
444
+                                # direction ok
445
+                                del_lst = []
446
+                                self._optimized_track.append(coord)
447
+                            else:
448
+                                del_lst.append(coord)
449
+                                #print "this one was in backwards direction (%d)!" % (len(del_lst))
450
+                                #print "  %.2fkm: last = %.1f\xc2\xb0 - this = %.1f\xc2\xb0" % (self.total_distance()/1000., last_angle, this_angle)
451
+                        else:
452
+                            del_lst.append(coord)
453
+                            #print "this one was with to high acceleration (%d)!" % (len(del_lst))
454
+                            #print "  %.2fkm: %.1fm/s\xc2\xb2" % (self.total_distance()/1000., acc)
455
+                    if len(del_lst) >= MAX_DELETED_POINTS:
456
+                        print("Moeglicherweise ist die Optimierung des Tracks noch nicht ausgereift genug.")
457
+                        print("  Bei %.1f km gab es %d Koordinaten die aussortiert worden waeren. Optimierung ausgelassen." % (self.get_total_distance() / 1000., MAX_DELETED_POINTS))
458
+                        self._optimized_track.extend(del_lst)
459
+                        del_lst = []
460
+        return self._optimized_track
461
+
462
+    def osm_map(self, map_code):
463
+        return self.area().osm_map(map_code)
464
+
465
+    def passed_hight(self):
466
+        """
467
+        .. warning:: Needs to be documented
468
+        """
469
+        if self._passed_hight is None:
470
+            if len(self) > 0:
471
+                self._passed_hight = 0.0
472
+                hight = self[0][coordinate.HIGHT]
473
+                if hight is not None:
474
+                    for c in self:
475
+                        last_hight = hight
476
+                        # hysteresis of 1 meter
477
+                        hightlist = [c[coordinate.HIGHT] - 1, hight, c[coordinate.HIGHT] + 1]
478
+                        hightlist.sort()
479
+                        hight = hightlist[1]
480
+                        if hight > last_hight:
481
+                            self._passed_hight += hight - last_hight
482
+        return self._passed_hight
483
+
484
+    def speedcharacteristic(self):
485
+        """
486
+        .. warning:: Needs to be documented
487
+        """
488
+        if self._speedcharacteristic is None:
489
+            pass    # TODO: implement functionality
490
+        return self._speedcharacteristic
491
+
492
+    def start_date(self):
493
+        """
494
+        .. warning:: Needs to be documented
495
+        """
496
+        if self._start_date is None:
497
+            if len(self) > 0:
498
+                self._start_date = self[0].get(coordinate.TIME)
499
+        return self._start_date
500
+
501
+    def total_distance(self):
502
+        """
503
+        .. warning:: Needs to be documented
504
+        """
505
+        if self._total_distance is None:
506
+            if len(self) > 0:
507
+                self._total_distance = 0.
508
+                for i in range(0, len(self) - 1):
509
+                    self._total_distance += self[i].dist_to(self[i + 1])
510
+        return self._total_distance
511
+
512
+    def total_time(self):
513
+        """
514
+        .. warning:: Needs to be documented
515
+        """
516
+        if self._total_time is None:
517
+            try:
518
+                self._total_time = self.end_date() - self.start_date()
519
+            except TypeError:
520
+                pass    # doing nothing will return None as needed, if calculation is not possible
521
+        return self._total_time
522
+
523
+
524
+'''
525
+class gpxmanipu():
526
+    debug=False
527
+    class trackmanipu():
528
+        def __init__(self):
529
+            self.lines=[]
530
+        def AddLine(self, line):
531
+            self.lines.append(line)
532
+        def SetName(self, name):
533
+            SEARCHFORSTARTTAG=0
534
+            SEARCHFORENDTAG=1
535
+            state=SEARCHFORSTARTTAG
536
+            for i in range(0, len(self.lines)):
537
+                if state==SEARCHFORSTARTTAG:
538
+                    if self.lines[i].find('<name>')>0:
539
+                        newline=self.lines[i][:self.lines[i].find('<name>')+6]
540
+                        newline+=name
541
+                        state=SEARCHFORENDTAG
542
+                    if state==SEARCHFORENDTAG:
543
+                        if self.lines[i].find('</name>')>0:
544
+                            self.lines[i]=newline+self.lines[i][self.lines[i].find('</name>'):]
545
+                        else:
546
+                            self.lines.remove(self.lines[i])
547
+        def GetTrack(self):
548
+            rv=''
549
+            for line in self.lines:
550
+                rv+=line
551
+            return rv
552
+    def __init__(self, fh):
553
+        HEADERSEARCH=0
554
+        TRACKSEARCH=1
555
+        state=HEADERSEARCH
556
+        self.header=''
557
+        self.tracks=[]
558
+        for line in fh:
559
+            if state==HEADERSEARCH:
560
+                if line.lstrip().startswith('<trk>'):
561
+                    state=TRACKSEARCH
562
+                    track=self.trackmanipu()
563
+                    track.AddLine(line)
564
+                else:
565
+                    self.header+=line
566
+            elif state==TRACKSEARCH:
567
+                if line.lstrip().startswith('</trk>'):
568
+                    track.AddLine(line)
569
+                    self.tracks.append(track)
570
+                    track=self.trackmanipu()
571
+                else:
572
+                    track.AddLine(line)
573
+        self.footer=track.GetTrack() # This was no track
574
+        del(track)
575
+        if self.debug:
576
+            print "Header found:"
577
+            print self.header[:20]+' ... '+self.header[-20:]
578
+            print str(len(self.tracks))+' tracks found:'
579
+            for track in self.tracks:
580
+                track=track.GetTrack()
581
+                print track[:20]+' ... '+track[-20:]
582
+            print "Footer found:"
583
+            print self.footer
584
+    def GetGpx(self):
585
+        rv=self.header
586
+        for track in self.tracks:
587
+            rv+=track.GetTrack()
588
+        rv+=self.footer
589
+        return rv
590
+    def SetTrackname(self, number, name):
591
+        if len(self.tracks)>number-1:
592
+            self.tracks[number].SetName(name)
593
+    def DeleteTrack(self, number):
594
+        if len(self.tracks)>number-1:
595
+            return self.tracks.pop(number)
596
+        else:
597
+            return None
598
+'''

+ 628
- 0
osm.py Переглянути файл

@@ -0,0 +1,628 @@
1
+#!/usr/bin/env python
2
+# -*- coding: utf-8 -*-
3
+#
4
+"""
5
+geo.osm (Open Streetmap)
6
+========================
7
+
8
+**Author:**
9
+
10
+* Dirk Alders <sudo-dirk@mount-mockery.de>
11
+
12
+**Description:**
13
+
14
+    This module is a submodule of :mod:`geo` and supports functions and classes for Open Streetmap.
15
+
16
+**Contentlist:**
17
+
18
+* :func:`geo.osm.landmark_link`
19
+
20
+**Unittest:**
21
+
22
+    See also the :download:`unittest <../../geo/_testresults_/unittest.pdf>` documentation.
23
+"""
24
+
25
+
26
+MAP_STANDARD = 'N'
27
+"""MAP definition for Standard Map"""
28
+MAP_LOCAL_TRAFIC = 'TN'
29
+"""MAP definition for Local Trafic Map"""
30
+MAP_CYCLEMAP = 'CN'
31
+"""MAP definition for Cyclemap"""
32
+MAP_HUMANITARIAN = 'HN'
33
+"""MAP definition for Humanitarian Map"""
34
+
35
+
36
+def landmark_link(coord, zoom_level=13, map_code=MAP_STANDARD):
37
+    """
38
+    :param coord: Target coordinate.
39
+    :type coord: geo.gps.coordinate
40
+    :param zoom_level: The zoom level of the map (see https://wiki.openstreetmap.org/wiki/Zoom_levels for more information)
41
+    :type zoom_level: int
42
+    :param map_code: One of the map_codes :class:`MAP_STANDARD`, :class:`MAP_LOCAL_TRAFIC`, :class:`MAP_CYCLEMAP`, :class:`MAP_HUMANITARIAN`.
43
+    :type map_code: str
44
+    :return: An openstreetmap-url for marking a position in a map.
45
+    :rtype: str
46
+
47
+    This Method generates an openstreetmap-url for marking a position in a map.
48
+
49
+    .. code-block:: python
50
+
51
+        >>> import geo
52
+
53
+        >>> gb = geo.gps.coordinate(lat=53.6908298,lon=12.1583252)
54
+        >>> geo.osm.landmark_link(gb)
55
+        'http://www.openstreetmap.org?mlat=53.690830&mlon=12.158325&zoom=13&layers=N'
56
+    """
57
+    lon = coord[coord.LONGITUDE]
58
+    lat = coord[coord.LATITUDE]
59
+    #
60
+    if lon is not None and lat is not None:
61
+        link = 'http://www.openstreetmap.org?mlat=%(' + coord.LATITUDE + ')f&mlon=%(' + coord.LONGITUDE + ')f&zoom=%(zoom)d&layers=%(map)s'
62
+        return link % {coord.LATITUDE: lat,
63
+                       coord.LONGITUDE: lon,
64
+                       'zoom': zoom_level,
65
+                       'map': map_code}
66
+    else:
67
+        return None
68
+
69
+
70
+class map_spec(object):
71
+    def __init__(self, **kwargs):
72
+        coord1 = kwargs.get('coord1')
73
+        coord2 = kwargs.get('coord2')
74
+        zoom_level = kwargs.get('zoom_level')
75
+        x = kwargs.get('x')
76
+        y = kwargs.get('y')
77
+
78
+    def get_resolution(self):
79
+        pass
80
+
81
+    def get_coord_range(self):
82
+        pass
83
+
84
+    def get_map(self):
85
+        pass
86
+
87
+    def point_to_coord(self, xy):
88
+        pass
89
+
90
+    def coord_to_point(self, coord):
91
+        pass
92
+
93
+class osm_map(object):
94
+    def __init__(self, **kwargs):
95
+        self.__map_spec__ = map_spec(kwargs)
96
+        map_code = kwargs.get('map_code')
97
+
98
+
99
+'''
100
+class get_from_web():
101
+    """
102
+    Class to download images from web.
103
+    """
104
+    def __init__(self, props=None):
105
+        """
106
+        Init routine for class get_from_web.
107
+        @param props: myapptools.AppProp instance with proxy information. This has to be a dictionary (see self._set_props).
108
+        """
109
+        self.props = props
110
+        if props != None:
111
+            # install a callback if property 'Proxy' had been changed. it will be called with the new proxy information.
112
+            props.InstallPostSetCallback('Proxy', self._props_callback)
113
+
114
+    def _props_callback(self, proxy):
115
+        """
116
+        Routione which is called, if proxy information had been changed. It will set the new proxy information.
117
+        @param proxy: dictionary with proxy information
118
+        """
119
+        self._set_props(**proxy)
120
+
121
+    def _set_props(self, use_proxy, host, port, use_user, user, use_passwd, passwd=None):
122
+        """
123
+        Routine to set the proxy information.
124
+        @param host: host to connect to
125
+        @param port: port which is used to connect the proxy
126
+        @param user: username for proxy
127
+        @param passwd: password to be used for user
128
+        """
129
+        proxy_str = None
130
+        if not use_proxy:
131
+            proxy_support = urllib2.ProxyHandler({})
132
+            opener = urllib2.build_opener(proxy_support, urllib2.HTTPHandler)
133
+            urllib2.install_opener(opener)
134
+        elif not use_user:
135
+            proxy_str = "http://%s:%d" % (host, port)
136
+        elif not use_passwd or passwd == None:
137
+            proxy_str = "http://%s@%s:%d" % (user, host, port)
138
+        else:
139
+            proxy_str = "http://%s:%s@%s:%d" % (user, passwd, host, port)
140
+        if proxy_str != None:
141
+            proxy_support = urllib2.ProxyHandler({"http": proxy_str})
142
+            opener = urllib2.build_opener(proxy_support, urllib2.HTTPHandler)
143
+            urllib2.install_opener(opener)
144
+
145
+    def get_image(self, url):
146
+        """
147
+        Routine to download an image from web.
148
+        @param url: url to the image
149
+        @return: image (type: python Image)
150
+        """
151
+        #try:
152
+        f = urllib2.urlopen(url)
153
+        im = Image.open(StringIO.StringIO(f.read()))
154
+        f.close()
155
+        return im
156
+        #except ???:
157
+        #    print "exception in get_from_web().get_image('%s')" % url
158
+        #    return None
159
+
160
+
161
+class tile_handler(dict):
162
+    """
163
+    Class to handle some tile_source classes and a default tile_source.
164
+    """
165
+    TILE_SIZE = 256
166
+
167
+    class tile_source():
168
+        """
169
+        Class to get tile by cache or url. It also stores the downloaded tiles to the cache directory.
170
+        """
171
+        TILEPATH = 'osm_tiles'
172
+
173
+        def __init__(self, gfw, name, url, min_zoom, max_zoom):
174
+            """
175
+            Init routine for class tile_source.
176
+            @param gfw: instance of get_from_web
177
+            @param name: name of tile_type
178
+            @param url: base url without zoom or tile information
179
+            @param min_zoom: minimum existing zoom level for this tile type
180
+            @param max_zoom: maximum existing zoom level for this tile type
181
+            """
182
+            self.gfw = gfw
183
+            self._name = name
184
+            self._url = url
185
+            self._zooms = range(min_zoom, max_zoom + 1)
186
+
187
+        def get_path(self, x, y, zoom_lvl):
188
+            """
189
+            Routine to get the tile-path information for a specific tile.
190
+            @param x: horizontal tile number
191
+            @param y: vertical tile number
192
+            @param zoom_lvl: zoom level for the tile
193
+            @return: path to tile as string
194
+            """
195
+            def intstr(num):
196
+                return str(int(num))
197
+            return os.path.join(__basepath__, self.TILEPATH, self._name, intstr(zoom_lvl), intstr(x), intstr(y) + '.png')
198
+
199
+        def get_url(self, x, y, zoom_lvl):
200
+            """
201
+            Routine to get the url information for a specific tile.
202
+            @param x: horizontal tile number
203
+            @param y: vertical tile number
204
+            @param zoom_lvl: zoom level for the tile
205
+            @return: url to tile as string
206
+            """
207
+            def intstr(num):
208
+                return str(int(num))
209
+            return self._url + '/' + intstr(zoom_lvl) + '/' + intstr(x) + '/' + intstr(y) + '.png'
210
+
211
+        def get_zooms(self):
212
+            """
213
+            Routine to get a list of available zoom levels for this source.
214
+            @return: zoom levels as a list (e.g. [0,1,2,3]).
215
+            """
216
+            return self._zooms
217
+
218
+        def get_tile(self, x, y, zoom_lvl, max_age, from_cache):
219
+            """
220
+            Routine to get a tile.
221
+            @param x: horizontal tile number
222
+            @param y: vertical tile number
223
+            @param zoom_lvl: zoom level for the tile
224
+            @param max_age: maximum age where no www-refresh is needed
225
+            @param from_cache: if True the tile is from cache
226
+            @return: tile as Image
227
+            """
228
+            filename = self.get_path(x, y, zoom_lvl)
229
+            url = self.get_url(x, y, zoom_lvl)
230
+            if from_cache:
231
+                try:
232
+                    return Image.open(filename)
233
+                except:
234
+                    return None
235
+            else:
236
+                local_time = calendar.timegm(time.gmtime())
237
+                try:
238
+                    tile_tm = os.path.getmtime(filename)
239
+                except:
240
+                    tile_tm = local_time - max_age - 1
241
+                if local_time - tile_tm > max_age:  # age depending refresh.
242
+                    im = self.gfw.get_image(url)
243
+                    try:
244
+                        self.save(im, filename)
245
+                    except:
246
+                        print "exception in tile_handler().get_tile"
247
+                        #TODO: exception handling.
248
+                        pass
249
+                    return im
250
+                else:
251
+                    return None
252
+
253
+        def save(self, im, filename):
254
+            """
255
+            Routine to save the image to cache (directory).
256
+            @param im: image to save (type: python Image)
257
+            @param filename: name of the file, which will be created
258
+            """
259
+            dirname = os.path.dirname(filename)
260
+            if not os.path.exists(dirname):
261
+                os.makedirs(dirname)
262
+            im.save(filename)
263
+
264
+    def __init__(self, gfw, props=None):
265
+        """
266
+        Init routine for class tile_handler
267
+        @param gfw: instance of get_from_web
268
+        @param props: myapptools.AppProp instance with tilehandler information. This has to be a dictionary (see self._set_proxy).
269
+        """
270
+        dict.__init__(self)
271
+        self.gfw = gfw
272
+        self._active_tile_source = None
273
+        self._max_age = 3600
274
+        self._append_tile_source(u'OSM-Mapnik', u'http://tile.openstreetmap.org', 0, 18)
275
+        self._append_tile_source(u'OSM-CycleMap', u'http://c.tile.opencyclemap.org/cycle', 0, 18)
276
+        try:
277
+            # install a callback if property 'Tilehandler' had been changed. it will be called with the new tilehandler information.
278
+            props.InstallPostSetCallback('Tilehandler', self._props_callback)
279
+        except:
280
+            #TODO: exception handling
281
+            pass
282
+
283
+    def _props_callback(self, tilehandler):
284
+        """
285
+        Routione which is called, if tilehandler information had been changed. It will set the new tilehandler information.
286
+        @param tilehandler: dictionary with tilehandler information
287
+        """
288
+        self.set_props(**tilehandler)
289
+
290
+    def set_props(self, source, max_age):
291
+        """
292
+        Routine to set the proxy information.
293
+        @param source: source for tiles.
294
+        @param max_age: maximum age for a tile till it will be refreshed.
295
+        """
296
+        self._set_default(source)
297
+        self._max_age = max_age
298
+
299
+    def _append_tile_source(self, name, url, min_zoom, max_zoom):
300
+        """
301
+        Routine to append a tilesource.
302
+        @param name: Name for this tilesource
303
+        @param url: URL for this tile source (without tile depending information e.g. zoom level, ...)
304
+        @param min_zoom: Minimum zoom level for this tilesource
305
+        @param max_zoom: Maximum zoom level for this tilesource
306
+        """
307
+        self[name] = self.tile_source(self.gfw, name, url, min_zoom, max_zoom)
308
+        if self._active_tile_source == None:
309
+            self._set_default(name)
310
+
311
+    def _set_default(self, name):
312
+        """
313
+        Routine to set the default tilesorce (by name).
314
+        @param name: Name for the default tilesource.
315
+        @return: True if name was available, False if not.
316
+        """
317
+        if name in self.keys():
318
+            self._active_tile_source = name
319
+            return True
320
+        else:
321
+            return False
322
+
323
+    def get_active_source(self):
324
+        """
325
+        Routine to get the Name of the active tile source.
326
+        @return: name of the active tile source
327
+        """
328
+        return self._active_tile_source
329
+
330
+    def get_max_age(self):
331
+        return self._max_age
332
+
333
+    def get_choices(self):
334
+        """
335
+        Routine to get the names of the possible tile sources.
336
+        @return: list of possible tile sources
337
+        """
338
+        return self.keys()
339
+
340
+    def get_zooms(self):
341
+        """
342
+        Routine to get a list of available zoom levels for this source.
343
+        @return: zoom levels as a list (e.g. [0,1,2,3]).
344
+        """
345
+        return self[self._active_tile_source].get_zooms()
346
+
347
+    def get_url(self, x, y, zoom_lvl):
348
+        """
349
+        Routine to get the url information for a specific tile.
350
+        @param x: horizontal tile number
351
+        @param y: vertical tile number
352
+        @param zoom_lvl: zoom level for the tile
353
+        @return: url to tile as string
354
+        """
355
+        return self[self._active_tile_source].get_url(x, y, zoom_lvl)
356
+
357
+    def get_tile(self, x, y, zoom_lvl, from_cache):
358
+        """
359
+        Routine to get a tile.
360
+        @param x: horizontal tile number
361
+        @param y: vertical tile number
362
+        @param zoom_lvl: zoom level for the tile
363
+        @param from_cache: if True the tile is from cache
364
+        @return: tile as Image
365
+        """
366
+        return self[self._active_tile_source].get_tile(x, y, zoom_lvl, self._max_age, from_cache)
367
+
368
+    def tile_num(self, coordinate, zoom):
369
+        """
370
+        Routine which calculates the needed tile for coordinates.
371
+        @param coordinate: geo.coordinate instance with geographic information.
372
+        @param zoom: zoom information for the needed tile
373
+        @return: return a tuple of two float values (x- and y-tile)
374
+        """
375
+        lat_rad = math.radians(coordinate[pylibs.geo.coordinate.LATITUDE])
376
+        n = 2.0 ** zoom
377
+        xtile = (coordinate[pylibs.geo.coordinate.LONGITUDE] + 180.0) / 360.0 * n
378
+        ytile = (1.0 - math.log(math.tan(lat_rad) + (1 / math.cos(lat_rad))) / math.pi) / 2.0 * n
379
+        return (xtile, ytile)
380
+
381
+    def coordinate(self, xtile, ytile, zoom):
382
+        """
383
+        Routine which calculates geographic information out of tile information.
384
+        @param xtile: number of the tile (x)
385
+        @param ytile: number of the tile (y)
386
+        @param zoom: zoom level
387
+        @return: geo.coordinate instance with the geographic information
388
+        """
389
+        n = 2.0 ** zoom
390
+        lon_deg = xtile / n * 360.0 - 180.0
391
+        lat_rad = math.atan(math.sinh(math.pi * (1 - 2 * ytile / n)))
392
+        lat_deg = math.degrees(lat_rad)
393
+        return pylibs.geo.coordinate(lon=lon_deg, lat=lat_deg)
394
+
395
+
396
+class osm_map():
397
+    """
398
+    This is an Image including an osm map
399
+    """
400
+    def __init__(self, th, callback_refresh):
401
+        """
402
+        Init routine for osm_map
403
+        @param th: tile_handler class needed to get tiles
404
+        @param callback_refresh: function to call, if osm_map had been changed (for refresh view)
405
+                                 callback gets two arguments
406
+                                 * the image or None if no image is available yet
407
+                                 * a description text (of the finished issue)
408
+        @param border: additional border around specified area
409
+        @param vers: refresh behaviour as described in VER_*
410
+        """
411
+        self.th = th
412
+        self.callback_refresh = callback_refresh
413
+        self._del_map_values()
414
+
415
+        self.running = False
416
+        self.stoprequest = False
417
+
418
+    def _del_map_values(self):
419
+        """
420
+        routine to reset (to None) map definitions (e.g. map_resolution, zoom_lvl, view range
421
+        """
422
+        self._image = None
423
+        self._map_resolution = None
424
+        self._zoom_lvl = None
425
+        self._center_coordinate = None
426
+
427
+    def _set_map_values(self, map_resolution, zoom_lvl, center_coordinate):
428
+        def zoom_limitation(th, zoom):
429
+            if zoom < min(th.get_zooms()):
430
+                return min(th.get_zooms())
431
+            if zoom > max(th.get_zooms()):
432
+                return max(th.get_zooms())
433
+            return zoom
434
+        self._image = Image.new('RGB', map_resolution, 'white')
435
+        self._map_resolution = map_resolution
436
+        self._zoom_lvl = zoom_limitation(self.th, zoom_lvl)
437
+        self._center_coordinate = center_coordinate
438
+
439
+    def disable(self):
440
+        self.stop_now()
441
+        self.callback_refresh = None
442
+
443
+    def get_image(self):
444
+        return self._image
445
+
446
+    def get_map_resolution(self):
447
+        return self._map_resolution
448
+
449
+    def get_zoom_lvl(self):
450
+        return self._zoom_lvl
451
+
452
+    def get_center_coordinate(self):
453
+        return self._center_coordinate
454
+
455
+    def _paste_tile(self, tile, xy):
456
+        """
457
+        routine to paste a single tile at xy in the map image.
458
+        @param tile: tile to paste in the image
459
+        @param xy: position to paste the tile. also negative or too large values are
460
+                   possible to paste just parts of a tile
461
+        """
462
+        try:
463
+            self._image.paste(tile, xy)
464
+        except:
465
+            print "exception in osm_map()._paste_tile"
466
+            #TODO: exception handling
467
+
468
+    def create_map_by_res_n_centercoord_n_zoomlvl(self, max_x_res, max_y_res, center_coordinate, zoom_lvl, cache_only=False):
469
+        """
470
+        routine to ...
471
+        @param max_x_res: maximum x resolution
472
+        @param max_y_res: maximum y resolution
473
+        @param center_coordinate: center coordinates (object of geo.coordinates)
474
+        @param zoom_lvl: zoom_level to use for d
475
+        """
476
+        if center_coordinate is not None and zoom_lvl is not None:
477
+            self._del_map_values()
478
+            #
479
+            # needed values for further calculations and map creation
480
+            #
481
+            self._set_map_values((max_x_res, max_y_res), zoom_lvl, center_coordinate)
482
+            self._create_map(cache_only)
483
+
484
+    def create_map_by_coord_n_zoomlvl(self, tl_coord, br_coord, zoom_lvl, cache_only=False):
485
+        """
486
+        @param tl_coord: top left coordinates (object of geo.coordinates)
487
+        @param br_coord: bottom right coordinates (object of geo.coordinates)
488
+        @param zoom_lvl: zoom_level to use for
489
+        """
490
+        center_coordinate = pylibs.geo.area(tl_coord, br_coord).center_pos()
491
+        tl_tile = self.th.tile_num(tl_coord, zoom_lvl)
492
+        br_tile = self.th.tile_num(br_coord, zoom_lvl)
493
+        max_x_res = int((br_tile[0] - tl_tile[0]) * self.th.TILE_SIZE)
494
+        max_y_res = int((br_tile[1] - tl_tile[1]) * self.th.TILE_SIZE)
495
+        self.create_map_by_res_n_centercoord_n_zoomlvl(max_x_res, max_y_res, center_coordinate, zoom_lvl, cache_only)
496
+
497
+    def create_map_by_res_n_coord(self, max_x_res, max_y_res, tl_coord, br_coord, cache_only=False):
498
+        """
499
+        @param max_x_res: maximum x resolution
500
+        @param max_y_res: maximum y resolution
501
+        @param tl_coord: top left coordinates (object of geo.coordinates)
502
+        @param br_coord: bottom right coordinates (object of geo.coordinates)
503
+
504
+        coord are not the used coordinated for the map corners, cause the zoom_lvl is quatisised
505
+        """
506
+        def coordinates_in_map(max_x, max_y, p1, p2, center, zoom_lvl):
507
+            tl = self.get_coord_by_xy(0, 0, (max_x, max_y), center, zoom_lvl)
508
+            br = self.get_coord_by_xy(max_x, max_y, (max_x, max_y), center, zoom_lvl)
509
+            area = pylibs.geo.area(tl, br)
510
+            return area.coordinate_in_area(p1) and area.coordinate_in_area(p2)
511
+        center_coordinate = pylibs.geo.area(tl_coord, br_coord).center_pos()
512
+        zoom_lvl = max(self.th.get_zooms())
513
+        while not coordinates_in_map(max_x_res, max_y_res, tl_coord, br_coord, center_coordinate, zoom_lvl):
514
+            zoom_lvl -= 1
515
+        self.create_map_by_res_n_centercoord_n_zoomlvl(max_x_res, max_y_res, center_coordinate, zoom_lvl, cache_only)
516
+
517
+    def stop_now(self):
518
+        self.stoprequest = True
519
+        while self.running:
520
+            pass
521
+        self.stoprequest = False
522
+
523
+    def get_coord_by_xy(self, x, y, map_resolution=None, center_coordinate=None, zoom_lvl=None):
524
+        zoom_lvl = zoom_lvl or self._zoom_lvl
525
+        tl_tile = self._get_tl_tile_num(map_resolution, center_coordinate, zoom_lvl)
526
+        xy_tile = (tl_tile[0] + float(x) / self.th.TILE_SIZE, tl_tile[1] + float(y) / self.th.TILE_SIZE)
527
+        return self.th.coordinate(xy_tile[0], xy_tile[1], zoom_lvl)
528
+
529
+    def get_xy_by_coord(self, coord, map_resolution=None, center_coordinate=None, zoom_lvl=None):
530
+        tl_tile = self._get_tl_tile_num(map_resolution, center_coordinate, zoom_lvl)
531
+        xy_tile = self.th.tile_num(coord, self._zoom_lvl)
532
+        x = int((xy_tile[0] - tl_tile[0]) * self.th.TILE_SIZE)
533
+        y = int((xy_tile[1] - tl_tile[1]) * self.th.TILE_SIZE)
534
+        return (x, y)
535
+
536
+    def _get_map_res_tiles(self, map_resolution=None):
537
+        """
538
+        returns the map resolution in number of tiles
539
+        """
540
+        map_resolution = map_resolution or self._map_resolution
541
+        if map_resolution:
542
+            return (map_resolution[0] / float(self.th.TILE_SIZE), map_resolution[1] / float(self.th.TILE_SIZE))
543
+        else:
544
+            return None
545
+
546
+    def _get_tl_tile_num(self, map_resolution=None, center_coordinate=None, zoom_lvl=None):
547
+        map_resolution = map_resolution or self._map_resolution
548
+        center_coordinate = center_coordinate or self._center_coordinate
549
+        zoom_lvl = zoom_lvl or self._zoom_lvl
550
+        #
551
+        if (map_resolution and center_coordinate and zoom_lvl):
552
+            center_tile_num = self.th.tile_num(center_coordinate, zoom_lvl)
553
+            map_resolution_tiles = self._get_map_res_tiles(map_resolution)
554
+            topleft_tile_num = (center_tile_num[0] - map_resolution_tiles[0] / 2, center_tile_num[1] - map_resolution_tiles[1] / 2)
555
+            return topleft_tile_num
556
+        else:
557
+            return None
558
+
559
+    def _get_br_tile_num(self, map_resolution=None, center_coordinate=None, zoom_lvl=None):
560
+        topleft_tile_num = self._get_tl_tile_num(map_resolution, center_coordinate, zoom_lvl)
561
+        map_resolution_tiles = self._get_map_res_tiles(map_resolution)
562
+        bottomright_tile_num = (topleft_tile_num[0] + map_resolution_tiles[0], topleft_tile_num[1] + map_resolution_tiles[1])
563
+        return bottomright_tile_num
564
+
565
+    def _get_xy_offset(self):
566
+        tl_tile = self._get_tl_tile_num()
567
+        x_offs = -int(tl_tile[0] % 1 * self.th.TILE_SIZE)
568
+        y_offs = -int(tl_tile[1] % 1 * self.th.TILE_SIZE)
569
+        return (x_offs, y_offs)
570
+
571
+    def _get_tile_list(self):
572
+        tl_tile = self._get_tl_tile_num()
573
+        br_tile = self._get_br_tile_num()
574
+        tile_list = []
575
+        for x in range(int(tl_tile[0]), int(br_tile[0]) + 1):
576
+            for y in range(int(tl_tile[1]), int(br_tile[1]) + 1):
577
+                tile_list.append((x, y, self._zoom_lvl))
578
+        return tile_list
579
+
580
+    def _create_map(self, cache_only):
581
+        """
582
+        @param resoultion: map target resolution
583
+        @param xy_offset: offset for top left tile (normally <= 0)
584
+        @param zoom_lvl: tile zoom_lvl
585
+        @param tile_list: list of tiles [[x1, x2, x3], [y1, y2]]
586
+        @param description: description text for callback function
587
+        """
588
+        def create_map_by_(xy_offset, tile_list, by_path):
589
+            #
590
+            # create map from already stored tiles (...by_path)
591
+            #
592
+            num_tiles = len(tile_list)
593
+            x0, y0 = tile_list[0][:2]
594
+            num = 0
595
+            for x, y, z in tile_list:
596
+                num += 1
597
+                if self.stoprequest:
598
+                    break
599
+                tile = self.th.get_tile(x, y, z, by_path)
600
+                if tile != None:
601
+                    # paste tile only if tile was available
602
+                    pos = (xy_offset[0] + (x - x0) * self.th.TILE_SIZE, xy_offset[1] + (y - y0) * self.th.TILE_SIZE)
603
+                    self._paste_tile(tile, pos)
604
+                    if not by_path:
605
+                        desc = "Tile " + self.th.get_url(x, y, z) + " added to map."
606
+                        prog = float(num) / num_tiles
607
+                        if self.callback_refresh:
608
+                            self.callback_refresh(self._image, desc, prog)
609
+        self.running = True
610
+        #TODO: ggf. Klasse um Uebersetzungen zu ermoeglichen.
611
+
612
+        create_map_by_(self._get_xy_offset(), self._get_tile_list(), by_path=True)
613
+        desc = 'Map creation from cache completeled.'
614
+        if self.callback_refresh:
615
+            self.callback_refresh(self._image, desc, 1.0)
616
+        if not cache_only:
617
+            create_map_by_(self._get_xy_offset(), self._get_tile_list(), by_path=False)
618
+            desc = 'Map creation completeled.'
619
+            if self.callback_refresh:
620
+                self.callback_refresh(self._image, desc, 1.0)
621
+        self.running = False
622
+
623
+
624
+def show_map(image, description, progress):
625
+    print description, "%5.1f%%" % (progress * 100.)
626
+    if image != None:
627
+        image.show()
628
+'''

+ 182
- 0
sun.py Переглянути файл

@@ -0,0 +1,182 @@
1
+#!/usr/bin/env python
2
+# -*- coding: utf-8 -*-
3
+#
4
+"""
5
+geo.sun (Sun)
6
+=============
7
+
8
+**Author:**
9
+
10
+* Dirk Alders <sudo-dirk@mount-mockery.de>
11
+* Formula from Dr. Roland Brodbeck, Calsky (http://lexikon.astronomie.info/zeitgleichung/neu.html)
12
+* First implementation by Alexander Klupp 2014-01-14
13
+
14
+**Description:**
15
+
16
+    This module is a submodule of :mod:`geo` and supports functions and classes for sun position.
17
+
18
+**Contentlist:**
19
+
20
+* :func:`geo.sun.sunset`
21
+* :func:`geo.sun.sunrise`
22
+
23
+**Unittest:**
24
+
25
+    See also the :download:`unittest <../../geo/_testresults_/unittest.pdf>` documentation.
26
+"""
27
+
28
+
29
+import calendar
30
+import math
31
+import time
32
+
33
+europe_berlin = {'lat': 52.520008, 'lon': 13.404954}
34
+
35
+
36
+def JulianischesDatum(Jahr, Monat, Tag, Stunde, Minuten, Sekunden):
37
+    if (Monat <= 2):
38
+        Monat = Monat + 12
39
+        Jahr = Jahr - 1
40
+    Gregor = (Jahr / 400) - (Jahr / 100) + (Jahr / 4)   # Gregorianischer Kalender
41
+    return 2400000.5 + 365 * Jahr - 679004 + Gregor + math.floor(30.6001 * (Monat + 1)) + Tag + Stunde / 24 + Minuten / 1440 + Sekunden / 86400
42
+
43
+
44
+def InPi(x):
45
+    n = int(x / (2 * math.pi))
46
+    x = x - n * 2 * math.pi
47
+    if (x < 0):
48
+        x += 2 * math.pi
49
+    return x
50
+
51
+
52
+def eps(T):
53
+    # Neigung der Erdachse
54
+    return math.pi / 180 * (23.43929111 + (-46.8150 * T - 0.00059 * T ** 2 + 0.001813 * T ** 3) / 3600)
55
+
56
+
57
+def BerechneZeitgleichung(T):
58
+    RA_Mittel = 18.71506921 + 2400.0513369 * T + (2.5862e-5 - 1.72e-9 * T) * T ** 2
59
+    M = InPi(2 * math.pi * (0.993133 + 99.997361 * T))
60
+    L = InPi(2 * math.pi * (0.7859453 + M / (2 * math.pi) + (6893 * math.sin(M) + 72 * math.sin(2 * M) + 6191.2 * T) / 1296e3))
61
+    e = eps(T)
62
+    RA = math.atan(math.tan(L) * math.cos(e))
63
+    if (RA < 0):
64
+        RA += math.pi
65
+    if (L > math.pi):
66
+        RA += math.pi
67
+    RA = 24 * RA / (2 * math.pi)
68
+    DK = math.asin(math.sin(e) * math.sin(L))
69
+    # Damit 0 <= RA_Mittel < 24
70
+    RA_Mittel = 24.0 * InPi(2 * math.pi * RA_Mittel / 24.0) / (2 * math.pi)
71
+    dRA = RA_Mittel - RA
72
+    if (dRA < -12.0):
73
+        dRA += 24.0
74
+    if (dRA > 12.0):
75
+        dRA -= 24.0
76
+    dRA = dRA * 1.0027379
77
+    return dRA, DK
78
+
79
+
80
+def Sonnenauf_untergang(JD, Zeitzone, coord):
81
+    # Zeitzone = 0 #Weltzeit
82
+    # Zeitzone = 1 #Winterzeit
83
+    # Zeitzone = 2 #Sommerzeit
84
+    # JD = JulianischesDatum
85
+
86
+    B = math.radians(coord.get('lat'))          # geographische Breite Erkelenz
87
+    GeographischeLaenge = coord.get('lon')      # geographische Laenge
88
+
89
+    JD2000 = 2451545
90
+    h = -50.0 / 60.0 * math.pi / 180
91
+    T = (JD - JD2000) / 36525
92
+
93
+    Zeitgleichung, DK = BerechneZeitgleichung(T)
94
+
95
+    Zeitdifferenz = 12 * math.acos((math.sin(h) - math.sin(B) * math.sin(DK)) / (math.cos(B) * math.cos(DK))) / math.pi
96
+
97
+    AufgangOrtszeit = 12 - Zeitdifferenz - Zeitgleichung
98
+    UntergangOrtszeit = 12 + Zeitdifferenz - Zeitgleichung
99
+    AufgangWeltzeit = AufgangOrtszeit - GeographischeLaenge / 15
100
+    UntergangWeltzeit = UntergangOrtszeit - GeographischeLaenge / 15
101
+
102
+    Aufgang = AufgangWeltzeit + Zeitzone
103
+    if (Aufgang < 0):
104
+        Aufgang += 24
105
+    elif (Aufgang >= 24):
106
+        Aufgang -= 24
107
+
108
+    AM = round(Aufgang * 60) / 60       # minutengenau runden
109
+
110
+    Untergang = UntergangWeltzeit + Zeitzone
111
+    if (Untergang < 0):
112
+        Untergang += 24
113
+    elif (Untergang >= 24):
114
+        Untergang -= 24
115
+
116
+    UM = round(Untergang * 60) / 60     # minutengenau runden
117
+
118
+    return AM, UM
119
+
120
+
121
+def sunrise(coord=europe_berlin, date=None):
122
+    """
123
+    :param coord: Target coordinate or None (default is central europe).
124
+    :type coord: geo.gps.coordinate
125
+    :param date: The day to calculate with or None (only year, month and day are relevant; default ist today)
126
+    :type date: time.struct_time
127
+    :return: The date and time information for the sunrise
128
+    :rtype: time.struct_time
129
+
130
+    This Method calculates the time for sunrise for a given date and coordinate.
131
+
132
+    .. code-block:: python
133
+
134
+        >>> import geo
135
+
136
+        >>> ...
137
+    """
138
+    date = date or time.localtime()
139
+
140
+    year, month, day = date[0:3]
141
+    dst = date[8]                             # Sommerzeit
142
+
143
+    AM = Sonnenauf_untergang(JulianischesDatum(year, month, day, 12, 0, 0), dst + 1, coord)[0]
144
+
145
+    tup_list = list(date)
146
+    tup_list[3] = 0
147
+    tup_list[4] = 0
148
+    tup_list[5] = 0
149
+    tm = time.mktime(time.struct_time(tup_list))
150
+    return time.localtime(tm + AM * 3600)
151
+
152
+
153
+def sunset(coord=europe_berlin, date=None):
154
+    """
155
+    :param coord: Target coordinate or None (default is central europe).
156
+    :type coord: geo.gps.coordinate
157
+    :param date: The day to calculate with or None (only year, month and day are relevant; default ist today)
158
+    :type date: time.struct_time
159
+    :return: The date and time information for the sunset
160
+    :rtype: time.struct_time
161
+
162
+    This Method calculates the time for sunrise for a given date and coordinate.
163
+
164
+    .. code-block:: python
165
+
166
+        >>> import geo
167
+
168
+        >>> ...
169
+    """
170
+    date = date or time.localtime()
171
+
172
+    year, month, day = date[0:3]
173
+    dst = date[8]                             # Sommerzeit
174
+
175
+    UM = Sonnenauf_untergang(JulianischesDatum(year, month, day, 12, 0, 0), dst + 1, coord)[1]
176
+
177
+    tup_list = list(date)
178
+    tup_list[3] = 0
179
+    tup_list[4] = 0
180
+    tup_list[5] = 0
181
+    tm = time.mktime(time.struct_time(tup_list))
182
+    return time.localtime(tm + UM * 3600)

Завантаження…
Відмінити
Зберегти