view flys-artifacts/src/main/java/de/intevation/flys/utils/FLYSUtils.java @ 4573:b87073a05f9d

flys-client: Patch to render combobox options as clickable links. The way of passing data arguments to the links and further to the Artifact feeding service is somewhat hacked and should be refactored (later...).
author Christian Lins <christian.lins@intevation.de>
date Tue, 27 Nov 2012 12:50:10 +0100
parents c62598c372ab
children e285630569dc
line wrap: on
line source
package de.intevation.flys.utils;

import org.apache.log4j.Logger;

import java.text.NumberFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.xml.xpath.XPathConstants;

import org.w3c.dom.Document;

import org.hibernate.SessionFactory;
import org.hibernate.impl.SessionFactoryImpl;

import gnu.trove.TDoubleArrayList;
import gnu.trove.TIntArrayList;
import gnu.trove.TLongArrayList;

import de.intevation.artifacts.Artifact;
import de.intevation.artifacts.CallContext;

import de.intevation.artifacts.common.utils.Config;
import de.intevation.artifacts.common.utils.XMLUtils;

import de.intevation.flys.backend.SessionFactoryProvider;

import de.intevation.flys.artifacts.context.FLYSContext;
import de.intevation.flys.artifacts.FLYSArtifact;
import de.intevation.flys.artifacts.WINFOArtifact;
import de.intevation.flys.artifacts.StaticWKmsArtifact;
import de.intevation.flys.artifacts.model.RiverFactory;
import de.intevation.flys.artifacts.model.LocationProvider;
import de.intevation.flys.artifacts.model.WQ;
import de.intevation.flys.artifacts.model.WKms;
import de.intevation.flys.artifacts.model.WQKms;

import de.intevation.artifactdatabase.state.State;
import de.intevation.flys.artifacts.states.WaterlevelSelectState;
import de.intevation.flys.artifacts.states.WDifferencesState;
import de.intevation.flys.model.Gauge;
import de.intevation.flys.model.MainValue;
import de.intevation.flys.model.River;


/** static helper methods to e.g. access FLYSArtifacts data. */
public class FLYSUtils {

    /** The logger that is used in this utility. */
    private static Logger logger = Logger.getLogger(FLYSUtils.class);

    public static enum KM_MODE { RANGE, LOCATIONS, NONE };

    /**
     * An enum that represents the 5 possible WQ modes in FLYS. The 5 values are
     * <i>QFREE</i> <i>QGAUGE</i> <i>WGAUGE</i> <i>WFREE</i> and <i>NONE</i>.
     */
    public static enum WQ_MODE { QFREE, QGAUGE, WFREE, WGAUGE, NONE };

    /**
     * An enum that represents the 4 possible WQ input modes in FLYS. The 4
     * values are
     * <i>ADAPTED</i> <i>SINGLE</i> <i>RANGE</i> and <i>NONE</i>.
     */
    public static enum WQ_INPUT { ADAPTED, SINGLE, RANGE, NONE };

    public static final Pattern NUMBERS_PATTERN =
        Pattern.compile("\\D*(\\d++.\\d*)\\D*");

    public static final String XPATH_RIVER_PROJECTION =
        "/artifact-database/floodmap/river[@name=$name]/srid/@value";

    public static final String XPATH_SHAPEFILE_DIR =
        "/artifact-database/floodmap/shapefile-path/@value";

    public static final String XPATH_VELOCITY_LOGFILE =
        "/artifact-database/floodmap/velocity/logfile/@path";

    public static final String XPATH_MAPSERVER_URL =
        "/artifact-database/floodmap/mapserver/server/@path";

    public static final String XPATH_MAPFILE_PATH =
        "/artifact-database/floodmap/mapserver/mapfile/@path";

    public static final String XPATH_MAPFILE_TEMPLATE =
        "/artifact-database/floodmap/mapserver/map-template/@path";

    public static final String XPATH_MAPSERVER_TEMPLATE_PATH =
        "/artifact-database/floodmap/mapserver/templates/@path";


    private FLYSUtils() {
    }


    /**
     * Pulls Artifact with given UUID fromm database.
     * @return FLYSArtifact with given UUID or null (in case of errors).
     */
    public static FLYSArtifact getArtifact(String uuid, CallContext context) {
        try {
            Artifact artifact = context.getDatabase().getRawArtifact(uuid);

            if (artifact == null) {
                logger.error("Artifact '" + uuid + "' does not exist.");
                return null;
            }

            if (!(artifact instanceof FLYSArtifact)) {
                logger.error("Artifact '" +uuid+ "' is no valid FLYSArtifact.");
                return null;
            }

            return (FLYSArtifact) artifact;
        }
        // TODO: catch more selective
        catch (Exception e) {
            logger.error("Cannot get FLYSArtifact " + uuid
                + " from database (" + e.getMessage() + ").");
            return null;
        }
    }


    /**
     * Returns the FLYSContext from context object.
     *
     * @param context The CallContext or the FLYSContext.
     *
     * @return the FLYSContext.
     */
    public static FLYSContext getFlysContext(Object context) {
        return context instanceof FLYSContext
            ? (FLYSContext) context
            : (FLYSContext) ((CallContext) context).globalContext();
    }


    /**
     * Convinience function to retrieve an XPath as string with replaced config
     * directory.
     *
     * @param xpath The XPath expression.
     *
     * @return a string with replaced config directory.
     */
    public static String getXPathString(String xpath) {
        String tmp = Config.getStringXPath(xpath);
        tmp        = Config.replaceConfigDir(tmp);

        return tmp;
    }


    public static boolean isUsingOracle() {
        SessionFactory sf = SessionFactoryProvider.getSessionFactory();

        String d = SessionFactoryProvider.getDriver((SessionFactoryImpl) sf);

        return d != null ? d.indexOf("Oracle") >= 0 : false;
    }


    /**
     * This method returns an WQ_MODE enum which is based on the parameters
     * stored in <i>flys</i> Artifact. If there is no <i>wq_isq</i> parameter
     * existing, WQ_MODE.NONE is returned.
     *
     * @param flys The FLYSArtifact that stores wq mode relevant parameters.
     *
     * @return an enum WQ_MODE.
     */
    public static WQ_MODE getWQMode(FLYSArtifact flys) {
        if (flys == null) {
            return WQ_MODE.NONE;
        }

        String values = flys.getDataAsString("wq_values");
        Boolean isQ   = flys.getDataAsBoolean("wq_isq");

        if (values != null) {
            return isQ ? WQ_MODE.QGAUGE : WQ_MODE.WGAUGE;
        }

        Boolean isFree = flys.getDataAsBoolean("wq_isfree");

        if (isQ != null && isQ) {
            return isFree ? WQ_MODE.QFREE : WQ_MODE.QGAUGE;
        }
        else if (isQ != null && !isQ) {
            return isFree ? WQ_MODE.WFREE : WQ_MODE.WGAUGE;
        }
        else {
            return WQ_MODE.NONE;
        }
    }


    public static WQ_INPUT getWQInputMode(FLYSArtifact flys) {
        if (flys == null) {
            return WQ_INPUT.NONE;
        }

        Boolean selection = flys.getDataAsBoolean("wq_isrange");
        String adapted = flys.getDataAsString("wq_values");

        if(adapted != null && adapted.length() > 0) {
            return WQ_INPUT.ADAPTED;
        }

        if (selection != null && selection) {
            return WQ_INPUT.RANGE;
        }
        else {
            return WQ_INPUT.SINGLE;
        }
    }

    public static KM_MODE getKmRangeMode(FLYSArtifact flys) {
        String mode = flys.getDataAsString("ld_mode");

        if (mode == null || mode.length() == 0) {
            return KM_MODE.NONE;
        }
        else if (mode.equals("distance"))  {
            return KM_MODE.RANGE;
        }
        else if (mode.equals("locations")) {
            return KM_MODE.LOCATIONS;
        }
        else {
            return KM_MODE.NONE;
        }
    }

    /**
     * Get min and max kilometer, independent of parametization
     * (ld_from/to vs ld_locations).
     */
    public static double[] getKmRange(FLYSArtifact flys) {
        switch (getKmRangeMode(flys)) {
            case RANGE: {
                return getKmFromTo(flys);
            }

            case LOCATIONS: {
                double[] locs = getLocations(flys);
                return new double[] { locs[0], locs[locs.length-1] };
            }

            case NONE: {
                double[] locs = getLocations(flys);
                if (locs != null) {
                    return new double[] { locs[0], locs[locs.length-1] };
                }
                else {
                    return getKmFromTo(flys);
                }
            }
        }

        return new double[] { Double.NaN, Double.NaN };
    }


    /**
     * Get bounds for river of artifact.
     * @param flysArtifact artifact which has a "river" data.
     * @return double array. min is at[0], max at[1]. null if given artifact is null
     */
    public static double[] getRiverMinMax(FLYSArtifact flysArtifact) {
        if (flysArtifact == null) {
            return null;
        }

        String riverName = flysArtifact.getDataAsString("river");

        if (riverName == null) {
            riverName = "";
        }

        logger.debug("Search for the min/max distances of '" + riverName + "'");

        River river = RiverFactory.getRiver(riverName);

        return river != null
            ? river.determineMinMaxDistance()
            : null;
    }


    public static double[] getKmFromTo(FLYSArtifact flys) {
        String strFrom = flys.getDataAsString("ld_from");
        String strTo   = flys.getDataAsString("ld_to");

        if (strFrom == null) {
            strFrom = flys.getDataAsString("from");
        }

        if (strTo == null) {
            strTo = flys.getDataAsString("to");
        }

        if (strFrom == null || strTo == null) {
            return null;
        }

        try {
            return new double[] {
                Double.parseDouble(strFrom),
                Double.parseDouble(strTo) };
        }
        catch (NumberFormatException nfe) {
            return null;
        }
    }


    /**
     * Return sorted array of locations at which stuff was calculated
     * (from ld_locations data), null if not parameterized this way.
     */
    public static double[] getLocations(FLYSArtifact flys) {
        String locationStr = flys.getDataAsString("ld_locations");

        if (locationStr == null || locationStr.length() == 0) {
            if (flys instanceof WINFOArtifact) {
                WINFOArtifact winfo = (WINFOArtifact) flys;
                if (winfo.getReferenceStartKm() != null) {
                    return new double[]
                        {
                            winfo.getReferenceStartKm().doubleValue(),
                            winfo.getReferenceEndKms()[0]
                        };
                }
            }
            return null;
        }

        String[] tmp               = locationStr.split(" ");
        TDoubleArrayList locations = new TDoubleArrayList();

        for (String l: tmp) {
            try {
                locations.add(Double.parseDouble(l));
            }
            catch (NumberFormatException nfe) {
            }
        }

        locations.sort();

        return locations.toNativeArray();
    }


    /**
     * Returns the Qs for a given FLYSArtifact. This method currently accepts
     * only instances of WINFOArtifact.
     *
     * @param flys A FLYSArtifact.
     *
     * @return the Qs.
     */
    public static double[] getQs(FLYSArtifact flys) {
        // XXX this is not nice!
        if (flys instanceof WINFOArtifact) {
            return ((WINFOArtifact) flys).getQs();
        }

        logger.warn("This method currently supports WINFOArtifact only!");

        return null;
    }


    /**
     * Returns the Ws for a given FLYSArtifact. This method currently accepts
     * only instances of WINFOArtifact.
     *
     * @param flys A FLYSArtifact.
     *
     * @return the Ws.
     */
    public static double[] getWs(FLYSArtifact flys) {
        // XXX this is not nice!
        if (flys instanceof WINFOArtifact) {
            return ((WINFOArtifact) flys).getWs();
        }

        logger.warn("This method currently supports WINFOArtifact only!");

        return null;
    }


    /**
     * Returns the selected River object based on the 'river' data that might
     * have been inserted by the user.
     *
     * @return the selected River or null if no river has been chosen yet.
     */
    public static River getRiver(FLYSArtifact flys) {
        String sRiver = getRivername(flys);

        return (sRiver != null)
            ? RiverFactory.getRiver(sRiver)
            : null;
    }


    /**
     * Returns the name of the river specified in the given <i>flys</i>
     * Artifact.
     *
     * @param flys The FLYSArtifact that stores a river relevant information.
     *
     * @return the name of the specified river or null.
     */
    public static String getRivername(FLYSArtifact flys) {
        return flys != null ? flys.getDataAsString("river") : null;
    }


    /**
     * Extracts the SRID defined in the global configuration for the river
     * specified in <i>artifact</i>.
     *
     * @param artifact The FLYSArtifact that stores the name of the river.
     *
     * @return the SRID as string (e.g. "31466").
     */
    public static String getRiverSrid(FLYSArtifact artifact) {
        String river = artifact.getDataAsString("river");

        if (river == null || river.length() == 0) {
            return null;
        }

        return getRiverSrid(river);
    }


    public static String getRiverSrid(String rivername) {
        Map<String, String> variables = new HashMap<String, String>(1);
        variables.put("name", rivername);

        Document cfg = Config.getConfig();

        return (String) XMLUtils.xpath(
            cfg,
            XPATH_RIVER_PROJECTION,
            XPathConstants.STRING,
            null,
            variables);
    }


    /**
     * Return the (first) Gauge corresponding to the given location(s) of
     * the artifact.
     * @param flys the artifact in question.
     * @return (First) gauge of locations of river of artifact.
     */
    public static Gauge getGauge(FLYSArtifact flys) {
        River river = getRiver(flys);

        if (river == null) {
            logger.debug("no river found");
            return null;
        }

        double[] dist  = getKmRange(flys);

        if (dist == null) {
            logger.debug("no range found");
            return null;
        }

        if (logger.isDebugEnabled()) {
            logger.debug("Determine gauge for:");
            logger.debug("... river: " + river.getName());
            logger.debug("... distance: " + dist[0] + " - " + dist[1]);
        }

        Gauge gauge = river.determineGauge(dist[0], dist[1]);

        String name = gauge != null ? gauge.getName() : "'n/a";
        logger.debug("Found gauge: " + name);

        return gauge;
    }


    public static String getGaugename(FLYSArtifact flys) {
        Gauge gauge = getGauge(flys);

        return gauge != null ? gauge.getName() : null;
    }


    public static Gauge getReferenceGauge(FLYSArtifact flys) {
        Long officialNumber = flys.getDataAsLong("reference_gauge");

        return officialNumber != null
            ? Gauge.getGaugeByOfficialNumber(officialNumber)
            : null;
    }


    public static String getReferenceGaugeName(FLYSArtifact flys) {
        Gauge refGauge = getReferenceGauge(flys);

        return refGauge != null
            ? refGauge.getName()
            : "-- not found --";
    }


    public static Double getValueFromWQ(WQ wq) {
        if (wq == null) {
            return null;
        }

        Matcher m = NUMBERS_PATTERN.matcher(wq.getName());

        if (m.matches()) {
            logger.debug("Found a number.");

            String raw = m.group(1);

            try {
                return Double.valueOf(raw);
            }
            catch (NumberFormatException nfe) {
            }
        }

        return null;
    }


    /** Creates human-readable name for a wsp (waterlevel/longitudinal section).
     * @param name will be split at '='s.
     */
    public static String createWspWTitle(
        WINFOArtifact winfo,
        CallContext   cc,
        String        name
    ) {
        String[] parts = name.split("=");

        NumberFormat nf = Formatter.getWaterlevelW(cc);

        String namedMainValue = null;

        boolean isQ    = winfo.isQ();
        boolean isFree = winfo.isFreeQ();

        double v;

        try {
            v = Double.valueOf(parts[1]);

            namedMainValue = getNamedMainValue(winfo.getGauge(), v);
        }
        catch (NumberFormatException nfe) {
            logger.warn("Cannot parse Double of: '" + parts[1] + "'");
            return name;
        }

        String prefix = null;

        if (isQ && !isFree && namedMainValue != null) {
            return "W (" + namedMainValue + ")";
        }

        if (isQ) {
            prefix = "Q=";
        }

        return prefix == null
            ? "W(" + nf.format(v) + ")"
            : "W(" + prefix + nf.format(v) + ")";
    }


    public static String createWspQTitle(
        WINFOArtifact winfo,
        CallContext   cc,
        String        name
    ) {
        String[] parts = name.split("=");

        NumberFormat nf = Formatter.getWaterlevelQ(cc);

        String namedMainValue = null;

        boolean isQ    = winfo.isQ();
        boolean isFree = winfo.isFreeQ();

        double v;

        try {
            v = Double.valueOf(parts[1]);

            namedMainValue = getNamedMainValue(winfo.getGauge(), v);
        }
        catch (NumberFormatException nfe) {
            logger.warn("Cannot parse Double of: '" + parts[1] + "'");
            return name;
        }

        String prefix = null;

        if (isQ && !isFree && namedMainValue != null) {
            return namedMainValue;
        }

        if (!isQ) {
            prefix = "W=";
        }

        return prefix == null
            ? "Q(" + nf.format(v) + ")"
            : "Q(" + prefix + nf.format(v) + ")";
    }


    /**
     * Returns the named main value if a Q was selected and if this Q fits to a
     * named main value. Otherwise, this function returns null.
     *
     * @param winfo The WINFO Artifact.
     * @param value The Q (or W) value.
     *
     * @return a named main value or null.
     */
    public static String getNamedMainValue(WINFOArtifact winfo, double value) {
        WQ_MODE wqmode = getWQMode(winfo);

        if (wqmode != WQ_MODE.QGAUGE) {
            return null;
        }
        else {
            return getNamedMainValue(winfo.getGauge(), value);
        }
    }


    public static String getNamedMainValue(Gauge gauge, double value) {
        List<MainValue> mainValues = gauge.getMainValues();
        logger.debug("Search named main value for: " + value);

        for (MainValue mv: mainValues) {
            if (mv.getValue().doubleValue() == value) {
                logger.debug("Found named main value: " + mv.getMainValue().getName());
                return mv.getMainValue().getName();
            }
        }

        logger.debug("Did not find a named main value for: " + value);
        return null;
    }


    /**
     *
     * @param nmv A string that represents a named main value.
     *
     * @throws NullPointerException if nmv is null.
     */
    public static String stripNamedMainValue(String nmv) {
        int startIndex = nmv.indexOf("(");
        int endIndex   = nmv.indexOf(")");

        if (startIndex > 0 && endIndex > 0 && startIndex < endIndex) {
            return nmv.substring(0, startIndex);
        }

        return nmv;
    }


    /**
     * Returns the URL of user mapfile for the owner of Artifact
     * <i>artifactId</i>.
     *
     * @param artifactId The UUID of an artifact.
     *
     * @return the URL of the user wms.
     */
    public static String getUserWMSUrl(String artifactId) {
        String url = getXPathString(XPATH_MAPSERVER_URL);
        url = url.endsWith("/") ? url + "user-wms" : url + "/" + "user-wms";

        return url;
    }


    /**
     * This method returns the description for a given <i>km</i> for a specific
     * river. The river is provided by the FLYSArtifact <i>flys</i>.
     *
     * @param flys The FLYSArtifact that provides a river.
     * @param km The kilometer.
     *
     * @return the description for <i>km</i> or an empty string if no
     * description was found.
     */
    public static String getLocationDescription(FLYSArtifact flys, double km) {
        String river = getRivername(flys);

        if (river == null) {
            return "";
        }

        return LocationProvider.getLocation(river, km);
    }


    /**
     * This method returns the differences for a w-differences calculation.
     *
     * @param winfo The WINFOArtifact.
     * @param context The context.
     *
     * @return The differences as string separated by semicolon and linebreak.
     */
    public static String getWDifferences(
        WINFOArtifact winfo,
        CallContext context)
    {
        State state = winfo.getCurrentState(context);
        if(state instanceof WDifferencesState) {
            String diffids = winfo.getDataAsString("diffids");
            String datas[] = diffids.split("#");

            // Validate the Data-Strings.
            for (String s: datas) {
                if (!WaterlevelSelectState.isValueValid(s)) {
                    return "";
                }
            }

            if (datas.length < 2) {
                return "";
            }

            String diffs = "";
            for(int i = 0; i < datas.length; i+=2) {
                // e.g.:
                // 42537f1e-3522-42ef-8968-635b03d8e9c6;longitudinal_section.w;1
                WKms minuendWKms = getWKms(StringUtil.unbracket(datas[i+0]),
                    context);
                WKms subtrahendWKms = getWKms(StringUtil.unbracket(datas[i+1]),
                    context);
                if (minuendWKms != null && subtrahendWKms != null) {
                    diffs += StringUtil.wWrap(minuendWKms.getName())
                        + " - " + StringUtil.wWrap(subtrahendWKms.getName());
                }
                diffs += ";\n";
            }
            return diffs;
        }
        else {
            logger.warn("Not a valid state for differences.");
            return "";
        }
    }


    protected static WKms getWKms(String mingle, CallContext context) {
        String[] def  = mingle.split(";");
        String   uuid = def[0];
        String   name = def[1];
        int      idx  = Integer.parseInt(def[2]);

        if (name.startsWith("staticwkms")) {
            StaticWKmsArtifact staticWKms =
                (StaticWKmsArtifact) FLYSUtils.getArtifact(
                    uuid,
                    context);
            WKms wkms = staticWKms.getWKms(idx);
            if (wkms == null)
                logger.error("No WKms from artifact.");
            return wkms;
        }

        WINFOArtifact flys = (WINFOArtifact) FLYSUtils.getArtifact(
            uuid,
            context);

        if (flys == null) {
            logger.warn("One of the artifacts (1) for diff calculation could not be loaded");
            return null;
        }
        else{
            WQKms[] wqkms = (WQKms[]) flys.getWaterlevelData().
                                              getData();
            if (wqkms == null)
            logger.warn("not  waterlevels in artifact");
            else if (wqkms.length < idx)
            logger.warn("not enough waterlevels in artifact");
            return wqkms[idx];
        }
    }


    /**
     * This method transform a string into an int array. Therefore, the string
     * <i>raw</i> must consist of int values separated by a <i>';'</i>.
     *
     * @param raw The raw integer array as string separated by a ';'.
     *
     * @return an array of int values.
     */
    public static int[] intArrayFromString(String raw) {
        String[] splitted = raw != null ? raw.split(";") : null;

        if (splitted == null || splitted.length == 0) {
            logger.warn("No integer values found in '" + raw + "'");
            return new int[0];
        }

        TIntArrayList integers = new TIntArrayList(splitted.length);

        for (String value: splitted) {
            try {
                integers.add(Integer.parseInt(value));
            }
            catch (NumberFormatException nfe) {
                logger.warn("Parsing integer failed: " + nfe);
            }
        }

        return integers.toNativeArray();
    }


    /**
     * This method transform a string into a long array. Therefore, the string
     * <i>raw</i> must consist of int values separated by a <i>';'</i>.
     *
     * @param raw The raw long array as string separated by a ';'.
     *
     * @return an array of int values.
     */
    public static long[] longArrayFromString(String raw) {
        String[] splitted = raw != null ? raw.split(";") : null;

        if (splitted == null || splitted.length == 0) {
            logger.warn("No long values found in '" + raw + "'");
            return new long[0];
        }

        TLongArrayList longs = new TLongArrayList(splitted.length);

        for (String value: splitted) {
            try {
                longs.add(Long.valueOf(value));
            }
            catch (NumberFormatException nfe) {
                logger.warn("Parsing long failed: " + nfe);
            }
        }

        return longs.toNativeArray();
    }


    /**
     * This method transform a string into an double array. Therefore, the
     * string <i>raw</i> must consist of double values separated by a
     * <i>';'</i>.
     *
     * @param raw The raw double array as string separated by a ';'.
     *
     * @return an array of double values.
     */
    public static double[] doubleArrayFromString(String raw) {
        String[] splitted = raw != null ? raw.split(";") : null;

        if (splitted == null || splitted.length == 0) {
            logger.warn("No double values found in '" + raw + "'");
            return new double[0];
        }

        TDoubleArrayList doubles = new TDoubleArrayList(splitted.length);

        for (String value: splitted) {
            try {
                doubles.add(Double.valueOf(value));
            }
            catch (NumberFormatException nfe) {
                logger.warn("Parsing double failed: " + nfe);
            }
        }

        return doubles.toNativeArray();
    }


    /**
     * Returns the gauges that match the selected kilometer range.
     *
     * @param flys the flys artifact.
     *
     * @return the gauges based on the selected kilometer range (null if
     *         none/no range set).
     */
    public static List<Gauge> getGauges(FLYSArtifact flys) {

        River river = getRiver(flys);
        if (river == null) {
            logger.debug("getGauges: no river!");
            return null;
        }

        double [] dist = getKmRange(flys);
        if (dist == null) {
            logger.debug("getGauges: no dist!");
            return null;
        }
        logger.debug("getGauges: " + dist[0] + " - " + dist[1]);

        return river.determineGauges(dist[0], dist[1]);
    }
}
// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :

http://dive4elements.wald.intevation.org