navigator.lua

--- Utilities and data relating to wilderness map navigation, both on land and sea.
--@module Navigator

Navigator = {

    _error     = -1,  -- The
    _lineInMap =  0,  -- Ship's last seen line in Navigator.maps.current
    _easting   = -1,  -- User's current easting
    _southing  = -1,  -- User's current southing

    _autoCalibrateDisabled = false,

}

Navigator.sailplans = {}
Navigator.cachedSailplan = {}

Navigator.maps = {}
Navigator.maps.full = {}

Navigator.harbourNames = {}
Navigator.lastKnownBearings = {}
Navigator.waypoints = {}

table.load(cloudDir..[[tables\Navigator_fullMap.lua]], Navigator.maps.full)
table.load(cloudDir..[[tables\waypoints.lua]], Navigator.waypoints)
table.load(cloudDir..[[tables\Navigator_harbourNames.lua]], Navigator.harbourNames)





-----------------------------------------------
--- Basic navigation
-- @section basicnav
-----------------------------------------------

----------------------------------------
--- Finds bearing and distance to waypoint from current location.
-- @tparam string targetWaypoint Shortname of the waypoint to located.
-- @treturn float Bearing to waypoint in degrees.
-- @treturn int Chebyshev distance to waypoint.
function Navigator.findWaypoint(targetWaypoint)

    targetWaypoint = string.lower(targetWaypoint)

    if Navigator.waypoints[targetWaypoint] then
        local toWPbearing = Navigator.getBearing( Navigator.easting, Navigator.southing, Navigator.waypoints[targetWaypoint].easting, Navigator.waypoints[targetWaypoint].southing )
        local toWPdistance = Navigator.getChebyshevDistance( Navigator.easting, Navigator.southing, Navigator.waypoints[targetWaypoint].easting, Navigator.waypoints[targetWaypoint].southing )
        return toWPbearing, toWPdistance
    else
        return false, false
    end

end

----------------------------------------
--- Returns the bearing to target coordinates from origin coordinates.
-- @tparam int _originX Origin point X-coordinate.
-- @tparam int _originY Origin point Y-coordinate.
-- @tparam int _targetX Target point X-coordinate.
-- @tparam int _targetY Target point Y-coordinate.
-- @treturn float Bearing to target coordinate in degrees.
-- @usage local bearing = myNavigator.getPointBearings(  418,443 , 553,221 )
-- @todo Rebuild this whole thing. It's a wreck.
function Navigator.getPointBearings(_originX,_originY,_targetX,_targetY)

    --Predeclarations

    local _quadrant = 0     -- What grid quadrant we're in, for converting to
    local _polarAngle = 0   --

    --Inverting the Y coordinate. UTM to screen coordinates, effectively.
    local _originY = math.abs(_originY) * -1
    local _targetY =  math.abs(_targetY) * -1

    --Find the target's coordinate relative to us, and we can assume we're at 0,0 to make this easy.
    local _relativeX = (_targetX - _originX)
    local _relativeY = (_targetY - _originY)

    if _relativeX == 0 and _relativeY == 0 then
        return 0
    else
        --Get the arctangent of the relative coordinates.
        local _arctanResult = math.deg(math.atan(_relativeY/_relativeX))
        --Find what quadrant our target point is in, and adjust the conversion accordingly.

        if _relativeX >= 0 then
            if _relativeY >= 0  then
                _quadrant = 1
                _polarAngle = _arctanResult
            elseif _relativeY < 0 then
                _quadrant = 4
                _polarAngle = _arctanResult + 360
            end
        elseif _relativeX < 0 then
            if _relativeY >= 0  then
                _quadrant = 2
                _polarAngle = _arctanResult + 180
            elseif _relativeY < 0 then
                _quadrant = 3
                _polarAngle = _arctanResult + 180
            end
        end

        -- Derive compass heading from polarAngle
        local _bearing = ((360 - _polarAngle) + 90)
        if _bearing > 360 then     --But if the polar angle is too small, we'll blow past 360!
            _bearing = _bearing - 360 --We'll subtract 360 if it does, that brings it back to correct.
        end

        return _bearing

    end

end

----------------------------------------
--- Returns the Chebyshev distance between two points.
-- Does not account for impassable seas or obstacles.
-- @tparam int originX Origin point X-coordinate.
-- @tparam int originY Origin point Y-coordinate.
-- @tparam int targetX Target point X-coordinate.
-- @tparam int targetY Target point Y-coordinate.
-- @treturn int Chebyshev distance to target point.
-- @usage local distance = myNavigator.getChebyshevDistance(374,804,447,878)
function Navigator.getChebyshevDistance(originX,originY,targetX,targetY)

    local cDist =  math.max( math.abs( targetX - originX ), math.abs( targetY - originY ) )

    return cDist

end

----------------------------------------
--- Locates an arbitrary number of the nearest harbours.
-- Gathers the name, distance, and bearings of the nearest harbours, and organises them for further processing.
-- Limited in number of returns by **numReturns**.
-- @tparam int numReturns The number of nearest harbours to return results for, after sorting.
-- @treturn table Indexed table containing information for the **numReturns** nearest harbours, sorted by ascending distance..
-- @usage local nearestHarbour = myNavigator.getNearestHarbours(1)[1]
function Navigator.getNearestHarbours(numReturns)

    numReturns = numReturns or 9999

    local distanceSortedHarbours = {}

    for k,v in pairs(Navigator.waypoints) do

        if v.type == "H" then
            table.insert(distanceSortedHarbours, {["name"] = v.shortName, ["distance"] = v.distance, ["bearing"] = v.bearing } )
        end

        table.sort(distanceSortedHarbours, function(a,b) return (a.distance < b.distance) end)

        for k,v in ipairs(distanceSortedHarbours) do
            if k > numReturns then
                distanceSortedHarbours[k] = nil
            end
        end

    end

    return distanceSortedHarbours

end

----------------------------------------
--- Returns a copy of the full waypoint table.
-- @treturn table A full copy of the master **Navigator.waypoints** table.
-- @usage for k,v in pairs(myNavigator.getWaypointTable()) do echo(v.name.."\n") end
function Navigator.getWaypointTable()
    return table.deepcopy(Navigator.waypoints)
end

----------------------------------------
--- Something to do with movement speed.
function Navigator.calculateMovementSpeed()

    local lastMetronTime  = 0 --stopStopWatch(tempShipStopwatch)
    local roundedBearing  = "error"
    local roundedDistance = "error"

    Navigator.updateINS()

    if Navigator.WPheading then
        roundedBearing = string.format("%.1f",Navigator.WPheading)
    end

    if Navigator.WPdistance then
        roundedDistance = Navigator.getChebyshevDistance(Navigator.easting,Navigator.southing,Navigator.WPeasting,Navigator.WPsouthing)
    end

    if roundedBearing == "225.0" or roundedBearing == "45.0" or roundedBearing == "315.0" or roundedBearing == "135.0" then
        --roundedDistance = round((roundedDistance/1.414))
    end

    --local timeToTarget = tonumber(round(roundedDistance*lastMetronTime))
    local timeToTarget = (roundedDistance*lastMetronTime)
    local timeToTargetHr,timeToTargetMn,timeToTargetSc = shms(timeToTarget,false)

    --moveCursor(70,getLineNumber())
    --cinsertText("<light_sky_blue>S/M: <white>"..lastMetronTime.."<light_sky_blue>s")

    --moveCursor(85,getLineNumber())
    --cinsertText("<light_sky_blue>TTT: <white>"..timeToTargetMn..":"..timeToTargetSc.."<light_sky_blue>s")

    resetStopWatch(tempShipStopwatch)
    startStopWatch(tempShipStopwatch)

end





--------------------------------------------------------------------------------
--- Getters/setters.
-- For creating and initialising Navigator object.
-- @section getterssetters
--------------------------------------------------------------------------------

----------------------------------------
--- Returns the player's current easting.
-- @treturn int Current Navigator easting.
function Navigator.getEasting()
    return Navigator._easting
end

----------------------------------------
--- Returns the player's current southing.
-- @treturn int Current Navigator southing.
function Navigator.getSouthing()
    return Navigator._southing
end

----------------------------------------
--- Returns the player's current CEP.
-- @treturn int Current Navigator CEP.
function Navigator.getError()
    return Navigator._error
end

----------------------------------------
--- Sets the player's current easting.
-- @tparam int input New Navigator easting.
-- @usage myNavigator.setEasting(261)
function Navigator.setEasting(input)
    input = tonumber(input)
    Navigator._easting = input
end

----------------------------------------
--- Sets the player's current southing.
-- @tparam int input New Navigator southing.
-- @usage myNavigator.setSouthing(729)
function Navigator.setSouthing(input)
    input = tonumber(input)
    Navigator._southing = input
end

----------------------------------------
--- Sets the player's current CEP.
-- @tparam int input New Navigator CEP.
-- @usage myNavigator.setError(2)
function Navigator.setError(input)
    input = tonumber(input)
    Navigator._error = input
end





--------------------------------------------------------------------------------
--- Location status
-- @section location
--------------------------------------------------------------------------------

----------------------------------------
--- Returns true if user is aboard a ship.
-- Does its best to avoid false positives, such as ferries, using a table of failure states as its metric.
-- @treturn bool Whether user is aboard a ship.
-- @usage if myNavigator.aboardShip() then...
-- @see Navigator.onSeafloor
function Navigator.aboardShip()

    local failStates = {
        -- If gmcp.Room isn't yet populatoed.
        (not gmcp.Room),
        -- If gmcp.Room.Info isn't yet populatoed.
        (not gmcp.Room.Info),
        -- If we're not even aboard a Vessel-type room.
        gmcp.Room.Info.environment ~= "Vessel",
        -- If we're on a Vessel, but there's no exits, and we're not in the crow's nest or bell... must be a ferry.
        (table.size(gmcp.Room.Info.exits) == 0) and (not string.find(string.lower(gmcp.Room.Info.name),"row's nest")) and (not string.find(string.lower(gmcp.Room.Info.name),"within a diving bell")),
        -- ??????????? Why this?????
        --string.find(gmcp.Room.Info.name,"crow's nest"),
        -- Well, being aboard the Margam doesn't help us much.
        string.find(gmcp.Room.Info.name,"Margam"),
    }

    return not table.contains(failStates,true)

end

----------------------------------------
--- Returns true if Navigator has a valid position set.
-- @treturn bool True, if valid position set.
-- @usage if myNavigator.havePosition() then...
function Navigator.havePosition()

    return ((Navigator._easting >= 0) and (Navigator._southing >= 0))

end

----------------------------------------
--- Returns true if user is in a subdivision.
-- @treturn bool True, if in subdivision.
-- @usage if myNavigator.inSubdivision() then...
function Navigator.inSubdivision()

    if not Navigator.inWildernessMap() then return false end

    local atpGrid, _, _ = string.match(gmcp.Room.Info.num,"^(%d+)(%d%d%d)(%d%d%d)$")

    return (tonumber(atpGrid) >= 17 and tonumber(atpGrid) <= 30)

end

----------------------------------------
--- Returns true if user is in the wilderness map.
-- Does not count being aboard a vessel as being in the wilderness map.
-- @treturn bool True, if in subdivision.
-- @usage if myNavigator.aboardShip() then...
-- @see Navigator.aboardShip
-- @see Navigator.onSeafloor
function Navigator.inWildernessMap()

    return (utf8.len(gmcp.Room.Info.num) >= 7)

end

----------------------------------------
--- Returns true if user is in in a seafloor area.
-- Useful to keep us from zeroing out data when we don't actually want to.
-- @treturn bool True, if in seafloor area.
-- @usage if myNavigator.onSeafloor() then...
-- @see Navigator.aboardShip
-- @see Navigator.inWildernessMap
function Navigator.onSeafloor()

    return (gmcp.Room.Info.area == "deep below the sea")

end















--------------------------------------------------------------------------------
--- Conversions.
-- @section conversions
--------------------------------------------------------------------------------

----------------------------------------
--- Converts a cardinal-direction string to heading in degrees.
-- @tparam string _cardinalDirection Direction (e.g. "north", "e", "south-southwest", "ene")
-- @treturn float Heading in degrees
function Navigator.cardinalToHeading(_cardinalDirection)

    local _cardinalDirection = string.lower(string.trim(_cardinalDirection))

    local _lookupTable = {
        ["north"]           = 360,      ["n"]   = 360,
        ["north-northeast"] = 22.5,     ["nne"] = 22.5,
        ["northeast"]       = 45,       ["ne"]  = 45,
        ["east-northeast"]  = 67.5,     ["ene"] = 67.5,
        ["east"]            = 90,       ["e"]   = 90,
        ["east-southeast"]  = 112.5,    ["ese"] = 112.5,
        ["southeast"]       = 135,      ["se"]  = 135,
        ["south-southeast"] = 157.5,    ["sse"] = 157.5,
        ["south"]           = 180,      ["s"]   = 180,
        ["south-southwest"] = 202.5,    ["ssw"] = 202.5,
        ["southwest"]       = 225,      ["sw"]  = 225,
        ["west-southwest"]  = 247.5,    ["wsw"] = 247.5,
        ["west"]            = 270,      ["w"]   = 270,
        ["west-northwest"]  = 292.5,    ["wnw"] = 292.5,
        ["northwest"]       = 315,      ["nw"]  = 315,
        ["north-northwest"] = 337.5,    ["nnw"] = 337.5,
    }

    return _lookupTable[_cardinalDirection] or -1

end

----------------------------------------
--- Converts standard three-parameter ATP coordinates to screen coordinates.
-- Used in setting our position when we have a wilderness room number.
-- @tparam int _roomNum The room number we wish to convert.
-- @treturn int Easting in screen coordinates.
-- @treturn int Southing in screen coordinates.
function Navigator.roomNumToCoords(_roomNum)

    local _roomNum_square, _roomNum_easting, _roomNum_southing = string.match(tostring(_roomNum), "^(%d+)(%d%d%d)(%d%d%d)$")
    _roomNum_square   = tonumber(_roomNum_square)
    _roomNum_easting  = tonumber(_roomNum_easting)
    _roomNum_southing = tonumber(_roomNum_southing)

    local _adjustmentMatrix = {
        [1]  = {   0,    0 },
        [2]  = { 250,    0 },
        [3]  = { 500,    0 },
        [4]  = { 750,    0 },
        [5]  = {   0,  250 },
        [6]  = { 250,  250 },
        [7]  = { 500,  250 },
        [8]  = { 750,  250 },
        [9]  = {   0,  500 },
        [10] = { 250,  500 },
        [11] = { 500,  500 },
        [12] = { 750,  500 },
        [13] = {   0,  750 },
        [14] = { 250,  750 },
        [15] = { 500,  750 },
        [16] = { 750,  750 },
        [31] = {   0, 1000 },
        [32] = { 250, 1000 },
        [33] = { 500, 1000 },
        [34] = { 750, 1000 },
        [35] = {   0, 1250 },
        [36] = { 250, 1250 },
        [37] = { 500, 1250 },
        [38] = { 750, 1250 },
        [39] = {   0, 1500 },
        [40] = { 250, 1500 },
        [41] = { 500, 1500 },
        [42] = { 750, 1500 },
    }

    local _return1 = (_roomNum_easting  + _adjustmentMatrix[_roomNum_square][1]) or -1
    local _return2 = (_roomNum_southing + _adjustmentMatrix[_roomNum_square][2]) or -1

    return _return1, _return2

end





--------------------------------------------------------------------------------
--- Auto-calibration.
-- @section autocal
--------------------------------------------------------------------------------

----------------------------------------
--- Returns current coordinates adjusted for input direction.
--  Also invoked by Figurehead for wavecall.
-- @tparam string _dirMoved Short-form direction string.
-- @tparam int _inputEasting Current Navigator easting.
-- @tparam int _inputSouthing Current Navigator southing.
-- @treturn int Adjusted easting.
-- @treturn int Adjusted southing.
-- @usage myNavigator.adjustCoords("n",625,715)
function Navigator.adjustCoords(_dirMoved,_inputEasting,_inputSouthing)

    local _errorString = "meep"
    local _cleanDirMoved = "uwu"

    _inputEasting = tonumber(_inputEasting)
    _inputSouthing = tonumber(_inputSouthing)

    local _adjustmentMatrix = {
        ["n"]  =  {  0, -1 },
        ["ne"] =  {  1, -1 },
        ["e"]  =  {  1,  0 },
        ["se"] =  {  1,  1 },
        ["s"]  =  {  0,  1 },
        ["sw"] =  { -1,  1 },
        ["w"]  =  { -1,  0 },
        ["nw"] =  { -1, -1 }
    }

    ---------------

    if type(_dirMoved) == "string" then
        _cleanDirMoved = string.lower( string.trim(_dirMoved) )
        _errorString = "\"".._cleanDirMoved.."\""
    else
        _cleanDirMoved = _dirMoved
        _errorString = tostring(_dirMoved)
    end

    assert(_adjustmentMatrix[_cleanDirMoved], "Method Navigator.adjustCoords() - _adjustmentMatrix["..tostring(_errorString).."] does not exist!")

    ---------------

    echo("Old coords: "..tostring(_inputEasting)..", "..tostring(_inputSouthing).."\n")
    echo("New coords: " .. tostring(_inputEasting  + _adjustmentMatrix[_cleanDirMoved][1]) .. ", " .. tostring(_inputSouthing + _adjustmentMatrix[_cleanDirMoved][2]) .. "\n\n" )

    ---------------

    return (_inputEasting  + _adjustmentMatrix[_cleanDirMoved][1]), (_inputSouthing + _adjustmentMatrix[_cleanDirMoved][2])

end

----------------------------------------
--- Doesn't actually autocalibrate, just takes in stuff to pass to self variables.
-- @todo Probably just throw it out tbh
-- @tparam int inputEasting New easting to set.
-- @tparam int inputSouthing New southing to set.
function Navigator.autoCal(inputEasting, inputSouthing)

    if not Navigator.autoCalibrateDisabled then

        if Navigator.easting ~= inputEasting or Navigator.southing ~= inputSouthing then
            oldEasting = Navigator.easting
            oldSouthing = Navigator.southing
            Navigator.easting = inputEasting
            Navigator.southing = inputSouthing
            Navigator.error = 0
            Navigator.movedNonCardinal = false
            --svo.prompttrigger("new calibration set", function() cecho("<green>INS auto-calibrated: <grey>"..oldEasting..","..oldSouthing.." > "..Navigator.easting..","..Navigator.southing.."") end )
            raiseEvent("ship position updated")
        elseif Navigator.easting == inputEasting and Navigator.southing == inputSouthing then
            Navigator.error = 0
            raiseEvent("ship position updated")
        end

    end

end

----------------------------------------
--- Populates searchTable with the relevant rows of text, before we begin finding candidates.
-- @tparam bool doFullSearch True, to search the entire map for candidates. False if building a limited array to search through.
function Navigator.buildSearchTable(doFullSearch)

    if doFullSearch then

        Navigator.searchTable = Navigator.fullMap
        Navigator.searchRow = 1
        Navigator.searchY = Navigator.searchRow - 1

    else -- Not implemented yet, full search is okay for now

        --[[local mapHeight = #Navigator.currentMap
        local numberOfRowsToSearch = (mapHeight + (Navigator.error * 10) + 10)


        for i=1,searchHeight do table.insert(Navigator.searchRows,Navigator.fullMap[i+])

        end
        (#Navigator.currentMap-1)/2
        Navigator.searchRow = ]]--

    end

end

----------------------------------------
--- It checks candidates?
function Navigator.checkCandidates(locationCandidates,shipColumn)

    local inLoop = true
    local reverseSearch = false
    local searchWidthPullback = 0

    while inLoop do

        inLoop = false

        -- Check if tile above is ocean before continuing!

        -- Run a comparative check on each entry
        if table.size(locationCandidates) >= 2 then
            if reverseSearch then
                local tempTable = {}
                for k,v in pairs(locationCandidates) do
                    table.insert(tempTable,{["x_pos"] = v.x_pos, ["y_pos"] = v.y_pos})
                end
                locationCandidates = table.deepcopy(tempTable)
            end
            for k,v in ipairs(locationCandidates) do
                local confidence = 0
                for i=1,Navigator.lineInMap-1+searchWidthPullback do
                    local delta = i
                    if reverseSearch then delta = i*-1 end
                    local searchTerm = string.sub( Navigator.fullMap[v.y_pos+1+delta], v.x_pos-#string.gsub(westString,"%%","")+1, v.x_pos+#string.gsub(eastString,"%%","")+1)
                    if string.find(searchTerm, Navigator.currentMap[Navigator.lineInMap+delta]) then
                        confidence = confidence + 1
                    else
                        locationCandidates[k] = nil
                        break
                    end
                    if confidence >= Navigator.lineInMap-1-searchWidthPullback-2 then
                        break
                    end
                end
            end
        end

        -- Stop if we have no entries in locationCandidates
        if table.size(locationCandidates) == 0 then
            navUpdateResult = 0
            inLoop = nil
            return
        end

        -- Stop if we have only one entry in locationCandidates
        if table.size(locationCandidates) == 1 then
            local final_x = -1
            local final_y = -1
            for k,v in pairs(locationCandidates) do
                final_x = v.x_pos final_y = v.y_pos
            end
            navUpdateResult = 1
            Navigator.autoCal(final_x, final_y)
            inLoop = nil
            return
        end

        if table.size(locationCandidates) >= 2 and not reverseSearch then
            inLoop = true
            reverseSearch = true
            --echo("DOING REVERSE SEARCH\n")
        else
            inLoop = nil
        end

        --if reverseSearch then
        --    echo("DOING FULL SEARCH\n")
        --    --inLoop = true
        --    reverseSearch = true
        --    searchWidthPullback = -1
        --end

    end

    --echo("a")

end

----------------------------------------
--- Old autocal
function Navigator.doShipAutocal()

    -- CASES --
    -- 1 - In open sea, use INS
    -- 2 - Open sea line (ship is wildcard), delta located
    -- 3 - Landmarked line, unique match
    -- 4 - Landmarked line, multiple matches


    --echo("debugWindow","\n")

    -- Get the line we want to use as the basis of our search, and the delta of said line.
    -- Remember that we have to assume anything could be beneath =, so we have to treat "  =  " as open sea!
    local searchLine, searchLineDelta = Navigator.findFirstNonBlankLine()
    Navigator.lineDelta = searchLineDelta or 0

    -- If we're in blank everything, we're in open sea and have no ability to calibrate.
    -- However, we should ignore this if we're docked. We can only be docked in so many spots!
    if Navigator.isInOpenSeas() and not Navigator.docked then
        navUpdateResult = "Open sea, in inertial mode"
        mfd.writeNavPage()
        blinkShipMovedIndicator("yellow")
        return
    end

    -- Get the strings west and east of us, to help size things later
    westString = string.sub(searchLine,1,(#searchLine-1)/2)
    eastString = string.sub(searchLine,(#searchLine-1)/2+2)

    -- Any sort of landmark? Cool, go get our locationCandidates table built
    locationCandidates = Navigator.gatherCandidates(mapLineToPattern(searchLine),Navigator.fullMap,Navigator.lineDelta)

    -------

    -- If we only found one location candidate...
    if #locationCandidates > 1 then

        -- Delete entries that're more than a certain distance away.
        local entriesDeleted = 0
        local distanceLimit = 50

        -----------

        for k,v in pairs(locationCandidates) do
            -- If it's reasonably close, delete it from the table. Don't do this if
            local candidateDistance = Navigator.getChebyshevDistance(Navigator.easting,Navigator.southing,v.x_pos,v.y_pos)
            if candidateDistance > distanceLimit and Navigator.easting >= 0 and Navigator.southing >= 0 and Navigator.error < 5 then
                locationCandidates[k] = nil
                entriesDeleted = entriesDeleted + 1
            end
        end

        -----------

        -- Go through the remaining candidates and evaluate them.
        for k,v in pairs(locationCandidates) do

            -- We have a location pair candidate
            --echo("\nLocation candidate "..k.." ("..v.x_pos..","..v.y_pos.."):\n")
            local totalMatch = false
            local candidateString = string.sub( Navigator.fullMap[v.y_pos+1], v.x_pos-#westString+1, v.x_pos+#eastString+1 )

            -- Search down from the northmost row (most negative delta)
            for i=(Navigator.lineInMap-1)*-1,Navigator.lineInMap-1 do
            local candidateString = string.sub( Navigator.fullMap[v.y_pos+1+i], v.x_pos-#westString+1, v.x_pos+#eastString+1 )
            local searchPatternString = Navigator.currentMapPatterns[Navigator.lineInMap+i]
            --echo("\""..Navigator.currentMap[Navigator.lineInMap+i].."\"")
            if string.find(candidateString,searchPatternString) then
                --echo("Match: Δ"..i.."\n")
            else
                --echo("Break: Δ"..i.."\n")
                locationCandidates[k] = nil
                break
            end
            end

        end

        -----------

        if table.size(locationCandidates) == 1 then
            for k,v in pairs(locationCandidates) do
                Navigator.easting = v.x_pos
                Navigator.southing = v.y_pos
                Navigator.error = 0
                navUpdateResult = "One candidate left ("..Navigator.easting..","..Navigator.southing..")"
            end
        end

    --checkCandidates(locationCandidates, inputColumn)
    elseif table.size(locationCandidates) == 1 then
        for k,v in pairs(locationCandidates) do
            Navigator.easting = v.x_pos
            Navigator.southing = v.y_pos-Navigator.lineDelta
            Navigator.error = 0
            navUpdateResult = "Single candidate found ("..Navigator.easting..","..Navigator.southing.." Δ"..Navigator.lineDelta..")"
        end
    else
        navUpdateResult = "Uhhhh"
    end

    -------------------------------

end

----------------------------------------
--- Finds nearest Δ candidate in Navigator.currentMap.
-- @treturn string Contents of the line containing the located reference. Used in later steps to verify location candidates, before heading into confidence calculations.
-- @treturn int Latitudinal distance from the central calibration point, Δ. Negative values are north.
-- @usage local locatorString, lineDelta = myNavigator.findLineDelta()
function Navigator.findLineDelta()

    -- If we're on an open ocean line
    if string.find(string.gsub(Navigator.currentMap[Navigator.lineInMap],"="," "), "^ +$") then

        local landFound = false
        Navigator.lineDelta = false

        -- Search southwards
        for i=1,Navigator.lineInMap-1 do
            local lineIsOcean = false
            if string.find(string.gsub(Navigator.currentMap[Navigator.lineInMap+i],"="," "), "^ +$") then
                lineIsOcean = true
            else
                lineIsOcean = false
            end
            if not lineIsOcean then
                landFound = Navigator.currentMap[Navigator.lineInMap+i]
                Navigator.lineDelta = i
                break
            end
        end

        -- Search northwards
        for i=-1,(Navigator.lineInMap-1)*-1,-1 do
            local lineIsOcean = false
            if string.find(string.gsub(Navigator.currentMap[Navigator.lineInMap+i],"="," "), "^ +$") then
                lineIsOcean = true
            else
                lineIsOcean = false
            end
            if not lineIsOcean then
                landFound = Navigator.currentMap[Navigator.lineInMap+i]
                Navigator.lineDelta = i
                break
            end
        end

        return landFound, Navigator.lineDelta

    else

        -- If not, just send back the line with delta 0.
        return Navigator.currentMap[Navigator.lineInMap], 0

    end

end

----------------------------------------
--- Gathers the candidates for our line.
-- @todo Add optional latitude/longitude limiting, for performance.
-- @tparam string searchString Map line to gather candidates for.
function Navigator.gatherCandidates(searchString)


    local candidateTable = {}
    --if Navigator.isInOpenSeas() then return {} end

    for k,v in ipairs(Navigator.fullMap) do
        local searchResult = string.find(Navigator.fullMap[k],searchString)
        if searchResult then
            table.insert(candidateTable, {
                ["x_pos"] = tonumber(searchResult + #westString - 1),
                ["y_pos"] = tonumber(k-1),
            })
        end
    end

    return candidateTable

end


----------------------------------------
-- Gets search height.
function Navigator.getSearchHeight()

    return #Navigator.currentMap

end




----------------------------------------
--- Feed our last movement direction to the INS routine. Adjusts current Navigator coords acordingly.
function Navigator.doINSmove()
    -- Feed our last movement direction to the INS routine. Adjusts current Navigator coords acordingly.
    local a = 1
end


----------------------------------------
--- See if all the lines of our map are blank spaces, to see if we even have any sort of landmarks.
function Navigator.isInOpenSeas()
    for k,v in ipairs(Navigator.currentMap) do
        if not string.find(string.gsub(v,"="," "),"^ +$") then
            return false
        end
    end
    return true
end


----------------------------------------
--- This finds things.
-- allegedly, not great though
-- @param inputTable what.
-- @param distanceLimit what.
function Navigator.trimLocationCandidates(inputTable, distanceLimit)

    local entriesDeleted = 0

    for k,v in pairs(locationCandidates) do

        -- If it's reasonably close, delete it from the table. Don't do this if
        local candidateDistance = Navigator.getChebyshevDistance(Navigator.easting,Navigator.southing,v.x_pos,v.y_pos)

        if candidateDistance > distanceLimit and Navigator.easting >= 0 and Navigator.southing >= 0 and Navigator.error < 5 then
            locationCandidates[k] = nil
            cecho("<medium_spring_green:black>Deleted candidate: <white:black>"..v.x_pos.."<medium_spring_green:black>,<white:black>"..v.y_pos.." <medium_spring_green:black>(<white:black>"..candidateDistance.."<medium_spring_green:black> Chebyshev distance)\n")
            entriesDeleted = entriesDeleted + 1
        end

    end

  cecho("<cyan:black>Deleted <white:black>"..entriesDeleted.." <cyan:black>candidates. Distance limit <white:black>"..distanceLimit.."<cyan:black>. CEP <white:black>"..Navigator.error..".\n")

end


----------------------------------------
--- Old INS stuff.
function Navigator.updateINS()

    if insDebug then
          if insStopWatch then
              stopStopWatch(insStopWatch)
              deleteStopWatch(insStopWatch)
          end
          insStopWatch = createStopWatch()
          startStopWatch(insStopWatch)
    end

    ---------------------

    --If no sailplan exists
    if not Navigator.sailplan[1] then

          Navigator.WPeasting  = -1       -- Easting of current target waypoint
          Navigator.WPsouthing = -1       -- Southing of current target waypoint
          Navigator.WPheading  = -1       -- Heading from current position to current waypoint
          Navigator.WPdistance = -1       -- Distance from current poisition to current waypoint

          Navigator.nextWPeasting  = -1   -- Easting of waypoint after current
          Navigator.nextWPsouthing = -1   -- Southing of waypoint after current
          Navigator.nextWPheading  = -1   -- Heading from current position to next waypoint
          Navigator.nextWPdistance = -1   -- Distance from current poisition to next waypoint

          Navigator.nextHeading  = -1     -- Heading between current and next waypoints
          Navigator.nextDistance = -1     -- Distance between current and next waypoints

    --If a sailplan exists
    elseif Navigator.sailplan[1] and Navigator.sailplan[(Navigator.sailplanIndex+1)] then

        local wp1 = Navigator.sailplan[Navigator.sailplanIndex]
        local wp2 = Navigator.sailplan[(Navigator.sailplanIndex+1)]

        local toWPheading = Navigator.getBearing( Navigator.easting, Navigator.southing, wp1.easting, wp1.southing )
        local toNextWPheading = Navigator.getBearing( Navigator.easting, Navigator.southing, wp2.easting, wp2.southing )
        local currentToNextWPheading = Navigator.getBearing( wp1.easting, wp1.southing, wp2.easting, wp2.southing )

        local toWPdistance = Navigator.getChebyshevDistance(Navigator.easting, Navigator.southing, wp1.easting, wp1.southing)
        local toNextWPdistance = Navigator.getChebyshevDistance(Navigator.easting, Navigator.southing, wp2.easting, wp2.southing)
        local currentToNextWPdistance = Navigator.getChebyshevDistance(wp1.easting, wp1.southing, wp2.easting, wp2.southing)

        local turnToWaypoint = false

        -- Assign to globals --

        Navigator.WPeasting  = wp1.easting        -- Easting of current target waypoint
        Navigator.WPsouthing = wp1.southing    -- Southing of current target waypoint
        Navigator.WPheading  = toWPheading        -- Heading from current position to current waypoint
        Navigator.WPdistance = toWPdistance    -- Distance from current poisition to current waypoint

        Navigator.nextWPeasting  = wp2.easting            -- Easting of waypoint after current
        Navigator.nextWPsouthing = wp2.southing        -- Southing of waypoint after current
        Navigator.nextWPheading  = toNextWPheading        -- Heading from current position to next waypoint
        Navigator.nextWPdistance = toNextWPdistance    -- Distance from current poisition to next waypoint

        Navigator.nextHeading  = currentToNextWPheading    -- Heading between current and next waypoints
        Navigator.nextDistance = currentToNextWPdistance    -- Distance between current and next waypoints

        if Navigator.isAtDestination() then
            cecho("\n<green>At destination\n\n")
            playSoundFile(cloudDir..[[Sounds\arriveDestination.wav]])
            turnToWaypoint = true
        elseif  Navigator.nextWPheading == Navigator.nextHeading   and   gmcp.Room.Info.environment == "Vessel"   and   Navigator.heading ~= Navigator.nextHeading   and   Navigator.easting ~= -1   and   Navigator.WPeasting ~= -1 then
            cecho("\n<green>Intercepted course\n\n")
            playSoundFile(cloudDir..[[Sounds\arriveDestination.wav]])
            turnToWaypoint = true
        end

        if turnToWaypoint then

            Navigator.ap.waypointTurn()

        end

    elseif Navigator.sailplan[1] and not Navigator.sailplan[2] then

        local wp1 = Navigator.sailplan[Navigator.sailplanIndex]
        local toWPheading, toWPdistance = Navigator.getBearing( Navigator.easting, Navigator.southing, wp1.easting, wp1.southing )

        Navigator.WPeasting  = wp1.easting           -- Easting of current target waypoint
        Navigator.WPsouthing = wp1.southing          -- Southing of current target waypoint
        Navigator.WPheading  = toWPheading           -- Heading from current position to current waypoint
        --Navigator.WPdistance = toWPdistance          -- Distance from current poisition to current waypoint
        Navigator.WPdistance = math.abs(Navigator.easting - wp1.easting) + math.abs(Navigator.southing - wp1.southing)

        Navigator.nextWPeasting  = -1                -- Easting of waypoint after current
        Navigator.nextWPsouthing = -1                -- Southing of waypoint after current
        Navigator.nextWPheading  = -1                -- Heading from current position to next waypoint
        Navigator.nextWPdistance = -1                -- Distance from current poisition to next waypoint

        Navigator.nextHeading  = -1                  -- Heading between current and next waypoints
        Navigator.nextDistance = -1                  -- Distance between current and next waypoints

    end

    if insStopWatch then
        local insElapsedTime = stopStopWatch(insStopWatch)
        display("INS routine time: "..insElapsedTime.."s")
        deleteStopWatch(insStopWatch)
        insStopWatch = nil
    end

    if Navigator.autopilotEnabled and Navigator.WPdistance < 10 and Navigator.speed > 0 and not Navigator.nextWpFlag then
        Navigator.nextWpFlag = true
        playSoundFile(cloudDir..[[Sounds\selfChord.wav]])
    elseif Navigator.autopilotEnabled and Navigator.WPdistance < 10 and Navigator.speed > 0 then
        Navigator.nextWpFlag = true
    else
        Navigator.nextWpFlag = nil
    end

end















-----------------------------------------------
--- Sailplan stuff
-- @section sailplan

----------------------------------------
--- Returns true if current position matches destination position.
-- @treturn bool True if at destination position.
function Navigator.isAtDestination()

    --if Navigator.easting ~= Navigator.WPeasting then
    --    return false
    --elseif Navigator.southing ~= Navigator.WPsouthing then
    --    return false
    --elseif gmcp.Room.Info.environment ~= "Vessel" then
    --    return false
    --elseif Navigator.easting == -1 then
    --    return false
    --else
    --    return true
    --end

end


----------------------------------------
--- Next steerpoint.
function Navigator.steerpointNext()

    if Navigator.sailplan[(Navigator.sailplanIndex+1)] then

        Navigator.sailplanIndex = Navigator.sailplanIndex + 1  -- Increment the index from 5 to 6, as WP6 (CITTN) is the next steerpoint

        Navigator.WPeasting  = Navigator.sailplan[Navigator.sailplanIndex].easting  -- Get the easting of CITTN
        Navigator.WPsouthing = Navigator.sailplan[Navigator.sailplanIndex].southing -- Get the southing of CITTN

    end

end

----------------------------------------
--- Previous steerpoint.
function Navigator.steerpointPrevious()

    if Navigator.sailplan[(Navigator.sailplanIndex-1)] then

        Navigator.sailplanIndex = Navigator.sailplanIndex - 1

        Navigator.WPeasting  = Navigator.sailplan[Navigator.sailplanIndex].easting
        Navigator.WPsouthing = Navigator.sailplan[Navigator.sailplanIndex].southing

    end

end




















































-----------------------------------------------
--- Autopilot
-- @section autopilot


------------------------------------------------
--- Container for autopilot functionality
Navigator.autopilot = {
    _autopilotEnabled = "",  -- If autopilot is enabled
    _stoppedToTurn = "",  -- If autopilot is stopped for purposes of turning
}

----------------------------------------
--- These do things, I guess.
-- Yep. I guess.
function Navigator.checkAlignment()      --

    --local WPheading, _ = Navigator.getBearing( Navigator.easting, Navigator.southing, Navigator.WPeasting, Navigator.WPsouthing )

    --if WPheading % 22.5 == 0 then
    --    return true
    --else
    --    return false
    --end

end



----------------------------------------
--- These do things, I guess.
-- Yep. I guess.
function Navigator.waypointTurn()

    if not Navigator.sailplan[(Navigator.sailplanIndex+1)] and not Navigator.state == 0 and gmcp.Room.Info.environment == "Vessel" then

        cecho("<green>\nDestination reached - autopilot disabled\n\n")
        Navigator.autopilotEnabled = false

    elseif Navigator.sailplan[Navigator.sailplanIndex] then

        Navigator.sailplanIndex = Navigator.sailplanIndex + 1  -- Increment the index from 5 to 6, as WP6 (CITTN) is the next steerpoint

        Navigator.WPeasting  = Navigator.sailplan[Navigator.sailplanIndex].easting  -- Get the easting of CITTN
        Navigator.WPsouthing = Navigator.sailplan[Navigator.sailplanIndex].southing -- Get the southing of CITTN

        if Navigator.autopilotEnabled then

            local postTurnHeading, postTurnDistance = Navigator.getBearing(Navigator.easting,Navigator.southing,Navigator.WPeasting,Navigator.WPsouthing)

            if Navigator.type ~= 1 then
                send("ship all stop")
                Navigator.stoppedToTurn = true
            end

            if postTurnHeading % 45 == 0 then
                send("queue add ship ship turn "..Navigator.longToNumeric())
            else
                cecho("<orange>\nSteerpoint error! - Navigator.updateINS()\n\n")
                playSoundFile(soundDir.."masterCaution.wav")
            end

        end

    end

end




















































-----------------------------------------------
--- Maps and map readers
-- @section mapobject


------------------------------------------------
--- Container for maps
Navigator.maps = {}

----------------------------------------
--- Full world map as a table. Table indices are 1 higher than their actual coordinate value!
Navigator.maps.full = {}

----------------------------------------
--- The last map sent to us by the game.
-- @usage
-- {
--   "nnnnnnnnnn....wwwwww     ",
--   "nnnnnnnnn.....wwwwwww    ",
--   "nnnnnnnn.......wwwwww    ",
--   "++++++++++++++.w.www     ",
--   "nnnnnn.........wwwww     ",
--   "nnnnnn..........ww       ",
--   "nnnnn..........ww        ",
--   "nnnnn.........ww         ",
--   "nnnnn.......=            ",
--   "nnnnn.........ww         ",
--   "nnnnn..........www       ",
--   "nnnnn.......wwwwww       ",
--   "nnnnn......wwwwww        ",
--   "nnnnnn.....www;www       ",
--   "nnnnnnn...wwwwwwwww      ",
--   "nnnnnnnn..wwwwwwwww      ",
--   "nnnnnnnn..wwwwwwww       "
-- }
Navigator.maps.current = {}


----------------------------------------
--- True if = under the cursor is our ship, as determined by text colour.
function Navigator.isOurShip()

    if string.find(matches[1],"C/S") then return false end

    local fg_r,fg_g,fg_b = getFgColor()
    local bg_r,bg_g,bg_b = getBgColor()

    return (fg_r..","..fg_g..","..fg_b == "255,255,255") and (bg_r..","..bg_g..","..bg_b == "0,0,0")

end


----------------------------------------
--- True if M under cursor is a seamonster, as determined by the text colour.
function Navigator.isSeamonster()

    if string.find(matches[1],"C/S") then return false end

    local fg_r,fg_g,fg_b = getFgColor()
    local bg_r,bg_g,bg_b = getBgColor()

    return (fg_r..","..fg_g..","..fg_b == "255,22,255") and (bg_r..","..bg_g..","..bg_b == "0,0,0")

end


----------------------------------------
--- True if = under the cursor is our ship, as determined by text colour.
function Navigator.isTargetShip()

    if string.find(matches[1],"C/S") then return false end

    local fg_r,fg_g,fg_b = getFgColor()
    local bg_r,bg_g,bg_b = getBgColor()

    return (fg_r..","..fg_g..","..fg_b == "255,22,255") and (bg_r..","..bg_g..","..bg_b == "0,0,0")

end
generated by LDoc 1.4.3 Last updated 2021-01-24 20:08:44