view flys-artifacts/src/main/java/de/intevation/flys/utils/FLYSUtils.java @ 4454:c0a418cbd80c

Register flowvelocity artifact factory.
author Felix Wolfsteller <felix.wolfsteller@intevation.de>
date Thu, 08 Nov 2012 16:51:58 +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