Table of Contents

Development tips and tricks

This section contains little examples to help develop easier in Capsis.

Pending:

1. How to use the Translator for french / english user interfaces

In Capsis, the default language is english: source code, comments, documentation…

The only exception is the multilingual graphical user interface.

Capsis can be launched in a given language with an option, e.g. capsis -l en (for english).

The Capsis main frame (the only JFrame) and all the dialogs and panels (including the ones in the modules and extensions) must use the Translator to write on the GUI. This concerns the window titles, all the labels including buttons texts, status messages, error messages…

At runtime, the Translator will swap a given 'translationKey' into a text in french, english…

The translations of these keys must be provided in files with a conventional name.

Example

In a class called 'mypackage.MyClass.java', I create a JButton.

JButton load = new JButton ("MyClass.loadTheFile");

I must provide a file in mypackage called MyClass_fr.properties containing the french translation…

# MyClass_fr.properties

MyClass.loadTheFile = Charger

(... other translations)

… and a second file in mypackage called MyClass_en.properties containing the english translation.

#MyClass_en.properties

MyClass.loadTheFile = Load

(... other translations)

The translator needs to be told which translation bundle it must rely on. Generally, you can add a static initializer at the top of the class to tell this to the Translator: such an initializer will be run only once even if you use the class several times (e.g. a dialog).

static {
    Translator.addBundle ("mypackage.MyClass");  // _en or _fr.properties will be added depending on the language
}

Notes

The methods in Translator are all static methods to make it easy to use from everywhere, e.g. Translator.swap (“MyClass.wrongFileName”).

There are conventions for the translation keys: it is suggested to build the name with two members separated by a dot '.':

The resulting key: MyClass.ageMustBeAPositiveInteger

The name should be significant: in case the translation is not found, the key will be written on the GUI. With these naming conventions, the message can be understood and the user will know in what class the translation is missing.

In any case, french and english bundles are required (default case). In case the developer does not speak french, english only is allowed. Other languages are also possible.

2. Checking the user values in JTextFields with Check

When the user is requested to entry values in textfields in a dialog box, the values must be tested, generally when the Ok button is hit. The user may enter any String he likes and may possibly do mistakes when entering wrong characters when numbers are asked.

To test the entered values, it is possible to use the shortcuts provided by the jeeb.lib.util.Check class. This class contains methods to check if a String (e.g. JTextField.getText ()) is a correct integer, double or file (…) and methods to retrieve the matching int, double of file.

If a format error is found, it may be reported to the user with a MassageDialog.print ();

import jeeb.lib.util.Check;
...
private boolean checkUserEntry () {
 
    // Checks...
    // Stand characteristics
    if (Check.isEmpty (standName.getText ().trim ())) {
        MessageDialog.print (this, Translator.swap ("ModisInitialDialog.standNameMustBeProvided"));
        return false;
    }
 
    if (!Check.isInt (standAge.getText ().trim ())) {
        MessageDialog.print (this, Translator.swap ("ModisInitialDialog.standAgeMustBeAnIntBetWeen12and70"));
        return false;
    }
    int a = Check.intValue (standAge.getText ().trim ());
    if (a < 12 || a > 70) {
        MessageDialog.print (this, Translator.swap ("ModisInitialDialog.standAgeMustBeAnIntBetWeen12and70"));
        return false;
    }
 
    if (!Check.isDouble (dominantHeight.getText ().trim ())) {
        MessageDialog.print (this, Translator.swap ("ModisInitialDialog.dominantHeightMustBeADoubleBetween6and30m"));
        return false;
    }
    double dh = Check.doubleValue (dominantHeight.getText ().trim ());
    if (dh < 6 || dh > 30) {
        MessageDialog.print (this, Translator.swap ("ModisInitialDialog.dominantHeightMustBeADoubleBetween6and30m"));
        return false;
    }

Note: in the above example, we use getText ().trim () on the textfields to get the String the user typed and remove the leading and trailing spaces / tabs.

Note: All the message reported to the user with MessageDialog pass through the Translator.swap (key) method to be translated in french / english.

3. Sending a user Message with MessageDialog

Note: this must NOT be used in the model package of your model because it can be run in script mode where no dialog box at all must be opened. You may use this in the Initial or Evolution Dialogs or in the GUI part of an extension (e.g. in an intervener, to report a user input error).

When a trouble occurs in a user dialog box, it is easy to tell the user with a message. The fastest way to do it is to use MessageDialog with its print () static methods (MessageDialog.print (…)).

The message may be accompanied by an exception. If given to the MessageDialog, a 'See Log“ button will be added to open the Log browser with a single click.

Note: the first parameter is a JComponent to help MessageDialog manage the focus correctly by searching the parent Window of the component and opening relatively to it (It may be a JPanel included in the Dialog or the Dialog itself…). Very often, if the message is sent from the code of the embedding dialog, 1st param is 'this'.

import jeeb.lib.util.MessageDialog;
...
// Note: for everything coming to the graphical user interface, we use the Translator
// A simple message
MessageDialog.print (this, Translator.swap ("MigInitialDialog.fileNameIsNotFile"));
 
// A more complex example (from the 'Migration' module)
// A error occurs: 1. we write in the Log, 2. we tell the user with 
// MessageDialog WITH the exception passed
try {
    initStand = model.loadInitStand (sets.inventoryFileName);
} catch (Exception e) {
    Log.println (Log.ERROR, "MigInitialDialog.preLoadAction ()", 
            "could not read inventory file " + sets.inventoryFileName, e);
    MessageDialog.print (this, 
            Translator.swap ("MigInitialDialog.couldNotReadInventoryFileSeeLog"), e);
    return;
}

4. Get the paths of the files and directories in a portable way (on Linux, Windows and other OS)

You can ask to the PathManager for the application installation directory:

import capsis.kernel.PathManager;
...
String dir = PathManager.getInstallDir ();
// e.g. dir = /home/coligny/workspace/capsis (Linux)
// OR C:\capsis (windows)

You may also quickly get the path of a directory relatively to the installation directory:

String modelsFileName = PathManager.getDir ("etc") + "/capsis.models";
// e.g. /home/coligny/workspace/capsis/etc/capsis.models  (Linux)
// OR C:\capsis\etc\capsis.models (windows)

You may use this tip to always declare you file paths in a portable way:

String wrongFileName = C:\capsis\etc\myDataFile.txt  // Never do this
String goodFileName = PathManager.getDir ("etc") + "/myDataFile.txt";  // Prefer this

5. Write a clone () method

A clone () method can return a copy of an object. The object needs to implement the Cloneable interface. This method relies on the clone () method of the Object class but some extra work needs to be done. The developer can choose to build an exact copy of the original or some object that needs to be completed (see null ref in the example (3) below).

The very specific case (2) below is rare: generally, all the objects linked to the original need to be cloned to be linked to the clone.

If instance variables also need to be cloned (see (1) below), the developer must make sure the related clone () method in the class of the variable is provided and correct.

/** Clone a PDGTree: first calls super.clone (), then clone the 
 *  instance variables.
 */
public Object clone () {
    try {
        // Object.clone () allocates the required memory for t (the clone) 
        // and copies the primitive types values (byte, short, int, long, 
        // float, double, char, boolean): we do not need to copy them.
        // But the reference variables are copied too: the clone instance variables
        // are the same objects than the original: this must generally be changed (see below)
        PDGTree t = (PDGTree) super.clone ();  // calls protected Object.clone ()
 
        // We must take care of all the Object (not primitive) instance variables
 
        // (1) If this ref is not null, clone the related object
        if (fmCell != null) {t.fmCell = (FmCell) fmCell.clone ();}
 
        // (2) In rare and specific cases, keep a ref to the same object than the original
        t.geeImpl = geeImpl;  // like immutable, a genotype, does not change
 
        // (3) It is also possible to set some references to null
        t.phenoValue = null;  // later, needs to be improved
 
        return t;
    } catch (Exception e) {
        Log.println (Log.ERROR, "PDGTree.clone ()", "Error while cloning", e);
        return null;
    }
}

To help writing clone () methods, some utility methods are provided in AmapTools.

public double[] clumping;
public double[][] speciesProportion;
 
...
public Object clone () {
    try {
        FmCell c = (FmCell) super.clone ();
        c.clumping = AmapTools.getCopy (clumping);
        c.speciesProportion = AmapTools.getCopy (speciesProportion);
        ...

6. Add a simple status bar to your dialog

If you want to report the status of a process without using Capsis status bar with StatusDispatcher, but you would prefer to have your own status field in your dialog, you may use directly StatusBar.

import jeeb.lib.util.task.StatusBar;
 
private StatusBar statusBar;
 
(...)
statusBar = new StatusBar ();
 
(...)
statusBar.print (Translator.swap ("DIntervention.chooseAnIntervener"));

Note: This is more than writing in a simpe JLabel: here, writing occurs immediately, without delay. The usual delay due to Java single thread gui management may prevent from seing some messages in a simple JLabel.setText (someMessage) in case a loud process is launched, e.g. a file loading.

7. Report the status of a process

In Capsis, it is possible to report the status of a given process by printing in the StatusDispatcher. In interactive mode, the messages will appear in the status bar of the main window. In Script mode (no gui), they will be written in the terminal.

All messages must be translated with Translator.swap (“ClassName.myMessage”) because they may appear on the user interface.

import jeeb.lib.util.StatusDispatcher;
...
/** These initializations are done once first stand is retrieved
 *  and before returning to Pilot.
 */
public void initializeModel (MountStand initStand) {
 
    MountPlot plot = (MountPlot) initStand.getPlot ();
 
    // 1. Create MountBeamSet with MountBeamSetFactory
    StatusDispatcher.print (Translator.swap ("MountModel.creatingBeamSet"));
    if (mountSettings.lightClassic) {
        createClassicBeamSet ();
    } else ...
    }
 
    // 2. Compute relative cell neighbourhoods
    StatusDispatcher.print (Translator.swap ("MountModel.computingNeighbourhoodsForBeams"));
    computeRelativeCellNeighbourhoods (initStand);
 
    ...
 
}

8. Report the progress of a process

The ProgressDispatcher can be used in the Capsis modules to report the progression of a process. It is frequently used in the main method: processEvolution () within the main growth loop.

In interactive mode, a progress bar will appear to show the progression. In Script mode (no gui), the progression will appear in the terminal like this: [0→9: 0 1 2 3 4 (we are currently at stage 4 out of 10).

import jeeb.lib.util.ProgressDispatcher;
...
// Init the progressDispatcher
ProgressDispatcher.setMinMax (1, totalTreeNumber);
 
int k = 1;
 
// Iterate on the trees
for (Iterator i = newStand.getTrees ().iterator (); i.hasNext ();) {
 
    // Set the current value
    ProgressDispatcher.setValue (k++);
    ...
 
}
 
// End of progression reporting
ProgressDispatcher.setValue (totalTreeNumber);
ProgressDispatcher.stop ()

9. Write text to a file

This code writes lines into a file. The file is created if it does not exist.

import java.io.*;
...
try {
    BufferedWriter out = new BufferedWriter (new FileWriter (fileName));
    out.write ("aString");
    out.newLine ();
    out.write ("anotherString");
    out.newLine ();
    out.close ();
} catch (Exception e) {
    Log.println (Log.ERROR, "MyClass.myMethod ()", 
            "Could not write in file: " + fileName, e);
}

10. Read text from a file

This code reads lines from a file.

import java.io.*;
...
try {
    BufferedReader in = new BufferedReader (new FileReader (fileName));
    String str;
    while ((str = in.readLine ()) != null) {
        process (str);
    }
    in.close();
} catch (Exception e) {
    Log.println (Log.ERROR, "MyClass.myMethod ()", 
            "Could not read from file: " + fileName, e);
}

11. Create a simple collection

An ArrayList is the simplest ordered collection.

A HashSet is the simplest unordered collection, it can contain no duplicates.

import java.util.*;
...
// Without generics
List list = new ArrayList ();
list.add (tree1);
list.add (tree2);
...
list.remove (tree1);
...
if (list.isEmpty ()) {...}
 
Set set = new HashSet ();
set.add (tree1);
set.add (tree3);
...
set.removeAll ();
 
// With generics: specify the type of the elements in the collection
List<String> list = new ArrayList<String> ();
list.add ("Name 1");
list.add ("Name 2");

12. Iterate on a collection

It is possible to iterate on all the elements in a given collection to do some action for each element.

// Without generics, e.g. a list of Strings 
List list = new ArrayList ();
...
for (Iterator i = list.iterator (); i.hasNext ();) {
    String s = (String) i.next ();
    someAction (s);
}
 
// With generics, e.g. a list of Strings
List<String> list = new ArrayList<String> ();
...
for (String s : list) {
    someAction (s);
}

13. Write in the Log

When a trouble occurs and you want to log it to the var/capsis.log file, use one of the following methods.

import jeeb.lib.util.Log;
...
// Log.println (gravity, "source class / method", 
//        "some message in english", ref to the exception (optional));
 
// ** The most common: an unexpected exception **
// Note: Starter.c () means 'constructor'. The parameters of the method are 
// not required here but you may use Starter.c (String) if several construtors
Log.println (Log.ERROR, "Starter.c ()", "Exception in Starter", e);
 
// An error, without Exception
Log.println (Log.ERROR, "KernelInit.init ()", "config error, aborting");
// A warning, with an Exception
Log.println (Log.WARNING, "KernelInit.init ()", 
        "Error while deleting etc/extensions.settings, passed ", e);
// A simple message
Log.println ("File was created: " + tmpDir);

If you like, you may write in custom log files, created on the fly with the given name.

// Writes in var/Understanding.log
Log.println("Understanding", 
        "Year "+(oldStand.getDate())+" tree "+ t.getId()+" taken to be grown;");

The logs are writen in capsis4/var/. Capsis default log file is named capsis.log. You may read it with a text editor or directly from Capsis from the Tool menu.

14. Alert

When something goes wrong or for specific notifications in a code that can be run in interactive mode (with windows / dialogs) or in scripts (for long or repetitive simulation without a gui), you may user Alert to notify the user.

If the code runs in a gui or if Alert.setInteractive (true) was called (e.g. by the Capsis top level window), a message dialog will appear. Otherwise, the message will be written in system.err (in the terminal).

Alert.print (message);
 
Alert.print (message, throwable);

15. Tune the objects fields in the inspectors

Inspectors can be opened when selecting from various Capsis viewers (e.g. from the Simple viewer).

The selected objects appear in a 2 columns table listing their attributes and values. The attributes are the public fieds in the object and also the result of the invocation of the public accessors (getXXX (), isXXX ()).

The resulting list is sometimes too long for a quick understanding. It can be tuned this way:

// E.g. in MountTree implements jeeb.lib.util.inspector.InspectorList
@Override
public Collection<String> getInspectorList () {
 
    return Arrays.asList("getId", "getAge", "getName", "getX", "getY", "getZ");
 
}

When the object is viewed in an inspector, it is possible to restrict the number of rows to the most interesting properties.

16. A tool to classify easily

See this doc on the AMAPstudio web site