Mercurial > dive4elements > gnv-client
changeset 521:1bf058f1a2d1
Generate seabed polygon to "Profilschnitte".
gnv-artifacts/trunk@615 c6561f87-3c4e-4783-a992-168aeb5c3f6f
line wrap: on
line diff
--- a/gnv-artifacts/ChangeLog Sun Jan 24 12:10:50 2010 +0000 +++ b/gnv-artifacts/ChangeLog Sun Jan 24 20:24:03 2010 +0000 @@ -1,5 +1,32 @@ 2010-01-24 Sascha L. Teichmann <sascha.teichmann@intevation.de> + * doc/conf/conf.xml: Added attribute "fill-color" to + /artifact-database/gnv/vertical-cross-section/ground/ configure + the color of the seabed polygon. + + * src/main/java/de/intevation/gnv/math/Interpolation3D.java: + Adjusted column depth to the deepest interpolated position to + prevent gaps. + + * src/main/java/de/intevation/gnv/chart/VerticalCrossSectionChart.java: + Add ground polygon to the chart. + + * src/main/java/de/intevation/gnv/state/profile/verticalcrosssection/OutputHelper.java: + New. Contains code to create the ground polygon. + + * src/main/java/de/intevation/gnv/raster/PolygonDatasetProducer.java, + src/main/java/de/intevation/gnv/raster/IsoPolygonSeriesProducer.java: + Handle temporary vertices more efficiently. + + * src/main/java/de/intevation/gnv/artifacts/context/GNVArtifactContext.java, + src/main/java/de/intevation/gnv/artifacts/context/GNVArtifactContextFactory.java: + Added configuration to set the color of the seabed polygon. + + * src/main/java/de/intevation/gnv/chart/VerticalCrossSectionChart.java: + Handle the seabad polygon color. + +2010-01-24 Sascha L. Teichmann <sascha.teichmann@intevation.de>fill-color + * contrib/palette2qgis.xsl: Cosmetic cleanups. 2010-01-23 Sascha L. Teichmann <sascha.teichmann@intevation.de>
--- a/gnv-artifacts/doc/conf/conf.xml Sun Jan 24 12:10:50 2010 +0000 +++ b/gnv-artifacts/doc/conf/conf.xml Sun Jan 24 20:24:03 2010 +0000 @@ -428,7 +428,7 @@ <vertical-cross-section> <!-- This section configures the "Profilschnitt" --> <samples width="1024" height="768"/> - <ground interpolation="bilinear" /> + <ground interpolation="bilinear" fill-color="#6d7067"/> <!-- <filters> <filter factory="de.intevation.gnv.raster.KernelFilter$GaussFactory"
--- a/gnv-artifacts/src/main/java/de/intevation/gnv/artifacts/context/GNVArtifactContext.java Sun Jan 24 12:10:50 2010 +0000 +++ b/gnv-artifacts/src/main/java/de/intevation/gnv/artifacts/context/GNVArtifactContext.java Sun Jan 24 20:24:03 2010 +0000 @@ -7,7 +7,9 @@ import org.w3c.dom.Document; +import java.awt.Color; import java.awt.Dimension; +import java.awt.Paint; import java.io.File; @@ -72,6 +74,12 @@ public static final String DEFAULT_VERTICAL_CROSS_SECTION_GROUND_INTERPOLATION = "bilinear"; + public static final String VERTICAL_CROSS_SECTION_GROUND_FILL_KEY = + "gnv.vertical.cross.section.ground.fill"; + + public static final Paint DEFAULT_VERTICAL_CROSS_SECTION_GROUND_FILL = + new Color(0x7c8683); + public GNVArtifactContext() { super(); log.debug("GNVArtifactContext.Constructor");
--- a/gnv-artifacts/src/main/java/de/intevation/gnv/artifacts/context/GNVArtifactContextFactory.java Sun Jan 24 12:10:50 2010 +0000 +++ b/gnv-artifacts/src/main/java/de/intevation/gnv/artifacts/context/GNVArtifactContextFactory.java Sun Jan 24 20:24:03 2010 +0000 @@ -3,6 +3,9 @@ */ package de.intevation.gnv.artifacts.context; +import java.awt.Color; +import java.awt.Paint; + import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; @@ -90,6 +93,9 @@ public final static String VERTICAL_CROSS_SECTION_GROUND_INTERPOLATION = "/artifact-database/gnv/vertical-cross-section/ground/@interpolation"; + public final static String VERTICAL_CROSS_SECTION_GROUND_FILL_COLOR = + "/artifact-database/gnv/vertical-cross-section/ground/@fill-color"; + /** * Constructor */ @@ -164,6 +170,39 @@ configureVerticalCrossSectionSamples(config, context); configureVerticalCrossSectionFilters(config, context); configureVerticalCrossSectionGroundInterpolation(config, context); + configureVerticalCrossSectionGroundFillColor(config, context); + } + + protected void configureVerticalCrossSectionGroundFillColor( + Document config, + GNVArtifactContext context + ) { + log.info("configuration of vertical cross section ground fill color"); + + String fillColor = Config.getStringXPath( + config, + VERTICAL_CROSS_SECTION_GROUND_FILL_COLOR); + + Paint fill = + GNVArtifactContext.DEFAULT_VERTICAL_CROSS_SECTION_GROUND_FILL; + + if (fillColor != null + && (fillColor = fillColor.trim()).length() != 0) { + try { + Color color = Color.decode(fillColor); + log.info("ground fill color: #" + + Integer.toHexString(color.getRGB())); + fill = color; + } + catch (NumberFormatException nfe) { + log.error("'" + fillColor + "' is not a valid color"); + } + } + + context.put( + GNVArtifactContext + .VERTICAL_CROSS_SECTION_GROUND_FILL_KEY, + fill); } protected void configureVerticalCrossSectionGroundInterpolation(
--- a/gnv-artifacts/src/main/java/de/intevation/gnv/chart/VerticalCrossSectionChart.java Sun Jan 24 12:10:50 2010 +0000 +++ b/gnv-artifacts/src/main/java/de/intevation/gnv/chart/VerticalCrossSectionChart.java Sun Jan 24 20:24:03 2010 +0000 @@ -1,36 +1,36 @@ package de.intevation.gnv.chart; -import java.util.Locale; +import de.intevation.gnv.jfreechart.PolygonDataset; +import de.intevation.gnv.jfreechart.PolygonPlot; +import de.intevation.gnv.jfreechart.PolygonRenderer; +import de.intevation.gnv.jfreechart.PolygonSeries; -import java.text.NumberFormat; +import de.intevation.gnv.math.AttributedXYColumns; + +import de.intevation.gnv.raster.Palette; import java.awt.Color; import java.awt.Paint; -import java.util.HashSet; - -import de.intevation.gnv.math.AttributedXYColumns; +import java.text.NumberFormat; -import de.intevation.gnv.jfreechart.PolygonDataset; -import de.intevation.gnv.jfreechart.PolygonPlot; -import de.intevation.gnv.jfreechart.PolygonSeries; -import de.intevation.gnv.jfreechart.PolygonRenderer; - -import de.intevation.gnv.raster.Palette; +import java.util.HashSet; +import java.util.Locale; +import java.util.Map; import org.jfree.chart.JFreeChart; +import org.jfree.chart.axis.NumberAxis; +import org.jfree.chart.axis.SymbolAxis; +import org.jfree.chart.axis.ValueAxis; + import org.jfree.chart.plot.PlotOrientation; -import org.jfree.chart.axis.ValueAxis; -import org.jfree.chart.axis.NumberAxis; -import org.jfree.chart.axis.SymbolAxis; +import org.jfree.chart.renderer.LookupPaintScale; import org.jfree.chart.title.PaintScaleLegend; import org.jfree.chart.title.TextTitle; -import org.jfree.chart.renderer.LookupPaintScale; - import org.jfree.ui.RectangleEdge; import org.jfree.ui.RectangleInsets; @@ -44,13 +44,28 @@ public static final class PalettePaintLookup implements PolygonRenderer.PaintLookup { - private Palette palette; + private Palette palette; + private Map<Integer, Paint> special; public PalettePaintLookup(Palette palette) { + this(palette, null); + } + + public PalettePaintLookup( + Palette palette, + Map<Integer, Paint> special + ) { this.palette = palette; + this.special = special; } public Paint getPaint(int index) { + if (special != null) { + Paint paint = special.get(index); + if (paint != null) { + return paint; + } + } return index < 0 ? Color.black : palette.getColor(index); @@ -79,6 +94,7 @@ protected JFreeChart chart; protected AttributedXYColumns columns; + protected Map<Integer, Paint> special; protected Palette palette; protected Locale locale; protected ChartLabels labels; @@ -92,8 +108,19 @@ Locale locale, ChartLabels labels ) { + this(columns, palette, null, locale, labels); + } + + public VerticalCrossSectionChart( + AttributedXYColumns columns, + Palette palette, + Map<Integer, Paint> special, + Locale locale, + ChartLabels labels + ) { this.columns = columns; this.palette = palette; + this.special = special; this.locale = locale; this.labels = labels; } @@ -112,7 +139,8 @@ for (int i = data.getSeriesCount()-1; i >= 0; --i) { PolygonSeries ps = data.getSeries(i); Integer fill = (Integer)ps.getAttribute("fill"); - if (fill != null) { + if (fill != null + && (special != null && !special.containsKey(fill))) { usedColors.add(fill); } } @@ -122,7 +150,7 @@ format.setMaximumFractionDigits(2); PolygonRenderer renderer = new PolygonRenderer( - new PalettePaintLookup(palette), + new PalettePaintLookup(palette, special), new LocalizedLabelGenerator(format)); ValueAxis domainAxis = new NumberAxis(this.labels.getDomainAxisLabel());
--- a/gnv-artifacts/src/main/java/de/intevation/gnv/math/Interpolation3D.java Sun Jan 24 12:10:50 2010 +0000 +++ b/gnv-artifacts/src/main/java/de/intevation/gnv/math/Interpolation3D.java Sun Jan 24 20:24:03 2010 +0000 @@ -33,6 +33,9 @@ protected int width; protected int height; + protected double cellWidth; + protected double cellHeight; + protected double [] raster; protected double [] depths; @@ -57,6 +60,14 @@ return width; } + public double getCellWidth() { + return cellWidth; + } + + public double getCellHeight() { + return cellHeight; + } + public double [] getRaster() { return raster; } @@ -160,7 +171,7 @@ i = 0; for (double p = cellWidth*0.5; i < depths.length; ++i, p += cellWidth) { double depth = depths[i]; - if (Double.isNaN(depth)) { + if (Double.isNaN(depth) || depth >= 0d) { continue; } linearToMap.locate(p, center); @@ -196,8 +207,8 @@ n2.x, n2.y, n3.x, n3.y, center.x); - int j = i; - for (double z = -cellHeight*0.5; + double z = -cellHeight*0.5; + for (int j = i; j < raster.length && z >= depth; z -= cellHeight, j += width) { @@ -220,11 +231,16 @@ center.y); raster[j] = value; } + // XXX: Adjusted depth to create no gap + // between last value and ground. + depths[i] = z+0.5d*cellHeight; } // down the x/y column } // all along the track - this.depths = depths; - this.raster = raster; + this.depths = depths; + this.raster = raster; + this.cellWidth = cellWidth; + this.cellHeight = cellHeight; return true; }
--- a/gnv-artifacts/src/main/java/de/intevation/gnv/raster/IsoPolygonSeriesProducer.java Sun Jan 24 12:10:50 2010 +0000 +++ b/gnv-artifacts/src/main/java/de/intevation/gnv/raster/IsoPolygonSeriesProducer.java Sun Jan 24 20:24:03 2010 +0000 @@ -71,7 +71,7 @@ vertices.add(m1*(head.a % width) + b1); vertices.add(m2*(head.a / width) + b2); ps.addRing(new CompactXYItems(vertices.toNativeArray())); - vertices.clear(); + vertices.reset(); } } @@ -93,7 +93,7 @@ vertices.add(m1*(last.b % width) + b1); vertices.add(m2*(last.b / width) + b2); ps.addRing(new CompactXYItems(vertices.toNativeArray())); - vertices.clear(); + vertices.reset(); } // for all in common open } // if map defined for key
--- a/gnv-artifacts/src/main/java/de/intevation/gnv/raster/PolygonDatasetProducer.java Sun Jan 24 12:10:50 2010 +0000 +++ b/gnv-artifacts/src/main/java/de/intevation/gnv/raster/PolygonDatasetProducer.java Sun Jan 24 20:24:03 2010 +0000 @@ -75,7 +75,7 @@ } while ((current = current.next) != head); ps.addRing(new CompactXYItems(vertices.toNativeArray())); - vertices.clear(); + vertices.reset(); } }
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gnv-artifacts/src/main/java/de/intevation/gnv/state/profile/verticalcrosssection/OutputHelper.java Sun Jan 24 20:24:03 2010 +0000 @@ -0,0 +1,87 @@ +package de.intevation.gnv.state.profile.verticalcrosssection; + +import de.intevation.gnv.jfreechart.CompactXYItems; +import de.intevation.gnv.jfreechart.PolygonSeries; + +import de.intevation.gnv.math.Interpolation3D; + +import gnu.trove.TDoubleArrayList; + +import org.apache.log4j.Logger; + +/** + * @author Sascha L. Teichmann (sascha.teichmann@intevation.de) + */ +public class OutputHelper +{ + private static Logger log = Logger.getLogger(OutputHelper.class); + + public static final double EPS = 1e-5d; + + private OutputHelper() { + } + + public static PolygonSeries createSeabedPolygon( + Interpolation3D interpolation, + Integer fill + ) { + double maxDepth = interpolation.getMaxDepth(); + double cellWidth = interpolation.getCellWidth(); + double cellHeight = interpolation.getCellHeight(); + + double [] depths = interpolation.getDepths(); + + double x = 0d; + + TDoubleArrayList vertices = new TDoubleArrayList(); + + PolygonSeries ps = new PolygonSeries(); + + for (int i = 0; i < depths.length; ++i, x += cellWidth) { + double depth = depths[i]; + + if (vertices.isEmpty()) { + if (Double.isNaN(depth) || depth == maxDepth) { + continue; + } + if (depth > 0d) depth = 0d; + vertices.add(x); vertices.add(maxDepth); + vertices.add(x); vertices.add(depth); + vertices.add(x+cellWidth); vertices.add(depth); + } + else { // in polygon + if (Double.isNaN(depth) || depth == maxDepth) { + vertices.add(x); vertices.add(maxDepth); + ps.addRing(new CompactXYItems(vertices.toNativeArray())); + vertices.reset(); + } + else { + if (depth > 0d) depth = 0d; + int N = vertices.size(); + if (N > 2 && Math.abs(depth - vertices.get(N-1)) < EPS) { + vertices.set(N-2, x+cellWidth); + } + else { + vertices.add(vertices.get(N-2)); vertices.add(depth); + vertices.add(x+cellWidth); vertices.add(depth); + } + } + } + } // for all depths + + if (!vertices.isEmpty()) { + vertices.add(vertices.get(vertices.size()-2)); + vertices.add(maxDepth); + ps.addRing(new CompactXYItems(vertices.toNativeArray())); + } + + if (ps.getItemCount() == 0) { + return null; + } + + ps.setAttribute("fill", fill); + + return ps; + } +} +// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :
--- a/gnv-artifacts/src/main/java/de/intevation/gnv/state/profile/verticalcrosssection/VerticalCrossSectionOutputState.java Sun Jan 24 12:10:50 2010 +0000 +++ b/gnv-artifacts/src/main/java/de/intevation/gnv/state/profile/verticalcrosssection/VerticalCrossSectionOutputState.java Sun Jan 24 20:24:03 2010 +0000 @@ -1,90 +1,85 @@ -/** - * - */ package de.intevation.gnv.state.profile.verticalcrosssection; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.UnsupportedEncodingException; - -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.Locale; -import java.util.Map; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import java.awt.Dimension; - -import org.apache.log4j.Logger; - -import org.jfree.chart.ChartTheme; - -import net.sf.ehcache.Element; - -import au.com.bytecode.opencsv.CSVWriter; - -import com.vividsolutions.jts.geom.Point; import com.vividsolutions.jts.geom.Coordinate; import de.intevation.artifacts.CallContext; +import de.intevation.gnv.artifacts.cache.CacheFactory; + import de.intevation.gnv.artifacts.context.GNVArtifactContext; -import de.intevation.gnv.artifacts.cache.CacheFactory; import de.intevation.gnv.artifacts.ressource.RessourceFactory; import de.intevation.gnv.chart.Chart; import de.intevation.gnv.chart.ChartLabels; -import de.intevation.gnv.chart.ChartStyle; import de.intevation.gnv.chart.VerticalCrossSectionChart; -import de.intevation.gnv.chart.exception.TechnicalChartException; - import de.intevation.gnv.geobackend.base.Result; import de.intevation.gnv.geobackend.base.ResultDescriptor; import de.intevation.gnv.geobackend.base.query.QueryExecutor; import de.intevation.gnv.geobackend.base.query.QueryExecutorFactory; + import de.intevation.gnv.geobackend.base.query.exception.QueryException; +import de.intevation.gnv.geobackend.sde.datasources.RasterObject; + +import de.intevation.gnv.jfreechart.PolygonDataset; +import de.intevation.gnv.jfreechart.PolygonSeries; + import de.intevation.gnv.math.AttributedXYColumns; import de.intevation.gnv.math.HeightValue; +import de.intevation.gnv.math.IJKey; +import de.intevation.gnv.math.Interpolation3D; +import de.intevation.gnv.math.LinearMetrics; import de.intevation.gnv.math.QueriedXYDepth; import de.intevation.gnv.math.XYColumn; -import de.intevation.gnv.math.IJKey; -import de.intevation.gnv.math.LinearMetrics; -import de.intevation.gnv.math.Interpolation3D; + +import de.intevation.gnv.raster.Filter; +import de.intevation.gnv.raster.IsoAttributeGenerator; +import de.intevation.gnv.raster.IsoPolygonSeriesProducer; +import de.intevation.gnv.raster.Palette; +import de.intevation.gnv.raster.PaletteManager; +import de.intevation.gnv.raster.PolygonDatasetProducer; +import de.intevation.gnv.raster.Raster; +import de.intevation.gnv.raster.Vectorizer; import de.intevation.gnv.state.InputData; import de.intevation.gnv.state.describedata.KeyValueDescibeData; + import de.intevation.gnv.state.exception.StateException; + import de.intevation.gnv.state.timeseries.TimeSeriesOutputState; import de.intevation.gnv.statistics.Statistics; import de.intevation.gnv.statistics.VerticalCrossSectionStatistics; import de.intevation.gnv.utils.DistanceCalculator; +import de.intevation.gnv.utils.StringUtils; import de.intevation.gnv.utils.WKTUtils; -import de.intevation.gnv.utils.StringUtils; -import de.intevation.gnv.raster.Filter; -import de.intevation.gnv.raster.PaletteManager; -import de.intevation.gnv.raster.Palette; -import de.intevation.gnv.raster.Raster; -import de.intevation.gnv.raster.PolygonDatasetProducer; -import de.intevation.gnv.raster.IsoAttributeGenerator; -import de.intevation.gnv.raster.IsoPolygonSeriesProducer; -import de.intevation.gnv.raster.Vectorizer; +import java.awt.Dimension; +import java.awt.Paint; -import de.intevation.gnv.jfreechart.PolygonDataset; -import de.intevation.gnv.jfreechart.PolygonSeries; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; -import de.intevation.gnv.geobackend.sde.datasources.RasterObject; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +import net.sf.ehcache.Element; + +import org.apache.log4j.Logger; + +import org.jfree.chart.ChartTheme; /** * @author Tim Englich (tim.englich@intevation.de) @@ -95,6 +90,8 @@ public static final String CHART_TYPE = "verticalcrosssection"; + public static final Integer GROUND_FILL_INDEX = Integer.valueOf(-2); + public static final String[] ATTRIBUTE_LIST = { "SHAPE", "Z", @@ -286,6 +283,16 @@ : new HashMap<Integer, PaletteManager>(); } + private static Paint getGroundFill(CallContext callContext) { + GNVArtifactContext context = + (GNVArtifactContext)callContext.globalContext(); + Paint fill = (Paint)context.get( + GNVArtifactContext.VERTICAL_CROSS_SECTION_GROUND_FILL_KEY); + return fill != null + ? fill + : GNVArtifactContext.DEFAULT_VERTICAL_CROSS_SECTION_GROUND_FILL; + } + public static final double EPSILON = 1e-5d; protected Object process( @@ -377,6 +384,8 @@ PolygonDataset pds = pdsp.getPolygonDataset(); + // Count number of colors before generating seabed + // because its used to determine the number of iso lines. int numColors = pds.getSeriesCount(); if (debug) { @@ -384,6 +393,16 @@ log.debug("number of colors: " + numColors); } + // generate seabed polygon + + PolygonSeries seabed = OutputHelper.createSeabedPolygon( + interpolation, + GROUND_FILL_INDEX); + + if (seabed != null) { + pds.addSeries(seabed); + } + // generate iso lines int numIso; @@ -524,8 +543,7 @@ return null; } - AttributedXYColumns columns = - (AttributedXYColumns)result; + AttributedXYColumns columns = (AttributedXYColumns)result; Integer parameterId = (Integer)columns.getAttribute("GROUP1"); // XXX: hardcoded @@ -545,9 +563,13 @@ return null; } + HashMap<Integer, Paint> special = new HashMap<Integer, Paint>(); + special.put(GROUND_FILL_INDEX, getGroundFill(callContext)); + chart = new VerticalCrossSectionChart( columns, paletteManager.getBase(), + special, locale, chartLables); @@ -561,28 +583,6 @@ return chart; } - - /** - * @see de.intevation.gnv.state.timeseries.TimeSeriesOutputState#createChart(java.io.OutputStream, - * java.util.Collection, java.util.Collection, java.lang.String, - * de.intevation.gnv.chart.ChartStyle, - * de.intevation.gnv.chart.ChartLabels) - */ - /* - @Override - protected void createChart(OutputStream outputStream, - Collection<KeyValueDescibeData> parameters, - Collection<KeyValueDescibeData> measurements, - Collection<KeyValueDescibeData> dates, - ChartStyle chartStyle, ChartLabels chartLables, - String uuid) throws IOException, - TechnicalChartException { - new VerticalCrossSectionChartFactory().createProfileChart(chartLables, - chartStyle, parameters, measurements, dates, outputStream, this - .getChartResult(uuid)); - } - */ - /** * @see de.intevation.gnv.state.timeseries.TimeSeriesOutputState#getStatisticsGenerator() */ @@ -601,43 +601,7 @@ ) throws UnsupportedEncodingException, IOException, StateException { - /* // TODO: Implement a substitution which makes sense. - if (chartResult != null) { - try { - CSVWriter writer = new CSVWriter(new OutputStreamWriter( - outputStream, "ISO-8859-1"), ','); - // USE THIS ENCODING BECAUSE OF - // PROBLEMS WITH EXCEL AND UTF-8 - Iterator<Result> it = chartResult.iterator(); - WKTReader wktReader = new WKTReader(); - while (it.hasNext()) { - Result result = it.next(); - int i = 0; - String[] entries = new String[9]; - Point p = (Point)wktReader.read(result.getString("SHAPE")); - entries[i++] = ""+p.getX(); - entries[i++] = ""+p.getY(); - entries[i++] = result.getString("Z"); - entries[i++] = result.getString("YORDINATE"); - entries[i++] = result.getString("GROUP1"); - entries[i++] = result.getString("GROUP2"); - entries[i++] = result.getString("IPOSITION"); - entries[i++] = result.getString("JPOSITION"); - entries[i++] = result.getString("KPOSITION"); - writer.writeNext(entries); - } - writer.close(); - } catch (ParseException e) { - log.error(e,e); - throw new StateException( - "Exception occured while parsing an Point from WKT."); - } - } else { - log.error("No Data given for generating an CSV-File."); - throw new StateException( - "No Data given for generating an CSV-File."); - } - */ + // TODO: Implement a substitution which makes sense. } } -// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8: +// vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 :