Subversion Repositories public iLand

Rev

Rev 1221 | Blame | Compare with Previous | Last modification | View Log | RSS feed

/********************************************************************************************
**    iLand - an individual based forest landscape and disturbance model
**    http://iland.boku.ac.at
**    Copyright (C) 2009-  Werner Rammer, Rupert Seidl
**
**    This program is free software: you can redistribute it and/or modify
**    it under the terms of the GNU General Public License as published by
**    the Free Software Foundation, either version 3 of the License, or
**    (at your option) any later version.
**
**    This program is distributed in the hope that it will be useful,
**    but WITHOUT ANY WARRANTY; without even the implied warranty of
**    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
**    GNU General Public License for more details.
**
**    You should have received a copy of the GNU General Public License
**    along with this program.  If not, see <http://www.gnu.org/licenses/>.
********************************************************************************************/


#include "abe_global.h"
#include "globalsettings.h"


#include "forestmanagementengine.h"
#include "activity.h"
#include "fmunit.h"
#include "fmstand.h"
#include "fmstp.h"
#include "agent.h"
#include "agenttype.h"
#include "fomescript.h"
#include "scriptglobal.h"
#include "fomescript.h"
#include "scheduler.h"

#include "unitout.h"
#include "abestandout.h"
#include "abestandremovalout.h"

#include "debugtimer.h"

// general iLand stuff
#include "xmlhelper.h"
#include "csvfile.h"
#include "model.h"
#include "mapgrid.h"
#include "helper.h"
#include "threadrunner.h"
#include "outputmanager.h"

#include "tree.h"
#include "resourceunit.h"



Q_LOGGING_CATEGORY(abe, "abe")

Q_LOGGING_CATEGORY(abeSetup, "abe.setup")

namespace ABE {

/** @defgroup abe iLand agent based forest management engine (ABE)
  ABE is the Agent Based management Engine that allows the simulation of both forest management activties (e.g., harvesting of trees)
  and forest managers (e.g., deciding when and where to execute an activity).
  The ABE framework relies heavily on a blend of C++ (for low-level management activties) and Javascript (for higher level definition of
  management programs).

  The smallest spatial entity is a forest stand (FMStand), which may be grouped into forest management unit (FMUnit). Forest managers (Agent) can select
  stand treatment programs (FMSTP) for a unit. The management activities derive from a basic activity (Activity); specialized code exists
  for various activities such as planting or thinning. A scheduler (Scheduler) keeps track of where and when to execute activities following
  guidelines given by the management agent (Agent). Agents represent individual foresters that may be grouped into AgentTypes (e.g., farmers).


 */



/** @class ForestManagementEngine
 * @ingroup abe
*/


ForestManagementEngine *ForestManagementEngine::singleton_fome_engine = 0;
int ForestManagementEngine::mMaxStandId = -1;
ForestManagementEngine::ForestManagementEngine()
{
    mScriptBridge = 0;
    singleton_fome_engine = this;
    mCancel = false;
    setupOutputs(); // add ABE output definitions
}

ForestManagementEngine::~ForestManagementEngine()
{
    clear();
    // script bridge: script ownership?
    //if (mScriptBridge)
    //    delete mScriptBridge;
    singleton_fome_engine = 0;
}

const MapGrid *ForestManagementEngine::standGrid()
{
    return GlobalSettings::instance()->model()->standGrid();
}


void ForestManagementEngine::setupScripting()
{
    // setup the ABE system
    const XmlHelper &xml = GlobalSettings::instance()->settings();

    ScriptGlobal::setupGlobalScripting(); // general iLand scripting helper functions and such

    // the link between the scripting and the C++ side of ABE
    if (mScriptBridge)
        delete mScriptBridge;
    mScriptBridge = new FomeScript;
    mScriptBridge->setupScriptEnvironment();

    QString file_name = GlobalSettings::instance()->path(xml.value("model.management.abe.file"));
    QString code = Helper::loadTextFile(file_name);
    qCDebug(abeSetup) << "Loading script file" << file_name;
    QJSValue result = GlobalSettings::instance()->scriptEngine()->evaluate(code,file_name);
    if (result.isError()) {
        int lineno = result.property("lineNumber").toInt();
        QStringList code_lines = code.replace('\r', "").split('\n'); // remove CR, split by LF
        QString code_part;
        for (int i=std::max(0, lineno - 5); i<std::min(lineno+5, code_lines.count()); ++i)
            code_part.append(QString("%1: %2 %3\n").arg(i).arg(code_lines[i]).arg(i==lineno?"  <---- [ERROR]":""));
        qCDebug(abeSetup) << "Javascript Error in file" << result.property("fileName").toString() << ":" << result.property("lineNumber").toInt() << ":" << result.toString() << ":\n" << code_part;
    }
}

void ForestManagementEngine::prepareRun()
{
    mStandLayoutChanged = false; // can be changed by salvage operations / stand polygon changes
}

void ForestManagementEngine::finalizeRun()
{
    // empty the harvest counter; it will be filled again
    // during the (next) year.

    foreach (FMStand *stand, mStands) {
        stand->resetHarvestCounter();
    }

    foreach (FMUnit *unit, mUnits) {
        unit->resetHarvestCounter();
    }

    //
    if (mStandLayoutChanged) {
        DebugTimer timer("ABE:stand_layout_update");
        // renew the internal stand grid
        FMStand **fm = mFMStandGrid.begin();
        for (int *p = standGrid()->grid().begin(); p!=standGrid()->grid().end(); ++p, ++fm)
            *fm = *p<0?0:mStandHash[*p];
        // renew neigborhood information in the stand grid
        const_cast<MapGrid*>(standGrid())->updateNeighborList();
        // renew the spatial indices
        const_cast<MapGrid*>(standGrid())->createIndex();
        mStandLayoutChanged = false;

        // now check the stands
        for (QVector<FMStand*>::iterator it=mStands.begin(); it!=mStands.end(); ++it) {
            // renew area
            (*it)->checkArea();
            // initial activity (if missing)
            if (!(*it)->currentActivity()) {
                (*it)->initialize();
            }
        }
    }

}

void ForestManagementEngine::setupOutputs()
{
    if (GlobalSettings::instance()->outputManager()->find("abeUnit"))
        return; // already set up
    GlobalSettings::instance()->outputManager()->addOutput(new UnitOut);
    GlobalSettings::instance()->outputManager()->addOutput(new ABEStandOut);
    GlobalSettings::instance()->outputManager()->addOutput(new ABEStandDetailsOut);
    GlobalSettings::instance()->outputManager()->addOutput(new ABEStandRemovalOut);
}

void ForestManagementEngine::runJavascript()
{
    QJSValue handler = scriptEngine()->globalObject().property("run");
    if (handler.isCallable()) {
        scriptBridge()->setExecutionContext(0, false);
        QJSValue result = handler.call(QJSValueList() << mCurrentYear);
        if (FMSTP::verbose())
            qCDebug(abe) << "executing 'run' function for year" << mCurrentYear << ", result:" << result.toString();
    }

    handler = scriptEngine()->globalObject().property("runStand");
    if (handler.isCallable()) {
        qCDebug(abe) << "running the 'runStand' javascript function for" << mStands.size() << "stands.";
        foreach (FMStand *stand, mStands) {
            scriptBridge()->setExecutionContext(stand, true);
            handler.call(QJSValueList() << mCurrentYear);
        }
    }
}

AgentType *ForestManagementEngine::agentType(const QString &name)
{
    for (int i=0;i<mAgentTypes.count();++i)
        if (mAgentTypes[i]->name()==name)
            return mAgentTypes[i];
    return 0;
}

Agent *ForestManagementEngine::agent(const QString &name)
{
    for (int i=0;i<mAgents.count();++i)
        if (mAgents[i]->name()==name)
            return mAgents[i];
    return 0;
}


/*---------------------------------------------------------------------
 * multithreaded execution routines
---------------------------------------------------------------------*/


FMUnit *nc_execute_unit(FMUnit *unit)
{
    if (ForestManagementEngine::instance()->isCancel())
        return unit;

    //qDebug() << "called for unit" << unit;
    const QMultiMap<FMUnit*, FMStand*> &stand_map = ForestManagementEngine::instance()->stands();
    QMultiMap<FMUnit*, FMStand*>::const_iterator it = stand_map.constFind(unit);
    int executed = 0;
    int total = 0;
    while (it!=stand_map.constEnd() && it.key()==unit) {
        it.value()->stp()->executeRepeatingActivities(it.value());
        if (it.value()->execute())
            ++executed;
        //MapGrid::freeLocksForStand( it.value()->id() );
        if (ForestManagementEngine::instance()->isCancel())
            break;

        ++it;
        ++total;
    }
    if (ForestManagementEngine::instance()->isCancel())
        return unit;

    if (FMSTP::verbose())
        qCDebug(abe) << "execute unit'" << unit->id() << "', ran" << executed << "of" << total;

    // now run the scheduler
    unit->scheduler()->run();

    // collect the harvests
    it = stand_map.constFind(unit);
    while (it!=stand_map.constEnd() && it.key()==unit) {
        unit->addRealizedHarvest(it.value()->totalHarvest());
        ++it;
    }


    return unit;
}

FMUnit *nc_plan_update_unit(FMUnit *unit)
{
    if (ForestManagementEngine::instance()->isCancel())
        return unit;

    if (ForestManagementEngine::instance()->currentYear() % 10 == 0) {
        qCDebug(abe) << "*** execute decadal plan update ***";
        unit->managementPlanUpdate();
        unit->runAgent();
    }


    // first update happens *after* a full year of running ABE.
    if (ForestManagementEngine::instance()->currentYear()>1)
        unit->updatePlanOfCurrentYear();

    return unit;
}



void ForestManagementEngine::setup()
{
    QLoggingCategory::setFilterRules("abe.debug=true\n" \
                                     "abe.setup.debug=true"); // enable *all*

    DebugTimer time_setup("ABE:setupScripting");
    clear();

    // (1) setup the scripting environment and load all the javascript code
    setupScripting();
    if (isCancel()) {
        throw IException(QString("ABE-Error (setup): %1").arg(mLastErrorMessage));
    }

    if (!GlobalSettings::instance()->model())
        throw IException("No model created.... invalid operation.");

    // (2) spatial data (stands, units, ...)
    const MapGrid *stand_grid = GlobalSettings::instance()->model()->standGrid();

    if (stand_grid==NULL || stand_grid->isValid()==false)
        throw IException("The ABE management model requires a valid stand grid.");

    const XmlHelper &xml = GlobalSettings::instance()->settings();

    QString data_file_name = GlobalSettings::instance()->path(xml.value("model.management.abe.agentDataFile"));
    qCDebug(abeSetup) << "loading ABE agentDataFile" << data_file_name << "...";
    CSVFile data_file(data_file_name);
    if (data_file.isEmpty())
        throw IException(QString("Stand-Initialization: the standDataFile file %1 is empty or missing!").arg(data_file_name));
    int ikey = data_file.columnIndex("id");
    int iunit = data_file.columnIndex("unit");
    int iagent = data_file.columnIndex("agent");
    int iagent_type = data_file.columnIndex("agentType");
    int istp = data_file.columnIndex("stp");
    // unit properties
    int ispeciescomp = data_file.columnIndex("speciesComposition");
    int ithinning = data_file.columnIndex("thinningIntensity");
    int irotation = data_file.columnIndex("U");
    int iMAI = data_file.columnIndex("MAI");
    int iharvest_mode = data_file.columnIndex("harvestMode");


    if (ikey<0 || iunit<0)
        throw IException("setup ABE agentDataFile: one (or two) of the required columns 'id' or 'unit' not available.");
    if (iagent<0 && iagent_type<0)
        throw IException("setup ABE agentDataFile: the columns 'agent' or 'agentType' are not available. You have to include at least one of the columns.");


    QList<QString> unit_codes;
    QHash<FMStand*, QString> initial_stps;
    for (int i=0;i<data_file.rowCount();++i) {
        int stand_id = data_file.value(i,ikey).toInt();
        if (!stand_grid->isValid(stand_id))
            continue; // skip stands that are not in the map (e.g. when a smaller extent is simulated)
        if (FMSTP::verbose())
            qCDebug(abeSetup) << "setting up stand" << stand_id;

        // check agents
        QString agent_code = iagent>-1 ? data_file.value(i, iagent).toString() : QString();
        QString agent_type_code = iagent_type>-1 ? data_file.value(i, iagent_type).toString() : QString();
        QString unit_id = data_file.value(i, iunit).toString();

        Agent *ag=0;
        AgentType *at=0;
        if (agent_code.isEmpty() && agent_type_code.isEmpty())
            throw IException(QString("setup ABE agentDataFile row '%1': no code for columns 'agent' and 'agentType' available.").arg(i) );

        if (!agent_code.isEmpty()) {
            // search for a specific agent
            ag = agent(agent_code);
            if (!ag)
                throw IException(QString("Agent '%1' is not set up (row '%2')! Use the 'newAgent()' JS function of agent-types to add agent definitions.").arg(agent_code).arg(i));
            at = ag->type();

        } else {
            // look up the agent type and create the agent on the fly
            // create the agent / agent type
            at = agentType(agent_type_code);
            if (!at)
                throw IException(QString("Agent type '%1' is not set up (row '%2')! Use the 'addAgentType()' JS function to add agent-type definitions.").arg(agent_type_code).arg(i));

            if (!unit_codes.contains(unit_id)) {
                // we create an agent for the unit only once (per unit)
                ag = at->createAgent();
            }
        }


        // check units
        FMUnit *unit = 0;
        if (!unit_codes.contains(unit_id)) {
            // create the unit
            unit = new FMUnit(ag);
            unit->setId(unit_id);
            if (iharvest_mode>-1)
                unit->setHarvestMode( data_file.value(i, iharvest_mode).toString());
            if (ithinning>-1)
                unit->setThinningIntensity( data_file.value(i, ithinning).toInt() );
            if (irotation>-1)
                unit->setU( data_file.value(i, irotation).toDouble() );
            if (iMAI>-1)
                unit->setAverageMAI(data_file.value(i, iMAI).toDouble());
            if (ispeciescomp>-1) {
                int index;
                index = at->speciesCompositionIndex( data_file.value(i, ispeciescomp).toString() );
                if (index==-1)
                    throw IException(QString("The species composition '%1' for unit '%2' is not a valid composition type (agent type: '%3').").arg(data_file.value(i, ispeciescomp).toString()).arg(unit->id()).arg(at->name()));
                unit->setTargetSpeciesCompositionIndex( index );
            }
            mUnits.append(unit);
            unit_codes.append(unit_id);
            ag->addUnit(unit); // add the unit to the list of managed units of the agent
        } else {
            // get unit by id ... in this case we have the same order of appending values
            unit = mUnits[unit_codes.indexOf(unit_id)];
        }

        // create stand
        FMStand *stand = new FMStand(unit,stand_id);
        if (istp>-1) {
            QString stp = data_file.value(i, istp).toString();
            initial_stps[stand] = stp;
        }
        mMaxStandId = qMax(mMaxStandId, stand_id);

        mUnitStandMap.insertMulti(unit,stand);
        mStands.append(stand);

    }

    // count the number of stands within each unit
    foreach(FMUnit *unit, mUnits)
        unit->setNumberOfStands( mUnitStandMap.count(unit) );

    // set up the stand grid (visualizations)...
    // set up a hash for helping to establish stand-id <-> fmstand-link
    mStandHash.clear();
    for (int i=0;i<mStands.size(); ++i) {
        mStandHash[mStands[i]->id()] = mStands[i];
    }

    mFMStandGrid.setup(standGrid()->grid().metricRect(), standGrid()->grid().cellsize());
    mFMStandGrid.initialize(0);
    FMStand **fm = mFMStandGrid.begin();
    for (int *p = standGrid()->grid().begin(); p!=standGrid()->grid().end(); ++p, ++fm)
        *fm = *p<0?0:mStandHash[*p];

    mStandLayers.setGrid(mFMStandGrid);
    mStandLayers.clearClasses();
    mStandLayers.registerLayers();

    // now initialize STPs (if they are defined in the init file)
    for (QHash<FMStand*,QString>::iterator it=initial_stps.begin(); it!=initial_stps.end(); ++it) {
        FMStand *s = it.key();
        FMSTP* stp = s->unit()->agent()->type()->stpByName(it.value());
        if (stp) {
            s->setSTP(stp);
        } else {
            qCDebug(abeSetup) << "Warning during reading of CSV setup file: the STP '" << it.value() << "' is not valid for Agenttype: " << s->unit()->agent()->type()->name();
        }
    }
    qCDebug(abeSetup) << "ABE setup completed.";
}

void ForestManagementEngine::initialize()
{

    DebugTimer time_setup("ABE:setup");

    foreach (FMStand* stand, mStands) {
        if (stand->stp()) {

            stand->setU( stand->unit()->U() );
            stand->setThinningIntensity( stand->unit()->thinningIntensity() );
            stand->setTargetSpeciesIndex( stand->unit()->targetSpeciesIndex() );

            stand->initialize();
            if (isCancel()) {
                throw IException(QString("ABE-Error: init of stand %2: %1").arg(mLastErrorMessage).arg(stand->id()));
            }
        }
    }

    // now initialize the agents....
    foreach(Agent *ag, mAgents) {
        ag->setup();
        if (isCancel()) {
            throw IException(QString("ABE-Error: setup of agent '%2': %1").arg(mLastErrorMessage).arg(ag->name()));
        }
    }

    // run the initial planning unit setup
    GlobalSettings::instance()->model()->threadExec().run(nc_plan_update_unit, mUnits);


    qCDebug(abeSetup) << "ABE setup complete." << mUnitStandMap.size() << "stands on" << mUnits.count() << "units, managed by" << mAgents.size() << "agents.";

}

void ForestManagementEngine::clear()
{
    qDeleteAll(mStands); // delete the stands
    mStands.clear();
    qDeleteAll(mUnits); // deletes the units
    mUnits.clear();
    mUnitStandMap.clear();

    qDeleteAll(mAgents);
    mAgents.clear();
    qDeleteAll(mAgentTypes);
    mAgentTypes.clear();
    qDeleteAll(mSTP);
    mSTP.clear();
    mCurrentYear = 0;
    mCancel = false;
    mLastErrorMessage = QString();
}

void ForestManagementEngine::abortExecution(const QString &message)
{
    mLastErrorMessage = message;
    mCancel = true;
}

void ForestManagementEngine::runOnInit(bool before_init)
{
    QString handler = before_init ? QStringLiteral("onInit") : QStringLiteral("onAfterInit");
    if (GlobalSettings::instance()->scriptEngine()->globalObject().hasProperty(handler)) {
        QJSValue result = GlobalSettings::instance()->scriptEngine()->evaluate(QString("%1()").arg(handler));
        if (result.isError())
            qCDebug(abeSetup) << "Javascript Error in global"<< handler << "-Handler:" << result.toString();

    }
}




/// this is the main function of the forest management engine.
/// the function is called every year.
void ForestManagementEngine::run(int debug_year)
{
    if (debug_year>-1) {
        mCurrentYear++;
    } else {
        mCurrentYear = GlobalSettings::instance()->currentYear();
    }
    // now re-evaluate stands
    if (FMSTP::verbose()) qCDebug(abe) << "ForestManagementEngine: run year" << mCurrentYear;


    prepareRun();

    // execute an event handler before invoking the ABE core
    runJavascript();

    {
    // launch the planning unit level update (annual and thorough analysis every ten years)
    DebugTimer plu("ABE:planUpdate");
    GlobalSettings::instance()->model()->threadExec().run(nc_plan_update_unit, mUnits, true);
    }

    GlobalSettings::instance()->model()->threadExec().run(nc_execute_unit, mUnits, true); // force single thread operation for now
    if (isCancel()) {
        throw IException(QString("ABE-Error: %1").arg(mLastErrorMessage));
    }

    // create outputs
    {
    DebugTimer plu("ABE:outputs");
    GlobalSettings::instance()->outputManager()->execute("abeUnit");
    GlobalSettings::instance()->outputManager()->execute("abeStand");
    GlobalSettings::instance()->outputManager()->execute("abeStandDetail");
    GlobalSettings::instance()->outputManager()->execute("abeStandRemoval");
    }

    finalizeRun();

}




void ForestManagementEngine::test()
{
    // test code
    try {
        //Activity::setVerbose(true);
        // setup the activities and the javascript environment...
        GlobalSettings::instance()->resetScriptEngine(); // clear the script
        ScriptGlobal::setupGlobalScripting(); // general iLand scripting helper functions and such
        if (mScriptBridge)
            delete mScriptBridge;
        mScriptBridge = new FomeScript;
        mScriptBridge->setupScriptEnvironment();

        //setup();

    } catch (const IException &e) {
        qDebug() << "An error occured:" << e.message();
    }
    QString file_name = "E:/Daten/iLand/modeling/abm/knowledge_base/test/test_stp.js";
    QString code = Helper::loadTextFile(file_name);
    QJSValue result = GlobalSettings::instance()->scriptEngine()->evaluate(code,file_name);
    if (result.isError()) {
        int lineno = result.property("lineNumber").toInt();
        QStringList code_lines = code.replace('\r', "").split('\n'); // remove CR, split by LF
        QString code_part;
        for (int i=std::max(0, lineno - 5); i<std::min(lineno+5, code_lines.count()); ++i)
            code_part.append(QString("%1: %2 %3\n").arg(i).arg(code_lines[i]).arg(i==lineno?"  <---- [ERROR]":""));
        qDebug() << "Javascript Error in file" << result.property("fileName").toString() << ":" << result.property("lineNumber").toInt() << ":" << result.toString() << ":\n" << code_part;
    }


//    try {
//        qDebug() << "*** test 1 ***";
//        FMSTP stp;
//        stp.setVerbose(true);
//        stp.setup(GlobalSettings::instance()->scriptEngine()->globalObject().property("stp"), "stp");
//        stp.dumpInfo();

//    } catch (const IException &e) {
//        qDebug() << "An error occured:" << e.message();
//    }
//    try {
//        qDebug() << "*** test 2 ***";
//        FMSTP stp2;
//        stp2.setVerbose(true);
//        stp2.setup(GlobalSettings::instance()->scriptEngine()->globalObject().property("degenerated"), "degenerated");
//        stp2.dumpInfo();
//    } catch (const IException &e) {
//        qDebug() << "An error occured:" << e.message();
//    }

    // dump all objects:
    foreach(FMSTP *stp, mSTP)
        stp->dumpInfo();

    setup();
    qDebug() << "finished";

}

QStringList ForestManagementEngine::evaluateClick(const QPointF coord, const QString &grid_name)
{
    Q_UNUSED(grid_name); // for the moment
    // find the stand at coord.
    FMStand *stand = mFMStandGrid.constValueAt(coord);
    if (stand)
        return stand->info();
    return QStringList();
}

QJSEngine *ForestManagementEngine::scriptEngine()
{
    // use global engine from iLand
    return GlobalSettings::instance()->scriptEngine();
}

FMSTP *ForestManagementEngine::stp(QString stp_name) const
{
    for (QVector<FMSTP*>::const_iterator it = mSTP.constBegin(); it!=mSTP.constEnd(); ++it)
        if ( (*it)->name() == stp_name )
            return *it;
    return 0;
}

FMStand *ForestManagementEngine::stand(int stand_id) const
{
    if (mStandHash.contains(stand_id))
        return mStandHash[stand_id];

    // exhaustive search... should not happen
    qCDebug(abe) << "ForestManagementEngine::stand() fallback to exhaustive search.";
    for (QVector<FMStand*>::const_iterator it=mStands.constBegin(); it!=mStands.constEnd(); ++it)
        if ( (*it)->id() == stand_id)
            return *it;
    return 0;
}

QStringList ForestManagementEngine::standIds() const
{
    QStringList standids;
    foreach(FMStand *s, mStands)
        standids.push_back(QString::number(s->id()));
    return standids;
}

void ForestManagementEngine::notifyTreeRemoval(Tree *tree, int reason)
{
    // we use an 'int' instead of Tree:TreeRemovalType because it does not work
    // with forward declaration (and I dont want to include the tree.h header in this class header).
    FMStand *stand = mFMStandGrid[tree->position()];
    if (stand)
        stand->notifyTreeRemoval(tree, reason);
    else
        qDebug() << "ForestManagementEngine::notifyTreeRemoval(): tree not on stand at (metric coords): " << tree->position() << "ID:" << tree->id();
}

bool ForestManagementEngine::notifyBarkbeetleAttack(const ResourceUnit *ru, const double generations, int n_infested_px)
{
    // find out which stands are within the resource unit
    GridRunner<FMStand*> gr(mFMStandGrid, ru->boundingBox());
    QHash<FMStand*, bool> processed_items;
    bool forest_changed = false;
    while (FMStand **s=gr.next()) {
        if (*s && !processed_items.contains(*s)) {
            processed_items[*s] = true;
            forest_changed |=  (*s)->notifyBarkBeetleAttack(generations, n_infested_px);
        }
    }
    return forest_changed;
}

QMutex protect_split;
FMStand *ForestManagementEngine::splitExistingStand(FMStand *stand)
{
    // get a new stand-id
    // make sure that the Id is only used once.
    QMutexLocker protector(&protect_split);
    int new_stand_id = ++mMaxStandId;

    FMUnit *unit = const_cast<FMUnit*> (stand->unit());
    FMStand *new_stand = new FMStand(unit,new_stand_id);

    mUnitStandMap.insertMulti(unit,new_stand);
    mStands.append(new_stand);
    mStandHash[new_stand_id] = new_stand;

    unit->setNumberOfStands( mUnitStandMap.count(unit) );

    mStandLayoutChanged = true;

    return new_stand;
}



} // namespace