import math
import time

import bpy
import blf
from mathutils import Vector
from mathutils import Quaternion
import mathutils

from .prefs import MouseStrafingPreferences
from .prefs import NavigationMouseButtonBinding

def getViews3D(context: bpy.types.Context) -> tuple[bpy.types.SpaceView3D, bpy.types.RegionView3D]:
    sv3d = getSpaceView3D(context)
    if not context.space_data.region_quadviews:
        return (sv3d, sv3d.region_3d)
    else:
        i = -1
        for region in context.area.regions:
            if region.type == 'WINDOW':
                i += 1
                if context.region == region:
                    break
        else:
            return (sv3d, sv3d.region_3d)
        return (sv3d, context.space_data.region_quadviews[i])

def getSpaceView3D(context: bpy.types.Context) -> bpy.types.SpaceView3D:
    area = context.area
    if area is not None and area.type == "VIEW_3D":
        return area.spaces[0]
    return None

def getViewPos(rv3d: bpy.types.RegionView3D) -> tuple[Vector, Vector]:
    pivotPos = Vector(rv3d.view_location)
    viewDir = Vector((0, 0, -1))
    viewDir.rotate(rv3d.view_rotation)
    return (pivotPos - viewDir * rv3d.view_distance, viewDir)

def setViewPos(rv3d: bpy.types.RegionView3D, viewPos: Vector):
    curPos, _viewDir = getViewPos(rv3d)
    offset = viewPos - curPos
    rv3d.view_location += offset

def signExp(base, exponent) -> float:
    v = abs(base)
    s = math.pow(v, exponent)
    return s if base > 0 else -s

def getKeyDown(previousKeyState: bool, event: bpy.types.Event) -> bool:
    return True if event.value == "PRESS" else (False if event.value == "RELEASE" else previousKeyState)

running = False
drawCallbackHandle = None

ms = 1_000_000 # one millisecond worth of nanoseconds for use with values returned by time.perf_counter_ns()

mouseSanityMultiplierPan = 0.003
mouseSanityMultiplierStrafe = 0.02
strafeSensitivityRanges = ((0.001, 0.001), (0.02, 0.002), (0.05, 0.005), (0.1, 0.01), (0.2, 0.02), (0.5, 0.05), (1.0, 0.1), (2.0, 0.2), (5.0, 0.5), (10.0, 1.0), (20.0, 2.0), (50.0, 5.0), 100)
focalLengthRanges = ((1, 0.125), (5, 0.25), (10, 0.5), (20, 1), (50, 2), (100, 2.5), (175, 5), 250)
fovRanges = ((0.125, 0.125), (5, 0.25), (15, 0.5), (30, 1), (130, 0.5), (160, 0.25), 179)

def calcAtLeastOneYearAgo(nowNs: int) -> int:
    return nowNs - 1000 * ms * 60 * 60 * 24 * 400

class CameraState(bpy.types.PropertyGroup):
    viewPos: bpy.props.FloatVectorProperty(size = 3, options = {"HIDDEN"})
    rot: bpy.props.FloatVectorProperty(size = 4, options = {"HIDDEN"})
    viewDist: bpy.props.FloatProperty(options = {"HIDDEN"})
    lens: bpy.props.FloatProperty(options = {"HIDDEN"})
    isPerspective: bpy.props.BoolProperty(options = {"HIDDEN"})

class CameraStates(bpy.types.PropertyGroup):
    savedStates: bpy.props.CollectionProperty(type = CameraState, options = {"HIDDEN"})
    usedStates: bpy.props.BoolVectorProperty(size = 10, default = (False, False, False, False, False, False, False, False, False, False), options = {"HIDDEN"})
    imminentState: bpy.props.PointerProperty(type = CameraState, options = {"HIDDEN"})
    imminentSlot: bpy.props.IntProperty(options = {"HIDDEN"}, default = -1)

class MouseStrafingOperator(bpy.types.Operator):
    """Strafe in the 3D View using the mouse."""
    bl_idname = "view3d.mouse_strafing"
    bl_label = "Mouse Strafing"
    bl_options = { "BLOCKING", "UNDO" }

    __slots__ = 'operatorKey', 'operatorKeyDown', 'operatorKeyPresses', 'inEscape', 'stopSignal', 'isInMouseAction', \
        'increasedMagnitude', 'increasedPrecision', 'changedBehavior', \
        'lmbDown', 'rmbDown', 'mmbDown', 'mb4Down', 'mb5Down', 'mb6Down', 'mb7Down', \
        'keyDownForward', 'keyDownLeft', 'keyDownBackward', 'keyDownRight', 'keyDownDown', 'keyDownUp', \
        'isWasding', 'wasdStartTimeNs', 'wasdPreviousTimeNs', 'wasdSpeedPercentage', \
        'keySaveStateDown', 'keySaveStateSlotDown', 'loadCameraState', 'loadedCameraState', 'imminentSaveStateTimeNs',  \
        'keyDownRelocatePivot', 'relocatePivotLock', 'adjustPivotSuccess', \
        'editStrafeSensitivityTimeNs', 'editFovTimeNs', 'editGearTimeNs', 'redrawAfterDrawCallback', 'keyCycleGearsDown', \
        'bewareWarpDist', 'previousDelta', 'ignoreMouseEvents', \
        'prefs', 'area', 'sv3d', 'rv3d', 'isUnitSystemNone', \
        'returnValue'

    def initFields(self, context: bpy.types.Context, event: bpy.types.Event):
        nowNs = time.perf_counter_ns()
        atLeastOneYearAgo = calcAtLeastOneYearAgo(nowNs)

        self.operatorKey = event.type
        self.operatorKeyDown = True
        self.operatorKeyPresses = 0
        self.inEscape = False
        self.isInMouseAction = False
        self.stopSignal = None

        self.increasedMagnitude, self.increasedPrecision, self.changedBehavior = False, False, False

        self.lmbDown, self.rmbDown, self.mmbDown, self.mb4Down, self.mb5Down, self.mb6Down, self.mb7Down = False, False, False, False, False, False, False

        self.keyDownForward, self.keyDownLeft, self.keyDownBackward, self.keyDownRight, self.keyDownDown, self.keyDownUp = False, False, False, False, False, False

        self.isWasding = False
        self.wasdStartTimeNs = 0
        self.wasdPreviousTimeNs = 0
        self.wasdSpeedPercentage = 0.0

        self.keySaveStateDown = False
        self.keySaveStateSlotDown = [False, False, False, False, False, False, False, False, False, False]
        self.loadCameraState = False
        self.loadedCameraState = False
        self.imminentSaveStateTimeNs = atLeastOneYearAgo

        self.keyDownRelocatePivot = False
        self.relocatePivotLock = False
        self.adjustPivotSuccess = False

        self.editStrafeSensitivityTimeNs = atLeastOneYearAgo
        self.editFovTimeNs = atLeastOneYearAgo
        self.editGearTimeNs = atLeastOneYearAgo
        self.redrawAfterDrawCallback = False
        self.keyCycleGearsDown = False

        self.bewareWarpDist = None
        self.previousDelta = None
        self.ignoreMouseEvents = 0

        self.prefs = context.preferences.addons[MouseStrafingPreferences.bl_idname].preferences
        self.area = context.area
        self.sv3d, self.rv3d = getViews3D(context)
        self.isUnitSystemNone = context.scene.unit_settings.system == 'NONE'

        self.returnValue = "FINISHED" if self.isEditingCamera() else "CANCELLED"

        self.snapGear()

        self.updateMode(context, event)

    def getActionFunc(self, action):
        if action == "turnXY":
            return self.turnXY
        elif action == "roll":
            return self.roll
        elif action == "strafeXZ":
            return self.strafeXZ
        elif action == "strafeXY":
            return self.strafeXY
        elif action == "strafeXRappel":
            return self.strafeXRappel
        elif action == "turnXRappel":
            return self.turnXRappel
        return self.nop

    def nop():
        return

    def turnXY(self, context: bpy.types.Context, delta: Vector):
        panDelta = delta * mouseSanityMultiplierPan * self.prefs.sensitivityPan * (0.85 if self.isWasding else 1) * self.getPanFactor(context)
        self.pan3dView(Vector((-panDelta[0] if self.prefs.invertMouseX else panDelta[0], -panDelta[1] if self.prefs.invertMouse else panDelta[1])))

    def roll(self, context: bpy.types.Context, delta: Vector):
        rollDelta = delta * mouseSanityMultiplierPan * self.prefs.sensitivityPan * (0.85 if self.isWasding else 1) * self.getRollFactor()
        self.roll3dView(Vector((-rollDelta[0] if self.prefs.invertMouseX else rollDelta[0], -rollDelta[1] if self.prefs.invertMouse else rollDelta[1])))

    def strafeXZ(self, context: bpy.types.Context, delta: Vector):
        strafeDelta = Vector((delta[0], delta[1])) * self.getMovementFactor(True, True)
        self.move3dView(Vector((-strafeDelta[0] if self.prefs.invertStrafeX else strafeDelta[0], 0, strafeDelta[1] if self.prefs.invertStrafeZ else -strafeDelta[1])), Vector((0, 0, 0)))

    def strafeXY(self, context: bpy.types.Context, delta: Vector):
        strafeDelta = Vector((delta[0], delta[1])) * self.getMovementFactor(True, True)
        self.move3dView(Vector((-strafeDelta[0] if self.prefs.invertStrafeX else strafeDelta[0], -strafeDelta[1] if self.prefs.invertStrafeY else strafeDelta[1], 0)), Vector((0, 0, 0)))

    def strafeXRappel(self, context: bpy.types.Context, delta: Vector):
        strafeDelta = Vector((delta[0], delta[1])) * self.getMovementFactor(True, True)
        self.move3dView(Vector((-strafeDelta[0] if self.prefs.invertStrafeX else strafeDelta[0], 0, 0)), Vector((0, 0, -strafeDelta[1] if self.prefs.invertStrafeY else strafeDelta[1])))

    def turnXRappel(self, context: bpy.types.Context, delta: Vector):
        panDelta = delta * mouseSanityMultiplierPan * self.prefs.sensitivityPan * (0.85 if self.isWasding else 1) * self.getPanFactor(context)
        strafeDelta = Vector((delta[0], delta[1])) * self.getMovementFactor(True, True)
        self.move3dView(Vector((0, 0, 0)), Vector((0, 0, -strafeDelta[1] if self.prefs.invertStrafeY else strafeDelta[1])))
        self.pan3dView(Vector((panDelta[0] * 0.8, 0)))

    def invoke(self, context: bpy.types.Context, event: bpy.types.Event):
        global running
        global drawCallbackHandle
        self.initFields(context, event)
        if not running and event.value == "PRESS":
            self.considerCenterCameraView()
            running = True
            context.window_manager.modal_handler_add(self)
            args = (self, context)
            if drawCallbackHandle is not None:
                bpy.types.SpaceView3D.draw_handler_remove(drawCallbackHandle, "WINDOW")
                drawCallbackHandle = None
            drawCallbackHandle = bpy.types.SpaceView3D.draw_handler_add(drawCallback, args, "WINDOW", "POST_PIXEL")
            context.area.tag_redraw()
            return {"RUNNING_MODAL"}
        return {"PASS_THROUGH"}

    def initSaveStates(self, context: bpy.types.Context):
        states: CameraStates = context.scene.mstrf_camera_save_states
        while len(states.savedStates) < 10:
            states.savedStates.add()

    def considerCenterCameraView(self):
        if self.rv3d.view_perspective == "CAMERA":
            self.rv3d.view_camera_offset = (0.0, 0.0)

    def modal(self, context: bpy.types.Context, event: bpy.types.Event):
        try:
            if not running:
                return {"CANCELLED"}
            if event.type == self.operatorKey:
                if event.value == "PRESS":
                    self.operatorKeyPresses = self.operatorKeyPresses + (0 if self.operatorKeyDown else 1)
                    self.operatorKeyDown = True
                elif event.value == "RELEASE":
                    self.operatorKeyDown = False
                if self.shouldExitOperator():
                    return self.exitOperator(context)
            modifierKeys = {"shift": event.shift, "ctrl": event.ctrl, "alt": event.alt, "omit": False}
            self.increasedMagnitude, self.increasedPrecision, self.changedBehavior = modifierKeys[self.prefs.increasedMagnitudeKey], modifierKeys[self.prefs.increasedPrecisionKey], modifierKeys[self.prefs.changedBehaviorKey]
            if event.type in { "MOUSEMOVE", "INBETWEEN_MOUSEMOVE" }:
                if self.ignoreMouseEvents > 0:
                    self.ignoreMouseEvents = self.ignoreMouseEvents - 1
                    self.bewareWarpDist = None
                    return {"RUNNING_MODAL"}
                delta = Vector((event.mouse_x - event.mouse_prev_x, event.mouse_y - event.mouse_prev_y))
                if self.bewareWarpDist is not None:
                    if delta.length > self.bewareWarpDist * 0.5 and delta.length > self.previousDelta.length * 2:
                        if self.prefs.debug:
                            print("mouse_strafing: detected cursor_warp glitch: using last mouse delta (" + str(self.previousDelta[0]) + ", " + str(self.previousDelta[1]) + ") instead of (" + str(delta[0]) + ", " + str(delta[1]) + ")")
                        delta = self.previousDelta
                    self.bewareWarpDist = None
                if self.isInMouseAction and not (event.mouse_x == event.mouse_prev_x and event.mouse_y == event.mouse_prev_y):
                    binding = self.getSatisfiedBinding()
                    if binding is not None:
                        self.getActionFunc(binding.action)(context, delta)
                    self.resetMouse(context, event)
            elif event.type in { "LEFTMOUSE", "RIGHTMOUSE", "MIDDLEMOUSE", "BUTTON4MOUSE", "BUTTON5MOUSE", "BUTTON6MOUSE", "BUTTON7MOUSE" }:
                return self.updateMode(context, event)
            elif event.type == "WHEELUPMOUSE" or event.type == "WHEELDOWNMOUSE":
                if self.changedBehavior:
                    self.nudgeFov(context, (event.type == "WHEELUPMOUSE") != self.prefs.scrollUpToZoomIn)
                elif self.prefs.wheelMoveFunction == "moveZ":
                    strafeFactor = self.getMovementFactor(False, self.prefs.useGearsWheel)
                    self.move3dView(Vector((0, 0, -self.prefs.wheelDistance * strafeFactor if event.type == "WHEELUPMOUSE" else self.prefs.wheelDistance * strafeFactor)), Vector((0, 0, 0)))
                elif self.prefs.wheelMoveFunction == "changeStrafeSensitivity":
                    magnitude = 1 if event.type == "WHEELUPMOUSE" else -1
                    magnitude = magnitude * 5 if self.increasedMagnitude else magnitude
                    prefs = bpy.context.preferences.addons[MouseStrafingPreferences.bl_idname].preferences
                    prefs.sensitivityStrafe = nudgeValue(prefs.sensitivityStrafe, magnitude, self.increasedPrecision, strafeSensitivityRanges)
                    bpy.context.preferences.use_preferences_save = True
                    self.editStrafeSensitivityTimeNs = time.perf_counter_ns()
            elif event.type == self.prefs.keyCycleGears:
                if event.value == "PRESS":
                    self.cycleGear(context, event.shift, not self.keyCycleGearsDown)
                self.keyCycleGearsDown = getKeyDown(self.keyCycleGearsDown, event)
            elif event.type in {self.prefs.keyForward, self.prefs.keyLeft, self.prefs.keyBackward, self.prefs.keyRight, self.prefs.keyDown, self.prefs.keyUp}:
                self.updateKeys(context, event)
                if self.stopSignal is None:
                    self.stopSignal = [False]
                    pinnedSv3d, pinnedRv3d = self.sv3d, self.rv3d
                    pinnedStopSignal = self.stopSignal
                    bpy.app.timers.register(lambda: fpsMove(self, pinnedSv3d, pinnedRv3d, pinnedStopSignal))
            elif event.type in {"ZERO", "ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", "EIGHT", "NINE"}:
                self.keySaveStateDown = getKeyDown(self.keySaveStateDown, event)
                slotIndex = parseDigitString(event.type)
                if self.keySaveStateDown and self.keySaveStateSlotDown[slotIndex]:
                    return {"RUNNING_MODAL"}
                self.keySaveStateSlotDown[slotIndex] = self.keySaveStateDown
                if self.keySaveStateDown:
                    self.processSaveState(slotIndex, context)
                else:
                    self.loadedCameraState = False
            elif event.type == self.prefs.keyLoadCameraState:
                if event.value == "PRESS":
                    self.loadCameraState = not self.loadCameraState
            elif event.type == self.prefs.keyRelocatePivot:
                if event.value == "PRESS" and not self.keyDownRelocatePivot:
                    if event.shift:
                        self.prefs.adjustPivot = not self.prefs.adjustPivot
                        bpy.context.preferences.use_preferences_save = True
                        self.relocatePivotLock = True
                        self.adjustPivotSuccess = False
                    else:
                        self.relocatePivotLock = False
                        if self.prefs.adjustPivot:
                            self.prefs.adjustPivot = False
                            bpy.context.preferences.use_preferences_save = True
                self.keyDownRelocatePivot = getKeyDown(self.keyDownRelocatePivot, event)
                if self.keyDownRelocatePivot and not self.prefs.adjustPivot and not self.relocatePivotLock and not event.shift:
                    self.adjustPivot(context)
            elif event.type == self.prefs.keyResetRoll:
                if event.value == "PRESS":
                    self.resetRoll(context)
            elif event.type == "ESC":
                self.inEscape = getKeyDown(self.inEscape, event)
                return self.considerExitOperator(context)
            else:
                return {"RUNNING_MODAL"}
            context.area.tag_redraw()
            return {"RUNNING_MODAL"}
        except:
            self.exitOperator(context, True)
            raise

    def getSatisfiedBinding(self) -> NavigationMouseButtonBinding:
        downButtons = []
        for button in ["lmb", "rmb", "mmb", "mb4", "mb5", "mb6", "mb7"]:
            if self.isButtonDown(button):
                downButtons.append(button)
        if len(downButtons) == 0:
            return None
        singleBindings = []
        doubleBindings = []
        for binding in self.prefs.buttonBindings:
            if binding.button1 in downButtons:
                if binding.button2 == "omit":
                    singleBindings.append(binding)
                elif binding.button2 in downButtons:
                    doubleBindings.append(binding)
        if len(doubleBindings) > 0:
            return doubleBindings[0]
        if len(singleBindings) > 0:
            return singleBindings[0]
        return None

    def isButtonDown(self, button: str) -> bool:
        return {"lmb": self.lmbDown, "rmb": self.rmbDown, "mmb": self.mmbDown, "mb4": self.mb4Down, "mb5": self.mb5Down, "mb6": self.mb6Down, "mb7": self.mb7Down}[button]

    def isAnyButtonDown(self) -> bool:
        return self.lmbDown or self.rmbDown or self.mmbDown or self.mb4Down or self.mb5Down or self.mb6Down or self.mb7Down

    def updateMode(self, context: bpy.types.Context, event: bpy.types.Event):
        self.lmbDown = event.value == "PRESS" if event.type == "LEFTMOUSE" else self.lmbDown
        self.rmbDown = event.value == "PRESS" if event.type == "RIGHTMOUSE" else self.rmbDown
        self.mmbDown = event.value == "PRESS" if event.type == "MIDDLEMOUSE" else self.mmbDown
        self.mb4Down = event.value == "PRESS" if event.type == "BUTTON4MOUSE" else self.mb4Down
        self.mb5Down = event.value == "PRESS" if event.type == "BUTTON5MOUSE" else self.mb5Down
        self.mb6Down = event.value == "PRESS" if event.type == "BUTTON6MOUSE" else self.mb6Down
        self.mb7Down = event.value == "PRESS" if event.type == "BUTTON7MOUSE" else self.mb7Down
        isAnyButtonDown = self.isAnyButtonDown()
        enteringMouseMode = isAnyButtonDown and not self.isInMouseAction
        leavingMouseMode = self.isInMouseAction and not isAnyButtonDown
        if enteringMouseMode:
            self.isInMouseAction = True
            context.window.cursor_set("NONE")
        elif leavingMouseMode:
            self.exitMouseMode(context)
            return self.considerExitOperator(context)
        context.area.tag_redraw()
        return {"RUNNING_MODAL"}

    def updateKeys(self, context: bpy.types.Context, event: bpy.types.Event):
        wasWasding = self.isWasding
        self.keyDownForward = event.value == "PRESS" if event.type == self.prefs.keyForward else self.keyDownForward
        self.keyDownLeft = event.value == "PRESS" if event.type == self.prefs.keyLeft else self.keyDownLeft
        self.keyDownBackward = event.value == "PRESS" if event.type == self.prefs.keyBackward else self.keyDownBackward
        self.keyDownRight = event.value == "PRESS" if event.type == self.prefs.keyRight else self.keyDownRight
        self.keyDownDown = event.value == "PRESS" if event.type == self.prefs.keyDown else self.keyDownDown
        self.keyDownUp = event.value == "PRESS" if event.type == self.prefs.keyUp else self.keyDownUp
        self.isWasding = self.keyDownForward or self.keyDownLeft or self.keyDownBackward or self.keyDownRight or self.keyDownDown or self.keyDownUp
        if not wasWasding or not self.isWasding:
            self.wasdSpeedPercentage = 0.0
        if not wasWasding and self.isWasding:
            self.wasdStartTimeNs = time.perf_counter_ns()
            self.wasdPreviousTimeNs = self.wasdStartTimeNs

    def pan3dView(self, delta: Vector):
        viewPos, viewRot, _viewDir = prepareCameraTransformation(self.sv3d, self.rv3d)
        yawRot = Quaternion(Vector((0, 0, 1)), -delta[0])
        pitchAxis = Vector((1, 0, 0))
        pitchAxis.rotate(viewRot)
        pitchRot = Quaternion(pitchAxis, delta[1])
        viewRot.rotate(pitchRot)
        viewRot.rotate(yawRot)
        applyCameraTranformation(self.sv3d, self.rv3d, viewPos, viewRot)

    def roll3dView(self, delta: Vector):
        viewPos, rot, viewDir = prepareCameraTransformation(self.sv3d, self.rv3d)
        roll = Quaternion(viewDir, delta[0])
        rot.rotate(roll)
        applyCameraTranformation(self.sv3d, self.rv3d, viewPos, rot)

    def move3dView(self, delta: Vector, globalDelta: Vector):
        viewPos, rot, _viewDir = prepareCameraTransformation(self.sv3d, self.rv3d)
        delta.rotate(Quaternion(rot))
        viewPos = viewPos + delta + globalDelta
        applyCameraTranformation(self.sv3d, self.rv3d, viewPos, rot)

    def cycleGear(self, context: bpy.types.Context, gearDown: bool, allowWrap: bool):
        availableGears = self.getGears()
        gear, gearIndex = self.findGear(availableGears)
        if gearIndex == -1:
            return
        change = -1 if gearDown else 1
        gearIndex = gearIndex + change
        if not allowWrap:
            if gearIndex < 0:
                gearIndex = 0
            elif gearIndex >= len(availableGears):
                gearIndex = len(availableGears) - 1
        else:
            gearIndex = gearIndex % len(availableGears)
        self.prefs.strafeGearSelected = availableGears[gearIndex]
        bpy.context.preferences.use_preferences_save = True
        self.editGearTimeNs = time.perf_counter_ns()

    def getGears(self):
        availableGears = []
        for gear in self.prefs.strafeGears:
            if gear != 0.0:
                availableGears.append(gear)
        return availableGears

    def findGear(self, gears: list) -> tuple[float, int]:
        smallestError = math.inf
        smallestErrorGear = 1.0
        smallestErrorGearIndex = -1
        index = 0
        for gear in gears:
            error = abs(gear - self.prefs.strafeGearSelected)
            if error <= smallestError:
                smallestError = error
                smallestErrorGear = gear
                smallestErrorGearIndex = index
            index = index + 1
        return smallestErrorGear, smallestErrorGearIndex

    def snapGear(self):
        self.prefs.strafeGearSelected, _gearIndex = self.findGear(self.getGears())

    def nudgeFov(self, context: bpy.types.Context, zoomOut: bool):
        if self.rv3d.view_perspective == "CAMERA":
            if self.sv3d.lock_camera and self.sv3d.camera is not None and self.sv3d.camera.type == "CAMERA" and type(self.sv3d.camera.data) is bpy.types.Camera:
                cam = bpy.types.Camera(self.sv3d.camera.data)
                sensorSize = getSensorSize(context, cam)
                cam.lens = self.nudgeLensValue(cam.lens, sensorSize[0], sensorSize[1], self.prefs.altWheelMoveFunction, zoomOut)
                self.editFovTimeNs = time.perf_counter_ns()
        else:
            sensorSize = getSensorSizeView3d(context)
            self.sv3d.lens = self.nudgeLensValue(self.sv3d.lens, sensorSize[0], sensorSize[1], self.prefs.altWheelMoveFunction, zoomOut)
            self.editFovTimeNs = time.perf_counter_ns()

    def nudgeLensValue(self, lens: float, sensorWidth: float, sensorHeight: float, method: str, zoomOut: bool) -> float:
        magnitude = 1 if zoomOut else -1
        magnitude = -magnitude if method == "changeFOV" else magnitude
        magnitude = magnitude * 5 if self.increasedMagnitude else magnitude
        if method == "changeHFOV":
            return fovToFocalLength(nudgeValue(focalLengthToFov(lens, sensorWidth), magnitude, self.increasedPrecision, fovRanges), sensorWidth)
        elif method == "changeVFOV":
            return fovToFocalLength(nudgeValue(focalLengthToFov(lens, sensorHeight), magnitude, self.increasedPrecision, fovRanges), sensorHeight)
        return nudgeValue(lens, magnitude, self.increasedPrecision, focalLengthRanges)

    def processSaveState(self, saveStateSlot, context: bpy.types.Context):
        self.initSaveStates(context)
        nowNs = time.perf_counter_ns()
        states: CameraStates = context.scene.mstrf_camera_save_states
        if self.loadCameraState:
            if states.imminentSlot != 0:
                self.saveCameraState(states, states.imminentSlot, states.imminentState)
                states.imminentSlot = -1
            if states.usedStates[saveStateSlot]:
                self.applyCameraState(context, states.savedStates[saveStateSlot])
                self.loadedCameraState = True
            self.loadCameraState = False
        elif states.imminentSlot == saveStateSlot and self.imminentSaveStateTimeNs > nowNs - 1000 * ms:
            if not states.usedStates[states.imminentSlot]:
                self.saveCameraState(states, states.imminentSlot, states.imminentState)
            self.applyCameraState(context, states.savedStates[saveStateSlot])
            self.loadedCameraState = True
            states.imminentSlot = -1
        else:
            self.loadedCameraState = False
            if states.imminentSlot >= 0:
                self.saveCameraState(states, states.imminentSlot, states.imminentState)
            viewPos, rot, _viewDir = prepareCameraTransformation(self.sv3d, self.rv3d)
            states.imminentState.viewPos = viewPos
            states.imminentState.rot = rot
            states.imminentState.viewDist = self.rv3d.view_distance
            states.imminentState.lens = self.sv3d.lens
            states.imminentState.isPerspective = self.rv3d.is_perspective
            states.imminentSlot = saveStateSlot
            self.imminentSaveStateTimeNs = nowNs

    def saveCameraState(self, states: CameraStates, slot, state: CameraState):
        states.usedStates[slot] = True
        states.savedStates[slot].viewPos = state.viewPos
        states.savedStates[slot].rot = state.rot
        states.savedStates[slot].viewDist = state.viewDist
        states.savedStates[slot].lens = state.lens
        states.savedStates[slot].isPerspective = state.isPerspective

    def applyCameraState(self, context:bpy.types.Context, cameraState: CameraState):
        if self.rv3d.is_perspective != cameraState.isPerspective:
            bpy.ops.view3d.view_persportho()
        self.rv3d.view_distance = cameraState.viewDist
        if not self.prefs.leaveFOV:
            self.sv3d.lens = cameraState.lens

        vP = cameraState.viewPos
        r = cameraState.rot
        applyCameraTranformation(self.sv3d, self.rv3d, Vector((vP[0], vP[1], vP[2])), Quaternion((r[0], r[1], r[2], r[3])))

    def getPanFactor(self, context: bpy.types.Context):
        focalLength, _sensorSize, _cam = self.getContextualLensSensor(context)
        mod = 50 / focalLength
        if mod > 1:
            mod = 1
        if self.isPrecisionRequested() and self.changedBehavior:
            return mod * 0.1
        return mod

    def getRollFactor(self):
        mod = 1.0
        if self.isPrecisionRequested() and self.changedBehavior:
            mod = 0.1
        return mod

    def getMovementFactor(self, isForMouseMovement: bool, useGear: bool) -> float:
        mod = self.getMovementFactorModifier()
        gear = self.prefs.strafeGearSelected if useGear else 1
        unitSystemConversion = self.prefs.speedMultiplierForUnitSystemNone if self.isUnitSystemNone else 1.0
        if isForMouseMovement:
            return mouseSanityMultiplierStrafe * self.prefs.sensitivityStrafe * mod * gear * unitSystemConversion
        return mod * gear * unitSystemConversion

    def getMovementFactorModifier(self):
        if self.isHigherMagnitudeRequested():
            return self.prefs.increasedMagnitudeSpeedFactor
        if self.isPrecisionRequested():
            return self.prefs.increasedPrecisionSpeedFactor
        return 1

    def getContextualLensSensor(self, context) -> tuple[float, tuple[float, float], bpy.types.Camera]:
        if self.isViewingCamera():
            cam = bpy.types.Camera(self.sv3d.camera.data)
            return cam.lens, getSensorSize(context, cam), cam
        else:
            return self.sv3d.lens, getSensorSizeView3d(context), None

    def isViewingCamera(self) -> bool:
        return self.rv3d.view_perspective == "CAMERA" and self.sv3d.camera is not None and self.sv3d.camera.type == "CAMERA" and type(self.sv3d.camera.data) is bpy.types.Camera

    def isEditingCamera(self) -> bool:
        return self.isViewingCamera() and self.sv3d.lock_camera

    def isHigherMagnitudeRequested(self) -> bool:
        return self.increasedMagnitude and (self.increasedMagnitude != self.increasedPrecision)

    def isPrecisionRequested(self) -> bool:
        return self.increasedPrecision and (self.increasedPrecision != self.increasedMagnitude)

    def adjustPivot(self, context: bpy.types.Context):
        self.adjustPivotSuccess = False
        hit = None
        viewPos, rot, viewDir = prepareCameraTransformation(self.sv3d, self.rv3d)
        castStart = viewPos + viewDir * self.sv3d.clip_start
        castLength = self.sv3d.clip_end - self.sv3d.clip_start
        if self.prefs.pivotAdjustmentIgnoreBackfaces == "always" or (self.prefs.pivotAdjustmentIgnoreBackfaces == "whenCulling" and self.sv3d.shading.show_backface_culling):
            hit = rayCastIgnoringBackfaces(context.scene, context.window.view_layer.depsgraph, castStart, viewDir, castLength)
        else:
            hit = context.scene.ray_cast(context.window.view_layer.depsgraph, castStart, viewDir, distance = castLength)
        if hit[0]:
            newPivotPos = viewPos + (Vector(hit[1]) - viewPos) * (1.0 + self.prefs.pivotDig * 0.01)
            self.rv3d.view_distance = (newPivotPos - viewPos).length
            applyCameraTranformation(self.sv3d, self.rv3d, viewPos, rot, True)
            self.adjustPivotSuccess = True

    def resetRoll(self, context: bpy.types.Context):
        viewPos, rot, viewDir = prepareCameraTransformation(self.sv3d, self.rv3d)
        up = Vector((0, 1, 0))
        up.rotate(rot)
        py = getPitchYaw(viewDir, up)
        newRot = Quaternion(Vector((1, 0, 0)), py[0])
        if py[0] != 0:
            newRot.rotate(Quaternion(Vector((0, 0, 1)), py[1]))
        applyCameraTranformation(self.sv3d, self.rv3d, viewPos, newRot)

    def wasdDelta(self):
        nowNs = time.perf_counter_ns()
        moveFactor = self.getMovementFactor(False, self.prefs.useGearsWasd)
        delta = moveFactor * self.wasdSpeedPercentage * self.prefs.wasdTopSpeed * ((nowNs - self.wasdPreviousTimeNs) / 1_000_000_000)
        self.wasdPreviousTimeNs = nowNs
        return delta

    def wasdAccelerate(self):
        if self.wasdSpeedPercentage < 1 and self.prefs.wasdTime > 0.0005:
            self.wasdSpeedPercentage = ((time.perf_counter_ns() - self.wasdStartTimeNs) / 1_000_000_000 / self.prefs.wasdTime)
            if self.wasdSpeedPercentage > 1:
                self.wasdSpeedPercentage = 1
        else:
            self.wasdSpeedPercentage = 1

    def exitMouseMode(self, context: bpy.types.Context):
        self.isInMouseAction = False
        self.centerMouse(context)
        context.window.cursor_set("DEFAULT")

    def centerMouse(self, context: bpy.types.Context):
        context.window.cursor_warp(context.region.x + context.region.width // 2, context.region.y + context.region.height // 2)
        self.ignoreMouseEvents = 1

    def resetMouse(self, context: bpy.types.Context, event: bpy.types.Event):
        if not (event.mouse_x < context.region.x + context.region.width // 4 or event.mouse_x > context.region.x + (3 * context.region.width) // 4 \
                or event.mouse_y < context.region.y + context.region.height // 4 or event.mouse_y > context.region.y + (3 * context.region.height) // 4):
            return
        target = (context.region.x + context.region.width // 2, context.region.y + context.region.height // 2)
        context.window.cursor_warp(target[0], target[1])
        self.previousDelta = Vector((event.mouse_x - event.mouse_prev_x, event.mouse_y - event.mouse_prev_y))
        self.bewareWarpDist = Vector((target[0] - event.mouse_x, target[1] - event.mouse_y)).length

    def considerExitOperator(self, context: bpy.types.Context):
        if self.shouldExitOperator():
            return self.exitOperator(context)
        return {"RUNNING_MODAL"}

    def shouldExitOperator(self):
        return (not self.prefs.toggleMode and not self.operatorKeyDown and not self.isInMouseAction) or (self.prefs.toggleMode and self.operatorKeyPresses > 0) or (self.inEscape)

    def exitOperator(self, context: bpy.types.Context, forceResetMouse: bool = False):
        global running
        global drawCallbackHandle
        if self.isInMouseAction or forceResetMouse:
            self.exitMouseMode(context)
        running = False
        if self.stopSignal is not None:
            self.stopSignal[0] = True
            self.stopSignal = None
        bpy.types.SpaceView3D.draw_handler_remove(drawCallbackHandle, "WINDOW")
        drawCallbackHandle = None
        if self.prefs.adjustPivot:
            self.adjustPivot(context)
        context.area.tag_redraw()
        return {self.returnValue}

def rayCastIgnoringBackfaces(scene: bpy.types.Scene, depsgraph, origin: Vector, direction: Vector, maxDistance: float):
    attemptCount = 0
    invert = False # there is no way to retrieve all hits, so we need to spam some rays back and forth.
    minCast = origin
    maxCast = origin + direction * maxDistance
    while attemptCount < 100:
        attemptCount = attemptCount + 1
        castDir = -direction if invert else direction
        castLength = (minCast - origin).dot(castDir) if invert else (maxCast - minCast).dot(castDir)
        if not invert and castLength <= 0:
            return rayCastMiss()
        hit = scene.ray_cast(depsgraph, origin, castDir, distance = castLength)
        if not hit[0]:
            return rayCastMiss()
        hitBackface = Vector(hit[2]).dot(direction) > 0
        if hitBackface:
            if invert:
                invert = not invert
                continue
            nudge = origin.length * 0.000001
            if nudge < 0.00001:
                nudge = 0.00001
            elif nudge > 0.004:
                nudge = 0.004
            origin = hit[1] + direction * nudge
            invert = not invert
            continue
        return hit
    return rayCastMiss()

def rayCastMiss():
    return (False, None, None, -1, None, None)

def clamp(x, min, max):
    return min if x < min else (max if x > max else x)

def magnitudeRepeat(nudgeFunc):
    def repeatNudgeFunc(value: float, magnitude: int, fine: bool, ranges: list) -> float:
        for i in range(0, -magnitude if magnitude < 0 else magnitude, 1):
            value = nudgeFunc(value, 1 if magnitude >= 0 else -1, fine, ranges)
        return value
    return repeatNudgeFunc

@magnitudeRepeat
def nudgeValue(value: float, magnitude: int, fine: bool, ranges: list) -> float:
    minimum = ranges[0][0]
    maximum = ranges[len(ranges) - 1]
    for i in range(len(ranges) - 2, -1, -1):
        if value+0.00001 >= ranges[i][0]:
            snappedValue = round(value, 3) if fine else round(value / ranges[i][1], 0) * ranges[i][1]
            if (snappedValue > value + 0.00001 and magnitude >= 0) or snappedValue < value - 0.00001 and magnitude < 0:
                value = snappedValue
                break
            value = snappedValue + (magnitude * 0.001 if fine else (ranges[i][1] if magnitude >= 0 else -ranges[i][1]))
            value = round(value, 3) if fine else round(value / ranges[i][1], 0) * ranges[i][1]
            if not fine and i > 0 and magnitude < 0 and value+0.00001 < ranges[i][0]:
                value = ranges[i][0] - ranges[i-1][1]
            break
    if value-0.00001 <= minimum:
        return minimum
    if value+0.00001 >= maximum:
        return maximum
    return value

def prepareCameraTransformation(sv3d: bpy.types.SpaceView3D, rv3d: bpy.types.RegionView3D) -> tuple[Vector, Quaternion, Vector]:
    considerViewToCamera(sv3d, rv3d)
    viewPos, _viewDir = getViewPos(rv3d)
    viewRot = rv3d.view_rotation
    if rv3d.view_perspective == "CAMERA" and sv3d.lock_camera and sv3d.camera is not None:
        viewPos, viewRot, _viewScale = sv3d.camera.matrix_local.decompose()
    viewDir = Vector((0, 0, -1))
    viewDir.rotate(viewRot)
    return viewPos, viewRot, viewDir

def applyCameraTranformation(sv3d: bpy.types.SpaceView3D, rv3d: bpy.types.RegionView3D, viewPos: Vector, viewRot: Quaternion, forceSetView = False):
    if not forceSetView and rv3d.view_perspective == "CAMERA" and sv3d.lock_camera and sv3d.camera is not None:
        axis, angle = viewRot.to_axis_angle()
        scale = sv3d.camera.matrix_local.to_scale()
        mat = mathutils.Matrix.Translation(viewPos) @ mathutils.Matrix.Rotation(angle, 4, axis) @ mathutils.Matrix.Diagonal(Vector((scale[0], scale[1], scale[2], 1)))
        sv3d.camera.matrix_local = mat
    else:
        rv3d.view_rotation = viewRot
        setViewPos(rv3d, viewPos)

def considerViewToCamera(sv3d: bpy.types.SpaceView3D, rv3d: bpy.types.RegionView3D):
    if rv3d.view_perspective == "CAMERA" and not sv3d.lock_camera and sv3d.camera is not None:
        viewToCamera(sv3d, rv3d) # break out of camera, since not locked
        rv3d.view_perspective = "PERSP"

def viewToCamera(sv3d: bpy.types.SpaceView3D, rv3d: bpy.types.RegionView3D):
    camera = sv3d.camera
    rv3d.view_rotation = camera.matrix_local.to_quaternion()
    setViewPos(rv3d, camera.location)

def getPitchYaw(dir: Vector, up: Vector):
    pitch = 0.0 if dir[2] <= -1 else (math.pi if dir[2] >= 1 else math.acos(-dir[2]))
    yaw = 0.0
    flatDir = dir
    if not (flatDir[2] > -0.999999 and flatDir[2] < 0.999999):
        flatDir = up
    elif not (flatDir[2] > -0.999 and flatDir[2] < 0.999):
        flatDir = flatDir * 1024 # normalizing a vector involves squaring its elements. making it longer here eliminates the chance of a division by zero.
    mirrorYaw = flatDir[1] < 0
    flatDir[2] = 0
    flatDir.normalize()
    x = flatDir[0]
    yaw = 0.5 * math.pi if x <= -1 else (-0.5 * math.pi if x >= 1 else -math.asin(x))
    if mirrorYaw:
        yaw = math.pi - yaw
    return Vector((pitch, yaw))

def fpsMove(op: MouseStrafingOperator, sv3d: bpy.types.SpaceView3D, rv3d: bpy.types.RegionView3D, stopSignal):
    if not running or stopSignal[0]: return None
    if op.isWasding: op.wasdAccelerate()
    delta = op.wasdDelta()
    deltaVecLocal = Vector((0, 0, 0))
    deltaVecGlobal = Vector((0, 0, 0))
    if op.keyDownForward: deltaVecLocal = deltaVecLocal + Vector((0, 0, -delta))
    if op.keyDownLeft: deltaVecLocal = deltaVecLocal + Vector((-delta, 0, 0))
    if op.keyDownBackward: deltaVecLocal = deltaVecLocal + Vector((0, 0, delta))
    if op.keyDownRight: deltaVecLocal = deltaVecLocal + Vector((delta, 0, 0))
    if op.keyDownDown:
        if op.prefs.wasdGlobalZ:
            deltaVecGlobal = deltaVecGlobal + Vector((0, 0, -delta))
        else:
            deltaVecLocal = deltaVecLocal + Vector((0, -delta, 0))
    if op.keyDownUp:
        if op.prefs.wasdGlobalZ:
            deltaVecGlobal = deltaVecGlobal + Vector((0, 0, delta))
        else:
            deltaVecLocal = deltaVecLocal + Vector((0, delta, 0))
    if op.keyDownForward or op.keyDownLeft or op.keyDownBackward or op.keyDownRight or op.keyDownDown or op.keyDownUp:
        bpy.context.view_layer.update() # Fixes being unable to pan while WASDing when camera is locked to view.
        op.move3dView(deltaVecLocal, deltaVecGlobal)
    return 0.001

def drawCallback(op: MouseStrafingOperator, context: bpy.types.Context):
    global drawCallbackHandle
    try:
        if context.area != op.area:
            return
        sv3d, rv3d = getViews3D(context)
        if rv3d != op.rv3d:
            return
        drawCrosshair(op, context)
        drawFovInfo(op, context)
        drawStrafeSensitivityInfo(op, context)
        drawGears(op, context)
        if op.redrawAfterDrawCallback:
            op.redrawAfterDrawCallback = False
            bpy.app.timers.register(context.area.tag_redraw, first_interval = 0)
    except:
        if drawCallbackHandle is not None:
            bpy.types.SpaceView3D.draw_handler_remove(drawCallbackHandle, "WINDOW")
            drawCallbackHandle = None
        raise

def drawCrosshair(op: MouseStrafingOperator, context: bpy.types.Context):
    color = (0.75, 0.75, 0.75, 1)
    if op.keyDownRelocatePivot:
        if op.prefs.adjustPivot:
            color = (1, 1, 0.05, 1)
        elif op.adjustPivotSuccess:
            color = (0.1, 1, 0.05, 1)
        else:
            color = (1, 0.1, 0.05, 1)
    elif op.keySaveStateDown:
        if op.loadedCameraState:
            color = (1, 1, 1, 1)
        else:
            color = (1, 0.05, 1, 1)
    elif op.loadCameraState:
        color = (0.05, 0.15, 1, 1)
    elif op.getSatisfiedBinding() is not None:
        if not op.prefs.showCrosshair:
            return
        color = (1, 1, 1, 1)
    blf.color(0, *color)
    blf.enable(0, blf.SHADOW)
    blf.shadow(0, 3, 0, 0, 0, 1)
    uiScale = context.preferences.system.ui_scale
    blf.size(0, int(20*uiScale), 72)
    x, y = context.region.width // 2 - int(8*uiScale), context.region.height // 2 - int(6*uiScale)
    blf.position(0, x, y, 0)
    blf.draw(0, "+")

def drawFovInfo(op: MouseStrafingOperator, context: bpy.types.Context):
    alphaFactor = drawFadeAlpha(op, op.editFovTimeNs, 1.0, 0.5)
    if alphaFactor == 0:
        return
    uiScale = context.preferences.system.ui_scale
    focalLength, sensorSize, cam = op.getContextualLensSensor(context)
    hFov = focalLengthToFov(focalLength, sensorSize[0])
    vFov = focalLengthToFov(focalLength, sensorSize[1])

    textLens = f"      {focalLength:.3f}mm"
    textHFov = f"  {hFov:.3f}°"
    textVFov = f"  {vFov:.3f}°"

    blf.enable(0, blf.SHADOW)
    blf.shadow(0, 3, 0, 0, 0, alphaFactor)
    blf.size(0, int(20*uiScale), 72)
    x, y = context.region.width // 2, context.region.height // 2

    b = 1 if op.prefs.altWheelMoveFunction == "changeVFOV" else 0.75
    blf.color(0, b, b, b, alphaFactor)
    drawText(x + int(100*uiScale), y, textVFov, halign = "CENTER", valign = "MIDDLE")

    b = 1 if op.prefs.altWheelMoveFunction == "changeFOV" else 0.75
    blf.color(0, b, b, b, alphaFactor)
    drawText(x + int(50*uiScale), y - (25*uiScale), textLens, halign = "CENTER", valign = "MIDDLE")

    b = 1 if op.prefs.altWheelMoveFunction == "changeHFOV" else 0.75
    blf.color(0, b, b, b, alphaFactor)
    drawText(x, y - int(50*uiScale), textHFov, halign = "CENTER", valign = "MIDDLE")

    if cam is not None:
        blf.size(0, int(12*uiScale), 72)
        blf.color(0, 1, 0.5, 0.1, alphaFactor)
        drawText(x, y - int(95*uiScale), "Showing Camera Values", halign = "CENTER")

def drawStrafeSensitivityInfo(op: MouseStrafingOperator, context: bpy.types.Context):
    alphaFactor = drawFadeAlpha(op, op.editStrafeSensitivityTimeNs, 1.0, 0.5)
    if alphaFactor == 0:
        return
    x, y = context.region.width // 2, context.region.height // 2
    blf.enable(0, blf.SHADOW)
    blf.shadow(0, 3, 0, 0, 0, alphaFactor)
    uiScale = context.preferences.system.ui_scale
    blf.size(0, int(20*uiScale), 72)
    blf.color(0, 1, 1, 1, alphaFactor)
    drawText(x, y + int(40*uiScale), f"{op.prefs.sensitivityStrafe:.3f}", halign = "CENTER")

def drawGears(op: MouseStrafingOperator, context: bpy.types.Context):
    alphaFactor = drawFadeAlpha(op, op.editGearTimeNs, 1.0, 0.5)
    if alphaFactor == 0:
        return
    x, y = context.region.width // 2, context.region.height // 2
    blf.enable(0, blf.SHADOW)
    blf.shadow(0, 3, 0, 0, 0, alphaFactor)
    uiScale = context.preferences.system.ui_scale
    blf.size(0, int(20*uiScale), 72)
    blf.color(0, 1, 1, 1, alphaFactor)
    availableGears = op.getGears()
    yoffset = ((len(availableGears) - 1) * 25) / 2
    index = 0
    for gear in availableGears:
        if gear == op.prefs.strafeGearSelected:
            blf.color(0, 1, 1, 1, alphaFactor)
        else:
            blf.color(0, 0.75, 0.75, 0.75, alphaFactor)
        drawText(x - (110*uiScale), y - int((yoffset-index*25)*uiScale), f"{gear:0.3f}", halign = "CENTER", valign = "MIDDLE")
        index = index + 1

def drawFadeAlpha(op: MouseStrafingOperator, wakeTimeNs: float, holdTime: float, fadeTime: float) -> float:
    nowNs = time.perf_counter_ns()
    passed = (nowNs - wakeTimeNs) / 1_000_000_000
    alphaFactor = 1.0
    if passed > holdTime:
        alphaFactor = 1.0 - (passed - holdTime) / fadeTime
    if alphaFactor <= 0:
        alphaFactor = 0.0
    else:
        op.redrawAfterDrawCallback = True
    return alphaFactor

def getSensorSizeView3d(context: bpy.types.Context) -> tuple[float, float]:
    sensorWidth = 72.0
    aspect = context.region.width / context.region.height
    if aspect < 1:
        sensorWidth = sensorWidth * aspect
    sensorHeight = sensorWidth / aspect
    return (sensorWidth, sensorHeight)

def getSensorSize(context: bpy.types.Context, cam: bpy.types.Camera) -> tuple[float, float]:
    aspect = (context.scene.render.resolution_x / context.scene.render.pixel_aspect_y) / (context.scene.render.resolution_y / context.scene.render.pixel_aspect_x)
    sensorWidth = cam.sensor_width
    sensorHeight = cam.sensor_height
    if cam.sensor_fit == "AUTO":
        if aspect < 1:
            sensorWidth = sensorWidth * aspect
        sensorHeight = sensorWidth / aspect
    elif cam.sensor_fit == "HORIZONTAL":
        sensorHeight = sensorWidth / aspect
    elif cam.sensor_fit == "VERTICAL":
        sensorWidth = sensorHeight * aspect
    return (sensorWidth, sensorHeight)

def focalLengthToFov(focalLength: float, sensorSideLength: float) -> float:
    return math.degrees(2 * math.atan(sensorSideLength / 2 / focalLength))

def fovToFocalLength(fov: float, sensorSideLength: float) -> float:
    return sensorSideLength / 2 / math.tan(math.radians(fov)/2)

def drawText(x: int, y: int, text: str, halign: str = "LEFT", valign: str = "BASELINE"):
    dims = blf.dimensions(0, text)
    if halign == "CENTER":
        x = x - dims[0]/2
    elif halign == "RIGHT":
        x = x - dims[0]
    if valign == "MIDDLE":
        y = y - dims[1]/2
    blf.position(0, x, y, 0)
    blf.draw(0, text)

def parseDigitString(s):
    options = { \
        "ZERO":  0, \
        "ONE":   1, \
        "TWO":   2, \
        "THREE": 3, \
        "FOUR":  4, \
        "FIVE":  5, \
        "SIX":   6, \
        "SEVEN": 7, \
        "EIGHT": 8, \
        "NINE":  9, \
    }
    return options[s]

km = None
kmi = None

def register_keymaps():
    global km
    global kmi
    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc:
        km = kc.keymaps.new(name = "3D View", space_type = "VIEW_3D", region_type = "WINDOW", modal=False)
        kmi = km.keymap_items.new(MouseStrafingOperator.bl_idname, "SPACE", "PRESS", head=True)

def unregister_keymaps():
    km.keymap_items.remove(kmi)


import click
import numpy as np
from copy import deepcopy
from matplotlib import pyplot as plt

def cost_function(x_true, x_em):
    """
    :param x_true: The pixels of original image (normalized). Each row is an RGB vector.
    :param x_em: The image that comes from the training of em.
    :return: The error between the real and the segmented image.
    """

    n = x_true.shape[0]  # The The number of our examples (pixels)

    return 1/n * np.sum(np.linalg.norm(x_true - x_em)**2)

def construct_image(height, width, g, m):
    """
    :param height: The height of the image
    :param width:  The width of the image
    :param g: Table (N x K). Contains the posterior probabilities of each example n belongs to each one
    of the K categories-segments.
    :param m: Table (K x 3). Contains an average vector (RGB color) from the data that belongs
    on the k-th category-segment
    :return: The normalized image which is needed to compute the error of the expectation maximisation algorithm and
    the colored image which is produced from the algorithm.
    """
    new_image = np.zeros((height * width, 3))

    for n in range(g.shape[0]):
        k = g[n].argmax()
        new_image[n] = m[k]

    flat = deepcopy(new_image)

    new_image = new_image.reshape((height, width, 3))

    k = m.shape[0]

    plt.imshow(new_image)
    plt.savefig('em_{}'.format(k))
    plt.show()

    return flat, new_image

def gaussian_mixture(x, p, m, s, k):
    """
    :param x: Our data (pixels). Each row is an RGB vector of a pixel
    :param p: Table (N x K). Contains the prior probabilities of each example n belongs to each category-segment k
    :param m: Table (K x 3). Contains an average vector(RGB) of the color from the data that belongs
    on category-segment k
    :param s: The covariance table S.
    :param k: The number of the categories.
    :return:  Table of shape (N x K) that contains the probabilities (that comes for mixture of Gaussian distributions)
    for each example n (pixel) belongs to each one of the K categories
    """
    probabilities = np.zeros((x.shape[0], k))

    for k_i in range(k):

        first_part = 1 / np.sqrt(2 * np.pi * s[k_i])
        second_part = np.exp(-(1 / (2 * s[k_i])) * (x - m[k_i, :]) ** 2)

        probabilities[:, k_i] = p[k_i] * np.prod(first_part * second_part, axis=1)

    return np.array(probabilities)

def log_likelihood(probabilities):
    return np.sum(np.log(np.sum(probabilities, axis=1)))

def maximization_step(x, g):
    """
    Execute the maximization stem of the algorithm and update the parameters
    :param x: Our data (pixels). Each row is an RGB vector of a pixel
    :param g: Table (N x K). Contains the posterior probabilities of each example n belongs to each one of
    the K categories-segments
    :return: The updated parameters.
    p: Table (N x K). Contains the prior probabilities of each example n belongs to each category-segment k
    m: Table (K x 3). Contains an average vector(RGB) of the color from the data that belongs on category-segment k
    s: The covariance table S.
    """

    k = g.shape[1]

    m = np.zeros((k, x.shape[1]))
    p = np.zeros(k)
    s = np.zeros(k)

    for k_i in range(k):
        g_k = g[:, k_i].reshape((-1, 1))

        m[k_i, :] = np.sum(g_k * x, axis=0) / np.sum(g_k)

        s[k_i] = np.sum(np.sum(g_k * ((x - m[k_i]) ** 2), axis=1)) / (x.shape[1] * np.sum(g_k))

        p[k_i] = np.sum(g_k) / x.shape[0]

    return p, m, s

def expectation_step(probabilities):
    """
    Execute the expectation step of the algorithm
    :param probabilities: Table (N x K). Contains the probabilities (that comes for Gaussian mixture)
    of each example n belongs to each category-segment k
    :return: Table (N x K). Contains the posterior probabilities of each example n belongs to each one of
    the K categories-segments
    """

    denominator = np.sum(probabilities, axis=1)

    return probabilities / denominator.reshape((-1, 1))

def initialize_parameters(k, d):
    """
    :param k: The number of categories-segments
    :param d: The dimension of each example-pixel (R, G, B) = 3
    :return: The prior probabilities, the average_vectors and the covariance table
    """

    # At the start, the prior probability of each category is the same and equal to 1/k
    prior = np.full(k, 1/k)

    # We have average_vector of d-dimension for each category and initialize
    # them with values from 0-1 because we have a normalized image.
    m = np.zeros((k, d))

    for i in range(m.shape[0]):
        m[i, :] = np.random.uniform(0, 1, d)

    # Initialize the covariance of each category
    # with values between 0.2-0.8 -> 60% of the real values
    s = np.random.uniform(0.2, 0.8, k)

    return prior, m, s

def expectation_maximization(x, k, iterations, tolerance):
    """
    :param x: Table dimension (N x 3) with the pixels of th image
    :param k: The number o categories-segments
    :param tolerance: The tolerance you accept
    :param iterations: The number of iterations you want to run the algorithm
    :return: The probabilities of N example belongings on the k category and
    the average vectors of each category
    """
    d = x.shape[1]  # The dimension of each example-pixel (R G B) = 3

    prior, m, s = initialize_parameters(k, d)

    prob = gaussian_mixture(x, prior, m, s, k)

    for t in range(iterations):

        log_likelihood_old = log_likelihood(prob)

        g = expectation_step(prob)

        prior, m, s = maximization_step(x, g)

        prob = gaussian_mixture(x, prior, m, s, k)

        log_likelihood_new = log_likelihood(prob)
        print('log_likelihood of {:<3} iteration: {}'.format(t, log_likelihood_new))

        if log_likelihood_new - log_likelihood_old < 0:
            print('Error in coding')

        if np.abs(log_likelihood_new - log_likelihood_old) < tolerance:
            print('Converged')
            break

    return g, m

@click.command()
@click.option('--segments', default=8)
@click.option('--path', default='im.jpg')
@click.option('--iterations', default=100)
@click.option('--tolerance', default=1e-6)
def main(segments, path, iterations, tolerance):
    img = plt.imread(path)

    print('Image shape: {}'.format(img.shape))

    #  Calculate our N independent pixels
    number_of_pixels = img.shape[0] * img.shape[1]

    #  Reshape the image in order to have a table (N x 3)
    data = img.reshape((number_of_pixels, 3))

    # Normalize our data
    data = data / 255

    post_probabilities, average_vectors = expectation_maximization(x=data,
                                                                   k=segments,
                                                                   iterations=iterations,
                                                                   tolerance=tolerance)

    flt, new_img = construct_image(img.shape[0], img.shape[1], post_probabilities, average_vectors)

    error = cost_function(data, flt)

    print('Total error: {}'.format(error))

if __name__ == '__main__':
    main()


import bs4
import requests
from requests import status_codes
import pandas as pd
import textwrap
from bs4 import BeautifulSoup as soup

papers = pd.read_csv('dataset.csv')
url = pd.DataFrame(papers, columns=['GTRProjectUrl', 'FundingOrgName'])
epsrc = url.loc[url['FundingOrgName'] == "EPSRC"]
abstracts = epsrc['GTRProjectUrl'].values

filename = "abstract1.csv"
f = open(filename, "w")
header = "All Abstracts"
f.write(header)

for abstract in abstracts:
    result = requests.get(abstract)
    if result.status_code == requests.codes.ok:
        src = result.content
        extract = soup(src, 'html.parser')
        content = extract.find("gtr:abstracttext")
        title = extract.find("gtr:title")
        funding = extract.find("gtr:valuepounds")
        print("Project Title: " + title.string)
        print("Funding Organization: EPSRC")
        print("Project Abstract: " + textwrap.fill(content.string), end='\n')
        print("Funded Value : £" + funding.string)
        print(len(abstracts))
        print(abstract)

        f.write("\n" + content.string.replace(",", "|"))

f.close()


from __future__ import unicode_literals
from .session import session
from . import utils
from . import enums
from pyquery import PyQuery as pq
import logging
logger = logging.getLogger(__name__)

@staticmethod
def setup_profile():
    """Initiates a new Steam Profile
    """
    resp = session.get(utils.url_community(
        'profiles', str(utils.get_steam_id())) + 'edit?welcomed=1')
    return resp and resp.ok

@staticmethod
def edit_profile(new_values=None):
    """Updates your Steam profile information.

    Current values are returned if `new_values` is not supplied.

    Supported values for editing are
    --------------------------------
    - `personaName`
    - `real_name`
    - `country`
    - `state`
    - `city`
    - `customURL`
    - `summary`
    - `profile_background`
    - `primary_group_steamid`

    Parameters
    ----------
    new_values : dict, optional
        Dict with editable values, for example::

            {
                'personaName': 'new display name',
                'summary': 'coolest guy ever'
            }

    Returns
    -------
    tuple of (error: str or None, dict)
        `(error, current values)` if `new_values` is not provided,
        else `(error, new values)`.
    """
    def editables(values):
        valid = ['personaName', 'real_name', 'country', 'state', 'city',
                 'customURL', 'summary', 'profile_background', 'primary_group_steamid']

        return {k: v for k, v in list(values.items()) if k in valid}

    def parseForValues(doc):
        values = {}

        all_inputs = doc('#editForm :input').filter(
            lambda i, this: this.tag != "button")
        # visible = all_inputs.filter("[type!='hidden']")
        without_file = all_inputs.filter("[type!='file']")

        for inp in without_file:
            values[inp.name] = inp.value

        return values

    edit_url = utils.url_community(
        'profiles', str(utils.get_steam_id())) + 'edit'
    doc = pq(session.get(edit_url).text)

    values = parseForValues(doc)

    if not new_values:
        return (None, editables(values))
    else:
        values.update(new_values)
        update_resp = pq(session.post(edit_url, data=values).text)
        values = parseForValues(update_resp)
        error = update_resp('#errorText .formRowFields')
        if error:
            return (error.text.strip(), editables(values))

        return (None, editables(values))

@staticmethod
def edit_privacy_settings(new_values=None):
    """Updates your Steam privacy settings.

    Current values are returned if `new_values` is not supplied.

    Supported values for editing are
    --------------------------------
    - `privacySetting`
    - `commentSetting`
    - `inventoryPrivacySetting`
    - `inventoryGiftPrivacy`

    Parameters
    ----------
    new_values : dict, optional
        Dict with editable values, for example::

            {
                'personaName': 'new display name',
                'summary': 'coolest guy ever'
            }

    Returns
    -------
    tuple of (error: str or None, dict)
        `(error, current values)` if `new_values` is not provided,
        else `(error, new values)`.
    """
    def editables(values):
        valid = ['privacySetting', 'commentSetting',
                 'inventoryPrivacySetting', 'inventoryGiftPrivacy']

        return {k: v for k, v in list(values.items()) if k in valid}

    def parseForValues(doc):
        all_inputs = doc('#editForm :input').filter(
            lambda i, this: this.tag != "button")

        values = {inp.name: inp.value for inp in all_inputs}

        for inp in doc('#editForm input:checked'):
            values[inp.name] = inp.value
            if inp.name in ['privacySetting', 'inventoryPrivacySetting']:
                values[inp.name] = enums.PrivacyState(int(inp.value))
            if inp.name == 'commentSetting':
                values[inp.name] = enums.CommentPrivacyState(inp.value)

        if values['inventoryGiftPrivacy'] is not None:
            values['inventoryGiftPrivacy'] = bool(
                int(values['inventoryGiftPrivacy']))
        else:
            values['inventoryGiftPrivacy'] = False

        return values

    edit_url = utils.url_community('profiles', str(
        utils.get_steam_id())) + 'edit/settings'
    doc = pq(session.get(edit_url).text)
    values = parseForValues(doc)

    if not new_values:
        return (None, editables(values))
    else:
        values.update(new_values)
        for k, v in list(editables(values).items()):
            if k == 'inventoryGiftPrivacy':
                values[k] = int(v)
            else:
                values[k] = v.value

        resp = session.post(edit_url, data=values)
        update_resp = pq(resp.text)
        values = parseForValues(update_resp)
        error = update_resp('#errorText .formRowFields')
        if error:
            return (error.text.strip(), editables(values))

        return (None, editables(values))

@staticmethod
def upload_avatar(image):
    """Sets the current account's avatar on Steam.

    Parameters
    ----------
    image : file
        File-like object, in binary mode.
    """
    data = {
        'MAX_FILE_SIZE': 1048576,
        'type': 'player_avatar_image',
        'sId': str(utils.get_steam_id()),
        'sessionid': utils.get_session_id(),
        'doSub': 1,
        'json': 1
    }

    files = {'avatar': image}
    resp = session.post(utils.url_community(
        'actions', 'FileUploader'), files=files, data=data)

    if not resp.ok:
        logger.error('Avatar upload: HTTP error %s', resp.status_code)
        return

    try:
        body = resp.json()
    except:
        body = {}

    if not body or not body.get('success'):
        logger.error('Avatar upload: Malformed response')

    if not body.get('success') and body.get('message'):
        logger.error('Avatar upload: %s', body['message'])

    return body


"""Video Details feature UI and event handlers"""
import os
from typing import Callable
import gradio as gr
from webui_utils.simple_config import SimpleConfig
from webui_utils.simple_icons import SimpleIcons
from webui_utils.simple_utils import seconds_to_hms, clean_dict, get_frac_str_as_float
from webui_utils.video_utils import get_video_details
from webui_utils.mtqdm import Mtqdm
from webui_tips import WebuiTips
from interpolate_engine import InterpolateEngine
from tabs.tab_base import TabBase

class VideoDetails(TabBase):
    """Encapsulates UI elements and events for the Video Details feature"""
    def __init__(self,
                    config : SimpleConfig,
                    engine : InterpolateEngine,
                    log_fn : Callable):
        TabBase.__init__(self, config, engine, log_fn)

    def render_tab(self):
        """Render tab into UI"""
        with gr.Tab("Video Details"):
            gr.HTML(SimpleIcons.BAR_CHART + "Show internal details for a media file",
                elem_id="tabheading")
            with gr.Row():
                input_file = gr.Text(max_lines=1, label="Media File Path",
                    placeholder="Path on this server to the media file to be inspected")
            with gr.Row():
                frame_rate = gr.Text(max_lines=1, label="Frame Rate")
                dimensions = gr.Text(max_lines=1, label="Dimensions")
                duration = gr.Text(max_lines=1, label="Duration")
                frame_count = gr.Text(max_lines=1, label="Frame Count")
                file_size = gr.Text(max_lines=1, label="File Size")
            count_frames = gr.Checkbox(value=False, label="Count Frames (Slower)",
                info="Scans file counting frames; required for media without frame count metadata")
            report_button = gr.Button("Get Details", variant="primary")
            output_text = gr.Textbox(label="Details", interactive=False)
            with gr.Accordion(SimpleIcons.TIPS_SYMBOL + " Guide", open=False):
                WebuiTips.video_details.render()
        report_button.click(self.create_report, inputs=[input_file, count_frames],
                    outputs=[output_text, frame_rate, duration, dimensions, frame_count, file_size])

    def create_report(self, input_path : str, count_frames : bool):
        """Create Report button handler"""
        if input_path:
            if os.path.exists(input_path):
                separator = ""
                report = []

                self.log(f"calling get_video_details for {input_path}")
                with Mtqdm().open_bar(total=1, desc="FFmpeg") as bar:
                    Mtqdm().message(bar, "FFmpeg in use ...")
                    data = get_video_details(input_path, count_frames=count_frames)
                    self.log("received details:")
                    self.log(str(data))
                    Mtqdm().update_bar(bar)

                error_data = data.get("error")
                if error_data:
                    report.append("ERROR: FFprobe returned an error processing the media file")
                    report.append(separator)
                    report.append("FFprobe command:")
                    report.append(error_data["ffprobe_cmd"])
                    report.append(separator)
                    report.append("Console output:")
                    report.append(error_data["console_output"])
                    return gr.update(value="\r\n".join(report)), None, None, None, None, None

                format = {}
                format_data = data["format"]
                format["filename"] = format_data.get("filename")
                format["start_time"] = seconds_to_hms(float(format_data.get("start_time", 0)))
                format["duration"] = seconds_to_hms(float(format_data.get("duration", 0)))
                file_size = f"{int(format_data.get('size', 0)):,d}"
                format["size"] = file_size
                format["bit_rate"] = f"{int(format_data.get('bit_rate', 0)):,d}"
                format["format_name"] = format_data.get("format_long_name")
                format = clean_dict(format)

                warning = "Unknown"
                video_summary = {}
                streams = []
                streams_data = data["streams"]
                for stream_data in streams_data:
                    index = stream_data.get("index")
                    codec_type = stream_data.get("codec_type")
                    stream = {}

                    avg_frame_rate = stream_data.get("avg_frame_rate")
                    avg_frame_rate = get_frac_str_as_float(avg_frame_rate)
                    r_frame_rate = stream_data.get("r_frame_rate")
                    r_frame_rate = get_frac_str_as_float(r_frame_rate)
                    frame_rate = avg_frame_rate or r_frame_rate
                    frame_rate = f"{frame_rate:0.2f}" if frame_rate else warning
                    stream["frame_rate"] = frame_rate

                    stream["start_time"] = seconds_to_hms(float(stream_data.get("start_time", 0)))
                    duration = seconds_to_hms(float(stream_data.get("duration", 0)))
                    stream["duration"] = duration
                    stream["width"] = stream_data.get("width")
                    stream["height"] = stream_data.get("height")

                    frame_count = stream_data.get("nb_read_frames") or stream_data.get("nb_frames")
                    frame_count = f"{int(frame_count):,d}" if frame_count else warning

                    stream["frame_count"] = frame_count
                    stream["bit_rate"] = f"{int(stream_data.get('bit_rate', 0)):,d}"
                    stream["codec_name"] = stream_data.get("codec_name")
                    stream["pix_fmt"] = stream_data.get("pix_fmt")
                    stream["sample_rate"] = stream_data.get("sample_rate")
                    stream["channels"] = stream_data.get("channels")
                    stream["channel_layout"] = stream_data.get("channel_layout")
                    stream["sample_aspect_ratio"] = stream_data.get("sample_aspect_ratio")
                    stream["display_aspect_ratio"] = stream_data.get("display_aspect_ratio")

                    # capture video details from the first found video stream
                    if not video_summary and codec_type == "video":
                        video_summary["frame_rate"] = frame_rate
                        video_summary["duration"] = duration[:duration.find(".")]
                        video_summary["dimensions"] = f"{stream['width']}x{stream['height']}"
                        video_summary["frame_count"] = frame_count
                        video_summary["file_size"] = file_size

                    stream = clean_dict(stream)
                    stream_name = f"#{index} {codec_type}"
                    streams.append({stream_name : stream})

                report.append("[Format]")
                for k, v in format.items():
                    report.append(f"{k}: {v}")
                report.append(separator)

                for stream in streams:
                    stream_name = list(stream.keys())[0]
                    stream_data = list(stream.values())[0]
                    report.append(f"[Stream {stream_name}]")
                    for k, v in stream_data.items():
                        report.append(f"{k}: {v}")
                    report.append(separator)
                report.append("More media file details availale in the Log Viewer")

                return gr.update(value="\r\n".join(report)),\
                                 video_summary.get("frame_rate"),\
                                 video_summary.get("duration"),\
                                 video_summary.get("dimensions"),\
                                 video_summary.get("frame_count"),\
                                 video_summary.get("file_size")
            else:
                return gr.update(value=f"media file {input_path} not found"), None, None, None,\
                                                                            None, None
        else:
            return gr.update(value="media file path must be supplied"), None, None, None, None, None


#!/home/msj/miniconda3/bin/python3

import matplotlib as mpl
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
from matplotlib.font_manager import FontProperties

from jinja2 import Environment, FileSystemLoader
import time
import datetime
import mobitools.weather.vanweather as vw
from mobitools.core.helpers import *
import matplotlib.lines as mlines
import os

from pandas.plotting import register_matplotlib_converters
register_matplotlib_converters()

#print('Setting MPL defaults')
#https://matplotlib.org/users/customizing.html
colors = ['#3778bf','#7bb274','#825f87',
          '#feb308','#59656d','#984447',
          '#e17701','#add9f4','#1b264f',
          '#4b296b','#380835','#607c8e',
          '#ca6641']
# colors = ['#3778bf','#7bb274',,'#feb308',
#               '#59656d']

mpl.rcParams['axes.prop_cycle'] = mpl.cycler('color',colors)
bg_color = '#ffffff'
fg_color = colors[1]
fg_color2 = colors[0]
fg_color3 = colors[3]
fg_color4 = colors[2]
ax_color = colors[4]

mpl.rcParams['font.family'] = 'sans-serif'
#mpl.rcParams['font.sans-serif'] = ['Helvetica']
plt.rc('text', color=ax_color)
#plt.rc('axes',titleweight='bold')

mpl.rcParams['axes.spines.right'] = False
mpl.rcParams['axes.spines.top'] = False
mpl.rcParams['axes.edgecolor'] = ax_color
mpl.rcParams['ytick.color'] = ax_color
mpl.rcParams['xtick.color'] = ax_color
mpl.rcParams['axes.labelcolor'] = ax_color
mpl.rcParams['axes.labelsize'] = 13
mpl.rcParams["figure.figsize"] = (7,5)

def yoy_plot(df,fname):

    yday_day = (datetime.datetime.now() - datetime.timedelta(1)).day
    yday_month = (datetime.datetime.now() - datetime.timedelta(1)).month

    todaydf = df[(df.index.month==yday_month) & (df.index.day==yday_day)]
    f,ax = plt.subplots()
    ax.plot(df,color=ax_color,alpha=0.5)
    ax.plot(todaydf.index,todaydf.values,color=fg_color)
    s = ax.scatter(todaydf.index,todaydf.values,color=fg_color)

    ax.tick_params(axis='x',labelrotation=45)

    labelfont = FontProperties(weight='roman',size=12)

    ax.set_ylabel('Trips')

    def get_text(date):
        wdf = vw.get_monthly_weather(date.strftime('%Y'),date.strftime('%m'))
        text = f"{date.strftime('%A, %b %d %Y')}\nDaily High: {wdf.loc[date.strftime('%Y-%m-%d'),'Max Temp']}°C\nPrecipitation: {wdf.loc[date.strftime('%Y-%m-%d'),'Total Precipmm']}mm"
        return text

    try:
        texts = [get_text(x) for x in todaydf.index]
    except:
        texts = [x for x in todaydf.index]

    for c,text in zip(s.get_offsets().data,texts):
        ax.annotate(text,c,color=fg_color,
                textcoords='offset points',xytext=(-60,-50),
                bbox=dict(boxstyle="round",fc='w',ec='w',alpha=0.9),
                fontproperties=labelfont
                #arrowprops=dict(arrowstyle = '-|>',color='k',alpha=0.5)
               )

    f.tight_layout()
    f.savefig(fname)

def simple_plot(df,fname,kind='line',highlight=False):
    f,ax = plt.subplots()

    if kind == 'line':

        if highlight:
            line = ax.plot(df[:-24].index,df.values[:-24],color=fg_color,alpha=0.3)
            ax.fill_between(df.index[:-24],0,df.values[:-24],color=fg_color,alpha=0.2)
            line = ax.plot(df.index[-25:],df.values[-25:],color=fg_color)
            ax.fill_between(df.index[-25:],0,df.values[-25:],color=fg_color,alpha=0.8)
        else:
            line = ax.plot(df.index,df.values,color=fg_color)
            ax.fill_between(df.index,0,df.values,color=fg_color,alpha=0.8)

        ax.xaxis.set_major_formatter(mdates.DateFormatter("%A"))
        ax.xaxis.get_ticklabels()[-1].set_visible(False) # removes the rightmost tick label

    if kind=='bar':
        bars = ax.bar(df.index,df.values,color=fg_color)
        # set ticks every week
        ax.xaxis.set_major_locator(mdates.WeekdayLocator())
        #set major ticks format
        ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %d'))
        if highlight:
            [x.set_alpha(0.6) for x in bars[:-1]]
    ax.tick_params(axis='x',labelrotation=45)
    ax.set_ylabel('Trips')
    f.tight_layout()
    f.savefig(fname)
    return f,ax

def weather_plot(df,fname,kind='line',highlight=False):
    f,(ax,wax) = plt.subplots(2,sharex=True,gridspec_kw={'height_ratios':[4.5,1]})

    if kind == 'line':
        line = ax.plot(df.index,df.values,color=fg_color)
        ax.fill_between(df.index,0,df.values,color=fg_color,alpha=0.8)

    if kind=='bar':
        bars = ax.bar(df.index,df.values,color=fg_color)
        # set ticks every week
        ax.xaxis.set_major_locator(mdates.WeekdayLocator())
        #set major ticks format
        ax.xaxis.set_major_formatter(mdates.DateFormatter('%b %d'))
        if highlight:
            [x.set_alpha(0.6) for x in bars[:-1]]
    ax.set_ylabel('Trips')

    wdf = vw.get_weather_range(df.index[0].strftime('%Y-%m'),
                                    (df.index[-1]-datetime.timedelta(1)).strftime('%Y-%m'))
    wdf = wdf.loc[[x for x in wdf.index if x in df.index],:]

    wax.bar(wdf.index,wdf['Total Precipmm'],color=fg_color2)
    wax2 = wax.twinx()
    #wax2 = set_ax_props(wax2)
    wax2.plot(wdf.index,wdf['Max Temp'],color=fg_color3)
    wax2.set_ylabel('High ($^\circ$C)')
    wax2.yaxis.label.set_color(fg_color3)
    wax.set_ylabel('Rain (mm)')
    wax.yaxis.label.set_color(fg_color2)
    wax.spines['right'].set_visible(True)
    wax2.spines['right'].set_visible(True)
    wax.tick_params(axis='x',labelrotation=45)
    wax2.tick_params(axis='x',labelrotation=45)

    f.tight_layout()
    f.savefig(fname)

    return f,ax

def cumsum_plot(df,fname):
    today = datetime.datetime.now().strftime('%Y-%m-%d')
    thisyear = datetime.datetime.now().strftime('%Y')
    f,ax = plt.subplots()
    def cumsum(df):

        df = df.sum(1).cumsum()
        df.index = df.index.map(lambda x: x.strftime('%H:%M'))
        df = df.shift()
        df.iloc[0] = 0
        return df

    for i,df in df.groupby(pd.Grouper(freq='d')):
        try:
            df = cumsum(df)
            ax.plot(df.index,df,color='gray',alpha=0.1)
        except IndexError:
            pass
    #print(df.head())
    ax.plot(df.index,df,alpha=1,color=fg_color)
    ax.scatter(df.index,df,alpha=1,color=fg_color)
    ax.set_ylabel("Hourly Cumulative Trips",color=ax_color)
    ax.set_xlabel("Time",color=ax_color)

    #year = df.index[-1].year
    #date = df.index[-1].date().strftime('%Y-%m-%d')
    gray_line = mlines.Line2D([], [], color=ax_color,  label="{} so far".format(thisyear))
    green_line= mlines.Line2D([], [], color=fg_color, marker='.',label="{}".format(today))
    ax.legend(handles=[green_line,gray_line])
    ax.tick_params(axis='x',labelrotation=45)
    f.tight_layout()
    f.savefig(fname)
    return f,ax

def make_station_map(date,fname,workingdir):

    #Load mobi daily data
    thdf = load_csv(workingdir+'/taken_hourly_df.csv')
    rhdf = load_csv(workingdir+'/returned_hourly_df.csv')
    tddf = thdf.groupby(pd.Grouper(freq='d')).sum()
    rddf = rhdf.groupby(pd.Grouper(freq='d')).sum()

    # Load stations geoDF
    sdf = get_stationsdf(workingdir)

    # Get day's trip counts
    addf = tddf + rddf
    addf = addf[sdf.loc[sdf['active'],'name']]
    trips = addf.loc[date].reset_index()
    trips.columns = ['name','trips']

    # make sure geoDF is on the left so it stays a geoDF
    sdf = pd.merge(sdf, trips, how='inner',on='name')

    bikeways_df = geopandas.read_file(f'{workingdir}/shapes/bikeways.shp')
    shoreline_df = geopandas.read_file(f'{workingdir}/shapes/shoreline2002.shp')

    f,ax = plt.subplots()
    bikeways_df.plot(ax=ax,color=fg_color)
    shoreline_df.plot(ax=ax,color='k')
    left = 485844
    right = 495513
    bottom = 5453579
    top = 5462500
    ax.axis('off')
    ax.set_xlim(left,right)
    ax.set_ylim(bottom,top)

    # Dummy scatters for the legend
    l1 = ax.scatter([0],[0], s=10, edgecolors='none',color=fg_color2,alpha=0.7)
    l2 = ax.scatter([0],[0], s=100, edgecolors='none',color=fg_color2,alpha=0.7)
    l3 = ax.scatter([0],[0], s=10, marker='x',edgecolors='none',color=fg_color4,alpha=0.7)

    labels=['0','10','100']
    ax.legend([l3,l1,l2],labels,title=f'Station Activity\n{date}')

    sdf.plot(ax=ax,markersize=list(sdf['trips']), alpha=0.7, zorder=100, color=fg_color2)

    sdf[sdf.trips==0].plot(ax=ax,color=fg_color4,alpha=0.7,markersize=10,marker='x',zorder=100)

    f.savefig(fname,bbox_inches='tight',pad_inches=0.0,transparent=False)

if __name__ == '__main__':
    weather_plot(tddf.sum(1).iloc[-365:],'./images/lastyear_daily.png',kind='line')
    weather_plot(tddf.sum(1)['2018-01':],'./images/2018-current_daily.png',kind='line')
    weather_plot(tddf.sum(1).iloc[-30:],'./images/lastmonth_daily.png',kind='bar',highlight=True)
    weather_plot(tddf.sum(1).loc[yday_min31:yday],'./images/lastmonth_daily_yesterday.png',kind='bar',highlight=True)

    simple_plot(thdf.sum(1).iloc[-7*24:-1],'./images/lastweek_hourly.png',kind='line')
    simple_plot(thdf.sum(1).loc[yday_min7:yday],'./images/lastweek_hourly_yesterday.png',kind='line')

    cumsum_plot(thdf[thisyear:],'./images/today_cumsum.png')
    cumsum_plot(thdf[thisyear:yday],imdir+'yesterday_cumsum.png')

    make_station_map(yday,imdir+'station_map_yesterday.png',workingdir)
    make_station_ani(yday,imdir+'station_ani_yesterday.gif',workingdir)

       # p = Plot().draw(thdf.sum(1).iloc[-7*24:-1],imdir+'lastweek_hourly.png',kind='line')
    #print("Drawing plots")
    #Plot().draw(tddf.sum(1),imdir+'alltime_rolling.png',weather=True,rolling=7)
    #p = Plot().draw(thdf.sum(1).loc[yday_min7:yday],imdir+'lastweek_hourly_yesterday.png')
    #p = Plot().draw(thdf[thisyear:],imdir+'today_cumsum.png',kind='cumsum')
    #p = Plot().draw(thdf[thisyear:yday],imdir+'yesterday_cumsum.png',kind='cumsum')
    #p = Plot().draw(tddf.sum(1).loc[yday_min31:yday],
    #            imdir+'lastmonth_daily_yesterday.png',
    #            kind='bar',weather=True,highlight=True)


# 문자열 word가 주어질 때, Dictionary를 활용해서 해당 문자열에서 등장한 모든 알파벳 개수를 구해서 출력하세요
# 딕셔너리 생성 후 i를 키값으로, 0을 밸류로 해서
#1씩 증가시킨다.

word = input()
d = {}
for i in word:
    d[i] = d.get(i, 0) + 1
for key, value in d.items():
    print(key, value)


import json
import logging
import os
from qgis.PyQt import QtWidgets
from qgis.PyQt import uic

# This loads your .ui file so that PyQt can populate your plugin with the elements from Qt Designer
from PyQt5 import QtCore
from PyQt5.QtCore import QVariant, QRunnable, pyqtSlot, QThreadPool
from PyQt5.QtGui import QIcon, QMovie
from PyQt5.QtWidgets import QComboBox, QHeaderView, QMessageBox, QCheckBox
from qgis._core import QgsMessageLog, Qgis, QgsFields, QgsField, QgsVectorLayer, QgsFeature, QgsGeometry, \
    QgsVectorFileWriter, QgsProject

from .kwg_sparqlquery import kwg_sparqlquery
from .kwg_sparqlutil import kwg_sparqlutil
from .kwg_util import kwg_util as UTIL

FORM_CLASS, _ = uic.loadUiType(os.path.join(
    os.path.dirname(__file__), 'kwg_plugin_enrichment_dialog_base.ui'))

class kwg_pluginEnrichmentDialog(QtWidgets.QDialog, FORM_CLASS):
    def __init__(self, parent=None, s2Cells=[], entityLi=[], spoDict={}, params={}):
        """Constructor."""
        super(kwg_pluginEnrichmentDialog, self).__init__(parent)
        # Set up the user interface from Designer through FORM_CLASS.
        # After self.setupUi() you can access any designer object by doing
        # self.<objectname>, and you can use autoconnect slots - see
        # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html
        # #widgets-and-dialogs-with-auto-connect
        self.setupUi(self)

        # logging
        self.logger = logging.getLogger()
        self.labelPropDict = dict()
        self.labelPropDict["LITERAL"] = "LITERAL"
        self.path = os.path.realpath(
            os.path.join(os.getcwd(), os.path.dirname(__file__)))

        if not os.path.exists(os.path.join(self.path, 'logs')):
            os.makedirs(os.path.join(self.path, 'logs'))
        handler = logging.FileHandler(os.path.join(self.path, 'logs', 'kwg_geoenrichment.log'), 'w+', 'utf-8')
        formatter = logging.Formatter(
            '%(asctime)s - %(levelname)s - %(filename)s:%(funcName)s - %(message)s')
        handler.setFormatter(formatter)  # Pass handler as a parameter, not assign
        self.logger.addHandler(handler)

        self.params = {}
        self.spoDict = spoDict
        self.s2Cells = s2Cells
        self.EntityLi = entityLi
        self.params=params
        if bool(params):
            self.ifaceObj = self.params["ifaceObj"]

        self.threadpool = QThreadPool()

        self.degreeCount = 0
        self.setUpTable()
        self.pushButton_learnMore.clicked.connect(self.addLearnMore)

        # displaying help
        self.displayingHelp = False
        self.setFixedWidth(620)
        self.plainTextEdit.setHidden(True)
        self.plainTextEdit.setReadOnly(True)

        self.toolButton.setIcon(QIcon(":/plugins/kwg_geoenrichment/resources/help-circle.png"))
        self.toolButton.setStyleSheet("background-color: transparent; color: #ffffff; border: none;")
        self.toolButton.setIconSize(QtCore.QSize(32, 32))

        self.movie = QMovie(":/plugins/kwg_geoenrichment/resources/loading.gif")
        self.loadingLabel.setMovie(self.movie)
        self.movie.start()
        self.loadingLabel.setScaledContents(True)
        self.loadingLabel.resize(27, 27)

        self.chosenVal = {}
        self.chosenVal["s"] = {}
        self.chosenVal["p"] = {}
        self.chosenVal["o"] = {}

        self.entitiesRetrieved = QCheckBox()
        self.entitiesRetrieved.setChecked(False)
        self.entitiesRetrieved.stateChanged.connect(lambda: self.populateFirstDegreeSubject())

        self.retrievingQuery = QCheckBox()
        self.retrievingQuery.setChecked(False)
        self.retrievingQuery.stateChanged.connect(lambda: self.queryStateHandler())

        self.toolButton.clicked.connect(self.displayHelp)

        self.sparql_query = kwg_sparqlquery()
        self.sparql_util = kwg_sparqlutil()

        if bool(self.spoDict):
            self.populateFirstDegreeSubject(spo=self.spoDict)

        stylesheet = os.path.join(self.path, 'style.qss')
        fo = open(stylesheet, "r")
        self.setStyleSheet(fo.read())
        fo.close()

    def setParams(self, params):
        self.params.update(params)
        self.ifaceObj = self.params["ifaceObj"]

    def queryStateHandler(self):
        worker = Worker(self.retrievingQuery, self.fetchingLabel, self.loadingLabel)
        self.threadpool.start(worker)

    def setUpTable(self):
        self.tableWidget.setColumnCount(3)
        self.tableWidget.verticalHeader().setVisible(False)
        self.tableWidget.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)

        self.tableWidget.horizontalHeader().hide()

        self.addLearnMore()

    def addLearnMore(self):

        # populate N degree subject based on the object property above
        if self.degreeCount > 0:
            finalObject = self.tableWidget.cellWidget(self.degreeCount - 1, 2).currentText()

            if finalObject is not None and finalObject != "--- SELECT ---" and finalObject != "LITERAL":
                self.tableWidget.insertRow(self.degreeCount)

                comboBox_S = QComboBox()
                comboBox_P = QComboBox()
                comboBox_O = QComboBox()

                self.tableWidget.setCellWidget(self.degreeCount, 0, comboBox_S)
                self.tableWidget.setCellWidget(self.degreeCount, 1, comboBox_P)
                self.tableWidget.setCellWidget(self.degreeCount, 2, comboBox_O)

                comboBox_S.show()
                comboBox_P.show()
                comboBox_O.show()

                self.tableWidget.cellWidget(self.degreeCount, 0).clear()
                self.tableWidget.cellWidget(self.degreeCount, 0).addItem("--- SELECT ---")
                self.spoDict[self.degreeCount] = {}
                self.spoDict[self.degreeCount]["s"] = {}
                for key in self.spoDict[self.degreeCount - 1]["o"]:
                    self.tableWidget.cellWidget(self.degreeCount, 0).addItem(self.updateLabelPropDict(self.sparql_util.make_prefixed_iri(key)))
                    self.spoDict[self.degreeCount]["s"][key] = self.spoDict[self.degreeCount - 1]["o"][key]
                index = self.tableWidget.cellWidget(self.degreeCount - 1, 2).findText(finalObject, QtCore.Qt.MatchFixedString)

                if index >= 0:
                    self.tableWidget.cellWidget(self.degreeCount, 0).setCurrentIndex(index)
                self.populateNDegreePredicate()
                self.degreeCount += 1
            else:
                msg = QMessageBox()
                msg.setIcon(QMessageBox.Warning)

                msg.setText("Can't add more content...")
                msg.setInformativeText("'LITERAL' data encountered!")
                msg.setWindowTitle("Learn More Warning!")
                msg.exec_()
        else:
            self.tableWidget.insertRow(self.degreeCount)

            comboBox_S = QComboBox()
            comboBox_P = QComboBox()
            comboBox_O = QComboBox()

            self.tableWidget.setCellWidget(self.degreeCount, 0, comboBox_S)
            self.tableWidget.setCellWidget(self.degreeCount, 1, comboBox_P)
            self.tableWidget.setCellWidget(self.degreeCount, 2, comboBox_O)

            comboBox_S.show()
            comboBox_P.show()
            comboBox_O.show()

            self.tableWidget.cellWidget(self.degreeCount, 0).clear()

            self.degreeCount += 1

    def execute(self):
        # manage first degree
        self.results = {}
        self.retrievingQuery.setChecked(True)

        try:
            self.tableWidget.cellWidget(0, 0).currentIndexChanged.disconnect()
        except TypeError:
            pass
        self.tableWidget.cellWidget(0, 0).clear()
        entityBindingObject = []

        for wkt in self.params["wkt_literal"]:
            entityBindingObject.extend(self.sparql_query.getEntityValuesFromGeometry(sparql_endpoint=self.params["end_point"],
                                                                           wkt_literal=wkt))

        for obj in entityBindingObject:
            try:
                self.EntityLi.append(obj["entity"]["value"])
            except Exception as e:
                continue

        self.entitiesRetrieved.setChecked(True)
        self.retrievingQuery.setChecked(False)
        # self.populateFirstDegreeSubject()
        return

    def get_results(self):
        return self.results

    def get_s2Cells(self):
        return self.s2Cells

    def get_entityLi(self):
        return self.EntityLi

    def get_spoDict(self):
        spo = {}
        spo[0] = {}
        spo[0]["s"] = self.spoDict[0]["s"]
        return spo

    def populateFirstDegreeSubject(self, spo={}):
        # self.entitiesRetrieved.stateChanged.disconnect()
        try:
            self.tableWidget.cellWidget(0, 0).currentIndexChanged.disconnect()
        except TypeError:
            pass
        self.retrievingQuery.setChecked(True)
        self.tableWidget.cellWidget(0, 0).clear()
        self.tableWidget.cellWidget(0, 0).addItem("--- SELECT ---")

        if not bool(spo):
            classObject = self.sparql_query.getFirstDegreeClass(sparql_endpoint=self.params["end_point"],
                                                                entityList=self.EntityLi)

            # populate the spoDict
            self.spoDict[0] = {}
            self.spoDict[0]["s"] = {}
            for obj in classObject:
                self.spoDict[0]["s"][obj["type"]["value"]] = self.sparql_util.make_prefixed_iri(obj["label"]["value"])
        else:
            self.spoDict[0] = spo[0]

        for key in self.spoDict[0]["s"]:
            self.tableWidget.cellWidget(0, 0).addItem(self.updateLabelPropDict(self.sparql_util.make_prefixed_iri(key)))
        self.retrievingQuery.setChecked(False)
        self.tableWidget.cellWidget(0, 0).currentIndexChanged.connect(lambda: self.populateFirstDegreePredicate())

    def handleError(self, errCode = ""):
        QgsMessageLog.logMessage("error occured " + errCode, "kwg_geoernichment", Qgis.Info)

    def populateFirstDegreePredicate(self):
        self.tableWidget.cellWidget(0, 1).clear()
        self.tableWidget.cellWidget(0, 1).addItem("--- SELECT ---")
        self.retrievingQuery.setChecked(True)

        self.sub0 = self.labelPropDict[self.tableWidget.cellWidget(0, 0).currentText()]
        self.chosenVal["s"][0] = self.sub0
        predicateObject = self.sparql_query.getFirstDegreePredicate(sparql_endpoint=self.params["end_point"],
                                                                    entityList=self.EntityLi,
                                                                    firstDegreeClass=self.sub0)

        # populate the spoDict
        self.spoDict[0]["p"] = {}
        for obj in predicateObject:
            if "label" in obj:
                self.spoDict[0]["p"][obj["p"]["value"]] = self.sparql_util.make_prefixed_iri(obj["label"]["value"])
            else:
                self.spoDict[0]["p"][obj["p"]["value"]] = ""

        for key in self.spoDict[0]["p"]:
            self.tableWidget.cellWidget(0, 1).addItem(self.updateLabelPropDict(self.sparql_util.make_prefixed_iri(key)))

        self.retrievingQuery.setChecked(False)
        self.tableWidget.cellWidget(0, 1).currentIndexChanged.connect(self.populateFirstDegreeObject)
        return

    def populateFirstDegreeObject(self):
        self.pred0 = self.labelPropDict[self.tableWidget.cellWidget(0, 1).currentText()]
        self.chosenVal["p"][0] = self.pred0
        self.tableWidget.cellWidget(0, 2).clear()
        self.tableWidget.cellWidget(0, 2).addItem("--- SELECT ---")
        self.retrievingQuery.setChecked(True)

        firstDegreeObject = self.sparql_query.getFirstDegreeObject(sparql_endpoint=self.params["end_point"],
                                                                   entityList=self.EntityLi,
                                                                   firstDegreeClass=self.sub0,
                                                                   firstDegreePredicate=self.pred0)

        self.logger.info("firstDegreeObject : ")
        self.logger.info(json.dumps(firstDegreeObject))

        # populate the spoDict
        self.spoDict[0]["o"] = {}
        for obj in firstDegreeObject:
            if "label" in obj:
                self.spoDict[0]["o"][obj["type"]["value"]] = self.sparql_util.make_prefixed_iri(obj["label"]["value"])
            else:
                self.spoDict[0]["o"][obj["type"]["value"]] = ""

        for key in self.spoDict[0]["o"]:
            self.tableWidget.cellWidget(0, 2).addItem(self.updateLabelPropDict(self.sparql_util.make_prefixed_iri(key)))

        self.retrievingQuery.setChecked(False)
        if not self.spoDict[0]["o"]:
            self.tableWidget.cellWidget(0, 2).addItem("LITERAL")

    def populateNDegreePredicate(self):
        i = self.degreeCount
        self.tableWidget.cellWidget(i, 1).clear()
        self.tableWidget.cellWidget(i, 1).addItem("--- SELECT ---")
        selectedVal = []
        for it in range(i+1):
            selectedVal.append(self.labelPropDict[self.tableWidget.cellWidget(it, 0).currentText()])
            if it < i:
                selectedVal.append(self.labelPropDict[self.tableWidget.cellWidget(it, 1).currentText()])
        # secondPropertyURLList.extend(self.getSecondDegreeProperty())
        secondPredObj = self.sparql_query.getNDegreePredicate(sparql_endpoint=self.params["end_point"],
                                                              entityList=self.EntityLi,
                                                              selectedVals=selectedVal,
                                                              degree=i + 1)

        # populate the spoDict
        self.spoDict[i]["p"] = {}
        for obj in secondPredObj:
            if "label" in obj:
                self.spoDict[i]["p"][obj["p"]["value"]] = self.sparql_util.make_prefixed_iri(obj["label"]["value"])
            else:
                self.spoDict[i]["p"][obj["p"]["value"]] = ""

        for key in self.spoDict[i]["p"]:
            self.tableWidget.cellWidget(i, 1).addItem(self.updateLabelPropDict(self.sparql_util.make_prefixed_iri(key)))
        self.tableWidget.cellWidget(i, 1).currentIndexChanged.connect(self.populateNDegreeObject)
        return

    def populateNDegreeObject(self):
        i = self.degreeCount
        self.tableWidget.cellWidget(i - 1, 2).clear()
        self.tableWidget.cellWidget(i - 1, 2).addItem("--- SELECT ---")

        selectedVal = []
        for it in range(i):
            selectedVal.append(self.labelPropDict[self.tableWidget.cellWidget(it, 0).currentText()])
            selectedVal.append(self.labelPropDict[self.tableWidget.cellWidget(it, 1).currentText()])
        # secondPropertyURLList.extend(self.getSecondDegreeProperty())
        secondPropObj = self.sparql_query.getNDegreeObject(sparql_endpoint=self.params["end_point"],
                                                           entityList=self.EntityLi,
                                                           selectedVals=selectedVal,
                                                           degree=i)

        # populate the spoDict
        self.spoDict[i - 1]["o"] = {}
        for obj in secondPropObj:
            if "label" in obj:
                self.spoDict[i - 1]["o"][obj["type"]["value"]] = self.sparql_util.make_prefixed_iri(obj["label"]["value"])
            else:
                self.spoDict[i - 1]["o"][obj["type"]["value"]] = ""

        for key in self.spoDict[i - 1]["o"]:
            self.tableWidget.cellWidget(i - 1, 2).addItem(self.updateLabelPropDict(self.sparql_util.make_prefixed_iri(key)))

        if not self.spoDict[i - 1]["o"]:
            self.tableWidget.cellWidget(i - 1, 2).addItem("LITERAL")

    def getDegree(self):
        return self.degreeCount

    def getResults(self):

        i = self.degreeCount
        selectedVal = []
        for it in range(i):
            subject = self.labelPropDict[self.tableWidget.cellWidget(it, 0).currentText()]
            predicate = self.labelPropDict[self.tableWidget.cellWidget(it, 1).currentText()]
            if subject is not None and subject != "--- SELECT ---" and subject != "LITERAL":
                selectedVal.append(subject)
            if predicate is not None and predicate != "--- SELECT ---" and predicate != "LITERAL":
                selectedVal.append(predicate)

        finalObject = self.labelPropDict[self.tableWidget.cellWidget(i-1, 2).currentText()]
        if finalObject is not None and finalObject != "--- SELECT ---" and finalObject != "LITERAL":
            selectedVal.append(finalObject)

        self.logger.debug("selectedVal + " + str(selectedVal))

        finalResultsObj = self.sparql_query.getNDegreeResults(sparql_endpoint=self.params["end_point"],
                                                           entityList=self.EntityLi,
                                                           selectedVals=selectedVal,
                                                           degree=i)

        self.logger.debug(str(len(finalResultsObj)))
        return finalResultsObj

    def displayHelp(self):
        if self.displayingHelp:
            self.displayingHelp = False
            self.plainTextEdit.setHidden(True)
            self.setFixedWidth(620)
        else:
            self.displayingHelp = True
            self.plainTextEdit.setVisible(True)
            self.setFixedWidth(900)

    def updateLabelPropDict(self, label):
        newLabel = ""
        if ":" in label:
            newLabel = (label.split(":")[1]).strip()
        else:
            newLabel = label
        self.labelPropDict[newLabel] = label
        return newLabel

class Worker(QRunnable, ):

    def __init__(self, retrievingQuery, fetchingLabel, loadingLabel):
        super(Worker, self).__init__()
        self.retrievingQuery = retrievingQuery
        self.loadingLabel = loadingLabel
        self.fetchingLabel = fetchingLabel

    @pyqtSlot()
    def run(self):
        '''
        Runnable code for executing S2 cells query
        '''
        if (self.retrievingQuery.isChecked()):
            # QgsMessageLog.logMessage("fetching...", "kwg_geoenrichment", Qgis.Info)
            self.fetchingLabel.show()
            self.loadingLabel.show()
        else:
            # QgsMessageLog.logMessage("done!", "kwg_geoenrichment", Qgis.Info)
            self.fetchingLabel.hide()
            self.loadingLabel.hide()