Mercurial > dive4elements > river
comparison artifacts/src/main/java/org/dive4elements/river/exports/WaterlevelExporter.java @ 9457:65f28328c9a3
ausgelagerte Wasserspiegellage AWSPL neue Spalte
author | gernotbelger |
---|---|
date | Tue, 28 Aug 2018 14:02:23 +0200 |
parents | 86d2cbfe7f7f |
children | f06e3766997f |
comparison
equal
deleted
inserted
replaced
9456:c96f6c8a6b03 | 9457:65f28328c9a3 |
---|---|
62 public class WaterlevelExporter extends AbstractExporter { | 62 public class WaterlevelExporter extends AbstractExporter { |
63 | 63 |
64 /** The log used in this exporter. */ | 64 /** The log used in this exporter. */ |
65 private static Logger log = Logger.getLogger(WaterlevelExporter.class); | 65 private static Logger log = Logger.getLogger(WaterlevelExporter.class); |
66 | 66 |
67 public static final String FACET_WST = "wst"; | 67 private static final String FACET_WST = "wst"; |
68 | 68 |
69 /* This should be the same as in the StaticWQKmsArtifact */ | 69 /* This should be the same as in the StaticWQKmsArtifact */ |
70 public static final String STATICWQKMSNAME = "staticwqkms"; | 70 private static final String STATICWQKMSNAME = "staticwqkms"; |
71 | 71 |
72 public static final String CSV_KM_HEADER = "export.waterlevel.csv.header.km"; | 72 public static final String CSV_KM_HEADER = "export.waterlevel.csv.header.km"; |
73 | 73 |
74 public static final String CSV_W_HEADER = "export.waterlevel.csv.header.w"; | 74 public static final String CSV_W_HEADER = "export.waterlevel.csv.header.w"; |
75 | 75 |
89 | 89 |
90 public static final String CSV_LOCATION_HEADER = "export.waterlevel.csv.header.location"; | 90 public static final String CSV_LOCATION_HEADER = "export.waterlevel.csv.header.location"; |
91 | 91 |
92 public static final String CSV_GAUGE_HEADER = "export.waterlevel.csv.header.gauge"; | 92 public static final String CSV_GAUGE_HEADER = "export.waterlevel.csv.header.gauge"; |
93 | 93 |
94 public static final String CSV_META_RESULT = "export.waterlevel.csv.meta.result"; | 94 private static final String CSV_META_RESULT = "export.waterlevel.csv.meta.result"; |
95 | 95 |
96 public static final String CSV_META_CREATION = "export.waterlevel.csv.meta.creation"; | 96 private static final String CSV_META_CREATION = "export.waterlevel.csv.meta.creation"; |
97 | 97 |
98 public static final String CSV_META_CALCULATIONBASE = "export.waterlevel.csv.meta.calculationbase"; | 98 private static final String CSV_META_CALCULATIONBASE = "export.waterlevel.csv.meta.calculationbase"; |
99 | 99 |
100 public static final String CSV_META_RIVER = "export.waterlevel.csv.meta.river"; | 100 private static final String CSV_META_RIVER = "export.waterlevel.csv.meta.river"; |
101 | 101 |
102 public static final String CSV_META_RANGE = "export.waterlevel.csv.meta.range"; | 102 private static final String CSV_META_RANGE = "export.waterlevel.csv.meta.range"; |
103 | 103 |
104 public static final String CSV_META_GAUGE = "export.waterlevel.csv.meta.gauge"; | 104 private static final String CSV_META_GAUGE = "export.waterlevel.csv.meta.gauge"; |
105 | 105 |
106 public static final String CSV_META_Q = "common.export.waterlevel.csv.meta.q"; | 106 private static final String CSV_META_Q = "common.export.waterlevel.csv.meta.q"; |
107 | 107 |
108 public static final String CSV_META_W = "export.waterlevel.csv.meta.w"; | 108 private static final String CSV_META_W = "export.waterlevel.csv.meta.w"; |
109 | 109 |
110 public static final String CSV_NOT_IN_GAUGE_RANGE = "export.waterlevel.csv.not.in.gauge.range"; | 110 public static final String CSV_NOT_IN_GAUGE_RANGE = "export.waterlevel.csv.not.in.gauge.range"; |
111 | 111 |
112 public static final Pattern NUMBERS_PATTERN = Pattern.compile("\\D*(\\d++.\\d*)\\D*"); | 112 private static final Pattern NUMBERS_PATTERN = Pattern.compile("\\D*(\\d++.\\d*)\\D*"); |
113 | 113 |
114 public static final String DEFAULT_CSV_KM_HEADER = "Fluss-Km"; | 114 public static final String DEFAULT_CSV_KM_HEADER = "Fluss-Km"; |
115 public static final String DEFAULT_CSV_W_HEADER = "W [NN + m]"; | 115 public static final String DEFAULT_CSV_W_HEADER = "W [NN + m]"; |
116 public static final String DEFAULT_CSV_Q_HEADER = "Q [m\u00b3/s]"; | 116 public static final String DEFAULT_CSV_Q_HEADER = "Q [m\u00b3/s]"; |
117 /** | 117 /** |
126 public static final String DEFAULT_CSV_W_DESC_HEADER = "W/Pegel [cm]"; | 126 public static final String DEFAULT_CSV_W_DESC_HEADER = "W/Pegel [cm]"; |
127 public static final String DEFAULT_CSV_LOCATION_HEADER = "Lage"; | 127 public static final String DEFAULT_CSV_LOCATION_HEADER = "Lage"; |
128 public static final String DEFAULT_CSV_GAUGE_HEADER = "Bezugspegel"; | 128 public static final String DEFAULT_CSV_GAUGE_HEADER = "Bezugspegel"; |
129 public static final String DEFAULT_CSV_NOT_IN_GAUGE_RANGE = "außerhalb des gewählten Bezugspegels"; | 129 public static final String DEFAULT_CSV_NOT_IN_GAUGE_RANGE = "außerhalb des gewählten Bezugspegels"; |
130 | 130 |
131 public static final String PDF_HEADER_MODE = "export.waterlevel.pdf.mode"; | 131 protected static final String PDF_HEADER_MODE = "export.waterlevel.pdf.mode"; |
132 public static final String JASPER_FILE = "export.waterlevel.pdf.file"; | 132 private static final String JASPER_FILE = "export.waterlevel.pdf.file"; |
133 | 133 |
134 /** The storage that contains all WQKms objects that are calculated. */ | 134 /** The storage that contains all WQKms objects that are calculated. */ |
135 protected List<WQKms[]> data; | 135 public List<WQKms[]> data; |
136 | 136 |
137 /** The storage that contains official fixings if available. */ | 137 /** The storage that contains official fixings if available. */ |
138 protected List<WQKms> officalFixings; | 138 public List<WQKms> officalFixings; |
139 | 139 |
140 public WaterlevelExporter() { | 140 public WaterlevelExporter() { |
141 this.data = new ArrayList<>(); | 141 this.data = new ArrayList<>(); |
142 } | 142 } |
143 | 143 |
214 * @param winfo | 214 * @param winfo |
215 * A WINFO Artifact. | 215 * A WINFO Artifact. |
216 * @param wqkms | 216 * @param wqkms |
217 * A WQKms object that should be prepared. | 217 * A WQKms object that should be prepared. |
218 */ | 218 */ |
219 protected String getColumnTitle(final WINFOArtifact winfo, final WQKms wqkms) { | 219 public String getColumnTitle(final WINFOArtifact winfo, final WQKms wqkms) { |
220 log.debug("WaterlevelExporter.getColumnTitle"); | 220 log.debug("WaterlevelExporter.getColumnTitle"); |
221 | 221 |
222 final String name = wqkms.getName(); | 222 final String name = wqkms.getName(); |
223 | 223 |
224 log.debug("Name of WQKms = '" + name + "'"); | 224 log.debug("Name of WQKms = '" + name + "'"); |
255 | 255 |
256 /** | 256 /** |
257 * @deprecated Use {@link WaterlevelDescriptionBuilder} instead. | 257 * @deprecated Use {@link WaterlevelDescriptionBuilder} instead. |
258 */ | 258 */ |
259 @Deprecated | 259 @Deprecated |
260 protected String getCSVRowTitle(final WINFOArtifact winfo, final WQKms wqkms) { | 260 public String getCSVRowTitle(final WINFOArtifact winfo, final WQKms wqkms) { |
261 log.debug("WaterlevelExporter.prepareNamedValue"); | 261 log.debug("WaterlevelExporter.prepareNamedValue"); |
262 | 262 |
263 final String name = wqkms.getName(); | 263 final String name = wqkms.getName(); |
264 | 264 |
265 log.debug("Name of WQKms = '" + name + "'"); | 265 log.debug("Name of WQKms = '" + name + "'"); |
289 * format. | 289 * format. |
290 * | 290 * |
291 * @deprecated Use {@link WaterlevelDescriptionBuilder} instead. | 291 * @deprecated Use {@link WaterlevelDescriptionBuilder} instead. |
292 */ | 292 */ |
293 @Deprecated | 293 @Deprecated |
294 protected String localizeWQKms(final WINFOArtifact winfo, final WQKms wqkms) { | 294 public String localizeWQKms(final WINFOArtifact winfo, final WQKms wqkms) { |
295 final WQ_MODE wqmode = RiverUtils.getWQMode(winfo); | 295 final WQ_MODE wqmode = RiverUtils.getWQMode(winfo); |
296 final Double rawValue = wqkms.getRawValue(); | 296 final Double rawValue = wqkms.getRawValue(); |
297 | 297 |
298 if (rawValue == null) { | 298 if (rawValue == null) { |
299 return wqkms.getName(); | 299 return wqkms.getName(); |
307 return "Q=" + nf.format(rawValue); | 307 return "Q=" + nf.format(rawValue); |
308 } | 308 } |
309 } | 309 } |
310 | 310 |
311 @Override | 311 @Override |
312 protected void writeCSVData(final CSVWriter writer) { | 312 public void writeCSVData(final CSVWriter writer) { |
313 log.info("WaterlevelExporter.writeData"); | 313 log.info("WaterlevelExporter.writeData"); |
314 | 314 |
315 final WQ_MODE mode = RiverUtils.getWQMode((D4EArtifact) this.master); | 315 final WQ_MODE mode = RiverUtils.getWQMode((D4EArtifact) this.master); |
316 final boolean atGauge = mode == WQ_MODE.QGAUGE || mode == WQ_MODE.WGAUGE; | 316 final boolean atGauge = mode == WQ_MODE.QGAUGE || mode == WQ_MODE.WGAUGE; |
317 final boolean isQ = mode == WQ_MODE.QGAUGE || mode == WQ_MODE.QFREE; | 317 final boolean isQ = mode == WQ_MODE.QGAUGE || mode == WQ_MODE.QFREE; |
369 * @param last: | 369 * @param last: |
370 * The last kilometer of the range | 370 * The last kilometer of the range |
371 * | 371 * |
372 * @return A new WQKms with the relevant data sorted by direction | 372 * @return A new WQKms with the relevant data sorted by direction |
373 */ | 373 */ |
374 private WQKms filterWQKms(final WQKms wqkms, final Double first, final Double last) { | 374 public final WQKms filterWQKms(final WQKms wqkms, final Double first, final Double last) { |
375 if (first.isNaN() || last.isNaN()) { | 375 if (first.isNaN() || last.isNaN()) { |
376 log.warn("Filtering official fixing without valid first/last."); | 376 log.warn("Filtering official fixing without valid first/last."); |
377 return wqkms; | 377 return wqkms; |
378 } | 378 } |
379 final int firstIdx = first > last ? wqkms.size() - 1 : 0; | 379 final int firstIdx = first > last ? wqkms.size() - 1 : 0; |
398 } | 398 } |
399 } | 399 } |
400 return filtered; | 400 return filtered; |
401 } | 401 } |
402 | 402 |
403 protected void writeCSVMeta(final CSVWriter writer) { | 403 public void writeCSVMeta(final CSVWriter writer) { |
404 log.info("WaterlevelExporter.writeCSVMeta"); | 404 log.info("WaterlevelExporter.writeCSVMeta"); |
405 | 405 |
406 // TODO use Access instead of RiverUtils | 406 // TODO use Access instead of RiverUtils |
407 | 407 |
408 final CallMeta meta = this.context.getMeta(); | 408 final CallMeta meta = this.context.getMeta(); |
509 } | 509 } |
510 } | 510 } |
511 return null; | 511 return null; |
512 } | 512 } |
513 | 513 |
514 private void writeRow4(final CSVWriter writer, final double wqkm[], final D4EArtifact flys) { | 514 protected void writeRow4(final CSVWriter writer, final double wqkm[], final D4EArtifact flys, final Gauge gauge) { |
515 final NumberFormat kmf = getKmFormatter(); | 515 final NumberFormat kmf = getKmFormatter(); |
516 final NumberFormat wf = getWFormatter(); | 516 final NumberFormat wf = getWFormatter(); |
517 final NumberFormat qf = getQFormatter(); | 517 final NumberFormat qf = getQFormatter(); |
518 writer.writeNext(new String[] { kmf.format(wqkm[2]), wf.format(wqkm[0]), qf.format(RiverUtils.roundQ(wqkm[1])), | 518 writer.writeNext(new String[] { kmf.format(wqkm[2]), wf.format(wqkm[0]), qf.format(RiverUtils.roundQ(wqkm[1])), |
519 RiverUtils.getLocationDescription(flys, wqkm[2]) }); | 519 RiverUtils.getLocationDescription(flys, wqkm[2]) }); |
520 } | 520 } |
521 | 521 |
522 /** Write an csv-row at gauge location. */ | 522 /** Write an csv-row at gauge location. */ |
523 private void writeRow6(final CSVWriter writer, final double wqkm[], final String wOrQDesc, final D4EArtifact flys, final String gaugeName) { | 523 protected void writeRow6(final CSVWriter writer, final double wqkm[], final String wOrQDesc, final D4EArtifact flys, final Gauge gauge) { |
524 final NumberFormat kmf = getKmFormatter(); | 524 final NumberFormat kmf = getKmFormatter(); |
525 final NumberFormat wf = getWFormatter(); | 525 final NumberFormat wf = getWFormatter(); |
526 final NumberFormat qf = getQFormatter(); | 526 final NumberFormat qf = getQFormatter(); |
527 | 527 |
528 writer.writeNext(new String[] { kmf.format(wqkm[2]), wf.format(wqkm[0]), qf.format(RiverUtils.roundQ(wqkm[1])), wOrQDesc, | 528 writer.writeNext(new String[] { kmf.format(wqkm[2]), wf.format(wqkm[0]), qf.format(RiverUtils.roundQ(wqkm[1])), wOrQDesc, |
529 RiverUtils.getLocationDescription(flys, wqkm[2]), gaugeName }); | 529 RiverUtils.getLocationDescription(flys, wqkm[2]), gauge.getName() }); |
530 } | 530 } |
531 | 531 |
532 /** | 532 /** |
533 * @deprecated Use {@link WaterlevelDescriptionBuilder} instead. | 533 * @deprecated Use {@link WaterlevelDescriptionBuilder} instead. |
534 */ | 534 */ |
535 @Deprecated | 535 @Deprecated |
536 private String getDesc(final WQKms wqkms, final boolean isQ) { | 536 public final String getDesc(final WQKms wqkms, final boolean isQ) { |
537 final D4EArtifact flys = (D4EArtifact) this.master; | 537 final D4EArtifact flys = (D4EArtifact) this.master; |
538 String colDesc = ""; | 538 String colDesc = ""; |
539 | 539 |
540 if (flys instanceof WINFOArtifact && isQ) { | 540 if (flys instanceof WINFOArtifact && isQ) { |
541 colDesc = getCSVRowTitle((WINFOArtifact) flys, wqkms); | 541 colDesc = getCSVRowTitle((WINFOArtifact) flys, wqkms); |
642 lastGauge = found; | 642 lastGauge = found; |
643 } else { | 643 } else { |
644 // TODO issue1114: Take correct gauge | 644 // TODO issue1114: Take correct gauge |
645 gaugeN = km >= a && km <= b ? gaugeName : notinrange; | 645 gaugeN = km >= a && km <= b ? gaugeName : notinrange; |
646 } | 646 } |
647 writeRow6(writer, result, desc, flys, gaugeN); | 647 writeRow6(writer, result, desc, flys, gauge); |
648 } | 648 } |
649 } else { // Not at gauge. | 649 } else { // Not at gauge. |
650 for (int i = 0; i < size; ++i) { | 650 for (int i = 0; i < size; ++i) { |
651 result = wqkms.get(i, result); | 651 result = wqkms.get(i, result); |
652 writeRow4(writer, result, flys); | 652 writeRow4(writer, result, flys, gauge); |
653 } | 653 } |
654 } | 654 } |
655 | 655 |
656 final long stopTime = System.currentTimeMillis(); | 656 final long stopTime = System.currentTimeMillis(); |
657 | 657 |
661 } | 661 } |
662 | 662 |
663 /** | 663 /** |
664 * Generates the output in WST format. | 664 * Generates the output in WST format. |
665 */ | 665 */ |
666 protected void generateWST() throws IOException { | 666 public void generateWST() throws IOException { |
667 log.info("WaterlevelExporter.generateWST"); | 667 log.info("WaterlevelExporter.generateWST"); |
668 | 668 |
669 final int cols = this.data.get(0).length + this.officalFixings.size(); | 669 final int cols = this.data.get(0).length + this.officalFixings.size(); |
670 final WstWriter writer = new WstWriter(cols); | 670 final WstWriter writer = new WstWriter(cols); |
671 | 671 |
672 writeWSTData(writer); | 672 writeWSTData(writer); |
673 | 673 |
674 writer.write(this.out); | 674 writer.write(this.out); |
675 } | 675 } |
676 | 676 |
677 protected void writeWSTData(final WstWriter writer) { | 677 public void writeWSTData(final WstWriter writer) { |
678 log.debug("WaterlevelExporter.writeWSTData"); | 678 log.debug("WaterlevelExporter.writeWSTData"); |
679 | 679 |
680 double[] result = new double[4]; | 680 double[] result = new double[4]; |
681 | 681 |
682 for (final WQKms[] tmp : this.data) { | 682 for (final WQKms[] tmp : this.data) { |
753 } else { | 753 } else { |
754 writer.addColumn(wqkms.getName()); | 754 writer.addColumn(wqkms.getName()); |
755 } | 755 } |
756 } | 756 } |
757 | 757 |
758 public void doWritePdf(final WKmsJRDataSource source, final String jasperFile) { | |
759 | |
760 final String confPath = Config.getConfigDirectory().toString(); | |
761 | |
762 final Map parameters = new HashMap(); | |
763 parameters.put("ReportTitle", "Exported Data"); | |
764 try { | |
765 final JasperPrint print = JasperFillManager.fillReport(confPath + jasperFile, parameters, source); | |
766 JasperExportManager.exportReportToPdfStream(print, this.out); | |
767 } | |
768 catch (final JRException je) { | |
769 log.warn("Error generating PDF Report!", je); | |
770 } | |
771 } | |
772 | |
758 @Override | 773 @Override |
759 protected void writePDF(final OutputStream out) { | 774 protected void writePDF(final OutputStream out) { |
760 log.debug("write PDF"); | 775 log.debug("write PDF"); |
761 final WKmsJRDataSource source = createJRData(); | 776 final WKmsJRDataSource source = createJRData(); |
762 | 777 final String jasperFile = Resources.getMsg(this.context.getMeta(), JASPER_FILE, "/jasper/waterlevel_en.jasper"); |
763 final String jasperFile = // "/jasper/waterlevel_en.jasper"; | 778 doWritePdf(source, jasperFile); |
764 Resources.getMsg(this.context.getMeta(), JASPER_FILE, "/jasper/waterlevel_en.jasper"); | 779 |
765 final String confPath = Config.getConfigDirectory().toString(); | 780 } |
766 | 781 |
767 final Map parameters = new HashMap(); | 782 private WKmsJRDataSource createJRData() { |
768 parameters.put("ReportTitle", "Exported Data"); | |
769 try { | |
770 final JasperPrint print = JasperFillManager.fillReport(confPath + jasperFile, parameters, source); | |
771 JasperExportManager.exportReportToPdfStream(print, out); | |
772 } | |
773 catch (final JRException je) { | |
774 log.warn("Error generating PDF Report!", je); | |
775 } | |
776 } | |
777 | |
778 protected WKmsJRDataSource createJRData() { | |
779 final WKmsJRDataSource source = new WKmsJRDataSource(); | 783 final WKmsJRDataSource source = new WKmsJRDataSource(); |
780 | 784 |
781 final WQ_MODE mode = RiverUtils.getWQMode((D4EArtifact) this.master); | 785 final WQ_MODE mode = RiverUtils.getWQMode((D4EArtifact) this.master); |
782 final boolean atGauge = mode == WQ_MODE.QGAUGE || mode == WQ_MODE.WGAUGE; | 786 final boolean atGauge = mode == WQ_MODE.QGAUGE || mode == WQ_MODE.WGAUGE; |
783 final boolean isQ = mode == WQ_MODE.QGAUGE || mode == WQ_MODE.QFREE; | 787 final boolean isQ = mode == WQ_MODE.QGAUGE || mode == WQ_MODE.QFREE; |
817 addWKmsData(filterWQKms(wqkms, first, last), atGauge, isQ, source); | 821 addWKmsData(filterWQKms(wqkms, first, last), atGauge, isQ, source); |
818 } | 822 } |
819 return source; | 823 return source; |
820 } | 824 } |
821 | 825 |
822 protected void addMetaData(final WKmsJRDataSource source) { | 826 public void addMetaData(final WKmsJRDataSource source) { |
823 final CallMeta meta = this.context.getMeta(); | 827 final CallMeta meta = this.context.getMeta(); |
824 | 828 |
825 final D4EArtifact flys = (D4EArtifact) this.master; | 829 final D4EArtifact flys = (D4EArtifact) this.master; |
826 | 830 |
827 source.addMetaData("river", RiverUtils.getRivername(flys)); | 831 source.addMetaData("river", RiverUtils.getRivername(flys)); |
833 source.addMetaData("date", df.format(new Date())); | 837 source.addMetaData("date", df.format(new Date())); |
834 | 838 |
835 final RangeAccess rangeAccess = new RangeAccess(flys); | 839 final RangeAccess rangeAccess = new RangeAccess(flys); |
836 final double[] kms = rangeAccess.getKmRange(); | 840 final double[] kms = rangeAccess.getKmRange(); |
837 source.addMetaData("range", kmf.format(kms[0]) + " - " + kmf.format(kms[kms.length - 1])); | 841 source.addMetaData("range", kmf.format(kms[0]) + " - " + kmf.format(kms[kms.length - 1])); |
842 | |
843 source.addMetaData("w_at_gauge_header", Resources.getMsg(meta, "fix.export.csv.w_at_gauge")); // dürfte kein Problem sein für Vorlagen, die kein | |
844 // "w_at_gauge" | |
845 // haben | |
838 | 846 |
839 source.addMetaData("gauge", RiverUtils.getGaugename(flys)); | 847 source.addMetaData("gauge", RiverUtils.getGaugename(flys)); |
840 | 848 |
841 source.addMetaData("calculation", Resources.getMsg(locale, PDF_HEADER_MODE, "Waterlevel")); | 849 source.addMetaData("calculation", Resources.getMsg(locale, PDF_HEADER_MODE, "Waterlevel")); |
842 } | 850 } |
879 RiverUtils.getLocationDescription(flys, result[2]), result[2] >= a && result[2] <= b ? gaugeName : notinrange }); | 887 RiverUtils.getLocationDescription(flys, result[2]), result[2] >= a && result[2] <= b ? gaugeName : notinrange }); |
880 } else { | 888 } else { |
881 source.addData(new String[] { kmf.format(result[2]), wf.format(result[0]), qf.format(RiverUtils.roundQ(result[1])), desc, | 889 source.addData(new String[] { kmf.format(result[2]), wf.format(result[0]), qf.format(RiverUtils.roundQ(result[1])), desc, |
882 RiverUtils.getLocationDescription(flys, result[2]), result[2] >= a && result[2] <= b ? gaugeName : notinrange }); | 890 RiverUtils.getLocationDescription(flys, result[2]), result[2] >= a && result[2] <= b ? gaugeName : notinrange }); |
883 } | 891 } |
892 | |
884 } | 893 } |
885 | 894 |
886 final long stopTime = System.currentTimeMillis(); | 895 final long stopTime = System.currentTimeMillis(); |
887 | 896 |
888 if (log.isDebugEnabled()) { | 897 if (log.isDebugEnabled()) { |
889 log.debug("Writing PDF data took " + (stopTime - startTime) / 1000f + " secs."); | 898 log.debug("Writing PDF data took " + (stopTime - startTime) / 1000f + " secs."); |
890 } | 899 } |
891 } | 900 } |
901 | |
892 } | 902 } |
893 // vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 : | 903 // vim:set ts=4 sw=4 si et sta sts=4 fenc=utf8 : |