Overview
A key aspect of Envision's architecture is the ability to define "plug-in" modules that extend Envision's functionality. This is a primary mechanism
for incorporating models of landscape change into an Envision application. Plug-ins are generally written in C++ and are coded as Dynamic Link Libraries (DLLs),
a mechanism allowing Windows programs to
incorporate new functionality into an application at run time. Most Windows-based compilers support the creation of DLLs. Creating
Envision-compliant DLL's is a fairly straight-forward process, but does require coding in a language supporting creation of DLL's.
Below, we provide a walk-though of the step required to create an Envision plug-in. We will be using the Microsoft Visual Studio compiler
in the examples below.
To facilitate writing Envision plug-ins, an SDK is available in the
“Downloads”
section of this site. This SDK includes required include and library files, as well as a set of example files and C++ classes designed to
make it relatively easy to write plug-ins. The steps required to create a basic plug-in are described below:
Envision currently supports three types of plug-ins: landscape change models (autonomous processes), evaluators, and visualizers. There are described in the following table:
Landscape Change Models
Landscape change models are the most common type of plug-in in Envision, and are used to
implement models the modify the landscape representation in the IDU coverage, and to generate output data collected by Envision during the course of a simulation.
They have full access to Envision internals, and can expose input variables (also called scenario variables) that can be set in scenario-specific ways if desired.
Evaluators
Evaluators are very similar to Landscape Change Models, but perform one additional task - returning a set of evaluation metrics that
are collected by Envision during the course of a run.
Visualizers
Visualizers are plug-ins whose sole purpose is to provide dynamic visualization of spatial or aspatial data during an Envision simulation run.
In the walk-though below, we will create a basic Envision plug-in that demonstrates the basics of developing your own plug-in, including setting up the approriate entry functions, accessing data available
in the IDU coverage, and writing your own data to the IDU coverage, as well as exposing non-spatial data outputs to Envision's runtime data collection system. We will develop a simple Landscape Change Model that
takes as input a mapping of fuel load by vegetation type, examines the IDU coverage for current vegetation patterns, populates a corresponding IDU attribute for fire risk, and generates a metric
of total fuel load on the landscape for capture by Envision. We will demonstrate the basics of setting up a plug-in, accessing data, and exposing output variables to Envision.
Walk-through Prerequisites
To complete the walk-through below, you will need the following:
- A current version of Envision, installed on our local machine. You can download Envision from this link.
- A recent version of the Microsoft Visual Studio C++ Compiler. A free version of this compiler is available from Microsoft at
https://visualstudio.microsoft.com/vs/ - Select the "Community" version for download and install on your local machine. The walk-through below assumes you have Visual Studio installed on your local machine.
- The Envision Software Development Kit (SDK) - available in the “Downloads” section of the Envision web site. This needs to
be installed locally on your development machine.
- A set of Envision project files. Below, we will be using a project from the Envision tutorials. A setup program for these files is available for download from this link. After downloading, run the setup program to install the necessary tutorial input files.
Step 1 - Create an MFC Extension Project
In Visual Studio, create a “New Project” of type MFC DLL. In the "Name" input box, put the name of your plug-in.
You can adjust the "location" as needed for setup.
When you click OK, you will be asked to set the “Application Settings”.
Indicate “MFC Extension DLL” and click Finish. The Visual Studio App Wizard will create a skeleton DLL for you, placing the resulting code files in the location you specified above.
In particular, note the following files: dllmain.cpp, myproject.cpp, and myproject.def, where myproject is the name of the DLL you
are creating. We will modify these files next.
Step 2 - Modify dllmain.cpp
Visual Studio will generate a 'dllmain.cpp' code file; we will replace this VS-generated file with the one from the Envision SDK.
Go to the location where you installed the Envision SDK, and copy the SDK-version of dllmain.cpp to your
project directory, overwriting the dllmain.cpp generated by VS. The "new" dllmain.cpp is can be seen at this link.
Because we are only creating a landscape change model plug-in, we can simplify this file substantially. We do this be deleting
the portions of the file that are not related to this type of plug-in, specifically all references to evaluators and visualizers.
Additionally, note the sections in the oringal file marked "TODO:" - we have made appropriate changes where noted to reflect
this specific plug-in.
The resulting dllmain.cpp is shown below. This will be the dllmain.cpp that we use for our plug-in, which we will call "FuelLoad".
Copy the code below into your dllmain.cpp file, replacing the existing content. This will be the version we use in our FuelLoad plug-in project.
// dllmain.cpp : Defines the initialization routines for the DLL.
//
#include "stdafx.h"
#include <afxwin.h>
#include <afxdllx.h>
#include <EnvEngine\EnvContext.h> // point this to your location
#include "FuelLoad.h" // We will create this file shortly
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
/**** indicate model/process instance pointer ****/
FuelLoad *theModel = NULL;
/**** indicate DLL Name ****/
static AFX_EXTENSION_MODULE FuelLoadDLL = { NULL, NULL };
extern "C" int APIENTRY
DllMain(HINSTANCE hInstance, DWORD dwReason, LPVOID lpReserved)
{
// Remove this if you use lpReserved
UNREFERENCED_PARAMETER(lpReserved);
if (dwReason == DLL_PROCESS_ATTACH)
{
// this code runs when the DLL is first loaded
/**** update trace string with module name ****/
TRACE0("FuelLoad.DLL Initializing!\n");
// Extension DLL one-time initialization
if (!AfxInitExtensionModule(FuelLoadDLL, hInstance))
return 0;
/**** Create instance of Fuel Model ****/
new CDynLinkLibrary(FuelLoadDLL);
/*** TODO: instantiate any models/processes ***/
ASSERT( theModel == NULL );
theModel = new FuelLoad;
}
else if (dwReason == DLL_PROCESS_DETACH)
{
// this code runs when the DLL is unleaded on exiting Envision
/**** Update module name in trace string ****/
TRACE0("FuelLoad.DLL Terminating!\n");
/**** Delete instantiated model ****/
if ( theModel != NULL)
delete theModel;
// Terminate the library before destructors are called
AfxTermExtensionModule(FuelLoadDLL);
}
return 1; // ok
}
//////////////////////////
// API function prototypes
//////////////////////////
extern "C" void PASCAL EXPORT GetExtInfo( ENV_EXT_INFO *pInfo );
// for landscape change model (autonomous process)
extern "C" BOOL PASCAL EXPORT APInit( EnvContext*, LPCTSTR initStr ); // called when DLL loaded
extern "C" BOOL PASCAL EXPORT APInitRun( EnvContext*, bool useInitialSeed ); // called when starting a simulation run
extern "C" BOOL PASCAL EXPORT APRun( EnvContext* ); // called at each time step (year)
extern "C" int PASCAL EXPORT APInputVar( int modelID, MODEL_VAR** modelVar ); // only needed if inputs exposed
extern "C" int PASCAL EXPORT APOutputVar( int modelID, MODEL_VAR** modelVar ); // only needed if outputs exposed
extern "C" BOOL PASCAL EXPORT APEndRun( EnvContext* ); // optional
extern "C" BOOL PASCAL EXPORT APSetup( EnvContext*, HWND hWnd ); // optional
/////////////////////////////////////////////////////////////////////////////////////
// API Implementations
/////////////////////////////////////////////////////////////////////////////////////
void PASCAL GetExtInfo( ENV_EXT_INFO *pInfo )
{
pInfo->types = EET_AUTOPROCESS;
pInfo->description = "Fuel Load Model";
}
/////////////////////////////////////////////////////////////////////////////////////
// Landscape Change Model Interfaces - These are just "C" "wrappers" around the C++
// implementation
/////////////////////////////////////////////////////////////////////////////////////
BOOL PASCAL APInit( EnvContext *pEnvContext, LPCTSTR initStr ) { return theModel->Init( pEnvContext, initStr ); }
BOOL PASCAL APInitRun( EnvContext *pEnvContext, bool useInitSeed ){ return theModel->InitRun( pEnvContext, useInitSeed ); }
BOOL PASCAL APRun( EnvContext *pEnvContext ) { return theModel->Run( pEnvContext ); }
BOOL PASCAL APEndRun( EnvContext *pEnvContext ) { return theModel->EndRun( pEnvContext ); }
int PASCAL APInputVar( int id, MODEL_VAR** modelVar ) { return theModel->InputVar( id, modelVar ); }
int PASCAL APOutputVar( int id, MODEL_VAR** modelVar ) { return theModel->OutputVar( id, modelVar ); }
BOOL PASCAL EXPORT APSetup( EnvContext *pContext, HWND hWnd ) { return theModel->Setup( pContext, hWnd ); }
Step 3 - Add exports to the Export Definition file (FuelLoad.def)
Visual Studio will generate a 'FuelLoad.def' file that defines what functions are exported from the DLL.
Again, we will replace this VS-generated file with the one from the Envision SDK.
Go to the location where you installed the Envision SDK, and copy the SDK-version of
EnvExtExample.def to your project directory, renaming it FuelLoad.def. Alternatively,
copy the text below into your existing FuelLoad.def file.
; FuelLoad.def : Declares exported functions
LIBRARY "FuelLoad"
EXPORTS
; Explicit exports can go here
GetExtInfo @1
APInit @2
APInitRun @3
APRun @4
APEndRun @5
APInputVar @6
APOutputVar @7
APSetup @8
Note that any function defined in dllmain.cpp above should have an entry in the .def file. The @ numbers in the
'def' file must be unique for each function.
Step 4 - Modify your Project Settings in Visual Studio
Right-click on your project in Visual Studio’s Solution Pane and select 'Properties'. This allows you to set various project settings. Change the following, being sure to select 'All Configurations' and 'All Platforms' as the configuration targets (at the top of the 'Properties' dialog box.
Required Project Settings ('All Configurations'/'All Platforms')
- General->Character Set: Not Set
- C/C++ ->General->Additional Include Directories: C:\Envision\SDK\include; (Note: the location may vary depending on where you installed the SDK)
- C/C++ ->Preprocessor->Preprocessor Definitions: add "__EXPORT__=__declspec( dllimport )"; Be sure to include the quotes!
- Linker->General->Additional Library Directories: C:\Envision\SDK\libs; (Note: the location may vary depending on where you installed the SDK)
- Linker->Input->Additional Dependencies: libs.lib
- Build Events->Post Build Events: copy $(TargetPath) C:\Envision (Note: the location specified in the second path may vary depending on where you installed the Envision)
Note that your setup may vary slightly based on your directory structure.
Additionally, we will need ad the EnvExtension.h and EnvExtension.cpp files to the project. These are available in the directory you installed the Envision
SDK into. Copy these files into your project directory and add them to your Visual Studio project via the Solution Explorer.
Finally, we will want to create a 64-bit version of the DLL, so be to sure to indicate the build platform is "x64".
Step 5 - Create model class files
The SDK provides two C++ classes, EnvEvalModel and EnvAutoProcess, to facilitate the creation of Envision Plug-ins. They subclass from EnvExtension. The classes provide several capabilities: 1) The provide default implemenations for all interface functions, 2) the manage input and output variables exposed by the models/processes to Envision, and 3) the provide a wrapper to facilitate making changes to the underlying map layers.
To create a
evaluator:
- Derive a subclass from EnvEval Model
To create a
landscape change model (autonous process):
- Derive a subclass from EnvAutoProcess
For both evaluators and landscape change models:
- Override any of EnvExtension::Init(), InitRun(), and Run() as needed. Other overrides are optional and depend on the needs of the plug-in.
- In the Init() method of the class, call EnvExtension::AddInputVar() and EnvExtension::AddOutputVar for all input and output variables exposed by the plug-in;
- When accessing the IDU map layer during processes, get the MapLayer pointer from the EnvContext object passed to the DLL; When making changes to the Map, DO NOT call the MapLayer directly; instead, use EnvExtension::UpdateIDU() (documented below) to make modifiction to the map.
Putting the concepts above to use, we will next develop the actual model captured in this plug-in. To facilitate
this process, we will take advantage of classes defined in the SDK. Because this is a landscape change model (autonomous process),
we will subclass our model from the EnvAutoProcess class provided by Envision. Alternatively, if we where developing an
Evaluative Model, we would derived our model from EnvEvalModel class.
For our model, we will only implement the Init, InitRun, and Run methods, and rely on the
default implementations provided in the EnvAutoProcess class for the remaining API functions.
The header file for our model class is provided below. This is pretty minimalist, but it shows the basics of
a model subclass implementing the Envision interfaces. Note that Visual Studio will create an empty .cpp file for the
project, but not a corresponding .h file. For this walk-through, you can create this header file using one of the following methods:
- in the Visual Studio IDE, right click on the FuelLoad project and select the "Add/Header File (.h) option, naming the file FuelLoad.h, and adding the code below to your new file,
- get the file from this link and save it to your project directory
#pragma once
#include
// this privides a basic class definition for this
// plug-in module.
// Note that we want to override the parent methods
// for Init, InitRun, and Run.
class FuelLoad : public EnvAutoProcess
{
public:
// constructor
~FuelLoad(void);
// override API Methods
BOOL Init(EnvContext *pEnvContext, LPCTSTR initStr);
BOOL InitRun(EnvContext *pEnvContext, bool useInitialSeed);
BOOL Run(EnvContext *pContext);
protected:
// we'll add model code here as needed
};
This provides the basics of defining a class that contains declarations for three of the Envision APIs
(Init, InitRun, and Run), but no implementations. To get a start on the implementation of these functions, open
the FuelLoad.cpp that Visual Studio created when you created the project, and paste the code below into this file:
// FuelLoad.cpp : Defines the initialization routines for the DLL.
//
#include "stdafx.h"
#include "FuelLoad.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
// constructor
FuelLoad::~FuelLoad(void)
{ }
// override API Methods
BOOL FuelLoad::Init(EnvContext *pEnvContext, LPCTSTR initStr)
{
return TRUE;
}
BOOL FuelLoad::InitRun(EnvContext *pEnvContext, bool useInitialSeed)
{
return TRUE;
}
BOOL FuelLoad::Run(EnvContext *pContext)
{
return TRUE;
}
At this point, your should be able to successfully compile your DLL. It does not yet perform any useful functions, but at least we have a compilable
plug-in and will add functionailty next.
Step 6 - Implement the Model Functionality
The FuelLoad model is intended to perform a simple task - scanning the IDU land use/land cover (LULC) information and assigning an appropriate fuel load to each IDU based on
the IDU's land cover class. (Note that we could easily accomplish this same functionality using the standard "Sync" plug-in, but that's somewhat beside the point!)
To make the model as general as possible, we will store mappings between and IDU land cover class and the corresponding fuel load
in an external XML file, making it easy to modify the mappings without having to make code changes or recompile the model DLL. We will take advantage of
classes available in the Envision SDK that support reading and parsing XML files to implement the functionaility of loading and storing the mappings. Our XML
input file looks like this:
<?xml version="1.0" encoding="utf-8"?>
<fuel_loads lulc_col="LULC_A" fuel_col="FUEL_LOAD">
<fuel_load lulc="1" load="3.06" />
<fuel_load lulc="2" load="5.32" />
<fuel_load lulc="3" load="12.3" />
</fuel_loads>
This first (outer) <fuel_loads> tag allows us to specify which columns in the IDU database contains LULC and fuel load information, respectively.
The child <fuel_load> tags define a set of mappings between specific lulc classes and corresponding fuel loads. Our model will need to do
several things:
- During initialization, when the plug-in is first loaded, it will need to read, parse, and store the information in the XML configuration file above. We will implement
this functionality in the "Init()" method.
- At each time step when a simulation is running, it will need to scan the IDUs and assign an appropriate fuel load. We will implement this functionality in the
"Run()" method.
Additionally, we will compute and expose as an output variable the average fuel load density on the landscape. We will do this in the Init() method; alternatively,
we could do this in the FuelLoad constructor. We will define a variable holding the current average (area-weighted) fuel load in our FuelLoad class.
Let's start with our header file. We will make three modifications.
- Add a data structure, in this case a map of LULC/Fuel Load pairs, that we will use to store the mappings defined in our input XMl files,
- Add a variable for storing the average fuel load that we will export,
- Adding a function "LoadXml()" that will load the input XML file, parse it's contents, and store the mappings in our class object.
#pragma once
#include <EnvExtension.h>
#include <map>
// this provides a basic class definition for this plug-in module.
// Note that we want to override the parent methods for Init, InitRun, and Run.
class FuelLoad : public EnvAutoProcess
{
public:
// constructor
FuelLoad(void);
// override API Methods
BOOL Init(EnvContext *pEnvContext, LPCTSTR initStr);
BOOL InitRun(EnvContext *pEnvContext, bool useInitialSeed);
BOOL Run(EnvContext *pContext);
protected:
// store the LULC, FuelLoad field indexes
int m_colLULC;
int m_colFuelLoad;
// output variables
float m_avgFuelLoad; // area-weighted average fuel load
// define a map that maps lulc codes into corresponding fuel loads
std::map<int,float> m_mappings; // key=lulc, value=fuel load
// function for loading map pairs from XML input file
bool LoadXml(LPCTSTR filename, EnvContext*);
};
Our class implementation file (FuelLoad.cpp) will be updated to include implementations for our API functions and
additional supporting code.
// FuelLoad.cpp : Defines the initialization routines for the DLL.
//
#include "stdafx.h"
#include "FuelLoad.h"
#include <PathManager.h>
#include <Report.h>
#include <tixml.h>
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
// constructor
FuelLoad::FuelLoad(void)
: m_colLULC(-1)
, m_colFuelLoad(-1)
, m_avgFuelLoad(0)
{ }
// override API Methods
// Init() gets called when the DLL is first loaded. We will load the XML input
// file (name specified in the .envx file) and populate the initial fuel loads
// based on the initial IDU lulc values. Additionally, we will 'expose' our output variable
BOOL FuelLoad::Init(EnvContext *pEnvContext, LPCTSTR initStr)
{
// note that initStr (defined in the .envx file) contains the name of the
// XML input file
bool result = LoadXml(initStr, pEnvContext); // load the XML input file.
// expose the output variable
AddOutputVar("Area-Weighted Avg Fuel Load", m_avgFuelLoad, "");
Run(pEnvContext); // populate initial fuel distributions
return TRUE;
}
// InitRun() gets called befor each simulation run. There is nothing special
// needed at beginning of a simulation run, so just return
BOOL FuelLoad::InitRun(EnvContext *pEnvContext, bool useInitialSeed)
{
return TRUE;
}
// Run() is called at each time step. We will iterate through the IDUs,
// get the lulc value and populating the fuel load field if a mapping
// if found. Additionally, we will calculate the area-weighted fuel load
// across the landscape
BOOL FuelLoad::Run(EnvContext *pEnvContext)
{
MapLayer *pLayer = (MapLayer*) pEnvContext->pMapLayer; // get a ptr to the IDU coverage
int colArea = pLayer->GetFieldCol("AREA");
float totalArea = 0;
float totalFuelLoad = 0;
// iterate through the IDU polygons, writing fuel loads if defined
for (MapLayer::Iterator idu = pLayer->Begin(); idu < pLayer->End(); idu++)
{
int lulc = -1;
pLayer->GetData(idu, m_colLULC, lulc);
float area = 0;
pLayer->GetData(idu, colArea, area);
float fuelLoad = 0;
// if mapping for this IDU's lulc class exists, get associated fuel load
try
{
fuelLoad = m_mappings.at(lulc);
}
catch (...) {}
// update the IDU
UpdateIDU(pEnvContext, idu, m_colFuelLoad, fuelLoad);
// update avg fuel load stats
totalArea += area;
totalFuelLoad += fuelLoad * area;
}
// compute final output variable value
m_avgFuelLoad = totalFuelLoad / totalArea;
return TRUE;
}
// Function for loading XML file
bool FuelLoad::LoadXml(LPCTSTR filename, EnvContext *pEnvContext)
{
// does the file somewhere on the path?
CString _filename;
if (PathManager::FindPath(filename, _filename) < 0) // return value: > 0 = success; < 0 = failure (file not found), 0 = path fully qualified and found
{
CString msg;
msg.Format("FuelLoad: Input file '%s' not found - this process will be disabled", filename);
Report::ErrorMsg(msg);
return false;
}
// Load the xml document from disk
TiXmlDocument doc;
bool ok = doc.LoadFile(filename);
if (!ok)
{
Report::ErrorMsg(doc.ErrorDesc());
return false;
}
// Get the root node attributes
TiXmlElement *pXmlRoot = doc.RootElement(); // <fuel_loads>
LPTSTR lulcCol = NULL, fuelLoadCol = NULL;
XML_ATTR attrs[] = {
// attr type address isReq checkCol
{ "lulc_col", TYPE_STRING, &lulcCol, true, 0 },
{ "fuel_col", TYPE_STRING, &fuelLoadCol, true, 0 },
{ NULL, TYPE_NULL, NULL, false, 0 } };
ok = TiXmlGetAttributes(pXmlRoot, attrs, filename);
if (!ok)
{
Report::ErrorMsg("FuelLoad: Missing required attribute(s) in <fuel_loads> tag (should contain 'lulc_col' and 'fuel_col' attributes)");
return false;
}
// make sure the columns exist in the IDU coverage
MapLayer *pLayer = (MapLayer*)pEnvContext->pMapLayer; // get a ptr to the IDU coverage
if (this->CheckCol(pLayer, m_colLULC, lulcCol, TYPE_INT, CC_MUST_EXIST) == false)
{
std::string str("FuelLoad: Missing required column ");
str += lulcCol;
Report::ErrorMsg(str.c_str());
}
// if the Fuel Load field doesn't exist, automatically add it.
this->CheckCol(pLayer, m_colFuelLoad, fuelLoadCol, TYPE_FLOAT, CC_AUTOADD);
// iterate though <fuel_load> tags, storing each pair in our map
TiXmlElement *pXmlFuelLoad = pXmlRoot->FirstChildElement( "fuel_load"); // <fuel_load>
while (pXmlFuelLoad != NULL)
{
int lulc = -1;
float fuelLoad = 0;
XML_ATTR attrs[] = {
// attr type address isReq checkCol
{ "lulc", TYPE_INT, &lulc, true, 0 },
{ "load", TYPE_FLOAT, &fuelLoad, true, 0 },
{ NULL, TYPE_NULL, NULL, false, 0 } };
ok = TiXmlGetAttributes(pXmlFuelLoad, attrs, filename );
if (!ok)
{
Report::ErrorMsg("FuelLoad: Missing required attribute(s) in <fuel_load> tag (should contain 'lulc' and 'load' attributes");
return false;
}
// add the pair to the map
m_mappings[lulc] = fuelLoad;
// get next <fuel_load> tag and repeat
pXmlFuelLoad = pXmlFuelLoad->NextSiblingElement("fuel_load");
}
// all done!
return true;
}
We now have a fully-capable Envision plug-in. Make sure the DLL is copied to the directory where your Envision binaries are located, and we are ready to incorporate
this plug-in into an Envision application.
Step 7 - Add the Model to your Envision application
The final step is to add your new model DLL to your Envision project. To do so, we will add an entry to the project's .envx file,
in the <models> section of the project file. The code below show the entry that is needed in the <models> section of your
project (.envx) file. 'name' is an arbitrary label for this model, 'path' indicates the path to the DLL containing the model,
'id' is unique identifier for this model (not really needed for this DLL, since it contains only a single model), 'use=1' indicates
the model should be included in the simulations, and 'timing=0' indicates this model should run before actor decision-making occurs within
a time step.
<model
name ='Fuel Load Model'
path ='FuelLoad.dll'
id ='0'
use ='1'
timing ='0'
/>
If you save the project (.envx) file with the model entry above added into the <models> section, you should now be able to
run a simulation that includes this model.