001 //$$HeadURL: http://svn.wald.intevation.org/svn/deegree/base/trunk/src/org/deegree/tools/shape/AVL2SLD.java $$ 002 /*---------------------------------------------------------------------------- 003 This file is part of deegree, http://deegree.org/ 004 Copyright (C) 2001-2009 by: 005 Department of Geography, University of Bonn 006 and 007 lat/lon GmbH 008 009 This library is free software; you can redistribute it and/or modify it under 010 the terms of the GNU Lesser General Public License as published by the Free 011 Software Foundation; either version 2.1 of the License, or (at your option) 012 any later version. 013 This library is distributed in the hope that it will be useful, but WITHOUT 014 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS 015 FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more 016 details. 017 You should have received a copy of the GNU Lesser General Public License 018 along with this library; if not, write to the Free Software Foundation, Inc., 019 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 020 021 Contact information: 022 023 lat/lon GmbH 024 Aennchenstr. 19, 53177 Bonn 025 Germany 026 http://lat-lon.de/ 027 028 Department of Geography, University of Bonn 029 Prof. Dr. Klaus Greve 030 Postfach 1147, 53001 Bonn 031 Germany 032 http://www.geographie.uni-bonn.de/deegree/ 033 034 e-mail: info@deegree.org 035 ----------------------------------------------------------------------------*/ 036 037 package org.deegree.tools.shape; 038 039 import java.awt.Color; 040 import java.awt.image.BufferedImage; 041 import java.io.File; 042 import java.io.FileOutputStream; 043 import java.io.FileReader; 044 import java.io.FileWriter; 045 import java.io.FilenameFilter; 046 import java.io.IOException; 047 import java.io.Reader; 048 import java.util.ArrayList; 049 import java.util.HashMap; 050 import java.util.List; 051 import java.util.Map; 052 import java.util.regex.Matcher; 053 import java.util.regex.Pattern; 054 055 import org.deegree.datatypes.QualifiedName; 056 import org.deegree.framework.util.ImageUtils; 057 import org.deegree.framework.util.StringTools; 058 import org.deegree.framework.xml.Marshallable; 059 import org.deegree.graphics.sld.AbstractLayer; 060 import org.deegree.graphics.sld.AbstractStyle; 061 import org.deegree.graphics.sld.ExternalGraphic; 062 import org.deegree.graphics.sld.FeatureTypeStyle; 063 import org.deegree.graphics.sld.Fill; 064 import org.deegree.graphics.sld.Font; 065 import org.deegree.graphics.sld.Graphic; 066 import org.deegree.graphics.sld.GraphicFill; 067 import org.deegree.graphics.sld.LabelPlacement; 068 import org.deegree.graphics.sld.LineSymbolizer; 069 import org.deegree.graphics.sld.Mark; 070 import org.deegree.graphics.sld.NamedLayer; 071 import org.deegree.graphics.sld.PointPlacement; 072 import org.deegree.graphics.sld.PointSymbolizer; 073 import org.deegree.graphics.sld.PolygonSymbolizer; 074 import org.deegree.graphics.sld.Rule; 075 import org.deegree.graphics.sld.Stroke; 076 import org.deegree.graphics.sld.StyleFactory; 077 import org.deegree.graphics.sld.StyledLayerDescriptor; 078 import org.deegree.graphics.sld.Symbolizer; 079 import org.deegree.graphics.sld.TextSymbolizer; 080 import org.deegree.io.shpapi.MainFile; 081 import org.deegree.io.shpapi.ShapeConst; 082 import org.deegree.model.filterencoding.ComplexFilter; 083 import org.deegree.model.filterencoding.Filter; 084 import org.deegree.model.filterencoding.FilterConstructionException; 085 import org.deegree.model.filterencoding.Literal; 086 import org.deegree.model.filterencoding.LogicalOperation; 087 import org.deegree.model.filterencoding.Operation; 088 import org.deegree.model.filterencoding.OperationDefines; 089 import org.deegree.model.filterencoding.PropertyIsCOMPOperation; 090 import org.deegree.model.filterencoding.PropertyName; 091 092 /** 093 * <p> 094 * This class converts ESRI *.avl files to OGC SLD documents. The current version of this tool isn't 095 * able to convert each and every construction that is possible with an *.avl file. But most of the 096 * common expressions will be mapped. 097 * </p> 098 * <p> 099 * Because SLD (version 1.0.0) does not know inline bitmap or fill pattern definition directly 100 * (maybe it is possible using some SVG tags) all polygon fill patterns must be converted to images 101 * that are written to the file system and referenced as external graphic by the created SLD style. 102 * The similar is true for symbol definitions. SLD just 'knowns' a few predefined symbols that are 103 * not able to capture all symbols known by ArcView. In this context deegree also will extend the 104 * well known symbol by using ASCII codes to references symbols defined in an available font. This 105 * will enable deegree to transform the ArcView option defining a symbol by using an ACSII character 106 * directly. At least even if the synthax for this is SLD compliant most SLD implementation probably 107 * won't be able to evaluate this. 108 * </p> 109 * 110 * @author <a href="mailto:poth@lat-lon.de">Andreas Poth </a> 111 * @author last edited by: $Author: apoth $ 112 * 113 * @version $Revision: 29436 $, $Date: 2011-01-24 09:15:57 +0100 (Mon, 24 Jan 2011) $ 114 */ 115 public class AVL2SLD { 116 117 private String fileRootName = null; 118 119 private String targetDir = null; 120 121 private Map<String, Map<String, Object>> blocks = new HashMap<String, Map<String, Object>>(); 122 123 private static AVLPointSymbolCodeList cl = null; 124 125 /** 126 * @param fileRootName 127 * @param targetDir 128 */ 129 public AVL2SLD( String fileRootName, String targetDir ) { 130 this.fileRootName = fileRootName; 131 this.targetDir = targetDir; 132 if ( !this.targetDir.endsWith( "/" ) ) { 133 this.targetDir = this.targetDir + "/"; 134 } 135 } 136 137 /** 138 * reads the avl file assigned to a class instance 139 * 140 * @throws IOException 141 */ 142 public void read() 143 throws IOException { 144 Reader reader = new FileReader( fileRootName + ".avl" ); 145 StringBuffer sb = new StringBuffer( 50000 ); 146 int c = 0; 147 while ( ( c = reader.read() ) > -1 ) { 148 if ( c == 9 ) 149 c = ' '; 150 // if ( c != 10 && c != 13) { 151 sb.append( (char) c ); 152 // } 153 } 154 reader.close(); 155 156 // create a entry in 'blocks' for each '(' ... ')' 157 // enclosed section 158 String[] s1 = splitavl( sb.toString() ); 159 160 for ( int i = 1; i < s1.length; i++ ) { 161 // write each KVP of section to a map and store it 162 // in the blocks map. 163 // the Class, Pattern and he Child key will be treated 164 // as array because it isn't unique 165 int pos = s1[i].indexOf( ' ' ); 166 if ( pos < 0 ) 167 continue; 168 String section = s1[i].substring( 0, pos ).trim(); 169 String[] s2 = StringTools.toArray( s1[i].substring( pos, s1[i].length() ), ":\n", false ); 170 Map<String, Object> block = new HashMap<String, Object>(); 171 for ( int j = 0; j < s2.length; j = j + 2 ) { 172 if ( s2[j].trim().equals( "Class" ) ) { 173 List<String> list = (List<String>) block.get( "Class" ); 174 if ( list == null ) { 175 list = new ArrayList<String>(); 176 } 177 list.add( s2[j + 1] ); 178 block.put( s2[j], list ); 179 } else if ( s2[j].trim().equals( "Child" ) ) { 180 List<String> list = (List<String>) block.get( "Child" ); 181 if ( list == null ) { 182 list = new ArrayList<String>(); 183 } 184 list.add( s2[j + 1] ); 185 block.put( s2[j], list ); 186 } else if ( s2[j].trim().equals( "Pattern" ) ) { 187 List<String> list = (List<String>) block.get( "Pattern" ); 188 if ( list == null ) { 189 list = new ArrayList<String>(); 190 } 191 list.add( s2[j + 1] ); 192 block.put( s2[j], list ); 193 } else if ( s2[j].trim().equals( "Bits" ) ) { 194 List<String> list = (List<String>) block.get( "Bits" ); 195 if ( list == null ) { 196 list = new ArrayList<String>(); 197 } 198 list.add( s2[j + 1] ); 199 block.put( s2[j], list ); 200 } else { 201 block.put( s2[j], s2[j + 1].trim() ); 202 } 203 } 204 blocks.put( section, block ); 205 } 206 207 } 208 209 /** 210 * returns a <tt>Style</tt>. For each class/child contained in a avl file one <tt>Rule</tt> 211 * will be created 212 * 213 * @return a <tt>Style</tt>. For each class/child contained in a avl file one <tt>Rule</tt> 214 * will be created 215 * @throws IOException 216 * @throws Exception 217 */ 218 public AbstractStyle getStyle() 219 throws IOException, Exception { 220 Map odb = blocks.get( "ODB.1" ); 221 String roots = (String) odb.get( "Roots" ); 222 Map legend = blocks.get( "Legend." + roots ); 223 String filterCol = null; 224 if ( legend.get( "FieldNames" ) != null ) { 225 Map block = blocks.get( "AVStr." + legend.get( "FieldNames" ) ); 226 filterCol = (String) block.get( "S" ); 227 filterCol = StringTools.validateString( filterCol, "\"" ).toUpperCase(); 228 } 229 230 int geoType = getGeometryType(); 231 232 AbstractStyle style = null; 233 switch ( geoType ) { 234 case ShapeConst.SHAPE_TYPE_POINT: 235 style = createPointStyle( legend, filterCol ); 236 break; 237 case ShapeConst.SHAPE_TYPE_POLYLINE: 238 style = createLinesStyle( legend, filterCol ); 239 break; 240 case ShapeConst.SHAPE_TYPE_POLYGON: 241 style = createPolygonStyle( legend, filterCol ); 242 break; 243 case ShapeConst.SHAPE_TYPE_MULTIPOINT: 244 style = createPointStyle( legend, filterCol ); 245 break; 246 default: 247 throw new Exception( "unknown geometry type: " + geoType ); 248 } 249 return style; 250 } 251 252 /** 253 * creates a <tt>StyledLayerDescriptor</tt> from the avl file assigned to the instace of a 254 * <tt>AVLReader</tt>. The returned instance of a <tt>StyledLayerDescriptor</tt> just 255 * contains one style that may containes several <tt>Rule</tt>s 256 * 257 * @return a <tt>StyledLayerDescriptor</tt> created from the avl file assigned to the instace 258 * of a <tt>AVLReader</tt>. The returned instance of a <tt>StyledLayerDescriptor</tt> 259 * just contains one style that may containes several <tt>Rule</tt>s 260 * @throws IOException 261 * @throws Exception 262 */ 263 public StyledLayerDescriptor getStyledLayerDescriptor() 264 throws IOException, Exception { 265 AbstractStyle style = getStyle(); 266 String[] t = StringTools.toArray( fileRootName, "/", false ); 267 String name = "default:" + t[t.length - 1]; 268 AbstractLayer layer = new NamedLayer( name, null, new AbstractStyle[] { style } ); 269 return new StyledLayerDescriptor( new AbstractLayer[] { layer }, "1.0.0" ); 270 } 271 272 /** 273 * @return parse a string and return array of blocks between braces "(" and ")". It accounts for 274 * braces in quoted strings. 275 * 276 * @param s 277 * string to parse 278 */ 279 public static String[] splitavl( String s ) { 280 if ( s == null || s.equals( "" ) ) { 281 return new String[0]; 282 } 283 284 Pattern pat = Pattern.compile( "\\(([^)\"]|\"[^\"]*\")*\\)" ); 285 Matcher mat = pat.matcher( s ); 286 int prevend = 0; 287 ArrayList<String> vec = new ArrayList<String>(); 288 while ( mat.find() ) { 289 int start = mat.start(); 290 int end = mat.end(); 291 if ( prevend < start - 1 ) { 292 String str = s.substring( prevend, start ).trim(); 293 if ( str.length() > 0 ) { 294 vec.add( str ); 295 } 296 } 297 String str = s.substring( start + 1, end - 1 ).trim(); 298 if ( str.length() > 0 ) { 299 vec.add( str ); 300 } 301 prevend = end; 302 } 303 if ( prevend < s.length() - 1 ) { 304 String str = s.substring( prevend ).trim(); 305 if ( str.length() > 0 ) { 306 vec.add( str ); 307 } 308 } 309 310 // no value selected 311 if ( vec.size() == 0 ) { 312 return new String[0]; 313 } 314 315 return vec.toArray( new String[vec.size()] ); 316 } 317 318 private int getGeometryType() 319 throws IOException { 320 MainFile mf = new MainFile( fileRootName ); 321 int type = mf.getShapeTypeByRecNo( 1 ); 322 mf.close(); 323 return type; 324 } 325 326 /** 327 * creates a <tt>Style</tt>. For each class/child contained in a avl file one <tt>Rule</tt> 328 * will be created 329 * 330 * @param legend 331 * @param filterCol 332 * @return a <tt>Style</tt>. 333 */ 334 // private AbstractStyle[] createPointStyles( Map legend, String filterCol ) { 335 // AbstractStyle[] styles = null; 336 // return styles; 337 // } 338 /** 339 * creates a <tt>Style</tt>. For each class/child contained in a avl file one <tt>Rule</tt> 340 * will be created 341 * 342 * @param legend 343 * @param filterCol 344 * @return a <tt>Style</tt>. 345 */ 346 private AbstractStyle createPointStyle( Map legend, String filterCol ) 347 throws FilterConstructionException { 348 try { 349 cl = new AVLPointSymbolCodeList(); 350 } catch ( Exception e ) { 351 e.printStackTrace(); 352 } 353 String tmp = (String) legend.get( "Symbols" ); 354 Map block = blocks.get( "SymList." + tmp ); 355 List classes = (List) legend.get( "Class" ); 356 List children = (List) block.get( "Child" ); 357 358 List<Rule> list = new ArrayList<Rule>( classes.size() ); 359 for ( int i = 0; i < classes.size(); i++ ) { 360 String clNo = (String) classes.get( i ); 361 Map clss = blocks.get( "LClass." + clNo ); 362 String childNo = (String) children.get( i ); 363 Map<String, Object> child = blocks.get( "CMkSym." + childNo ); 364 Rule rule = null; 365 if ( child == null ) { 366 child = blocks.remove( "BMkSym." + childNo ); 367 rule = createSimplePointRule( clss, child, filterCol ); 368 } else { 369 rule = createComplexPointRule( clss, child, filterCol ); 370 } 371 if ( rule != null ) { 372 list.add( rule ); 373 } 374 } 375 Rule[] rules = list.toArray( new Rule[list.size()] ); 376 FeatureTypeStyle fts = StyleFactory.createFeatureTypeStyle( rules ); 377 378 String[] t = StringTools.toArray( fileRootName, "/", false ); 379 String name = "default:" + t[t.length - 1]; 380 return StyleFactory.createStyle( name, null, null, fts ); 381 } 382 383 /** 384 * creates a Style for a line symbol 385 * 386 * @param clss 387 * @param child 388 * @return a Style for a line symbol 389 */ 390 private Rule createSimplePointRule( Map clss, Map<String, Object> child, String filterCol ) { 391 392 if ( clss.get( "IsNoData" ) != null ) { 393 return null; 394 } 395 396 String label = (String) clss.get( "Label" ); 397 label = StringTools.validateString( label, "\"" ); 398 Filter filter = createFilter( clss, filterCol ); 399 // get foreground color 400 String clrIdx = (String) child.get( "Color" ); 401 Map<String, Object> color = blocks.get( "TClr." + clrIdx ); 402 double fgOpacity = 1f; 403 if ( color != null && "\"Transparent\"".equals( color.get( "Name" ) ) ) { 404 fgOpacity = 0f; 405 } 406 Color fgColor = createColor( color ); 407 if ( fgColor == null ) { 408 fgColor = Color.BLACK; 409 } 410 // get background color (what ever this means) 411 clrIdx = (String) child.get( "BgColor" ); 412 color = blocks.get( "TClr." + clrIdx ); 413 double bgOpacity = 1f; 414 if ( color != null && "\"Transparent\"".equals( color.get( "Name" ) ) ) { 415 bgOpacity = 0f; 416 } 417 Color bgColor = createColor( color ); 418 if ( bgColor == null ) { 419 bgColor = Color.BLACK; 420 } 421 422 double size = Double.parseDouble( (String) child.get( "Size" ) ); 423 double rotation = Double.parseDouble( (String) child.get( "Angle" ) ); 424 425 // creation of a font from a avl file is a triky thing because 426 // esri uses their own font names and codes 427 // --> don' know if this works 428 String fntIdx = (String) child.get( "Font" ); 429 Map fntMap = blocks.get( "NFont." + fntIdx ); 430 Font font = createFont( fntMap ); 431 PointPlacement pp = StyleFactory.createPointPlacement(); 432 LabelPlacement labelPlacement = StyleFactory.createLabelPlacement( pp ); 433 TextSymbolizer textSym = StyleFactory.createTextSymbolizer( fgColor, font, filterCol, labelPlacement ); 434 435 String patternIdx = (String) ( (ArrayList) child.get( "Pattern" ) ).get( 0 ); 436 String symbol = cl.getSymbol( patternIdx ); 437 438 // create a Mark with a stroke and fill to controll both 439 // opacities 440 Stroke stroke = StyleFactory.createStroke( fgColor, 1, fgOpacity, null, "mitre", "butt" ); 441 Fill fill = StyleFactory.createFill( bgColor, bgOpacity ); 442 Mark mark = StyleFactory.createMark( symbol, fill, stroke ); 443 444 Graphic graphic = StyleFactory.createGraphic( null, mark, 1, size, rotation ); 445 PointSymbolizer pointSym = StyleFactory.createPointSymbolizer( graphic ); 446 447 return StyleFactory.createRule( new Symbolizer[] { pointSym, textSym }, label, label, "", null, filter, false, 448 0, 9E99 ); 449 } 450 451 private Rule createComplexPointRule( Map clss, Map<String, Object> child, String filterCol ) 452 throws FilterConstructionException { 453 454 if ( clss.get( "IsNoData" ) != null ) { 455 return null; 456 } 457 458 String tmp = (String) child.get( "Symbols" ); 459 Map block = blocks.get( "SymList." + tmp ); 460 List children = (List) block.get( "Child" ); 461 462 List<Symbolizer> smbls = new ArrayList<Symbolizer>(); 463 for ( int i = 0; i < children.size(); i++ ) { 464 String childNo = (String) children.get( i ); 465 Map child_ = blocks.get( "CMkSym." + childNo ); 466 Rule rule = null; 467 if ( child_ == null ) { 468 child = blocks.remove( "BMkSym." + childNo ); 469 rule = createSimplePointRule( clss, child, filterCol ); 470 } else { 471 rule = createComplexPointRule( clss, child, filterCol ); 472 } 473 Symbolizer[] sym = rule.getSymbolizers(); 474 for ( int j = 0; j < sym.length; j++ ) { 475 smbls.add( sym[j] ); 476 } 477 } 478 Symbolizer[] sym = new Symbolizer[smbls.size()]; 479 sym = smbls.toArray( sym ); 480 481 String label = (String) clss.get( "Label" ); 482 label = StringTools.validateString( label, "\"" ); 483 Filter filter = createFilter( clss, filterCol ); 484 485 return StyleFactory.createRule( sym, label, label, "", null, filter, false, 0, 9E99 ); 486 } 487 488 private Font createFont( Map fntMap ) { 489 String idx = (String) fntMap.get( "Family" ); 490 String family = (String) blocks.get( "AVStr." + idx ).get( "S" ); 491 idx = (String) fntMap.get( "Name" ); 492 String name = (String) blocks.get( "AVStr." + idx ).get( "S" ); 493 idx = (String) fntMap.get( "Style" ); 494 String style = (String) blocks.get( "AVStr." + idx ).get( "S" ); 495 String weight = (String) fntMap.get( "Weight" ); 496 String wideness = (String) fntMap.get( "Wideness" ); 497 498 boolean italic = style.equals( "Italic" ); 499 boolean bold = Integer.parseInt( weight ) > 1; 500 501 return StyleFactory.createFont( family, italic, bold, 12 ); 502 } 503 504 /** 505 * creates a <tt>Style</tt>. For each class/child contained in a avl file one <tt>Rule</tt> 506 * will be created 507 * 508 * @param legend 509 * @param filterCol 510 * @return a <tt>Style</tt>. 511 */ 512 private AbstractStyle createLinesStyle( Map legend, String filterCol ) { 513 String tmp = (String) legend.get( "Symbols" ); 514 Map block = blocks.get( "SymList." + tmp ); 515 List classes = (List) legend.get( "Class" ); 516 List children = (List) block.get( "Child" ); 517 List<Rule> list = new ArrayList<Rule>( classes.size() ); 518 for ( int i = 0; i < classes.size(); i++ ) { 519 String clNo = (String) classes.get( i ); 520 Map clss = blocks.get( "LClass." + clNo ); 521 String childNo = (String) children.get( i ); 522 Map<String, Object> child = blocks.get( "BLnSym." + childNo ); 523 524 if ( child == null ) { 525 // won't be treated correctly because we can't transform 526 // lines with sambols at the moment 527 child = blocks.get( "CLnSym." + childNo ); 528 } 529 Rule rule = createLineRule( clss, child, filterCol ); 530 if ( rule != null ) { 531 list.add( rule ); 532 } 533 } 534 Rule[] rules = list.toArray( new Rule[list.size()] ); 535 FeatureTypeStyle fts = StyleFactory.createFeatureTypeStyle( rules ); 536 537 String[] t = StringTools.toArray( fileRootName, "/", false ); 538 String name = "default:" + t[t.length - 1]; 539 return StyleFactory.createStyle( name, null, null, fts ); 540 } 541 542 /** 543 * creates a Style for a line symbol 544 * 545 * @param clss 546 * @param child 547 * @return a Style for a line symbol 548 */ 549 private Rule createLineRule( Map clss, Map<String, Object> child, String filterCol ) { 550 551 if ( clss.get( "IsNoData" ) != null ) { 552 return null; 553 } 554 555 String label = (String) clss.get( "Label" ); 556 label = StringTools.validateString( label, "\"" ); 557 Filter filter = createFilter( clss, filterCol ); 558 559 String clrIdx = (String) child.get( "Color" ); 560 Map<String, Object> color = blocks.get( "TClr." + clrIdx ); 561 double opacity = 1f; 562 if ( color != null && "\"Transparent\"".equals( color.get( "Name" ) ) && color.size() > 0 ) { 563 opacity = 0f; 564 } 565 Color colour = createColor( color ); 566 if ( colour == null ) { 567 colour = Color.BLACK; 568 } 569 570 double width = 1.0; // default Width 571 if ( child.get( "Width" ) != null ) { 572 width = Double.parseDouble( (String) child.get( "Width" ) ) + width; 573 } 574 // double width = Double.parseDouble( (String)child.get("Width") ) + 1; 575 List<String> pl = (List<String>) child.get( "Pattern" ); 576 577 if ( child.get( "Pattern" ) == null ) { // create a default pattern List if 578 // it is null 579 pl = new ArrayList<String>(); 580 for ( int i = 0; i < 16; i++ ) { // Fill the default List with 581 // default values "0.00000000000000" 582 pl.add( "0.00000000000000" ); 583 } 584 } 585 586 // List pl = (List)child.get("Pattern"); 587 float[] dashArray = createDashArray( pl ); 588 Stroke stroke = StyleFactory.createStroke( colour, width, opacity, dashArray, "mitre", "butt" ); 589 LineSymbolizer lineSym = StyleFactory.createLineSymbolizer( stroke ); 590 591 return StyleFactory.createRule( new Symbolizer[] { lineSym }, label, label, "", null, filter, false, 0, 9E99 ); 592 } 593 594 /** 595 * creates a <tt>Style</tt>. For each class/child contained in a avl file one <tt>Rule</tt> 596 * will be created 597 * 598 * @param legend 599 * @param filterCol 600 * @return a <tt>Style</tt>. 601 */ 602 private AbstractStyle createPolygonStyle( Map legend, String filterCol ) 603 throws Exception { 604 String tmp = (String) legend.get( "Symbols" ); 605 Map block = blocks.get( "SymList." + tmp ); 606 List classes = (List) legend.get( "Class" ); 607 List children = (List) block.get( "Child" ); 608 List<Rule> list = new ArrayList<Rule>( classes.size() ); 609 for ( int i = 0; i < classes.size(); i++ ) { 610 String clNo = (String) classes.get( i ); 611 Map clss = blocks.get( "LClass." + clNo ); 612 String childNo = (String) children.get( i ); 613 Map<String, Object> child = blocks.get( "BShSym." + childNo ); 614 Rule rule = null; 615 if ( child == null ) { 616 // VShSym is a vector polygon fill 617 child = blocks.get( "VShSym." + childNo ); 618 if ( child == null ) { 619 rule = createPolygonVecRule( clss, filterCol ); 620 } else { 621 rule = createPolygonVecRule( clss, child, filterCol ); 622 } 623 } else { 624 rule = createPolygonBMPRule( clss, child, filterCol ); 625 } 626 if ( rule != null ) { 627 list.add( rule ); 628 } 629 // TODO 630 // write special method for vector polygon fill 631 } 632 Rule[] rules = list.toArray( new Rule[list.size()] ); 633 FeatureTypeStyle fts = StyleFactory.createFeatureTypeStyle( rules ); 634 635 String[] t = StringTools.toArray( fileRootName, "/", false ); 636 String name = "default:" + t[t.length - 1]; 637 return StyleFactory.createStyle( name, null, null, fts ); 638 } 639 640 /** 641 * creates a Style for a line symbol 642 * 643 * @param clss 644 * @param child 645 * @return a Style for a line symbol 646 */ 647 private Rule createPolygonBMPRule( Map clss, Map<String, Object> child, String filterCol ) 648 throws Exception { 649 650 if ( clss.get( "IsNoData" ) != null ) { 651 return null; 652 } 653 654 String label = (String) clss.get( "Label" ); 655 label = StringTools.validateString( label, "\"" ); 656 657 Filter filter = null; 658 if ( filterCol != null ) { 659 filter = createFilter( clss, filterCol ); 660 } 661 // get foreground color 662 String clrIdx = (String) child.get( "Color" ); 663 Map<String, Object> color = blocks.get( "TClr." + clrIdx ); 664 double opacity = 1f; 665 if ( color != null && "\"Transparent\"".equals( color.get( "Name" ) ) ) { 666 opacity = 0f; 667 } 668 669 Color fgColor = createColor( color ); 670 if ( fgColor == null ) { 671 fgColor = Color.BLACK; 672 } 673 // get color of the outlining stroke 674 clrIdx = (String) child.get( "OutlineColor" ); 675 color = blocks.get( "TClr." + clrIdx ); 676 double outLOpacity = 1f; 677 if ( color != null && "\"Transparent\"".equals( color.get( "Name" ) ) ) { 678 outLOpacity = 0f; 679 } 680 Color outLColor = createColor( color ); 681 if ( outLColor == null ) { 682 outLColor = Color.BLACK; 683 } 684 // get background color 685 clrIdx = (String) child.get( "BgColor" ); 686 color = blocks.get( "TClr." + clrIdx ); 687 double bgOpacity = 1f; 688 if ( color != null && "\"Transparent\"".equals( color.get( "Name" ) ) ) { 689 bgOpacity = 0f; 690 } 691 Color bgColor = createColor( color ); 692 693 // create fill pattern as an image that will be referenced as 694 // external graphic 695 String stippleIdx = (String) child.get( "Stipple" ); 696 String src = null; 697 if ( stippleIdx != null ) { 698 Map stipple = blocks.get( "Stipple." + stippleIdx ); 699 src = createExternalGraphicFromStipple( stipple, label, fgColor, bgColor ); 700 } 701 702 double width = Double.parseDouble( (String) child.get( "OutlineWidth" ) ) + 1; 703 Stroke stroke = StyleFactory.createStroke( outLColor, width, outLOpacity, null, "mitre", "butt" ); 704 Fill fill = null; 705 if ( stippleIdx != null ) { 706 ExternalGraphic eg = StyleFactory.createExternalGraphic( "file:///" + src, "image/gif" ); 707 Graphic graph = StyleFactory.createGraphic( eg, null, opacity, 10, 0 ); 708 GraphicFill gf = StyleFactory.createGraphicFill( graph ); 709 fill = StyleFactory.createFill( fgColor, opacity, gf ); 710 } else { 711 fill = StyleFactory.createFill( fgColor, opacity ); 712 } 713 PolygonSymbolizer polySym = StyleFactory.createPolygonSymbolizer( stroke, fill ); 714 return StyleFactory.createRule( new Symbolizer[] { polySym }, label, label, "", null, filter, false, 0, 9E99 ); 715 } 716 717 /** 718 * creates a Style for a line symbol 719 * 720 * @param clss 721 * @param child 722 * @return a Style for a line symbol 723 */ 724 private Rule createPolygonVecRule( Map clss, Map<String, Object> child, String filterCol ) 725 throws Exception { 726 727 if ( clss.get( "IsNoData" ) != null ) { 728 return null; 729 } 730 731 String label = (String) clss.get( "Label" ); 732 label = StringTools.validateString( label, "\"" ); 733 734 Filter filter = null; 735 if ( filterCol != null ) { 736 filter = createFilter( clss, filterCol ); 737 } 738 // get foreground color 739 String clrIdx = (String) child.get( "Color" ); 740 Map<String, Object> color = blocks.get( "TClr." + clrIdx ); 741 double opacity = 1f; 742 if ( color != null && "\"Transparent\"".equals( color.get( "Name" ) ) ) { 743 opacity = 0f; 744 } 745 Color fgColor = createColor( color ); 746 if ( fgColor == null ) { 747 fgColor = Color.BLACK; 748 } 749 // get color of the outlining stroke 750 clrIdx = (String) child.get( "OutlineColor" ); 751 color = blocks.get( "TClr." + clrIdx ); 752 double outLOpacity = 1f; 753 if ( color != null && "\"Transparent\"".equals( color.get( "Name" ) ) ) { 754 outLOpacity = 0f; 755 } 756 Color outLColor = createColor( color ); 757 if ( outLColor == null ) { 758 outLColor = Color.BLACK; 759 } 760 // get background color 761 clrIdx = (String) child.get( "BgColor" ); 762 color = blocks.get( "TClr." + clrIdx ); 763 double bgOpacity = 1f; 764 if ( color != null && "\"Transparent\"".equals( color.get( "Name" ) ) ) { 765 bgOpacity = 0f; 766 } 767 Color bgColor = createColor( color ); 768 769 // create fill pattern as an image that will be referenced as 770 // external graphic 771 String stippleIdx = (String) child.get( "Stipple" ); 772 String src = null; 773 if ( stippleIdx != null ) { 774 Map stipple = blocks.get( "Stipple." + stippleIdx ); 775 src = createExternalGraphicFromStipple( stipple, label, fgColor, bgColor ); 776 } 777 778 double width = Double.parseDouble( (String) child.get( "OutlineWidth" ) ) + 1; 779 Stroke stroke = StyleFactory.createStroke( outLColor, width, outLOpacity, null, "mitre", "butt" ); 780 Fill fill = null; 781 if ( stippleIdx != null ) { 782 ExternalGraphic eg = StyleFactory.createExternalGraphic( "file:///" + src, "image/gif" ); 783 Graphic graph = StyleFactory.createGraphic( eg, null, opacity, 10, 0 ); 784 GraphicFill gf = StyleFactory.createGraphicFill( graph ); 785 fill = StyleFactory.createFill( fgColor, opacity, gf ); 786 } else { 787 fill = StyleFactory.createFill( fgColor, opacity ); 788 } 789 PolygonSymbolizer polySym = StyleFactory.createPolygonSymbolizer( stroke, fill ); 790 return StyleFactory.createRule( new Symbolizer[] { polySym }, label, label, "", null, filter, false, 0, 9E99 ); 791 } 792 793 /** 794 * creates a Style for a line symbol 795 * 796 * @param clss 797 * @return a Style for a line symbol 798 */ 799 private Rule createPolygonVecRule( Map clss, String filterCol ) 800 throws Exception { 801 802 if ( clss.get( "IsNoData" ) != null ) { 803 return null; 804 } 805 806 String label = (String) clss.get( "Label" ); 807 label = StringTools.validateString( label, "\"" ); 808 809 Filter filter = null; 810 if ( filterCol != null ) { 811 filter = createFilter( clss, filterCol ); 812 } 813 // get foreground color 814 double opacity = 1f; 815 Color fgColor = Color.BLACK; 816 817 // get color of the outlining stroke 818 double outLOpacity = 0f; 819 Color outLColor = Color.BLACK; 820 821 // create fill pattern as an image that will be referenced as 822 // external graphic 823 824 Stroke stroke = StyleFactory.createStroke( outLColor, 1, outLOpacity, null, "mitre", "butt" ); 825 Fill fill = StyleFactory.createFill( fgColor, opacity ); 826 PolygonSymbolizer polySym = StyleFactory.createPolygonSymbolizer( stroke, fill ); 827 return StyleFactory.createRule( new Symbolizer[] { polySym }, label, label, "", null, filter, false, 0, 9E99 ); 828 } 829 830 /** 831 * creates an image from a stipple and stores it as a gif image. The method returns the full 832 * name of the stored image. 833 * 834 * @param stipple 835 * @return an image from a stipple and stores it as a gif image. The method returns the full 836 * name of the stored image. 837 */ 838 private String createExternalGraphicFromStipple( Map stipple, String label, Color fg, Color bg ) 839 throws Exception { 840 841 if ( label != null ) { 842 label = label.replace( ' ', '_' ); 843 label = label.replace( '.', '_' ); 844 label = label.replace( ';', '_' ); 845 label = label.replace( ',', '_' ); 846 label = label.replace( '-', '_' ); 847 label = label.replace( ':', '_' ); 848 } 849 String tmp = (String) stipple.get( "Columns" ); 850 int cols = Integer.parseInt( tmp ); 851 tmp = (String) stipple.get( "Rows" ); 852 int rows = Integer.parseInt( tmp ); 853 854 List bList = (List) stipple.get( "Bits" ); 855 StringBuffer bStr = new StringBuffer( 1000 ); 856 for ( int i = 0; i < bList.size(); i++ ) { 857 String[] t = StringTools.toArray( ( (String) bList.get( i ) ).trim(), " ", false ); 858 for ( int j = 0; j < t.length; j++ ) { 859 bStr.append( t[j] ); 860 } 861 } 862 863 char[] ch = bStr.toString().toCharArray(); 864 865 BufferedImage bi = createFillPattern( cols, rows, ch, fg, bg ); 866 867 final String format = "gif"; 868 869 String[] t = StringTools.toArray( fileRootName, "/", false ); 870 String base = t[t.length - 1]; 871 StringBuffer fileName = new StringBuffer(); 872 fileName.append( targetDir ).append( base ); 873 if ( label != null ) { 874 fileName.append( "_" ).append( label ).append( "." ).append( format ); 875 } else { 876 fileName.append( "." ).append( format ); 877 } 878 879 // FileOutputStream fos = new FileOutputStream( targetDir + base + '_' + label + "." + 880 // format ); 881 FileOutputStream fos = new FileOutputStream( fileName.toString() ); 882 ImageUtils.saveImage( bi, fos, format, 1.0f ); 883 // Encoders.encodeGif(fos,bi); 884 fos.close(); 885 886 if ( targetDir.startsWith( "/" ) ) { 887 if ( label != null ) { 888 return targetDir.substring( 1, targetDir.length() ) + base + '_' + label + "." + format; 889 } else { 890 return targetDir.substring( 1, targetDir.length() ) + base + "." + format; 891 } 892 } else { 893 if ( label != null ) { 894 return targetDir + base + '_' + label + "." + format; 895 } else { 896 return targetDir + base + "." + format; 897 } 898 899 } 900 901 } 902 903 /** 904 * creates a <tt>BufferedImage</tt> using the stipple contained in the passed char array. 905 * 906 * @param cols 907 * @param rows 908 * @param ch 909 * @return a <tt>BufferedImage</tt> using the stipple contained in the passed char array. 910 */ 911 private BufferedImage createFillPattern( int cols, int rows, char[] ch, Color fg, Color bg ) { 912 BufferedImage bi = new BufferedImage( cols, rows, BufferedImage.TYPE_INT_ARGB ); 913 int cntChar = 0; 914 byte[] bTmp = null; 915 char chr = ' '; 916 int hexCnt = 0; 917 if ( cols % 8 != 0 ) { 918 hexCnt = ( cols / 8 + 1 ) * 8; 919 } else { 920 hexCnt = cols; 921 } 922 for ( int i = 0; i < rows; i++ ) { 923 for ( int j = 0; j < hexCnt; j++ ) { 924 if ( j % 4 == 0 ) { 925 chr = ch[cntChar++]; 926 bTmp = getBits( chr ); 927 } 928 if ( j < cols ) { 929 if ( bTmp[j % 4] == 0 ) { 930 if ( bg != null ) { 931 bi.setRGB( j, i, bg.getRGB() ); 932 } 933 } else { 934 bi.setRGB( j, i, fg.getRGB() ); 935 } 936 } 937 } 938 } 939 return bi; 940 } 941 942 private byte[] getBits( int ch ) { 943 switch ( ch ) { 944 case '0': 945 return new byte[] { 0, 0, 0, 0 }; 946 case '1': 947 return new byte[] { 0, 0, 0, 1 }; 948 case '2': 949 return new byte[] { 0, 0, 1, 0 }; 950 case '3': 951 return new byte[] { 0, 0, 1, 1 }; 952 case '4': 953 return new byte[] { 0, 1, 0, 0 }; 954 case '5': 955 return new byte[] { 0, 1, 0, 1 }; 956 case '6': 957 return new byte[] { 0, 1, 1, 0 }; 958 case '7': 959 return new byte[] { 0, 1, 1, 1 }; 960 case '8': 961 return new byte[] { 1, 0, 0, 0 }; 962 case '9': 963 return new byte[] { 1, 0, 0, 1 }; 964 case 'a': 965 return new byte[] { 1, 0, 1, 0 }; 966 case 'b': 967 return new byte[] { 1, 0, 1, 1 }; 968 case 'c': 969 return new byte[] { 1, 1, 0, 0 }; 970 case 'd': 971 return new byte[] { 1, 1, 0, 1 }; 972 case 'e': 973 return new byte[] { 1, 1, 1, 0 }; 974 case 'f': 975 return new byte[] { 1, 1, 1, 1 }; 976 default: 977 return new byte[] { 0, 0, 0, 0 }; 978 } 979 } 980 981 /** 982 * @return creates a dasharray for constructing a stroke from the pattern entries of a avl 983 * xxxSym. block 984 * 985 * @param pl 986 */ 987 private float[] createDashArray( List<String> pl ) { 988 int cnt = 0; 989 for ( int i = 0; i < pl.size(); i++ ) { 990 if ( Float.parseFloat( pl.get( i ) ) > 0 ) { 991 cnt++; 992 } else { 993 break; 994 } 995 } 996 float[] pattern = null; 997 if ( cnt > 0 ) { 998 pattern = new float[cnt]; 999 for ( int i = 0; i < pattern.length; i++ ) { 1000 pattern[i] = Float.parseFloat( pl.get( i ) ); 1001 } 1002 } 1003 return pattern; 1004 } 1005 1006 /** 1007 * creates a AWT color from a val color block 1008 * 1009 * @param color 1010 * @return a AWT color from a val color block 1011 */ 1012 private Color createColor( Map<String, Object> color ) { 1013 StringBuffer hex = new StringBuffer( "0x" ); 1014 if ( color != null && !"\"Transparent\"".equals( color.get( "Name" ) ) ) { 1015 String red = (String) color.get( "Red" ); 1016 if ( red == null ) 1017 red = "0x0000"; 1018 int c = (int) ( ( Integer.decode( red ).intValue() / 65535f ) * 255 ); 1019 if ( c < 15 ) { 1020 hex.append( 0 ); 1021 } 1022 hex.append( Integer.toHexString( c ) ); 1023 String green = (String) color.get( "Green" ); 1024 if ( green == null ) 1025 green = "0x0000"; 1026 c = (int) ( ( Integer.decode( green ).intValue() / 65535f ) * 255 ); 1027 if ( c < 15 ) { 1028 hex.append( 0 ); 1029 } 1030 hex.append( Integer.toHexString( c ) ); 1031 String blue = (String) color.get( "Blue" ); 1032 if ( blue == null ) 1033 blue = "0x0000"; 1034 c = (int) ( ( Integer.decode( blue ).intValue() / 65535f ) * 255 ); 1035 if ( c < 15 ) { 1036 hex.append( 0 ); 1037 } 1038 hex.append( Integer.toHexString( c ) ); 1039 } else { 1040 // hex.append("000000"); 1041 return null; 1042 } 1043 return Color.decode( hex.toString() ); 1044 } 1045 1046 private Filter createFilter( Map clss, String filterCol ) { 1047 1048 if ( clss.get( "Label" ) == null ) { 1049 return null; 1050 } 1051 Filter filter = null; 1052 1053 String maxN = (String) clss.get( "MaxStr" ); 1054 String minN = (String) clss.get( "MinStr" ); 1055 if ( maxN != null && minN != null ) { 1056 filter = createStringFilter( clss, filterCol ); 1057 } else { 1058 filter = createNumberFilter( clss, filterCol ); 1059 } 1060 1061 return filter; 1062 } 1063 1064 private Filter createStringFilter( Map clss, String filterCol ) { 1065 Filter filter; 1066 Operation oper = null; 1067 String maxN = (String) clss.get( "MaxStr" ); 1068 String minN = (String) clss.get( "MinStr" ); 1069 maxN = maxN.substring( 1, maxN.length() - 1 ); 1070 minN = minN.substring( 1, minN.length() - 1 ); 1071 if ( maxN.equals( minN ) ) { 1072 oper = new PropertyIsCOMPOperation( OperationDefines.PROPERTYISEQUALTO, 1073 new PropertyName( new QualifiedName( filterCol ) ), new Literal( minN ) ); 1074 } else { 1075 ArrayList<Operation> list = new ArrayList<Operation>(); 1076 Operation op = new PropertyIsCOMPOperation( OperationDefines.PROPERTYISLESSTHANOREQUALTO, 1077 new PropertyName( new QualifiedName( filterCol ) ), 1078 new Literal( maxN ) ); 1079 list.add( op ); 1080 op = new PropertyIsCOMPOperation( OperationDefines.PROPERTYISGREATERTHANOREQUALTO, 1081 new PropertyName( new QualifiedName( filterCol ) ), new Literal( minN ) ); 1082 list.add( op ); 1083 1084 oper = new LogicalOperation( OperationDefines.AND, list ); 1085 } 1086 filter = new ComplexFilter( oper ); 1087 return filter; 1088 } 1089 1090 private Filter createNumberFilter( Map clss, String filterCol ) { 1091 1092 String maxN = (String) clss.get( "MaxNum" ); 1093 String minN = (String) clss.get( "MinNum" ); 1094 if ( maxN == null ) { 1095 maxN = "9E99"; 1096 } 1097 if ( minN == null ) { 1098 minN = "-9E99"; 1099 } 1100 double t1 = Double.parseDouble( minN ); 1101 double t2 = Double.parseDouble( maxN ); 1102 Operation oper = null; 1103 if ( t1 == t2 ) { 1104 // if t1 == t2 no range is defined and so an 'is equal to' 1105 // opertaion must be used 1106 if ( ( (int) t1 ) == t1 ) { 1107 // if no significant fraction values are present 1108 // cast the value to int 1109 oper = new PropertyIsCOMPOperation( OperationDefines.PROPERTYISEQUALTO, 1110 new PropertyName( new QualifiedName( filterCol ) ), 1111 new Literal( "" + ( (int) t1 ) ) ); 1112 } else { 1113 oper = new PropertyIsCOMPOperation( OperationDefines.PROPERTYISEQUALTO, 1114 new PropertyName( new QualifiedName( filterCol ) ), 1115 new Literal( "" + t1 ) ); 1116 } 1117 } else { 1118 // if t1 != t2 range of valid values is defined and so an so two 1119 // operation (one for each border of the range) are used 1120 ArrayList<Operation> list = new ArrayList<Operation>(); 1121 Operation op = new PropertyIsCOMPOperation( OperationDefines.PROPERTYISLESSTHANOREQUALTO, 1122 new PropertyName( new QualifiedName( filterCol ) ), 1123 new Literal( maxN ) ); 1124 list.add( op ); 1125 op = new PropertyIsCOMPOperation( OperationDefines.PROPERTYISGREATERTHANOREQUALTO, 1126 new PropertyName( new QualifiedName( filterCol ) ), new Literal( minN ) ); 1127 list.add( op ); 1128 1129 oper = new LogicalOperation( OperationDefines.AND, list ); 1130 1131 } 1132 1133 return new ComplexFilter( oper ); 1134 1135 } 1136 1137 private static String[] getAVLFiles( String dir ) { 1138 File file = new File( dir ); 1139 return file.list( new DFileFilter() ); 1140 1141 } 1142 1143 private static void printHelp() { 1144 1145 System.out.println( "Converts ESRI *.avl files to OGC SLD documents. The current version of " ); 1146 System.out.println( "this tool isn't able to convert each and every construction that is " ); 1147 System.out.println( "possible with an *.avl file. But most of the common expressions will be mapped." ); 1148 System.out.println( "Supported are *.avl files for point, lines and polygons." ); 1149 System.out.println( "" ); 1150 System.out.println( "-sourceFile -> full path to the *.avl file to be converted. " ); 1151 System.out.println( " in the same directory a shapefile with the same name as the *.avl" ); 1152 System.out.println( " file must exist! (conditional, -sourceFile or -sourceDir must " ); 1153 System.out.println( " be defined)" ); 1154 System.out.println( "-sourceDir -> directory containing one or more *.avl files. for each existing" ); 1155 System.out.println( " *.avl file a shapefile with the same base name must exist. " ); 1156 System.out.println( " (conditional, -sourceFile or -sourceDir must be defined)" ); 1157 System.out.println( "-targetDir -> directory where the created SLD document(s) will be stored. (optional)" ); 1158 System.out.println( "-h -> print this help" ); 1159 } 1160 1161 /** 1162 * 1163 * @param args 1164 * @throws Exception 1165 */ 1166 public static void main( String[] args ) 1167 throws Exception { 1168 1169 Map<String, String> map = new HashMap<String, String>(); 1170 1171 for ( int i = 0; i < args.length; i += 2 ) { 1172 map.put( args[i], args[i + 1] ); 1173 } 1174 1175 if ( map.get( "-sourceFile" ) == null && map.get( "-sourceDir" ) == null && map.get( "-h" ) == null ) { 1176 System.out.println( "-sourceFile or -sourceDir must be defined!" ); 1177 System.out.println(); 1178 printHelp(); 1179 System.exit( 1 ); 1180 } 1181 1182 if ( map.get( "-h" ) != null ) { 1183 printHelp(); 1184 System.exit( 0 ); 1185 } 1186 1187 String targetDir = "."; 1188 String[] sourceFiles = null; 1189 if ( map.get( "-sourceFile" ) != null ) { 1190 sourceFiles = new String[] { map.get( "-sourceFile" ) }; 1191 // set the default target directory to the sourceFile's directory 1192 targetDir = sourceFiles[0].substring( 0, sourceFiles[0].lastIndexOf( "/" ) ); 1193 } 1194 1195 if ( sourceFiles == null ) { 1196 String sourceDir = map.get( "-sourceDir" ); 1197 sourceFiles = getAVLFiles( sourceDir ); 1198 for ( int i = 0; i < sourceFiles.length; i++ ) { 1199 sourceFiles[i] = sourceDir + '/' + sourceFiles[i]; 1200 } 1201 // set the default target directory to the source directory 1202 targetDir = sourceDir; 1203 1204 } 1205 1206 // String targetDir = "."; 1207 if ( map.get( "-targetDir" ) != null ) { 1208 targetDir = map.get( "-targetDir" ); 1209 } 1210 1211 for ( int i = 0; i < sourceFiles.length; i++ ) { 1212 System.out.println( "processing: " + sourceFiles[i] ); 1213 int pos = sourceFiles[i].lastIndexOf( '.' ); 1214 String file = sourceFiles[i].substring( 0, pos ); 1215 AVL2SLD avl = new AVL2SLD( file, targetDir ); 1216 avl.read(); 1217 StyledLayerDescriptor sld = avl.getStyledLayerDescriptor(); 1218 String[] t = StringTools.toArray( file, "/", false ); 1219 FileWriter fos = new FileWriter( targetDir + '/' + t[t.length - 1] + ".xml" ); 1220 fos.write( ( (Marshallable) sld ).exportAsXML() ); 1221 fos.close(); 1222 } 1223 } 1224 1225 /** 1226 * 1227 * 1228 * @version $Revision: 29436 $ 1229 * @author <a href="mailto:poth@lat-lon.de">Andreas Poth</a> 1230 */ 1231 private static class DFileFilter implements FilenameFilter { 1232 /** 1233 * @param f 1234 * @return true if accepted 1235 */ 1236 public boolean accept( File f, String name ) { 1237 int pos = name.lastIndexOf( "." ); 1238 String ext = name.substring( pos + 1 ); 1239 return ext.toUpperCase().equals( "AVL" ); 1240 } 1241 } 1242 1243 }