001 // $HeadURL: svn+ssh://rbezema@svn.wald.intevation.org/deegree/base/tags/2.1/src/org/deegree/ogcwebservices/wcs/WCService.java $
002 /*---------------- FILE HEADER ------------------------------------------
003
004 This file is part of deegree.
005 Copyright (C) 2001-2007 by:
006 EXSE, Department of Geography, University of Bonn
007 http://www.giub.uni-bonn.de/deegree/
008 lat/lon GmbH
009 http://www.lat-lon.de
010
011 This library is free software; you can redistribute it and/or
012 modify it under the terms of the GNU Lesser General Public
013 License as published by the Free Software Foundation; either
014 version 2.1 of the License, or (at your option) any later version.
015
016 This library is distributed in the hope that it will be useful,
017 but WITHOUT ANY WARRANTY; without even the implied warranty of
018 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
019 Lesser General Public License for more details.
020
021 You should have received a copy of the GNU Lesser General Public
022 License along with this library; if not, write to the Free Software
023 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
024
025 Contact:
026
027 Andreas Poth
028 lat/lon GmbH
029 Aennchenstr. 19
030 53115 Bonn
031 Germany
032 E-Mail: poth@lat-lon.de
033
034 Prof. Dr. Klaus Greve
035 Department of Geography
036 University of Bonn
037 Meckenheimer Allee 166
038 53115 Bonn
039 Germany
040 E-Mail: greve@giub.uni-bonn.de
041
042 ---------------------------------------------------------------------------*/
043 package org.deegree.ogcwebservices.wcs;
044
045 import java.awt.image.BufferedImage;
046 import java.io.IOException;
047 import java.io.InputStream;
048 import java.net.URL;
049 import java.util.ArrayList;
050 import java.util.List;
051 import java.util.Properties;
052
053 import org.deegree.datatypes.parameter.GeneralParameterValueIm;
054 import org.deegree.datatypes.parameter.OperationParameterIm;
055 import org.deegree.framework.log.ILogger;
056 import org.deegree.framework.log.LoggerFactory;
057 import org.deegree.framework.util.StringTools;
058 import org.deegree.io.JDBCConnection;
059 import org.deegree.io.oraclegeoraster.GeoRasterDescription;
060 import org.deegree.model.coverage.Coverage;
061 import org.deegree.model.coverage.grid.AbstractGridCoverage;
062 import org.deegree.model.coverage.grid.Format;
063 import org.deegree.model.coverage.grid.GridCoverageExchange;
064 import org.deegree.model.coverage.grid.GridCoverageReader;
065 import org.deegree.model.coverage.grid.ImageGridCoverage;
066 import org.deegree.model.crs.CRSException;
067 import org.deegree.model.crs.CRSTransformationException;
068 import org.deegree.model.crs.GeoTransformer;
069 import org.deegree.model.crs.IGeoTransformer;
070 import org.deegree.model.spatialschema.Envelope;
071 import org.deegree.ogcwebservices.InvalidParameterValueException;
072 import org.deegree.ogcwebservices.OGCWebService;
073 import org.deegree.ogcwebservices.OGCWebServiceException;
074 import org.deegree.ogcwebservices.OGCWebServiceRequest;
075 import org.deegree.ogcwebservices.getcapabilities.OGCCapabilities;
076 import org.deegree.ogcwebservices.wcs.configuration.Directory;
077 import org.deegree.ogcwebservices.wcs.configuration.DirectoryResolution;
078 import org.deegree.ogcwebservices.wcs.configuration.Extension;
079 import org.deegree.ogcwebservices.wcs.configuration.File;
080 import org.deegree.ogcwebservices.wcs.configuration.FileResolution;
081 import org.deegree.ogcwebservices.wcs.configuration.OracleGeoRasterResolution;
082 import org.deegree.ogcwebservices.wcs.configuration.Resolution;
083 import org.deegree.ogcwebservices.wcs.configuration.Shape;
084 import org.deegree.ogcwebservices.wcs.configuration.ShapeResolution;
085 import org.deegree.ogcwebservices.wcs.configuration.WCSConfiguration;
086 import org.deegree.ogcwebservices.wcs.describecoverage.CoverageDescription;
087 import org.deegree.ogcwebservices.wcs.describecoverage.CoverageOffering;
088 import org.deegree.ogcwebservices.wcs.describecoverage.DescribeCoverage;
089 import org.deegree.ogcwebservices.wcs.describecoverage.InvalidCoverageDescriptionExcpetion;
090 import org.deegree.ogcwebservices.wcs.getcapabilities.ContentMetadata;
091 import org.deegree.ogcwebservices.wcs.getcapabilities.WCSGetCapabilities;
092 import org.deegree.ogcwebservices.wcs.getcapabilities.WCSRequestValidator;
093 import org.deegree.ogcwebservices.wcs.getcoverage.GetCoverage;
094 import org.deegree.ogcwebservices.wcs.getcoverage.ResultCoverage;
095 import org.deegree.ogcwebservices.wcs.getcoverage.SpatialSubset;
096 import org.xml.sax.SAXException;
097
098 /**
099 * @version $Revision: 7788 $
100 * @author <a href="mailto:poth@lat-lon.de">Andreas Poth </a>
101 * @author last edited by: $Author: apoth $
102 *
103 * @version 1.0. $Revision: 7788 $, $Date: 2007-07-19 17:21:54 +0200 (Do, 19 Jul 2007) $
104 *
105 * @since 2.0
106 */
107
108 public class WCService implements OGCWebService {
109
110 private static final ILogger LOG = LoggerFactory.getLogger( WCService.class );
111
112 private int nor = 5;
113
114 private int degree = 3;
115
116 /**
117 *
118 */
119 private WCSConfiguration configuration = null;
120
121 /**
122 * creates a WCService from a configuration
123 *
124 * @param configuration
125 */
126 public WCService( WCSConfiguration configuration ) {
127 this.configuration = configuration;
128 URL url = WCService.class.getResource( "crstransform.properties" );
129 Properties props = new Properties();
130 try {
131 InputStream is = url.openStream();
132 props.load( is );
133 is.close();
134 nor = Integer.parseInt( props.getProperty( "number_of_reference_points" ) );
135 degree = Integer.parseInt( props.getProperty( "degree" ) );
136 } catch ( Exception e ) {
137 LOG.logError( e.getMessage(), e );
138 LOG.logInfo( "could not load definiton for crs transformation parameters, use default values" );
139 }
140 }
141
142 /**
143 * returns the capabilities of the WCS
144 *
145 * @return capabilities of the WCS
146 */
147 public OGCCapabilities getCapabilities() {
148 return configuration;
149 }
150
151 /**
152 * @param request
153 * @return a CoverageDescription fitting the request
154 * @throws OGCWebServiceException
155 * if an exception occurs in the process of creating the description.
156 */
157 private CoverageDescription describeCoverage( DescribeCoverage request )
158 throws OGCWebServiceException {
159
160 WCSRequestValidator.validate( configuration, request );
161 CoverageOffering[] co = null;
162 try {
163 co = getCoverageOfferings( request );
164 } catch ( IOException ioe ) {
165 LOG.logError( StringTools.stackTraceToString( ioe ) );
166 throw new OGCWebServiceException( ioe.getMessage() );
167 } catch ( SAXException saxe ) {
168 LOG.logError( StringTools.stackTraceToString( saxe ) );
169 throw new OGCWebServiceException( saxe.getMessage() );
170 }
171 CoverageDescription cd = new CoverageDescription( co, request.getVersion() );
172 return cd;
173 }
174
175 /**
176 * @param request
177 * @return a given Coverage for the request
178 * @throws OGCWebServiceException
179 * if any kind of exception occurs
180 */
181 private Coverage getCoverage( GetCoverage request )
182 throws OGCWebServiceException {
183
184 WCSRequestValidator.validate( configuration, request );
185 Coverage cov = null;
186 if ( request.getOutput().getFormat().getCode().equals( "GML" ) ) {
187 CoverageOffering co;
188 try {
189 co = getCoverageOffering( request );
190 } catch ( InvalidCoverageDescriptionExcpetion e ) {
191 LOG.logError( "CoverageDescription is not valid", e );
192 throw new OGCWebServiceException( getClass().getName(), "CoverageDescription is not valid: "
193 + e.getMessage() );
194 } catch ( IOException e ) {
195 LOG.logError( "could not read CoverageDescription", e );
196 throw new OGCWebServiceException( getClass().getName(), "could not read CoverageDescription: "
197 + e.getMessage() );
198 } catch ( SAXException e ) {
199 LOG.logError( "could not parse CoverageDescription", e );
200 throw new OGCWebServiceException( getClass().getName(), "could not parse CoverageDescription: "
201 + e.getMessage() );
202 }
203 Envelope env = request.getDomainSubset().getSpatialSubset().getEnvelope();
204 BufferedImage bi = new BufferedImage( 2, 2, BufferedImage.TYPE_INT_ARGB );
205 cov = new ImageGridCoverage( co, env, bi );
206 } else {
207 cov = readCoverage( request );
208 }
209
210 return cov;
211 }
212
213 /**
214 * method for event based request procrssing
215 *
216 * @param request
217 * object containing the request.
218 * @return depending on the request one of, {@link WCSGetCapabilities}, {@link GetCoverage} or
219 * {@link DescribeCoverage}
220 */
221 public Object doService( OGCWebServiceRequest request )
222 throws OGCWebServiceException {
223
224 Object response = null;
225 if ( request instanceof WCSGetCapabilities ) {
226 WCSRequestValidator.validate( configuration, request );
227 response = getCapabilities();
228 } else if ( request instanceof GetCoverage ) {
229 Coverage cov = getCoverage( (GetCoverage) request );
230 response = new ResultCoverage( cov, cov.getClass(), ( (GetCoverage) request ).getOutput().getFormat(),
231 (GetCoverage) request );
232 } else if ( request instanceof DescribeCoverage ) {
233 response = describeCoverage( (DescribeCoverage) request );
234 }
235 return response;
236 }
237
238 /**
239 * returns the <tt>CoverageOffering</tt> s according to the coverages names contained in the
240 * passed request. If the request doesn't contain one or more named coverage
241 * <tt>CoverageOffering</tt> s for all coverages known by the WCS will be returned.
242 *
243 * @param request
244 * DescribeCoverage request
245 * @return the configured coverings
246 * @throws IOException
247 * @throws SAXException
248 * @throws InvalidCoverageDescriptionExcpetion
249 */
250 private CoverageOffering[] getCoverageOfferings( DescribeCoverage request )
251 throws IOException, SAXException, InvalidCoverageDescriptionExcpetion {
252
253 String[] coverages = request.getCoverages();
254 CoverageOffering[] co = null;
255 ContentMetadata cm = configuration.getContentMetadata();
256 if ( coverages.length == 0 ) {
257 // get descriptions of all coverages
258 CoverageOfferingBrief[] cob = cm.getCoverageOfferingBrief();
259 co = new CoverageOffering[cob.length];
260 for ( int i = 0; i < cob.length; i++ ) {
261 URL url = cob[i].getConfiguration();
262 CoverageDescription cd = CoverageDescription.createCoverageDescription( url );
263 co[i] = cd.getCoverageOffering( cob[i].getName() );
264 }
265 } else {
266 // get descriptions of all requested coverages
267 co = new CoverageOffering[coverages.length];
268 for ( int i = 0; i < coverages.length; i++ ) {
269 CoverageOfferingBrief cob = cm.getCoverageOfferingBrief( coverages[i] );
270 URL url = cob.getConfiguration();
271 CoverageDescription cd = CoverageDescription.createCoverageDescription( url );
272 co[i] = cd.getCoverageOffering( cob.getName() );
273 }
274 }
275
276 return co;
277 }
278
279 /**
280 * The method reads and returns the coverage described by the passed request.
281 *
282 * @param request
283 * @return a Coverage read from the given resolution
284 * @throws InvalidCoverageDescriptionExcpetion
285 */
286 private Coverage readCoverage( GetCoverage request )
287 throws InvalidCoverageDescriptionExcpetion, InvalidParameterValueException,
288 OGCWebServiceException {
289
290 Coverage result = null;
291
292 try {
293 CoverageOffering co = getCoverageOffering( request );
294
295 Resolution[] resolutions = getResolutions( co, request );
296 if ( resolutions == null || resolutions.length == 0 ) {
297 throw new InvalidParameterValueException(
298 "No data source defined the requested combination of spatial resolution and ranges" );
299 }
300 GridCoverageReader reader = null;
301 LOG.logDebug( "getting responsible GridCoverageReader" );
302 if ( resolutions[0] instanceof FileResolution ) {
303 reader = getFileReader( resolutions, co, request );
304 } else if ( resolutions[0] instanceof ShapeResolution ) {
305 reader = getShapeReader( resolutions, co, request );
306 } else if ( resolutions[0] instanceof DirectoryResolution ) {
307 reader = getDirectoryReader( resolutions, co, request );
308 } else if ( resolutions[0] instanceof OracleGeoRasterResolution ) {
309 reader = getOracleGeoRasterReader( resolutions, co, request );
310 }
311
312 LOG.logDebug( "resolution reader: " + resolutions[0] );
313 LOG.logDebug( "found reader: " + reader.getClass() );
314 List<GeneralParameterValueIm> list = new ArrayList<GeneralParameterValueIm>( 20 );
315 Envelope size = (Envelope) request.getDomainSubset().getSpatialSubset().getGrid();
316 OperationParameterIm op = new OperationParameterIm( "width", null, new Integer( (int) size.getWidth() + 1 ) );
317 list.add( new GeneralParameterValueIm( op ) );
318 op = new OperationParameterIm( "height", null, new Integer( (int) size.getHeight() + 1 ) );
319 list.add( new GeneralParameterValueIm( op ) );
320 GeneralParameterValueIm[] gpvs = new GeneralParameterValueIm[list.size()];
321 result = reader.read( list.toArray( gpvs ) );
322 if ( result == null ) {
323 throw new InvalidCoverageDescriptionExcpetion( "Couldn't read a coverage for the requested resolution and/or area" );
324 }
325 LOG.logDebug( "found result: " + result );
326
327 // transform Coverage into another CRS if required
328 String crs = request.getOutput().getCrs().getCode();
329 if ( crs == null ) {
330 crs = request.getDomainSubset().getRequestSRS().getCode();
331 }
332 if ( !crs.equalsIgnoreCase( co.getSupportedCRSs().getNativeSRSs()[0].getCodes()[0] ) ) {
333 LOG.logDebug( "transforming coverage to " + crs );
334 IGeoTransformer gt = new GeoTransformer( crs );
335 result = gt.transform( (AbstractGridCoverage) result, nor, degree, null );
336 }
337
338 } catch ( IOException e ) {
339 LOG.logError( e.getMessage(), e );
340 throw new OGCWebServiceException( e.getMessage() );
341 } catch ( SAXException e ) {
342 LOG.logError( e.getMessage(), e );
343 throw new OGCWebServiceException( e.getMessage() );
344 } catch ( CRSTransformationException e ) {
345 LOG.logError( e.getMessage(), e );
346 throw new OGCWebServiceException( e.getMessage() );
347 } catch (CRSException e) {
348 LOG.logError( e.getMessage(), e );
349 throw new OGCWebServiceException( e.getMessage() );
350 }
351 return result;
352 }
353
354 /**
355 * returns the <tt>CoverageOffering</tt> describing the access to the data sources behind the
356 * requested coverage
357 *
358 * @param request
359 * GetCoverage request
360 * @return the Coverage Offering fitting the request
361 * @throws IOException
362 * @throws SAXException
363 * @throws InvalidCoverageDescriptionExcpetion
364 */
365 private CoverageOffering getCoverageOffering( GetCoverage request )
366 throws IOException, SAXException, InvalidCoverageDescriptionExcpetion {
367
368 ContentMetadata cm = configuration.getContentMetadata();
369 CoverageOfferingBrief cob = cm.getCoverageOfferingBrief( request.getSourceCoverage() );
370 URL url = cob.getConfiguration();
371 CoverageDescription cd = CoverageDescription.createCoverageDescription( url );
372 return cd.getCoverageOffering( request.getSourceCoverage() );
373 }
374
375 /**
376 * returns the <tt>Resolution</tt> s matching the scale, region and range parameters of the
377 * passed request
378 *
379 * @param co
380 * @param request
381 * @return the <tt>Resolution</tt> s matching the scale, region and range parameters of the
382 * passed request
383 * @throws CRSException
384 * @throws CRSTransformationException
385 */
386 private Resolution[] getResolutions( CoverageOffering co, GetCoverage request ) throws CRSException, CRSTransformationException {
387
388 Extension extension = co.getExtension();
389 SpatialSubset sps = request.getDomainSubset().getSpatialSubset();
390 // determine resolution of the requested coverage
391 Envelope env = calculateRequestEnvelope( request, co.getSupportedCRSs().getNativeSRSs()[0].getCodes()[0] );
392 Envelope grid = (Envelope) sps.getGrid();
393 double qx = env.getWidth() / grid.getWidth();
394 double qy = env.getHeight() / grid.getHeight();
395 double reso = qx;
396 // if x- and y-direction has different resolution in the GetCoverage
397 // request use the finest
398 if ( qy < qx ) {
399 reso = qy;
400 }
401 Resolution[] res = extension.getResolutions( reso );
402
403 return res;
404 }
405
406 /**
407 * returns a <tt>GridCoverageReader</tt> for accessing the data source of the target coverage
408 * of the passed GetCoverage request. The reader will be constructed from all <tt>File</tt> s
409 * matching the filter conditions defined in the passed GeCoverage request. <BR>
410 * At the moment just the first field of the passed <tt>Resolution</tt> array will be
411 * considered!
412 *
413 * @param resolutions
414 * <tT>Resolution</tt> to get a reader for
415 * @param co
416 * description of the requested coverage
417 * @param request
418 * @return <tt>GridCoverageReader</tt>
419 * @throws IOException
420 * @throws CRSException
421 * @throws CRSTransformationException
422 */
423 private GridCoverageReader getFileReader( Resolution[] resolutions, CoverageOffering co, GetCoverage request )
424 throws IOException, InvalidParameterValueException, CRSException, CRSTransformationException {
425
426 String nativeCRS = co.getSupportedCRSs().getNativeSRSs()[0].getCodes()[0];
427 // calculates the envevole to be used by the created GridCoverageReader
428 Envelope envelope = calculateRequestEnvelope( request, nativeCRS );
429
430 File[] files = ( (FileResolution) resolutions[0] ).getFiles();
431 List<File> list = new ArrayList<File>();
432 for ( int i = 0; i < files.length; i++ ) {
433 Envelope fileEnv = files[i].getEnvelope();
434 if ( fileEnv.intersects( envelope ) ) {
435 list.add( files[i] );
436 }
437 }
438 files = list.toArray( new File[list.size()] );
439
440 GridCoverageExchange gce = new GridCoverageExchange( null );
441 Format format = new Format( co.getSupportedFormats().getNativeFormat() );
442 GridCoverageReader reader = gce.getReader( files, co, envelope, format );
443
444 return reader;
445 }
446
447 /**
448 * returns a <tt>GridCoverageReader</tt> for accessing the data source of the target coverage
449 * of the passed GetCoverage request. The reader will be constructed from all <tt>Shape</tt> s
450 * matching the filter conditions defined in the passed GeCoverage request. At least this should
451 * be just one! <BR>
452 * At the moment just the first field of the passed <tt>Resolution</tt> array will be
453 * considered!
454 *
455 * @param resolutions
456 * @param co
457 * @param request
458 * @return a GridCoverageReader which is able to read shape files.
459 * @throws IOException
460 * @throws CRSException
461 * @throws CRSTransformationException
462 */
463 private GridCoverageReader getShapeReader( Resolution[] resolutions, CoverageOffering co, GetCoverage request )
464 throws IOException, InvalidParameterValueException, CRSException, CRSTransformationException {
465
466 String nativeCRS = co.getSupportedCRSs().getNativeSRSs()[0].getCodes()[0];
467 // calculates the envevole to be used by the created GridCoverageReader
468 Envelope envelope = calculateRequestEnvelope( request, nativeCRS );
469
470 Shape shape = ( (ShapeResolution) resolutions[0] ).getShape();
471
472 GridCoverageExchange gce = new GridCoverageExchange( null );
473 Format format = new Format( co.getSupportedFormats().getNativeFormat() );
474 return gce.getReader( shape, co, envelope, format );
475
476 }
477
478 /**
479 * returns a <tt>GridCoverageReader</tt> for accessing the data source of the target coverage
480 * of the passed GetCoverage request. The reader will be constructed from all <tt>Directory</tt>
481 * s matching the filter conditions defined in the passed GeCoverage request. At least this
482 * should be just one! <BR>
483 * At the moment just the first field of the passed <tt>Resolution</tt> array will be
484 * considered!
485 *
486 * @param resolutions
487 * @param co
488 * @param request
489 * @return the GridCoverageReader which reads directories
490 * @throws IOException
491 * @throws CRSException
492 * @throws CRSTransformationException
493 */
494 private GridCoverageReader getDirectoryReader( Resolution[] resolutions, CoverageOffering co, GetCoverage request )
495 throws IOException, InvalidParameterValueException, CRSException, CRSTransformationException {
496
497 String nativeCRS = co.getSupportedCRSs().getNativeSRSs()[0].getCodes()[0];
498 // calculates the envevole to be used by the created GridCoverageReader
499 Envelope envelope = calculateRequestEnvelope( request, nativeCRS );
500
501 Directory[] dirs = ( (DirectoryResolution) resolutions[0] ).getDirectories( envelope );
502
503 GridCoverageExchange gce = new GridCoverageExchange( null );
504 Format format = new Format( co.getSupportedFormats().getNativeFormat() );
505
506 GridCoverageReader reader = gce.getReader( dirs, co, envelope, format );
507
508 return reader;
509 }
510
511 /**
512 * returns a <tt>GridCoverageReader</tt> for accessing the data source of the target coverage
513 * of the passed GetCoverage request. The reader will be constructed from the JDBCV connnection
514 * defined in the CoverageDescription extension.<BR>
515 * At the moment just the first field of the passed <tt>Resolution</tt> array will be
516 * considered!
517 *
518 * @param resolutions
519 * @param co
520 * @param request
521 * @return a <tt>GridCoverageReader</tt>.
522 * @throws InvalidParameterValueException
523 * @throws IOException
524 * @throws CRSException
525 * @throws CRSTransformationException
526 */
527 private GridCoverageReader getOracleGeoRasterReader( Resolution[] resolutions, CoverageOffering co,
528 GetCoverage request )
529 throws InvalidParameterValueException, IOException, CRSException, CRSTransformationException {
530
531 String nativeCRS = co.getSupportedCRSs().getNativeSRSs()[0].getCodes()[0];
532 // calculates the envevole to be used by the created GridCoverageReader
533 Envelope envelope = calculateRequestEnvelope( request, nativeCRS );
534
535 JDBCConnection jdbc = ( (OracleGeoRasterResolution) resolutions[0] ).getJDBCConnection();
536 String table = ( (OracleGeoRasterResolution) resolutions[0] ).getTable();
537 String rdtTable = ( (OracleGeoRasterResolution) resolutions[0] ).getRdtTable();
538 String column = ( (OracleGeoRasterResolution) resolutions[0] ).getColumn();
539 String identification = ( (OracleGeoRasterResolution) resolutions[0] ).getIdentification();
540 int level = ( (OracleGeoRasterResolution) resolutions[0] ).getLevel();
541 GeoRasterDescription grd = new GeoRasterDescription( jdbc, table, rdtTable, column, identification, level );
542
543 GridCoverageExchange gce = new GridCoverageExchange( null );
544 Format format = new Format( co.getSupportedFormats().getNativeFormat() );
545
546 return gce.getReader( grd, co, envelope, format );
547
548 }
549
550 /**
551 * According to WCS 1.0.0 the CRS of the GetCoverage request BBOX can be different to the
552 * desired CRS of the resulting coverage. This method transforms the request CRS to the output
553 * CRS if requiered. At the moment deegree WCS doesn't support transformation of grid coverages
554 * so the output CRS will always be the native CRS of te data.
555 *
556 * @param request
557 * @param nativeCrs
558 * @return a boundingbox of the request
559 * @throws CRSTransformationException
560 * @throws CRSException
561 */
562 private Envelope calculateRequestEnvelope( GetCoverage request, String nativeCrs )
563 throws CRSException, CRSTransformationException {
564
565 SpatialSubset spsu = request.getDomainSubset().getSpatialSubset();
566 Envelope envelope = spsu.getEnvelope();
567
568 String reqCrs = request.getDomainSubset().getRequestSRS().getCode();
569
570 IGeoTransformer gt = new GeoTransformer( nativeCrs );
571 return gt.transform( envelope, reqCrs );
572
573 }
574
575 }