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