--
-- NitrogenMap
--
-- @author Stefan Maurus
-- @date 18/09/2020
--
-- Copyright (C) GIANTS Software GmbH, Confidential, All Rights Reserved.


NitrogenMap = {}

NitrogenMap.MOD_NAME = g_currentModName
NitrogenMap.NUM_BITS = 4 -- max. 16 values

local NitrogenMap_mt = Class(NitrogenMap, ValueMap)

function NitrogenMap.new(pfModule, customMt)
    local self = ValueMap.new(pfModule, customMt or NitrogenMap_mt)

    self.name = "nitrogenMap"
    self.id = "N_MAP"
    self.label = "ui_mapOverviewNitrogen"

    self.densityMapModifiersInitialize = {}
    self.densityMapModifiersHarvest = {}
    self.densityMapModifiersSpray = {}
    self.densityMapModifiersDestroyFruit = {}
    self.densityMapModifiersResetLock = {}
    self.densityMapModifiersFieldInfo = {}
    self.densityMapModifiersFruitCheck = {}

    self.lastActualValue = -1
    self.lastTargetValue = -1
    self.lastYieldPotential = -1

    self.minimapGradientUVs = {getNormalizedUVs({194, 27, 252, 12})}
    self.minimapGradientColorBlindUVs = {getNormalizedUVs({194, 43, 252, 12})}
    self.minimapLabelName = g_i18n:getText("ui_mapOverviewNitrogen", NitrogenMap.MOD_NAME)

    return self
end

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

    self.densityMapModifiersInitialize = {}
    self.densityMapModifiersHarvest = {}
    self.densityMapModifiersSpray = {}
    self.densityMapModifiersDestroyFruit = {}
    self.densityMapModifiersResetLock = {}
    self.densityMapModifiersFieldInfo = {}
    self.densityMapModifiersFruitCheck = {}

    self.lastActualValue = -1
    self.lastTargetValue = -1
    self.lastYieldPotential = -1
end

function NitrogenMap:initTerrain(mission, terrainId, filename)
end

function NitrogenMap:loadFromXML(xmlFile, key, baseDirectory, configFileName, mapFilename)
    key = key .. ".nitrogenMap"

    self.firstChannel = getXMLInt(xmlFile, key .. ".soilStateMap#firstChannel") or 1
    self.numChannels = getXMLInt(xmlFile, key .. ".soilStateMap#numChannels") or 4
    self.maxValue = getXMLInt(xmlFile, key .. ".soilStateMap#maxValue") or (2 ^ self.numChannels - 1)

    self.coverFirstChannel = getXMLInt(xmlFile, key .. ".soilStateMap#coverFirstChannel") or 5
    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.noiseFilename = getXMLString(xmlFile, key .. ".noiseMap#filename")
    self.noiseNumChannels = getXMLInt(xmlFile, key .. ".noiseMap#numChannels") or 2
    self.noiseResolution = getXMLInt(xmlFile, key .. ".noiseMap#resolution") or 1024
    self.noiseMaxValue = 2 ^ self.noiseNumChannels - 1

    self.bitVectorMapNoise = createBitVectorMap("nitrogenNoiseMap")
    if self.noiseFilename ~= nil then
        self.noiseFilename = Utils.getFilename(self.noiseFilename, baseDirectory)
        if not loadBitVectorMapFromFile(self.bitVectorMapNoise, self.noiseFilename, self.noiseNumChannels) then
            g_logManager:xmlWarning(configFileName, "Error while loading pH noise map '%s'", self.noiseFilename)
            self.noiseFilename = nil
        end
    end

    if self.noiseFilename == nil then
        -- load dummy noise map if not found
        loadBitVectorMapNew(self.bitVectorMapNoise, self.noiseResolution, self.noiseResolution, self.noiseNumChannels, false)
    end

    self.bitVectorMapNStateChange = self:loadSavedBitVectorMap("nLockStateMap", "precisionFarming_nLockStateMap.grle", 2, self.noiseResolution)
    self:addBitVectorMapToSave(self.bitVectorMapNStateChange, "precisionFarming_nLockStateMap.grle")

    self.bitVectorMapNInitMask = self:loadSavedBitVectorMap("nInitMaskMap", "nInitMaskMap.grle", 1, self.noiseResolution)

    self.bitVectorMapNFruitFilterMask = self:loadSavedBitVectorMap("nFruitFilterMask", "nFruitFilterMask.grle", 1, self.noiseResolution)
    self.bitVectorMapNFruitDestroyMask = self:loadSavedBitVectorMap("nFruitDestroyMask", "nFruitDestroyMask.grle", 2, self.noiseResolution)

    self.nitrogenValues = {}

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

        local nitrogenValue = {}
        nitrogenValue.value = getXMLInt(xmlFile, baseKey .. "#value") or 0
        nitrogenValue.realValue = getXMLFloat(xmlFile, baseKey.."#realValue") or 0

        nitrogenValue.color = StringUtil.getVectorNFromString(getXMLString(xmlFile, baseKey.."#color"), 3) or {0, 0, 0}
        nitrogenValue.colorBlind = StringUtil.getVectorNFromString(getXMLString(xmlFile, baseKey.."#colorBlind"), 3) or {0, 0, 0}

        table.insert(self.nitrogenValues, nitrogenValue)

        i = i + 1
    end

    self.initialValues = {}

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

        local initialValue = {}
        initialValue.soilTypeIndex = (getXMLInt(xmlFile, baseKey .. "#soilTypeIndex") or 1) - 1
        initialValue.baseRange = StringUtil.getVectorNFromString(getXMLString(xmlFile, baseKey .. "#baseValueRange"), self.noiseMaxValue + 1)

        table.insert(self.initialValues, initialValue)

        i = i + 1
    end

    self.fertilizerUsage = {}
    self.fertilizerUsage.amountPerState = getXMLFloat(xmlFile, key .. ".fertilizerUsage#amountPerState") or 20
    self.fertilizerUsage.nAmounts = {}
    i = 0
    while true do
        local baseKey = string.format("%s.fertilizerUsage.nAmount(%d)", key, i)
        if not hasXMLProperty(xmlFile, baseKey) then
            break
        end

        local fillTypeName = getXMLString(xmlFile, baseKey .. "#fillType")
        if fillTypeName ~= nil then
            local fillTypeIndex = g_fillTypeManager:getFillTypeIndexByName(fillTypeName)
            if fillTypeIndex ~= nil and fillTypeIndex ~= FillType.UNKNOWN then
                local nAmount = {}
                nAmount.fillTypeIndex = fillTypeIndex
                nAmount.amount = getXMLFloat(xmlFile, baseKey .. "#amount") or 1
                table.insert(self.fertilizerUsage.nAmounts, nAmount)
            else
                g_logManager:xmlWarning(configFileName, "Invalid fill type for nitrogen fertilizer amount '%s'", baseKey)
            end
        else
            g_logManager:xmlWarning(configFileName, "Invalid fill type for nitrogen fertilizer amount '%s'", baseKey)
        end

        i = i + 1
    end

    self.fruitRequirements = {}
    self.registeredFruitRequirements = {}
    self:loadFruitRequirementsFromXML(configFileName, xmlFile, key)

    local missionInfo = g_currentMission.missionInfo
    local mapXMLFilename = Utils.getFilename(missionInfo.mapXMLFilename, g_currentMission.baseDirectory)
    local mapXMLFile = loadXMLFile("MapXML", mapXMLFilename)
    if mapXMLFile ~= nil then
        self:loadFruitRequirementsFromXML(missionInfo.mapXMLFilename, mapXMLFile, "map.precisionFarming")
    end

    -- those fruit types which have been not set yet will get a default value (first from xml)
    for _, fruitType in ipairs(g_fruitTypeManager:getFruitTypes()) do
        if self.registeredFruitRequirements[fruitType.index] == nil then
            local fruitRequirement = {}
            fruitRequirement.fruitTypeName = fruitType.name
            fruitRequirement.fruitType = fruitType

            fruitRequirement.bySoilType = self.fruitRequirements[1].bySoilType
            fruitRequirement.averageTargetLevel = self.fruitRequirements[1].averageTargetLevel or 0

            table.insert(self.fruitRequirements, fruitRequirement)

            g_logManager:devInfo("Use default Nitrogen requirements for fruitType '%s'", fruitType.name)
        end
    end

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

        local yieldMapping = {}
        yieldMapping.levelDifference = getXMLInt(xmlFile, baseKey .. "#levelDifference") or 0
        yieldMapping.yieldFactor = getXMLFloat(xmlFile, baseKey .. "#yieldFactor") or 1
        table.insert(self.yieldMappings, yieldMapping)

        i = i + 1
    end

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

        local fillTypeName = getXMLString(xmlFile, baseKey .. "#fillType")
        if fillTypeName ~= nil then
            local fillTypeIndex = g_fillTypeManager:getFillTypeIndexByName(fillTypeName)
            if fillTypeIndex ~= nil and fillTypeIndex ~= FillType.UNKNOWN then
                local applicationRate = {}
                applicationRate.fillTypeIndex = fillTypeIndex
                applicationRate.autoAdjustToFruit = Utils.getNoNil(getXMLBool(xmlFile, baseKey .. "#autoAdjustToFruit"), false)
                applicationRate.regularRate = getXMLFloat(xmlFile, baseKey .. "#regularRate")

                applicationRate.ratesBySoilType = {}
                local j = 0
                while true do
                    local soilKey = string.format("%s.soil(%d)", baseKey, j)
                    if not hasXMLProperty(xmlFile, soilKey) then
                        break
                    end

                    local rateBySoil = {}
                    rateBySoil.soilTypeIndex = getXMLInt(xmlFile, soilKey .. "#soilTypeIndex") or 1
                    rateBySoil.rate = getXMLInt(xmlFile, soilKey .. "#rate") or 1
                    table.insert(applicationRate.ratesBySoilType, rateBySoil)

                    j = j + 1
                end

                table.insert(self.applicationRates, applicationRate)
            else
                g_logManager:xmlWarning(configFileName, "Invalid fill type for nitrogen application rate '%s'", baseKey)
            end
        else
            g_logManager:xmlWarning(configFileName, "Invalid fill type for nitrogen application rate '%s'", baseKey)
        end

        i = i + 1
    end

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

        local levelDifferenceColor = {}
        levelDifferenceColor.levelDifference = getXMLInt(xmlFile, baseKey .. "#levelDifference") or 0
        levelDifferenceColor.color = StringUtil.getVectorNFromString(getXMLString(xmlFile, baseKey.."#color"), 3) or {0, 0, 0}
        levelDifferenceColor.colorBlind = StringUtil.getVectorNFromString(getXMLString(xmlFile, baseKey.."#colorBlind"), 3) or {0, 0, 0}

        levelDifferenceColor.color[4] = 1
        levelDifferenceColor.colorBlind[4] = 1

        levelDifferenceColor.additionalText = g_i18n:convertText(getXMLString(xmlFile, baseKey.."#text"), NitrogenMap.MOD_NAME)

        table.insert(self.levelDifferenceColors, levelDifferenceColor)

        i = i + 1
    end

    self.outdatedLabel = g_i18n:convertText(getXMLString(xmlFile, key..".texts#outdatedLabel") or "$l10n_ui_precisionFarming_outdatedData", NitrogenMap.MOD_NAME)

    self.minimapGradientLabelName = string.format("%d - %d kg/ha", self:getMinMaxValue())

    self.sharedSoilStateMap = g_precisionFarming.sharedSoilStateMap
    self.soilMap = g_precisionFarming.soilMap

    return true
end


function NitrogenMap:loadFruitRequirementsFromXML(configFileName, xmlFile, key)
    local i = 0
    while true do
        local baseKey = string.format("%s.fruitRequirements.fruitRequirement(%d)", key, i)
        if not hasXMLProperty(xmlFile, baseKey) then
            break
        end

        local isOverwrittenRequirement = false

        local fruitRequirement = {}
        fruitRequirement.fruitTypeName = getXMLString(xmlFile, baseKey .. "#fruitTypeName")
        if fruitRequirement.fruitTypeName ~= nil then
            for j=1, #self.fruitRequirements do
                local _fruitRequirement = self.fruitRequirements[j]
                if _fruitRequirement.fruitTypeName:lower() == fruitRequirement.fruitTypeName:lower() then
                    fruitRequirement = _fruitRequirement
                    isOverwrittenRequirement = true
                end
            end

            fruitRequirement.alwaysAllowFertilization = Utils.getNoNil(getXMLBool(xmlFile, baseKey .. "#alwaysAllowFertilization"), Utils.getNoNil(fruitRequirement.alwaysAllowFertilization, false))
            fruitRequirement.ignoreOverfertilization = Utils.getNoNil(getXMLBool(xmlFile, baseKey .. "#ignoreOverfertilization"), false)

            local fruitType = g_fruitTypeManager:getFruitTypeByName(fruitRequirement.fruitTypeName)
            if fruitType ~= nil then
                fruitRequirement.fruitType = fruitType

                fruitRequirement.bySoilType = fruitRequirement.bySoilType or {}

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

                    local soilSettings = {}
                    soilSettings.soilTypeIndex = (getXMLInt(xmlFile, soilKey .. "#soilTypeIndex") or 1) - 1

                    local isOverwrittenSoilType = false
                    for j=1, #fruitRequirement.bySoilType do
                        local _soilSettings = fruitRequirement.bySoilType[j]
                        if _soilSettings.soilTypeIndex == soilSettings.soilTypeIndex then
                            soilSettings = _soilSettings
                            isOverwrittenSoilType = true
                        end
                    end

                    local targetLevel = getXMLInt(xmlFile, soilKey .. "#targetLevel")
                    if targetLevel ~= nil then
                        local _, internalTarget = self:getNearestNitrogenValueFromValue(targetLevel)
                        targetLevel = internalTarget
                    end

                    soilSettings.targetLevel = targetLevel or soilSettings.targetLevel or 0

                    local reduction = getXMLInt(xmlFile, soilKey .. "#reduction")
                    if reduction ~= nil then
                        local internalReduction = math.floor(reduction / self.fertilizerUsage.amountPerState)
                        reduction = internalReduction - 1
                    end
                    soilSettings.reduction = reduction or soilSettings.reduction or 0

                    soilSettings.yieldPotential = getXMLFloat(xmlFile, soilKey .. "#yieldPotential") or soilSettings.yieldPotential or 1

                    if not isOverwrittenSoilType then
                        table.insert(fruitRequirement.bySoilType, soilSettings)
                    end

                    j = j + 1
                end

                fruitRequirement.averageTargetLevel = 0
                local numSettings = #fruitRequirement.bySoilType
                if numSettings > 0 then
                    local targetLevelSum = 0
                    for l=1, numSettings do
                        local soilSettings = fruitRequirement.bySoilType[l]
                        targetLevelSum = targetLevelSum + soilSettings.targetLevel
                    end

                    fruitRequirement.averageTargetLevel = targetLevelSum / numSettings
                end

                if not isOverwrittenRequirement then
                    table.insert(self.fruitRequirements, fruitRequirement)
                    self.registeredFruitRequirements[fruitType.index] = true
                end
            end
        else
            g_logManager:xmlWarning(configFileName, "Invalid fruit type for nitrogen fruitRequirement '%s'", baseKey)
        end

        i = i + 1
    end
end

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

function NitrogenMap:setInitialState(soilBitVector, soilTypeFirstChannel, soilTypeNumChannels, coverChannel, farmlandMask)
    local soilStateMap = self.sharedSoilStateMap
    if soilStateMap ~= nil then
        local modifier = self.densityMapModifiersInitialize.modifier
        local soilFilter = self.densityMapModifiersInitialize.soilFilter
        local noiseFilter = self.densityMapModifiersInitialize.noiseFilter
        local nInitModifier = self.densityMapModifiersInitialize.nInitModifier
        local nInitMaskFilter = self.densityMapModifiersInitialize.nInitMaskFilter

        if modifier == nil or soilFilter == nil or noiseFilter == nil or nInitModifier == nil or nInitMaskFilter == nil then
            self.densityMapModifiersInitialize.modifier = DensityMapModifier:new(soilStateMap.bitVectorMap, self.firstChannel, self.numChannels)
            modifier = self.densityMapModifiersInitialize.modifier

            self.densityMapModifiersInitialize.soilFilter = DensityMapFilter:new(soilBitVector, soilTypeFirstChannel, soilTypeNumChannels)
            soilFilter = self.densityMapModifiersInitialize.soilFilter

            self.densityMapModifiersInitialize.noiseFilter = DensityMapFilter:new(self.bitVectorMapNoise, 0, self.noiseNumChannels)
            noiseFilter = self.densityMapModifiersInitialize.noiseFilter

            self.densityMapModifiersInitialize.nInitModifier = DensityMapModifier:new(self.bitVectorMapNInitMask, 0, 1)
            nInitModifier = self.densityMapModifiersInitialize.nInitModifier

            self.densityMapModifiersInitialize.nInitMaskFilter = DensityMapFilter:new(self.bitVectorMapNInitMask, 0, 1)
            nInitMaskFilter = self.densityMapModifiersInitialize.nInitMaskFilter
            nInitMaskFilter:setValueCompareParams("equals", 1)
        end

        for i=1, #self.initialValues do
            local initialValue = self.initialValues[i]
            soilFilter:setValueCompareParams("equals", initialValue.soilTypeIndex)

            if farmlandMask ~= nil then
                nInitModifier:executeSet(0)
                nInitModifier:executeSet(1, soilFilter, farmlandMask)
            end

            for j=1, self.noiseMaxValue + 1 do
                noiseFilter:setValueCompareParams("equals", j - 1)
                if farmlandMask ~= nil then
                    modifier:executeSet(initialValue.baseRange[j], nInitMaskFilter, noiseFilter)
                else
                    modifier:executeSet(initialValue.baseRange[j], soilFilter, noiseFilter)
                end
            end
        end

        self:setMinimapRequiresUpdate(true)
    end
end

function NitrogenMap:onHarvestCoverUpdate(fruitFilter, sprayTypeFilter, fruitIndex, fruitUsed, startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, useMinForageState)
    if fruitUsed then
        local soilStateMap = self.sharedSoilStateMap
        local soilMap = self.soilMap
        if soilStateMap ~= nil and soilMap ~= nil and soilMap.bitVectorMap ~= nil then
            local modifier = self.densityMapModifiersHarvest.modifier
            local modifierLock = self.densityMapModifiersHarvest.modifierLock
            local soilFilter = self.densityMapModifiersHarvest.soilFilter
            local nFilter = self.densityMapModifiersHarvest.nFilter
            local lockFilter = self.densityMapModifiersHarvest.lockFilter

            if modifier == nil or modifierLock == nil or lockFilter == nil or soilFilter == nil or nFilter == nil then
                self.densityMapModifiersHarvest.modifier = DensityMapModifier:new(soilStateMap.bitVectorMap, self.firstChannel, self.numChannels)
                modifier = self.densityMapModifiersHarvest.modifier
                modifier:setPolygonRoundingMode("inclusive")

                self.densityMapModifiersHarvest.soilFilter = DensityMapFilter:new(soilMap.bitVectorMap, soilMap.typeFirstChannel, soilMap.typeNumChannels)
                soilFilter = self.densityMapModifiersHarvest.soilFilter

                self.densityMapModifiersHarvest.nFilter = DensityMapFilter:new(soilStateMap.bitVectorMap, self.firstChannel, self.numChannels)
                nFilter = self.densityMapModifiersHarvest.nFilter
                nFilter:setValueCompareParams("between", 1, self.maxValue)

                self.densityMapModifiersHarvest.modifierLock = DensityMapModifier:new(self.bitVectorMapNStateChange, 0, 1)
                modifierLock = self.densityMapModifiersHarvest.modifierLock
                modifierLock:setPolygonRoundingMode("inclusive")

                self.densityMapModifiersHarvest.lockFilter = DensityMapFilter:new(self.bitVectorMapNStateChange, 0, 1)
                lockFilter = self.densityMapModifiersHarvest.lockFilter
                lockFilter:setValueCompareParams("equals", 1)
            end

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

            local nMapChanged = false
            for i=1, #self.fruitRequirements do
                local fruitRequirement = self.fruitRequirements[i]
                if fruitRequirement.fruitType.index == fruitIndex then
                    for j=1, #fruitRequirement.bySoilType do
                        local soilSettings = fruitRequirement.bySoilType[j]
                        soilFilter:setValueCompareParams("equals", soilSettings.soilTypeIndex)

                        modifierLock:executeSet(1, fruitFilter, sprayTypeFilter)

                        local acc, numPixels, _ = modifier:executeGet(lockFilter, soilFilter, nFilter)
                        if numPixels > 0 then
                            local state = acc / numPixels
                            modifier:executeSet(math.max(state - (soilSettings.reduction - 1), 1), lockFilter, soilFilter, nFilter)

                            self.lastActualValue = state
                            self.lastTargetValue = soilSettings.targetLevel
                            self.lastYieldPotential = soilSettings.yieldPotential
                            self.lastIgnoreOverfertilization = fruitRequirement.ignoreOverfertilization
                            self.lastRegularNValue = fruitRequirement.averageTargetLevel
                            nMapChanged = true
                        end

                        modifierLock:executeSet(0)
                    end
                end
            end

            if nMapChanged then
                self:setMinimapRequiresUpdate(true)
            end

            return nMapChanged
        end
    end

    return false
end

local function applyAutomaticSprayRateForFillType(fruitRequirement, modifier, fruitFilter, soilFilter, nFilter, tempFruitModifier, tempFruitFilter, maskFilter, ignoreFruitFilter)
    local numPixelsChanged = 0
    local autoFruitTypeIndex = FruitType.UNKNOWN
    local autoSoilTypeIndex = 0
    local foundLevel = 0
    local targetLevel = 0
    local numChangedTypes = 0
    local appliedAutoFruitTypeRate = false

    local fruitIndex = fruitRequirement.fruitType.index
    local ids = g_currentMission.fruits[fruitIndex]
    if ids == nil or ids.id == 0 then
        return appliedAutoFruitTypeRate, foundLevel, targetLevel, numChangedTypes, numPixelsChanged, autoFruitTypeIndex, autoSoilTypeIndex
    end

    local fruitDesc = g_fruitTypeManager:getFruitTypeByIndex(fruitIndex)
    if not ignoreFruitFilter then
        tempFruitModifier:executeSet(0)
        fruitFilter:resetDensityMapAndChannels(ids.id, fruitDesc.startStateChannel, fruitDesc.numStateChannels)

        if fruitRequirement.alwaysAllowFertilization then
            -- also include cut states
            fruitFilter:setValueCompareParams("greater", 1)
        else
            fruitFilter:setValueCompareParams("between", 1, fruitDesc.minHarvestingGrowthState - 1)
        end

        tempFruitModifier:executeSet(1, fruitFilter)

        if g_precisionFarming:getIsSeasonsActive() then
            fruitFilter:setValueCompareParams("equals", 11) -- seasons initial growth state
            tempFruitModifier:executeSet(1, fruitFilter)
        end
    else
        tempFruitModifier:executeSet(1)
    end

    -- ignore outdated and covered pixels
    maskFilter:setValueCompareParams("between", 0, 1)
    tempFruitModifier:executeSet(0, maskFilter)

    local _, numPixels, _ = modifier:executeGet(tempFruitFilter)
    if numPixels > 0 then
        for j=1, #fruitRequirement.bySoilType do
            local soilSettings = fruitRequirement.bySoilType[j]
            soilFilter:setValueCompareParams("equals", soilSettings.soilTypeIndex)
            nFilter:setValueCompareParams("between", 1, soilSettings.targetLevel - 1)

            local acc
            acc, numPixels, _ = modifier:executeGet(tempFruitFilter, soilFilter, nFilter)
            if numPixels > 0 then
                local state = acc / numPixels
                foundLevel = foundLevel + state
                targetLevel = targetLevel + soilSettings.targetLevel
                numPixelsChanged = numPixelsChanged + numPixels
                numChangedTypes = numChangedTypes + 1

                modifier:executeSet(math.max(state, soilSettings.targetLevel), tempFruitFilter, soilFilter, nFilter)
                autoFruitTypeIndex = fruitIndex
                autoSoilTypeIndex = soilSettings.soilTypeIndex
                appliedAutoFruitTypeRate = true
            else
                -- check if we have valid fruit ground below, so we don't need to apply the manual value
                _, numPixels, _ = modifier:executeGet(tempFruitFilter, soilFilter)
                if numPixels > 0 then
                    appliedAutoFruitTypeRate = true
                end
            end
        end
    end

    return appliedAutoFruitTypeRate, foundLevel, targetLevel, numChangedTypes, numPixelsChanged, autoFruitTypeIndex, autoSoilTypeIndex
end

function NitrogenMap:updateSprayArea(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, sprayTypeIndex, automaticallyAdjusted, manualSprayRate, forcedFruitType)
    local soilStateMap = self.sharedSoilStateMap
    local soilMap = self.soilMap
    if soilStateMap ~= nil and soilMap ~= nil and soilMap.bitVectorMap ~= nil then
        local desc = g_sprayTypeManager:getSprayTypeByIndex(sprayTypeIndex)
        if desc ~= nil then
            if desc.isFertilizer then
                local modifier = self.densityMapModifiersSpray.modifier
                local soilFilter = self.densityMapModifiersSpray.soilFilter
                local nFilter = self.densityMapModifiersSpray.nFilter
                local maskFilter = self.densityMapModifiersSpray.maskFilter
                local fruitFilter = self.densityMapModifiersSpray.fruitFilter
                local modifierLock = self.densityMapModifiersSpray.modifierLock
                local lockFilter = self.densityMapModifiersSpray.lockFilter
                local sprayTypeFilter = self.densityMapModifiersSpray.sprayTypeFilter
                local tempFruitModifier = self.densityMapModifiersSpray.tempFruitModifier
                local tempFruitFilter = self.densityMapModifiersSpray.tempFruitFilter

                if modifier == nil or soilFilter == nil or nFilter == nil or maskFilter == nil or fruitFilter == nil or modifierLock == nil or lockFilter == nil or sprayTypeFilter == nil or tempFruitModifier == nil or tempFruitFilter == nil then
                    self.densityMapModifiersSpray.modifier = DensityMapModifier:new(soilStateMap.bitVectorMap, self.firstChannel, self.numChannels)
                    modifier = self.densityMapModifiersSpray.modifier
                    modifier:setPolygonRoundingMode("inclusive")

                    self.densityMapModifiersSpray.soilFilter = DensityMapFilter:new(soilMap.bitVectorMap, soilMap.typeFirstChannel, soilMap.typeNumChannels)
                    soilFilter = self.densityMapModifiersSpray.soilFilter

                    self.densityMapModifiersSpray.nFilter = DensityMapFilter:new(soilStateMap.bitVectorMap, self.firstChannel, self.numChannels)
                    nFilter = self.densityMapModifiersSpray.nFilter

                    self.densityMapModifiersSpray.maskFilter = DensityMapFilter:new(soilStateMap.bitVectorMap, self.coverFirstChannel, self.coverNumChannels)
                    maskFilter = self.densityMapModifiersSpray.maskFilter
                    maskFilter:setValueCompareParams("greater", 1)

                    self.densityMapModifiersSpray.fruitFilter =  DensityMapFilter:new(modifier)
                    fruitFilter = self.densityMapModifiersSpray.fruitFilter

                    self.densityMapModifiersSpray.modifierLock = DensityMapModifier:new(self.bitVectorMapNStateChange, 1, 1)
                    modifierLock = self.densityMapModifiersSpray.modifierLock
                    modifierLock:setPolygonRoundingMode("inclusive")

                    self.densityMapModifiersSpray.lockFilter = DensityMapFilter:new(self.bitVectorMapNStateChange, 1, 1)
                    lockFilter = self.densityMapModifiersSpray.lockFilter
                    lockFilter:setValueCompareParams("equals", 0)

                    self.densityMapModifiersSpray.sprayTypeFilter = DensityMapFilter:new(g_currentMission.terrainDetailId, g_currentMission.sprayFirstChannel, g_currentMission.sprayNumChannels)
                    sprayTypeFilter = self.densityMapModifiersSpray.sprayTypeFilter
                    sprayTypeFilter:setValueCompareParams("greater", 0)

                    self.densityMapModifiersSpray.tempFruitModifier = DensityMapModifier:new(self.bitVectorMapNFruitFilterMask, 0, 1)
                    tempFruitModifier = self.densityMapModifiersSpray.tempFruitModifier
                    tempFruitModifier:setPolygonRoundingMode("inclusive")

                    self.densityMapModifiersSpray.tempFruitFilter = DensityMapFilter:new(self.bitVectorMapNFruitFilterMask, 0, 1)
                    tempFruitFilter = self.densityMapModifiersSpray.tempFruitFilter
                    tempFruitFilter:setValueCompareParams("equals", 1)
                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")
                modifierLock:setParallelogramDensityMapCoords(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, "ppp")
                tempFruitModifier:setParallelogramDensityMapCoords(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, "ppp")

                local numPixelsChanged = 0
                local autoFruitTypeIndex = FruitType.UNKNOWN
                local autoSoilTypeIndex = 0
                local foundLevel = 0
                local targetLevel = 0
                local numChangedTypes = 0

                -- set all pixels which have the sprayTypeFilter not equal to our target ground type back to 0
                -- this includes also pixels which have 8 locked pixels and just 1 unlocked pixels
                -- so we need to set the pixels which have at least one locked pixels back to 1
                -- operation needs to be done in two steps since we don't have a "notequal"
                sprayTypeFilter:setValueCompareParams("between", 0, desc.groundType - 1)
                modifierLock:executeSet(0, sprayTypeFilter)

                sprayTypeFilter:setValueCompareParams("greater", desc.groundType)
                modifierLock:executeSet(0, sprayTypeFilter)

                sprayTypeFilter:setValueCompareParams("equals", desc.groundType)
                modifierLock:executeSet(1, sprayTypeFilter)

                sprayTypeFilter:setValueCompareParams("equals", 0)

                -- if the field is not sampled we apply a default spray rate
                maskFilter:setValueCompareParams("greater", 1)
                local _, unlockedPixels, _ = modifier:executeGet(maskFilter)
                if unlockedPixels == 0 and automaticallyAdjusted then
                    automaticallyAdjusted = false

                    for i=1, #self.applicationRates do
                        local applicationRate = self.applicationRates[i]
                        if applicationRate.fillTypeIndex == desc.fillType.index then
                            manualSprayRate = applicationRate.regularRate / self.fertilizerUsage.amountPerState
                        end
                    end
                elseif unlockedPixels > 0 then
                    -- lock outdated or covered pixels
                    maskFilter:setValueCompareParams("between", 0, 1)
                    modifierLock:executeSet(1, maskFilter)
                end

                for ai=1, #self.applicationRates do
                    local applicationRate = self.applicationRates[ai]
                    if applicationRate.fillTypeIndex == desc.fillType.index then
                        local appliedAutoFruitTypeRate = false
                        if applicationRate.autoAdjustToFruit and automaticallyAdjusted and forcedFruitType == nil then
                            for i=1, #self.fruitRequirements do
                                local fruitRequirement = self.fruitRequirements[i]

                                local _appliedAutoFruitTypeRate,
                                _foundLevel,
                                _targetLevel,
                                _numChangedTypes,
                                _numPixelsChanged,
                                _autoFruitTypeIndex,
                                _autoSoilTypeIndex = applyAutomaticSprayRateForFillType(fruitRequirement, modifier, fruitFilter, soilFilter, nFilter, tempFruitModifier, tempFruitFilter, maskFilter, false)

                                appliedAutoFruitTypeRate = appliedAutoFruitTypeRate or _appliedAutoFruitTypeRate

                                foundLevel = foundLevel + _foundLevel
                                targetLevel = targetLevel + _targetLevel
                                numPixelsChanged = numPixelsChanged + _numPixelsChanged
                                numChangedTypes = numChangedTypes + _numChangedTypes

                                if _autoFruitTypeIndex ~= FruitType.UNKNOWN then
                                    autoFruitTypeIndex = _autoFruitTypeIndex
                                end
                                if _autoSoilTypeIndex > 0 then
                                    autoSoilTypeIndex = _autoSoilTypeIndex
                                end
                            end
                        end

                        -- if no fruit is planted yet, we adjust the rate so it would match the first fruit from the xml (wheat)
                        if applicationRate.autoAdjustToFruit and not appliedAutoFruitTypeRate and automaticallyAdjusted then
                            local fruitRequirement = self.fruitRequirements[1]
                            if forcedFruitType ~= nil then
                                for i=1, #self.fruitRequirements do
                                    if self.fruitRequirements[i].fruitType.index == forcedFruitType then
                                        fruitRequirement = self.fruitRequirements[i]
                                    end
                                end

                                autoFruitTypeIndex = forcedFruitType
                            end

                            local _,
                            _foundLevel,
                            _targetLevel,
                            _numChangedTypes,
                            _numPixelsChanged,
                            _,
                            _autoSoilTypeIndex = applyAutomaticSprayRateForFillType(fruitRequirement, modifier, fruitFilter, soilFilter, nFilter, tempFruitModifier, tempFruitFilter, maskFilter, true)

                            foundLevel = foundLevel + _foundLevel
                            targetLevel = targetLevel + _targetLevel
                            numPixelsChanged = numPixelsChanged + _numPixelsChanged
                            numChangedTypes = numChangedTypes + _numChangedTypes

                            if _autoSoilTypeIndex > 0 then
                                autoSoilTypeIndex = _autoSoilTypeIndex
                            end
                        end

                        if not applicationRate.autoAdjustToFruit and automaticallyAdjusted then
                            for i=1, #applicationRate.ratesBySoilType do
                                local rateBySoilType = applicationRate.ratesBySoilType[i]

                                soilFilter:setValueCompareParams("equals", rateBySoilType.soilTypeIndex - 1)

                                -- the pixels which would be higher then the max. value we set directly to the max value
                                nFilter:setValueCompareParams("between", self.maxValue - rateBySoilType.rate + 1, self.maxValue)
                                local acc, numPixels, _ = modifier:executeSet(self.maxValue, lockFilter, soilFilter, nFilter)
                                if numPixels > 0 then
                                    local state = acc / numPixels
                                    foundLevel = foundLevel + state
                                    targetLevel = targetLevel + self.maxValue
                                    numPixelsChanged = numPixelsChanged + numPixels
                                    numChangedTypes = numChangedTypes + 1
                                    autoSoilTypeIndex = rateBySoilType.soilTypeIndex
                                end

                                nFilter:setValueCompareParams("between", 1, self.maxValue - rateBySoilType.rate)
                                acc, numPixels, _ = modifier:executeGet(lockFilter, soilFilter, nFilter)
                                if numPixels > 0 then
                                    local state = acc / numPixels
                                    foundLevel = foundLevel + state
                                    targetLevel = targetLevel + state + rateBySoilType.rate
                                    numPixelsChanged = numPixelsChanged + numPixels
                                    numChangedTypes = numChangedTypes + 1

                                    modifier:executeAdd(rateBySoilType.rate, lockFilter, soilFilter, nFilter)

                                    autoSoilTypeIndex = rateBySoilType.soilTypeIndex
                                end
                            end
                        elseif not automaticallyAdjusted then
                            local acc, numPixels, _ = modifier:executeGet(lockFilter)
                            if numPixels > 0 then
                                local currentState = acc / numPixels

                                -- the pixels which would be higher then the max. value we set directly to the max value
                                nFilter:setValueCompareParams("between", self.maxValue - manualSprayRate + 1, self.maxValue)
                                modifier:executeSet(self.maxValue, lockFilter, nFilter)

                                nFilter:setValueCompareParams("between", 1, self.maxValue - manualSprayRate)
                                modifier:executeAdd(manualSprayRate, lockFilter, nFilter)

                                foundLevel = foundLevel + currentState
                                targetLevel = targetLevel + math.min(currentState + manualSprayRate, self.maxValue)
                                numPixelsChanged = numPixelsChanged + numPixels
                                numChangedTypes = numChangedTypes + 1

                                if unlockedPixels == 0 then
                                    foundLevel = 0
                                    targetLevel = manualSprayRate
                                    numChangedTypes = 1
                                end
                            end
                        end

                        break
                    end
                end

                foundLevel = math.floor(foundLevel / numChangedTypes)
                targetLevel = math.floor(targetLevel / numChangedTypes)
                local changeLevel = targetLevel - foundLevel

                if numPixelsChanged > 0 then
                    self:setMinimapRequiresUpdate(true)
                end

                return numPixelsChanged, autoFruitTypeIndex, autoSoilTypeIndex, foundLevel, targetLevel, changeLevel, automaticallyAdjusted
            end
        end
    end

    return 0
end

function NitrogenMap:postUpdateSprayArea(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, sprayTypeIndex, automaticallyAdjusted, manualSprayRate)
    local soilStateMap = self.sharedSoilStateMap
    local soilMap = self.soilMap
    if soilStateMap ~= nil and soilMap ~= nil and soilMap.bitVectorMap ~= nil then
        local desc = g_sprayTypeManager:getSprayTypeByIndex(sprayTypeIndex)
        if desc ~= nil then
            if desc.isFertilizer then
                local modifierLock = self.densityMapModifiersSpray.modifierLock
                local sprayTypeFilter = self.densityMapModifiersSpray.sprayTypeFilter

                if modifierLock ~= nil and sprayTypeFilter ~= nil then
                    startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ = worldCoordsToLocalCoords(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, soilStateMap.sizeX, g_currentMission.terrainSize)
                    modifierLock:setParallelogramDensityMapCoords(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, "ppp")

                    -- reset all and set all pixels which have at least one locked pixel to 1
                    modifierLock:setPolygonRoundingMode("inclusive")
                    modifierLock:executeSet(0)
                    sprayTypeFilter:setValueCompareParams("equals", desc.groundType)
                    modifierLock:executeSet(1, sprayTypeFilter)
                end
            end
        end
    end

    return 0
end

function NitrogenMap:updateDestroyCommonArea(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, limitToField, addFertilizerLock)
    local soilStateMap = self.sharedSoilStateMap
    local soilMap = self.soilMap
    if soilStateMap ~= nil and soilMap ~= nil and soilMap.bitVectorMap ~= nil then

        local modifier = self.densityMapModifiersDestroyFruit.modifier
        local nFilter = self.densityMapModifiersDestroyFruit.nFilter
        local fruitFilter = self.densityMapModifiersDestroyFruit.fruitFilter
        local modifierLock = self.densityMapModifiersDestroyFruit.modifierLock
        local lockFilter = self.densityMapModifiersDestroyFruit.lockFilter
        local fruitIndices = self.densityMapModifiersDestroyFruit.fruitIndices

        if modifier == nil or nFilter == nil or fruitFilter == nil or modifierLock == nil or lockFilter == nil or fruitIndices == nil then
            self.densityMapModifiersDestroyFruit.modifier = DensityMapModifier:new(soilStateMap.bitVectorMap, self.firstChannel, self.numChannels)
            modifier = self.densityMapModifiersDestroyFruit.modifier
            modifier:setPolygonRoundingMode("inclusive")

            self.densityMapModifiersDestroyFruit.nFilter = DensityMapFilter:new(soilStateMap.bitVectorMap, self.firstChannel, self.numChannels)
            nFilter = self.densityMapModifiersDestroyFruit.nFilter
            nFilter:setValueCompareParams("between", 0, self.maxValue - 1)

            self.densityMapModifiersDestroyFruit.fruitFilter =  DensityMapFilter:new(modifier)
            fruitFilter = self.densityMapModifiersDestroyFruit.fruitFilter

            self.densityMapModifiersDestroyFruit.modifierLock = DensityMapModifier:new(self.bitVectorMapNFruitDestroyMask, 0, 2)
            modifierLock = self.densityMapModifiersDestroyFruit.modifierLock
            modifierLock:setPolygonRoundingMode("inclusive")

            self.densityMapModifiersDestroyFruit.lockFilter = DensityMapFilter:new(self.bitVectorMapNFruitDestroyMask, 0, 2)
            lockFilter = self.densityMapModifiersDestroyFruit.lockFilter
            lockFilter:setValueCompareParams("equals", 1)

            self.densityMapModifiersDestroyFruit.fruitIndices = {}
            fruitIndices = self.densityMapModifiersDestroyFruit.fruitIndices

            for i=1, 15 do
                fruitIndices[i] = {index=0, id=0, active=false}
            end
        end

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

        for i=1, 15 do
            fruitIndices[i].active = false
        end

        modifierLock:executeSet(0)
        for index, entry in pairs(g_currentMission.fruits) do
            local desc = g_fruitTypeManager:getFruitTypeByIndex(index)

            if desc.weed == nil then
                fruitFilter:resetDensityMapAndChannels(entry.id, desc.startStateChannel, desc.numStateChannels)
                fruitFilter:setValueCompareParams("between", 2, desc.cutState-1)

                -- set lock mask to '1' on every pixel where fruit is and track the fruit types we detected
                -- so we save some performance while doing the post checks
                local _, numPixels = modifierLock:executeSet(1, fruitFilter)
                if numPixels > 0 then
                    for i=1, 15 do
                        if not fruitIndices[i].active then
                            fruitIndices[i].index = index
                            fruitIndices[i].id = entry.id
                            fruitIndices[i].active = true
                            break
                        end
                    end
                end
            end
        end
    end

    return 0
end

function NitrogenMap:postUpdateDestroyCommonArea(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, limitToField, addFertilizerLock)
    local soilStateMap = self.sharedSoilStateMap
    local soilMap = self.soilMap
    if soilStateMap ~= nil and soilMap ~= nil and soilMap.bitVectorMap ~= nil then
        local modifier = self.densityMapModifiersDestroyFruit.modifier
        local nFilter = self.densityMapModifiersDestroyFruit.nFilter
        local fruitFilter = self.densityMapModifiersDestroyFruit.fruitFilter
        local modifierLock = self.densityMapModifiersDestroyFruit.modifierLock
        local lockFilter = self.densityMapModifiersDestroyFruit.lockFilter
        local fruitIndices = self.densityMapModifiersDestroyFruit.fruitIndices

        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")

        for i=1, 15 do
            if fruitIndices[i].active then
                local desc = g_fruitTypeManager:getFruitTypeByIndex(fruitIndices[i].index)

                fruitFilter:resetDensityMapAndChannels(fruitIndices[i].id, desc.startStateChannel, desc.numStateChannels)
                fruitFilter:setValueCompareParams("between", 2, desc.cutState-1)

                -- set lock mask to '2' where we still have fruit
                -- so if the lock mask is still at '1' the fruit has been removed at this spot
                modifierLock:executeSet(2, fruitFilter)
            else
                break
            end
        end

        -- set all to max. value that would go above max value
        nFilter:setValueCompareParams("equals", self.maxValue - 1)
        modifier:executeSet(self.maxValue, lockFilter, nFilter)

        -- increase level on others
        nFilter:setValueCompareParams("between", 0, self.maxValue - 1)
        modifier:executeAdd(2, lockFilter, nFilter)
    end

    return 0
end

function NitrogenMap:getLevelAtWorldPos(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.sharedSoilStateMap.bitVectorMap, x, z, self.firstChannel, self.numChannels)
    end

    return 0
end

function NitrogenMap:getTargetLevelAtWorldPos(x, z, size, forcedFruitType, fillType, nLevel)
    local soilMap = self.soilMap
    if soilMap ~= nil and soilMap.bitVectorMap ~= nil then
        size = size or 1
        local modifierFruit = self.densityMapModifiersFruitCheck.modifierFruit

        if modifierFruit == nil then
            self.densityMapModifiersFruitCheck.modifierFruit = DensityMapModifier:new(g_currentMission.terrainDetailId, g_currentMission.sprayFirstChannel, g_currentMission.sprayNumChannels)
            modifierFruit = self.densityMapModifiersFruitCheck.modifierFruit
        end

        local lx = (x + g_currentMission.terrainSize * 0.5) / g_currentMission.terrainSize * soilMap.sizeX
        local lz = (z + g_currentMission.terrainSize * 0.5) / g_currentMission.terrainSize * soilMap.sizeY

        local coverValue = getBitVectorMapPoint(self.sharedSoilStateMap.bitVectorMap, lx, lz, self.coverFirstChannel, self.coverNumChannels)
        if coverValue > 1 then
            local soilTypeIndex = getBitVectorMapPoint(soilMap.bitVectorMap, lx, lz, soilMap.typeFirstChannel, soilMap.typeNumChannels)

            -- in case we have a fill type that is not auto adjusting to plant we check for fixed spray rate by soil type
            if fillType ~= nil and fillType ~= FillType.UNKNOWN then
                for i=1, #self.applicationRates do
                    local applicationRate = self.applicationRates[i]
                    if applicationRate.fillTypeIndex == fillType then
                        if not applicationRate.autoAdjustToFruit then
                            for j=1, #applicationRate.ratesBySoilType do
                                local rateBySoilType = applicationRate.ratesBySoilType[j]
                                if rateBySoilType.soilTypeIndex == soilTypeIndex + 1 then
                                    return (nLevel or 0) + rateBySoilType.rate, soilTypeIndex + 1, FruitType.UNKNOWN
                                end
                            end
                        end
                    end
                end
            end

            local halfSize = size * 0.5
            local startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ = x + halfSize, z + halfSize, x - halfSize, z + halfSize, x + halfSize, z - halfSize
            modifierFruit:setParallelogramWorldCoords(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, "ppp")

            local foundFruitTypeIndex = forcedFruitType
            if foundFruitTypeIndex == nil then
                for index, entry in pairs(g_currentMission.fruits) do
                    local desc = g_fruitTypeManager:getFruitTypeByIndex(index)
                    if desc.weed == nil then
                        modifierFruit:resetDensityMapAndChannels(entry.id, desc.startStateChannel, desc.numStateChannels)

                        local acc, numPixels, _ = modifierFruit:executeGet()
                        if numPixels > 0 then
                            local state = math.floor(acc / numPixels) - 1

                            local allowCutState = false
                            for i=1, #self.fruitRequirements do
                                if self.fruitRequirements[i].fruitType.index == index then
                                    allowCutState = self.fruitRequirements[i].alwaysAllowFertilization
                                end
                            end

                            if state >= 0 and state ~= desc.cutState or allowCutState then
                                foundFruitTypeIndex = index
                                break
                            end
                        end
                    end
                end
            end

            local fruitRequirement
            if foundFruitTypeIndex ~= nil then
                for i=1, #self.fruitRequirements do
                    if self.fruitRequirements[i].fruitType.index == foundFruitTypeIndex then
                        fruitRequirement = self.fruitRequirements[i]
                        break
                    end
                end
            else
                fruitRequirement = self.fruitRequirements[1]
            end

            if fruitRequirement ~= nil then
                for j=1, #fruitRequirement.bySoilType do
                    local soilSettings = fruitRequirement.bySoilType[j]
                    if soilSettings.soilTypeIndex == soilTypeIndex then
                        return soilSettings.targetLevel, soilTypeIndex + 1, foundFruitTypeIndex
                    end
                end
            end

            return 0, soilTypeIndex + 1, FruitType.UNKNOWN
        end
    end

    return 0, 0, FruitType.UNKNOWN
end

function NitrogenMap:getFertilizerUsage(workingWidth, speedLimit, statesChanged, fillTypeIndex, dt, sprayAmountAutoMode, nApplyAutoModeFruitType, actualNitrogen)
    local requiredLitersPerHa = self:getFertilizerUsageByStateChange(statesChanged, fillTypeIndex)
    local litersPerUpdate = requiredLitersPerHa * (speedLimit / 3600) / (10000 / workingWidth) * dt

    local regularUsage = 0
    if requiredLitersPerHa > 0 then
        if nApplyAutoModeFruitType ~= FruitType.UNKNOWN then
            -- if we automatically adjust ourself to fruit type the regular usage would be the adjustment to the average N requirement
            for i=1, #self.fruitRequirements do
                local fruitRequirement = self.fruitRequirements[i]
                if fruitRequirement.fruitType.index == nApplyAutoModeFruitType then
                    local _, internalActualNitrogen = self:getNearestNitrogenValueFromValue(actualNitrogen)
                    local requiredLitersPerHaReg, _ = self:getFertilizerUsageByStateChange(math.max(fruitRequirement.averageTargetLevel - internalActualNitrogen, 0), fillTypeIndex)
                    regularUsage = requiredLitersPerHaReg * (speedLimit / 3600) / (10000 / workingWidth) * dt
                end
            end
        else
            -- if we are not automatically adjusting to fruit type we use the default rate defined in the xml
            for i=1, #self.applicationRates do
                local applicationRate = self.applicationRates[i]
                if applicationRate.fillTypeIndex == fillTypeIndex then
                    -- kg N/ha to fertilizer litersPerUpdate
                    local requiredLitersPerHaReg, _ = self:getFertilizerUsageByNitrogenAmount(applicationRate.regularRate, fillTypeIndex)
                    regularUsage = requiredLitersPerHaReg * (speedLimit / 3600) / (10000 / workingWidth) * dt
                end
            end
        end
    end

    return litersPerUpdate, requiredLitersPerHa, regularUsage
end

function NitrogenMap:getFertilizerUsageByStateChange(statesChanged, fillTypeIndex)
    local requiredNAmount = statesChanged * self.fertilizerUsage.amountPerState
    return self:getFertilizerUsageByNitrogenAmount(requiredNAmount, fillTypeIndex)
end

function NitrogenMap:getFertilizerUsageByNitrogenAmount(nitrogenAmount, fillTypeIndex)
    local requiredLitersPerHa = 0
    local requiredMassPerHa = 0
    for i=1, #self.fertilizerUsage.nAmounts do
        local nAmount = self.fertilizerUsage.nAmounts[i]
        if nAmount.fillTypeIndex == fillTypeIndex then
            local fillTypeDesc = g_fillTypeManager:getFillTypeByIndex(fillTypeIndex)
            local massPerLiter = fillTypeDesc.massPerLiter / FillTypeManager.MASS_SCALE
            if fillTypeIndex ~= FillType.LIQUIDMANURE then
                if fillTypeDesc ~= nil then
                    requiredLitersPerHa = nitrogenAmount / (massPerLiter * 1000) / nAmount.amount
                    requiredMassPerHa = requiredLitersPerHa * massPerLiter
                end
            else
                -- for liquid manure we calculate n/1000l, not weight
                requiredLitersPerHa = nitrogenAmount / nAmount.amount
                requiredMassPerHa = requiredLitersPerHa * massPerLiter
            end
        end
    end

    return requiredLitersPerHa, requiredMassPerHa
end

function NitrogenMap:getNitrogenAmountFromFillType(fillTypeIndex)
    for i=1, #self.fertilizerUsage.nAmounts do
        local nAmount = self.fertilizerUsage.nAmounts[i]
        if nAmount.fillTypeIndex == fillTypeIndex then
            return nAmount.amount
        end
    end

    return 0
end

function NitrogenMap:getNitrogenFromChangedStates(statesChanged)
    return statesChanged * self.fertilizerUsage.amountPerState
end

function NitrogenMap:getMinMaxValue()
    if #self.nitrogenValues > 0 then
        return self.nitrogenValues[1].realValue, self.nitrogenValues[#self.nitrogenValues].realValue, #self.nitrogenValues
    end

    return 0, 1, 0
end

function NitrogenMap:getNitrogenValueFromInternalValue(internal)
    for i=1, #self.nitrogenValues do
        local nitrogenValue = self.nitrogenValues[i]
        if nitrogenValue.value == math.floor(internal) then
            return nitrogenValue.realValue
        end
    end

    return 0
end

function NitrogenMap:getNearestNitrogenValueFromValue(value)
    local minDifference = 1000
    local minValue = 0
    local minInternal = 0
    for i=1, #self.nitrogenValues do
        local pHValue = self.nitrogenValues[i].realValue
        local difference = math.abs(value - pHValue)
        if difference < minDifference then
            minDifference = difference
            minValue = pHValue
            minInternal = self.nitrogenValues[i].value
        end
    end

    return minValue, minInternal
end

function NitrogenMap:updateLastNitrogenValues()
    local actual = self.lastActualValue
    local target = self.lastTargetValue
    local yieldPotential = self.lastYieldPotential
    local ignoreOverfertilization = self.lastIgnoreOverfertilization
    local regularNLevel = self.lastRegularNValue

    self.lastActualValue = -1
    self.lastTargetValue = -1
    self.lastYieldPotential = -1
    self.lastIgnoreOverfertilization = nil
    self.lastRegularNValue = -1

    return actual, target, yieldPotential, regularNLevel, ignoreOverfertilization
end

function NitrogenMap:getYieldFactorByLevelDifference(difference, ignoreOverfertilization)
    if difference > 0 and ignoreOverfertilization == true then
        difference = 0
    end

    local yieldFactor = 0
    for i=1, #self.yieldMappings do
        local yieldMapping = self.yieldMappings[i]
        if difference >= yieldMapping.levelDifference then
            yieldFactor = yieldMapping.yieldFactor
        else
            break
        end
    end

    return yieldFactor
end

function NitrogenMap:buildOverlay(overlay, filter, isColorBlindMode)
    resetDensityMapVisualizationOverlay(overlay)
    setOverlayColor(overlay, 1, 1, 1, 1)

    local soilStateMap = self.sharedSoilStateMap
    if soilStateMap ~= nil then
        -- render black out state on top so it's independent of filter
        setDensityMapVisualizationOverlayStateColor(overlay, soilStateMap.bitVectorMap, 0, self.coverFirstChannel, self.coverNumChannels, 1, 0, 0, 0)

        for i=1, #self.nitrogenValues do
            local nitrogenValue = self.nitrogenValues[i]

            if filter[i] then
                local color = nitrogenValue.color
                if isColorBlindMode then
                    color = nitrogenValue.colorBlind
                end

                local sr, sg, sb = color[1], color[2], color[3]
                local r, g, b
                for j=1, self.coverMaxValue do
                    local alpha = (j / self.coverMaxValue) ^ 2
                    r, g, b = sr * alpha, sg * alpha, sb * alpha

                    local value = j + bitShiftLeft(nitrogenValue.value, self.coverNumChannels)
                    setDensityMapVisualizationOverlayStateColor(overlay, soilStateMap.bitVectorMap, 0, self.firstChannel - self.coverNumChannels, self.numChannels + self.coverNumChannels, value, r, g, b)
                end
            end
        end
    end
end

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

        for i=1, #self.nitrogenValues do
            local nitrogenValue = self.nitrogenValues[i]

            local nValueToDisplay = {}
            nValueToDisplay.colors = {}
            nValueToDisplay.colors[true] = {nitrogenValue.colorBlind}
            nValueToDisplay.colors[false] = {nitrogenValue.color}
            nValueToDisplay.description = string.format("%d kg/ha", nitrogenValue.realValue)

            table.insert(self.valuesToDisplay, nValueToDisplay)
        end

        local nValueToDisplay = {}
        nValueToDisplay.colors = {}
        nValueToDisplay.colors[true] = {{0, 0, 0}}
        nValueToDisplay.colors[false] = {{0, 0, 0}}
        nValueToDisplay.description = self.outdatedLabel
        table.insert(self.valuesToDisplay, nValueToDisplay)
    end

    return self.valuesToDisplay
end

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

        local numValues = #self.nitrogenValues
        for i=1, numValues + 1 do
            table.insert(self.valueFilter, true)
            table.insert(self.valueFilterEnabled, i <= numValues)
        end
    end

    return self.valueFilter, self.valueFilterEnabled
end

function NitrogenMap:getMinimapZoomFactor()
    return 0.1
end

function NitrogenMap:collectFieldInfos(fieldInfoDisplayExtension)
    local name = g_i18n:getText("pa_fieldInfo_nValue", NitrogenMap.MOD_NAME)
    fieldInfoDisplayExtension:addFieldInfo(name, self, self.updateFieldInfoDisplay, 3)
end

function NitrogenMap:getHelpLinePage()
    return 4
end

function NitrogenMap:updateFieldInfoDisplay(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, isColorBlindMode)
    local soilStateMap = self.sharedSoilStateMap
    local soilMap = self.soilMap
    if soilStateMap ~= nil and soilMap ~= nil and soilMap.bitVectorMap ~= nil then
        local modifier = self.densityMapModifiersFieldInfo.modifier
        local soilFilter = self.densityMapModifiersFieldInfo.soilFilter
        local coverFilter = self.densityMapModifiersFieldInfo.coverFilter
        local fruitFilter = self.densityMapModifiersFieldInfo.fruitFilter
        local tempFruitModifier = self.densityMapModifiersFieldInfo.tempFruitModifier
        local tempFruitFilter = self.densityMapModifiersFieldInfo.tempFruitFilter

        if modifier == nil or soilFilter == nil or coverFilter == nil or fruitFilter == nil or tempFruitModifier == nil or tempFruitFilter == nil then
            self.densityMapModifiersFieldInfo.modifier = DensityMapModifier:new(soilStateMap.bitVectorMap, self.firstChannel, self.numChannels)
            modifier = self.densityMapModifiersFieldInfo.modifier

            self.densityMapModifiersFieldInfo.soilFilter = DensityMapFilter:new(soilMap.bitVectorMap, soilMap.typeFirstChannel, soilMap.typeNumChannels)
            soilFilter = self.densityMapModifiersFieldInfo.soilFilter

            self.densityMapModifiersFieldInfo.coverFilter = DensityMapFilter:new(soilStateMap.bitVectorMap, self.coverFirstChannel, self.coverNumChannels)
            coverFilter = self.densityMapModifiersFieldInfo.coverFilter
            coverFilter:setValueCompareParams("greater", 1)

            self.densityMapModifiersFieldInfo.fruitFilter =  DensityMapFilter:new(modifier)
            fruitFilter = self.densityMapModifiersFieldInfo.fruitFilter

            self.densityMapModifiersFieldInfo.tempFruitModifier = DensityMapModifier:new(self.bitVectorMapNFruitFilterMask, 0, 1)
            tempFruitModifier = self.densityMapModifiersFieldInfo.tempFruitModifier
            tempFruitModifier:setPolygonRoundingMode("inclusive")

            self.densityMapModifiersFieldInfo.tempFruitFilter = DensityMapFilter:new(self.bitVectorMapNFruitFilterMask, 0, 1)
            tempFruitFilter = self.densityMapModifiersFieldInfo.tempFruitFilter
            tempFruitFilter:setValueCompareParams("equals", 1)
        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")
        tempFruitModifier:setParallelogramDensityMapCoords(startWorldX, startWorldZ, widthWorldX, widthWorldZ, heightWorldX, heightWorldZ, "ppp")

        for i=1, #self.fruitRequirements do
            local fruitRequirement = self.fruitRequirements[i]

            local fruitIndex = fruitRequirement.fruitType.index

            local ids = g_currentMission.fruits[fruitIndex]
            if ids ~= nil and ids.id ~= 0 then
                tempFruitModifier:executeSet(0)
                local fruitDesc = g_fruitTypeManager:getFruitTypeByIndex(fruitIndex)
                fruitFilter:resetDensityMapAndChannels(ids.id, fruitDesc.startStateChannel, fruitDesc.numStateChannels)

                if fruitRequirement.alwaysAllowFertilization then
                    -- also include cut states
                    fruitFilter:setValueCompareParams("greater", 1)
                else
                    fruitFilter:setValueCompareParams("between", 1, fruitDesc.maxHarvestingGrowthState)
                end

                tempFruitModifier:executeSet(1, fruitFilter, coverFilter)

                if g_precisionFarming:getIsSeasonsActive() then
                    fruitFilter:setValueCompareParams("equals", 11) -- seasons initial growth state
                    tempFruitModifier:executeSet(1, fruitFilter, coverFilter)
                end

                for j=1, #fruitRequirement.bySoilType do
                    local soilSettings = fruitRequirement.bySoilType[j]
                    soilFilter:setValueCompareParams("equals", soilSettings.soilTypeIndex)

                    local acc, numPixels, totalPixels = modifier:executeGet(tempFruitFilter, soilFilter)
                    if numPixels > totalPixels * 0.3 then -- at least 30% of pixels need to match - so we easily ignore a few plants on field borders without extra filtering
                        local nLevel = math.floor(acc / numPixels)

                        local color
                        local additionalText
                        local levelDifference = math.abs(soilSettings.targetLevel - nLevel)
                        for li=1, #self.levelDifferenceColors do
                            local levelDifferenceColor = self.levelDifferenceColors[li]
                            if levelDifferenceColor.levelDifference <= levelDifference then
                                if isColorBlindMode then
                                    color = levelDifferenceColor.colorBlind
                                else
                                    color = levelDifferenceColor.color
                                end

                                additionalText = levelDifferenceColor.additionalText
                            end
                        end

                        return string.format("%d / %d kg/ha", self:getNitrogenValueFromInternalValue(nLevel), self:getNitrogenValueFromInternalValue(soilSettings.targetLevel)), color, additionalText
                    end
                end
            end
        end

        local acc, numPixels, _ = modifier:executeGet(coverFilter)
        if numPixels > 0 then
            local nLevel = math.floor(acc / numPixels)
            return string.format("%d kg/ha", self:getNitrogenValueFromInternalValue(nLevel))
        end
    end

    return nil
end

function NitrogenMap:overwriteGameFunctions(pfModule)
    NitrogenMap:superClass().overwriteGameFunctions(self, pfModule)

    pfModule:overwriteGameFunction(FertilizingSowingMachine, "processSowingMachineArea", function(superFunc, vehicle, superFunc2, workArea, dt)
        if vehicle.preProcessExtUnderRootFertilizerArea ~= nil then
            vehicle:preProcessExtUnderRootFertilizerArea(workArea, dt)
        end

        local changedArea, totalArea = superFunc(vehicle, superFunc2, workArea, dt)

        if vehicle.postProcessExtUnderRootFertilizerArea ~= nil then
            vehicle:postProcessExtUnderRootFertilizerArea(workArea, dt, changedArea, totalArea)
        end

        return changedArea, totalArea
    end)

    pfModule:overwriteGameFunction(FertilizingCultivator, "processCultivatorArea", function(superFunc, vehicle, superFunc2, workArea, dt)
        if vehicle.preProcessExtUnderRootFertilizerArea ~= nil then
            vehicle:preProcessExtUnderRootFertilizerArea(workArea, dt)
        end

        local changedArea, totalArea = superFunc(vehicle, superFunc2, workArea, dt)

        if vehicle.postProcessExtUnderRootFertilizerArea ~= nil then
            vehicle:postProcessExtUnderRootFertilizerArea(workArea, dt, changedArea, totalArea)
        end

        return changedArea, totalArea
    end)
end
