--
-- SoilMap
--
-- @author Stefan Maurus
-- @date 29/06/2020
--
-- Copyright (C) GIANTS Software GmbH, Confidential, All Rights Reserved.


SoilMap = {}

SoilMap.MOD_NAME = g_currentModName
SoilMap.GUI_ELEMENTS = PrecisionFarming.BASE_DIRECTORY .. "gui/ui_elements.png"

local SoilMap_mt = Class(SoilMap, ValueMap)

function SoilMap.new(pfModule, customMt)
    local self = ValueMap.new(pfModule, customMt or SoilMap_mt)

    self.filename = "precisionFarming_soilMap.grle"
    self.name = "soilMap"
    self.id = "SOIL_MAP"
    self.label = "ui_mapOverviewSoilPA"

    self.pendingSoilSamplesPerFarm = {}

    self.moneyChangeType = MoneyType.getMoneyType("other", "info_samplesAnalysed")

    self.densityMapModifiersReset = {}
    self.densityMapModifiersUncover = {}
    self.densityMapModifiersUpdate = {}
    self.densityMapModifiersResetLock = {}
    self.densityMapModifiersSellFarmland = {}
    self.densityMapModifiersFieldInfo = {}

    self.mapFrame = nil

    addConsoleCommand("paUncoverField", "Uncovers given field", "debugUncoverField", self)
    addConsoleCommand("paUncoverAll", "Uncovers all fields", "debugUncoverAll", self)
    addConsoleCommand("paReduceCoverState", "Reduces cover State for given field", "debugReduceCoverStateField", self)
    addConsoleCommand("paReduceCoverStateAll", "Reduces cover State for all fields", "debugReduceCoverStateAll", self)

    local width, height = getNormalizedScreenValues(85, 85)
    local samplingCircleOverlay = Overlay:new(SoilMap.GUI_ELEMENTS, 0, 0, width, height)
    samplingCircleOverlay:setUVs(getNormalizedUVs({3, 67, 122, 122}))
    samplingCircleOverlay:setColor(0.1, 0.5, 0.1, 0.5)
    self.samplingCircleElement = HUDElement:new(samplingCircleOverlay)

    self.minimapSamplingState = false
    self.minimapUpdateTimer = 0

    self.minimapLabelName = g_i18n:getText("ui_mapOverviewSoilPA", SoilMap.MOD_NAME)

    return self
end

function SoilMap:initialize()
    SoilMap:superClass().initialize(self)

    self.pendingSoilSamplesPerFarm = {}

    self.densityMapModifiersReset = {}
    self.densityMapModifiersUncover = {}
    self.densityMapModifiersUpdate = {}
    self.densityMapModifiersResetLock = {}
    self.densityMapModifiersSellFarmland = {}
    self.densityMapModifiersFieldInfo = {}

    self.loadFilename = nil
end

function SoilMap:delete()
    removeConsoleCommand("paUncoverField")
    removeConsoleCommand("paUncoverAll")
    removeConsoleCommand("paReduceCoverState")
    removeConsoleCommand("paReduceCoverStateAll")

    SoilMap:superClass().delete(self)
end

function SoilMap:getGlobalI18N(list)
    table.insert(list, "info_samplesAnalysed")
    table.insert(list, "ui_precisionFarming_laboratory")
    table.insert(list, "ui_precisionFarming_laboratory_idle")
    table.insert(list, "ui_precisionFarming_laboratory_analyse")
end

function SoilMap:loadFromXML(xmlFile, key, baseDirectory, configFileName, mapFilename)
    key = key .. ".soilMap"

    local missionInfo = g_currentMission.missionInfo
    local savegameFilename
    if missionInfo.savegameDirectory ~= nil then
        savegameFilename = missionInfo.savegameDirectory.."/"..self.filename
    end

    if savegameFilename ~= nil and fileExists(savegameFilename) then
        self.loadFilename = savegameFilename
    else
        local mapXMLFilename = Utils.getFilename(missionInfo.mapXMLFilename, g_currentMission.baseDirectory)
        self.mapXMLFile = loadXMLFile("MapXML", mapXMLFilename)
        local customSoilMap = getXMLString(self.mapXMLFile, "map.precisionFarming.soilMap#filename")
        if customSoilMap ~= nil then
            customSoilMap = Utils.getFilename(customSoilMap, g_currentMission.baseDirectory)

            if fileExists(customSoilMap) then
                self.loadFilename = customSoilMap
            end
        end

        local i = 0
        while true do
            local mapKey = string.format("%s.predefinedMaps.predefinedMap(%d)", key, i)
            if not hasXMLProperty(xmlFile, mapKey) then
                break
            end

            local filename = getXMLString(xmlFile, mapKey .. "#filename")
            if filename ~= nil then
                filename = Utils.getFilename(filename, baseDirectory)

                if fileExists(filename) then
                    local isDefault = getXMLBool(xmlFile, mapKey .. "#isDefault")
                    if isDefault ~= nil and isDefault then
                        if self.loadFilename == nil then
                            self.loadFilename = filename
                        end
                    end

                    local mapIdentifier = getXMLString(xmlFile, mapKey .. "#mapIdentifier")
                    if mapIdentifier ~= nil then
                        if mapFilename:find(mapIdentifier) ~= nil then
                            if self.loadFilename == nil then
                                self.loadFilename = filename
                            end
                        end
                    end
                else
                    g_logManager:xmlWarning(configFileName, "Soil map '%s' could not be found", key)
                end
            else
                g_logManager:xmlWarning(configFileName, "Unknown filename in '%s'", key)
            end

            i = i + 1
        end
    end

    self.numChannels = getXMLInt(xmlFile, key .. ".bitVectorMap#numChannels") or 3

    self.bitVectorMap = createBitVectorMap("SoilMap")
    if self.loadFilename ~= nil then
        if not loadBitVectorMapFromFile(self.bitVectorMap, self.loadFilename, self.numChannels) then
            g_logManager:xmlWarning(configFileName, "Error while loading soil map '%s'", self.loadFilename)
            self.loadFilename = nil
        else
            local size, _ = getBitVectorMapSize(self.bitVectorMap)
            if size ~= 1024 then
                self.loadFilename = nil

                g_logManager:xmlWarning(configFileName, "Found soil map with wrong size '%s'. Soil map needs to be 1024x1024!", self.loadFilename)
            else
                g_logManager:info("Load soil map '%s'", self.loadFilename)
            end
        end
    end

    if self.loadFilename == nil then
        -- if we could not load a soil map we create a dummy
        self.bitVectorMap = createBitVectorMap("SoilMap")
        loadBitVectorMapNew(self.bitVectorMap, 1024, 1024, self.numChannels, false)
    end

    self:addBitVectorMapToSync(self.bitVectorMap)
    self:addBitVectorMapToSave(self.bitVectorMap, self.filename)

    self.sizeX, self.sizeY = getBitVectorMapSize(self.bitVectorMap)

    self.bitVectorMapSoilSampleFarmId = self:loadSavedBitVectorMap("soilSampleFarmIdMap", "precisionFarming_soilSampleFarmIdMap.grle", 4, self.sizeX)
    self:addBitVectorMapToSave(self.bitVectorMapSoilSampleFarmId, "precisionFarming_soilSampleFarmIdMap.grle")

    self.typeFirstChannel = getXMLInt(xmlFile, key .. ".bitVectorMap#typeFirstChannel") or 0
    self.typeNumChannels = getXMLInt(xmlFile, key .. ".bitVectorMap#typeNumChannels") or 2
    self.coverChannel = getXMLInt(xmlFile, key .. ".bitVectorMap#coverChannel") or 2

    self.coverFirstChannel = getXMLInt(xmlFile, key .. ".soilStateMap#coverFirstChannel") or 0
    self.coverNumChannels = getXMLInt(xmlFile, key .. ".soilStateMap#coverNumChannels") or 4
    self.coverMaxValue = getXMLInt(xmlFile, key .. ".soilStateMap#coverMaxStates") or 7
    if self.coverMaxValue + 1 > 2 ^ self.coverNumChannels - 1 then
        g_logManager:xmlWarning(configFileName, "Not enough bits available for soil map cover states.")
    end

    self.lockChannel = getXMLInt(xmlFile, key .. ".soilStateMap#lockChannel") or 4

    self.sampledColor = StringUtil.getVectorNFromString(getXMLString(xmlFile, key..".sampling#sampledColor") or "0 0 0", 3)
    self.sampledColorBlind = StringUtil.getVectorNFromString(getXMLString(xmlFile, key..".sampling#sampledColorBlind") or "0 0 0", 3)
    self.sampledText = g_i18n:convertText(getXMLString(xmlFile, key..".sampling#name"), SoilMap.MOD_NAME)

    self.pricePerSample = StringUtil.getVectorNFromString(getXMLString(xmlFile, key..".sampling#pricePerSample") or "50 100 150", 3)
    self.analyseTimePerSample = (getXMLFloat(xmlFile, key..".sampling#analyseTimeSecPerSample") or 5) * 1000

    self.texts = {}
    self.texts.laboratoryIdle = g_i18n:convertText(getXMLString(xmlFile, key..".sampling.texts#idleText") or "$l10n_ui_precisionFarming_laboratory_idle", SoilMap.MOD_NAME)
    self.texts.laboratoryAnalyse = g_i18n:convertText(getXMLString(xmlFile, key..".sampling.texts#analyseText") or "$l10n_ui_precisionFarming_laboratory_analyse", SoilMap.MOD_NAME)

    self.soilTypes = {}

    local i = 0
    while true do
        local typeKey = string.format("%s.soilTypes.soilType(%d)", key, i)
        if not hasXMLProperty(xmlFile, typeKey) then
            break
        end

        local soilType = {}
        soilType.name = g_i18n:convertText(getXMLString(xmlFile, typeKey.."#name"), SoilMap.MOD_NAME)
        soilType.yieldPotential = getXMLFloat(xmlFile, typeKey.."#yieldPotential") or 1
        soilType.color = StringUtil.getVectorNFromString(getXMLString(xmlFile, typeKey.."#color"), 3) or {0, 0, 0}
        soilType.colorBlind = StringUtil.getVectorNFromString(getXMLString(xmlFile, typeKey.."#colorBlind"), 3) or {0, 0, 0}

        table.insert(self.soilTypes, soilType)

        i = i + 1
    end

    self.sharedSoilStateMap = self.pfModule.sharedSoilStateMap

    g_farmlandManager:addStateChangeListener(self)

    return true
end

function SoilMap:postLoad(xmlFile, key, baseDirectory, configFileName, mapFilename)
    if self.sharedSoilStateMap.newValueMap then
        local startTime = getTimeSec()

        if self.pfModule.pHMap ~= nil then
            self.pfModule.pHMap:setInitialState(self.bitVectorMap, self.typeFirstChannel, self.typeNumChannels, self.coverChannel)
        end
        if self.pfModule.nitrogenMap ~= nil then
            self.pfModule.nitrogenMap:setInitialState(self.bitVectorMap, self.typeFirstChannel, self.typeNumChannels, self.coverChannel)
        end

        g_logManager:devInfo("Initialized pH and Nitrogen Map in %d ms", (getTimeSec() - startTime) * 1000)
    end

    return true
end

function SoilMap:delete()
    g_farmlandManager:removeStateChangeListener(self)
end

function SoilMap:loadFromItemsXML(xmlFile, key)
    key = key .. ".soilMap"

    local i = 0
    while true do
        local baseKey = string.format("%s.pendingSoilSamples(%d)", key, i)
        if not hasXMLProperty(xmlFile, baseKey) then
            break
        end

        local farmId = getXMLInt(xmlFile, baseKey .. "#farmId")
        local timer = getXMLFloat(xmlFile, baseKey .. "#timer") or self.analyseTimePerSample
        local toAnalyse = getXMLInt(xmlFile, baseKey .. "#toAnalyse")
        local totalAnalysed = getXMLInt(xmlFile, baseKey .. "#totalAnalysed")
        if farmId ~= nil and toAnalyse ~= nil and totalAnalysed ~= nil then
            self.pendingSoilSamplesPerFarm[farmId] = {toAnalyse=toAnalyse, totalAnalysed=totalAnalysed, timer=timer}
        end

        i = i + 1
    end
end

function SoilMap:saveToXMLFile(xmlFile, key, usedModNames)
    key = key .. ".soilMap"

    local i = 0
    for farmId, data in pairs(self.pendingSoilSamplesPerFarm) do
        local baseKey = string.format("%s.pendingSoilSamples(%d)", key, i)
        setXMLInt(xmlFile, baseKey .. "#farmId", farmId)
        setXMLFloat(xmlFile, baseKey .. "#timer", data.timer)
        setXMLInt(xmlFile, baseKey .. "#toAnalyse", data.toAnalyse)
        setXMLInt(xmlFile, baseKey .. "#totalAnalysed", data.totalAnalysed)

        i = i + 1
    end
end

function SoilMap:update(dt)
    for farmId, data in pairs(self.pendingSoilSamplesPerFarm) do
        data.timer = data.timer - dt * g_currentMission.missionInfo.timeScale
        if data.timer <= 0 then
            data.toAnalyse = data.toAnalyse - 1
            if data.toAnalyse <= 0 then
                self:uncoverAnalysedArea(farmId)

                local price = self.pricePerSample[g_currentMission.missionInfo.economicDifficulty] or 0
                price = price * data.totalAnalysed
                g_currentMission:addMoney(-price, farmId, self.moneyChangeType, true, true)

                self.pendingSoilSamplesPerFarm[farmId] = nil
            else
                data.timer = self.analyseTimePerSample
            end

            self:updateLaboratoryText()
        end
    end

    if self.minimapSamplingState then
        self.samplingCircleElement:setColor(0.5, 0.5, 0.1, IngameMap.alpha)
    end

    if self.minimapUpdateTimer > 0 then
        self.minimapUpdateTimer = self.minimapUpdateTimer - dt
        if self.minimapUpdateTimer <= 0 then
            self:setMinimapRequiresUpdate(true)
        end
    end
end

function SoilMap:setMapFrame(mapFrame)
    self.mapFrame = mapFrame

    self:updateLaboratoryText()
end

function SoilMap:updateLaboratoryText()
    if self.mapFrame ~= nil then
        local farmId = g_currentMission:getFarmId()
        if self.pendingSoilSamplesPerFarm[farmId] ~= nil then
            self.mapFrame.laboratoryInfoText:setText(string.format(self.texts.laboratoryAnalyse, self.pendingSoilSamplesPerFarm[farmId].totalAnalysed), true)
        else
            self.mapFrame.laboratoryInfoText:setText(self.texts.laboratoryIdle, true)
        end
    end
end

function SoilMap:analyseSoilSamples(farmId, numSamples)
    if self.pendingSoilSamplesPerFarm[farmId] == nil then
        self.pendingSoilSamplesPerFarm[farmId] = {toAnalyse=numSamples, totalAnalysed=numSamples, timer=self.analyseTimePerSample}
    else
        self.pendingSoilSamplesPerFarm[farmId].toAnalyse = self.pendingSoilSamplesPerFarm[farmId].toAnalyse + numSamples
        self.pendingSoilSamplesPerFarm[farmId].totalAnalysed = self.pendingSoilSamplesPerFarm[farmId].totalAnalysed + numSamples
        self.pendingSoilSamplesPerFarm[farmId].timer = self.analyseTimePerSample
    end

    self:updateLaboratoryText()
end

local function worldCoordsToLocalCoords(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, size, terrainSize)
    return math.floor(size * (startWorldX+terrainSize*0.5) / terrainSize),
           math.floor(size * (startWorldZ+terrainSize*0.5) / terrainSize),
           math.floor(size * (widthWorldX+terrainSize*0.5) / terrainSize),
           math.floor(size * (widthWorldZ+terrainSize*0.5) / terrainSize),
           math.floor(size * (heightWorldX+terrainSize*0.5) / terrainSize),
           math.floor(size * (heightWorldZ+terrainSize*0.5) / terrainSize)
end

function SoilMap:analyseArea(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, state, farmId)
    if self.sharedSoilStateMap ~= nil then
        local modifier = self.densityMapModifiersReset.modifier
        local maskFilter = self.densityMapModifiersReset.maskFilter
        local modifierFarmIdMap = self.densityMapModifiersReset.modifierFarmIdMap

        if modifier == nil or maskFilter == nil or modifierFarmIdMap == nil then
            self.densityMapModifiersReset.modifier = DensityMapModifier:new(self.sharedSoilStateMap.bitVectorMap, self.coverFirstChannel, self.coverNumChannels)
            modifier = self.densityMapModifiersReset.modifier

            self.densityMapModifiersReset.maskFilter = DensityMapFilter:new(g_currentMission.terrainDetailId, g_currentMission.terrainDetailTypeFirstChannel, g_currentMission.terrainDetailTypeNumChannels)

            maskFilter = self.densityMapModifiersReset.maskFilter
            maskFilter:setValueCompareParams("greater", 0)

            self.densityMapModifiersReset.modifierFarmIdMap = DensityMapModifier:new(self.bitVectorMapSoilSampleFarmId, 0, 4)
            modifierFarmIdMap = self.densityMapModifiersReset.modifierFarmIdMap
        end

        startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ = worldCoordsToLocalCoords(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, self.sizeX, g_currentMission.terrainSize)
        modifier:setParallelogramDensityMapCoords(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, "ppp")
        modifierFarmIdMap:setParallelogramDensityMapCoords(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, "ppp")

        modifier:executeSet(state or self.coverMaxValue + 1, maskFilter)

        modifierFarmIdMap:executeSet(farmId, maskFilter)

        self.minimapUpdateTimer = 250
    end
end

function SoilMap:uncoverAnalysedArea(farmId)
    if self.sharedSoilStateMap ~= nil then
        local modifier1 = self.densityMapModifiersUncover.modifier1
        local modifier2 = self.densityMapModifiersUncover.modifier2
        local maskFilter = self.densityMapModifiersUncover.maskFilter
        local farmIdFilter = self.densityMapModifiersUncover.farmIdFilter

        if modifier1 == nil or modifier2 == nil or maskFilter == nil then
            local soilStateMap = self.sharedSoilStateMap
            self.densityMapModifiersUncover.modifier1 = DensityMapModifier:new(soilStateMap.bitVectorMap, self.coverFirstChannel, self.coverNumChannels)
            modifier1 = self.densityMapModifiersUncover.modifier1
            modifier1:setParallelogramDensityMapCoords(0, 0, 0, soilStateMap.sizeY, soilStateMap.sizeX, 0, "ppp")

            self.densityMapModifiersUncover.modifier2 = DensityMapModifier:new(self.bitVectorMap, self.coverChannel, 1)
            modifier2 = self.densityMapModifiersUncover.modifier2
            modifier2:setParallelogramDensityMapCoords(0, 0, 0, self.sizeY, self.sizeX, 0, "ppp")

            self.densityMapModifiersUncover.maskFilter = DensityMapFilter:new(soilStateMap.bitVectorMap, self.coverFirstChannel, self.coverNumChannels)
            maskFilter = self.densityMapModifiersUncover.maskFilter
            maskFilter:setValueCompareParams("equals", self.coverMaxValue + 1)

            self.densityMapModifiersUncover.farmIdFilter = DensityMapFilter:new(self.bitVectorMapSoilSampleFarmId, 0, 4)
            farmIdFilter = self.densityMapModifiersUncover.farmIdFilter
        end

        if farmId ~= nil then
            farmIdFilter:setValueCompareParams("equals", farmId)
        else
            farmIdFilter:setValueCompareParams("between", 0, 2 ^ 4 - 1)
        end

        -- uncover soil only here, so the pH and nitrogen maps can initialize the whole not uncovered area that has been sampled
        -- so after selling and rebuying the farmlands, also the pH and nitrgen maps are reset
        modifier2:executeSet(1, maskFilter, farmIdFilter)
        modifier1:executeSet(self.coverMaxValue, maskFilter, farmIdFilter)

        self.pfModule:updatePrecisionFarmingOverlays()
        self:setMinimapRequiresUpdate(true)
    end
end

function SoilMap:updateCoverArea(fruitIndex, startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, useMinForageState, ignoreLockChannel)
    if self.sharedSoilStateMap ~= nil then
        local soilStateMap = self.sharedSoilStateMap

        local ids = g_currentMission.fruits[fruitIndex]

        local modifier = self.densityMapModifiersUpdate.modifier
        local filter1 = self.densityMapModifiersUpdate.filter1
        local filter2 = self.densityMapModifiersUpdate.filter2
        local filter3 = self.densityMapModifiersUpdate.filter3

        if modifier == nil or filter1 == nil or filter2 == nil or filter3 == nil then
            self.densityMapModifiersUpdate.modifier = DensityMapModifier:new(soilStateMap.bitVectorMap, self.coverFirstChannel, self.coverNumChannels)
            modifier = self.densityMapModifiersUpdate.modifier
            modifier:setPolygonRoundingMode("inclusive")

            self.densityMapModifiersUpdate.filter1 =  DensityMapFilter:new(modifier)
            filter1 = self.densityMapModifiersUpdate.filter1

            self.densityMapModifiersUpdate.filter2 =  DensityMapFilter:new(soilStateMap.bitVectorMap, self.lockChannel, 1)
            filter2 = self.densityMapModifiersUpdate.filter2
            filter2:setValueCompareParams("equals", 0)

            self.densityMapModifiersUpdate.filter3 =  DensityMapFilter:new(soilStateMap.bitVectorMap, self.coverFirstChannel, self.coverNumChannels)
            filter3 = self.densityMapModifiersUpdate.filter3
            filter3:setValueCompareParams("between", 2, self.coverMaxValue)
        end

        if ids ~= nil and ids.id ~= nil then
            local id = ids.id
            local desc = g_fruitTypeManager:getFruitTypeByIndex(fruitIndex)

            local minState = desc.minHarvestingGrowthState
            if useMinForageState then
                minState = desc.minForageGrowthState
            end

            filter1:resetDensityMapAndChannels(id, desc.startStateChannel, desc.numStateChannels)
            filter1:setValueCompareParams("between", minState + 1, desc.maxHarvestingGrowthState + 1)    -- add 1 since growth state 0 has density value 1
        else
            -- ignore fruit type, just use it as duplicated lock channel check
            filter1:resetDensityMapAndChannels(soilStateMap.bitVectorMap, self.lockChannel, 1)
            filter1:setValueCompareParams("equals", 0)
        end

        startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ = worldCoordsToLocalCoords(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, soilStateMap.sizeX, g_currentMission.terrainSize)

        modifier:setParallelogramDensityMapCoords(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, "ppp")

        modifier:setDensityMapChannels(self.coverFirstChannel, self.coverNumChannels)
        local _, _, numPixels, _ = modifier:executeAdd(-1, filter1, filter2, filter3)

        local phMapUpdated = false
        local nMapUpdated = false

        if fruitIndex ~= nil then
            if self.pfModule.pHMap ~= nil then
                phMapUpdated = self.pfModule.pHMap:onHarvestCoverUpdate(filter1, filter2, fruitIndex, numPixels < 0, startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, useMinForageState)
            end

            if self.pfModule.nitrogenMap ~= nil then
                nMapUpdated = self.pfModule.nitrogenMap:onHarvestCoverUpdate(filter1, filter2, fruitIndex, numPixels < 0, startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, useMinForageState)
            end
        end

        -- set lock bit
        if ignoreLockChannel == nil or ignoreLockChannel == false then
            modifier:setDensityMapChannels(self.lockChannel, 1)
            modifier:executeSet(1, filter1, filter2, filter3)
        end

        return phMapUpdated, nMapUpdated
    end

    return false, false
end

function SoilMap:resetCoverLock(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ)
    if self.sharedSoilStateMap ~= nil then
        local modifier = self.densityMapModifiersResetLock.modifier
        if modifier == nil then
            self.densityMapModifiersResetLock.modifier = DensityMapModifier:new(self.sharedSoilStateMap.bitVectorMap, self.lockChannel, 1)
            modifier = self.densityMapModifiersResetLock.modifier
        end

        startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ = worldCoordsToLocalCoords(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, self.sharedSoilStateMap.sizeX, g_currentMission.terrainSize)

        modifier:setParallelogramDensityMapCoords(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, "ppp")
        modifier:executeSet(0)
    end
end

function SoilMap:onFarmlandStateChanged(farmlandId, farmId)
    if farmId == FarmlandManager.NO_OWNER_FARM_ID then
        local modifier = self.densityMapModifiersSellFarmland.modifier
        local modifierCover = self.densityMapModifiersSellFarmland.modifierCover
        local farmlandMask = self.densityMapModifiersSellFarmland.farmlandMask
        local farmlandManager = g_farmlandManager
        if modifier == nil or modifierCover == nil or farmlandMask == nil then
            self.densityMapModifiersSellFarmland.modifier = DensityMapModifier:new(self.bitVectorMap, self.coverChannel, 1)
            modifier = self.densityMapModifiersSellFarmland.modifier

            self.densityMapModifiersSellFarmland.modifierCover = DensityMapModifier:new(self.sharedSoilStateMap.bitVectorMap, self.coverFirstChannel, self.coverNumChannels)
            modifierCover = self.densityMapModifiersSellFarmland.modifierCover

            self.densityMapModifiersSellFarmland.farmlandMask = DensityMapFilter:new(farmlandManager.localMap, 0, farmlandManager.numberOfBits)
            farmlandMask = self.densityMapModifiersSellFarmland.farmlandMask
        end

        farmlandMask:setValueCompareParams("equals", farmlandId)
        modifier:executeSet(0, farmlandMask)
        modifierCover:executeSet(0, farmlandMask)

        if self.pfModule.pHMap ~= nil then
            self.pfModule.pHMap:setInitialState(self.bitVectorMap, self.typeFirstChannel, self.typeNumChannels, self.coverChannel, farmlandMask)
        end
        if self.pfModule.nitrogenMap ~= nil then
            self.pfModule.nitrogenMap:setInitialState(self.bitVectorMap, self.typeFirstChannel, self.typeNumChannels, self.coverChannel, farmlandMask)
        end

        self.pfModule:updatePrecisionFarmingOverlays()
        self:setMinimapRequiresUpdate(true)
    end
end

function SoilMap:getTypeIndexAtWorldPos(x, z)
    x = (x + g_currentMission.terrainSize * 0.5) / g_currentMission.terrainSize * self.sharedSoilStateMap.sizeX
    z = (z + g_currentMission.terrainSize * 0.5) / g_currentMission.terrainSize * self.sharedSoilStateMap.sizeY

    local coverValue = getBitVectorMapPoint(self.sharedSoilStateMap.bitVectorMap, x, z, self.coverFirstChannel, self.coverNumChannels)
    if coverValue > 1 then
        return getBitVectorMapPoint(self.bitVectorMap, x, z, self.typeFirstChannel, self.typeNumChannels) + 1
    end

    return 0
end

function SoilMap:buildOverlay(overlay, valueFilter, isColorBlindMode)
    resetDensityMapVisualizationOverlay(overlay)
    setOverlayColor(overlay, 1, 1, 1, 1)

    if self.sharedSoilStateMap ~= nil then
        -- render samples taken state also on top
        local sampledColor = self.sampledColor
        if isColorBlindMode then
            sampledColor = self.sampledColorBlind
        end
        setDensityMapVisualizationOverlayStateColor(overlay, self.sharedSoilStateMap.bitVectorMap, 0, self.coverFirstChannel, self.coverNumChannels, self.coverMaxValue + 1, sampledColor[1], sampledColor[2], sampledColor[3])
    end

    local soilMapId = self.bitVectorMap
    for i=1, #self.soilTypes do
        if valueFilter[i] then
            local soilType = self.soilTypes[i]

            local color = soilType.color
            if isColorBlindMode then
                color = soilType.colorBlind
            end
            local value = bitShiftLeft(1, self.coverChannel) + i - 1
            setDensityMapVisualizationOverlayStateColor(overlay, soilMapId, 0, self.typeFirstChannel, self.typeNumChannels + 1, value, color[1], color[2], color[3])
        end
    end
end

function SoilMap:getDisplayValues()
    if self.valuesToDisplay == nil then
        self.valuesToDisplay = {}

        for i=1, #self.soilTypes do
            local soilType = self.soilTypes[i]

            local soilTypeToDisplay = {}
            soilTypeToDisplay.colors = {}
            soilTypeToDisplay.colors[true] = {{soilType.colorBlind[1], soilType.colorBlind[2], soilType.colorBlind[3]}}
            soilTypeToDisplay.colors[false] = {{soilType.color[1], soilType.color[2], soilType.color[3]}}
            soilTypeToDisplay.description = soilType.name

            table.insert(self.valuesToDisplay, soilTypeToDisplay)
        end

        local soilTypeToDisplay = {}
        soilTypeToDisplay.colors = {}
        soilTypeToDisplay.colors[true] = {{self.sampledColorBlind[1], self.sampledColorBlind[2], self.sampledColorBlind[3]}}
        soilTypeToDisplay.colors[false] = {{self.sampledColor[1], self.sampledColor[2], self.sampledColor[3]}}
        soilTypeToDisplay.description = self.sampledText

        table.insert(self.valuesToDisplay, soilTypeToDisplay)
    end

    return self.valuesToDisplay
end

function SoilMap:getSoilTypeByIndex(soilTypeIndex)
    return self.soilTypes[soilTypeIndex]
end

function SoilMap:getYieldPotentialBySoilTypeIndex(soilTypeIndex)
    if self.soilTypes[soilTypeIndex] == nil then
        return 1
    end

    return self.soilTypes[soilTypeIndex].yieldPotential
end

function SoilMap:getValueFilter()
    if self.valueFilter == nil or self.valueFilterEnabled == nil then
        self.valueFilter = {}
        self.valueFilterEnabled = {}

        -- add one additional filter state for 'sampled' state, which is not filterable
        local numSoilTypes = #self.soilTypes
        for i=1, numSoilTypes + 1 do
            table.insert(self.valueFilter, true)
            table.insert(self.valueFilterEnabled, i <= numSoilTypes)
        end
    end

    return self.valueFilter, self.valueFilterEnabled
end

function SoilMap:getMinimapValueFilter()
    if self.minimapValueFilter == nil  then
        self.minimapValueFilter = {}

        -- show everything on minimap, independent of filter in menu
        for i=1, #self.soilTypes + 1 do
            table.insert(self.minimapValueFilter, true)
        end
    end

    return self.minimapValueFilter, self.minimapValueFilter
end

function SoilMap:getMinimapAdditionalElement()
    return self.samplingCircleElement
end

function SoilMap:getMinimapZoomFactor()
    return 0.1
end

function SoilMap:getMinimapUpdateTimeLimit()
    -- more rapid update since we don't need to update that often and so the user sees immediate results
    return 5
end

function SoilMap:setMinimapSamplingState(state)
    self.minimapSamplingState = state
    if not state then
        self.samplingCircleElement:setColor(0.1, 0.5, 0.1, 0.5)
    end
end

function SoilMap:collectFieldInfos(fieldInfoDisplayExtension)
    local name = g_i18n:getText("pa_fieldInfo_soil", SoilMap.MOD_NAME)
    fieldInfoDisplayExtension:addFieldInfo(name, self, self.updateFieldInfoDisplay, 1)
end

function SoilMap:updateFieldInfoDisplay(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ)
    local modifier = self.densityMapModifiersFieldInfo.modifier
    local maskFilter = self.densityMapModifiersFieldInfo.maskFilter
    if modifier == nil then
        self.densityMapModifiersFieldInfo.modifier = DensityMapModifier:new(self.bitVectorMap, self.typeFirstChannel, self.typeNumChannels)
        modifier = self.densityMapModifiersFieldInfo.modifier

        self.densityMapModifiersFieldInfo.maskFilter = DensityMapFilter:new(self.bitVectorMap, self.coverChannel, 1)
        maskFilter = self.densityMapModifiersFieldInfo.maskFilter
        maskFilter:setValueCompareParams("equals", 1)
    end

    startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ = worldCoordsToLocalCoords(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, self.sizeX, g_currentMission.terrainSize)
    modifier:setParallelogramDensityMapCoords(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, "ppp")

    local acc, numPixels, _ = modifier:executeGet(maskFilter)
    local soilTypeIndex = math.floor(acc / numPixels) + 1
    local soilType = self.soilTypes[soilTypeIndex]
    if soilType ~= nil then
        return soilType.name
    end

    return nil
end

function SoilMap:getHelpLinePage()
    return 1
end

function SoilMap:overwriteGameFunctions(pfModule)
    SoilMap:superClass().overwriteGameFunctions(self, pfModule)
end

function SoilMap:getCoordsFromFieldDimensions(fieldDimensions, index)
    local dimWidth = getChildAt(fieldDimensions, index)
    local dimStart = getChildAt(dimWidth, 0)
    local dimHeight = getChildAt(dimWidth, 1)

    local startWorldX, _, startWorldZ = getWorldTranslation(dimStart)
    local widthWorldX, _, widthWorldZ = getWorldTranslation(dimWidth)
    local heightWorldX, _, heightWorldZ = getWorldTranslation(dimHeight)

    return startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ
end

function SoilMap:debugUncoverField(fieldIndex)
    local field = g_fieldManager:getFieldByIndex(tonumber(fieldIndex))
    if field ~= nil and field.fieldDimensions ~= nil then
        local numDimensions = getNumOfChildren(field.fieldDimensions)
        for i=1, numDimensions do
            local startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ = self:getCoordsFromFieldDimensions(field.fieldDimensions, i-1)
            self:analyseArea(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, nil, g_farmlandManager:getFarmlandOwner(field.farmland.id))
        end

        self:uncoverAnalysedArea()
    end
end

function SoilMap:debugUncoverAll()
    for i=1, #g_fieldManager.fields do
        self:debugUncoverField(i)
    end
end

function SoilMap:debugReduceCoverStateField(fieldIndex)
    local field = g_fieldManager:getFieldByIndex(tonumber(fieldIndex))
    if field ~= nil and field.fieldDimensions ~= nil then
        local numDimensions = getNumOfChildren(field.fieldDimensions)

        for i=1, numDimensions do
            local startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ = self:getCoordsFromFieldDimensions(field.fieldDimensions, i-1)
            self:updateCoverArea(nil, startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, true)
        end

        for i=1, numDimensions do
            local startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ = self:getCoordsFromFieldDimensions(field.fieldDimensions, i-1)
            self:resetCoverLock(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ)
        end
    end

    self.pfModule:updatePrecisionFarmingOverlays()
end

function SoilMap:debugReduceCoverStateAll()
    for i=1, #g_fieldManager.fields do
        self:debugReduceCoverStateField(i)
    end
end
