Removed home-grown Profiling class, cProfile does a much better job.
[blender-addons-contrib.git] / lamp_geographical_sun.py
1 # -*- coding: utf-8 -*-
2 #
3 # ***** BEGIN GPL LICENSE BLOCK *****
4 #
5 # --------------------------------------------------------------------------
6 # Blender 2.5 Geographical Sun Add-On
7 # --------------------------------------------------------------------------
8 #
9 # Authors:
10 # Doug Hammond
11 #
12 # This program is free software; you can redistribute it and/or
13 # modify it under the terms of the GNU General Public License
14 # as published by the Free Software Foundation; either version 2
15 # of the License, or (at your option) any later version.
16 #
17 # This program is distributed in the hope that it will be useful,
18 # but WITHOUT ANY WARRANTY; without even the implied warranty of
19 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20 # GNU General Public License for more details.
21 #
22 # You should have received a copy of the GNU General Public License
23 # along with this program; if not, see <http://www.gnu.org/licenses/>.
24 #
25 # ***** END GPL LICENCE BLOCK *****
26 #
27
28 # System imports
29 #-----------------------------------------------------------------------------
30 import datetime, math, time
31 today = datetime.datetime.now()
32
33 # Blender imports
34 #----------------------------------------------------------------------------- 
35 import bpy
36 from extensions_framework import Addon, declarative_property_group
37 from extensions_framework.ui import property_group_renderer
38
39 # Addon setup
40 #----------------------------------------------------------------------------- 
41 bl_info = {
42     "name": "Geographical Sun",
43     "author": "Doug Hammond (dougal2)",
44     "version": (0, 0, 1),
45     "blender": (2, 56, 0),
46     "category": "Object",
47     "location": "Lamp data > Geographical Sun",
48     "warning": "",
49     "wiki_url": "",
50     "tracker_url": "",
51     "description": "Set SUN Lamp rotation according to geographical time and location."
52 }
53 GeoSunAddon = Addon(bl_info)
54
55 # Sun rotation calculator implementation
56 #----------------------------------------------------------------------------- 
57 class sun_calculator(object):
58     """
59     Based on SunLight v1.0 by Miguel Kabantsov (miguelkab@gmail.com)
60     Replaces the faulty sun position calculation algorythm with a precise calculation (Source for algorythm: http://de.wikipedia.org/wiki/Sonnenstand),
61     Co-Ordinates: http://www.bcca.org/misc/qiblih/latlong.html
62     Author: Nils-Peter Fischer (Nils-Peter.Fischer@web.de)
63     """
64     
65     location_list = [
66         ("EUROPE",[
67             ("Antwerp, Belgium",            67),
68             ("Berlin, Germany",             1),
69             ("Bratislava, Slovak Republic", 70),
70             ("Brno, Czech Republic",        72),
71             ("Brussles, Belgium",           68),
72             ("Geneva, Switzerland",         65),
73             ("Helsinki, Finland",           7),
74             ("Innsbruck, Austria",          62),
75             ("Kyiv, Ukraine",               64),
76             ("London, England",             10),
77             ("Lyon, France",                66),
78             ("Nitra, Slovak Republic",      69),
79             ("Oslo, Norway",                58),
80             ("Paris, France",               15),
81             ("Praha, Czech Republic",       71),
82             ("Rome, Italy",                 18),
83             ("Telfs, Austria",              63),
84             ("Warsaw, Poland",              74),
85             ("Wroclaw, Poland",             73),
86             ("Zurich, Switzerland",         21),
87         ]),
88         
89         ("WORLD CITIES", [
90             ("Beijing, China",              0),
91             ("Bombay, India",               2),
92             ("Buenos Aires, Argentina",     3),
93             ("Cairo, Egypt",                4),
94             ("Cape Town, South Africa",     5),
95             ("Caracas, Venezuela",          6),
96             ("Curitiba, Brazil",            60),
97             ("Hong Kong, China",            8),
98             ("Jerusalem, Israel",           9),
99             ("Joinville, Brazil",           61),
100             ("Mexico City, Mexico",         11),
101             ("Moscow, Russia",              12),
102             ("New Delhi, India",            13),
103             ("Ottawa, Canada",              14),
104             ("Rio de Janeiro, Brazil",      16),
105             ("Riyadh, Saudi Arabia",        17),
106             ("Sao Paulo, Brazil",           59),
107             ("Sydney, Australia",           19),
108             ("Tokyo, Japan",                20),
109         ]),
110         
111         ("US CITIES", [
112             ("Albuquerque, NM",             22),
113             ("Anchorage, AK",               23),
114             ("Atlanta, GA",                 24),
115             ("Austin, TX",                  25),
116             ("Birmingham, AL",              26),
117             ("Bismarck, ND",                27),
118             ("Boston, MA",                  28),
119             ("Boulder, CO",                 29),
120             ("Chicago, IL",                 30),
121             ("Dallas, TX",                  31),
122             ("Denver, CO",                  32),
123             ("Detroit, MI",                 33),
124             ("Honolulu, HI",                34),
125             ("Houston, TX",                 35),
126             ("Indianapolis, IN",            36),
127             ("Jackson, MS",                 37),
128             ("Kansas City, MO",             38),
129             ("Los Angeles, CA",             39),
130             ("Menomonee Falls, WI",         40),
131             ("Miami, FL",                   41),
132             ("Minneapolis, MN",             42),
133             ("New Orleans, LA",             43),
134             ("New York City, NY",           44),
135             ("Oklahoma City, OK",           45),
136             ("Philadelphia, PA",            46),
137             ("Phoenix, AZ",                 47),
138             ("Pittsburgh, PA",              48),
139             ("Portland, ME",                49),
140             ("Portland, OR",                50),
141             ("Raleigh, NC",                 51),
142             ("Richmond, VA",                52),
143             ("Saint Louis, MO",             53),
144             ("San Diego, CA",               54),
145             ("San Francisco, CA",           55),
146             ("Seattle, WA",                 56),
147             ("Washington DC",               57),
148         ])
149     ]
150     
151     location_data = {
152         # Europe
153         67: ( 51.2167, -4.4, 1),
154         1:  ( 52.33, -13.30, 1),
155         70: ( 48.17, -17.17, 1),
156         72: ( 49.2, -16.63, 1),
157         68: ( 58.8467, -4.3525, 1),
158         65: ( 46.217, -6.150, 1),
159         7:  ( 60.1667, -24.9667,2),
160         62: ( 47.2672, -11.3928, 1),
161         64: ( 50.75, -30.0833, 2),
162         10: ( 51.50, 0.0, 0),
163         66: ( 45.767, -4.833, 1),
164         69: ( 48.32, -18.07, 1),
165         58: ( 59.56, -10.41, 1),
166         15: ( 48.8667, -2.667, 1),
167         71: ( 50.08, -14.46, 1),
168         18: ( 41.90, -12.4833, 1),
169         63: ( 47.3, -11.0667, 1),
170         74: ( 52.232, -21.008, 1),
171         73: ( 51.108, -17.038, 1),
172         21: ( 47.3833, -8.5333, 1),
173         
174         # World Cities
175         0:  ( 39.9167, -116.4167, 8),
176         2:  ( 18.9333, -72.8333, 5.5),
177         3:  (-34.60, 58.45, -3),
178         4:  ( 30.10, -31.3667, 2),
179         5:  (-33.9167, -18.3667, 2),
180         6:  ( 10.50, 66.9333, -4),
181         60: (-25.4278, 49.2731, -3),
182         8:  ( 22.25, -114.1667, 8),
183         9:  ( 31.7833, -35.2333, 2),
184         61: (-29.3044, 48.8456, -3),
185         11: ( 19.4, 99.15, -6),
186         12: ( 55.75, -37.5833, 3),
187         13: ( 28.6, -77.2, 5.5),
188         14: ( 45.41667, 75.7, -5),
189         16: (-22.90, 43.2333, -3),
190         17: ( 24.633, -46.71667, 3),
191         59: ( -23.5475, 46.6361, -3),
192         19: (-33.8667, -151.2167,10),
193         20: ( 35.70, -139.7667, 9), 
194         
195         # US Cities
196         22: ( 35.0833, 106.65, -7),
197         23: ( 61.217, 149.90, -9),
198         24: ( 33.733, 84.383, -5),
199         25: ( 30.283, 97.733, -6),
200         26: ( 33.521, 86.8025, -6),
201         27: ( 46.817, 100.783, -6),
202         28: ( 42.35, 71.05, -5),
203         29: ( 40.125, 105.237, -7),
204         30: ( 41.85, 87.65, -6),
205         31: ( 32.46, 96.47, -6),
206         32: ( 39.733, 104.983, -7),
207         33: ( 42.333, 83.05, -5),
208         34: ( 21.30, 157.85, -10),
209         35: ( 29.75, 95.35, -6),
210         36: ( 39.767, 86.15, -5),
211         37: ( 32.283, 90.183, -6),
212         38: ( 39.083, 94.567, -6),
213         39: ( 34.05, 118.233, -8),
214         40: ( 43.11, 88.10, -6),
215         41: ( 25.767, 80.183, -5),
216         42: ( 44.967, 93.25, -6),
217         43: ( 29.95, 90.067, -6),
218         44: ( 40.7167, 74.0167, -5),
219         45: ( 35.483, 97.533, -6),
220         46: ( 39.95, 75.15, -5),
221         47: ( 33.433, 112.067,-7),
222         48: ( 40.433, 79.9833, -5),
223         49: ( 43.666, 70.283, -5),
224         50: ( 45.517, 122.65, -8),
225         51: ( 35.783, 78.65, -5),
226         52: ( 37.5667, 77.450, -5),
227         53: ( 38.6167, 90.1833, -6),
228         54: ( 32.7667, 117.2167, -8),
229         55: ( 37.7667, 122.4167, -8),
230         56: ( 47.60, 122.3167, -8),
231         57: ( 38.8833, 77.0333, -5),
232     }
233     
234     # mathematical helpers
235     @staticmethod
236     def sind(deg):
237         return math.sin(math.radians(deg))
238     
239     @staticmethod
240     def cosd(deg):
241         return math.cos(math.radians(deg))
242     
243     @staticmethod
244     def tand(deg):
245         return math.tan(math.radians(deg))
246     
247     @staticmethod
248     def asind(deg):
249         return math.degrees(math.asin(deg))
250     
251     @staticmethod
252     def atand(deg):
253         return math.degrees(math.atan(deg))
254     
255     @staticmethod
256     def geo_sun_astronomicJulianDate(Year, Month, Day, LocalTime, Timezone):
257         if Month > 2.0:
258             Y = Year
259             M = Month
260         else:
261             Y = Year - 1.0
262             M = Month + 12.0
263             
264         UT = LocalTime - Timezone
265         hour = UT / 24.0
266         A = int(Y/100.0)
267         
268         JD = math.floor(365.25*(Y+4716.0)) + math.floor(30.6001*(M+1.0)) + Day + hour - 1524.4
269         
270         # The following section is adopted from netCDF4 netcdftime implementation.
271         # Copyright: 2008 by Jeffrey Whitaker
272         # License: http://www.opensource.org/licenses/mit-license.php
273         if JD >= 2299170.5:
274             # 1582 October 15 (Gregorian Calendar)
275             B = 2.0 - A + int(A/4.0)
276         elif JD < 2299160.5:
277             # 1582 October 5 (Julian Calendar)
278             B = 0
279         else:
280             raise Exception('ERROR: Date falls in the gap between Julian and Gregorian calendars.')
281             B = 0
282         
283         return JD+B
284     
285     @staticmethod
286     def geoSunData(Latitude, Longitude, Year, Month, Day, LocalTime, Timezone):
287         JD = sun_calculator.geo_sun_astronomicJulianDate(Year, Month, Day, LocalTime, Timezone)
288         
289         phi = Latitude
290         llambda = Longitude
291                 
292         n = JD - 2451545.0
293         LDeg = (280.460 + 0.9856474*n) - (math.floor((280.460 + 0.9856474*n)/360.0) * 360.0)
294         gDeg = (357.528 + 0.9856003*n) - (math.floor((357.528 + 0.9856003*n)/360.0) * 360.0)
295         LambdaDeg = LDeg + 1.915 * sun_calculator.sind(gDeg) + 0.02 * sun_calculator.sind(2.0*gDeg)
296         
297         epsilonDeg = 23.439 - 0.0000004*n
298         
299         alphaDeg = sun_calculator.atand( (sun_calculator.cosd(epsilonDeg) * sun_calculator.sind(LambdaDeg)) / sun_calculator.cosd(LambdaDeg) )
300         if sun_calculator.cosd(LambdaDeg) < 0.0:
301             alphaDeg += 180.0
302             
303         deltaDeg = sun_calculator.asind( sun_calculator.sind(epsilonDeg) * sun_calculator.sind(LambdaDeg) )
304         
305         JDNull = sun_calculator.geo_sun_astronomicJulianDate(Year, Month, Day, 0.0, 0.0)
306         
307         TNull = (JDNull - 2451545.0) / 36525.0
308         T = LocalTime - Timezone
309         
310         thetaGh = 6.697376 + 2400.05134*TNull + 1.002738*T
311         thetaGh -= math.floor(thetaGh/24.0) * 24.0
312         
313         thetaG = thetaGh * 15.0
314         theta = thetaG + llambda
315         
316         tau = theta - alphaDeg
317         
318         a = sun_calculator.atand( sun_calculator.sind(tau) / ( sun_calculator.cosd(tau)*sun_calculator.sind(phi) - sun_calculator.tand(deltaDeg)*sun_calculator.cosd(phi)) )
319         if sun_calculator.cosd(tau)*sun_calculator.sind(phi) - sun_calculator.tand(deltaDeg)*sun_calculator.cosd(phi) < 0.0:
320             a += 180.0
321         
322         h = sun_calculator.asind( sun_calculator.cosd(deltaDeg)*sun_calculator.cosd(tau)*sun_calculator.cosd(phi) + sun_calculator.sind(deltaDeg)*sun_calculator.sind(phi) )
323         
324         R = 1.02 / (sun_calculator.tand (h+(10.3/(h+5.11))))
325         hR = h + R/60.0
326         
327         azimuth = a
328         elevation = hR
329         
330         return azimuth, elevation
331
332 # Addon classes
333 #----------------------------------------------------------------------------- 
334 @GeoSunAddon.addon_register_class
335 class OBJECT_OT_set_geographical_sun_now(bpy.types.Operator):
336     bl_idname = 'object.set_geographical_sun_now'
337     bl_label = 'Set time to NOW'
338     
339     @classmethod
340     def poll(cls, context):
341         cl = context.lamp
342         return cl and cl.type == 'SUN'
343     
344     def execute(self, context):
345         GSP = context.lamp.GeoSunProperties
346         
347         now = datetime.datetime.now()
348         for p in ("hour", "minute", "day", "month", "year"):
349             setattr(
350                 GSP,
351                 p,
352                 getattr(now, p)
353             )
354         GSP.tz = time.timezone
355         GSP.dst = False
356         
357         return {'FINISHED'}
358
359 @GeoSunAddon.addon_register_class
360 class OBJECT_OT_set_geographical_sun_pos(bpy.types.Operator):
361     bl_idname = 'object.set_geographical_sun_pos'
362     bl_label = 'Set SUN position'
363     
364     @classmethod
365     def poll(cls, context):
366         cl = context.lamp
367         return cl and cl.type == 'SUN'
368     
369     def execute(self, context):
370         try:
371             GSP = context.lamp.GeoSunProperties
372             
373             dst = 1 if GSP.dst else 0
374             
375             az,el = sun_calculator.geoSunData(
376                 GSP.lat,
377                 GSP.long,
378                 GSP.year,
379                 GSP.month,
380                 GSP.day,
381                 GSP.hour + GSP.minute/60.0,
382                 -GSP.tz + dst
383             )
384             
385             context.object.rotation_euler = ( math.radians(90-el), 0, math.radians(-az) )
386             return {'FINISHED'}
387         except Exception as err:
388             self.report({'ERROR'}, str(err))
389             return {'CANCELLED'}
390
391 @GeoSunAddon.addon_register_class
392 class OBJECT_OT_set_geographical_location_preset(bpy.types.Operator):
393     bl_idname = 'object.set_geographical_location_preset'
394     bl_label = 'Apply location preset'
395     
396     index = bpy.props.IntProperty()
397     
398     @classmethod
399     def poll(cls, context):
400         cl = context.lamp
401         return cl and cl.type == 'SUN'
402     
403     def execute(self, context):
404         GSP = context.lamp.GeoSunProperties
405         GSP.lat, GSP.long, GSP.tz = sun_calculator.location_data[self.properties.index]
406         return {'FINISHED'}
407
408 # Dynamic submenu magic !
409
410 def draw_generator(locations):
411     def draw(self, context):
412         sl = self.layout
413         for location in locations:
414             location_name, location_index = location
415             sl.operator('OBJECT_OT_set_geographical_location_preset', text=location_name).index = location_index
416     return draw
417
418 submenus = []
419 for label, locations in sun_calculator.location_list:
420     submenu_idname = 'OBJECT_MT_geo_sun_location_cat%d'%len(submenus)
421     submenu = type(
422         submenu_idname,
423         (bpy.types.Menu,),
424         {
425             'bl_idname': submenu_idname,
426             'bl_label': label,
427             'draw': draw_generator(locations)
428         }
429     )
430     GeoSunAddon.addon_register_class(submenu)
431     submenus.append(submenu)
432
433 @GeoSunAddon.addon_register_class
434 class OBJECT_MT_geo_sun_location(bpy.types.Menu):
435     bl_label = 'Location preset'
436     
437     def draw(self, context):
438         sl = self.layout
439         for sm in submenus:
440             sl.menu(sm.bl_idname)
441
442 @GeoSunAddon.addon_register_class
443 class GeoSunProperties(declarative_property_group):
444     ef_attach_to = ['Lamp']
445     
446     controls = [
447         ['hour', 'minute'],
448         ['day', 'month', 'year'],
449         ['tz', 'dst'],
450         'location_menu',
451         ['lat', 'long'],
452         ['set_time', 'set_posn'],
453     ]
454     
455     properties = [
456         {
457             'type': 'int',
458             'attr': 'minute',
459             'name': 'Minute',
460             'min': 0,
461             'soft_min': 0,
462             'max': 59,
463             'soft_max': 59,
464             'default': today.minute
465         },
466         {
467             'type': 'int',
468             'attr': 'hour',
469             'name': 'Hour',
470             'min': 0,
471             'soft_min': 0,
472             'max': 24,
473             'soft_max': 24,
474             'default': today.hour
475         },
476         {
477             'type': 'int',
478             'attr': 'day',
479             'name': 'Day',
480             'min': 1,
481             'soft_min': 1,
482             'max': 31,
483             'soft_max': 31,
484             'default': today.day
485         },
486         {
487             'type': 'int',
488             'attr': 'month',
489             'name': 'Month',
490             'min': 1,
491             'soft_min': 1,
492             'max': 12,
493             'soft_max': 12,
494             'default': today.month
495         },
496         {
497             'type': 'int',
498             'attr': 'year',
499             'name': 'Year',
500             'min': datetime.MINYEAR,
501             'soft_min': datetime.MINYEAR,
502             'max': datetime.MAXYEAR,
503             'soft_max': datetime.MAXYEAR,
504             'default': today.year
505         },
506         {
507             'type': 'int',
508             'attr': 'tz',
509             'name': 'Time zone',
510             'min': -13,
511             'soft_min': -13,
512             'max': 13,
513             'soft_max': 13,
514             'default': time.timezone
515         },
516         {
517             'type': 'bool',
518             'attr': 'dst',
519             'name': 'DST',
520             'default': False
521         },
522         {
523             'type': 'float',
524             'attr': 'lat',
525             'name': 'Lat.',
526             'min': -180.0,
527             'soft_min': -180.0,
528             'max': 180.0,
529             'soft_max': 180.0,
530             'default': 0.0
531         },
532         {
533             'type': 'float',
534             'attr': 'long',
535             'name': 'Long.',
536             'min': -90.0,
537             'soft_min': -90.0,
538             'max': 90.0,
539             'soft_max': 90.0,
540             'default': 0.0
541         },
542         
543         # draw operators and menus
544         {
545             'attr': 'location_menu',
546             'type': 'menu',
547             'menu': 'OBJECT_MT_geo_sun_location'
548         },
549         {
550             'attr': 'set_time',
551             'type': 'operator',
552             'operator': 'object.set_geographical_sun_now',
553             'icon': 'PREVIEW_RANGE'
554         },
555         {
556             'attr': 'set_posn',
557             'type': 'operator',
558             'operator': 'object.set_geographical_sun_pos',
559             'icon': 'WORLD_DATA'
560         },
561     ]
562
563 @GeoSunAddon.addon_register_class
564 class GeoSunPanel(property_group_renderer):
565     bl_space_type = 'PROPERTIES'
566     bl_region_type = 'WINDOW'
567     bl_context = 'data'
568     bl_label = 'Geographical Sun'
569     
570     display_property_groups = [
571         ( ('lamp',), 'GeoSunProperties' )
572     ]
573     
574     @classmethod
575     def poll(cls, context):
576         cl = context.lamp
577         return cl and cl.type == 'SUN'
578
579 # Bootstrap the Addon
580 #----------------------------------------------------------------------------- 
581 register, unregister = GeoSunAddon.init_functions()