sascha@2729: package de.intevation.flys.artifacts.model.fixings; sascha@2729: sascha@2729: import de.intevation.flys.artifacts.FixationArtifactAccess; sascha@2729: sascha@2729: import de.intevation.flys.artifacts.math.fitting.Function; sascha@2729: import de.intevation.flys.artifacts.math.fitting.FunctionFactory; sascha@2729: sascha@2729: import de.intevation.flys.artifacts.model.Calculation; sascha@2729: import de.intevation.flys.artifacts.model.CalculationResult; sascha@2729: import de.intevation.flys.artifacts.model.FixingsColumn; sascha@2729: import de.intevation.flys.artifacts.model.FixingsColumnFactory; sascha@3008: sascha@3008: import de.intevation.flys.artifacts.model.FixingsOverview.AndFilter; sascha@3008: import de.intevation.flys.artifacts.model.FixingsOverview.DateRangeFilter; sascha@3121: sascha@3121: import de.intevation.flys.artifacts.model.FixingsOverview.Fixing.Filter; sascha@3121: sascha@3008: import de.intevation.flys.artifacts.model.FixingsOverview.Fixing; sascha@3121: import de.intevation.flys.artifacts.model.FixingsOverview.IdsFilter; sascha@3008: import de.intevation.flys.artifacts.model.FixingsOverview.KmFilter; sascha@3121: import de.intevation.flys.artifacts.model.FixingsOverview.SectorFilter; sascha@3008: import de.intevation.flys.artifacts.model.FixingsOverview.SectorRangeFilter; sascha@3008: sascha@2729: import de.intevation.flys.artifacts.model.FixingsOverview; sascha@2729: import de.intevation.flys.artifacts.model.FixingsOverviewFactory; sascha@2729: import de.intevation.flys.artifacts.model.Parameters; sascha@3145: import de.intevation.flys.artifacts.model.Range; sascha@2729: sascha@3008: import de.intevation.flys.utils.DateAverager; sascha@2729: import de.intevation.flys.utils.DoubleUtil; sascha@3014: import de.intevation.flys.utils.KMIndex; sascha@2729: sascha@2729: import java.util.ArrayList; sascha@3008: import java.util.Date; sascha@3008: import java.util.HashMap; sascha@2729: import java.util.List; sascha@3008: import java.util.Map; sascha@2729: sascha@3145: import org.apache.commons.math.stat.descriptive.moment.StandardDeviation; sascha@3145: sascha@2729: import org.apache.log4j.Logger; sascha@2729: sascha@2729: public class FixCalculation sascha@2729: extends Calculation sascha@2729: { sascha@2786: private static Logger log = Logger.getLogger(FixCalculation.class); sascha@2729: sascha@3011: public static final double EPSILON = 1e-4; sascha@3011: sascha@3003: protected String river; sascha@3003: protected double from; sascha@3003: protected double to; sascha@3003: protected double step; sascha@3003: protected boolean preprocessing; sascha@3003: protected String function; sascha@3003: protected int [] events; sascha@3121: protected DateRange referencePeriod; sascha@3003: protected DateRange [] analysisPeriods; sascha@3003: protected int qSectorStart; sascha@3003: protected int qSectorEnd; sascha@2729: sascha@2729: public FixCalculation() { sascha@2729: } sascha@2729: sascha@2729: public FixCalculation(FixationArtifactAccess access) { sascha@2729: sascha@3121: String river = access.getRiver(); sascha@3121: Double from = access.getFrom(); sascha@3121: Double to = access.getTo(); sascha@3121: Double step = access.getStep(); sascha@3121: String function = access.getFunction(); sascha@3121: int [] events = access.getEvents(); sascha@3121: DateRange referencePeriod = access.getReferencePeriod(); sascha@3003: DateRange [] analysisPeriods = access.getAnalysisPeriods(); sascha@3121: Integer qSectorStart = access.getQSectorStart(); sascha@3121: Integer qSectorEnd = access.getQSectorEnd(); sascha@3121: Boolean preprocessing = access.getPreprocessing(); sascha@2729: sascha@2729: if (river == null) { sascha@2729: // TODO: i18n sascha@2729: addProblem("fix.missing.river"); sascha@2729: } sascha@2729: sascha@2729: if (from == null) { sascha@2729: // TODO: i18n sascha@2729: addProblem("fix.missing.from"); sascha@2729: } sascha@2729: sascha@2729: if (to == null) { sascha@2729: // TODO: i18n sascha@2729: addProblem("fix.missing.to"); sascha@2729: } sascha@2729: sascha@2729: if (step == null) { sascha@2729: // TODO: i18n sascha@2729: addProblem("fix.missing.step"); sascha@2729: } sascha@2729: sascha@2729: if (function == null) { sascha@2729: // TODO: i18n sascha@2729: addProblem("fix.missing.function"); sascha@2729: } sascha@2729: sascha@2729: if (events == null || events.length < 1) { sascha@2729: // TODO: i18n sascha@2729: addProblem("fix.missing.events"); sascha@2729: } sascha@2729: sascha@3121: if (referencePeriod == null) { sascha@3121: // TODO: i18n sascha@3121: addProblem("fix.missing.reference.period"); sascha@3121: } sascha@3121: sascha@2744: if (analysisPeriods == null || analysisPeriods.length < 1) { sascha@2744: // TODO: i18n sascha@2744: addProblem("fix.missing.analysis.periods"); sascha@2744: } sascha@2744: sascha@2744: if (qSectorStart == null) { sascha@2744: // TODO: i18n sascha@2744: addProblem("fix.missing.qstart.sector"); sascha@2744: } sascha@2744: sascha@2744: if (qSectorEnd == null) { sascha@2744: // TODO: i18n sascha@2744: addProblem("fix.missing.qend.sector"); sascha@2744: } sascha@2744: sascha@3011: if (preprocessing == null) { sascha@3011: // TODO: i18n sascha@3011: addProblem("fix.missing.preprocessing"); sascha@3011: } sascha@3011: sascha@2729: if (!hasProblems()) { sascha@2744: this.river = river; sascha@2744: this.from = from; sascha@2744: this.to = to; sascha@2744: this.step = step; sascha@2744: this.function = function; sascha@2744: this.events = events; sascha@3121: this.referencePeriod = referencePeriod; sascha@2744: this.analysisPeriods = analysisPeriods; sascha@2744: this.qSectorStart = qSectorStart; sascha@2744: this.qSectorEnd = qSectorEnd; sascha@3011: this.preprocessing = preprocessing; sascha@2729: } sascha@2729: } sascha@2729: sascha@2729: public CalculationResult calculate() { sascha@2729: sascha@2992: boolean debug = log.isDebugEnabled(); sascha@2992: sascha@2729: FixingsOverview overview = sascha@2729: FixingsOverviewFactory.getOverview(river); sascha@2729: sascha@2729: if (overview == null) { sascha@2729: addProblem("fix.no.overview.available"); sascha@2729: } sascha@2729: sascha@2729: Function func = FunctionFactory.getInstance() sascha@2729: .getFunction(function); sascha@2729: sascha@2729: if (func == null) { sascha@2729: // TODO: i18n sascha@2729: addProblem("fix.invalid.function.name"); sascha@2729: } sascha@2729: sascha@2729: if (hasProblems()) { sascha@2729: return new CalculationResult(this); sascha@2729: } sascha@2729: sascha@3011: final List eventColumns = getEventColumns(overview); sascha@2729: sascha@3011: if (eventColumns.size() < 2) { sascha@2729: // TODO: i18n sascha@2729: addProblem("fix.too.less.data.columns"); sascha@2729: return new CalculationResult(this); sascha@2729: } sascha@2729: sascha@2992: double [] kms = DoubleUtil.explode(from, to, step / 1000.0); sascha@2729: sascha@3106: final double [] qs = new double[eventColumns.size()]; sascha@3106: final double [] ws = new double[qs.length]; sascha@3106: final boolean [] interpolated = new boolean[ws.length]; sascha@3011: sascha@3096: Fitting.QWDFactory qwdFactory = new Fitting.QWDFactory() { sascha@3022: @Override sascha@3096: public QWD create(double q, double w) { sascha@3022: // Check all the event columns for close match sascha@3022: // and take the description and the date from meta. sascha@3022: for (int i = 0; i < qs.length; ++i) { sascha@3022: if (Math.abs(qs[i]-q) < EPSILON sascha@3022: && Math.abs(ws[i]-w) < EPSILON) { sascha@3022: Column column = eventColumns.get(i); sascha@3096: return new QWD( sascha@3137: qs[i], ws[i], sascha@3022: column.getDescription(), sascha@3096: column.getDate(), sascha@3106: interpolated[i], sascha@3096: 0d); sascha@3011: } sascha@3011: } sascha@3022: log.warn("cannot find column for (" + q + ", " + w + ")"); sascha@3096: return new QWD(q, w); sascha@3022: } sascha@3022: }; sascha@3011: sascha@3096: Fitting fitting = new Fitting(func, qwdFactory, preprocessing); sascha@2729: sascha@2729: String [] parameterNames = func.getParameterNames(); sascha@2729: sascha@2729: Parameters results = sascha@2729: new Parameters(createColumnNames(parameterNames)); sascha@2729: sascha@2729: boolean invalid = false; sascha@2729: sascha@2992: if (debug) { sascha@2992: log.debug("number of kms: " + kms.length); sascha@2992: } sascha@2992: sascha@3096: KMIndex outliers = new KMIndex(); sascha@3096: KMIndex referenced = new KMIndex(kms.length); sascha@3011: sascha@3011: int kmIndex = results.columnIndex("km"); sascha@3011: int chiSqrIndex = results.columnIndex("chi_sqr"); sascha@3065: int maxQIndex = results.columnIndex("max_q"); sascha@3107: int stdDevIndex = results.columnIndex("std-dev"); sascha@3011: int [] parameterIndices = results.columnIndices(parameterNames); sascha@3010: sascha@2992: int numFailed = 0; sascha@2992: sascha@2729: for (int i = 0; i < kms.length; ++i) { sascha@2729: double km = kms[i]; sascha@2729: sascha@3011: // Fill Qs and Ws from event columns. sascha@2729: for (int j = 0; j < ws.length; ++j) { sascha@3106: interpolated[j] = eventColumns.get(j).getQW(km, qs, ws, j); sascha@2729: } sascha@2729: sascha@3011: fitting.reset(); sascha@3010: sascha@3011: if (!fitting.fit(qs, ws)) { sascha@2992: ++numFailed; sascha@3011: // TODO: i18n sascha@3011: addProblem(km, "fix.fitting.failed"); sascha@2729: continue; sascha@2729: } sascha@2729: sascha@3022: referenced.add(km, fitting.referencedToArray()); sascha@3022: sascha@3011: if (fitting.hasOutliers()) { sascha@3014: outliers.add(km, fitting.outliersToArray()); sascha@3011: } sascha@3011: sascha@2729: int row = results.newRow(); sascha@2729: sascha@3010: results.set(row, kmIndex, km); sascha@3011: results.set(row, chiSqrIndex, fitting.getChiSquare()); sascha@3107: results.set(row, stdDevIndex, fitting.getStandardDeviation()); sascha@3107: results.set(row, maxQIndex, fitting.getMaxQ()); sascha@3011: invalid |= results.set( sascha@3011: row, parameterIndices, fitting.getParameters()); sascha@2729: } sascha@2729: sascha@2992: if (debug) { sascha@2992: log.debug("success: " + (kms.length - numFailed)); sascha@2992: log.debug("failed: " + numFailed); sascha@2992: } sascha@2992: sascha@2729: if (invalid) { sascha@2729: // TODO: i18n sascha@2729: addProblem("fix.invalid.values"); sascha@2729: results.removeNaNs(); sascha@2729: } sascha@2729: sascha@3020: KMIndex analysisPeriods = sascha@3020: calculateAnalysisPeriods(func, results, overview); sascha@2744: sascha@3014: outliers.sort(); sascha@3022: referenced.sort(); sascha@3020: analysisPeriods.sort(); sascha@3014: sascha@3022: FixResult fr = new FixResult( sascha@3076: results, sascha@3022: referenced, outliers, sascha@3022: analysisPeriods); sascha@2786: sascha@2786: return new CalculationResult(fr, this); sascha@2744: } sascha@2744: sascha@3011: sascha@3011: protected List getEventColumns(FixingsOverview overview) { sascha@3011: sascha@3011: FixingsColumnFactory fcf = FixingsColumnFactory.getInstance(); sascha@3011: sascha@3121: IdsFilter ids = new IdsFilter(events); sascha@3121: DateRangeFilter rdf = new DateRangeFilter( sascha@3121: referencePeriod.getFrom(), sascha@3121: referencePeriod.getTo()); sascha@3121: Filter filter = new AndFilter().add(rdf).add(ids); sascha@3011: sascha@3121: List metas = overview.filter(null, filter); sascha@3011: sascha@3121: List columns = new ArrayList(metas.size()); sascha@3121: sascha@3121: for (Fixing.Column meta: metas) { sascha@3121: sascha@3121: FixingsColumn data = fcf.getColumnData(meta); sascha@3011: if (data == null) { sascha@3011: // TODO: i18n sascha@3121: addProblem("fix.cannot.load.data"); sascha@3011: } sascha@3121: else { sascha@3137: columns.add(new Column(meta, data)); sascha@3121: } sascha@3011: } sascha@3011: sascha@3011: return columns; sascha@3011: } sascha@3152: sascha@3011: sascha@3020: protected KMIndex calculateAnalysisPeriods( sascha@3008: Function function, sascha@3008: Parameters parameters, sascha@3008: FixingsOverview overview sascha@3008: ) { sascha@3008: Range range = new Range(from, to); sascha@3008: sascha@3008: int kmIndex = parameters.columnIndex("km"); sascha@3065: int maxQIndex = parameters.columnIndex("max_q"); sascha@3008: sascha@3008: ColumnCache cc = new ColumnCache(); sascha@3008: sascha@3008: double [] wq = new double[2]; sascha@3008: sascha@3076: int [] parameterIndices = sascha@3008: parameters.columnIndices(function.getParameterNames()); sascha@3008: sascha@3008: double [] parameterValues = new double[parameterIndices.length]; sascha@3008: sascha@3008: DateAverager dateAverager = new DateAverager(); sascha@3008: sascha@3020: KMIndex results = sascha@3020: new KMIndex(parameters.size()); sascha@3020: sascha@3121: IdsFilter idsFilter = new IdsFilter(events); sascha@3121: sascha@3020: for (int row = 0, R = parameters.size(); row < R; ++row) { sascha@3008: double km = parameters.get(row, kmIndex); sascha@3008: parameters.get(row, parameterIndices, parameterValues); sascha@3008: sascha@3008: // This is the paraterized function for a given km. sascha@3008: de.intevation.flys.artifacts.math.Function instance = sascha@3008: function.instantiate(parameterValues); sascha@3008: sascha@3008: KmFilter kmFilter = new KmFilter(km); sascha@3008: sascha@3020: ArrayList periodResults = sascha@3020: new ArrayList(analysisPeriods.length); sascha@3020: sascha@3008: for (DateRange analysisPeriod: analysisPeriods) { sascha@3008: DateRangeFilter drf = new DateRangeFilter( sascha@3008: analysisPeriod.getFrom(), sascha@3008: analysisPeriod.getTo()); sascha@3008: sascha@3145: QWD [] qSectorAverages = new QWD[4]; sascha@3145: double [] qSectorStdDevs = new double[4]; sascha@3145: sascha@3020: ArrayList allQWDs = new ArrayList(); sascha@3020: sascha@3008: // for all Q sectors. sascha@3008: for (int qSector = qSectorStart; qSector < qSectorEnd; ++qSector) { sascha@3008: sascha@3121: Filter filter = new AndFilter() sascha@3121: .add(kmFilter) sascha@3121: .add(new SectorFilter(qSector)) sascha@3121: .add(drf) sascha@3121: .add(idsFilter); sascha@3008: sascha@3008: List metas = overview.filter(range, filter); sascha@3008: sascha@3008: if (metas.isEmpty()) { sascha@3008: // No fixings for km and analysis period sascha@3008: continue; sascha@3008: } sascha@3008: sascha@3008: double sumQ = 0.0; sascha@3008: double sumW = 0.0; sascha@3008: sascha@3145: StandardDeviation stdDev = new StandardDeviation(); sascha@3145: sascha@3008: List qwds = new ArrayList(metas.size()); sascha@3008: sascha@3008: dateAverager.clear(); sascha@3008: sascha@3008: for (Fixing.Column meta: metas) { sascha@3008: if (meta.findQSector(km) != qSector) { sascha@3008: // Ignore not matching sectors. sascha@3008: continue; sascha@3008: } sascha@3008: sascha@3008: Column column = cc.getColumn(meta); sascha@3011: if (column == null || !column.getQW(km, wq)) { sascha@3008: continue; sascha@3008: } sascha@3008: sascha@3008: double fw = instance.value(wq[1]); sascha@3008: if (Double.isNaN(fw)) { sascha@3008: continue; sascha@3008: } sascha@3008: sascha@3008: double dw = (wq[0] - fw)*100.0; sascha@3008: sascha@3145: stdDev.increment(dw); sascha@3145: sascha@3008: Date date = column.getDate(); sascha@3008: String description = column.getDescription(); sascha@3008: sascha@3106: QWD qwd = new QWD( sascha@3106: wq[1], wq[0], description, date, true, dw); sascha@3008: sascha@3008: qwds.add(qwd); sascha@3008: sascha@3008: sumW += wq[0]; sascha@3008: sumQ += wq[1]; sascha@3008: sascha@3008: dateAverager.add(date); sascha@3008: } sascha@3008: sascha@3008: // Calulate average per Q sector. sascha@3008: int N = qwds.size(); sascha@3008: if (N > 0) { sascha@3020: allQWDs.addAll(qwds); sascha@3008: double avgW = sumW / N; sascha@3008: double avgQ = sumQ / N; sascha@3008: sascha@3008: double avgFw = instance.value(avgQ); sascha@3008: if (!Double.isNaN(avgFw)) { sascha@3008: double avgDw = (avgW - avgFw)*100.0; sascha@3008: Date avgDate = dateAverager.getAverage(); sascha@3008: sascha@3008: String avgDescription = "avg.deltawt." + qSector; sascha@3008: sascha@3008: QWD avgQWD = new QWD( sascha@3106: avgQ, avgW, avgDescription, avgDate, true, avgDw); sascha@3008: sascha@3020: qSectorAverages[qSector] = avgQWD; sascha@3008: } sascha@3145: qSectorStdDevs[qSector] = stdDev.getResult(); sascha@3145: } sascha@3145: else { sascha@3145: qSectorStdDevs[qSector] = Double.NaN; sascha@3008: } sascha@3008: } // for all Q sectors sascha@3020: sascha@3020: QWD [] aqwds = allQWDs.toArray(new QWD[allQWDs.size()]); sascha@3020: sascha@3020: AnalysisPeriod periodResult = new AnalysisPeriod( sascha@3145: analysisPeriod, sascha@3145: aqwds, sascha@3145: qSectorAverages, sascha@3145: qSectorStdDevs); sascha@3020: periodResults.add(periodResult); sascha@3008: } sascha@3020: sascha@3065: double maxQ = -Double.MAX_VALUE; sascha@3065: for (AnalysisPeriod ap: periodResults) { sascha@3065: double q = ap.getMaxQ(); sascha@3065: if (q > maxQ) { sascha@3065: maxQ = q; sascha@3065: } sascha@3065: } sascha@3065: sascha@3065: double oldMaxQ = parameters.get(row, maxQIndex); sascha@3065: if (oldMaxQ < maxQ) { sascha@3065: parameters.set(row, maxQIndex, maxQ); sascha@3065: } sascha@3065: sascha@3020: results.add(km, periodResults.toArray( sascha@3020: new AnalysisPeriod[periodResults.size()])); sascha@3008: } sascha@3008: sascha@3008: return results; sascha@3008: } sascha@3008: sascha@3076: /** Helper class to bundle the meta information of a column sascha@2744: * and the real data. sascha@2744: */ sascha@2744: protected static class Column { sascha@2744: sascha@2744: protected Fixing.Column meta; sascha@2744: protected FixingsColumn data; sascha@2744: sascha@2744: public Column() { sascha@2744: } sascha@2744: sascha@2744: public Column(Fixing.Column meta, FixingsColumn data) { sascha@2744: this.meta = meta; sascha@2744: this.data = data; sascha@2744: } sascha@3008: sascha@3008: public Date getDate() { sascha@3008: return meta.getStartTime(); sascha@3008: } sascha@3008: sascha@3008: public String getDescription() { sascha@3008: return meta.getDescription(); sascha@3008: } sascha@3008: sascha@3011: public boolean getQW( sascha@3076: double km, sascha@3076: double [] qs, sascha@3011: double [] ws, sascha@3011: int index sascha@3011: ) { sascha@3011: qs[index] = data.getQ(km); sascha@3011: return data.getW(km, ws, index); sascha@3011: } sascha@3011: sascha@3011: public boolean getQW(double km, double [] wq) { sascha@3008: data.getW(km, wq, 0); sascha@3008: if (Double.isNaN(wq[0])) return false; sascha@3008: wq[1] = data.getQ(km); sascha@3008: return !Double.isNaN(wq[1]); sascha@3008: } sascha@2744: } // class Column sascha@2744: sascha@3008: sascha@3008: /** sascha@3008: * Helper class to find the data belonging to meta info more quickly. sascha@3008: */ sascha@3008: public static class ColumnCache { sascha@3008: sascha@3008: protected Map columns; sascha@3008: sascha@3008: public ColumnCache() { sascha@3008: columns = new HashMap(); sascha@3008: } sascha@3008: sascha@3008: public Column getColumn(Fixing.Column meta) { sascha@3008: Integer key = meta.getId(); sascha@3008: Column column = columns.get(key); sascha@3008: if (column == null) { sascha@3008: FixingsColumn data = FixingsColumnFactory sascha@3008: .getInstance() sascha@3008: .getColumnData(meta); sascha@3008: if (data != null) { sascha@3008: column = new Column(meta, data); sascha@3008: columns.put(key, column); sascha@3008: } sascha@3008: } sascha@3008: return column; sascha@3008: } sascha@3008: } // class ColumnCache sascha@3008: sascha@2744: /** Fetch meta and data columns for analysis periods. */ sascha@2744: protected Column [][] getAnalysisColumns(FixingsOverview overview) { sascha@2744: sascha@2993: boolean debug = log.isDebugEnabled(); sascha@2993: if (debug) { sascha@2993: log.debug("number analysis periods: " + analysisPeriods.length); sascha@2993: } sascha@2993: sascha@2744: Column columns [][] = new Column[analysisPeriods.length][]; sascha@2744: sascha@2744: Range range = new Range(from, to); sascha@2744: SectorRangeFilter sectorRangeFilter = sascha@2744: new SectorRangeFilter(qSectorStart, qSectorEnd); sascha@2744: sascha@2744: FixingsColumnFactory fcf = FixingsColumnFactory.getInstance(); sascha@2744: sascha@2744: for (int i = 0; i < columns.length; ++i) { sascha@2744: sascha@2744: // Construct filter for period. sascha@3003: DateRange period = analysisPeriods[i]; sascha@2744: sascha@2744: AndFilter filter = new AndFilter(); sascha@2744: sascha@2744: DateRangeFilter dateRangeFilter = sascha@3003: new DateRangeFilter(period.getFrom(), period.getTo()); sascha@2744: sascha@2744: filter.add(dateRangeFilter); sascha@2744: filter.add(sectorRangeFilter); sascha@2744: sascha@2744: List metaCols = overview.filter(range, filter); sascha@2744: sascha@2993: if (debug) { sascha@2993: log.debug("number of filtered columns: " + metaCols.size()); sascha@2993: } sascha@2993: sascha@2744: ArrayList cols = new ArrayList(metaCols.size()); sascha@2744: sascha@2744: // Only use columns which have data. sascha@2744: for (Fixing.Column meta: metaCols) { sascha@2744: FixingsColumn data = fcf.getColumnData(meta); sascha@2744: if (data != null) { sascha@2744: cols.add(new Column(meta, data)); sascha@2744: } sascha@2744: } sascha@2993: sascha@2993: if (debug) { sascha@2993: log.debug("failed loading: " + (metaCols.size()-cols.size())); sascha@2993: } sascha@2744: columns[i] = cols.toArray(new Column[cols.size()]); sascha@2744: } sascha@2744: sascha@2744: return columns; sascha@2729: } sascha@2729: sascha@2729: protected static String [] createColumnNames(String [] parameters) { sascha@3107: String [] result = new String[parameters.length + 4]; sascha@2729: result[0] = "km"; sascha@3029: result[1] = "chi_sqr"; sascha@3065: result[2] = "max_q"; sascha@3107: result[3] = "std-dev"; sascha@3107: System.arraycopy(parameters, 0, result, 4, parameters.length); sascha@2729: return result; sascha@2729: } sascha@2729: } sascha@2729: // vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :