{
    "slots":{
        "0":{"name":"agScreen","type":{"events":[],"methods":[]}},
        "1":{"name":"slot2","type":{"events":[],"methods":[]}},
        "2":{"name":"slot3","type":{"events":[],"methods":[]}},
        "3":{"name":"slot4","type":{"events":[],"methods":[]}},
        "4":{"name":"slot5","type":{"events":[],"methods":[]}},
        "5":{"name":"slot6","type":{"events":[],"methods":[]}},
        "6":{"name":"slot7","type":{"events":[],"methods":[]}},
        "7":{"name":"slot8","type":{"events":[],"methods":[]}},
        "8":{"name":"slot9","type":{"events":[],"methods":[]}},
        "9":{"name":"slot10","type":{"events":[],"methods":[]}},
        "-1":{"name":"unit","type":{"events":[],"methods":[]}},
        "-2":{"name":"system","type":{"events":[],"methods":[]}},
        "-3":{"name":"library","type":{"events":[],"methods":[]}}
    },
    "handlers":[
        {"code":"----------------------------\n-- Antigravity Controller --\n--   By W3asel (1337joe)  --\n----------------------------\n-- Bundled: 2022-05-25T20:07:44Z\n-- Latest version always available here: https://github.com/1337joe/du-ship-displays\n\n-- container for shared state for anti-grav controller\n_G.agController = {}\n\n-------------------------\n-- Begin Configuration --\n-------------------------\nlocal agUpdateFrequency = 10 --export: Antigravity data/screen update rate (Hz)\n\n-- slot definitions\n_G.agController.slots = {}\n_G.agController.slots.core = core -- if not found by name will autodetect\n_G.agController.slots.antigrav = agg -- if not found by name will autodetect\n_G.agController.slots.screen = agScreen -- if not found by name will autodetect\n_G.agController.slots.databank = databank -- if not found by name will autodetect\n\nlocal agMinAltitude = 1000 --export: Min altitude to allow setting on anti-grav (m), raise this if you don't want a non-default lower limit.\nlocal agMinG = 0.1 --export: Below this value of g no altitude or vertical velocity will be reported.\n\n-----------------------\n-- End Configuration --\n-----------------------\n\n-- link missing slot inputs / validate provided slots\nlocal slots = _G.agController.slots\nlocal module = \"antigrav\"\nslots.screen = _G.Utilities.loadSlot(slots.screen, \"ScreenUnit\", nil, module, \"screen\")\nslots.screen.activate()\nslots.antigrav = _G.Utilities.loadSlot(slots.antigrav, \"AntiGravityGeneratorUnit\", slots.screen, module, \"antigrav\")\nslots.core = _G.Utilities.loadSlot(slots.core, \"CoreUnitDynamic\", slots.screen, module, \"core\")\nslots.databank = _G.Utilities.loadSlot(slots.databank, \"DataBankUnit\", slots.screen, module, \"databank\", true,\n                     \"No databank found, controller state will not persist between sessions.\")\n\n-- hide widgets\nunit.hide()\n\nlocal core = _G.agController.slots.core\nlocal antigrav = _G.agController.slots.antigrav\nlocal databank = _G.agController.slots.databank\n\n-- load preferences, either from databank or exported parameters\nlocal AG_UPDATE_FREQUENCY_KEY = \"AG.unit:UPDATE_FREQUENCY\"\nlocal AG_UPDATE_FREQUENCY = _G.Utilities.getPreference(databank, AG_UPDATE_FREQUENCY_KEY, agUpdateFrequency)\nlocal MIN_AG_ALTITUDE_KEY = \"AG.unit:MIN_AG_ALTITUDE\"\nlocal MIN_AG_ALTITUDE = _G.Utilities.getPreference(databank, MIN_AG_ALTITUDE_KEY, agMinAltitude)\nlocal MIN_AG_G_KEY = \"AG.unit:MIN_AG_G\"\nlocal MIN_AG_G = _G.Utilities.getPreference(databank, MIN_AG_G_KEY, agMinG)\n\nlocal TARGET_ALTITUDE_KEY = \"AntigravTargetAltitude\"\n\nlocal vec3 = require(\"cpml.vec3\")\nlocal planetReference0 = PlanetaryReference(_G.atlas)[0]\nlocal piHalf = math.pi / 2\n\n-- declare methods\n--- Compute vertical velocity by projecting world velocity onto world vertical vector\nlocal function calculateVertVel(core)\n    local verticalVelocity\n\n    -- compute vertical velocity by projecting world velocity onto world vertical vector\n    local vel = vec3.new(core.getWorldVelocity())\n    local vert = vec3.new(core.getWorldVertical())\n    local verticalVelocity = vel:project_on(vert):len()\n\n    -- add sign\n    if vel:angle_between(vert) < piHalf then\n        verticalVelocity = -1 * verticalVelocity\n    end\n\n    return verticalVelocity\nend\n\nfunction _G.agController:updateState()\n    if databank then\n        self.targetAltitude = databank.getFloatValue(TARGET_ALTITUDE_KEY)\n    end\n\n    self.currentAltitude = core.getAltitude()\n\n    if self.currentAltitude == 0 then\n        local coreWorldPos = core.getConstructWorldPos()\n        local closestBody = planetReference0:closestBody(coreWorldPos)\n\n        -- core.g() is thrown off by the activity of the antigravity generator\n        if closestBody:getGravity(coreWorldPos):len() < MIN_AG_G then\n            self.verticalVelocity = 0 / 0 -- nan\n            self.currentAltitude = 0 / 0 -- nan\n        else\n            -- calculate altitude from position\n            self.currentAltitude = closestBody:getAltitude(coreWorldPos)\n\n            self.verticalVelocity = calculateVertVel(core)\n        end\n    else\n        self.verticalVelocity = calculateVertVel(core)\n    end\n\n    self.agState = antigrav.getState() == 1\n    local data = antigrav.getData()\n    self.baseAltitude = antigrav.getBaseAltitude()\n    self.agField = tonumber(string.match(data, \"\\\"antiGravityField\\\":([%d.-]+)\"))\n    self.agPower = tonumber(string.match(data, \"\\\"antiGPower\\\":([%d.-]+)\"))\n\n    -- signal draw of screen with updated state\n    _G.agScreenController.needRefresh = true\nend\n\nfunction _G.agController:setBaseAltitude(target)\n    if target < MIN_AG_ALTITUDE then\n        target = MIN_AG_ALTITUDE\n    end\n\n    self.targetAltitude = target\n    self.slots.antigrav.setBaseAltitude(target)\n\n    if databank then\n        databank.setFloatValue(TARGET_ALTITUDE_KEY, target)\n    end\n\n    self:updateState()\nend\n\nfunction _G.agController:setAgState(newState)\n    if newState == self.agState then\n        return\n    end\n\n    local state\n    if newState then\n        self.slots.antigrav.activate()\n    else\n        self.slots.antigrav.deactivate()\n    end\n\n    self:updateState()\nend\n\n-- init screen\n_G.agScreenController:init(_G.agController)\n\n-- init stored values\nif databank and databank.hasKey(TARGET_ALTITUDE_KEY) == 1 then\n    _G.agController:setBaseAltitude(databank.getFloatValue(TARGET_ALTITUDE_KEY))\nelse\n    _G.agController:setBaseAltitude(antigrav.getBaseAltitude())\nend\n\n_G.agController:updateState()\n\n-- schedule updating\nunit.setTimer(\"updateAg\", 1 / AG_UPDATE_FREQUENCY)\n","filter":{"args":[],"signature":"start()","slotKey":"-1"},"key":"0"},
        {"code":"_G.agController:updateState()","filter":{"args":[{"value":"updateAg"}],"signature":"tick(timerId)","slotKey":"-1"},"key":"1"},
        {"code":"--- Run first, define agScreenController basic functionality: SVG-specific definitions and functions are not included.\n\n-- constants and editable lua script parameters\nlocal MIN_ADJUSTMENT_VALUE = 1\n\nlocal agMaxAdjustmentValue = 10000 --export: Max step size for altitude adjustment (m)\nlocal MAX_ADJUSTMENT_PREF_KEY = \"AG.screen:MAX_ADJUSTMENT_VALUE\"\n\nlocal agUseKMpH = true --export: True for km/h, false for m/s\nlocal USE_KMPH_PREF_KEY = \"AG.screen:USE_KMPH\"\nlocal MPS_TO_MPH = 3600\n\nlocal ALTITUDE_ADJUST_KEY = \"altitudeAdjustment\"\n\n-- initialize object and fields\n_G.agScreenController = {\n    mouse = {\n        x = -1,\n        y = -1,\n        pressed = nil,\n        state = false\n    },\n    locked = false,\n    needRefresh = false,\n}\n\nfunction _G.agScreenController:init(controller)\n    self.controller = controller\n    self.screen = controller.slots.screen\n    self.databank = controller.slots.databank\n\n    self.MAX_ADJUSTMENT_VALUE = _G.Utilities.getPreference(self.databank, MAX_ADJUSTMENT_PREF_KEY, agMaxAdjustmentValue, true)\n    self.USE_KMPH = _G.Utilities.getPreference(self.databank, USE_KMPH_PREF_KEY, agUseKMpH)\n\n    if self.databank and self.databank.hasKey(ALTITUDE_ADJUST_KEY) == 1 then\n        self:setAltitudeAdjust(self.databank.getIntValue(ALTITUDE_ADJUST_KEY))\n    else\n        self:setAltitudeAdjust(1000)\n    end\nend\n\n--- Handle a mouse down event at the provided coordinates.\nfunction _G.agScreenController:mouseDown(x, y)\n    self.mouse.x = x\n    self.mouse.y = y\n    self.mouse.pressed = _G.ScreenUtils.detectButton(self.buttonCoordinates, x, y)\nend\n\n--- Handle a mouse up event at the provided coordinates.\nfunction _G.agScreenController:mouseUp(x, y)\n    local released = _G.ScreenUtils.detectButton(self.buttonCoordinates, x, y)\n    if released and self.mouse.pressed == released then\n        local modified = self:handleButton(released)\n        self.needRefresh = self.needRefresh or modified\n    end\n    self.mouse.pressed = nil\nend\n\nfunction _G.agScreenController:setAltitudeAdjust(newAdjust)\n    if newAdjust < MIN_ADJUSTMENT_VALUE then\n        newAdjust = MIN_ADJUSTMENT_VALUE\n    elseif newAdjust > self.MAX_ADJUSTMENT_VALUE then\n        newAdjust = self.MAX_ADJUSTMENT_VALUE\n    end\n\n    if self.altitudeAdjustment == newAdjust then\n        return false\n    end\n    self.altitudeAdjustment = newAdjust\n\n    if self.databank then\n        self.databank.setIntValue(ALTITUDE_ADJUST_KEY, newAdjust)\n    end\n    return true\nend\n\n--- Returns vertical velocity in the user-chosen units.\nfunction _G.agScreenController:getVerticalVelocity()\n    local verticalVelocity, verticalUnits\n    if self.USE_KMPH then\n        local mph = self.controller.verticalVelocity * MPS_TO_MPH\n        local lessThan = math.abs(mph) < 1000\n        if lessThan then\n            mph = 1000 * math.abs(mph) / mph\n        end\n        verticalVelocity, verticalUnits = _G.Utilities.printableNumber(mph, \"m/h\")\n        if lessThan then\n            verticalVelocity = \"<1.0\"\n        end\n    else\n        verticalVelocity, verticalUnits = _G.Utilities.printableNumber(self.controller.verticalVelocity, \"m/s\")\n    end\n    return verticalVelocity, verticalUnits\nend\n\n--- Render the interface to the screen.\nfunction _G.agScreenController:refresh()\n    assert(false, \"Should be overridden by SVG-specific method.\")\nend\n\n--- Processes the input indicated by the provided button id.\n-- @treturn boolean True if the state was changed by this action.\nfunction _G.agScreenController:handleButton(buttonId)\n    assert(false, \"Should be overridden by SVG-specific method.\")\nend\n","filter":{"args":[],"signature":"start()","slotKey":"0"},"key":"2"},
        {"code":"--- Run second, define agScreenController SVG-specific functionality: rendering, buttons, etc\n\n-- constants for svg file\nlocal MOUSE_OVER_CLASS = \"mouseOver\"\nlocal HIDDEN_CLASS = \"hidden\"\nlocal PANEL_CLASS_ADJUSTMENT = \"adjustmentWidgets\"\nlocal PANEL_CLASS_STATUS = \"statusWidgets\"\nlocal ELEMENT_CLASS_UNLOCKED_BUTTON = \"unlockedClass\"\nlocal ELEMENT_CLASS_UNLOCKING_LABEL = \"unlockSlideClass\"\nlocal ELEMENT_CLASS_LOCKED_BUTTON = \"lockedClass\"\nlocal ELEMENT_CLASS_ALTITUDE_UP = \"altitudeUpClass\"\nlocal ELEMENT_CLASS_ALTITUDE_DOWN = \"altitudeDownClass\"\nlocal ELEMENT_CLASS_ADJUST_UP = \"adjustUpClass\"\nlocal ELEMENT_CLASS_ADJUST_DOWN = \"adjustDownClass\"\nlocal ELEMENT_CLASS_LEFT_SLIDER = \"leftSliderClass\"\nlocal ELEMENT_CLASS_RIGHT_SLIDER = \"rightSliderClass\"\nlocal ELEMENT_CLASS_DISABLED = \"disabledText\"\nlocal ELEMENT_CLASS_NEED_PULSORS = \"pulsorsText\"\nlocal ELEMENT_CLASS_NO_POWER_WARNING = \"noPowerWarning\"\nlocal ELEMENT_CLASS_POWER_IS_OFF = \"powerIsOffClass\"\nlocal ELEMENT_CLASS_POWER_IS_ON = \"powerIsOnClass\"\nlocal ELEMENT_CLASS_POWER_SLIDER = \"powerSlideClass\"\n\nlocal ALTITUDE_ADJUST_KEY = \"altitudeAdjustment\"\n\n-- constant button definition labels\n_G.agScreenController.BUTTON_ALTITUDE_UP = \"Altitude Up\"\n_G.agScreenController.BUTTON_ALTITUDE_DOWN = \"Altitude Down\"\n_G.agScreenController.BUTTON_ALTITUDE_ADJUST_UP = \"Altitude Adjust Up\"\n_G.agScreenController.BUTTON_ALTITUDE_ADJUST_DOWN = \"Altitude Adjust Down\"\n_G.agScreenController.BUTTON_TARGET_ALTITUDE_SLIDER = \"Target Altitude Slider\"\n_G.agScreenController.BUTTON_MATCH_CURRENT_ALTITUDE = \"Match Current Altitude\"\n_G.agScreenController.BUTTON_LOCK = \"Lock\"\n_G.agScreenController.BUTTON_UNLOCK = \"Unlock\"\n_G.agScreenController.BUTTON_POWER_OFF = \"Power Off\"\n_G.agScreenController.BUTTON_POWER_ON = \"Power On\"\n\n-- both sliders on same level, pre-compute y ranges with 5% buffer\n_G.agScreenController.sliderYMin = nil -- override with SVG-specific value\n_G.agScreenController.sliderYMax = nil -- override with SVG-specific value\n\nfunction _G.agScreenController.calculateSliderIndicator(altitude)\n    assert(false, \"Should be overridden by SVG-specific method.\")\nend\n\nfunction _G.agScreenController.calculateSliderAltitude(indicatorY)\n    assert(false, \"Should be overridden by SVG-specific method.\")\nend\n\nfunction _G.agScreenController:refresh()\n    -- refresh conditions: needRefresh, mouse down\n    if not (self.needRefresh or self.mouse.pressed) then\n        return\n    end\n    self.needRefresh = false\n\n    -- update mouse position for tracking drags\n    self.mouse.x = self.screen.getMouseX()\n    self.mouse.y = self.screen.getMouseY()\n    self.mouse.state = self.screen.getMouseState() == 1\n    -- if mouse has left screen remove pressed flag\n    if self.mouse.x < 0 then\n        self.mouse.pressed = nil\n    end\n\n    local html = self.SVG_TEMPLATE\n\n    -- track mouse drags\n    if self.locked and self.mouse.pressed == _G.agScreenController.BUTTON_UNLOCK then\n        -- if unlocking then check mouse against bounds of slide bar\n        if not self.mouse.state or self.mouse.y < _G.agScreenController.sliderYMin or self.mouse.y > _G.agScreenController.sliderYMax then\n            self.mouse.pressed = nil\n        elseif self.mouse.x > self.buttonCoordinates[_G.agScreenController.BUTTON_LOCK].x1 then\n            self.mouse.pressed = nil\n            self.locked = false\n        else\n            html = _G.ScreenUtils.replaceClass(html, ELEMENT_CLASS_UNLOCKING_LABEL, \"\")\n            html = string.gsub(html, \"(id=\\\"locked\\\" x=)\\\"%d+\", \"%1\\\"\" .. (self.mouse.x * 1920))\n        end\n    elseif self.controller.agState and self.mouse.pressed == _G.agScreenController.BUTTON_POWER_OFF then\n        -- if powering off then check mouse against bounds of slide bar\n        if not self.mouse.state or self.mouse.y < _G.agScreenController.sliderYMin or self.mouse.y > _G.agScreenController.sliderYMax then\n            self.mouse.pressed = nil\n        elseif self.mouse.x < self.buttonCoordinates[_G.agScreenController.BUTTON_POWER_ON].x2 then\n            self.mouse.pressed = nil\n            self.controller:setAgState(false)\n        else\n            html = _G.ScreenUtils.replaceClass(html, ELEMENT_CLASS_POWER_SLIDER, \"\")\n            html = string.gsub(html, \"(id=\\\"power\\\" x=)\\\"%d+\", \"%1\\\"\" .. (self.mouse.x * 1920))\n        end\n    elseif not self.locked and self.mouse.pressed == _G.agScreenController.BUTTON_TARGET_ALTITUDE_SLIDER then\n        -- if dragging altitude track mouse\n        if not self.mouse.state then\n            self.mouse.pressed = nil\n        else\n            local target = _G.agScreenController.calculateSliderAltitude(self.mouse.y)\n\n            self.controller:setBaseAltitude(target)\n        end\n    end\n\n    -- extract values to show in svg\n    local targetAltitude = self.controller.targetAltitude\n\n    local targetAltitudeString\n    if self.locked then\n        targetAltitudeString = math.floor(targetAltitude)\n    else\n        targetAltitudeString = \"\"\n        local targetAltitudeRemainder = targetAltitude\n        local adjustmentRemainder = self.altitudeAdjustment\n        while targetAltitudeRemainder > 0 or adjustmentRemainder > 0 do\n            local nextDigit = math.floor(targetAltitudeRemainder % 10)\n            if adjustmentRemainder == 1 then\n                targetAltitudeString = string.format('<tspan class=\"adjust\">%d</tspan>%s',\n                    nextDigit, targetAltitudeString)\n            else\n                targetAltitudeString = nextDigit .. targetAltitudeString\n            end\n            targetAltitudeRemainder = math.floor(targetAltitudeRemainder / 10)\n            adjustmentRemainder = math.floor(adjustmentRemainder / 10)\n        end\n    end\n\n    local baseAltitude = math.floor(self.controller.baseAltitude)\n    local verticalVelocity, verticalUnits = self:getVerticalVelocity()\n    local currentAltitude = math.floor(self.controller.currentAltitude + 0.5)\n    local agPower = math.floor(self.controller.agPower * 100 + 0.5)\n    local agField = math.floor(self.controller.agField * 100 + 0.5)\n    local targetAltitudeSliderHeight = _G.agScreenController.calculateSliderIndicator(targetAltitude)\n    local currentAltitudeSliderHeight\n    if currentAltitude ~= currentAltitude then -- test for nan\n        currentAltitude = \"N/A\"\n        currentAltitudeSliderHeight = -1000\n        verticalVelocity = \"N/A\"\n        verticalUnits = \"\"\n    elseif currentAltitude <= 0 then\n        -- breaks log scale\n        currentAltitudeSliderHeight = -1000\n    else\n        currentAltitudeSliderHeight = _G.agScreenController.calculateSliderIndicator(currentAltitude)\n    end\n    local baseAltitudeSliderHeight = _G.agScreenController.calculateSliderIndicator(baseAltitude)\n\n    -- insert values to svg and render\n    html = _G.Utilities.sanitizeFormatString(html)\n    html = string.format(html, currentAltitudeSliderHeight, targetAltitudeSliderHeight, baseAltitudeSliderHeight,\n        targetAltitudeString, baseAltitude, verticalVelocity, verticalUnits,\n               currentAltitude, agField, agPower)\n\n    -- adjust visibility for state\n    -- controls locked\n    if self.locked then\n        html = _G.ScreenUtils.replaceClass(html, PANEL_CLASS_ADJUSTMENT, HIDDEN_CLASS)\n        html = _G.ScreenUtils.replaceClass(html, ELEMENT_CLASS_UNLOCKED_BUTTON, HIDDEN_CLASS)\n    else\n        html = _G.ScreenUtils.replaceClass(html, ELEMENT_CLASS_LOCKED_BUTTON, HIDDEN_CLASS)\n    end\n    -- AG power/error state\n    if not self.controller.agState then\n        -- powered off\n        html = _G.ScreenUtils.replaceClass(html, ELEMENT_CLASS_POWER_IS_ON, HIDDEN_CLASS)\n        html = _G.ScreenUtils.replaceClass(html, PANEL_CLASS_STATUS, HIDDEN_CLASS)\n        html = _G.ScreenUtils.replaceClass(html, ELEMENT_CLASS_DISABLED, \"\")\n    else\n        -- powered on\n        html = _G.ScreenUtils.replaceClass(html, ELEMENT_CLASS_POWER_IS_OFF, HIDDEN_CLASS)\n\n        if agField <= 50 then -- use rounded number from display\n            -- insufficient pulsors\n            html = _G.ScreenUtils.replaceClass(html, PANEL_CLASS_STATUS, HIDDEN_CLASS)\n            html = _G.ScreenUtils.replaceClass(html, ELEMENT_CLASS_NEED_PULSORS, \"\")\n        elseif agPower <= 0 then -- use rounded number from display\n            html = _G.ScreenUtils.replaceClass(html, ELEMENT_CLASS_NO_POWER_WARNING, \"\")\n        end\n    end\n\n    if not self.mouse.pressed then\n        -- add mouse-over highlights\n        local mouseOver, _ = _G.ScreenUtils.detectButton(self.buttonCoordinates, self.mouse.x, self.mouse.y)\n        if mouseOver == _G.agScreenController.BUTTON_ALTITUDE_ADJUST_UP then\n            html = _G.ScreenUtils.replaceClass(html, ELEMENT_CLASS_ADJUST_UP, MOUSE_OVER_CLASS)\n        elseif mouseOver == _G.agScreenController.BUTTON_ALTITUDE_ADJUST_DOWN then\n            html = _G.ScreenUtils.replaceClass(html, ELEMENT_CLASS_ADJUST_DOWN, MOUSE_OVER_CLASS)\n        elseif mouseOver == _G.agScreenController.BUTTON_ALTITUDE_UP then\n            html = _G.ScreenUtils.replaceClass(html, ELEMENT_CLASS_ALTITUDE_UP, MOUSE_OVER_CLASS)\n        elseif mouseOver == _G.agScreenController.BUTTON_ALTITUDE_DOWN then\n            html = _G.ScreenUtils.replaceClass(html, ELEMENT_CLASS_ALTITUDE_DOWN, MOUSE_OVER_CLASS)\n        elseif not self.locked and mouseOver == _G.agScreenController.BUTTON_TARGET_ALTITUDE_SLIDER then\n            html = _G.ScreenUtils.replaceClass(html, ELEMENT_CLASS_LEFT_SLIDER, MOUSE_OVER_CLASS)\n        elseif not self.locked and mouseOver == _G.agScreenController.BUTTON_MATCH_CURRENT_ALTITUDE then\n            html = _G.ScreenUtils.replaceClass(html, ELEMENT_CLASS_RIGHT_SLIDER, MOUSE_OVER_CLASS)\n        elseif mouseOver == _G.agScreenController.BUTTON_LOCK then\n            html = _G.ScreenUtils.replaceClass(html, ELEMENT_CLASS_UNLOCKED_BUTTON, MOUSE_OVER_CLASS)\n        elseif mouseOver == _G.agScreenController.BUTTON_UNLOCK then\n            html = _G.ScreenUtils.replaceClass(html, ELEMENT_CLASS_LOCKED_BUTTON, MOUSE_OVER_CLASS)\n        elseif mouseOver == _G.agScreenController.BUTTON_POWER_OFF then\n            html = _G.ScreenUtils.replaceClass(html, ELEMENT_CLASS_POWER_IS_ON, MOUSE_OVER_CLASS)\n        elseif mouseOver == _G.agScreenController.BUTTON_POWER_ON then\n            html = _G.ScreenUtils.replaceClass(html, ELEMENT_CLASS_POWER_IS_OFF, MOUSE_OVER_CLASS)\n        end\n    end\n\n    self.screen.setHTML(html)\nend\n\n--- Processes the input indicated by the provided button id.\n-- @treturn boolean True if the state was changed by this action.\nfunction _G.agScreenController:handleButton(buttonId)\n    local modified = false\n\n    if not self.locked then\n        if buttonId == _G.agScreenController.BUTTON_ALTITUDE_UP then\n            local adjusted = self.controller.targetAltitude + self.altitudeAdjustment\n            modified = adjusted ~= self.controller.targetAltitude\n\n            self.controller:setBaseAltitude(adjusted)\n\n        elseif buttonId == _G.agScreenController.BUTTON_ALTITUDE_DOWN then\n            local adjusted = self.controller.targetAltitude - self.altitudeAdjustment\n            modified = adjusted ~= self.controller.targetAltitude\n\n            self.controller:setBaseAltitude(adjusted)\n\n        elseif buttonId == _G.agScreenController.BUTTON_ALTITUDE_ADJUST_UP then\n            modified = self:setAltitudeAdjust(self.altitudeAdjustment * 10)\n\n        elseif buttonId == _G.agScreenController.BUTTON_ALTITUDE_ADJUST_DOWN then\n            modified = self:setAltitudeAdjust(self.altitudeAdjustment / 10)\n\n        elseif buttonId == _G.agScreenController.BUTTON_MATCH_CURRENT_ALTITUDE then\n            local adjusted = self.controller.currentAltitude\n            modified = adjusted ~= self.controller.targetAltitude\n\n            self.controller:setBaseAltitude(adjusted)\n\n        elseif buttonId == _G.agScreenController.BUTTON_LOCK then\n            self.locked = true\n            modified = true\n\n        elseif buttonId == _G.agScreenController.BUTTON_POWER_ON then\n            self.controller:setAgState(true)\n            modified = true\n        end\n    end\n\n    return modified\nend\n","filter":{"args":[],"signature":"start()","slotKey":"0"},"key":"3"},
        {"code":"--- Run third, define agScreenController basic SVG-specific functionality: SVG image, button coordinates, slider scaling\n\n-- constants for svg file\nlocal SCREEN_HEIGHT = 1080\nlocal MAX_SLIDER_ALTITUDE = 200000\nlocal MIN_SLIDER_ALTITUDE = 1000\nlocal ALT_SLIDER_TOP = 54\nlocal ALT_SLIDER_BOTTOM = 1026\n\n-- add SVG-specific fields\n_G.agScreenController.SVG_TEMPLATE = [[<svg viewBox=\"0 0 1920 1080\" style=\"width:100%; height:100%\" class=\"bootstrap\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" preserveAspectRatio=\"none\"><style>text{text-transform:none;font-family:arial;font-weight:normal;}.hidden, .unlockSlideClass, .disabledText, .powerSlideClass, .pulsorsText, .noPowerWarning{display:none;}.altitude{fill:#538be3;}.adjust{fill:#065de8;}.current{fill:#ffbe4d;}.label{font-size:60px;fill:#777777;}.majorValue{font-size:150px;letter-spacing:20px;}.minorValue{font-size:75px;fill:#aaaaaa;}.baseAltitude{fill:#b67400 !important;}.indicator{stroke:#000000;stroke-width:3;}.units{fill:#555555;}.error{fill:#880000;font-size:150px;}.mouseOver{stroke:#ffa200 !important;stroke-width:10px;}#slider text{text-anchor:middle;font-size:25px;fill:#ffffff;}#slider line{stroke-width:4;stroke:#333333;}</style><defs><path id=\"arrowIndicator\" d=\"M 20,0 A 100,75 0 0,0 96,-44 L 96,-25 A 100,75 0 0,0 192,-54 A 100,100 0 0,0 192,54 A 100,75 0 0,0 96,25 L 96,44 A 100,75 0 0,0 20,0 z\"/><use id=\"rightIndicator\" xlink:href=\"#arrowIndicator\"/><use id=\"leftIndicator\" xlink:href=\"#arrowIndicator\" transform=\"scale(-1,1)\"/><path id=\"baseIndicator\" d=\"M -15,0 A 20,15 0 0,1 0,10 A 20,15 0 0,1 15,0 A 20,15 0 0,1 0,-10 A 20,15 0 0,1 -15,0 z\"/><polygon id=\"chevronArrow\" points=\"-192,20 0,-108 192,20 152,54 0,-54 -152,54\"/><g id=\"altitudeUp\"><use xlink:href=\"#chevronArrow\"/></g><g id=\"altitudeDown\"><use transform=\"scale(1,-1)\" xlink:href=\"#altitudeUp\"/></g><g id=\"adjustDown\"><use transform=\"rotate(90) scale(0.28,0.28) translate(0,-108)\" xlink:href=\"#chevronArrow\"/></g><g id=\"adjustUp\"><use transform=\"scale(-1,1)\" xlink:href=\"#adjustDown\"/></g><path id=\"locked\" d=\"m -35,0 l 0,-15 a 35,35 0 0,1 70,0 l 0,15 5,0 0,54 -80,0 0,-54 z m 50,0 l 0,-15 a 15,15 0 0,0 -30,0 l 0,15 z\" fill=\"#004400\"/><path id=\"unlocked\" d=\"m 15,0 l 0,-15 a 35,35 0 0,1 70,0 l 0,15 -20,0 l 0,-15 a 15,15 0 0,0 -30,0 l 0,15 l 5,0 0,54 -80,0 0,-54 5,0 z\" fill=\"#440000\"/><g id=\"powerIsOff\"><path d=\"M -32.2,-23.7 A 10,10 0 0,1 -18,-9.5 A 25.5,25.5 0 1,0 18,-9.5 A 10,10 0 0,1 32.2,-23.7 A 45.5,45.5 0 1,1 -32.2,-23.7 z\" fill=\"#440000\"/><path d=\"M -10,-8 l 0,-32 a 10,10 0 0,1 20,0 l 0,32 a 10,10 0 0,1 -20,0 z\" fill=\"#444444\"/></g><g id=\"powerIsOn\"><path d=\"M -32.2,-23.7 A 10,10 0 0,1 -18,-9.5 A 25.5,25.5 0 1,0 18,-9.5 A 10,10 0 0,1 32.2,-23.7 A 45.5,45.5 0 1,1 -32.2,-23.7 z\" fill=\"#444444\"/><path d=\"M -10,-8 l 0,-32 a 10,10 0 0,1 20,0 l 0,32 a 10,10 0 0,1 -20,0 z\" fill=\"#004400\"/></g><clipPath id=\"sliderSwoop\"><path d=\"M 920,1026 Q 920,526 768,54 L 1152,54 Q 1000,526 1000,1026 Z\"/></clipPath><linearGradient id=\"altitudeGradient\" gradientTransform=\"rotate(90)\"><stop offset=\"0%\" stop-color=\"#000000\"/><stop offset=\"70%\" stop-color=\"#87ceeb\"/><stop offset=\"92.4%\" stop-color=\"#ffff00\"/><stop offset=\"100%\" stop-color=\"#ff0000\"/></linearGradient></defs><rect x=\"0\" y=\"0\" fill=\"#000000\" width=\"1920\" height=\"1080\"/><g id=\"slider\"><g clip-path=\"url(#sliderSwoop)\"><rect x=\"768\" y=\"54\" width=\"384\" height=\"972\" fill=\"url(#altitudeGradient)\"/><line x1=\"940\" y1=\"899\" x2=\"980\" y2=\"899\"/><line x1=\"940\" y1=\"824\" x2=\"980\" y2=\"824\"/><line x1=\"940\" y1=\"772\" x2=\"980\" y2=\"772\"/><line x1=\"918\" y1=\"731\" x2=\"928\" y2=\"731\"/><line x1=\"992\" y1=\"731\" x2=\"1002\" y2=\"731\"/><text x=\"960\" y=\"738\">5,000</text><line x1=\"940\" y1=\"697\" x2=\"980\" y2=\"697\"/><line x1=\"940\" y1=\"669\" x2=\"980\" y2=\"669\"/><line x1=\"940\" y1=\"645\" x2=\"980\" y2=\"645\"/><line x1=\"940\" y1=\"623\" x2=\"980\" y2=\"623\"/><line x1=\"768\" y1=\"604\" x2=\"925\" y2=\"604\"/><line x1=\"995\" y1=\"604\" x2=\"1152\" y2=\"604\"/><text x=\"960\" y=\"611\">10,000</text><line x1=\"768\" y1=\"476\" x2=\"1152\" y2=\"476\"/><line x1=\"768\" y1=\"402\" x2=\"1152\" y2=\"402\"/><line x1=\"768\" y1=\"349\" x2=\"1152\" y2=\"349\"/><line x1=\"768\" y1=\"308\" x2=\"925\" y2=\"308\"/><line x1=\"995\" y1=\"308\" x2=\"1152\" y2=\"308\"/><text x=\"960\" y=\"315\">50,000</text><line x1=\"768\" y1=\"275\" x2=\"1152\" y2=\"275\"/><line x1=\"768\" y1=\"247\" x2=\"1152\" y2=\"247\"/><line x1=\"768\" y1=\"222\" x2=\"1152\" y2=\"222\"/><line x1=\"768\" y1=\"200\" x2=\"1152\" y2=\"200\"/><line x1=\"768\" y1=\"181\" x2=\"920\" y2=\"181\"/><line x1=\"1000\" y1=\"181\" x2=\"1152\" y2=\"181\"/><text x=\"960\" y=\"188\">100,000</text></g><text x=\"960\" y=\"1068.5\" style=\"font-size:40px;fill:#ff0000;\">1000 <tspan class=\"units\">m</tspan></text><text x=\"960\" y=\"42.5\" style=\"font-size:40px;stroke:#000000;\">200000 <tspan class=\"units\">m</tspan></text><use xlink:href=\"#rightIndicator\" x=\"960\" y=\"%d\" class=\"current indicator rightSliderClass\"/><use xlink:href=\"#leftIndicator\" x=\"960\" y=\"%d\" class=\"altitude indicator leftSliderClass\"/><use xlink:href=\"#baseIndicator\" x=\"960\" y=\"%d\" class=\"baseAltitude indicator\"/></g><g id=\"targetPanel\"><g id=\"lockBar\"><use x=\"672\" y=\"54\" xlink:href=\"#unlocked\" class=\"unlockedClass\"/><text x=\"384\" y=\"81\" text-anchor=\"middle\" textLength=\"384\" class=\"label unlockSlideClass\">&gt;Slide&gt;</text><use id=\"locked\" x=\"96\" y=\"54\" xlink:href=\"#locked\" class=\"lockedClass\"/></g><text x=\"384\" y=\"594\" text-anchor=\"middle\" class=\"altitude majorValue\">%s</text><text x=\"384\" y=\"648\" text-anchor=\"middle\" class=\"label\">Target Altitude (m)</text><text x=\"384\" y=\"1053\" text-anchor=\"end\" class=\"label\">Base Altitude:</text><text x=\"384\" y=\"1053\" class=\"minorValue baseAltitude\">%d <tspan class=\"units\">m</tspan></text><g class=\"adjustmentWidgets\"><g class=\"adjust\"><use xlink:href=\"#altitudeUp\" x=\"384\" y=\"270\" class=\"altitudeUpClass\"/><use xlink:href=\"#altitudeDown\" x=\"384\" y=\"810\" class=\"altitudeDownClass\"/></g><g class=\"adjust\"><use x=\"672\" y=\"540\" xlink:href=\"#adjustDown\" class=\"adjustDownClass\"/><use x=\"96\" y=\"540\" xlink:href=\"#adjustUp\" class=\"adjustUpClass\"/></g></g></g><g id=\"currentPanel\"><g id=\"powerBar\"><use x=\"1248\" y=\"54\" xlink:href=\"#powerIsOff\" class=\"powerIsOffClass\"/><text x=\"1536\" y=\"81\" text-anchor=\"middle\" textLength=\"384\" class=\"label powerSlideClass\">&lt;Slide&lt;</text><use id=\"power\" x=\"1824\" y=\"54\" xlink:href=\"#powerIsOn\" class=\"powerIsOnClass\"/></g><text x=\"1536\" y=\"298\" text-anchor=\"end\" class=\"label\">Vertical Vel:</text><text x=\"1536\" y=\"298\" class=\"minorValue\">%s <tspan class=\"units\">%s</tspan></text><text x=\"1536\" y=\"594\" text-anchor=\"middle\" class=\"current majorValue\">%s</text><text x=\"1536\" y=\"648\" text-anchor=\"middle\" class=\"label\">Current Altitude (m)</text><g class=\"statusWidgets\"><text x=\"1632\" y=\"891\" text-anchor=\"end\" class=\"label\">Field Strength:</text><text x=\"1632\" y=\"891\" class=\"minorValue\">%d <tspan class=\"units\">Es</tspan></text><text x=\"1632\" y=\"999\" text-anchor=\"end\" class=\"label\">Power Level:</text><text x=\"1632\" y=\"999\" class=\"minorValue\">%d <tspan class=\"units\">%</tspan></text></g><g class=\"disabledText error\"><text x=\"1536\" y=\"823\" text-anchor=\"middle\" textLength=\"768\">Anti-Gravity</text><text x=\"1536\" y=\"945\" text-anchor=\"middle\">Generator</text><text x=\"1536\" y=\"1067\" text-anchor=\"middle\">Offline</text></g><g class=\"pulsorsText error\"><text x=\"1536\" y=\"823\" text-anchor=\"middle\">Insufficient</text><text x=\"1536\" y=\"945\" text-anchor=\"middle\">Pulsors</text></g><g class=\"noPowerWarning error\"><text x=\"1536\" y=\"1060\" text-anchor=\"middle\" font-size=\"40px\">Current Altitude too far from Base Altitude</text></g></g></svg>]]\n\n-- Define button ranges, either in tables of x1,y1,x2,y2 or lists of those tables.\nlocal buttonCoordinates = {}\nbuttonCoordinates[_G.agScreenController.BUTTON_ALTITUDE_UP] = {\n    x1 = 0.05, x2 = 0.35,\n    y1 = 0.1, y2 = 0.4\n}\nbuttonCoordinates[_G.agScreenController.BUTTON_ALTITUDE_DOWN] = {\n    x1 = 0.05, x2 = 0.35,\n    y1 = 0.6, y2 = 0.9\n}\nbuttonCoordinates[_G.agScreenController.BUTTON_ALTITUDE_ADJUST_DOWN] = {\n    x1 = 0.35, x2 = 0.4,\n    y1 = 0.4, y2 = 0.6\n}\nbuttonCoordinates[_G.agScreenController.BUTTON_ALTITUDE_ADJUST_UP] = {\n    x1 = 0.0, x2 = 0.05,\n    y1 = 0.4, y2 = 0.6\n}\nbuttonCoordinates[_G.agScreenController.BUTTON_TARGET_ALTITUDE_SLIDER] = {\n    x1 = 0.4, x2 = 0.5,\n    y1 = 0.0, y2 = 1.0\n}\nbuttonCoordinates[_G.agScreenController.BUTTON_MATCH_CURRENT_ALTITUDE] = {\n    x1 = 0.5, x2 = 0.6,\n    y1 = 0.0, y2 = 1.0\n}\nbuttonCoordinates[_G.agScreenController.BUTTON_LOCK] = {\n    x1 = 0.3, x2 = 0.4,\n    y1 = 0.0, y2 = 0.1\n}\nbuttonCoordinates[_G.agScreenController.BUTTON_UNLOCK] = {\n    x1 = 0.0, x2 = 0.1,\n    y1 = 0.0, y2 = 0.1\n}\nbuttonCoordinates[_G.agScreenController.BUTTON_POWER_OFF] = {\n    x1 = 0.9, x2 = 1.0,\n    y1 = 0.0, y2 = 0.1\n}\nbuttonCoordinates[_G.agScreenController.BUTTON_POWER_ON] = {\n    x1 = 0.6, x2 = 0.7,\n    y1 = 0.0, y2 = 0.1\n}\n-- save to controller for press/release event handling\n_G.agScreenController.buttonCoordinates = buttonCoordinates\n\n-- both sliders on same level, pre-compute y ranges with 5% buffer\n_G.agScreenController.sliderYMin = buttonCoordinates[_G.agScreenController.BUTTON_UNLOCK].y1 - SCREEN_HEIGHT * 0.05\n_G.agScreenController.sliderYMax = buttonCoordinates[_G.agScreenController.BUTTON_UNLOCK].y2 + SCREEN_HEIGHT * 0.05\n\n-- pre-computed values for less computation in render thread\nlocal logMin = math.log(MIN_SLIDER_ALTITUDE)\nlocal logMax = math.log(MAX_SLIDER_ALTITUDE)\nlocal scaleHeight = ALT_SLIDER_BOTTOM - ALT_SLIDER_TOP\nlocal scaleHeightOverLogDifference = scaleHeight / (logMax - logMin)\n\n-- yPixel = sliderBottom - (sliderHeight * (log(altitude) - log(minAltitude)) / (log(maxAltitude) - log(minAltitude)))\nfunction _G.agScreenController.calculateSliderIndicator(altitude)\n    return math.floor(ALT_SLIDER_BOTTOM - scaleHeightOverLogDifference * (math.log(altitude) - logMin) + 0.5)\nend\n\n-- altitude = e^((sliderBottom - yPixel) / sliderHeight * (log(maxAltitude) - log(minAltitude)) + log(minAltitude)\nfunction _G.agScreenController.calculateSliderAltitude(indicatorY)\n    local indicatorYpixels = indicatorY * SCREEN_HEIGHT\n    local target = math.floor(math.exp((ALT_SLIDER_BOTTOM - indicatorYpixels) / scaleHeightOverLogDifference + logMin) + 0.5)\n    if target > MAX_SLIDER_ALTITUDE then\n        target = MAX_SLIDER_ALTITUDE\n    end\n    return target\nend\n","filter":{"args":[],"signature":"start()","slotKey":"0"},"key":"4"},
        {"code":"_G.agScreenController:refresh()","filter":{"args":[],"signature":"update()","slotKey":"-2"},"key":"5"},
        {"code":"_G.agScreenController:mouseDown(x, y)","filter":{"args":[{"variable":"*"},{"variable":"*"}],"signature":"mouseDown(x,y)","slotKey":"0"},"key":"6"},
        {"code":"_G.agScreenController:mouseUp(x, y)","filter":{"args":[{"variable":"*"},{"variable":"*"}],"signature":"mouseUp(x,y)","slotKey":"0"},"key":"7"},
        {"code":"--- Utilities module.\n-- Frequently needed utility functions.\n-- @module Utilities\n\n-- Guard to keep this module from reinitializing any time the start event fires if placed in libraries/system slot.\nif _G.Utilities then\n    return\nend\n_G.Utilities = {}\n\nlocal SI_PREFIXES = {\"\", \"k\", \"M\", \"G\", \"T\", \"P\", \"E\", \"Z\", \"Y\"}\n--- Converts raw float to formatted SI prefix with limited decimal places.\n-- @tparam number value The number to format.\n-- @tparam string units The units label to apply SI prefixes to.\n-- @treturn string The formated number for display.\n-- @treturn string The units with SI prefix applied.\nfunction _G.Utilities.printableNumber(value, units)\n    -- can't process nil, 0 breaks the sign calculation\n    if not value or value == 0 then\n        return \"0.0\", units\n    end\n\n    local adjustedValue = math.abs(value)\n    local sign = value / adjustedValue\n    local factor = 1 -- index of no prefix\n    while adjustedValue >= 999.5 and factor < #SI_PREFIXES do\n        adjustedValue = adjustedValue / 1000\n        factor = factor + 1\n    end\n\n    if adjustedValue < 9.95 then -- rounded to 10, show 1 decimal place\n        return string.format(\"%.1f\", sign * math.floor(adjustedValue * 10 + 0.5) / 10), SI_PREFIXES[factor] .. units\n    end\n    return string.format(\"%.0f\", sign * math.floor(adjustedValue + 0.5)), SI_PREFIXES[factor] .. units\nend\n\n--- Escapes % symbols that are not part of string format strings.\n-- @tparam string text The string to sanitize.\nfunction _G.Utilities.sanitizeFormatString(text)\n    text = string.gsub(text, \"%%([^sdf])\", \"%%%%%1\")\n    text = string.gsub(text, \"%%%%(%d*%.%d*f)\", \"%%%1\") -- allow float specifiers\n    text = string.gsub(text, \"%%$\", \"%%%%\") -- handle % at end of string\n    return text\nend\n\n--- Finds the first slot on 'unit' that has element class 'slotClass' and is not listed in the exclude list.\n-- @tparam string slotClass The element class of the target slot. May instead be a table containing a list of class names.\n-- @tparam table exclude A list of slots to exclude from search.\n-- @return The first element found of the desired type, or nil if none is found.\n-- @return The name of the slot where the returned element was found.\nfunction _G.Utilities.findFirstSlot(slotClass, exclude)\n    if type(slotClass) ~= \"table\" then\n        slotClass = {slotClass}\n    end\n    exclude = exclude or {}\n\n    for key, value in pairs(unit) do\n\n        -- ignore excluded elements\n        for _, exc in pairs(exclude) do\n            if value == exc then\n                goto continueOuter\n            end\n        end\n\n        if value and type(value) == \"table\" and value.getElementClass then\n            for _, class in pairs(slotClass) do\n                if value.getElementClass() == class then\n                    return value, key\n                end\n            end\n        end\n\n        ::continueOuter::\n    end\n\n    return nil, nil\nend\n\n-- Verifies the valid argument, if not true then it prints the provided message to the optional screen and to the programming board error log, halting execution.\n-- @param valid The condition to test, typically a boolean.\n-- @tparam string message The message to display on failure.\n-- @tparam ScreenUnit/ScreenSignUnit screen The optional screen for displaying the message on in case of failure.\nlocal function assertValid(valid, message, screen)\n    if not valid then\n        if screen and screen.setCenteredText and type(screen.setCenteredText) == \"function\" then\n            screen.setCenteredText(message)\n        end\n        error(message)\n    end\nend\n\n--- Attempts to verify the provided slot against the expected type, finding missing slot inputs in unit.\n-- @tparam Element provided A named slot that should fill the need for this type. May be nil.\n-- @tparam string targetClass The ElementClass to look for/validate against. May be a table containing a list of classes.\n-- @tparam ScreenUnit/ScreenSignUnit errorScreen A screen to display error messages to on failure.\n-- @tparam string moduleName The name of the module, to help disambiguate problems when multiple modules are run on the same controller.\n-- @tparam string mappedSlotName The internal name of the slot to indicate exactly what mapping failed.\n-- @tparam boolean optional True if this element is optional and should not produce an error on failure to map.\n-- @tparam string optionalMessage A message to print to the console on failure to map an optional element.\nfunction _G.Utilities.loadSlot(provided, targetClass, errorScreen, moduleName, mappedSlotName, optional, optionalMessage)\n    if type(targetClass) ~= \"table\" then\n        targetClass = {targetClass}\n    end\n    local slotName\n\n    local typedSlot = provided\n    if not (typedSlot and type(typedSlot) == \"table\" and typedSlot.getElementClass) then\n        typedSlot, slotName = _G.Utilities.findFirstSlot(targetClass)\n        if not optional then\n            assertValid(typedSlot, string.format(\"%s: %s link not found.\", moduleName, mappedSlotName), errorScreen)\n        end\n\n        if typedSlot then\n            system.print(string.format(\"Slot %s mapped to %s %s.\", slotName, moduleName, mappedSlotName))\n        elseif optionalMessage and string.len(optionalMessage) > 0 then\n            system.print(string.format(\"%s: %s\", moduleName, optionalMessage))\n        end\n    else\n        local class = typedSlot.getElementClass()\n        local valid = false\n        for _, tClass in pairs(targetClass) do\n            valid = valid or class == tClass\n        end\n        assertValid(valid, string.format(\"%s %s slot is of type: %s\", moduleName, mappedSlotName, class), errorScreen)\n    end\n    return typedSlot\nend\n\nlocal useParameterSettings = false --export: Toggle this on to override stored preferences with parameter-set values, otherwise will load from databank if available.\n-- can't export value from table, but would rather use it from the utilities object\n_G.Utilities.USE_PARAMETER_SETTINGS = useParameterSettings\n\n--- Returns the preferred preference value, storing that in the databank for future use if available. Type will be inferred from the default value provided.\n-- @param databank The databank to use for preferences.\n-- @tparam string key The databank preference key to look up/store to.\n-- @param defaultValue The value to use if the databank doesn't contain key.\n-- @return The preference value to use.\nfunction _G.Utilities.getPreference(databank, key, defaultValue)\n    local isBool = type(defaultValue) == \"boolean\"\n    local isNumber = type(defaultValue) == \"number\"\n    local prefValue\n\n    if databank then\n        if databank.hasKey(key) == 1 and not _G.Utilities.USE_PARAMETER_SETTINGS then\n            if isBool then\n                prefValue = databank.getIntValue(key) == 1\n            elseif isNumber then\n                prefValue = databank.getFloatValue(key)\n            else\n                prefValue = databank.getStringValue(key)\n            end\n        else\n            prefValue = defaultValue\n        end\n\n        if isBool then\n            local storeValue = 0\n            if prefValue then\n                storeValue = 1\n            end\n            databank.setIntValue(key, storeValue)\n        elseif isNumber then\n            databank.setFloatValue(key, tonumber(prefValue))\n        else\n            databank.setStringValue(key, prefValue)\n        end\n    else\n        prefValue = defaultValue\n    end\n\n    return prefValue\nend\n\nreturn _G.Utilities\n","filter":{"args":[],"signature":"start()","slotKey":"-3"},"key":"8"},
        {"code":"--- Screen utils module.\n-- Frequently needed screen functions.\n-- @module ScreenUtils\n\n-- Guard to keep this module from reinitializing any time the start event fires if placed in libraries/system slot.\nif _G.ScreenUtils then\n    return\nend\n_G.ScreenUtils = {}\n\n--- Replaces a value from within a class attribute.\nfunction _G.ScreenUtils.replaceClass(html, find, replace)\n    -- ensure preceeded and followed by \" or space\n    return string.gsub(html, \"([\\\"%s])\" .. find .. \"([%s\\\"])\", \"%1\" .. replace .. \"%2\")\nend\n\n--- Adds an additional value to a class attribute.\nfunction _G.ScreenUtils.addClass(html, find, add)\n    -- ensure preceeded and followed by \" or space\n    return string.gsub(html, \"([\\\"%s]\" .. find .. \")([%s\\\"])\", \"%1 \" .. add .. \"%2\")\nend\n\n--- Returns the button that intersects the provided coordinates or nil if none is found.\n-- @tparam table buttonCoordinates Table of \"buttonLabel\" => {x1, y1, x2, y2} or \"buttonLabel\" => {1={x1, y1, x2, y2}, 2={x1, y1, x2, y2}, ...}\n-- @tparam number x The x screen position to test.\n-- @tparam number y The y screen position to test.\nfunction _G.ScreenUtils.detectButton(buttonCoordinates, x, y)\n    local found = false\n    local index = nil\n    for buttonLabel, coords in pairs(buttonCoordinates) do\n        if coords.x1 then\n            if x > coords.x1 and x < coords.x2 and y > coords.y1 and y < coords.y2 then\n                found = true\n            end\n        else\n            for i, innerCoords in pairs(coords) do\n                if innerCoords.x1 then\n                    if x > innerCoords.x1 and x < innerCoords.x2 and y > innerCoords.y1 and y < innerCoords.y2 then\n                        found = true\n                        index = i\n                    end\n                else\n                    break\n                end\n            end\n        end\n\n        -- test for inactive flag, reset found if not active\n        if found then\n            found = coords.active ~= false\n        end\n\n        if found then\n            return buttonLabel, index\n        end\n    end\n    return nil\nend\n\n--- Replaces the class for the currently moused-over button with the mouseoverClass\n-- @tparam table buttonCoordinates Table of \"buttonLabel\" => {x1, y1, x2, y2, class} or\n--  \"buttonLabel\" => {1={x1, y1, x2, y2}, 2={x1, y1, x2, y2}, ..., class=\"elementClass\"} or\n--  \"buttonLabel\" => {x1, y1, x2, y2, {class1, class2, ...}}\n-- @tparam number x The x screen position to test.\n-- @tparam number y The y screen position to test.\n-- @tparam string html The html document to update.\n-- @tparam string mouseoverClass The css class to replace the current button with.\nfunction _G.ScreenUtils.mouseoverButtons(buttonCoordinates, x, y, html, mouseoverClass)\n    local mouseover, index = _G.ScreenUtils.detectButton(buttonCoordinates, x, y)\n    -- nil doesn't concatenate nicely\n    index = index or \"\"\n\n    if mouseover then\n        if type(buttonCoordinates[mouseover].class) == \"table\" then\n            local newHtml = html\n            for _,findClass in pairs(buttonCoordinates[mouseover].class) do\n                newHtml = _G.ScreenUtils.replaceClass(newHtml, findClass, mouseoverClass)\n            end\n            return newHtml\n        else\n            local findClass = buttonCoordinates[mouseover].class .. index\n            return _G.ScreenUtils.replaceClass(html, findClass, mouseoverClass)\n        end\n    end\n    return html\nend\n\nreturn _G.ScreenUtils\n","filter":{"args":[],"signature":"start()","slotKey":"-3"},"key":"9"},
        {"code":"-- From: https://gitlab.com/JayleBreak/dualuniverse/-/blob/master/DUflightfiles/autoconf/custom/atlas.lua\n-- Guard to keep this module from reinitializing any time the start event fires if placed in libraries/system slot.\nif _G.atlas then\n    return\nend\n\n_G.atlas = {\n-- return {\n[0] = {\n  [1]={\n    GM=6930729684,\n    bodyId=1,\n    center={x=17465536.000,y=22665536.000,z=-34464.000},\n    name='Madis',\n    planetarySystemId=0,\n    radius=44300\n  },\n  [2]={\n    GM=157470826617,\n    bodyId=2,\n    center={x=-8.000,y=-8.000,z=-126303.000},\n    name='Alioth',\n    planetarySystemId=0,\n    radius=126068\n  },\n  [3]={\n    GM=11776905000,\n    bodyId=3,\n    center={x=29165536.000,y=10865536.000,z=65536.000},\n    name='Thades',\n    planetarySystemId=0,\n    radius=49000\n  },\n  [4]={\n    GM=14893847582,\n    bodyId=4,\n    center={x=-13234464.000,y=55765536.000,z=465536.000},\n    name='Talemai',\n    planetarySystemId=0,\n    radius=57450\n  },\n  [5]={\n    GM=16951680000,\n    bodyId=5,\n    center={x=-43534464.000,y=22565536.000,z=-48934464.000},\n    name='Feli',\n    planetarySystemId=0,\n    radius=60000\n  },\n  [6]={\n    GM=10502547741,\n    bodyId=6,\n    center={x=52765536.000,y=27165538.000,z=52065535.000},\n    name='Sicari',\n    planetarySystemId=0,\n    radius=51100\n  },\n  [7]={\n    GM=13033380591,\n    bodyId=7,\n    center={x=58665538.000,y=29665535.000,z=58165535.000},\n    name='Sinnen',\n    planetarySystemId=0,\n    radius=54950\n  },\n  [8]={\n    GM=18477723600,\n    bodyId=8,\n    center={x=80865538.000,y=54665536.000,z=-934463.940},\n    name='Teoma',\n    planetarySystemId=0,\n    radius=62000\n  },\n  [9]={\n    GM=18606274330,\n    bodyId=9,\n    center={x=-94134462.000,y=12765534.000,z=-3634464.000},\n    name='Jago',\n    planetarySystemId=0,\n    radius=61590\n  },\n  [10]={\n    GM=78480000,\n    bodyId=10,\n    center={x=17448118.224,y=22966846.286,z=143078.820},\n    name='Madis Moon 1',\n    planetarySystemId=0,\n    radius=10000\n  },\n  [11]={\n    GM=237402000,\n    bodyId=11,\n    center={x=17194626.000,y=22243633.880,z=-214962.810},\n    name='Madis Moon 2',\n    planetarySystemId=0,\n    radius=11000\n  },\n  [12]={\n    GM=265046609,\n    bodyId=12,\n    center={x=17520614.000,y=22184730.000,z=-309989.990},\n    name='Madis Moon 3',\n    planetarySystemId=0,\n    radius=15005\n  },\n  [21]={\n    GM=2118960000,\n    bodyId=21,\n    center={x=457933.000,y=-1509011.000,z=115524.000},\n    name='Alioth Moon 1',\n    planetarySystemId=0,\n    radius=30000\n  },\n  [22]={\n    GM=2165833514,\n    bodyId=22,\n    center={x=-1692694.000,y=729681.000,z=-411464.000},\n    name='Alioth Moon 4',\n    planetarySystemId=0,\n    radius=30330\n  },\n  [26]={\n    GM=68234043600,\n    bodyId=26,\n    center={x=-1404835.000,y=562655.000,z=-285074.000},\n    name='Sanctuary',\n    planetarySystemId=0,\n    radius=83400\n  },\n  [30]={\n    GM=211564034,\n    bodyId=30,\n    center={x=29214402.000,y=10907080.695,z=433858.200},\n    name='Thades Moon 1',\n    planetarySystemId=0,\n    radius=14002\n  },\n  [31]={\n    GM=264870000,\n    bodyId=31,\n    center={x=29404193.000,y=10432768.000,z=19554.131},\n    name='Thades Moon 2',\n    planetarySystemId=0,\n    radius=15000\n  },\n  [40]={\n    GM=141264000,\n    bodyId=40,\n    center={x=-13503090.000,y=55594325.000,z=769838.640},\n    name='Talemai Moon 2',\n    planetarySystemId=0,\n    radius=12000\n  },\n  [41]={\n    GM=106830900,\n    bodyId=41,\n    center={x=-12800515.000,y=55700259.000,z=325207.840},\n    name='Talemai Moon 3',\n    planetarySystemId=0,\n    radius=11000\n  },\n  [42]={\n    GM=264870000,\n    bodyId=42,\n    center={x=-13058408.000,y=55781856.000,z=740177.760},\n    name='Talemai Moon 1',\n    planetarySystemId=0,\n    radius=15000\n  },\n  [50]={\n    GM=499917600,\n    bodyId=50,\n    center={x=-43902841.780,y=22261034.700,z=-48862386.000},\n    name='Feli Moon 1',\n    planetarySystemId=0,\n    radius=14000\n  },\n  [70]={\n    GM=396912600,\n    bodyId=70,\n    center={x=58969616.000,y=29797945.000,z=57969449.000},\n    name='Sinnen Moon 1',\n    planetarySystemId=0,\n    radius=17000\n  },\n  [100]={\n    GM=13975172474,\n    bodyId=100,\n    center={x=98865536.000,y=-13534464.000,z=-934461.990},\n    name='Lacobus',\n    planetarySystemId=0,\n    radius=55650\n  },\n  [101]={\n    GM=264870000,\n    bodyId=101,\n    center={x=98905288.170,y=-13950921.100,z=-647589.530},\n    name='Lacobus Moon 3',\n    planetarySystemId=0,\n    radius=15000\n  },\n  [102]={\n    GM=444981600,\n    bodyId=102,\n    center={x=99180968.000,y=-13783862.000,z=-926156.400},\n    name='Lacobus Moon 1',\n    planetarySystemId=0,\n    radius=18000\n  },\n  [103]={\n    GM=211503600,\n    bodyId=103,\n    center={x=99250052.000,y=-13629215.000,z=-1059341.400},\n    name='Lacobus Moon 2',\n    planetarySystemId=0,\n    radius=14000\n  },\n  [110]={\n    GM=9204742375,\n    bodyId=110,\n    center={x=14165536.000,y=-85634465.000,z=-934464.300},\n    name='Symeon',\n    planetarySystemId=0,\n    radius=49050\n  },\n  [120]={\n    GM=7135606629,\n    bodyId=120,\n    center={x=2865536.700,y=-99034464.000,z=-934462.020},\n    name='Ion',\n    planetarySystemId=0,\n    radius=44950\n  },\n  [121]={\n    GM=106830900,\n    bodyId=121,\n    center={x=2472916.800,y=-99133747.000,z=-1133582.800},\n    name='Ion Moon 1',\n    planetarySystemId=0,\n    radius=11000\n  },\n  [122]={\n    GM=176580000,\n    bodyId=122,\n    center={x=2995424.500,y=-99275010.000,z=-1378480.700},\n    name='Ion Moon 2',\n    planetarySystemId=0,\n    radius=15000\n  }  \n }\n}\n","filter":{"args":[],"signature":"start()","slotKey":"-3"},"key":"10"},
        {"code":"-- https://gitlab.com/JayleBreak/dualuniverse/-/blob/master/DUflightfiles/autoconf/custom/planetref.lua\n\n--[[ \n  Provide coordinate transforms and access to kinematic related parameters\n  Author: JayleBreak\n\n  Usage (unit.start):\n  PlanetaryReference = require('planetref')\n  galaxyReference = PlanetaryReference(referenceTableSource)\n  helios = galaxyReference[0] -- PlanetaryReference.PlanetarySystem instance\n  alioth = helios[2]          -- PlanetaryReference.BodyParameters instance\n\n  Methods:\n    PlanetaryReference:getPlanetarySystem - based on planetary system ID.\n    PlanetaryReference.isMapPosition - 'true' if an instance of 'MapPosition'\n    PlanetaryReference.createBodyParameters - for entry into reference table\n    PlanetaryReference.BodyParameters - a class containing a body's information.\n    PlanetaryReference.MapPosition - a class for map coordinates\n    PlanetaryReference.PlanetarySystem - a container for planetary system info.\n\n    PlanetarySystem:castIntersections - from a position in a given direction.\n    PlanetarySystem:closestBody - to the specified coordinates.\n    PlanetarySystem:convertToBodyIdAndWorldCoordinates - from map coordinates.\n    PlanetarySystem:getBodyParameters - from reference table.\n    PlanetarySystem:getPlanetarySystemId - for the instance.\n\n    BodyParameters:convertToWorldCoordinates - from map coordinates\n    BodyParameters:convertToMapPosition - from world coordinates\n    BodyParameters:getAltitude - of world coordinates\n    BodyParameters:getDistance - from center to world coordinates\n    BodyParameters:getGravity - at a given position in world coordinates.\n\n  Description\n  An instance of the 'PlanetaryReference' \"class\" can contain transform and\n  kinematic reference information for all planetary systems in DualUniverse.\n  Each planetary system is identified by a numeric identifier. Currently,\n  the only planetary system, Helios, has the identifier: zero. This \"class\"\n  supports the indexing ('[]') operation which is equivalent to the\n  use of the 'getPlanetarySystem' method. It also supports the 'pairs()'\n  method for iterating over planetary systems.\n  \n  An instance of the 'PlanetarySystem' \"class\" contains all reference\n  information for a specific system. It supports the indexing ('[]') and\n  'pairs()' functions which allows iteration over each \"body\" in the\n  system where the key is the numeric body ID. It also supports the\n  'tostring()' method.\n\n  An instance of the 'BodyParameters' \"class\" contains all reference\n  information for a single celestial \"body\" (a moon or planet). It supports\n  the 'tostring()' method, and contains the data members:\n          planetarySystemId - numeric planetary system ID\n          bodyId            - numeric body ID\n          radius            - radius of the body in meters (zero altitude)\n          center            - world coordinates of the body's center position\n          GM                - the gravitation parameter (g = GM/radius^2)\n  Note that the user is allowed to add custom fields (e.g. body name), but\n  should insure that complex table values have the '__tostring' metamethod\n  implemented.\n\n  Transform and Kinematics:\n  \"World\" coordinates is a cartesian coordinate system with an origin at an\n  arbitrary fixed point in a planetary system and with distances measured in\n  meters. The coordinates are expressible either as a simple table of 3 values\n  or an instance of the 'vec3' class.  In either case, the planetary system\n  identity is implicit.\n\n  \"Map\" coordinates is a geographic coordinate system with an origin at the\n  center of an identified (by a numeric value) celestial body which is a\n  member of an identified (also a numeric value) planetary system. Note that\n  the convention that latitude, longitude, and altitude values will be the\n  position's x, y, and z world coordinates in the special case of body ID 0.\n\n  The kinematic parameters in the reference data permit calculations of the\n  gravitational attraction of the celestial body on other objects.\n\n  Reference Data:\n  This is an example of reference data with a single entry assigned to\n  planetary system ID 0, and body ID 2 ('Alioth'):\n    referenceTable = {\n          [0] = { [2] = { planetarySystemId = 0,\n                          bodyId = 2,\n                          radius = 126068,\n                          center = vec3({x=-8, y=-8, z=-126303}),\n                          GM = 1.572199+11 } -- as in F=-GMm/r^2\n          }\n      }\n    ref=PlanetaryReference(referenceTable)\n\n  Collecting Reference Data:\n  A combination of information from the \"Map\" screen in the DU user interface,\n  and values reported by the DU Lua API can be the source of the reference\n  table's data (planetarySystemId, bodyId, and surfaceArea is from the user\n  interface):\n    referenceTable = {}\n    referenceTable[planetarySystemId][bodyId] =\n         PlanetaryReference.createBodyParameters(planetarySystemId,\n                                                 bodyId,\n                                                 surfaceArea,\n                                                 core.getConstructWorldPos(),\n                                                 core.getWorldVertical(),\n                                                 core.getAltitude(),\n                                                 core.g())\n\n\n  Adapting Data Sources:\n  Other sources of data can be adapted or converted. An example of adapting a\n  table, defined in the file: 'planets.lua', containing information on a single\n  planetary system and using celestial body name as the key follows (note that\n  a 'name' field is added to the BodyParameters instance transparently after\n  construction, and the '__pairs' meta function is required to support the\n  'closestBody' and '__tostring' methods):\n    ref=PlanetaryReference(\n        {[0] = setmetatable(require('planets'),\n                        { __index = function(bodies, bodyId)\n                             for _,v in pairs(bodies) do\n                                 if v and v.bodyId == bodyId then return v end\n                             end\n                             return nil\n                           end,\n                         __pairs = function(bodies)\n                             return function(t, k)\n                                     local nk, nv = next(t, k)\n                                     if nv then\n                                         local GM = nv.gravity * nv.radius^2\n                                         local bp = BodyParameters(0,\n                                                                   nv.id,\n                                                                   nv.radius,\n                                                                   nv.pos,\n                                                                   GM)\n                                         bp.name = nk\n                                         return nk, bp\n                                    end\n                                    return nk, nv\n                                 end, bodies, nil\n                           end })\n    })\n    \n  Converting Data Sources:\n  An instance of 'PlanetaryReference' that has been adapted to a data source\n  can be used to convert that source to simple table. For example,\n  using the adapted instance shown above:\n    load('convertedData=' .. tostring(ref))()\n    newRef=PlanetaryReference(convertedData)\n\n  Also See: kepler.lua\n  ]]--\n\n--[[                    START OF LOCAL IMPLEMENTATION DETAILS             ]]--\n\n-- Type checks\n\nlocal function isNumber(n)  return type(n)           == 'number' end\nlocal function isSNumber(n) return type(tonumber(n)) == 'number' end\nlocal function isTable(t)   return type(t)           == 'table'  end\nlocal function isString(s)  return type(s)           == 'string' end\nlocal function isVector(v)  return isTable(v)\n                                    and isNumber(v.x and v.y and v.z) end\n\nlocal function isMapPosition(m) return isTable(m) and isNumber(m.latitude  and\n                                                               m.longitude and\n                                                               m.altitude  and\n                                                               m.bodyId    and\n                                                               m.systemId) end\n\n-- Constants\n\nlocal deg2rad    = math.pi/180\nlocal rad2deg    = 180/math.pi\nlocal epsilon    = 1e-10\nlocal num        = ' *([+-]?%d+%.?%d*e?[+-]?%d*)'\nlocal posPattern = '::pos{' .. num .. ',' .. num .. ',' ..  num .. ',' ..\n                   num ..  ',' .. num .. '}'\n\n-- Utilities\n\nlocal utils  = require('cpml.utils')\nlocal vec3   = require('cpml.vec3')\nlocal clamp  = utils.clamp\n\nlocal function float_eq(a,b)\n    if a == 0 then return math.abs(b) < 1e-09 end\n    if b == 0 then return math.abs(a) < 1e-09 end\n    return math.abs(a - b) < math.max(math.abs(a),math.abs(b))*epsilon\nend\n\nlocal function formatNumber(n)\n    local result = string.gsub(\n                    string.reverse(string.format('%.4f',n)),\n                    '^0*%.?','')\n    return result == '' and '0' or string.reverse(result)\nend\n\nlocal function formatValue(obj)\n    if isVector(obj) then\n        return string.format('{x=%.3f,y=%.3f,z=%.3f}', obj.x, obj.y, obj.z)\n    end\n\n    if isTable(obj) and not getmetatable(obj) then\n        local list = {}\n        local nxt  = next(obj)\n\n        if type(nxt) == 'nil' or nxt == 1 then -- assume this is an array\n            list = obj\n        else\n            for k,v in pairs(obj) do\n                local value = formatValue(v)\n                if type(k) == 'number' then\n                    table.insert(list, string.format('[%s]=%s', k, value))\n                else\n                    table.insert(list, string.format('%s=%s',   k, value))\n                end\n            end\n        end\n        return string.format('{%s}', table.concat(list, ','))\n    end\n\n    if isString(obj) then\n        return string.format(\"'%s'\", obj:gsub(\"'\",[[\\']]))\n    end\n    return tostring(obj)\nend\n\n-- CLASSES\n\n-- BodyParameters: Attributes of planetary bodies (planets and moons)\n\nlocal BodyParameters = {}\nBodyParameters.__index = BodyParameters\nBodyParameters.__tostring =\n    function(obj, indent)\n        local sep = indent or ''\n        local keys = {}\n        for k in pairs(obj) do table.insert(keys, k) end\n        table.sort(keys)\n        local list = {}\n        for _, k in ipairs(keys) do\n            local value = formatValue(obj[k])\n            if type(k) == 'number' then\n                table.insert(list, string.format('[%s]=%s', k, value))\n            else\n                table.insert(list, string.format('%s=%s', k, value))\n            end\n        end\n        if indent then\n            return string.format('%s%s',\n                                 indent,\n                                 table.concat(list, ',\\n' .. indent))\n        end\n        return string.format('{%s}', table.concat(list, ','))\n    end\nBodyParameters.__eq = function(lhs, rhs)\n        return lhs.planetarySystemId == rhs.planetarySystemId and\n               lhs.bodyId            == rhs.bodyId            and\n               float_eq(lhs.radius, rhs.radius)               and\n               float_eq(lhs.center.x, rhs.center.x)           and\n               float_eq(lhs.center.y, rhs.center.y)           and\n               float_eq(lhs.center.z, rhs.center.z)           and\n               float_eq(lhs.GM, rhs.GM)\n    end\n\nlocal function mkBodyParameters(systemId, bodyId, radius, worldCoordinates, GM)\n    -- 'worldCoordinates' can be either table or vec3\n    assert(isSNumber(systemId),\n           'Argument 1 (planetarySystemId) must be a number:' .. type(systemId))\n    assert(isSNumber(bodyId),\n           'Argument 2 (bodyId) must be a number:' .. type(bodyId))\n    assert(isSNumber(radius),\n           'Argument 3 (radius) must be a number:' .. type(radius))\n    assert(isTable(worldCoordinates),\n           'Argument 4 (worldCoordinates) must be a array or vec3.' ..\n           type(worldCoordinates))\n    assert(isSNumber(GM),\n           'Argument 5 (GM) must be a number:' .. type(GM))\n    return setmetatable({planetarySystemId = tonumber(systemId),\n                         bodyId            = tonumber(bodyId),\n                         radius            = tonumber(radius),\n                         center            = vec3(worldCoordinates),\n                         GM                = tonumber(GM) }, BodyParameters)\nend\n\n-- MapPosition: Geographical coordinates of a point on a planetary body.\n\nlocal MapPosition = {}\nMapPosition.__index = MapPosition\nMapPosition.__tostring = function(p)\n        return string.format('::pos{%d,%d,%s,%s,%s}',\n                             p.systemId,\n                             p.bodyId,\n                             formatNumber(p.latitude*rad2deg),\n                             formatNumber(p.longitude*rad2deg),\n                             formatNumber(p.altitude))\n    end\nMapPosition.__eq       = function(lhs, rhs)\n        return lhs.bodyId   == rhs.bodyId              and\n               lhs.systemId == rhs.systemId            and\n               float_eq(lhs.latitude,   rhs.latitude)  and\n               float_eq(lhs.altitude,   rhs.altitude)  and\n               (float_eq(lhs.longitude, rhs.longitude) or\n                float_eq(lhs.latitude, math.pi/2)      or\n                float_eq(lhs.latitude, -math.pi/2))\n    end\n\n-- latitude and longitude are in degrees while altitude is in meters\n\nlocal function mkMapPosition(overload, bodyId, latitude, longitude, altitude)\n    local systemId = overload -- Id or '::pos{...}' string\n\n    if isString(overload) and not longitude and not altitude and\n                              not bodyId    and not latitude then\n        systemId, bodyId, latitude, longitude, altitude =\n                                            string.match(overload, posPattern)\n        assert(systemId, 'Argument 1 (position string) is malformed.')\n    else\n        assert(isSNumber(systemId),\n               'Argument 1 (systemId) must be a number:' .. type(systemId))\n        assert(isSNumber(bodyId),\n               'Argument 2 (bodyId) must be a number:' .. type(bodyId))\n        assert(isSNumber(latitude),\n               'Argument 3 (latitude) must be in degrees:' .. type(latitude))\n        assert(isSNumber(longitude),\n               'Argument 4 (longitude) must be in degrees:' .. type(longitude))\n        assert(isSNumber(altitude),\n               'Argument 5 (altitude) must be in meters:' .. type(altitude))\n    end\n    systemId  = tonumber(systemId)\n    bodyId    = tonumber(bodyId)\n    latitude  = tonumber(latitude)\n    longitude = tonumber(longitude)\n    altitude  = tonumber(altitude)\n\n    if bodyId == 0 then -- this is a hack to represent points in space\n        return setmetatable({latitude  = latitude,\n                             longitude = longitude,\n                             altitude  = altitude,\n                             bodyId    = bodyId,\n                             systemId  = systemId}, MapPosition)\n    end\n    return setmetatable({latitude  = deg2rad*clamp(latitude, -90, 90),\n                         longitude = deg2rad*(longitude % 360),\n                         altitude  = altitude,\n                         bodyId    = bodyId,\n                         systemId  = systemId}, MapPosition)\nend\n\n-- PlanetarySystem - map body IDs to BodyParameters\n\nlocal PlanetarySystem = {}\nPlanetarySystem.__index = PlanetarySystem\n\nPlanetarySystem.__tostring =\n    function (obj, indent)\n        local sep = indent and (indent .. '  ' )\n        local bdylist = {}\n        local keys = {}\n        for k in pairs(obj) do table.insert(keys, k) end\n        table.sort(keys)\n        for _, bi in ipairs(keys) do\n            bdy = obj[bi]\n            local bdys = BodyParameters.__tostring(bdy, sep)\n            if indent then\n                table.insert(bdylist,\n                             string.format('[%s]={\\n%s\\n%s}',\n                                           bi, bdys, indent))\n            else\n                table.insert(bdylist, string.format('  [%s]=%s', bi, bdys))\n            end\n        end\n        if indent then\n            return string.format('\\n%s%s%s',\n                                 indent,\n                                 table.concat(bdylist, ',\\n' .. indent),\n                                 indent)\n        end\n        return string.format('{\\n%s\\n}', table.concat(bdylist, ',\\n'))\n    end\n\nlocal function mkPlanetarySystem(referenceTable)\n    local atlas = {}\n    local pid\n    for _, v in pairs(referenceTable) do\n        local id = v.planetarySystemId\n\n        if type(id) ~= 'number' then\n            error('Invalid planetary system ID: ' .. tostring(id))\n        elseif pid and id ~= pid then\n            error('Mismatch planetary system IDs: ' .. id .. ' and '\n                  .. pid)\n        end\n        local bid = v.bodyId\n        if type(bid) ~= 'number' then\n            error('Invalid body ID: ' .. tostring(bid))\n        elseif atlas[bid] then\n            error('Duplicate body ID: ' .. tostring(bid))\n        end\n        setmetatable(v.center, getmetatable(vec3.unit_x))\n        atlas[bid] = setmetatable(v, BodyParameters)\n        pid = id\n    end\n    return setmetatable(atlas, PlanetarySystem)\nend\n\n-- PlanetaryReference - map planetary system ID to PlanetarySystem\n\nPlanetaryReference = {}\n\nlocal function mkPlanetaryReference(referenceTable)\n    return setmetatable({ galaxyAtlas = referenceTable or {} },\n                          PlanetaryReference)\nend\n\nPlanetaryReference.__index        = \n    function(t,i)\n        if type(i) == 'number' then\n            local system = t.galaxyAtlas[i]\n            return mkPlanetarySystem(system)\n        end\n        return rawget(PlanetaryReference, i)\n    end\nPlanetaryReference.__pairs        =\n    function(obj)\n        return  function(t, k)\n                    local nk, nv = next(t, k)\n                    return nk, nv and mkPlanetarySystem(nv)\n                end, obj.galaxyAtlas, nil\n    end\nPlanetaryReference.__tostring     =\n    function (obj)\n        local pslist = {}\n        for _,ps in pairs(obj or {}) do\n            local psi = ps:getPlanetarySystemId()\n            local pss = PlanetarySystem.__tostring(ps, '    ')\n            table.insert(pslist,\n                         string.format('  [%s]={%s\\n  }', psi, pss))\n        end\n        return string.format('{\\n%s\\n}\\n', table.concat(pslist,',\\n'))\n    end\n\n\n--[[                       START OF PUBLIC INTERFACE                       ]]--\n\n\n-- PlanetaryReference CLASS METHODS:\n\n--\n-- BodyParameters - create an instance of BodyParameters class\n-- planetarySystemId  [in]: the body's planetary system ID.\n-- bodyId             [in]: the body's ID.\n-- radius             [in]: the radius in meters of the planetary body.\n-- bodyCenter         [in]: the world coordinates of the center (vec3 or table).\n-- GM                 [in]: the body's standard gravitational parameter.\n-- return: an instance of BodyParameters class.\n--\nPlanetaryReference.BodyParameters = mkBodyParameters\n\n--\n-- MapPosition - create an instance of the MapPosition class\n-- overload [in]: either a planetary system ID or a position string ('::pos...')\n-- bodyId [in]:   (ignored if overload is a position string) the body's ID.\n-- latitude [in]: (ignored if overload is a position string) the latitude.\n-- longitude [in]:(ignored if overload is a position string) the longitude.\n-- altitude [in]: (ignored if overload is a position string) the altitude.\n-- return: the class instance\n--\nPlanetaryReference.MapPosition    = mkMapPosition\n\n--\n-- PlanetarySystem - create an instance of PlanetarySystem class\n-- referenceData [in]: a table (indexed by bodyId) of body reference info.\n-- return: the class instance\n--\nPlanetaryReference.PlanetarySystem = mkPlanetarySystem\n\n--\n-- createBodyParameters - create an instance of BodyParameters class\n-- planetarySystemId  [in]: the body's planetary system ID.\n-- bodyId             [in]: the body's ID.\n-- surfaceArea        [in]: the body's surface area in square meters.\n-- aPosition          [in]: world coordinates of a position near the body.\n-- verticalAtPosition [in]: a vector pointing towards the body center.\n-- altitudeAtPosition [in]: the altitude in meters at the position.\n-- gravityAtPosition  [in]: the magnitude of the gravitational acceleration.\n-- return: an instance of BodyParameters class.\n--\nfunction PlanetaryReference.createBodyParameters(planetarySystemId,\n                                                 bodyId,\n                                                 surfaceArea,\n                                                 aPosition,\n                                                 verticalAtPosition,\n                                                 altitudeAtPosition,\n                                                 gravityAtPosition)\n    assert(isSNumber(planetarySystemId),\n           'Argument 1 (planetarySystemId) must be a number:' ..\n           type(planetarySystemId))\n    assert(isSNumber(bodyId),\n           'Argument 2 (bodyId) must be a number:' .. type(bodyId))\n    assert(isSNumber(surfaceArea),\n           'Argument 3 (surfaceArea) must be a number:' .. type(surfaceArea))\n    assert(isTable(aPosition),\n           'Argument 4 (aPosition) must be an array or vec3:' ..\n           type(aPosition))\n    assert(isTable(verticalAtPosition),\n           'Argument 5 (verticalAtPosition) must be an array or vec3:' ..\n           type(verticalAtPosition))\n    assert(isSNumber(altitudeAtPosition),\n           'Argument 6 (altitude) must be in meters:' ..\n           type(altitudeAtPosition))\n    assert(isSNumber(gravityAtPosition),\n           'Argument 7 (gravityAtPosition) must be number:' ..\n           type(gravityAtPosition))\n    local radius   = math.sqrt(surfaceArea/4/math.pi)\n    local distance = radius + altitudeAtPosition\n    local center   = vec3(aPosition) + distance*vec3(verticalAtPosition)\n    local GM       = gravityAtPosition * distance * distance\n    return mkBodyParameters(planetarySystemId, bodyId, radius, center, GM)\nend\n\n--\n-- isMapPosition - check for the presence of the 'MapPosition' fields\n-- valueToTest [in]: the value to be checked\n-- return: 'true' if all required fields are present in the input value\n--\nPlanetaryReference.isMapPosition  = isMapPosition\n\n-- PlanetaryReference INSTANCE METHODS:\n\n--\n-- getPlanetarySystem - get the planetary system using ID or MapPosition as key\n-- overload [in]: either the planetary system ID or a MapPosition that has it.\n-- return: instance of 'PlanetarySystem' class or nil on error\n--\nfunction PlanetaryReference:getPlanetarySystem(overload)\n    if self.galaxyAtlas then\n        local planetarySystemId = overload\n\n        if isMapPosition(overload) then\n            planetarySystemId = overload.systemId\n        end\n\n        if type(planetarySystemId) == 'number' then\n            local system = self.galaxyAtlas[i]\n            if system then\n                if getmetatable(nv) ~= PlanetarySystem then\n                    system = mkPlanetarySystem(system)\n                end\n                return system\n            end\n        end\n    end\n    return nil\nend\n\n-- PlanetarySystem INSTANCE METHODS:\n\n--\n-- castIntersections - Find the closest body that intersects a \"ray cast\".\n-- origin [in]: the origin of the \"ray cast\" in world coordinates\n-- direction [in]: the direction of the \"ray cast\" as a 'vec3' instance.\n-- sizeCalculator [in]: (default: returns 1.05*radius) Returns size given body.\n-- bodyIds[in]: (default: all IDs in system) check only the given IDs.\n-- return: The closest body that blocks the cast or 'nil' if none.\n--\nfunction PlanetarySystem:castIntersections(origin,\n                                           direction,\n                                           sizeCalculator,\n                                           bodyIds)\n    local sizeCalculator = sizeCalculator or \n                            function (body) return 1.05*body.radius end\n    local candidates = {}\n\n    if bodyIds then\n        for _,i in ipairs(bodyIds) do candidates[i] = self[i] end\n    else\n        bodyIds = {}\n        for k,body in pairs(self) do\n            table.insert(bodyIds, k)\n            candidates[k] = body\n        end\n    end\n    local function compare(b1,b2)\n        local v1 = candidates[b1].center - origin\n        local v2 = candidates[b2].center - origin\n        return v1:len() < v2:len()\n    end\n    table.sort(bodyIds, compare)\n    local dir = direction:normalize()\n\n    for i, id in ipairs(bodyIds) do\n        local body   = candidates[id]\n        local c_oV3  = body.center - origin\n        local radius = sizeCalculator(body)\n        local dot    = c_oV3:dot(dir)\n        local desc   = dot^2 - (c_oV3:len2() - radius^2)\n\n        if desc >= 0 then\n            local root     = math.sqrt(desc)\n            local farSide  = dot + root\n            local nearSide = dot - root\n            if nearSide > 0 then\n                return body, farSide, nearSide\n            elseif farSide > 0 then\n                return body, farSide, nil\n            end\n        end\n    end\n    return nil, nil, nil\nend\n\n--\n-- closestBody - find the closest body to a given set of world coordinates\n-- coordinates       [in]: the world coordinates of position in space\n-- return: an instance of the BodyParameters object closest to 'coordinates'\n--\nfunction PlanetarySystem:closestBody(coordinates)\n    assert(type(coordinates) == 'table', 'Invalid coordinates.')\n    local minDistance2, body\n    local coord = vec3(coordinates)\n\n    for _,params in pairs(self) do\n        local distance2 = (params.center - coord):len2()\n        if not body or distance2 < minDistance2 then\n            body         = params\n            minDistance2 = distance2\n        end\n    end\n    return body\nend\n\n--\n-- convertToBodyIdAndWorldCoordinates - map to body Id and world coordinates\n-- overload [in]: an instance of MapPosition or a position string ('::pos...)\n-- return: a vec3 instance containing the world coordinates or 'nil' on error.\n--\nfunction PlanetarySystem:convertToBodyIdAndWorldCoordinates(overload)\n    local mapPosition = overload\n    if isString(overload) then\n        mapPosition = mkMapPosition(overload)\n    end\n\n    if mapPosition.bodyId == 0 then\n        return 0, vec3(mapPosition.latitude,\n                       mapPosition.longitude,\n                       mapPosition.altitude)\n    end\n    local params = self:getBodyParameters(mapPosition)\n\n    if params then\n        return mapPosition.bodyId,\n               params:convertToWorldCoordinates(mapPosition)\n    end\nend\n\n--\n-- getBodyParameters - get or create an instance of BodyParameters class\n-- overload [in]: either an instance of MapPosition or a body's ID.\n-- return: a BodyParameters instance or 'nil' if body ID is not found.\n--\nfunction PlanetarySystem:getBodyParameters(overload)\n    local bodyId = overload\n\n    if isMapPosition(overload) then\n        bodyId = overload.bodyId\n    end\n    assert(isSNumber(bodyId),\n               'Argument 1 (bodyId) must be a number:' .. type(bodyId))\n\n    return self[bodyId]\nend\n\n--\n-- getPlanetarySystemId - get the planetary system ID for this instance\n-- return: the planetary system ID or nil if no planets are in the system.\n--\nfunction PlanetarySystem:getPlanetarySystemId()\n    local k, v = next(self)\n    return v and v.planetarySystemId\nend\n\n-- BodyParameters INSTANCE METHODS:\n\n--\n-- convertToMapPosition - create an instance of MapPosition from coordinates\n-- worldCoordinates [in]: the world coordinates of the map position.\n-- return: an instance of MapPosition class\n--\nfunction BodyParameters:convertToMapPosition(worldCoordinates)\n    assert(isTable(worldCoordinates),\n           'Argument 1 (worldCoordinates) must be an array or vec3:' ..\n           type(worldCoordinates))\n    local worldVec  = vec3(worldCoordinates) \n\n    if self.bodyId == 0 then\n        return setmetatable({latitude  = worldVec.x,\n                             longitude = worldVec.y,\n                             altitude  = worldVec.z,\n                             bodyId    = 0,\n                             systemId  = self.planetarySystemId}, MapPosition)\n    end\n    local coords    = worldVec - self.center\n    local distance  = coords:len()\n    local altitude  = distance - self.radius\n    local latitude  = 0\n    local longitude = 0\n\n    if not float_eq(distance, 0) then\n        local phi = math.atan(coords.y, coords.x)\n        longitude = phi >= 0 and phi or (2*math.pi + phi)\n        latitude  = math.pi/2 - math.acos(coords.z/distance)\n    end\n    return setmetatable({latitude  = latitude,\n                         longitude = longitude,\n                         altitude  = altitude,\n                         bodyId    = self.bodyId,\n                         systemId  = self.planetarySystemId}, MapPosition)\nend\n\n--\n-- convertToWorldCoordinates - convert a map position to world coordinates\n-- overload [in]: an instance of MapPosition or a position string ('::pos...')\n--\nfunction BodyParameters:convertToWorldCoordinates(overload)\n    local mapPosition = isString(overload) and\n                                           mkMapPosition(overload) or overload\n    if mapPosition.bodyId == 0 then -- support deep space map position\n        return vec3(mapPosition.latitude,\n                    mapPosition.longitude,\n                    mapPosition.altitude)\n    end\n    assert(isMapPosition(mapPosition),\n           'Argument 1 (mapPosition) is not an instance of \"MapPosition\".')\n    assert(mapPosition.systemId == self.planetarySystemId,\n           'Argument 1 (mapPosition) has a different planetary system ID.')\n    assert(mapPosition.bodyId == self.bodyId,\n           'Argument 1 (mapPosition) has a different planetary body ID.')\n    local xproj = math.cos(mapPosition.latitude)\n    return self.center + (self.radius + mapPosition.altitude) *\n           vec3(xproj*math.cos(mapPosition.longitude),\n                xproj*math.sin(mapPosition.longitude),\n                math.sin(mapPosition.latitude))\nend\n\n--\n-- getAltitude - calculate the altitude of a point given in world coordinates.\n-- worldCoordinates [in]: the world coordinates of the point.\n-- return: the altitude in meters\n--\nfunction BodyParameters:getAltitude(worldCoordinates)\n    return (vec3(worldCoordinates) - self.center):len() - self.radius\nend\n\n--\n-- getDistance - calculate the distance to a point given in world coordinates.\n-- worldCoordinates [in]: the world coordinates of the point.\n-- return: the distance in meters\n--\nfunction BodyParameters:getDistance(worldCoordinates)\n    return (vec3(worldCoordinates) - self.center):len()\nend\n\n--\n-- getGravity - calculate the gravity vector induced by the body.\n-- worldCoordinates [in]: the world coordinates of the point.\n-- return: the gravity vector in meter/seconds^2\n--\nfunction BodyParameters:getGravity(worldCoordinates)\n    local radial = self.center - vec3(worldCoordinates) -- directed towards body\n    local len2   = radial:len2()\n    return (self.GM/len2) * radial/math.sqrt(len2)\nend\n\n-- end of module\n\nreturn setmetatable(PlanetaryReference,\n                    { __call = function(_,...)\n                                    return mkPlanetaryReference(...)\n                               end })\n\n","filter":{"args":[],"signature":"start()","slotKey":"-3"},"key":"11"}
    ],
    "methods":[],
    "events":[]
}
