From ca85dbc76c57decc803fff09dcb60bb6fc21e03a Mon Sep 17 00:00:00 2001
From: meta-androcto <meta.androcto1@gmail.com>
Date: Fri, 20 Sep 2019 16:23:17 +1000
Subject: [PATCH] sun position: update working version: T69936

---
 sun_position/__init__.py   |   88 ++-
 sun_position/hdr.py        | 1150 ++++++++----------------------------
 sun_position/map.py        |  195 +++---
 sun_position/north.py      |  186 +++---
 sun_position/operators.py  |  313 ----------
 sun_position/properties.py |  441 +++++++-------
 sun_position/sun_calc.py   |  711 ++++++++++++----------
 sun_position/ui_sun.py     |  926 ++++++++++-------------------
 8 files changed, 1403 insertions(+), 2607 deletions(-)
 delete mode 100644 sun_position/operators.py

diff --git a/sun_position/__init__.py b/sun_position/__init__.py
index c8e03f77..d276463e 100644
--- a/sun_position/__init__.py
+++ b/sun_position/__init__.py
@@ -28,67 +28,65 @@
 # NASA's image use policy can be found at:
 # http://www.nasa.gov/audience/formedia/features/MP_Photo_Guidelines.html
 # --------------------------------------------------------------------------
+# The geo parser script is by Maximilian Högner, released
+# under the GNU GPL license:
+# http://hoegners.de/Maxi/geo/
+# --------------------------------------------------------------------------
 
 # <pep8 compliant>
 
 bl_info = {
-    "name": "Sun Position 2.8",
-    "author": "Michael Martin, Kevan Cress",
-    "version": (3, 0, 1),
+    "name": "Sun Position",
+    "author": "Michael Martin",
+    "version": (3, 1, 0),
     "blender": (2, 80, 0),
     "location": "World > Sun Position",
     "description": "Show sun position with objects and/or sky texture",
-    "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/" \
-        "Scripts/3D_interaction/Sun_Position",
-        "tracker_url": "https://projects.blender.org/tracker/" \
-        "index.php?func=detail&aid=29714",
-    "category": "3D View"}  #  "Lighting"} ?
+    "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
+    "Scripts/3D_interaction/Sun_Position",
+    "tracker_url": "https://projects.blender.org/tracker/"
+    "index.php?func=detail&aid=29714",
+    "category": "Lighting"}
 
-import bpy
-from . properties import *
-from . ui_sun import *
-from . map import SunPos_Help
-from . hdr import SunPos_HdrHelp
+if "bpy" in locals():
+    import importlib
+    importlib.reload(properties)
+    importlib.reload(ui_sun)
+    importlib.reload(map)
+    importlib.reload(hdr)
 
-############################################################################
+else:
+    from . import properties, ui_sun, map, hdr
+
+import bpy
 
-classes = (
-    SunPos_OT_Controller,
-    SunPos_OT_Preferences,
-    SunPos_OT_PreferencesDone,
-    SunPos_OT_DayRange,
-    SunPos_OT_SetObjectGroup,
-    SunPos_OT_ClearObjectGroup,
-    SunPos_OT_TimePlace,
-    SunPos_OT_Map,
-    SunPos_OT_Hdr,
-    SPOS_PT_Panel,
-    SunPos_OT_MapChoice,
-    SunPos_Help,
-    SunPos_HdrHelp,
-)
 
 def register():
-    bpy.utils.register_class(SunPosSettings)
-    bpy.types.Scene.SunPos_property = (
-        bpy.props.PointerProperty(type=SunPosSettings,
+    bpy.utils.register_class(properties.SunPosProperties)
+    bpy.types.Scene.sun_pos_properties = (
+        bpy.props.PointerProperty(type=properties.SunPosProperties,
                         name="Sun Position",
                         description="Sun Position Settings"))
-    bpy.utils.register_class(SunPosPreferences)
-    bpy.types.Scene.SunPos_pref_property = (
-        bpy.props.PointerProperty(type=SunPosPreferences,
-                        name="Sun Position Preferences",
-                        description="SP Preferences"))
+    bpy.utils.register_class(properties.SunPosAddonPreferences)
+    bpy.utils.register_class(ui_sun.SUNPOS_OT_AddPreset)
+    bpy.utils.register_class(ui_sun.SUNPOS_OT_DefaultPresets)
+    bpy.utils.register_class(ui_sun.SUNPOS_MT_Presets)
+    bpy.utils.register_class(ui_sun.SUNPOS_PT_Panel)
+    bpy.utils.register_class(hdr.SUNPOS_OT_ShowHdr)
+    # bpy.utils.register_class(map.SunPos_Help)
 
-    for c in classes:
-        bpy.utils.register_class(c)
+    bpy.app.handlers.frame_change_post.append(sun_calc.sun_handler)
 
 
 def unregister():
-    for c in reversed(classes):
-        bpy.utils.unregister_class(c)
+    # bpy.utils.unregister_class(map.SunPos_Help)
+    bpy.utils.unregister_class(hdr.SUNPOS_OT_ShowHdr)
+    bpy.utils.unregister_class(ui_sun.SUNPOS_PT_Panel)
+    bpy.utils.unregister_class(ui_sun.SUNPOS_MT_Presets)
+    bpy.utils.unregister_class(ui_sun.SUNPOS_OT_DefaultPresets)
+    bpy.utils.unregister_class(ui_sun.SUNPOS_OT_AddPreset)
+    bpy.utils.unregister_class(properties.SunPosAddonPreferences)
+    del bpy.types.Scene.sun_pos_properties
+    bpy.utils.unregister_class(properties.SunPosProperties)
 
-    del bpy.types.Scene.SunPos_pref_property
-    bpy.utils.unregister_class(SunPosPreferences)
-    del bpy.types.Scene.SunPos_property
-    bpy.utils.unregister_class(SunPosSettings)
+    bpy.app.handlers.frame_change_post.remove(sun_calc.sun_handler)
diff --git a/sun_position/hdr.py b/sun_position/hdr.py
index e7f908f6..774b42df 100644
--- a/sun_position/hdr.py
+++ b/sun_position/hdr.py
@@ -1,936 +1,294 @@
+### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
 # -*- coding: utf-8 -*-
 
 import bpy
+import gpu
 import bgl
-import blf
-import sys
-import os
-import math
-
-from . sun_calc import degToRad, radToDeg, format_hms
-from . properties import Display, Sun
-
-# ---------------------------------------------------------------------------
-
-
-class HdrObject:
-
-    class Origin:
-        x = 0
-        y = 0
-
-    def __init__(self, t, w, h):
-        self.type = t
-        self.width = w
-        self.height = h
-        self.heightFactor = .50
-        self.opacity = 1.0
-        self.focused = False
-        self.view3d_area = None
-        self.origin = self.Origin()
-
-    def set_dimensions(self, width):
-        self.width = width
-        self.height = int(width * self.heightFactor)
-
-    def check_focus(self, context, event):
-        self.focused = self.is_focused(context, event)
-        return self.focused
-
-    def is_focused(self, context, event):
-        if context.area != self.view3d_area:
-            return False
-
-        x = event.mouse_region_x
-        y = event.mouse_region_y
-
-        for reg in self.view3d_area.regions:
-            if reg.type == 'WINDOW':
-                if x < 0 or x > reg.width:
-                    return False
-                else:
-                    break
-
-        if x < self.origin.x or x > (self.origin.x + self.width) or \
-                y < self.origin.y or y > (self.origin.y + self.height) or \
-                y < 0 or y > reg.height:
-            return False
-        return True
-
-    def near_border(self, context, event):
-        if context.area != self.view3d_area:
-            return False
-
-        x = event.mouse_region_x
-        y = event.mouse_region_y
-
-        for reg in self.view3d_area.regions:
-            if reg.type == 'WINDOW':
-                if x < 20 or x > (reg.width - 20) or \
-                        y < 20 or y > (reg.height - 20):
-                    return True
-                else:
-                    break
-        return False
-
-
-# ---------------------------------------------------------------------------
+from gpu_extras.batch import batch_for_shader
+from mathutils import Vector
+from gpu_extras.presets import draw_texture_2d
+from math import sqrt, pi, atan2, asin
 
 
-class HdrClass:
+vertex_shader = '''
+uniform mat4 ModelViewProjectionMatrix;
 
-    class mouse:
-        pass
+/* Keep in sync with intern/opencolorio/gpu_shader_display_transform_vertex.glsl */
+in vec2 texCoord;
+in vec2 pos;
+out vec2 texCoord_interp;
 
-    class grab:
+void main()
+{
+  gl_Position = ModelViewProjectionMatrix * vec4(pos.xy, 0.0f, 1.0f);
+  gl_Position.z = 1.0;
+  texCoord_interp = texCoord;
+}'''
 
-        class spot:
-            pass
+fragment_shader = '''
+in vec2 texCoord_interp;
+out vec4 fragColor;
 
-        class offset:
-            pass
+uniform sampler2D image;
+uniform float exposure;
 
-    class zoom:
-        pass
+void main()
+{
+  fragColor = texture(image, texCoord_interp) * exposure;
+}'''
 
-    class image:
-        pass
+# shader = gpu.types.GPUShader(vertex_shader, fragment_shader)
 
-    class last:
-        pass
 
-    def __init__(self):
-        self.handler1 = None
-        self.handler2 = None
-        self.view3d_area = None
-        self.draw_region = None
-        self.glImage = None
-        self.init_zoom_preference = True
-        self.reset()
-        self.last.filename = None
-        self.last.image = None
-        self.last.pixels = None
-        self.last.projection = None
+def draw_callback_px(self, context):
+    nt = bpy.context.scene.world.node_tree.nodes
+    env_tex_node = nt.get(bpy.context.scene.sun_pos_properties.hdr_texture)
+    image = env_tex_node.image
 
-    def init(self):
-        self.object = [HdrObject('MAP', 0, 0), HdrObject('TEXT', 100, 160)]
-        self.object[0].set_dimensions(200)
-        self.object[1].origin.x = 10
-        self.object[1].origin.y = 80
-
-    def zoom_preferences(self, invert_zoom_wheel, invert_mouse_zoom):
-        self.init_zoom_preference = False
-        if invert_zoom_wheel:
-            self.zoom.wheel_up = 'OUT'
-            self.zoom.wheel_down = 'IN'
-        else:
-            self.zoom.wheel_up = 'IN'
-            self.zoom.wheel_down = 'OUT'
-        if invert_mouse_zoom:
-            self.zoom.mouse_up = 'IN'
-            self.zoom.mouse_down = 'OUT'
-        else:
-            self.zoom.mouse_up = 'OUT'
-            self.zoom.mouse_down = 'IN'
-
-    def reset(self):
-        self.init()
-        self.action = None
-        self.isActive = False
-        self.start = False
-        self.stop = False
-        self.lockCrosshair = True
-        self.elevation = 0.0
-        self.azimuth = 0.0
-        self.ctrlPress = False
-        self.altPress = False
-        self.mouse.x = 0
-        self.mouse.y = 0
-        self.grab.spot.x = 0
-        self.grab.spot.y = 0
-        self.grab.offset.x = 0
-        self.grab.offset.y = 0
-        self.zoom.width = 0
-        self.zoom.x = 0
-        self.zoom.y = 0
-        self.image.name = None
-        self.image.bindcode = 0
-        self.image.loaded = False
-        self.image.free_it = False
-
-    def clear_callbacks(self):
-        if self.handler2 is not None:
-            bpy.types.SpaceView3D.draw_handler_remove(self.handler2, 'WINDOW')
-            self.handler2 = None
-        if self.handler1 is not None:
-            bpy.types.SpaceView3D.draw_handler_remove(self.handler1, 'WINDOW')
-            self.handler1 = None
-
-    def set_view3d_area(self, area):
-        for obj in self.object:
-            obj.view3d_area = area
-
-    def activate(self, context):
-        if context.area.type == 'PROPERTIES':
-            self.reset()
-
-            def fw(self, context):
-                self.draw_region = context.region
-                areas = bpy.context.screen.areas
-                for area in areas:
-                    if area.type == 'VIEW_3D':
-                        self.view3d_area = context.area
-                        for reg in area.regions:
-                            if reg.type == 'WINDOW':
-                                self.draw_region = reg
-                                Display.refresh()
-                                return True
-                return False
-            if not fw(self, context):
-                self.draw_region = context.region
-
-            self.set_view3d_area(self.view3d_area)
-            self.start = True
-            self.handler1 = bpy.types.SpaceView3D.draw_handler_add(
-                                   Hdr_load_callback,
-                                   (self, context), 'WINDOW', 'POST_PIXEL')
-            self.isActive = True
-            return True
-        else:
-            return False
-
-    def activateBGLcallback(self, context):
-        self.handler2 = bpy.types.SpaceView3D.draw_handler_add(
-                    Draw_hdr_callback, (self, context), 'WINDOW', 'POST_PIXEL')
-        self.view3d_area = context.area
-        self.set_view3d_area(self.view3d_area)
-        bpy.ops.sunpos.hdr('INVOKE_DEFAULT')
-
-    def deactivate(self):
-        self.clear_callbacks()
-        self.stop = False
-        self.action = None
-        self.image.loaded = False
-        if self.glImage is not None:
-            try:
-                if self.glImage.bindcode == self.image.bindcode:
-                    self.glImage.gl_free()
-                    bpy.data.images.remove(self.glImage)
-            except:
-                pass
-        self.image.free_it = False
-        self.glImage = None
-        self.image.bindcode = 0
-        self.isActive = False
-        #if Sun.SP:  # why removed?
-        Sun.SP.ShowHdr = False  # indent?
-        Sun.SP.BindToSun = True  # indent?
-        Sun.BindToSun = False
-        Display.refresh()
-
-    def make_dummy_file(self, file_name):
-        fname = file_name.replace("\\", "/")
-        if os.path.exists(fname):
-            return False
-        else:
-            return True
-
-    def load_blender_image(self, file_name):
-        fn = file_name
-        self.image.name = fn.replace("\\", "/")
-        if os.path.exists(self.image.name):
-            try:
-                self.glImage = bpy.data.images.load(self.image.name)
-                self.glImage.pixels = self.last.pixels
-                if self.glImage is not None:
-                    self.image.loaded = True
-                    self.glImage.user_clear()
-                    self.object[0].heightFactor = \
-                        self.glImage.size[1] / self.glImage.size[0]
-                    return True
-                else:
-                    return False
-            except:
-                pass
-        return False
-
-    def load_gl_image(self):
-        for i in range(1, 6):  # Make up to 6 tries to load image
-            self.glImage.gl_load(bgl.GL_NEAREST, bgl.GL_NEAREST)
-            if self.glImage.bindcode != 0:
-                self.image.bindcode = self.glImage.bindcode
-                return True
-        return False
-
-    def locked_crosshair_event(self, action, event):
-        self.mouse.x = event.mouse_region_x
-        self.mouse.y = event.mouse_region_y
-        self.grab.offset.x = event.mouse_region_x
-        self.grab.offset.y = event.mouse_region_y
-        self.lockCrosshair = True
-        Display.refresh()
-        return Hdr_function[action](event)
-
-    # -----------------------------------------------------------------------
-
-    def event_controller(self, context, event):
-
-        if not Sun.SP.ShowHdr or event.type == 'TIMER':
-            return {'PASS_THROUGH'}
+    if self.area != context.area:
+        return
 
-        hdrInFocus = self.object[0].check_focus(context, event)
+    if image.gl_load():
+        raise Exception()
+
+    left = 0
+    bottom = 0
+    top = context.area.height
+    right = context.area.width
+
+    position = Vector((right, top)) / 2 + self.offset
+    scale = Vector((context.area.width, context.area.width / 2)) * self.scale
+
+    shader = gpu.types.GPUShader(vertex_shader, fragment_shader)
+
+    coords = ((-0.5, -0.5), (0.5, -0.5), (0.5, 0.5), (-0.5, 0.5))
+    uv_coords = ((0, 0), (1, 0), (1, 1), (0, 1))
+    batch = batch_for_shader(shader, 'TRI_FAN',
+        {"pos" : coords,
+         "texCoord" : uv_coords})
+
+    bgl.glActiveTexture(bgl.GL_TEXTURE0)
+    bgl.glBindTexture(bgl.GL_TEXTURE_2D, image.bindcode)
+
+
+    with gpu.matrix.push_pop():
+        gpu.matrix.translate(position)
+        gpu.matrix.scale(scale)
+
+        shader.bind()
+        shader.uniform_int("image", 0)
+        shader.uniform_float("exposure", self.exposure)
+        batch.draw(shader)
+
+    # Crosshair
+    # vertical
+    coords = ((self.mouse_position[0], bottom), (self.mouse_position[0], top))
+    colors = ((1,)*4,)*2
+    shader = gpu.shader.from_builtin('2D_FLAT_COLOR')
+    batch = batch_for_shader(shader, 'LINES',
+                             {"pos": coords, "color": colors})
+    shader.bind()
+    batch.draw(shader)
+
+    # horizontal
+    if bottom <= self.mouse_position[1] <= top:
+        coords = ((0, self.mouse_position[1]), (context.area.width, self.mouse_position[1]))
+        batch = batch_for_shader(shader, 'LINES',
+                                 {"pos": coords, "color": colors})
+        shader.bind()
+        batch.draw(shader)
+
+
+class SUNPOS_OT_ShowHdr(bpy.types.Operator):
+    """Tooltip"""
+    bl_idname = "world.sunpos_show_hdr"
+    bl_label = "Sync Sun to Texture"
+
+    exposure = 1.0
+
+    def update(self, context, event):
+        sun_props = context.scene.sun_pos_properties
+        mouse_position_abs = Vector((event.mouse_x, event.mouse_y))
+
+        # Get current area
+        for area in context.screen.areas:
+            # Compare absolute mouse position to area bounds
+            if (area.x < mouse_position_abs.x < area.x + area.width
+                    and area.y < mouse_position_abs.y < area.y + area.height):
+                self.area = area
+            if area.type == 'VIEW_3D':
+                # Redraw all areas
+                area.tag_redraw()
+
+        if self.area.type == 'VIEW_3D':
+            self.top = self.area.height
+            self.right = self.area.width
+
+            nt = context.scene.world.node_tree.nodes
+            env_tex = nt.get(sun_props.hdr_texture)
+
+            # Mouse position relative to window
+            self.mouse_position = Vector((mouse_position_abs.x - self.area.x,
+                                          mouse_position_abs.y - self.area.y))
+
+            self.selected_point = (self.mouse_position - self.offset - Vector((self.right, self.top))/2) / self.scale
+            u = self.selected_point.x / self.area.width + 0.5
+            v = (self.selected_point.y) / (self.area.width / 2) + 0.5
+
+            # Set elevation and azimuth from selected point
+            if env_tex.projection == 'EQUIRECTANGULAR':
+                el = v * pi - pi/2
+                az = u * pi*2 - pi/2 + env_tex.texture_mapping.rotation.z
+
+                # Clamp elevation
+                el = max(el, -pi/2)
+                el = min(el, pi/2)
+
+                sun_props.hdr_elevation = el
+                sun_props.hdr_azimuth = az
+            elif env_tex.projection == 'MIRROR_BALL':
+                # Formula from intern/cycles/kernel/kernel_projection.h
+                # Point on sphere
+                dir = Vector()
+
+                # Normalize to -1, 1
+                dir.x = 2.0 * u - 1.0
+                dir.z = 2.0 * v - 1.0
+
+                # Outside bounds
+                if (dir.x * dir.x + dir.z * dir.z > 1.0):
+                    dir = Vector()
 
-        if event.type == 'MOUSEMOVE':
-            if self.action == None:
-                if hdrInFocus:
-                    if self.object[0].near_border(context, event):
-                        return {'PASS_THROUGH'}
-                    else:
-                        return {'RUNNING_MODAL'}
                 else:
-                    return {'PASS_THROUGH'}
-            if self.action in ('PAN', 'ZOOM', 'G'):
-                return self.locked_crosshair_event(self.action, event)
-
-        if event.type in ('LEFT_CTRL', 'LEFT_ALT', 'RIGHT_CTRL', 'RIGHT_ALT'):
-            Key_function[event.type](event)
-            return {'RUNNING_MODAL'}
-
-        self.object[1].check_focus(context, event)
-
-        if event.type in ('MIDDLEMOUSE', 'LEFTMOUSE', 'G'):
-            val = Key_function[event.type](context, event)
-            if val:
-                return val
-        elif event.type in ('RIGHTMOUSE', 'ESC', 'H', 'F1'):
-            Display.refresh()
-            return Key_function[event.type](event)
-
-        if self.action in ('PAN', 'ZOOM', 'G'):
-            return self.locked_crosshair_event(self.action, event)
-
-        if not hdrInFocus:
-            return {'PASS_THROUGH'}
+                    dir.y = -sqrt(max(1.0 - dir.x * dir.x - dir.z * dir.z, 0.0))
 
-        self.mouse.x = event.mouse_region_x
-        self.mouse.y = event.mouse_region_y
+                    # Reflection
+                    i = Vector((0.0, -1.0, 0.0))
 
-        if event.type == 'WHEELUPMOUSE':
-            wheel_zoom(self.zoom.wheel_up)
-        elif event.type == 'WHEELDOWNMOUSE':
-            wheel_zoom(self.zoom.wheel_down)
-        elif self.action == 'CROSS':
-            self.lockCrosshair = False
-            Display.refresh()
-
-        return {'RUNNING_MODAL'}
-
-# ---------------------------------------------------------------------------
-
-
-Hdr = HdrClass()
-
-
-# ---------------------------------------------------------------------------
+                    dir = 2.0 * dir.dot(i) * dir - i
 
+                # Convert vector to euler
+                el = asin(dir.z)
+                az = atan2(dir.x, dir.y)
+                sun_props.hdr_elevation = el
+                sun_props.hdr_azimuth = az
 
-def key_Ctrl(event):
-    if event.value == 'PRESS':
-        Hdr.ctrlPress = True
-    elif event.value == 'RELEASE':
-        Hdr.ctrlPress = False
-
-
-def key_Alt(event):
-    if event.value == 'PRESS':
-        Hdr.altPress = True
-    elif event.value == 'RELEASE':
-        Hdr.altPress = False
-
-
-def key_Esc(event):
-    if Hdr.object[0].focused:
-        if Hdr.action is None:
-            Hdr.stop = True
-            return {'FINISHED'}
-    if Hdr.action is not None:
-        if event.value == 'RELEASE':
-            Hdr.action = None
-        return {'RUNNING_MODAL'}
-    return {'PASS_THROUGH'}
-
-
-def key_LeftMouse(context, event):
-    if event.value == 'PRESS':
-        if Hdr.action is not None:
-            Hdr.action = None
-            Display.refresh()
-            return {'RUNNING_MODAL'}
-        elif Hdr.object[0].focused:
-            if Hdr.object[0].near_border(context, event):
-                Hdr.action = 'BORDER'
-                Hdr.lockCrosshair = True
-                return {'PASS_THROUGH'}
-            Hdr.action = 'CROSS'
-            Hdr.lockCrosshair = False
-            Display.refresh()
-        else:
-            return {'PASS_THROUGH'}
-    elif event.value == 'RELEASE':
-        Hdr.lockCrosshair = True
-        Hdr.action = None
-        Display.refresh()
-        return {'PASS_THROUGH'}
-
-    return False
-
-
-def key_MiddleMouse(context, event):
-    if event.value == 'PRESS':
-        if Hdr.object[0].focused:
-            if Hdr.ctrlPress:
-                hdr = Hdr.object[0]
-                Hdr.action = 'ZOOM'
-                Hdr.zoom.width = hdr.width
-                Hdr.zoom.x = hdr.origin.x
-                Hdr.zoom.y = hdr.origin.y
             else:
-                Hdr.action = 'PAN' if Hdr.action != 'PAN' else None
-            Hdr.grab.spot.x = event.mouse_region_x
-            Hdr.grab.spot.y = event.mouse_region_y
-            return False
-        else:
-            return {'PASS_THROUGH'}
-    elif event.value == 'RELEASE':
-        Hdr.action = None
-        Hdr.object[0].focused = False
-        Hdr.object[1].focused = False
-        Display.refresh()
-    return {'RUNNING_MODAL'}
-
-# ---------------------------------------------------------------------------
-
-
-def key_G(context, event):
-    if event.value == 'PRESS':
-        if Hdr.object[0].focused:
-            Hdr.action = 'PAN' if Hdr.action != 'PAN' else None
-            Hdr.grab.spot.x = event.mouse_region_x
-            Hdr.grab.spot.y = event.mouse_region_y
-            return False
-        else:
-            return {'PASS_THROUGH'}
-    return {'RUNNING_MODAL'}
-
-
-def key_H(event):
-    if event.value == 'PRESS':
-        if Hdr.object[0].focused:
-            Hdr.action = None
-            bpy.ops.object.hdrhelp_operator('INVOKE_DEFAULT')
-    return {'RUNNING_MODAL'}
-
-
-# ---------------------------------------------------------------------------
-
-def hdr_Pan(event):
-    return {'RUNNING_MODAL'}
-
-
-def hdr_Zoom(event):
-    mouse_zoom()
-    return {'RUNNING_MODAL'}
-
-
-def hdr_G(event):
-    if Hdr.grab.offset.x < Hdr.grab.spot.x:
-        off = Hdr.grab.spot.x - Hdr.grab.offset.x
-        Hdr.object[1].origin.x -= off
-    else:
-        off = Hdr.grab.offset.x - Hdr.grab.spot.x
-        Hdr.object[1].origin.x += off
-    if Hdr.grab.offset.y < Hdr.grab.spot.y:
-        off = Hdr.grab.spot.y - Hdr.grab.offset.y
-        Hdr.object[1].origin.y -= off
-    else:
-        off = Hdr.grab.offset.y - Hdr.grab.spot.y
-        Hdr.object[1].origin.y += off
-
-    Hdr.grab.spot.x = Hdr.mouse.x
-    Hdr.grab.spot.y = Hdr.mouse.y
-
-    return {'RUNNING_MODAL'}
+                self.report({'ERROR'}, 'Unknown projection')
+                return {'CANCELLED'}
 
+    def pan(self, context, event):
+        self.offset += Vector((event.mouse_region_x - self.mouse_prev_x,
+                               event.mouse_region_y - self.mouse_prev_y))
+        self.mouse_prev_x, self.mouse_prev_y = event.mouse_region_x, event.mouse_region_y
 
-############################################################################
-
-Key_function = dict([('LEFT_CTRL', key_Ctrl), ('LEFT_ALT', key_Alt),
-                    ('RIGHT_CTRL', key_Ctrl), ('RIGHT_ALT', key_Alt),
-                    ('MIDDLEMOUSE', key_MiddleMouse),
-                    ('LEFTMOUSE', key_LeftMouse),
-                    ('RIGHTMOUSE', key_Esc), ('ESC', key_Esc),
-                    ('G', key_G), ('H', key_H), ('F1', key_H)])
-
-# ---------------------------------------------------------------------------
-
-Hdr_function = dict([('PAN', hdr_Pan), ('ZOOM', hdr_Zoom),
-                     ('G', hdr_G)])
-
-############################################################################
-
-
-def wheel_zoom(action):
-    mf = 0.2 if Hdr.ctrlPress else 0.0
-    af = 0.07 if Hdr.altPress else 0.0
-    if action == 'IN':
-        scale = 1.10 + mf - af
-    else:
-        scale = .90 - mf + af
-    if Hdr.object[0].width * scale < 50:
-        return
-    else:
-        Hdr.object[0].set_dimensions(int(int(Hdr.object[0].width * scale)))
-    x = Hdr.mouse.x - Hdr.object[0].origin.x
-    y = Hdr.mouse.y - Hdr.object[0].origin.y
-    Hdr.object[0].origin.x += x - int(x * scale)
-    Hdr.object[0].origin.y += y - int(y * scale)
-    Hdr.lockCrosshair = True
-    Display.refresh()
-
-
-def mouse_zoom():
-    if Hdr.mouse.y > Hdr.grab.spot.y:
-        s = Hdr.mouse.y - Hdr.grab.spot.y
-        action = Hdr.zoom.mouse_up
-    elif Hdr.mouse.y < Hdr.grab.spot.y:
-        s = Hdr.grab.spot.y - Hdr.mouse.y
-        action = Hdr.zoom.mouse_down
-    else:
-        s = 0
-        action = Hdr.zoom.mouse_down
-
-    if action == 'IN':
-        scale = 1 + s * .01
-    else:
-        scale = 1 - s * .006
-
-    w = int(Hdr.zoom.width * scale)
-    if w < 50:
-        return
-    Hdr.object[0].set_dimensions(w)
-    x = Hdr.grab.spot.x - Hdr.zoom.x
-    y = Hdr.grab.spot.y - Hdr.zoom.y
-    Hdr.object[0].origin.x = x - int(x * scale) + Hdr.zoom.x
-    Hdr.object[0].origin.y = y - int(y * scale) + Hdr.zoom.y
-
-
-# --------------------------------------------------------------------------
-
-
-############################################################################
-
-
-def Hdr_load_callback(self, context):
-
-    def remove_imagedata(name):
-        bd = bpy.data
-        for i in bd.images:
-            if i.name == name:
-                bd.images.remove(i)
+    def modal(self, context, event):
+        self.area.tag_redraw()
+        if event.type == 'MOUSEMOVE':
+            if self.is_panning:
+                self.pan(context, event)
+            self.update(context, event)
+
+        # Confirm
+        elif event.type in {'LEFTMOUSE', 'RET'}:
+            bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
+            for area in context.screen.areas:
+                area.tag_redraw()
+            # Bind the environment texture to the sun
+            context.scene.sun_pos_properties.bind_to_sun = True
+            context.workspace.status_text_set(None)
+            return {'FINISHED'}
 
-    if Sun.SP.ShowHdr and not Hdr.image.loaded:
-        Hdr.glImage = None
-        fileName = None
-        projection = "EQUIRECTANGULAR"
-        try:
-            nt = bpy.context.scene.world.node_tree.nodes
-            envTex = nt.get(Sun.HDR_texture)
-            if envTex.type != "TEX_ENVIRONMENT":
-                Sun.SP.ShowHdr = False
-            elif envTex.image == None:
-                Sun.SP.ShowHdr = False
+        # Cancel
+        elif event.type in {'RIGHTMOUSE', 'ESC'}:
+            bpy.types.SpaceView3D.draw_handler_remove(self._handle, 'WINDOW')
+            for area in context.screen.areas:
+                area.tag_redraw()
+            # Reset previous values
+            context.scene.sun_pos_properties.hdr_elevation = self.initial_elevation
+            context.scene.sun_pos_properties.hdr_azimuth = self.initial_azimuth
+            context.workspace.status_text_set(None)
+            return {'CANCELLED'}
+
+        # Set exposure or zoom
+        elif event.type == 'WHEELUPMOUSE':
+            # Exposure
+            if event.ctrl:
+                self.exposure *= 1.1
+            # Zoom
             else:
-                envTex.texture_mapping.rotation.z = 0.0
-                projection = envTex.projection
-                prefs = bpy.context.preferences
-                fileName = prefs.filepaths.temporary_directory + "tmpSun.png"
-
-                st = envTex.image.copy()
-                if projection == "MIRROR_BALL":
-                    st.scale(256, 256)
-                    Hdr.last.image = st.copy()
-                    Hdr.last.image.scale(512, 256)
-                    ConvertToLatLong(st, Hdr.last.image)
-                else:
-                    st.scale(512, 256)
-                    Hdr.last.image = st.copy()
-                    Hdr.last.image.scale(512, 256)
-
-                if Hdr.make_dummy_file(fileName) == True:
-                    Hdr.last.image.save_render(fileName)
-                Hdr.last.pixels = list(Hdr.last.image.pixels)
-        except:
-            pass
-
-        if Sun.SP.ShowHdr:
-            if not Hdr.load_blender_image(fileName):
-                print("Could not load image file: ", Hdr.image.name)
-                Sun.SP.ShowHdr = False
+                self.scale *= 1.1
+                self.offset -= (self.mouse_position - (Vector((self.right, self.top)) / 2 + self.offset)) / 10.0
+                self.update(context, event)
+        elif event.type == 'WHEELDOWNMOUSE':
+            # Exposure
+            if event.ctrl:
+                self.exposure /= 1.1
+            # Zoom
             else:
-                try:
-                    nt = bpy.context.scene.world.node_tree.nodes
-                    envTex = nt.get(Sun.HDR_texture)
-                    if projection == "MIRROR_BALL":
-                        envTex.texture_mapping.rotation.z = degToRad(270.0)
-                    else:
-                        envTex.texture_mapping.rotation.z = degToRad(90.0)
-                except:
-                    pass
-
-    if Hdr.start:
-        def set_region_data():
-            Hdr.activateBGLcallback(context)
-            bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MAG_FILTER,
-                                bgl.GL_LINEAR)
-            bgl.glTexParameteri(bgl.GL_TEXTURE_2D, bgl.GL_TEXTURE_MIN_FILTER,
-                                bgl.GL_LINEAR)
-            Hdr.object[0].set_dimensions(0)
-            Hdr.toolProps = None
-            Hdr.toolPropsWidth = 0
-            for reg in Hdr.view3d_area.regions:
-                if reg.type == 'TOOL_PROPS':
-                    Hdr.toolProps = reg
-                    Hdr.toolProps_width = reg.width
-                elif reg.type == 'WINDOW':
-                    Hdr.region = reg
-                    Hdr.saved_region_width = reg.width
-                    Hdr.object[0].set_dimensions(int(reg.width * .5))
-                    Hdr.object[0].origin.x = \
-                        int((Hdr.region.width - Hdr.object[0].width) / 2)
-                    Hdr.object[0].origin.y = 2
-            return
-
-        Hdr.start = False
-        if Hdr.image.loaded:
-            if not Hdr.load_gl_image():
-                print("Could not load image file: ", Hdr.image.name)
-            elif Hdr.glImage.bindcode != 0:
-                Hdr.image.free_it = True
-                set_region_data()
-                return
-            print("Could not get texture in gl_load()")
-            Hdr.glImage = None
-            Hdr.image.bindcode = 0
-            Sun.SP.ShowHdr = False
-        else:
-            Sun.SP.ShowHdr = False
-        return
-
-    if Hdr.stop:
-        Hdr.deactivate()
-        return
-
-    return
-
-
-############################################################################
-# Thanks to Domino for the Pixel and ImageBuffer classes
-############################################################################
-
-
-class Pixel:
-
-    def __init__(self, r=0.0, g=0.0, b=0.0, a=None, colour=None):
-        self.r = r
-        self.g = g
-        self.b = b
-        self.a = a
-        if colour:
-            self.r = colour[0]
-            self.g = colour[1]
-            self.b = colour[2]
-            if len(colour) > 3:
-                self.a = colour[3]
-        if self.a is None:
-            self.a = 1.0
-
-    def as_tuple(self):
-        return (self.r, self.g, self.b, self.a)
-
+                self.scale /= 1.1
+                self.offset += (self.mouse_position - (Vector((self.right, self.top)) / 2 + self.offset)) / 11.0
+                self.update(context, event)
+
+        # Toggle pan
+        elif event.type == 'MIDDLEMOUSE':
+            if event.value == 'PRESS':
+                self.mouse_prev_x, self.mouse_prev_y = event.mouse_region_x, event.mouse_region_y
+                self.is_panning = True
+            elif event.value == 'RELEASE':
+                self.is_panning = False
 
-class ImageBuffer:
-
-    def __init__(self, image, clear=False):
-        self.image = image
-        self.x, self.y = self.image.size
-        if clear:
-            self.clear()
-        else:
-            self.buffer = list(self.image.pixels)
-
-    def update(self):
-        self.image.pixels = self.buffer
-
-    def _index(self, x, y):
-        if x < 0 or y < 0 or x >= self.x or y >= self.y:
-            return None
-        return (x + y * self.x) * 4
-
-    def clear(self):
-        self.buffer = [0.0 for i in range(self.x * self.y * 4)]
-
-    def set_pixel(self, x, y, colour):
-        index = self._index(x, y)
-        if index is not None:
-            self.buffer[index:index + 4] = colour.as_tuple()
-
-    def get_pixel(self, x, y):
-        index = self._index(x, y)
-        if index is not None:
-            return Pixel(colour=self.buffer[index:index + 4])
         else:
-            return None
-
-
-def ConvertToLatLong(inpic, outpic):
-
-    width = inpic.size[0]
-    height = inpic.size[1]
-
-    uv_width = width * 2 - 1
-    uv_height = height - 1
-
-    pc_width = 0.5 * width
-    pc_height = 0.5 * height
-
-    p_in = ImageBuffer(inpic)
-    p_out = ImageBuffer(outpic)
-    p_out.clear()
+            return {'PASS_THROUGH'}
 
-    flip = width * 2 - 1
+        return {'RUNNING_MODAL'}
 
-    for col in range(0, width * 2):
-        phi = (col / uv_width) * 2 * math.pi
-        for row in range(0, height):
-            theta = (1 - row / uv_height) * math.pi
-            m = math.sqrt(2 * (1 + math.sin(-theta) * math.sin(phi)))
-            x = int((math.sin(theta) * math.cos(phi) / m + 1) * pc_width)
-            y = int((math.cos(theta) / m + 1) * pc_height)
-            pixel = p_in.get_pixel(x, y)
-            p_out.set_pixel(flip - col, row, pixel)
-    p_out.update()
+    def invoke(self, context, event):
+        self.is_panning = False
+        self.mouse_prev_x = 0.0
+        self.mouse_prev_y = 0.0
+        self.offset = Vector((0.0, 0.0))
+        self.scale = 1.0
 
+        # Get at least one 3D View
+        area_3d = None
+        for a in context.screen.areas:
+            if a.type == 'VIEW_3D':
+                area_3d = a
+                break
 
-############################################################################
+        if area_3d is None:
+            self.report({'ERROR'}, 'Could not find 3D View')
+            return {'CANCELLED'}
 
+        self.area = context.area
 
-def Draw_hdr_callback(self, context):
+        self.mouse_position = event.mouse_region_x, event.mouse_region_y
 
-    if context.area != Hdr.view3d_area:
-        return
-    elif context.area.type == 'PROPERTIES' and \
-            context.space_data.context != 'WORLD':
-        return
+        self.initial_elevation = context.scene.sun_pos_properties.hdr_elevation
+        self.initial_azimuth = context.scene.sun_pos_properties.hdr_azimuth
 
-    # Check if window area has changed for sticky zoom
-    theHdr = Hdr.object[0]
-    if Hdr.region.width < Hdr.saved_region_width:
-        diff = Hdr.saved_region_width - Hdr.region.width
-        if theHdr.origin.x + theHdr.width > Hdr.saved_region_width:
-            if theHdr.origin.x > 0:
-                theHdr.origin.x -= diff
-            else:
-                theHdr.width -= diff
-        else:
-            if Hdr.toolProps is not None:
-                if Hdr.toolProps.width > Hdr.toolProps_width:
-                    theHdr.origin.x -= diff
-                Hdr.toolProps_width = Hdr.toolProps.width
-            if theHdr.origin.x < 0:
-                theHdr.origin.x += diff
-    else:
-        diff = Hdr.region.width - Hdr.saved_region_width
-        if theHdr.width > Hdr.saved_region_width:
-            theHdr.width += diff
-        else:
-            if Hdr.toolProps is not None:
-                if Hdr.toolProps.width < Hdr.toolProps_width:
-                    theHdr.origin.x += diff
-                Hdr.toolProps_width = Hdr.toolProps.width
-    theHdr.set_dimensions(theHdr.width)
-    Hdr.saved_region_width = Hdr.region.width
-
-    zAzim = theHdr.width / 2
-    azimFac = zAzim / 180
-    zElev = theHdr.height / 2
-    elevFac = zElev / 90
-    crossChange = True
-
-    if not Hdr.action == 'PAN':
-        x = Hdr.mouse.x
-        y = Hdr.mouse.y
-        if x < theHdr.origin.x or x > theHdr.origin.x + theHdr.width:
-            crossChange = False
-            x = 0
-        else:
-            testBoundary = theHdr.origin.x + theHdr.width
-            if testBoundary < Hdr.region.width:
-                rightBoundary = testBoundary
-            else:
-                rightBoundary = Hdr.region.width
-            if x > rightBoundary:
-                crossChange = False
-                x = rightBoundary
-        cX = x - zAzim - theHdr.origin.x
-
-        if azimFac:
-            newAzimuth = cX / azimFac
-        else:
-            newAzimuth = 0.0
-
-        if y < theHdr.origin.y or y < 0:
-            crossChange = False
-            y = 0
-        elif y > theHdr.origin.y + theHdr.height:
-            crossChange = False
-            y = theHdr.origin.y + theHdr.height
-        cY = y - zElev - theHdr.origin.y
-
-        if elevFac:
-            newElevation = cY / elevFac
-        else:
-            newElevation = 0.0
+        context.workspace.status_text_set("Enter/LMB: confirm, Esc/RMB: cancel, MMB: pan, mouse wheel: zoom, Ctrl + mouse wheel: set exposure")
 
-        if newElevation == Hdr.elevation and newAzimuth == Hdr.azimuth:
-            crossChange = False
-        else:
-            Hdr.elevation = newElevation
-            Hdr.azimuth = newAzimuth
-    else:
-        if Hdr.grab.offset.x < Hdr.grab.spot.x:
-            off = Hdr.grab.spot.x - Hdr.grab.offset.x
-            theHdr.origin.x -= off
-        else:
-            off = Hdr.grab.offset.x - Hdr.grab.spot.x
-            theHdr.origin.x += off
-        if Hdr.grab.offset.y < Hdr.grab.spot.y:
-            off = Hdr.grab.spot.y - Hdr.grab.offset.y
-            theHdr.origin.y -= off
-        else:
-            off = Hdr.grab.offset.y - Hdr.grab.spot.y
-            theHdr.origin.y += off
-        Hdr.grab.spot.x = Hdr.mouse.x
-        Hdr.grab.spot.y = Hdr.mouse.y
-
-    Lx = theHdr.origin.x
-    Ly = theHdr.origin.y
-
-    # ---------------------
-    # Draw a textured quad
-    # ---------------------
-
-    bgl.glEnable(bgl.GL_BLEND)
-    if Hdr.glImage.bindcode == 0:
-        Hdr.load_gl_image()
-    bgl.glBindTexture(bgl.GL_TEXTURE_2D, Hdr.glImage.bindcode)
-    bgl.glEnable(bgl.GL_TEXTURE_2D)
-    bgl.glColor4f(1.0, 1.0, 1.0, Hdr.object[0].opacity)
-    bgl.glBegin(bgl.GL_QUADS)
-    bgl.glTexCoord2f(0.0, 0.0)
-    bgl.glVertex2f(Lx, Ly)
-    bgl.glTexCoord2f(1.0, 0.0)
-    bgl.glVertex2f(Lx + theHdr.width, Ly)
-    bgl.glTexCoord2f(1.0, 1.0)
-    bgl.glVertex2f(Lx + theHdr.width, Ly + theHdr.height)
-    bgl.glTexCoord2f(0.0, 1.0)
-    bgl.glVertex2f(Lx, theHdr.height + Ly)
-    bgl.glEnd()
-    bgl.glDisable(bgl.GL_TEXTURE_2D)
-
-    # ---------------------
-    # draw the crosshair
-    # ---------------------
-    x = theHdr.width / 2.0
-    if crossChange and not Hdr.lockCrosshair:
-        Sun.SP.HDR_azimuth = degToRad(newAzimuth + 180)
-    azimuth = ((radToDeg(Sun.SP.HDR_azimuth) - 180) * x / 180.0) + x
-
-    bgl.glEnable(bgl.GL_BLEND)
-    bgl.glEnable(bgl.GL_LINES)
-    bgl.glLineWidth(1.0)
-    alpha = 0.8
-    color = (0.4, 0.4, 0.4, alpha)
-    bgl.glColor4f(color[0], color[1], color[2], color[3])
-    bgl.glBegin(bgl.GL_LINES)
-    bgl.glVertex2f(Lx + azimuth, Ly)
-    bgl.glVertex2f(Lx + azimuth, Ly + theHdr.height)
-    bgl.glEnd()
-
-    y = theHdr.height / 2.0
-    if crossChange and not Hdr.lockCrosshair:
-        Sun.SP.HDR_elevation = newElevation
-    elevation = (Sun.SP.HDR_elevation * y / 90.0) + y
-
-    bgl.glColor4f(color[0], color[1], color[2], color[3])
-    bgl.glBegin(bgl.GL_LINES)
-    bgl.glVertex2f(Lx, Ly + elevation)
-    bgl.glVertex2f(Lx + theHdr.width, Ly + elevation)
-    bgl.glEnd()
-
-    # ---------------------
-    # draw the border
-    # ---------------------
-    bgl.glDisable(bgl.GL_BLEND)
-    color = (0.6, 0.6, .6, 1.0)
-    bgl.glColor4f(color[0], color[1], color[2], color[3])
-    bgl.glBegin(bgl.GL_LINE_LOOP)
-    bgl.glVertex2f(Lx, Ly)
-    bgl.glVertex2f(Lx + theHdr.width, Ly)
-    bgl.glVertex2f(Lx + theHdr.width, Ly + theHdr.height)
-    bgl.glVertex2f(Lx, theHdr.height + Ly)
-    bgl.glVertex2f(Lx, Ly)
-    bgl.glEnd()
-
-    bgl.glLineWidth(1.0)
-    bgl.glDisable(bgl.GL_LINES)
-    bgl.glFlush()
-
-# ---------------------------------------------------------------------------
-
-
-class SunPos_HdrHelp(bpy.types.Operator):
-    bl_idname = "object.hdrhelp_operator"
-    bl_label = "Hdr help"
-
-    def execute(self, context):
-        self.report({'INFO'}, self.message)
-        return {'FINISHED'}
+        self._handle = bpy.types.SpaceView3D.draw_handler_add(draw_callback_px,
+            (self, context), 'WINDOW', 'POST_PIXEL')
+        context.window_manager.modal_handler_add(self)
 
-    def invoke(self, context, event):
-        wm = context.window_manager
-        return wm.invoke_popup(self, width=400, height=200)
-
-    def draw(self, context):
-        self.layout.label(text="Available commands:")
-
-        row = self.layout.row()
-        split = row.split(factor=.26)
-        colL = split.column()
-        colR = split.column()
-        colL.label(text="Esc or Right Mouse ")
-        colR.label(text="Close map or text.")
-        colL.label(text="Left Mouse")
-        colR.label(text="Move crosshair.")
-        colL.label(text="G or MiddleMouse")
-        colR.label(text="Pan mode. Grab and move map or text.")
-        colL.label(text="Ctrl Middlemouse")
-        colR.label(text="Mouse zoom to point.")
-        self.layout.label("--- The following are changed by moving " +
-                          "the mouse or using the scroll wheel.")
-        self.layout.label(text="--- Use Ctrl for coarse increments or Alt for fine.")
-        row = self.layout.row()
-        split = row.split(factor=.25)
-        colL = split.column()
-        colR = split.column()
-        colL.label(text="Scroll wheel")
-        colR.label(text="Zoom to point.")
+        return {'RUNNING_MODAL'}
diff --git a/sun_position/map.py b/sun_position/map.py
index 9585ee4a..e90b6692 100644
--- a/sun_position/map.py
+++ b/sun_position/map.py
@@ -1,3 +1,21 @@
+### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
 # -*- coding: utf-8 -*-
 
 import bpy
@@ -8,8 +26,8 @@ import os
 import datetime
 import math
 
-from . sun_calc import degToRad, format_hms
-from . properties import Display, Sun
+from math import radians
+from . sun_calc import format_hms, sun
 
 # ---------------------------------------------------------------------------
 
@@ -24,16 +42,16 @@ class MapObject:
         self.type = t
         self.width = w
         self.height = h
-        self.heightFactor = .50
+        self.height_factor = .50
         self.opacity = 1.0
         self.focused = False
         self.view3d_area = None
-        self.colorStyle = 'N'
+        self.color_style = 'N'
         self.origin = self.Origin()
 
     def set_dimensions(self, width):
         self.width = width
-        self.height = int(width * self.heightFactor)
+        self.height = int(width * self.height_factor)
 
     def check_focus(self, context, event):
         self.focused = self.is_focused(context, event)
@@ -132,16 +150,16 @@ class MapClass:
     def reset(self, location):
         self.init(location)
         self.action = None
-        self.isActive = False
+        self.is_active = False
         self.start = False
         self.stop = False
-        self.lockCrosshair = True
-        self.showInfo = False
+        self.lock_crosshair = True
+        self.show_info = False
         self.latitude = 0.0
         self.longitude = 0.0
-        self.ctrlPress = False
-        self.altPress = False
-        self.lineWidth = 1.0
+        self.ctrl_press = False
+        self.alt_press = False
+        self.line_width = 1.0
         self.textureless = False
         self.mouse.x = 0
         self.mouse.y = 0
@@ -205,7 +223,7 @@ class MapClass:
                 self.handler1 = bpy.types.SpaceView3D.draw_handler_add(
                                    Map_load_callback,
                                    (self, context), 'WINDOW', 'POST_PIXEL')
-            self.isActive = True
+            self.is_active = True
             return True
         else:
             return False
@@ -238,14 +256,13 @@ class MapClass:
         self.image.free_it = False
         self.glImage = None
         self.image.bindcode = 0
-        self.isActive = False
-        #if Sun.SP:  # why removed?
-        Sun.SP.ShowMap = False  # indent?
+        self.is_active = False
+        sun.sp.show_map = False
 
     def load_blender_image(self, file_name):
         if file_name == "None":
             self.textureless = True
-            self.object[0].heightFactor = .5
+            self.object[0].height_factor = .5
             return True
         else:
             self.textureless = False
@@ -259,7 +276,7 @@ class MapClass:
                         if self.glImage is not None:
                             self.image.loaded = True
                             self.glImage.user_clear()
-                            self.object[0].heightFactor = \
+                            self.object[0].height_factor = \
                                 self.glImage.size[1] / self.glImage.size[0]
                             return True
                         else:
@@ -286,7 +303,7 @@ class MapClass:
                 fy -= 20
             return fy
 
-        if text.colorStyle == 'N':
+        if text.color_style == 'N':
             tColor = (0.8, 0.8, 0.8, 1.0)
             vColor = (1.0, 1.0, 1.0, 1.0)
         else:
@@ -296,31 +313,31 @@ class MapClass:
         blf.size(0, 14, 72)
         fx = text.origin.x
         fy = text.origin.y + 140
-        fy = text_line(fx + 10, fy, True, tColor, str(Sun.SP.Month) +
-                       " / " + str(Sun.SP.Day) +
-                       " / " + str(Sun.SP.Year))
+        fy = text_line(fx + 10, fy, True, tColor, str(sun.sp.month) +
+                      " / " + str(sun.sp.day) +
+                      " / " + str(sun.sp.year))
 
         fy = text_line(fx, fy, False, tColor, "  Day: ")
-        fy = text_line(fx + 40, fy, True, vColor, str(Sun.SP.Day_of_year))
+        fy = text_line(fx + 40, fy, True, vColor, str(sun.sp.day_of_year))
         fy = text_line(fx, fy, False, tColor, "Time: ")
-        fy = text_line(fx + 40, fy, True, vColor, format_hms(Sun.SP.Time))
+        fy = text_line(fx + 40, fy, True, vColor, format_hms(sun.sp.time))
 
-        if Sun.ShowRiseSet:
+        if sun.ShowRiseSet:
             fy -= 10
             fy = text_line(fx, fy, True, tColor, "Solar Noon:")
             fy = text_line(fx + 14, fy, True, vColor,
-                           format_hms(Sun.SolarNoon.time))
+                           format_hms(sun.SolarNoon.time))
             fy -= 10
             fy = text_line(fx, fy, False, tColor, "Rise: ")
-            if Sun.RiseSetOK:
+            if sun.RiseSetOK:
                 fy = text_line(fx + 40, fy, True, vColor,
-                               format_hms(Sun.Sunrise.time))
+                               format_hms(sun.Sunrise.time))
             else:
                 fy = text_line(fx + 40, fy, True, vColor, "--------")
             fy = text_line(fx, fy, False, tColor, " Set: ")
-            if Sun.RiseSetOK:
+            if sun.RiseSetOK:
                 fy = text_line(fx + 40, fy, True, vColor,
-                               format_hms(Sun.Sunset.time))
+                               format_hms(sun.sunset.time))
             else:
                 fy = text_line(fx + 40, fy, True, vColor, "--------")
 
@@ -337,7 +354,7 @@ class MapClass:
 
     def event_controller(self, context, event):
 
-        if not Sun.SP.ShowMap or event.type == 'TIMER':
+        if not sun.sp.show_map or event.type == 'TIMER':
             return {'PASS_THROUGH'}
 
         mapInFocus = self.object[0].check_focus(context, event)
@@ -488,7 +505,7 @@ def key_MiddleMouse(context, event):
 def key_A(event):
     if event.value == 'PRESS':
         if Map.object[0].focused:
-            Sun.SP.ObjectGroup = 'ANALEMMA'
+            sun.sp.object_group = 'ANALEMMA'
         else:
             return {'PASS_THROUGH'}
     return {'RUNNING_MODAL'}
@@ -497,10 +514,10 @@ def key_A(event):
 def key_C(event):
     if event.value == 'PRESS':
         if Map.object[0].focused or Map.object[1].focused:
-            if Map.object[1].colorStyle == 'N':
-                Map.object[1].colorStyle = 'R'
+            if Map.object[1].color_style == 'N':
+                Map.object[1].color_style = 'R'
             else:
-                Map.object[1].colorStyle = 'N'
+                Map.object[1].color_style = 'N'
         else:
             return {'PASS_THROUGH'}
     return {'RUNNING_MODAL'}
@@ -509,7 +526,7 @@ def key_C(event):
 def key_E(event):
     if event.value == 'PRESS':
         if Map.object[0].focused:
-            Sun.SP.ObjectGroup = 'ECLIPTIC'
+            sun.sp.object_group = 'ECLIPTIC'
         else:
             return {'PASS_THROUGH'}
     return {'RUNNING_MODAL'}
@@ -556,8 +573,8 @@ def key_S(event):
     if event.value == 'PRESS':
         if Map.object[0].focused:
             Map.showInfo = True if not Map.showInfo else False
-            Sun.PP.ShowRiseSet = True
-            Sun.ShowRiseSet = True
+            sun.PP.ShowRiseSet = True
+            sun.ShowRiseSet = True
         else:
             return {'PASS_THROUGH'}
     return {'RUNNING_MODAL'}
@@ -749,10 +766,10 @@ def mouse_zoom():
 
 
 def check_time_boundary():
-    if Sun.SP.Time > 24.0:
-        Sun.SP.Time += -24.0
-    elif Sun.SP.Time < 0.0:
-        Sun.SP.Time += 24.0
+    if sun.sp.time > 24.0:
+        sun.sp.time += -24.0
+    elif sun.sp.time < 0.0:
+        sun.sp.time += 24.0
     Display.refresh()
 
 
@@ -762,7 +779,7 @@ def time_change_wheel(action):
     else:
         val = -1.0 if not Map.altPress else -0.0166
     mf = 1.5 if Map.ctrlPress else 1.0
-    Sun.SP.Time += mf * val
+    sun.sp.time += mf * val
     check_time_boundary()
 
 
@@ -774,7 +791,7 @@ def time_change():
         sx = 0.0001 if Map.mouse.x > Map.grab.spot.x else -0.0001
     else:
         sx = (Map.mouse.x - Map.grab.spot.x) / mf
-    Sun.SP.Time += sx
+    sun.sp.time += sx
     Map.grab.spot.x = Map.mouse.x
     check_time_boundary()
 
@@ -799,18 +816,18 @@ def day_change():
 
 
 def day_change_bounds(wf):
-    if Sun.SP.Day_of_year + wf > 366:
-        Sun.SP.Day_of_year = 1
-        Sun.SP.Year += 1
-    elif Sun.SP.Day_of_year + wf < 1:
-        Sun.SP.Day_of_year = 366
-        Sun.SP.Year -= 1
+    if sun.sp.day_of_year + wf > 366:
+        sun.sp.day_of_year = 1
+        sun.sp.year += 1
+    elif sun.sp.day_of_year + wf < 1:
+        sun.sp.day_of_year = 366
+        sun.sp.year -= 1
     else:
-        Sun.SP.Day_of_year += wf
-    dt = (datetime.date(Sun.SP.Year, 1, 1) +
-             datetime.timedelta(Sun.SP.Day_of_year - 1))
-    Sun.SP.Day = dt.day
-    Sun.SP.Month = dt.month
+        sun.sp.day_of_year += wf
+    dt = (datetime.date(sun.sp.year, 1, 1) +
+             datetime.timedelta(sun.sp.day_of_year - 1))
+    sun.sp.day = dt.day
+    sun.sp.month = dt.month
     Display.refresh()
 
 # ---------------------------------------------------------------------------
@@ -861,22 +878,22 @@ def opacity_change_bounds(obj):
 
 def X_change_wheel(action):
     wf = wheel_factor(action)
-    if Sun.SP.Longitude + wf > 180.0:
-        Sun.SP.Longitude = -180.0
-    elif Sun.SP.Longitude + wf < -180.0:
-        Sun.SP.Longitude = 180.0
+    if sun.sp.longitude + wf > 180.0:
+        sun.sp.longitude = -180.0
+    elif sun.sp.longitude + wf < -180.0:
+        sun.sp.longitude = 180.0
     else:
-        Sun.SP.Longitude += wf
+        sun.sp.longitude += wf
 
 
 def Y_change_wheel(action):
     wf = wheel_factor(action)
-    if Sun.SP.Latitude + wf > 90.0:
-        Sun.SP.Latitude = -90.0
-    elif Sun.SP.Latitude + wf < -90.0:
-        Sun.SP.Latitude = 90.0
+    if sun.sp.latitude + wf > 90.0:
+        sun.sp.latitude = -90.0
+    elif sun.sp.latitude + wf < -90.0:
+        sun.sp.latitude = 90.0
     else:
-        Sun.SP.Latitude += wf
+        sun.sp.latitude += wf
 
 
 def wheel_factor(action):
@@ -892,11 +909,11 @@ def wheel_factor(action):
 
 def Map_load_callback(self, context):
 
-    if Sun.SP.ShowMap and not Map.image.loaded:
+    if sun.sp.show_map and not Map.image.loaded:
         Map.glImage = None
-        if not Map.load_blender_image(Sun.MapName):
+        if not Map.load_blender_image(sun.MapName):
             print("Could not load image file: ", Map.image.name)
-            Sun.SP.ShowMap = False
+            sun.SP.ShowMap = False
 
     if Map.start:
         def set_region_data():
@@ -933,13 +950,13 @@ def Map_load_callback(self, context):
             print("Could not get texture in gl_load()")
             Map.glImage = None
             Map.image.bindcode = 0
-            Sun.SP.ShowMap = False
+            sun.sp.show_map = False
 
         elif Map.textureless:
             set_region_data()
             return
         else:
-            Sun.SP.ShowMap = False
+            sun.sp.show_map = False
         return
 
     if Map.stop:
@@ -992,10 +1009,10 @@ def Draw_map_callback(self, context):
     # cylindrical projection with lat/long 0/0 exactly
     # in the middle of the image.
 
-    zLong = theMap.width / 2
-    longFac = zLong / 180
-    zLat = theMap.height / 2
-    latFac = zLat / 90
+    zLong = the_map.width / 2
+    longFac = z_long / 180
+    zLat = the_map.height / 2
+    latFac = z_lat / 90
     crossChange = True
 
     if not Map.action == 'PAN':
@@ -1092,8 +1109,8 @@ def Draw_map_callback(self, context):
     x = theMap.width / 2.0
     if crossChange and not Map.lockCrosshair:
         if Map.action != 'Y':
-            Sun.SP.Longitude = newLongitude
-    longitude = (Sun.SP.Longitude * x / 180.0) + x
+            sun.sp.longitude = newLongitude
+    longitude = (sun.sp.longitude * x / 180.0) + x
 
     bgl.glEnable(bgl.GL_BLEND)
     bgl.glEnable(bgl.GL_LINES)
@@ -1109,8 +1126,8 @@ def Draw_map_callback(self, context):
     y = theMap.height / 2.0
     if crossChange and not Map.lockCrosshair:
         if Map.action != 'X':
-            Sun.SP.Latitude = newLatitude
-    latitude = (Sun.SP.Latitude * y / 90.0) + y
+            sun.sp.latitude = newLatitude
+    latitude = (sun.sp.latitude * y / 90.0) + y
 
     alpha = 1.0 if Map.action == 'X' else 0.5
     color = (0.894, 0.741, .510, alpha)
@@ -1134,7 +1151,7 @@ def Draw_map_callback(self, context):
     bgl.glVertex2f(Lx, Ly)
     bgl.glEnd()
 
-    if not Sun.ShowRiseSet or not Map.lineWidth:
+    if not sun.ShowRiseSet or not Map.lineWidth:
         bgl.glDisable(bgl.GL_LINES)
         bgl.glFlush()
         return
@@ -1159,14 +1176,14 @@ def Draw_map_callback(self, context):
     py = Ly + latitude
 
     radius = 30 + Map.lineWidth * 10
-    if Sun.RiseSetOK and Map.lineWidth:
+    if sun.RiseSetOK and Map.lineWidth:
         color = (0.2, 0.6, 1.0, 0.9)
-        angle = -(degToRad(Sun.Sunrise.azimuth) - math.pi / 2)
+        angle = -(radians(sun.sunrise.azimuth) - math.pi / 2)
         bgl.glLineWidth(Map.lineWidth)
         draw_angled_line(color, angle, px, py)
 
         color = (0.86, 0.18, 0.18, 0.9)
-        angle = -(degToRad(Sun.Sunset.azimuth) - math.pi / 2)
+        angle = -(radians(sun.sunset.azimuth) - math.pi / 2)
         draw_angled_line(color, angle, px, py)
 
     # ------------------------
@@ -1174,18 +1191,18 @@ def Draw_map_callback(self, context):
     # ------------------------
 
     if Map.textureless:
-        phi = degToRad(Sun.AzNorth) * -1
+        phi = radians(sun.AzNorth) * -1
     else:
-        phi = degToRad(Sun.Azimuth) * -1
-    x = math.sin(phi) * math.sin(-Sun.Theta) * (radius + 10)
-    y = math.sin(Sun.Theta) * math.cos(phi) * (radius + 10)
+        phi = radians(sun.Azimuth) * -1
+    x = math.sin(phi) * math.sin(-sun.Theta) * (radius + 10)
+    y = math.sin(sun.Theta) * math.cos(phi) * (radius + 10)
     night = (0.24, 0.29, 0.94, 0.9)
     day = (0.85, 0.77, 0.60, 0.9)
-    if Sun.SolarNoon.elevation < 0.0:
+    if sun.SolarNoon.elevation < 0.0:
         color = night
-    elif Sun.Elevation >= Sun.Sunrise.elevation:
-        if Sun.Time >= Sun.Sunset.time and \
-                Sun.Elevation <= Sun.Sunset.elevation:
+    elif sun.Elevation >= sun.Sunrise.elevation:
+        if sun.Time >= sun.Sunset.time and \
+                sun.Elevation <= sun.Sunset.elevation:
             color = night
         else:
             color = day
diff --git a/sun_position/north.py b/sun_position/north.py
index 98269ae2..3ef5f822 100644
--- a/sun_position/north.py
+++ b/sun_position/north.py
@@ -1,87 +1,111 @@
+### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
 import bpy
 import bgl
 import math
 import gpu
 from gpu_extras.batch import batch_for_shader
 from mathutils import Vector
-from .shader import Dashed_Shader_3D
-from . properties import Sun
-
-dashedLineShader = gpu.types.GPUShader(Dashed_Shader_3D.vertex_shader, Dashed_Shader_3D.fragment_shader)
-
-class NorthClass:
-
-    def __init__(self):
-        self.handler = None
-        self.isActive = False
-
-    def refresh_screen(self):
-        #bpy.context.scene.cursor.location.x += 0.0
-        bpy.context.area.tag_redraw()
-
-    def activate(self, context):
-
-        if context.area.type == 'PROPERTIES':
-            self.handler = bpy.types.SpaceView3D.draw_handler_add(
-                               DrawNorth_callback,
-                               #(self, context), 'WINDOW', 'POST_PIXEL')  # why changed?
-                               (self, context), 'WINDOW', 'POST_VIEW')
-            self.isActive = True
-            self.refresh_screen()
-            return True
-        return False
-
-    def deactivate(self):
-        if self.handler is not None:
-            bpy.types.SpaceView3D.draw_handler_remove(self.handler, 'WINDOW')
-            self.handler = None
-        self.isActive = False
-        self.refresh_screen()
-        #if Sun.SP:  # why removed?
-        Sun.SP.ShowNorth = False  # indent?
-        Sun.ShowNorth = False
-
-
-North = NorthClass()
-
-
-def DrawNorth_callback(self, context):
-
-    if not Sun.SP.ShowNorth and North.isActive:
-        North.deactivate()
-        return
-
-    # ------------------------------------------------------------------
-    # Set up the compass needle using the current north offset angle
-    # less 90 degrees.  This forces the unit circle to begin at the
-    # 12 O'clock instead of 3 O'clock position.
-    # ------------------------------------------------------------------
-    #color = (0.2, 0.6, 1.0, 0.7)
-    color = (0.2, 0.6, 1.0, 1)
-    radius = 100
-    angle = -(Sun.NorthOffset - math.pi / 2)
-    x = math.cos(angle) * radius
-    y = math.sin(angle) * radius
-
-    #p1, p2 = (0, 0, 0), (x, y, 0)   # Start & end of needle
-    # much removed / changed from original below
-    bgl.glEnable(bgl.GL_MULTISAMPLE)
-    bgl.glEnable(bgl.GL_LINE_SMOOTH)
-    bgl.glEnable(bgl.GL_BLEND)
-
-    bgl.glEnable(bgl.GL_DEPTH_TEST)
-    bgl.glDepthMask(False)
-
-    bgl.glLineWidth(2)
-
-
-    p1 = (0, 0, 0)
-    p2 = (x/20 , y/20, 0)
-    coords = [p1, p2]   # Start & end of needle
-    arclengths = [0,(Vector(p1)-Vector(p2)).length]
-    batch = batch_for_shader(dashedLineShader, 'LINES', {"pos": coords,"arcLength":arclengths})
-
-    dashedLineShader.bind()
-    dashedLineShader.uniform_float("finalColor", color)
-    dashedLineShader.uniform_float("u_Scale", 10)
-    batch.draw(dashedLineShader)
+
+
+if bpy.app.background:  # ignore north line in background mode
+    def north_update(self, context):
+        pass
+else:
+    vertex_shader = '''
+        uniform mat4 u_ViewProjectionMatrix;
+
+        in vec3 position;
+
+        flat out vec2 v_StartPos;
+        out vec4 v_VertPos;
+
+        void main()
+        {
+            vec4 pos    = u_ViewProjectionMatrix * vec4(position, 1.0f);
+            gl_Position = pos;
+            v_StartPos    = (pos / pos.w).xy;
+            v_VertPos     = pos;
+        }
+    '''
+
+    fragment_shader = '''
+        uniform vec4 u_Color;
+
+        flat in vec2 v_StartPos;
+        in vec4 v_VertPos;
+
+        uniform vec2 u_Resolution;
+
+        void main()
+        {
+            vec4 vertPos_2d = v_VertPos / v_VertPos.w;
+            vec2 dir  = (vertPos_2d.xy - v_StartPos.xy) * u_Resolution;
+            float dist = length(dir);
+
+            if (step(sin(dist / 5.0f), 0.0) == 1) discard;
+
+            gl_FragColor = u_Color;
+        }
+    '''
+
+    shader = gpu.types.GPUShader(vertex_shader, fragment_shader)
+
+    def draw_north_callback():
+        # ------------------------------------------------------------------
+        # Set up the compass needle using the current north offset angle
+        # less 90 degrees.  This forces the unit circle to begin at the
+        # 12 O'clock instead of 3 O'clock position.
+        # ------------------------------------------------------------------
+        addon_prefs = bpy.context.preferences.addons[__package__].preferences
+        sun_props = bpy.context.scene.sun_pos_properties
+
+        color = (0.2, 0.6, 1.0, 0.7)
+        radius = 100
+        angle = -(sun_props.north_offset - math.pi / 2)
+        x = math.cos(angle) * radius
+        y = math.sin(angle) * radius
+
+        coords = Vector((x, y, 0)), Vector((0, 0, 0))   # Start & end of needle
+
+        batch = batch_for_shader(
+            shader, 'LINE_STRIP',
+            {"position": coords},
+        )
+        shader.bind()
+
+        matrix = bpy.context.region_data.perspective_matrix
+        shader.uniform_float("u_ViewProjectionMatrix", matrix)
+        shader.uniform_float("u_Resolution", (bpy.context.region.width, bpy.context.region.height))
+        shader.uniform_float("u_Color", color)
+        bgl.glLineWidth(2.0)
+        batch.draw(shader)
+
+
+    _handle = None
+
+
+    def north_update(self, context):
+        global _handle
+        if self.show_north and _handle is None:
+            _handle = bpy.types.SpaceView3D.draw_handler_add(draw_north_callback, (), 'WINDOW', 'POST_VIEW')
+        elif _handle is not None:
+            bpy.types.SpaceView3D.draw_handler_remove(_handle, 'WINDOW')
+            _handle = None
+        context.area.tag_redraw()
diff --git a/sun_position/operators.py b/sun_position/operators.py
deleted file mode 100644
index 08fe42b6..00000000
--- a/sun_position/operators.py
+++ /dev/null
@@ -1,313 +0,0 @@
-import bpy
-import datetime
-
-from . properties import *
-from . sun_calc import Move_sun
-from . north import *
-from . map import Map
-from . hdr import Hdr
-
-# ---------------------------------------------------------------------------
-
-
-class ControlClass:
-
-    region = None
-    handler = None
-
-    def callback(self, os, context):
-        if Sun.SP.IsActive:
-            if self.panel_changed():
-                Move_sun()
-        else:
-            self.remove_handler()
-
-    def activate(self, context):
-        if context.area.type == 'PROPERTIES':
-            if Display.ENABLE:
-                Display.setAction('PANEL')
-                Sun.SP.IsActive = True
-                self.region = context.region
-                self.add_handler(context)
-                return {'RUNNING_MODAL'}
-            else:
-                Display.setAction('ENABLE')
-                Sun.SP.IsActive = False
-                Map.deactivate()
-                Hdr.deactivate()
-                return {'FINISHED'}
-        else:
-            self.report({'WARNING'}, "Context not available")
-            return {'CANCELLED'}
-
-    def add_handler(self, context):
-        self.handler = bpy.types.SpaceView3D.draw_handler_add(self.callback,
-                              (self, context), 'WINDOW', 'POST_PIXEL')
-
-    def remove_handler(self):
-        if self.handler:
-            bpy.types.SpaceView3D.draw_handler_remove(self.handler, 'WINDOW')
-        self.handler = None
-
-    def panel_changed(self):
-        rv = False
-        sp = Sun.SP
-
-        if not Sun.UseDayMonth and sp.Day_of_year != Sun.Day_of_year:
-            dt = (datetime.date(sp.Year, 1, 1) +
-                    datetime.timedelta(sp.Day_of_year - 1))
-            Sun.Day = dt.day
-            Sun.Month = dt.month
-            Sun.Day_of_year = sp.Day_of_year
-            sp.Day = dt.day
-            sp.Month = dt.month
-            rv = True
-        elif (sp.Day != Sun.Day or sp.Month != Sun.Month):
-            try:
-                dt = datetime.date(sp.Year, sp.Month, sp.Day)
-                sp.Day_of_year = dt.timetuple().tm_yday
-                Sun.Day = sp.Day
-                Sun.Month = sp.Month
-                Sun.Day_of_year = sp.Day_of_year
-                rv = True
-            except:
-                pass
-
-        if Sun.PP.UsageMode == "HDR":
-            if sp.BindToSun != Sun.BindToSun:
-                Sun.BindToSun = sp.BindToSun
-                if Sun.BindToSun:
-                    nt = bpy.context.scene.world.node_tree.nodes
-                    envTex = nt.get(sp.HDR_texture)
-                    if envTex:
-                        if envTex.type == "TEX_ENVIRONMENT":
-                            Sun.Bind.tex_location = envTex.texture_mapping.rotation
-                            Sun.Bind.azStart = sp.HDR_azimuth
-                            obj = bpy.context.view_layer.objects.get(Sun.SunObject)
-                Sun.HDR_texture = sp.HDR_texture
-                Sun.Elevation = sp.HDR_elevation
-                Sun.Azimuth = sp.HDR_azimuth
-                Sun.Bind.elevation = sp.HDR_elevation
-                Sun.Bind.azimuth = sp.HDR_azimuth
-                Sun.SunDistance = sp.SunDistance
-                return True
-            if (sp.HDR_elevation != Sun.Bind.elevation or
-                    sp.HDR_azimuth != Sun.Bind.azimuth or
-                    sp.SunDistance != Sun.SunDistance):
-                Sun.Elevation = sp.HDR_elevation
-                Sun.Azimuth = sp.HDR_azimuth
-                Sun.Bind.elevation = sp.HDR_elevation
-                Sun.Bind.azimuth = sp.HDR_azimuth
-                Sun.SunDistance = sp.SunDistance
-                return True
-            return False
-
-        if (rv or sp.Time != Sun.Time or
-                sp.TimeSpread != Sun.TimeSpread or
-                sp.SunDistance != Sun.SunDistance or
-                sp.Latitude != Sun.Latitude or
-                sp.Longitude != Sun.Longitude or
-                sp.UTCzone != Sun.UTCzone or
-                sp.Year != Sun.Year or
-                sp.UseSkyTexture != Sun.UseSkyTexture or
-                sp.SkyTexture != Sun.SkyTexture or
-                sp.HDR_texture != Sun.HDR_texture or
-                sp.UseSunObject != Sun.UseSunObject or
-                sp.SunObject != Sun.SunObject or
-                sp.UseObjectGroup != Sun.UseObjectGroup or
-                sp.ObjectGroup != Sun.ObjectGroup or
-                sp.DaylightSavings != Sun.DaylightSavings or
-                sp.ShowRefraction != Sun.ShowRefraction or
-                sp.ShowNorth != Sun.ShowNorth or
-                sp.NorthOffset != Sun.NorthOffset):
-
-            Sun.Time = sp.Time
-            Sun.TimeSpread = sp.TimeSpread
-            Sun.SunDistance = sp.SunDistance
-            Sun.Latitude = sp.Latitude
-            Sun.Longitude = sp.Longitude
-            Sun.UTCzone = sp.UTCzone
-            Sun.Year = sp.Year
-            Sun.UseSkyTexture = sp.UseSkyTexture
-            Sun.SkyTexture = sp.SkyTexture
-            Sun.HDR_texture = sp.HDR_texture
-            Sun.UseSunObject = sp.UseSunObject
-            Sun.SunObject = sp.SunObject
-            Sun.UseObjectGroup = sp.UseObjectGroup
-            Sun.ObjectGroup = sp.ObjectGroup
-            Sun.DaylightSavings = sp.DaylightSavings
-            Sun.ShowRefraction = sp.ShowRefraction
-            Sun.ShowNorth = sp.ShowNorth
-            Sun.NorthOffset = sp.NorthOffset
-            return True
-        return False
-
-
-Controller = ControlClass()
-
-# ---------------------------------------------------------------------------
-
-
-class SunPos_OT_Controller(bpy.types.Operator):
-    bl_idname = "world.sunpos_controller"
-    bl_label = "Sun panel event handler"
-    bl_description = "Enable sun panel"
-
-    def __del__(self):
-        Stop_all_handlers()
-        Controller.remove_handler()
-        Display.setAction('ENABLE')
-        #try:
-        Sun.SP.IsActive = False  # indent?
-        #except:
-        #    pass
-
-    def modal(self, context, event):
-
-        if Display.PANEL:
-
-            if Sun.SP.ShowMap:
-                if not Map.isActive:
-                    if not Map.activate(context):
-                        Sun.SP.ShowMap = False
-            elif Map.isActive:
-                Map.deactivate()
-
-            if Sun.SP.ShowHdr:
-                if not Hdr.isActive:
-                    Sun.SP.BindToSun = False
-                    if not Hdr.activate(context):
-                        Sun.SP.ShowHdr = False
-            elif Hdr.isActive:
-                Hdr.deactivate()
-
-            if Sun.SP.ShowNorth:
-                if not North.isActive:
-                    North.activate(context)
-            elif North.isActive:
-                North.deactivate()
-
-            return {'PASS_THROUGH'}
-
-        Display.refresh()
-        return {'FINISHED'}
-
-    def invoke(self, context, event):
-
-        Sun.verify_ObjectGroup()
-        Map.init(Sun.PP.MapLocation)
-        Hdr.init()
-        retval = Controller.activate(context)
-        if retval != {'RUNNING_MODAL'}:
-            return retval
-
-        context.window_manager.modal_handler_add(self)
-        Sun.PreBlend_handler = SunPos_new_blendfile
-        bpy.app.handlers.load_pre.append(SunPos_new_blendfile)
-        Sun.Frame_handler = Frame_handler
-        bpy.app.handlers.frame_change_pre.append(Frame_handler)
-
-        Display.setAction('PANEL')
-        Sun.SP.IsActive = True
-
-        return {'RUNNING_MODAL'}
-
-############################################################################
-
-
-class SunPos_OT_Map(bpy.types.Operator):
-    bl_idname = "sunpos.map"
-    bl_label = "World map"
-
-    def modal(self, context, event):
-        if Map.view3d_area != context.area or not Sun.SP.ShowMap:
-            Map.deactivate()
-            Display.refresh()
-            return {'FINISHED'}
-        elif not Display.PANEL:
-            Stop_all_handlers()
-            return {'FINISHED'}
-        return Map.event_controller(context, event)
-
-    def invoke(self, context, event):
-        context.window_manager.modal_handler_add(self)
-        Display.refresh()
-        return {'RUNNING_MODAL'}
-
-############################################################################
-
-
-class SunPos_OT_Hdr(bpy.types.Operator):
-    bl_idname = "sunpos.hdr"
-    bl_label = "HDR map"
-
-    def modal(self, context, event):
-        if Hdr.view3d_area != context.area or not Sun.SP.ShowHdr:
-            Hdr.deactivate()
-            Display.refresh()
-            return {'FINISHED'}
-        elif not Display.PANEL:
-            Stop_all_handlers()
-            return {'FINISHED'}
-        return Hdr.event_controller(context, event)
-
-    def invoke(self, context, event):
-        context.window_manager.modal_handler_add(self)
-        Display.refresh()
-        return {'RUNNING_MODAL'}
-
-############################################################################
-
-
-def SunPos_new_blendfile(context):
-    Stop_all_handlers()
-    Cleanup_objects()
-
-
-def Cleanup_callback(self, context):
-    Stop_all_handlers()
-    Cleanup_objects()
-
-
-def Cleanup_objects():
-    try:
-        #if Sun.SP:  # why removed?
-        Sun.SP.UseObjectGroup = False  # indent?
-        Sun.UseObjectGroup = False
-    except:
-        pass
-    del Sun.Selected_objects[:]
-    del Sun.Selected_names[:]
-    Display.setAction('ENABLE')
-    #if Sun.SP:  # why removed?
-    Sun.SP.IsActive = False  # indent?
-
-
-def Stop_all_handlers():
-    North.deactivate()
-    Map.deactivate()
-    Hdr.deactivate()
-
-    if Sun.Frame_handler is not None:
-        try:
-            bpy.app.handlers.frame_change_pre.remove(Frame_handler)
-        except:
-            pass
-    Sun.Frame_handler = None
-
-    if Sun.PreBlend_handler is not None:
-        try:
-            bpy.app.handlers.load_pre.remove(SunPos_new_blendfile)
-        except:
-            pass
-    Sun.PreBlend_handler = None
-
-############################################################################
-# The Frame_handler is called while rendering when the scene changes
-# to make sure objects are updated according to any keyframes.
-############################################################################
-
-
-def Frame_handler(context):
-    Controller.panel_changed()
-    Move_sun()
diff --git a/sun_position/properties.py b/sun_position/properties.py
index 14cbd428..11d2eaed 100644
--- a/sun_position/properties.py
+++ b/sun_position/properties.py
@@ -1,304 +1,243 @@
-import bpy
-from bpy.props import (
-    StringProperty, EnumProperty,
-    IntProperty, FloatProperty,
-    BoolProperty
-)
-from bpy.types import (
-    PropertyGroup
-)
-
-class DisplayAction:
-    ENABLE = True
-    PANEL = False
-    PREFERENCES = False
-
-    def __init__(self):
-        self.invert_zoom_wheel = False
-        self.invert_mouse_zoom = False
-
-    def setAction(self, VAL):
-        if VAL == 'ENABLE':
-            self.ENABLE = True
-            self.PANEL = self.PREFERENCES = False
-        elif VAL == 'PANEL':
-            self.PANEL = True
-            self.ENABLE = self.PREFERENCES = False
-        else:
-            self.PREFERENCES = True
-            self.ENABLE = self.PANEL = False
-
-    def refresh(self):
-        # Touching the cursor forces a screen refresh
-        #bpy.context.scene.cursor.location.x += 0.0
-        bpy.context.area.tag_redraw()
-
-
-Display = DisplayAction()
-Display.setAction('ENABLE')
+### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
 
-############################################################################
-# SunClass is used for storing and comparing changes in panel values
-# as well as general traffic direction.
-############################################################################
+import bpy
+from bpy.types import AddonPreferences, PropertyGroup
+from bpy.props import (StringProperty, EnumProperty, IntProperty,
+                       FloatProperty, BoolProperty, PointerProperty)
 
+from .sun_calc import sun_update, parse_coordinates
+from .north import north_update
 
-class SunClass:
-
-    class TazEl:
-        time = 0.0
-        azimuth = 0.0
-        elevation = 0.0
-
-    class CLAMP:
-        tex_location = None
-        elevation = 0.0
-        azimuth = 0.0
-        azStart = 0.0
-        azDiff = 0.0
-
-    Sunrise = TazEl()
-    Sunset = TazEl()
-    SolarNoon = TazEl()
-    RiseSetOK = False
-
-    Bind = CLAMP()
-    BindToSun = False
-    PlaceLabel = "Time & Place"
-    PanelLabel = "Panel Location"
-    MapName = "WorldMapLR.jpg"
-    MapLocation = 'VIEWPORT'
-
-    Latitude = 0.0
-    Longitude = 0.0
-    Elevation = 0.0
-    Azimuth = 0.0
-    AzNorth = 0.0
-    Phi = 0.0
-    Theta = 0.0
-    LX = 0.0
-    LY = 0.0
-    LZ = 0.0
-
-    Month = 0
-    Day = 0
-    Year = 0
-    Day_of_year = 0
-    Time = 0.0
-
-    UTCzone = 0
-    SunDistance = 0.0
-    TimeSpread = 23.50
-    UseDayMonth = True
-    DaylightSavings = False
-    ShowRiseSet = True
-    ShowRefraction = True
-    NorthOffset = 0.0
-
-    UseSunObject = False
-    SunObject = "Sun"
-
-    UseSkyTexture = False
-    SkyTexture = "Sky Texture"
-    HDR_texture = "Environment Texture"
-
-    UseObjectGroup = False
-    ObjectGroup = 'ECLIPTIC'
-    Selected_objects = []
-    Selected_names = []
-    ObjectGroup_verified = False
-
-    PreBlend_handler = None
-    Frame_handler = None
-    SP = None
-    PP = None
-    Counter = 0
-
-    # ----------------------------------------------------------------------
-    # There are times when the object group needs to be deleted such as
-    # when opening a new file or when objects might be deleted that are
-    # part of the group. If such is the case, delete the object group.
-    # ----------------------------------------------------------------------
-
-    def verify_ObjectGroup(self):
-        if not self.ObjectGroup_verified:
-            if len(self.Selected_names) > 0:
-                names = [x.name for x in bpy.data.objects]
-                for x in self.Selected_names:
-                    if not x in names:
-                        del self.Selected_objects[:]
-                        del self.Selected_names[:]
-                        break
-            self.ObjectGroup_verified = True
-
-Sun = SunClass()
 
 ############################################################################
 # Sun panel properties
 ############################################################################
 
 
-class SunPosSettings(PropertyGroup):
-
-    IsActive: BoolProperty(
-        description="True if panel enabled.",
-        default=False)
+class SunPosProperties(PropertyGroup):
+    usage_mode: EnumProperty(
+        name="Usage mode",
+        description="Operate in normal mode or environment texture mode",
+        items=(
+            ('NORMAL', "Normal", ""),
+            ('HDR', "Sun + HDR texture", ""),
+        ),
+        default='NORMAL',
+        update=sun_update)
 
-    ShowMap: BoolProperty(
+    show_map: BoolProperty(
         description="Show world map.",
-        default=False)
+        default=False,
+        update=sun_update)
 
-    DaylightSavings: BoolProperty(
+    daylight_savings: BoolProperty(
         description="Daylight savings time adds 1 hour to standard time.",
-        default=0)
+        default=0,
+        update=sun_update)
 
-    ShowRefraction: BoolProperty(
+    show_refraction: BoolProperty(
         description="Show apparent sun position due to refraction.",
-        default=1)
+        default=1,
+        update=sun_update)
 
-    ShowNorth: BoolProperty(
+    show_north: BoolProperty(
         description="Draws line pointing north.",
-        default=0)
+        default=0,
+        update=north_update)
 
-    Latitude: FloatProperty(
+    latitude: FloatProperty(
         attr="",
         name="Latitude",
         description="Latitude: (+) Northern (-) Southern",
         soft_min=-90.000, soft_max=90.000, step=3.001,
-        default=40.000, precision=3)
+        default=40.000, precision=3,
+        update=sun_update)
 
-    Longitude: FloatProperty(
+    longitude: FloatProperty(
         attr="",
         name="Longitude",
         description="Longitude: (-) West of Greenwich  (+) East of Greenwich",
         soft_min=-180.000, soft_max=180.000,
-        step=3.001, default=1.000, precision=3)
+        step=3.001, default=1.000, precision=3,
+        update=sun_update)
 
-    Month: IntProperty(
+    co_parser: StringProperty(
+        attr="",
+        name="Enter coordinates",
+        description="Enter coordinates from an online map",
+        update=parse_coordinates)
+
+    month: IntProperty(
         attr="",
         name="Month",
         description="",
-        min=1, max=12, default=6)
+        min=1, max=12, default=6,
+        update=sun_update)
 
-    Day: IntProperty(
+    day: IntProperty(
         attr="",
         name="Day",
         description="",
-        min=1, max=31, default=21)
+        min=1, max=31, default=21,
+        update=sun_update)
 
-    Year: IntProperty(
+    year: IntProperty(
         attr="",
         name="Year",
         description="",
-        min=1800, max=4000, default=2012)
+        min=1800, max=4000, default=2012,
+        update=sun_update)
+
+    use_day_of_year: BoolProperty(
+        description="Use a single value for day of year",
+        name="Use day of year",
+        default=False,
+        update=sun_update)
 
-    Day_of_year: IntProperty(
+    day_of_year: IntProperty(
         attr="",
         name="Day of year",
         description="",
-        min=1, max=366, default=1)
+        min=1, max=366, default=1,
+        update=sun_update)
 
-    UTCzone: IntProperty(
+    UTC_zone: IntProperty(
         attr="",
         name="UTC zone",
         description="Time zone: Difference from Greenwich England in hours.",
-        min=0, max=12, default=0)
+        min=-12, max=12, default=0,
+        update=sun_update)
 
-    Time: FloatProperty(
+    time: FloatProperty(
         attr="",
         name="Time",
         description="",
         precision=4,
-        soft_min=0.00, soft_max=23.9999, step=1.00, default=12.00)
+        soft_min=0.00, soft_max=23.9999, step=1.00, default=12.00,
+        update=sun_update)
 
-    NorthOffset: FloatProperty(
+    north_offset: FloatProperty(
         attr="",
         name="",
         description="North offset in degrees or radians "
                     "from scene's units settings",
         unit="ROTATION",
-        soft_min=-3.14159265, soft_max=3.14159265, step=10.00, default=0.00)
+        soft_min=-3.14159265, soft_max=3.14159265, step=10.00, default=0.00,
+        update=sun_update)
 
-    SunDistance: FloatProperty(
+    sun_distance: FloatProperty(
         attr="",
         name="Distance",
         description="Distance to sun from XYZ axes intersection.",
         unit="LENGTH",
-        soft_min=1, soft_max=3000.00, step=10.00, default=50.00)
+        soft_min=1, soft_max=3000.00, step=10.00, default=50.00,
+        update=sun_update)
+
+    use_sun_object: BoolProperty(
+        description="Enable sun positioning of named lamp or mesh",
+        default=False,
+        update=sun_update)
+
+    sun_object: PointerProperty(
+        type=bpy.types.Object,
+        # default="Sun",
+        description="Sun object to set in the scene",
+        poll=lambda self, obj: obj.type == 'LIGHT',
+        update=sun_update)
+
+    use_object_collection: BoolProperty(
+        description="Allow a group of objects to be positioned.",
+        default=False,
+        update=sun_update)
 
-    UseSunObject: BoolProperty(
-        description="Enable sun positioning of named light or mesh",
-        default=False)
+    object_collection: PointerProperty(
+        type=bpy.types.Collection,
+        description="Collection of objects used for analemma",
+        update=sun_update)
 
-    SunObject: StringProperty(
-        default="Sun",
-        name="theSun",
-        description="Name of sun object")
+    object_collection_type: EnumProperty(
+        name="Display type",
+        description="Show object group on ecliptic or as analemma",
+        items=(
+            ('ECLIPTIC', "On the Ecliptic", ""),
+            ('ANALEMMA', "As Analemma", ""),
+        ),
+        default='ECLIPTIC',
+        update=sun_update)
 
-    UseSkyTexture: BoolProperty(
+    use_sky_texture: BoolProperty(
         description="Enable use of Cycles' "
                     "sky texture. World nodes must be enabled, "
                     "then set color to Sky Texture.",
-        default=False)
+        default=False,
+        update=sun_update)
 
-    SkyTexture: StringProperty(
+    sky_texture: StringProperty(
         default="Sky Texture",
         name="sunSky",
-        description="Name of sky texture to be used")
+        description="Name of sky texture to be used",
+        update=sun_update)
 
-    ShowHdr: BoolProperty(
-        description="Click to set sun location on environment texture",
-        default=False)
-
-    HDR_texture: StringProperty(
+    hdr_texture: StringProperty(
         default="Environment Texture",
         name="envSky",
         description="Name of texture to use. World nodes must be enabled "
-                    "and color set to Environment Texture")
+                    "and color set to Environment Texture",
+        update=sun_update)
 
-    HDR_azimuth: FloatProperty(
+    hdr_azimuth: FloatProperty(
         attr="",
         name="Rotation",
         description="Rotation angle of sun and environment texture "
                     "in degrees or radians from scene's units settings",
         unit="ROTATION",
-        step=1.00, default=0.00, precision=3)
+        step=10.0,
+        default=0.0, precision=3,
+        update=sun_update)
 
-    HDR_elevation: FloatProperty(
+    hdr_elevation: FloatProperty(
         attr="",
         name="Elevation",
         description="Elevation angle of sun",
-        step=3.001,
-        default=0.000, precision=3)
+        unit="ROTATION",
+        step=10.0,
+        default=0.0, precision=3,
+        update=sun_update)
 
-    BindToSun: BoolProperty(
+    bind_to_sun: BoolProperty(
         description="If true, Environment texture moves with sun.",
-        default=False)
-
-    UseObjectGroup: BoolProperty(
-        description="Allow a group of objects to be positioned.",
-        default=False)
+        default=False,
+        update=sun_update)
 
-    TimeSpread: FloatProperty(
+    time_spread: FloatProperty(
         attr="",
         name="Time Spread",
         description="Time period in which to spread object group",
         precision=4,
-        soft_min=1.00, soft_max=24.00, step=1.00, default=23.00)
+        soft_min=1.00, soft_max=24.00, step=1.00, default=23.00,
+        update=sun_update)
 
-    ObjectGroup: EnumProperty(
-        name="Display type",
-        description="Show object group on ecliptic or as analemma",
-        items=(
-            ('ECLIPTIC', "On the Ecliptic", ""),
-            ('ANALEMMA', "As and Analemma", ""),
-        ),
-        default='ECLIPTIC')
-
-    Location: StringProperty(
+    location: StringProperty(
         default="view3d",
         name="location",
-        description="panel location")
+        description="panel location",
+        update=sun_update)
 
 
 ############################################################################
@@ -306,18 +245,10 @@ class SunPosSettings(PropertyGroup):
 ############################################################################
 
 
-class SunPosPreferences(PropertyGroup):
+class SunPosAddonPreferences(AddonPreferences):
+    bl_idname = __package__
 
-    UsageMode: EnumProperty(
-        name="Usage mode",
-        description="operate in normal mode or environment texture mode",
-        items=(
-            ('NORMAL', "Normal", ""),
-            ('HDR', "Sun + HDR texture", ""),
-        ),
-        default='NORMAL')
-
-    MapLocation: EnumProperty(
+    map_location: EnumProperty(
         name="Map location",
         description="Display map in viewport or world panel",
         items=(
@@ -326,43 +257,69 @@ class SunPosPreferences(PropertyGroup):
         ),
         default='VIEWPORT')
 
-    UseOneColumn: BoolProperty(
-        description="Set panel to use one column.",
-        default=False)
-
-    UseTimePlace: BoolProperty(
-        description="Show time/place presets.",
+    use_time_place: BoolProperty(
+        description="Show time/place presets",
         default=False)
 
-    UseObjectGroup: BoolProperty(
-        description="Use object group option.",
+    use_object_collection: BoolProperty(
+        description="Use object collection",
         default=True)
 
-    ShowDMS: BoolProperty(
-        description="Show lat/long degrees,minutes,seconds labels.",
+    show_dms: BoolProperty(
+        description="Show lat/long degrees, minutes, seconds labels",
         default=True)
 
-    ShowNorth: BoolProperty(
-        description="Show north offset choice and slider.",
-        default=True)
+    show_north: BoolProperty(
+        description="Show north offset choice and slider",
+        default=True,
+        update=sun_update)
 
-    ShowRefraction: BoolProperty(
-        description="Show sun refraction choice.",
-        default=True)
+    show_refraction: BoolProperty(
+        description="Show sun refraction choice",
+        default=True,
+        update=sun_update)
 
-    ShowAzEl: BoolProperty(
-        description="Show azimuth and solar elevation info.",
+    show_az_el: BoolProperty(
+        description="Show azimuth and solar elevation info",
         default=True)
 
-    ShowDST: BoolProperty(
-        description="Show daylight savings time choice.",
+    show_dst: BoolProperty(
+        description="Show daylight savings time choice",
         default=True)
 
-    ShowRiseSet: BoolProperty(
-        description="Show sunrise and sunset.",
+    show_rise_set: BoolProperty(
+        description="Show sunrise and sunset",
         default=True)
 
-    MapName: StringProperty(
-        default="WorldMap.jpg",
-        name="WorldMap",
-        description="Name of world map")
+    # Uncomment this if you add other map images
+    # map_name: StringProperty(
+    #     default="WorldMap.jpg",
+    #     name="WorldMap",
+    #     description="Name of world map")
+
+    def draw(self, context):
+        layout = self.layout
+
+        box = layout.box()
+        col = box.column()
+
+        # Uncomment this if you add other map images
+        # row = col.row()
+        # row.label(text="World map options:")
+        # row.operator_menu_enum('world.wmp_operator',
+        #                        'map_presets', text=Sun.map_name)
+        # col.separator()
+
+        col.prop(self, "map_location")
+        col.separator()
+
+        col.label(text="Show or use:")
+        flow = col.grid_flow(columns=0, even_columns=True, even_rows=False, align=False)
+        flow.prop(self, "use_time_place", text="Time/place presets")
+        flow.prop(self, "use_object_collection", text="Use collection")
+        flow.prop(self, "show_dms", text="D° M' S\"")
+        flow.prop(self, "show_north", text="North offset")
+        flow.prop(self, "show_refraction", text="Refraction")
+        flow.prop(self, "show_az_el", text="Azimuth, elevation")
+        flow.prop(self, "show_dst", text="Daylight savings time")
+        flow.prop(self, "show_rise_set", text="Sunrise, sunset")
diff --git a/sun_position/sun_calc.py b/sun_position/sun_calc.py
index d9d13794..ed6107d7 100644
--- a/sun_position/sun_calc.py
+++ b/sun_position/sun_calc.py
@@ -1,25 +1,257 @@
+### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
+import bpy
+from bpy.app.handlers import persistent
 from mathutils import *
 import math
+from math import degrees, radians, pi
 import datetime
+from .geo import parse_position
+
+
+############################################################################
+#
+# SunClass is used for storing intermediate sun calculations.
+#
+############################################################################
+
+class SunClass:
+
+    class TazEl:
+        time = 0.0
+        azimuth = 0.0
+        elevation = 0.0
+
+    class CLAMP:
+        elevation = 0.0
+        azimuth = 0.0
+        az_start_sun = 0.0
+        az_start_env = 0.0
+
+    sunrise = TazEl()
+    sunset = TazEl()
+    solar_noon = TazEl()
+    rise_set_ok = False
+
+    bind = CLAMP()
+    bind_to_sun = False
 
-from . properties import *
+    latitude = 0.0
+    longitude = 0.0
+    elevation = 0.0
+    azimuth = 0.0
+
+    month = 0
+    day = 0
+    year = 0
+    day_of_year = 0
+    time = 0.0
+
+    UTC_zone = 0
+    sun_distance = 0.0
+    daylight_savings = False
+
+
+sun = SunClass()
+
+
+def sun_update(self, context):
+    update_time(context)
+    move_sun(context)
+
+def parse_coordinates(self, context):
+    error_message = "ERROR: Could not parse coordinates"
+    sun_props = context.scene.sun_pos_properties
+
+    if sun_props.co_parser:
+        parsed_co = parse_position(sun_props.co_parser)
+
+        if parsed_co is not None and len(parsed_co) == 2:
+            sun_props.latitude, sun_props.longitude = parsed_co
+        elif sun_props.co_parser != error_message:
+            sun_props.co_parser = error_message
+
+        # Clear prop
+    if sun_props.co_parser not in {'', error_message}:
+        sun_props.co_parser = ''
+
+@persistent
+def sun_handler(scene):
+    update_time(bpy.context)
+    move_sun(bpy.context)
+
+
+############################################################################
+#
+# move_sun() will cycle through all the selected objects
+# and call set_sun_position to place them in the sky.
+#
+############################################################################
 
-Degrees = "\xb0"
 
+def move_sun(context):
+    addon_prefs = context.preferences.addons[__package__].preferences
+    sun_props = context.scene.sun_pos_properties
+
+    if sun_props.usage_mode == "HDR":
+        nt = context.scene.world.node_tree.nodes
+        env_tex = nt.get(sun_props.hdr_texture)
+
+        if sun.bind_to_sun != sun_props.bind_to_sun:
+            # bind_to_sun was just toggled
+            sun.bind_to_sun = sun_props.bind_to_sun
+            sun.bind.az_start_sun = sun_props.hdr_azimuth
+            if env_tex:
+                sun.bind.az_start_env = env_tex.texture_mapping.rotation.z
+
+        if env_tex and sun_props.bind_to_sun:
+            az = sun_props.hdr_azimuth - sun.bind.az_start_sun + sun.bind.az_start_env
+            env_tex.texture_mapping.rotation.z = az
+
+        if sun_props.sun_object:
+            sun.theta = math.pi / 2 - sun_props.hdr_elevation
+            sun.phi = -sun_props.hdr_azimuth
+
+            locX = math.sin(sun.phi) * math.sin(-sun.theta) * sun_props.sun_distance
+            locY = math.sin(sun.theta) * math.cos(sun.phi) * sun_props.sun_distance
+            locZ = math.cos(sun.theta) * sun_props.sun_distance
+            sun_props.sun_object.location = locX, locY, locZ
+            sun_props.sun_object.rotation_euler = (sun_props.hdr_elevation - pi/2,
+                                                   0, -sun_props.hdr_azimuth)
+        return
+
+    local_time = sun_props.time
+    if sun_props.longitude > 0.0:
+        zone = sun_props.UTC_zone * -1
+    else:
+        zone = sun_props.UTC_zone
+    if sun_props.daylight_savings:
+        zone -= 1
 
-def format_time(theTime, UTCzone, daylightSavings, longitude):
-    hh = str(int(theTime))
-    min = (theTime - int(theTime)) * 60
+    north_offset = degrees(sun_props.north_offset)
+
+    if addon_prefs.show_rise_set:
+        calc_sunrise_sunset(1)
+        calc_sunrise_sunset(0)
+
+    get_sun_position(local_time, sun_props.latitude, sun_props.longitude,
+            north_offset, zone, sun_props.month, sun_props.day, sun_props.year,
+            sun_props.sun_distance)
+
+    if sun_props.use_sky_texture and sun_props.sky_texture:
+        sky_node = bpy.context.scene.world.node_tree.nodes.get(sun_props.sky_texture)
+        if sky_node is not None:
+            locX = math.sin(sun.phi) * math.sin(-sun.theta)
+            locY = math.sin(sun.theta) * math.cos(sun.phi)
+            locZ = math.cos(sun.theta)
+            sky_node.texture_mapping.rotation.z = 0.0
+            sky_node.sun_direction = locX, locY, locZ
+
+    # Sun object
+    if ((sun_props.use_sun_object or sun_props.usage_mode == 'HDR')
+            and sun_props.sun_object
+            and sun_props.sun_object.name in context.view_layer.objects):
+        obj = sun_props.sun_object
+        set_sun_position(obj, sun_props.sun_distance)
+        if obj.type == 'LIGHT':
+            obj.rotation_euler = (
+                (math.radians(sun.elevation - 90), 0,
+                 math.radians(-sun.az_north)))
+
+    # Sun collection
+    if (addon_prefs.use_object_collection
+            and sun_props.use_object_collection
+            and sun_props.object_collection):
+        sun_objects = sun_props.object_collection.objects
+        object_count = len(sun_objects)
+        if sun_props.object_collection_type == 'ECLIPTIC':
+            # Ecliptic
+            if object_count > 1:
+                time_increment = sun_props.time_spread / (object_count - 1)
+                local_time = local_time + time_increment * (object_count - 1)
+            else:
+                time_increment = sun_props.time_spread
+
+            for obj in sun_objects:
+                get_sun_position(local_time, sun_props.latitude,
+                                 sun_props.longitude, north_offset, zone,
+                                 sun_props.month, sun_props.day,
+                                 sun_props.year, sun_props.sun_distance)
+                set_sun_position(obj, sun_props.sun_distance)
+                local_time -= time_increment
+                obj.rotation_euler = (
+                    (math.radians(sun.elevation - 90), 0,
+                     math.radians(-sun.az_north)))
+        else:
+            # Analemma
+            day_increment = 365 / object_count
+            day = sun_props.day_of_year + day_increment * (object_count - 1)
+            for obj in sun_objects:
+                dt = (datetime.date(sun_props.year, 1, 1) +
+                      datetime.timedelta(day - 1))
+                get_sun_position(local_time, sun_props.latitude,
+                                 sun_props.longitude, north_offset, zone,
+                                 dt.month, dt.day, sun_props.year,
+                                 sun_props.sun_distance)
+                set_sun_position(obj, sun_props.sun_distance)
+                day -= day_increment
+                obj.rotation_euler = (
+                    (math.radians(sun.elevation - 90), 0,
+                     math.radians(-sun.az_north)))
+
+def update_time(context):
+    addon_prefs = context.preferences.addons[__package__].preferences
+    sun_props = context.scene.sun_pos_properties
+
+    if not sun_props.use_day_of_year:
+        dt = datetime.date(sun_props.year, sun_props.month, sun_props.day)
+        day_of_year = dt.timetuple().tm_yday
+        if sun_props.day_of_year != day_of_year:
+            sun_props.day_of_year = day_of_year
+        sun.day = sun_props.day
+        sun.month = sun_props.month
+        sun.day_of_year = day_of_year
+    else:
+        dt = (datetime.date(sun_props.year, 1, 1) +
+              datetime.timedelta(sun_props.day_of_year - 1))
+        sun.day = dt.day
+        sun.month = dt.month
+        sun.day_of_year = sun_props.day_of_year
+        if sun_props.day != dt.day:
+            sun_props.day = dt.day
+        if sun_props.month != dt.month:
+            sun_props.month = dt.month
+
+
+def format_time(the_time, UTC_zone, daylight_savings, longitude):
+    hh = str(int(the_time))
+    min = (the_time - int(the_time)) * 60
     sec = int((min - int(min)) * 60)
     mm = "0" + str(int(min)) if min < 10 else str(int(min))
     ss = "0" + str(sec) if sec < 10 else str(sec)
 
-    zone = UTCzone
+    zone = UTC_zone
     if(longitude < 0):
         zone *= -1
-    if daylightSavings:
+    if daylight_savings:
         zone += 1
-    gt = int(theTime) - zone
+    gt = int(the_time) - zone
 
     if gt < 0:
         gt = 24 + gt
@@ -31,9 +263,9 @@ def format_time(theTime, UTCzone, daylightSavings, longitude):
             "UTC: " + gt + ":" + mm + ":" + ss)
 
 
-def format_hms(theTime):
-    hh = str(int(theTime))
-    min = (theTime - int(theTime)) * 60
+def format_hms(the_time):
+    hh = str(int(the_time))
+    min = (the_time - int(the_time)) * 60
     sec = int((min - int(min)) * 60)
     mm = "0" + str(int(min)) if min < 10 else str(int(min))
     ss = "0" + str(sec) if sec < 10 else str(sec)
@@ -41,162 +273,24 @@ def format_hms(theTime):
     return (hh + ":" + mm + ":" + ss)
 
 
-def format_lat_long(latLong, isLatitude):
-    hh = str(abs(int(latLong)))
-    min = abs((latLong - int(latLong)) * 60)
+def format_lat_long(lat_long, is_latitude):
+    hh = str(abs(int(lat_long)))
+    min = abs((lat_long - int(lat_long)) * 60)
     sec = abs(int((min - int(min)) * 60))
     mm = "0" + str(int(min)) if min < 10 else str(int(min))
     ss = "0" + str(sec) if sec < 10 else str(sec)
-    if latLong == 0:
-        coordTag = " "
+    if lat_long == 0:
+        coord_tag = " "
     else:
-        if isLatitude:
-            coordTag = " N" if latLong > 0 else " S"
+        if is_latitude:
+            coord_tag = " N" if lat_long > 0 else " S"
         else:
-            coordTag = " E" if latLong > 0 else " W"
-
-    return hh + Degrees + " " + mm + "' " + ss + '"' + coordTag
+            coord_tag = " E" if lat_long > 0 else " W"
 
-############################################################################
-#
-# PlaceSun() will cycle through all the selected objects of type LIGHT or
-# MESH and call setSunPosition to place them in the sky.
-#
-############################################################################
+    return hh + "° " + mm + "' " + ss + '"' + coord_tag
 
 
-def Move_sun():
-    if Sun.PP.UsageMode == "HDR":
-        Sun.Theta = math.pi / 2 - degToRad(Sun.Elevation)
-        Sun.Phi = -Sun.Azimuth
-
-        locX = math.sin(Sun.Phi) * math.sin(-Sun.Theta) * Sun.SunDistance
-        locY = math.sin(Sun.Theta) * math.cos(Sun.Phi) * Sun.SunDistance
-        locZ = math.cos(Sun.Theta) * Sun.SunDistance
-
-        try:
-            nt = bpy.context.scene.world.node_tree.nodes
-            envTex = nt.get(Sun.HDR_texture)
-            if Sun.Bind.azDiff and envTex.texture_mapping.rotation.z == 0.0:
-                envTex.texture_mapping.rotation.z = Sun.Bind.azDiff
-
-            if envTex and Sun.BindToSun:
-                az = Sun.Azimuth
-                if Sun.Bind.azStart < az:
-                    taz = az - Sun.Bind.azStart
-                else:
-                    taz = -(Sun.Bind.azStart - az)
-                envTex.texture_mapping.rotation.z += taz
-                Sun.Bind.azStart = az
-
-            string = Sun.SunObject
-            newString = string[3:1000]
-            obj = bpy.context.view_layer.objects.get(newString)
-
-            try:
-                obj.location = locX, locY, locZ
-            except:
-                pass
-
-            if obj.type == 'LIGHT':
-                obj.rotation_euler = (
-                    (math.radians(Sun.Elevation - 90), 0, -Sun.Azimuth))
-        except:
-            pass
-        return True
 
-    totalObjects = len(Sun.Selected_objects)
-
-    localTime = Sun.Time
-    if Sun.Longitude > 0.0:
-        zone = Sun.UTCzone * -1
-    else:
-        zone = Sun.UTCzone
-    if Sun.DaylightSavings:
-        zone -= 1
-
-    northOffset = radToDeg(Sun.NorthOffset)
-
-    if Sun.ShowRiseSet:
-        calcSunrise_Sunset(1)
-        calcSunrise_Sunset(0)
-
-    getSunPosition(None, localTime, Sun.Latitude, Sun.Longitude,
-            northOffset, zone, Sun.Month, Sun.Day, Sun.Year,
-            Sun.SunDistance)
-
-    if Sun.UseSkyTexture:
-        try:
-            nt = bpy.context.scene.world.node_tree.nodes
-            sunTex = nt.get(Sun.SkyTexture)
-            if sunTex:
-                locX = math.sin(Sun.Phi) * math.sin(-Sun.Theta)
-                locY = math.sin(Sun.Theta) * math.cos(Sun.Phi)
-                locZ = math.cos(Sun.Theta)
-                sunTex.texture_mapping.rotation.z = 0.0
-                sunTex.sun_direction = locX, locY, locZ
-
-        except:
-            pass
-
-    if Sun.UseSunObject:
-        try:
-            obj = bpy.context.view_layer.objects.get(Sun.SunObject)
-            setSunPosition(obj, Sun.SunDistance)
-            if obj.type == 'LIGHT':
-                obj.rotation_euler = (
-                    (math.radians(Sun.Elevation - 90), 0,
-                    math.radians(-Sun.AzNorth)))
-        except:
-            pass
-
-    if totalObjects < 1 or not Sun.UseObjectGroup:
-        return False
-
-    if Sun.ObjectGroup == 'ECLIPTIC':
-        # Ecliptic
-        if totalObjects > 1:
-            timeIncrement = Sun.TimeSpread / (totalObjects - 1)
-            localTime = localTime + timeIncrement * (totalObjects - 1)
-        else:
-            timeIncrement = Sun.TimeSpread
-
-        for obj in Sun.Selected_objects:
-            mesh = obj.type
-            if mesh == 'LIGHT' or mesh == 'MESH':
-                getSunPosition(obj,
-                        localTime,
-                        Sun.Latitude, Sun.Longitude,
-                        northOffset, zone,
-                        Sun.Month, Sun.Day, Sun.Year,
-                        Sun.SunDistance)
-                setSunPosition(obj, Sun.SunDistance)
-                localTime = localTime - timeIncrement
-                if mesh == 'LIGHT':
-                    obj.rotation_euler = (
-                        (math.radians(Sun.Elevation - 90), 0,
-                        math.radians(-Sun.AzNorth)))
-    else:
-        # Analemma
-        dayIncrement = 365 / totalObjects
-        day = Sun.Day_of_year + dayIncrement * (totalObjects - 1)
-        for obj in Sun.Selected_objects:
-            mesh = obj.type
-            if mesh == 'LIGHT' or mesh == 'MESH':
-                dt = (datetime.date(Sun.Year, 1, 1) +
-                      datetime.timedelta(day - 1))
-                getSunPosition(obj, localTime,
-                        Sun.Latitude, Sun.Longitude,
-                        northOffset, zone, dt.month, dt.day,
-                        Sun.Year, Sun.SunDistance)
-                setSunPosition(obj, Sun.SunDistance)
-                day -= dayIncrement
-                if mesh == 'LIGHT':
-                    obj.rotation_euler = (
-                        (math.radians(Sun.Elevation - 90), 0,
-                        math.radians(-Sun.AzNorth)))
-
-    return True
 
 ############################################################################
 #
@@ -216,36 +310,39 @@ def Move_sun():
 ############################################################################
 
 
-def getSunPosition(obj, localTime, latitude, longitude, northOffset,
-                   utcZone, month, day, year, distance):
+def get_sun_position(local_time, latitude, longitude, north_offset,
+                   utc_zone, month, day, year, distance):
+
+    addon_prefs = bpy.context.preferences.addons[__package__].preferences
+    sun_props = bpy.context.scene.sun_pos_properties
 
     longitude *= -1                 # for internal calculations
-    utcTime = localTime + utcZone   # Set Greenwich Meridian Time
+    utc_time = local_time + utc_zone   # Set Greenwich Meridian Time
 
     if latitude > 89.93:            # Latitude 90 and -90 gives
-        latitude = degToRad(89.93)  # erroneous results so nudge it
+        latitude = radians(89.93)  # erroneous results so nudge it
     elif latitude < -89.93:
-        latitude = degToRad(-89.93)
+        latitude = radians(-89.93)
     else:
-        latitude = degToRad(latitude)
+        latitude = radians(latitude)
 
-    t = julianTimeFromY2k(utcTime, year, month, day)
+    t = julian_time_from_y2k(utc_time, year, month, day)
 
-    e = degToRad(obliquityCorrection(t))
-    L = apparentLongitudeOfSun(t)
-    solarDec = sunDeclination(e, L)
-    eqtime = calcEquationOfTime(t)
+    e = radians(obliquity_correction(t))
+    L = apparent_longitude_of_sun(t)
+    solar_dec = sun_declination(e, L)
+    eqtime = calc_equation_of_time(t)
 
-    timeCorrection = (eqtime - 4 * longitude) + 60 * utcZone
-    trueSolarTime = ((utcTime - utcZone) * 60.0 + timeCorrection) % 1440
+    time_correction = (eqtime - 4 * longitude) + 60 * utc_zone
+    true_solar_time = ((utc_time - utc_zone) * 60.0 + time_correction) % 1440
 
-    hourAngle = trueSolarTime / 4.0 - 180.0
-    if hourAngle < -180.0:
-        hourAngle += 360.0
+    hour_angle = true_solar_time / 4.0 - 180.0
+    if hour_angle < -180.0:
+        hour_angle += 360.0
 
-    csz = (math.sin(latitude) * math.sin(solarDec) +
-           math.cos(latitude) * math.cos(solarDec) *
-           math.cos(degToRad(hourAngle)))
+    csz = (math.sin(latitude) * math.sin(solar_dec) +
+           math.cos(latitude) * math.cos(solar_dec) *
+           math.cos(radians(hour_angle)))
     if csz > 1.0:
         csz = 1.0
     elif csz < -1.0:
@@ -253,15 +350,15 @@ def getSunPosition(obj, localTime, latitude, longitude, northOffset,
 
     zenith = math.acos(csz)
 
-    azDenom = math.cos(latitude) * math.sin(zenith)
+    az_denom = math.cos(latitude) * math.sin(zenith)
 
-    if abs(azDenom) > 0.001:
-        azRad = ((math.sin(latitude) *
-                  math.cos(zenith)) - math.sin(solarDec)) / azDenom
-        if abs(azRad) > 1.0:
-            azRad = -1.0 if (azRad < 0.0) else 1.0
-        azimuth = 180.0 - radToDeg(math.acos(azRad))
-        if hourAngle > 0.0:
+    if abs(az_denom) > 0.001:
+        az_rad = ((math.sin(latitude) *
+                  math.cos(zenith)) - math.sin(solar_dec)) / az_denom
+        if abs(az_rad) > 1.0:
+            az_rad = -1.0 if (az_rad < 0.0) else 1.0
+        azimuth = 180.0 - degrees(math.acos(az_rad))
+        if hour_angle > 0.0:
             azimuth = -azimuth
     else:
         azimuth = 180.0 if (latitude > 0.0) else 0.0
@@ -269,79 +366,77 @@ def getSunPosition(obj, localTime, latitude, longitude, northOffset,
     if azimuth < 0.0:
         azimuth = azimuth + 360.0
 
-    exoatmElevation = 90.0 - radToDeg(zenith)
+    exoatm_elevation = 90.0 - degrees(zenith)
 
-    if exoatmElevation > 85.0:
-        refractionCorrection = 0.0
-    else:
-        te = math.tan(degToRad(exoatmElevation))
-        if exoatmElevation > 5.0:
-            refractionCorrection = (
-                58.1 / te - 0.07 / (te ** 3) + 0.000086 / (te ** 5))
-        elif (exoatmElevation > -0.575):
-            s1 = (-12.79 + exoatmElevation * 0.711)
-            s2 = (103.4 + exoatmElevation * (s1))
-            s3 = (-518.2 + exoatmElevation * (s2))
-            refractionCorrection = 1735.0 + exoatmElevation * (s3)
+    if addon_prefs.show_refraction and sun_props.show_refraction:
+        if exoatm_elevation > 85.0:
+            refraction_correction = 0.0
         else:
-            refractionCorrection = -20.774 / te
-
-        refractionCorrection = refractionCorrection / 3600
+            te = math.tan(radians(exoatm_elevation))
+            if exoatm_elevation > 5.0:
+                refraction_correction = (
+                    58.1 / te - 0.07 / (te ** 3) + 0.000086 / (te ** 5))
+            elif (exoatm_elevation > -0.575):
+                s1 = (-12.79 + exoatm_elevation * 0.711)
+                s2 = (103.4 + exoatm_elevation * (s1))
+                s3 = (-518.2 + exoatm_elevation * (s2))
+                refraction_correction = 1735.0 + exoatm_elevation * (s3)
+            else:
+                refraction_correction = -20.774 / te
+
+        refraction_correction = refraction_correction / 3600
+        solar_elevation = 90.0 - (degrees(zenith) - refraction_correction)
 
-    if Sun.ShowRefraction:
-        solarElevation = 90.0 - (radToDeg(zenith) - refractionCorrection)
     else:
-        solarElevation = 90.0 - radToDeg(zenith)
+        solar_elevation = 90.0 - degrees(zenith)
 
-    solarAzimuth = azimuth + northOffset
+    solar_azimuth = azimuth
+    if addon_prefs.show_north:
+        solar_azimuth += north_offset
 
-    Sun.AzNorth = solarAzimuth
+    sun.az_north = solar_azimuth
 
-    Sun.Theta = math.pi / 2 - degToRad(solarElevation)
-    Sun.Phi = degToRad(solarAzimuth) * -1
-    Sun.Azimuth = azimuth
-    Sun.Elevation = solarElevation
+    sun.theta = math.pi / 2 - radians(solar_elevation)
+    sun.phi = radians(solar_azimuth) * -1
+    sun.azimuth = azimuth
+    sun.elevation = solar_elevation
 
 
-def setSunPosition(obj, distance):
-
-    locX = math.sin(Sun.Phi) * math.sin(-Sun.Theta) * distance
-    locY = math.sin(Sun.Theta) * math.cos(Sun.Phi) * distance
-    locZ = math.cos(Sun.Theta) * distance
+def set_sun_position(obj, distance):
+    locX = math.sin(sun.phi) * math.sin(-sun.theta) * distance
+    locY = math.sin(sun.theta) * math.cos(sun.phi) * distance
+    locZ = math.cos(sun.theta) * distance
 
     #----------------------------------------------
     # Update selected object in viewport
     #----------------------------------------------
-    try:
-        obj.location = locX, locY, locZ
-    except:
-        pass
+    obj.location = locX, locY, locZ
 
 
-def calcSunriseSetUTC(rise, jd, latitude, longitude):
-    t = calcTimeJulianCent(jd)
-    eqTime = calcEquationOfTime(t)
-    solarDec = calcSunDeclination(t)
-    hourAngle = calcHourAngleSunrise(latitude, solarDec)
+def calc_sunrise_set_UTC(rise, jd, latitude, longitude):
+    t = calc_time_julian_cent(jd)
+    eq_time = calc_equation_of_time(t)
+    solar_dec = calc_sun_declination(t)
+    hour_angle = calc_hour_angle_sunrise(latitude, solar_dec)
     if not rise:
-        hourAngle = -hourAngle
-    delta = longitude + radToDeg(hourAngle)
-    timeUTC = 720 - (4.0 * delta) - eqTime
-    return timeUTC
+        hour_angle = -hour_angle
+    delta = longitude + degrees(hour_angle)
+    time_UTC = 720 - (4.0 * delta) - eq_time
+    return time_UTC
 
 
-def calcSunDeclination(t):
-    e = degToRad(obliquityCorrection(t))
-    L = apparentLongitudeOfSun(t)
-    solarDec = sunDeclination(e, L)
-    return solarDec
+def calc_sun_declination(t):
+    e = radians(obliquity_correction(t))
+    L = apparent_longitude_of_sun(t)
+    solar_dec = sun_declination(e, L)
+    return solar_dec
 
 
-def calcHourAngleSunrise(lat, solarDec):
-    latRad = degToRad(lat)
-    HAarg = (math.cos(degToRad(90.833)) /
-             (math.cos(latRad) * math.cos(solarDec))
-             - math.tan(latRad) * math.tan(solarDec))
+def calc_hour_angle_sunrise(lat, solar_dec):
+    lat_rad = radians(lat)
+    HAarg = (math.cos(radians(90.833)) /
+            (math.cos(lat_rad) * math.cos(solar_dec))
+            - math.tan(lat_rad) * math.tan(solar_dec))
     if HAarg < -1.0:
         HAarg = -1.0
     elif HAarg > 1.0:
@@ -350,53 +445,53 @@ def calcHourAngleSunrise(lat, solarDec):
     return HA
 
 
-def calcSolarNoon(jd, longitude, timezone, dst):
-    t = calcTimeJulianCent(jd - longitude / 360.0)
-    eqTime = calcEquationOfTime(t)
-    noonOffset = 720.0 - (longitude * 4.0) - eqTime
-    newt = calcTimeJulianCent(jd + noonOffset / 1440.0)
-    eqTime = calcEquationOfTime(newt)
+def calc_solar_noon(jd, longitude, timezone, dst):
+    t = calc_time_julian_cent(jd - longitude / 360.0)
+    eq_time = calc_equation_of_time(t)
+    noon_offset = 720.0 - (longitude * 4.0) - eq_time
+    newt = calc_time_julian_cent(jd + noon_offset / 1440.0)
+    eq_time = calc_equation_of_time(newt)
 
     nv = 780.0 if dst else 720.0
-    noonLocal = (nv - (longitude * 4.0) - eqTime + (timezone * 60.0)) % 1440
-    Sun.SolarNoon.time = noonLocal / 60.0
+    noon_local = (nv- (longitude * 4.0) - eq_time + (timezone * 60.0)) % 1440
+    sun.solar_noon.time = noon_local / 60.0
 
 
-def calcSunrise_Sunset(rise):
-    if Sun.Longitude > 0:
-        zone = Sun.UTCzone * -1
+def calc_sunrise_sunset(rise):
+    if sun.longitude > 0:
+        zone = sun.UTC_zone * -1
     else:
-        zone = Sun.UTCzone
-
-    jd = getJulianDay(Sun.Year, Sun.Month, Sun.Day)
-    timeUTC = calcSunriseSetUTC(rise, jd, Sun.Latitude, Sun.Longitude)
-    newTimeUTC = calcSunriseSetUTC(rise, jd + timeUTC / 1440.0,
-                     Sun.Latitude, Sun.Longitude)
-    timeLocal = newTimeUTC + (-zone * 60.0)
-    tl = timeLocal / 60.0
-    getSunPosition(None, tl, Sun.Latitude, Sun.Longitude, 0.0,
-            zone, Sun.Month, Sun.Day, Sun.Year,
-            Sun.SunDistance)
-    if Sun.DaylightSavings:
-        timeLocal += 60.0
-        tl = timeLocal / 60.0
+        zone = sun.UTC_zone
+
+    jd = get_julian_day(sun.year, sun.month, sun.day)
+    time_UTC = calc_sunrise_set_UTC(rise, jd, sun.latitude, sun.longitude)
+    new_time_UTC = calc_sunrise_set_UTC(rise, jd + time_UTC / 1440.0,
+                     sun.latitude, sun.longitude)
+    time_local = new_time_UTC + (-zone * 60.0)
+    tl = time_local / 60.0
+    get_sun_position(tl, sun.latitude, sun.longitude, 0.0,
+            zone, sun.month, sun.day, sun.year,
+            sun.sun_distance)
+    if sun.daylight_savings:
+        time_local += 60.0
+        tl = time_local / 60.0
     if tl < 0.0:
         tl += 24.0
     elif tl > 24.0:
         tl -= 24.0
     if rise:
-        Sun.Sunrise.time = tl
-        Sun.Sunrise.azimuth = Sun.Azimuth
-        Sun.Sunrise.elevation = Sun.Elevation
-        calcSolarNoon(jd, Sun.Longitude, -zone, Sun.DaylightSavings)
-        getSunPosition(None, Sun.SolarNoon.time, Sun.Latitude, Sun.Longitude,
-                    0.0, zone, Sun.Month, Sun.Day, Sun.Year,
-                    Sun.SunDistance)
-        Sun.SolarNoon.elevation = Sun.Elevation
+        sun.sunrise.time = tl
+        sun.sunrise.azimuth = sun.azimuth
+        sun.sunrise.elevation = sun.elevation
+        calc_solar_noon(jd, sun.longitude, -zone, sun.daylight_savings)
+        get_sun_position(sun.solar_noon.time, sun.latitude, sun.longitude,
+            0.0, zone, sun.month, sun.day, sun.year,
+            sun.sun_distance)
+        sun.solar_noon.elevation = sun.elevation
     else:
-        Sun.Sunset.time = tl
-        Sun.Sunset.azimuth = Sun.Azimuth
-        Sun.Sunset.elevation = Sun.Elevation
+        sun.sunset.time = tl
+        sun.sunset.azimuth = sun.azimuth
+        sun.sunset.elevation = sun.elevation
 
 ##########################################################################
 ## Get the elapsed julian time since 1/1/2000 12:00 gmt
@@ -404,14 +499,14 @@ def calcSunrise_Sunset(rise):
 ##########################################################################
 
 
-def julianTimeFromY2k(utcTime, year, month, day):
+def julian_time_from_y2k(utc_time, year, month, day):
     century = 36525.0  # Days in Julian Century
     epoch = 2451545.0  # Julian Day for 1/1/2000 12:00 gmt
-    jd = getJulianDay(year, month, day)
-    return ((jd + (utcTime / 24)) - epoch) / century
+    jd = get_julian_day(year, month, day)
+    return ((jd + (utc_time / 24)) - epoch) / century
 
 
-def getJulianDay(year, month, day):
+def get_julian_day(year, month, day):
     if month <= 2:
         year -= 1
         month += 12
@@ -422,21 +517,21 @@ def getJulianDay(year, month, day):
     return jd
 
 
-def calcTimeJulianCent(jd):
+def calc_time_julian_cent(jd):
     t = (jd - 2451545.0) / 36525.0
     return t
 
 
-def sunDeclination(e, L):
+def sun_declination(e, L):
     return (math.asin(math.sin(e) * math.sin(L)))
 
 
-def calcEquationOfTime(t):
-    epsilon = obliquityCorrection(t)
-    ml = degToRad(meanLongitudeSun(t))
-    e = eccentricityEarthOrbit(t)
-    m = degToRad(meanAnomalySun(t))
-    y = math.tan(degToRad(epsilon) / 2.0)
+def calc_equation_of_time(t):
+    epsilon = obliquity_correction(t)
+    ml = radians(mean_longitude_sun(t))
+    e = eccentricity_earth_orbit(t)
+    m = radians(mean_anomaly_sun(t))
+    y = math.tan(radians(epsilon) / 2.0)
     y = y * y
     sin2ml = math.sin(2.0 * ml)
     cos2ml = math.cos(2.0 * ml)
@@ -445,59 +540,51 @@ def calcEquationOfTime(t):
     sin2m = math.sin(2.0 * m)
     etime = (y * sin2ml - 2.0 * e * sinm + 4.0 * e * y *
              sinm * cos2ml - 0.5 * y ** 2 * sin4ml - 1.25 * e ** 2 * sin2m)
-    return (radToDeg(etime) * 4)
+    return (degrees(etime) * 4)
 
 
-def obliquityCorrection(t):
-    ec = obliquityOfEcliptic(t)
+def obliquity_correction(t):
+    ec = obliquity_of_ecliptic(t)
     omega = 125.04 - 1934.136 * t
-    return (ec + 0.00256 * math.cos(degToRad(omega)))
+    return (ec + 0.00256 * math.cos(radians(omega)))
 
 
-def obliquityOfEcliptic(t):
+def obliquity_of_ecliptic(t):
     return ((23.0 + 26.0 / 60 + (21.4480 - 46.8150) / 3600 * t -
             (0.00059 / 3600) * t ** 2 + (0.001813 / 3600) * t ** 3))
 
 
-def trueLongitudeOfSun(t):
-    return (meanLongitudeSun(t) + equationOfSunCenter(t))
+def true_longitude_of_sun(t):
+    return (mean_longitude_sun(t) + equation_of_sun_center(t))
 
 
-def calcSunApparentLong(t):
-    o = trueLongitudeOfSun(t)
+def calc_sun_apparent_long(t):
+    o = true_longitude_of_sun(t)
     omega = 125.04 - 1934.136 * t
-    lamb = o - 0.00569 - 0.00478 * math.sin(degToRad(omega))
+    lamb = o - 0.00569 - 0.00478 * math.sin(radians(omega))
     return lamb
 
 
-def apparentLongitudeOfSun(t):
-    return (degToRad(trueLongitudeOfSun(t) - 0.00569 - 0.00478 *
-            math.sin(degToRad(125.04 - 1934.136 * t))))
+def apparent_longitude_of_sun(t):
+    return (radians(true_longitude_of_sun(t) - 0.00569 - 0.00478 *
+            math.sin(radians(125.04 - 1934.136 * t))))
 
 
-def meanLongitudeSun(t):
+def mean_longitude_sun(t):
     return (280.46646 + 36000.76983 * t + 0.0003032 * t ** 2) % 360
 
 
-def equationOfSunCenter(t):
-    m = degToRad(meanAnomalySun(t))
+def equation_of_sun_center(t):
+    m = radians(mean_anomaly_sun(t))
     c = ((1.914602 - 0.004817 * t - 0.000014 * t ** 2) * math.sin(m) +
         (0.019993 - 0.000101 * t) * math.sin(m * 2) +
          0.000289 * math.sin(m * 3))
     return c
 
 
-def meanAnomalySun(t):
+def mean_anomaly_sun(t):
     return (357.52911 + t * (35999.05029 - 0.0001537 * t))
 
 
-def eccentricityEarthOrbit(t):
+def eccentricity_earth_orbit(t):
     return (0.016708634 - 0.000042037 * t - 0.0000001267 * t ** 2)
-
-
-def degToRad(angleDeg):
-    return (math.pi * angleDeg / 180.0)
-
-
-def radToDeg(angleRad):
-    return (180.0 * angleRad / math.pi)
diff --git a/sun_position/ui_sun.py b/sun_position/ui_sun.py
index d83a3d93..a923ce9e 100644
--- a/sun_position/ui_sun.py
+++ b/sun_position/ui_sun.py
@@ -1,679 +1,347 @@
+### BEGIN GPL LICENSE BLOCK #####
+#
+#  This program is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU General Public License
+#  as published by the Free Software Foundation; either version 2
+#  of the License, or (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software Foundation,
+#  Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+#
+# ##### END GPL LICENSE BLOCK #####
+
 import bpy
+from bpy.types import Operator, Menu
+from bl_operators.presets import AddPresetBase
+import os
 import datetime
+from math import radians
+
+from .sun_calc import (format_lat_long, format_time, format_hms,
+                        move_sun, sun)
+
+
+# -------------------------------------------------------------------
+# Choice list of places, month and day at 12:00 noon
+# -------------------------------------------------------------------
+
+
+class SUNPOS_MT_Presets(Menu):
+    bl_label = "Sun Position Presets"
+    preset_subdir = "operator/sun_position"
+    preset_operator = "script.execute_preset"
+    draw = Menu.draw_preset
+
+
+class SUNPOS_OT_AddPreset(AddPresetBase, Operator):
+    '''Add Sun Position preset'''
+    bl_idname = "world.sunpos_add_preset"
+    bl_label = "Add Sun Position preset"
+    preset_menu = "SUNPOS_MT_Presets"
+
+    # variable used for all preset values
+    preset_defines = [
+        "sun_props = bpy.context.scene.sun_pos_properties"
+    ]
+
+    # properties to store in the preset
+    preset_values = [
+        "sun_props.day",
+        "sun_props.month",
+        "sun_props.time",
+        "sun_props.year",
+        "sun_props.UTC_zone",
+        "sun_props.daylight_savings",
+        "sun_props.latitude",
+        "sun_props.longitude",
+    ]
+
+    # where to store the preset
+    preset_subdir = "operator/sun_position"
 
-from . properties import *
-from . operators import *
-from . sun_calc import Degrees, format_lat_long, degToRad, \
-                       format_time, format_hms, Move_sun
 
-#---------------------------------------------------------------------------
+class SUNPOS_OT_DefaultPresets(Operator):
+    '''Copy Sun Position default presets'''
+    bl_idname = "world.sunpos_default_presets"
+    bl_label = "Copy Sun Position default presets"
+
+    def execute(self, context):
+        preset_dirpath = bpy.utils.user_resource('SCRIPTS', path="presets/operator/sun_position", create=True)
+        # Why these in particular?
+        presets = {"north_pole.py": [6, 21, 12.0, 0, 90.000, 0.0000, False],
+                   "equator_vernal_equinox.py":   [3, 20, 12.0, 0, 0.0000, 0.0000, False],
+                   "rio_de_janeiro_may_10th.py":  [5, 10, 12.0, 3, -22.9002, -43.2334, False],
+                   "tokyo_august_20th.py":        [8, 20, 12.0, 9, 35.7002, 139.7669, False],
+                   "boston_autumnal_equinox.py":  [9, 22, 12.0, 5, 42.3502, -71.0500, True],
+                   "boston_vernal_equinox.py":    [3, 20, 12.0, 5, 42.3502, -71.0500, True],
+                   "honolulu_winter_solstice.py": [12, 21, 12.0, 10, 21.3001, -157.850, False],
+                   "honolulu_summer_solstice.py": [6, 21, 12.0, 10, 21.3001, -157.850, False]}
+
+        script = '''import bpy
+sun_props = bpy.context.scene.sun_pos_properties
+
+sun_props.month = {:d}
+sun_props.day = {:d}
+sun_props.time = {:f}
+sun_props.UTC_zone = {:d}
+sun_props.latitude = {:f}
+sun_props.longitude = {:f}
+sun_props.daylight_savings = {}
+'''
+
+        for path, p in presets.items():
+            print(p)
+            with open(os.path.join(preset_dirpath, path), 'w') as f:
+                f.write(script.format(*p))
+
+        return {'FINISHED'}
+
+# -------------------------------------------------------------------
 #
 #   Draw the Sun Panel, sliders, et. al.
 #
-#---------------------------------------------------------------------------
-
-class SPOS_PT_Panel(bpy.types.Panel):
+# -------------------------------------------------------------------
 
-    #bl_idname = "panel.SunPos_world"
+class SUNPOS_PT_Panel(bpy.types.Panel):
+    bl_idname = "SUNPOS_PT_world"
     bl_space_type = "PROPERTIES"
     bl_region_type = "WINDOW"
     bl_context = "world"
     bl_label = "Sun Position"
     bl_options = {'DEFAULT_CLOSED'}
 
-    @classmethod
-    def poll(cls, context):
-        if context.area.type == 'PROPERTIES':
-            return 1
-        return 0
-
-    def enable(self, layout):
-        row = layout.row()
-        split = row.split(factor=.90)
-        colL = split.column()
-        colL.alignment = 'LEFT'
-        colR = split.column()
-        colR.alignment = 'RIGHT'
-        colL.operator('world.sunpos_controller', text='Enable', icon='PLAY')
-        colR.operator('world.sunpos_preferences', text='', icon='PREFERENCES')
-        Map.init_zoom_preference = True
-
-    def disable(self, context, layout):
-        p = context.scene.SunPos_pref_property
-        if Map.init_zoom_preference:
-            Map.zoom_preferences(bpy.context.preferences.inputs.invert_zoom_wheel,
-                             bpy.context.preferences.inputs.invert_mouse_zoom)
-            Hdr.zoom_preferences(bpy.context.preferences.inputs.invert_zoom_wheel,
-                             bpy.context.preferences.inputs.invert_mouse_zoom)
-        row = self.layout.row()
-        if p.UseOneColumn:
-            col1 = row.column()
-            col2 = col1
-        elif p.UseTimePlace:
-            split = row.split(factor=.3)
-            col1 = split.column()
-            col2 = split.column()
-        else:
-            col1 = row.column()
-
-        col1.operator('world.sunpos_controller', text='Disable', icon='X')
-        if p.UseTimePlace:
-            col2.operator_menu_enum('world.pdp_operator',
-                'timePlacePresets', text=Sun.PlaceLabel)
-
-    def show_preferences(self, context, layout):
-        p = context.scene.SunPos_pref_property
-        box = self.layout.box()
-        row = box.row(align=True)
-        row.alignment = 'CENTER'
-        row.label(text="Preferences")
-        col = box.column(align=True)
-        col.label(text="Usage mode:")
-        col.props_enum(p, "UsageMode")
-        col.separator()
-
-        if p.UsageMode == "NORMAL":
-            cb = col.box()
-            tr = cb.row()
-            rr = tr.row(align=True)
-            cs = rr.split()
-            cl = cs.column()
-            cr = cs.column()
-
-            cl.label(text="World map options:")
-            cl.operator_menu_enum('world.wmp_operator',
-                              'mapPresets', text=Sun.MapName)
-            cr.label(text="Display map in:")
-            cr.props_enum(p, "MapLocation")
-            col.separator()
-            col.label(text="Show or use:")
-            col.alignment = 'LEFT'
-            col.prop(p, "UseOneColumn", text="Single column mode")
-            col.prop(p, "UseTimePlace", text="Time/place presets")
-            col.prop(p, "UseObjectGroup", text="Object group")
-            col.prop(p, "ShowDMS", text="D\xb0 M' S\"")
-            col.prop(p, "ShowNorth", text="North offset")
-            col.prop(p, "ShowRefraction", text="Refraction")
-            col.prop(p, "ShowAzEl", text="Azimuth, elevation")
-            col.prop(p, "ShowDST", text="Daylight savings time")
-            col.prop(p, "ShowRiseSet", text="Sunrise, sunset")
-            col.separator()
-        col.operator('world.sunpos_pref_done', text='Done', icon='QUIT')
-        Sun.ShowRiseSet = p.ShowRiseSet
-
     def draw(self, context):
-        sp = context.scene.SunPos_property
-        p = context.scene.SunPos_pref_property
+        sp = context.scene.sun_pos_properties
+        p = context.preferences.addons[__package__].preferences
         layout = self.layout
-        if Display.PREFERENCES:
-            self.show_preferences(context, layout)
-        elif Display.ENABLE:
-            Sun.SP = sp
-            Sun.PP = p
-            self.enable(layout)
-        else:
-            Sun.SP = sp
-            Sun.PP = p
-            self.disable(context, layout)
-        if Display.PANEL:
-            if Sun.SP.IsActive:
-                self.draw_panel(context, sp, p, layout)
-            else:
-                Display.setAction('ENABLE')
+        self.draw_panel(context, sp, p, layout)
 
     def draw_panel(self, context, sp, p, layout):
-        if p.UsageMode == "HDR":
-            self.draw_environ_panel(context, sp, p, layout)
-        elif p.UseOneColumn:
-            self.draw_one_column(context, sp, p, layout)
+        self.layout.label(text="Usage mode:")
+        self.layout.prop(sp, "usage_mode", expand=True)
+        if sp.usage_mode == "HDR":
+            self.draw_environ_mode_panel(context, sp, p, layout)
         else:
-            self.draw_two_columns(context, sp, p, layout)
+            self.draw_normal_mode_panel(context, sp, p, layout)
 
-    def draw_environ_panel(self, context, sp, p, layout):
+    def draw_environ_mode_panel(self, context, sp, p, layout):
         box = self.layout.box()
-        toprow = box.row()
-        row = toprow.row(align=False)
-        row.alignment = 'CENTER'
-        col = row.column(align=True)
+        flow = box.grid_flow(row_major=True, columns=0, even_columns=True,
+                             even_rows=False, align=False)
         have_texture = False
 
-        try:
-            col.separator()
-            col.label(text="Use environment texture:")
-            col.prop_search(sp, "HDR_texture",
-                    context.scene.world.node_tree, "nodes", text="")
-            col.separator()
-            try:
-                nt = bpy.context.scene.world.node_tree.nodes
-                envTex = nt.get(sp.HDR_texture)
-                if envTex:
-                    if envTex.type == "TEX_ENVIRONMENT":
-                        if envTex.image != None:
-                            have_texture = True
-                    if Sun.Bind.azDiff == 0:
-                        if envTex.texture_mapping.rotation.z == 0.0:
-                            Sun.Bind.azDiff = degToRad(90.0)
-            except:
-                pass
-        except:
-            pass
-        try:
-            col.label(text="Use sun object:")
-            col.prop_search(sp, "SunObject",
-                    context.view_layer, "objects", text="")
-            #Sun.SunObject = sp.SunObject  # why removed?
-        except:
-            pass
-
-        
-        col.separator()
-        col.prop(sp, "SunDistance")
-        if not sp.BindToSun:
-            col.prop(sp, "HDR_elevation")
-        col.prop(sp, "HDR_azimuth")
+        col = flow.column()
+        col.label(text="Environment texture:")
+        col.prop_search(sp, "hdr_texture",
+                        context.scene.world.node_tree, "nodes", text="")
         col.separator()
-        toprow1 = box.row()
-        row1 = toprow1.row(align=False)
-        row1.alignment = 'CENTER'
-        if not sp.BindToSun:
-            row1.prop(sp, "BindToSun", toggle=True, icon="CONSTRAINT",
-                    text="Bind Texture to Sun ")
-        else:
-            row1.prop(sp, "BindToSun", toggle=True, icon="CONSTRAINT",
-                    text="Release binding")
-
-        toprow2 = box.row()
-        row2 = toprow2.row(align=False)
-        row2.alignment = 'CENTER'
-        row2.prop(sp, "ShowHdr", text="Sync Sun to Texture", toggle=True, icon='LIGHT_SUN')
-        if have_texture == False:
-            row2.enabled = False
-        elif sp.BindToSun:
-            row2.enabled = False
-        else:
-            row2.enabled = True
-        if have_texture == False:
-            row1.enabled = False
-        elif sp.ShowHdr:
-            row1.enabled = False
-        else:
-            row1.enabled = True
-        
 
-    def draw_one_column(self, context, sp, p, layout):
-        box = self.layout.box()
-        toprow = box.row()
-        row = toprow.row(align=False)
-        row.alignment = 'CENTER'
-
-        col = row.column(align=True)
-        col.prop(sp, "UseSkyTexture", text="Cycles sky")
-        if sp.UseSkyTexture:
-            try:
-                col.prop_search(sp, "SkyTexture",
-                        context.scene.world.node_tree, "nodes", text="")
-            except:
-                pass
-        col.prop(sp, "UseSunObject", text="Use object")
-        if(sp.UseSunObject):
-            try:
-                col.prop_search(sp, "SunObject",
+        col = flow.column()
+        col.label(text="Sun object:")
+        col.prop_search(sp, "sun_object",
                         context.view_layer, "objects", text="")
-            except:
-                pass
-
-        if p.UseObjectGroup:
-            col.prop(sp, "UseObjectGroup", text="Object group")
-            if sp.UseObjectGroup:
-                Sun.verify_ObjectGroup()
-                if len(Sun.Selected_objects) > 0:
-                    col.operator('world.sunpos_clear_objects',
-                                 'Release Group')
-                    col.separator()
-                    if(sp.ObjectGroup == 'ECLIPTIC'):
-                        col.prop(sp, "TimeSpread")
-                    col.props_enum(sp, "ObjectGroup")
-                else:
-                    col.operator('world.sunpos_set_objects',
-                                 text='Set Object Group')
-            else:
-                Sun.ObjectGroup_verified = False
-
-        row = layout.row()
-        row.prop(sp, "ShowMap", text="Show Map", toggle=True, icon='WORLD')
-
-        box = self.layout.box()
-        toprow = box.row()
-        row = toprow.row(align=True)
-        row.alignment = 'CENTER'
-        col = row.column(align=True)
-        col.alignment = 'CENTER'
-        col.prop(sp, "Latitude")
-        if p.ShowDMS:
-            col.label(text=format_lat_long(sp.Latitude, True))
-        col.prop(sp, "Longitude")
-        if p.ShowDMS:
-            col.label(text=format_lat_long(sp.Longitude, False))
-        cb = col.box()
-        tr = cb.row()
-        rr = tr.row(align=True)
-        if p.ShowNorth:
-            cs = rr.split()
-            cl = cs.column()
-            cr = cs.column()
-            cl.prop(sp, "ShowNorth", text="Show North", toggle=True)
-            cr.prop(sp, "NorthOffset")
-            col.prop(sp, "SunDistance")
-        else:
-            rr.prop(sp, "SunDistance")
-
-        if p.ShowRefraction:
-            col.prop(sp, "ShowRefraction", text="Show refraction")
-        if p.ShowAzEl:
-            col.label(text="Azimuth: " +
-                      str(round(Sun.Azimuth, 3)) + Degrees)
-            col.label(text="Elevation: " +
-                      str(round(Sun.Elevation, 3)) + Degrees)
-        box = self.layout.box()
-        toprow = box.row()
-        row = toprow.row(align=False)
-        row.alignment = 'CENTER'
-        col = row.column(align=False)
-        col.alignment = 'CENTER'
-        tr = col.row()
-        rr = tr.row(align=True)
-
-        if Sun.UseDayMonth:
-            cs = rr.split(factor=.82)
-            cl = cs.column()
-            cl.alignment = 'LEFT'
-            cr = cs.column()
-            cr.alignment = 'RIGHT'
-            cl.prop(sp, "Month")
-            cr.operator('world.sunpos_day_range',text= '',
-                          icon='SORTTIME')
-            col.prop(sp, "Day")
-        else:
-            cs = rr.split(factor=.90)
-            cl = cs.column()
-            cr = cs.column()
-            cl.alignment = 'LEFT'
-            cr.alignment = 'RIGHT'
-            cl.prop(sp, "Day_of_year")
-            cr.operator('world.sunpos_day_range', '',
-                          icon='SORTTIME')
-
-        col.prop(sp, "Year")
-        col.prop(sp, "UTCzone", slider=True)
-        col.prop(sp, "Time")
-        lt, ut = format_time(sp.Time, sp.UTCzone,
-                             sp.DaylightSavings, sp.Longitude)
-        col.label(text=lt, icon='TIME')
-        col.label(text="  " + ut, icon='PREVIEW_RANGE')
-
-        if p.ShowRiseSet:
-            if Sun.Sunrise.time == Sun.Sunset.time or \
-                    Sun.Sunrise.elevation > -0.4 or Sun.Sunset.elevation > -0.4:
-                Sun.RiseSetOK = False
-                tsr = "Sunrise: --------"
-                tss = " Sunset: --------"
-            else:
-                Sun.RiseSetOK = True
-                sr = format_hms(Sun.Sunrise.time)
-                ss = format_hms(Sun.Sunset.time)
-                tsr = "Sunrise: " + sr
-                tss = " Sunset: " + ss
-            col.label(text=tsr, icon='LIGHT_SUN')
-            col.label(text=tss, icon='SOLO_ON')
-
-        if p.ShowDST:
-            col.prop(sp, "DaylightSavings", text="Daylight Savings")
+        col.separator()
 
-    def draw_two_columns(self, context, sp, p, layout):
-        box = self.layout.box()
-        toprow = box.row()
-        row = toprow.row(align=True)
-        row.alignment = 'CENTER'
-        col = row.column(align=True)
-        split = col.split(factor=.5)
-        cL = split.column()
-        cR = split.column()
-        cL.alignment = 'LEFT'
-        cR.alignment = 'RIGHT'
-
-        cLi = cRi = 1
-        cL.prop(sp, "UseSkyTexture", text="Cycles sky")
-        if sp.UseSkyTexture:
-            try:
-                cL.prop_search(sp, "SkyTexture",
-                        context.scene.world.node_tree, "nodes", text="")
-                cLi += 1
-            except:
-                pass
-        cR.prop(sp, "UseSunObject", text="Use object")
-        if(sp.UseSunObject):
-            try:
-                cR.prop_search(sp, "SunObject",
-                    context.view_layer, "objects", text="")
-                cRi += 1
-            except:
-                pass
-
-        if p.UseObjectGroup:
-            cLi += 1
-            cL.prop(sp, "UseObjectGroup", text="Object group")
-            if sp.UseObjectGroup:
-                Sun.verify_ObjectGroup()
-                if len(Sun.Selected_objects) > 0:
-                    while cRi < cLi:
-                        cR.label(text=" ")
-                        cRi += 1
-                    cL.operator('world.sunpos_clear_objects',
-                                text='Release Group')
-                    cL.label(text=" ")
-                    if(sp.ObjectGroup == 'ECLIPTIC'):
-                        cR.prop(sp, "TimeSpread")
-                    cR.props_enum(sp, "ObjectGroup")
-                else:
-                    cL.operator('world.sunpos_set_objects',
-                                text='Set Object Group')
-            else:
-                Sun.ObjectGroup_verified = False
+        col = flow.column(align=True)
+        col.prop(sp, "sun_distance")
+        if not sp.bind_to_sun:
+            col.prop(sp, "hdr_elevation")
+        col.prop(sp, "hdr_azimuth")
+        col.separator()
 
-        box = self.layout.box()
-        toprow = box.row()
-        row = toprow.row(align=False)
-        row.alignment = 'CENTER'
-        col = row.column(align=False)
-        #col.prop(sp, "ShowMap", text="Show Map", toggle=True, icon='WORLD')  # why commented out?
-        distanceSet = False
-
-        if p.ShowDMS:
-            split = col.split(factor=.5)
-            cL = split.column()
-            cR = split.column()
-            cL.alignment = 'LEFT'
-            cR.alignment = 'RIGHT'
-
-            cL.prop(sp, "Latitude")
-            cR.label(text=format_lat_long(sp.Latitude, True))
-            cL.prop(sp, "Longitude")
-            cR.label(text=format_lat_long(sp.Longitude, False))
-            if p.ShowNorth:
-                cL.prop(sp, "ShowNorth", text="Show North", toggle=True)
-                cR.prop(sp, "NorthOffset")
-            if p.ShowAzEl:
-                cL.label(text="Azimuth: " +
-                        str(round(Sun.Azimuth, 3)) + Degrees)
-                cR.label(text="Elevation: " +
-                        str(round(Sun.Elevation, 3)) + Degrees)
-            if p.ShowRefraction:
-                cL.prop(sp, "ShowRefraction", text="Show refraction")
-                cR.prop(sp, "SunDistance")
-                distanceSet = True
+        col = flow.column(align=True)
+        row1 = col.row()
+        if sp.bind_to_sun:
+            prop_text="Release binding"
         else:
-            cb = col.box()
-            tr = cb.row()
-            rr = tr.row(align=True)
-            cs = rr.split()
-            cL = cs.column(align=True)
-            cR = cs.column(align=True)
-            cL.prop(sp, "Latitude")
-            cR.prop(sp, "Longitude")
-            if p.ShowNorth:
-                col.separator()
-                cL.prop(sp, "ShowNorth", text="Show North", toggle=True)
-                cR.prop(sp, "NorthOffset")
-            if p.ShowAzEl:
-                cL.label(text="Azimuth: " +
-                         str(round(Sun.Azimuth, 3)) + Degrees)
-                cR.label(text="Elevation: " +
-                         str(round(Sun.Elevation, 3)) + Degrees)
-            if p.ShowRefraction:
-                cL.prop(sp, "ShowRefraction", text="Show refraction")
-                cR.prop(sp, "SunDistance")
-                distanceSet = True
-        if not distanceSet:
-            col.prop(sp, "SunDistance")
+            prop_text="Bind Texture to Sun "
+        row1.prop(sp, "bind_to_sun", toggle=True, icon="CONSTRAINT",
+                  text=prop_text)
+
+        row = col.row()
+        row.enabled = not sp.bind_to_sun
+        row.operator("world.sunpos_show_hdr", icon='LIGHT_SUN')
+
+    def draw_normal_mode_panel(self, context, sp, p, layout):
+        if p.use_time_place:
+            row = layout.row(align=True)
+            row.menu(SUNPOS_MT_Presets.__name__, text=SUNPOS_MT_Presets.bl_label)
+            row.operator(SUNPOS_OT_AddPreset.bl_idname, text="", icon='ADD')
+            row.operator(SUNPOS_OT_AddPreset.bl_idname, text="", icon='REMOVE').remove_active = True
+            row.operator(SUNPOS_OT_DefaultPresets.bl_idname, text="", icon='FILE_REFRESH')
 
         box = self.layout.box()
-        toprow = box.row()
-        row = toprow.row(align=False)
-        row.alignment = 'CENTER'
-        if Sun.UseDayMonth:
-            split = row.split(factor=.5)
-            colL = split.column()
-            colMid = split.column()
-            colMsplit = colMid.split(factor=.82)
-            colM = colMsplit.column()
-            colR = colMsplit.column()
-            colL.prop(sp, "Month")
-            colM.prop(sp, "Day")
-            colR.operator('world.sunpos_day_range', text= '',
-                          icon='SORTTIME')
-        else:
-            split = row.split(factor=.50)
-            colL = split.column()
-            colL.alignment = 'LEFT'
-            colMid = split.column()
-            colMsplit = colMid.split(factor=.90)
-            colM = colMsplit.column()
-            colR = colM.column()
-            colR.alignment = 'RIGHT'
-            colL.prop(sp, "Day_of_year")
-            colR.operator('world.sunpos_day_range',text= '',
-                          icon='SORTTIME')
-
-        colL.prop(sp, "Year")
-        colM.prop(sp, "UTCzone", slider=True)
-        lt, ut = format_time(sp.Time,
-                             sp.UTCzone,
-                             sp.DaylightSavings,
-                             sp.Longitude)
-        colL.prop(sp, "Time")
-        colM.label(text=lt, icon='TIME')
-        if p.ShowDST:
-            colL.prop(sp, "DaylightSavings", text="Daylight Savings")
-        colM.label(text="  " + ut, icon='PREVIEW_RANGE')
-        if p.ShowRiseSet:
-            if Sun.Sunrise.time == Sun.Sunset.time or \
-                    Sun.Sunrise.elevation > -0.4 or Sun.Sunset.elevation > -0.4:
-                Sun.RiseSetOK = False
-                tsr = "Sunrise: --------"
-                tss = " Sunset: --------"
-            else:
-                Sun.RiseSetOK = True
-                sr = format_hms(Sun.Sunrise.time)
-                ss = format_hms(Sun.Sunset.time)
-                tsr = "Sunrise: " + sr
-                tss = " Sunset: " + ss
-            colL.label(text=tsr, icon='LIGHT_SUN')
-            if p.ShowDST:
-                colM.label(text=tss, icon='SOLO_ON')
-            else:
-                colL.label(text=tss, icon='SOLO_ON')
+        flow = box.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False)
 
-############################################################################
+        col = flow.column()
+        col.prop(sp, "use_sky_texture", text="Cycles sky")
+        if sp.use_sky_texture:
+            col.prop_search(sp, "sky_texture", context.scene.world.node_tree,
+                            "nodes", text="")
+        col.separator()
 
+        col = flow.column()
+        col.prop(sp, "use_sun_object", text="Use object")
+        if sp.use_sun_object:
+            col.prop(sp, "sun_object", text="")
+        col.separator()
 
-class SunPos_OT_Preferences(bpy.types.Operator):
-    bl_idname = "world.sunpos_preferences"
-    bl_label = "Set preferences"
-    bl_description = "Press to set your preferences"
-
-    def execute(self, context):
-        Display.setAction('PREFERENCES')
-        return {'FINISHED'}
+        col = flow.column()
+        if p.use_object_collection:
+            col.prop(sp, "use_object_collection", text="Use collection")
+            if sp.use_object_collection:
+                col.prop(sp, "object_collection", text="")
+                if sp.object_collection:
+                    col.prop(sp, "object_collection_type")
+                    if sp.object_collection_type == 'ECLIPTIC':
+                        col.prop(sp, "time_spread")
 
+        box = self.layout.box()
+        box.prop(sp, "show_map", text="Show Map", toggle=True, icon='WORLD')
 
-class SunPos_OT_PreferencesDone(bpy.types.Operator):
-    bl_idname = "world.sunpos_pref_done"
-    bl_label = "Preferences done"
-    bl_description = "Press to complete your preferences"
+        col = box.column(align=True)
+        col.label(text="Enter coordinates:")
+        col.prop(sp, "co_parser", text='', icon='URL')
+
+        box.separator()
+
+        flow = box.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False)
+
+        col = flow.column(align=True)
+        col.prop(sp, "latitude")
+        if p.show_dms:
+            row = col.row()
+            row.alignment = 'RIGHT'
+            row.label(text=format_lat_long(sp.latitude, True))
+
+        col = flow.column(align=True)
+        col.prop(sp, "longitude")
+        if p.show_dms:
+            row = col.row()
+            row.alignment = 'RIGHT'
+            row.label(text=format_lat_long(sp.longitude, False))
+        col.separator()
 
-    def execute(self, context):
-        Display.setAction('ENABLE')
-        p = context.scene.SunPos_pref_property
-        Sun.UsageMode = p.UsageMode
-        Sun.MapLocation = p.MapLocation
-        if not p.UseObjectGroup:
-            sp = context.scene.SunPos_property
-            sp.UseObjectGroup = False
-            Sun.UseObjectGroup = False
-        return {'FINISHED'}
+        if p.show_north:
+            col = flow.column(align=True)
+            col.prop(sp, "show_north", text="Show North", toggle=True)
+            col.prop(sp, "north_offset")
+            col.separator()
 
+        if p.show_az_el:
+            col = flow.column(align=True)
+            row = col.row()
+            row.alignment = 'RIGHT'
+            row.label(text="Azimuth: " +
+                     str(round(sun.azimuth, 3)) + "°")
+            row = col.row()
+            row.alignment = 'RIGHT'
+            row.label(text="Elevation: " +
+                     str(round(sun.elevation, 3)) + "°")
+            col.separator()
 
-class SunPos_OT_DayRange(bpy.types.Operator):
-    bl_idname = "world.sunpos_day_range"
-    bl_label = "toggleDayRange"
-    bl_description = "Toggle day or (month / day) range"
+        if p.show_refraction:
+            col = flow.column()
+            col.prop(sp, "show_refraction", text="Show refraction")
+            col.separator()
 
-    def execute(self, context):
-        sp = context.scene.SunPos_property
-
-        if Sun.UseDayMonth:
-            try:
-                dt = datetime.date(sp.Year, sp.Month, sp.Day)
-                sp.Day_of_year = dt.timetuple().tm_yday
-            except:
-                pass
-            Sun.UseDayMonth = False
-        else:
-            Sun.UseDayMonth = True
-            dt = (datetime.date(sp.Year, 1, 1) +
-                 datetime.timedelta(sp.Day_of_year - 1))
-            sp.Day = dt.day
-            sp.Month = dt.month
-        return {'FINISHED'}
+        col = flow.column()
+        col.prop(sp, "sun_distance")
 
 
-class SunPos_OT_SetObjectGroup(bpy.types.Operator):
-    bl_idname = "world.sunpos_set_objects"
-    bl_label = "Set object group"
-    bl_description = "Set currently selected objects as object group"
+        box = self.layout.box()
+        flow = box.grid_flow(row_major=True, columns=0, even_columns=True, even_rows=False, align=False)
 
-    def execute(self, context):
-        del Sun.Selected_objects[:]
-        del Sun.Selected_names[:]
-        if (len(bpy.context.selected_objects) > 0):
-            Sun.Selected_names = [x.name for x in bpy.context.selected_objects]
-            Sun.Selected_objects = bpy.context.selected_objects
-            bpy.ops.object.select_all(action='DESELECT')
-            Move_sun()
+        col = flow.column(align=True)
+        col.prop(sp, "use_day_of_year",
+                 icon='SORTTIME')
+        if sp.use_day_of_year:
+            col.prop(sp, "day_of_year")
         else:
-            self.report({'WARNING'}, "No objects selected")
-        return {'FINISHED'}
-
-
-class SunPos_OT_ClearObjectGroup(bpy.types.Operator):
-    bl_idname = "world.sunpos_clear_objects"
-    bl_label = "Release object group"
-    bl_description = "Release object group"
+            col.prop(sp, "month")
+            col.prop(sp, "day")
+        col.prop(sp, "year")
+        col.separator()
 
-    def execute(self, context):
-        bpy.ops.object.select_all(action='DESELECT')
-        Sun.ObjectGroup_verified = False
-        Sun.verify_ObjectGroup()
-        try:
-            for x in Sun.Selected_objects:
-                x.select_set(True)
-        except:
-            pass
-        del Sun.Selected_objects[:]
-        del Sun.Selected_names[:]
-        return {'FINISHED'}
+        col = flow.column(align=True)
+        lt, ut = format_time(sp.time,
+                             sp.UTC_zone,
+                             sp.daylight_savings,
+                             sp.longitude)
+        col.prop(sp, "time")
+        col.prop(sp, "UTC_zone")
+        if p.show_dst:
+            col.prop(sp, "daylight_savings", text="Daylight Savings")
+        col.separator()
 
-# ---------------------------------------------------------------------------
-# Choice List of places, month and day at 12:00 noon
-# ---------------------------------------------------------------------------
-
-
-class SunPos_OT_TimePlace(bpy.types.Operator):
-    bl_idname = "world.pdp_operator"
-    bl_label = "Place & Day Presets"
-
-    #-----------  Description  ---------     M   D  UTC    Lat        Long   DaySav
-    pdp = [["North Pole, Summer Solstice",   6, 21,  0,   90.000,    0.0000, False],
-         ["Equator, Vernal Equinox",         3, 20,  0,   0.0000,    0.0000, False],
-         ["Rio de Janeiro, May 10th",        5, 10,  3, -22.9002,  -43.2334, False],
-         ["Tokyo, August 20th",              8, 20,  9,  35.7002,  139.7669, False],
-         ["Boston, Autumnal Equinox",        9, 22,  5,  42.3502,  -71.0500,  True],
-         ["Boston, Vernal Equinox",          3, 20,  5,  42.3502,  -71.0500,  True],
-         ["Honolulu, Winter Solstice",      12, 21, 10,  21.3001, -157.850, False],
-         ["Honolulu, Summer Solstice",       6, 21, 10,  21.3001, -157.850, False]]
-
-    from bpy.props import EnumProperty
-
-    timePlacePresets: EnumProperty(
-        name="Time & place presets",
-        description="Preset Place & Day",
-        items=(
-            ("7", pdp[7][0], ""),
-            ("6", pdp[6][0], ""),
-            ("5", pdp[5][0], ""),
-            ("4", pdp[4][0], ""),
-            ("3", pdp[3][0], ""),
-            ("2", pdp[2][0], ""),
-            ("1", pdp[1][0], ""),
-            ("0", pdp[0][0], ""),
-            ),
-        default="4")
+        col = flow.column(align=True)
+        col.alignment = 'CENTER'
+        col.label(text=lt, icon='TIME')
+        col.label(text="  " + ut, icon='PREVIEW_RANGE')
+        col.separator()
 
-    def execute(self, context):
-        sp = context.scene.SunPos_property
-        pdp = self.pdp
-        i = int(self.properties.timePlacePresets)
-        it = pdp[i]
-        Sun.PlaceLabel = it[0]
-        sp.Month = it[1]
-        sp.Day = it[2]
-        sp.Time = 12.00
-        sp.UTCzone = it[3]
-        sp.Latitude = it[4]
-        sp.Longitude = it[5]
-        sp.DaylightSavings = it[6]
-        dt = datetime.date(sp.Year, sp.Month, sp.Day)
-        sp.Day_of_year = dt.timetuple().tm_yday
-        # Force screen update
-        Display.refresh()
+        col = flow.column(align=True)
+        col.alignment = 'CENTER'
+        if p.show_rise_set:
+            if (sun.sunrise.time == sun.sunset.time
+                    or sun.sunrise.elevation > -0.4
+                    or sun.sunset.elevation  > -0.4):
+                sun.rise_set_ok = False
+                tsr = "sunrise: --------"
+                tss = " sunset: --------"
+            else:
+                sun.rise_set_ok = True
+                sr = format_hms(sun.sunrise.time)
+                ss = format_hms(sun.sunset.time)
+                tsr = "Sunrise: " + sr
+                tss = " Sunset: " + ss
+            col.label(text=tsr, icon='LIGHT_SUN')
+            col.label(text=tss, icon='SOLO_ON')
 
-        return {'FINISHED'}
 
-# ---------------------------------------------------------------------------
+# -------------------------------------------------------------------
 # Choice List of world maps
-# ---------------------------------------------------------------------------
-
-
-class SunPos_OT_MapChoice(bpy.types.Operator):
-    bl_idname = "world.wmp_operator"
-    bl_label = "World map files"
-    """
-    wmp = [["1536 x 768", "WorldMap.jpg"],
-           ["768 x 384", "WorldMapLR.jpg"],
-           ["512 x 256", "WorldMapLLR.jpg"],
-           ["Textureless", "None"]]
-    """
-    # S.L. provide one single optimized map < 100k
-    wmp = [["1536 x 768", "World.jpg"],
-           ["Textureless", "None"]]
-    from bpy.props import EnumProperty
-
-    mapPresets: EnumProperty(
-        name="World map presets",
-        description="world map files",
-        items=(
-            # ("3", wmp[3][0], ""),
-            #("2", wmp[2][0], ""),
-            ("1", wmp[1][0], ""),
-            ("0", wmp[0][0], ""),
-        ),
-        default="1")
-
-    def execute(self, context):
-        sp = context.scene.SunPos_property
-        wmp = self.wmp
-        i = int(self.properties.mapPresets)
-        sp.MapName = wmp[i]
-        Sun.MapName = wmp[i][1]
-
-        return {'FINISHED'}
+#
+# Uncomment this if you add other map images
+# -------------------------------------------------------------------
+
+
+# class SUNPOS_OT_MapChoice(bpy.types.Operator):
+#     bl_idname = "world.wmp_operator"
+#     bl_label = "World map files"
+#     """
+#     wmp = [["1536 x 768", "WorldMap.jpg"],
+#            ["768 x 384", "WorldMapLR.jpg"],
+#            ["512 x 256", "WorldMapLLR.jpg"],
+#            ["Textureless", "None"]]
+#     """
+#     # S.L. provide one single optimized map < 100k
+#     wmp = [["1536 x 768", "World.jpg"],
+#            ["Textureless", "None"]]
+#     from bpy.props import EnumProperty
+
+#     mapPresets: EnumProperty(
+#         name="World map presets",
+#         description="world map files",
+#         items=(
+#             # ("3", wmp[3][0], ""),
+#             #("2", wmp[2][0], ""),
+#             ("1", wmp[1][0], ""),
+#             ("0", wmp[0][0], ""),
+#         ),
+#         default="1")
+
+#     def execute(self, context):
+#         sp = context.scene.sun_pos_properties
+#         wmp = self.wmp
+#         i = int(self.properties.mapPresets)
+#         sp.map_name = wmp[i]
+#         Sun.MapName = wmp[i][1]
+
+#         return {'FINISHED'}
-- 
GitLab