Statistics
| Revision:

gvsig-gdal / trunk / org.gvsig.gdal / org.gvsig.gdal.prov / org.gvsig.gdal.prov.ogr / src / main / java / org / gvsig / gdal / prov / ogr / OGRDataStoreProvider.java @ 1261

History | View | Annotate | Download (23.6 KB)

1
/**
2
 * gvSIG. Desktop Geographic Information System.
3
 *
4
 * Copyright ? 2007-2016 gvSIG Association
5
 *
6
 * This program is free software; you can redistribute it and/or
7
 * modify it under the terms of the GNU General Public License
8
 * as published by the Free Software Foundation; either version 2
9
 * of the License, or (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU General Public License
17
 * along with this program; if not, write to the Free Software
18
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
19
 * MA  02110-1301, USA.
20
 *
21
 * For any additional information, do not hesitate to contact us
22
 * at info AT gvsig.com, or visit our website www.gvsig.com.
23
 */
24
package org.gvsig.gdal.prov.ogr;
25

    
26
import java.util.ArrayList;
27
import java.util.Iterator;
28
import java.util.List;
29

    
30
import org.apache.commons.lang3.StringUtils;
31
import org.gdal.ogr.DataSource;
32
import org.gdal.ogr.Feature;
33
import org.gdal.ogr.FeatureDefn;
34
import org.gdal.ogr.FieldDefn;
35
import org.gdal.ogr.GeomFieldDefn;
36
import org.gdal.ogr.Geometry;
37
import org.gdal.ogr.Layer;
38
import org.gdal.ogr.ogr;
39
import org.gdal.ogr.ogrConstants;
40

    
41
import org.gvsig.fmap.dal.DataStore;
42
import org.gvsig.fmap.dal.DataStoreParameters;
43
import org.gvsig.fmap.dal.DataTypes;
44
import org.gvsig.fmap.dal.FileHelper;
45
import org.gvsig.fmap.dal.exception.DataException;
46
import org.gvsig.fmap.dal.exception.InitializeException;
47
import org.gvsig.fmap.dal.exception.OpenException;
48
import org.gvsig.fmap.dal.exception.ReadRuntimeException;
49
import org.gvsig.fmap.dal.feature.EditableFeatureAttributeDescriptor;
50
import org.gvsig.fmap.dal.feature.FeatureAttributeDescriptor;
51
import org.gvsig.fmap.dal.feature.FeatureQuery;
52
import org.gvsig.fmap.dal.feature.FeatureQueryOrder;
53
import org.gvsig.fmap.dal.feature.FeatureQueryOrder.FeatureQueryOrderMember;
54
import org.gvsig.fmap.dal.feature.FeatureType;
55
import org.gvsig.fmap.dal.feature.FeatureType.FeatureTypeChanged;
56
import org.gvsig.fmap.dal.feature.spi.AbstractFeatureStoreProvider;
57
import org.gvsig.fmap.dal.feature.spi.DefaultFeatureProvider;
58
import org.gvsig.fmap.dal.feature.spi.FeatureProvider;
59
import org.gvsig.fmap.dal.feature.spi.FeatureReferenceProviderServices;
60
import org.gvsig.fmap.dal.feature.spi.FeatureSetProvider;
61
import org.gvsig.fmap.dal.feature.spi.FeatureStoreProvider;
62
import org.gvsig.fmap.dal.resource.ResourceAction;
63
import org.gvsig.fmap.dal.resource.file.FileResource;
64
import org.gvsig.fmap.dal.resource.spi.ResourceConsumer;
65
import org.gvsig.fmap.dal.resource.spi.ResourceProvider;
66
import org.gvsig.fmap.dal.spi.DataStoreProviderServices;
67
import org.gvsig.fmap.geom.Geometry.SUBTYPES;
68
import org.gvsig.fmap.geom.GeometryLocator;
69
import org.gvsig.fmap.geom.primitive.Envelope;
70
import org.gvsig.fmap.geom.type.GeometryTypeNotSupportedException;
71
import org.gvsig.fmap.geom.type.GeometryTypeNotValidException;
72
import org.gvsig.tools.dynobject.DynObject;
73
import org.gvsig.tools.evaluator.Evaluator;
74
import org.gvsig.tools.exception.BaseException;
75

    
76
import org.slf4j.Logger;
77
import org.slf4j.LoggerFactory;
78

    
79
/**
80
 *
81
 * @author <a href="mailto:lmarques@disid.com">Lluis Marques</a>
82
 *
83
 */
84
public class OGRDataStoreProvider extends AbstractFeatureStoreProvider implements
85
    FeatureStoreProvider, ResourceConsumer {
86

    
87
    private static final Logger LOG = LoggerFactory.getLogger(OGRDataStoreProvider.class);
88

    
89
    /**
90
     *
91
     */
92
    public static final String METADATA_DEFINITION_NAME = "OGRDataStoreProvider";
93

    
94
    /**
95
     *
96
     */
97
    public static final String NAME = "OGRDataStoreProvider";
98

    
99
    /**
100
     *
101
     */
102
    public static final String DESCRIPTION = "OGR provider to open vectorial resources";
103

    
104
    protected DataSource dataSource;
105

    
106
    private Envelope envelope;
107

    
108
    private Layer newLayer;
109

    
110
    protected ResourceProvider resourceProvider;
111

    
112
    private Boolean updateSupport;
113

    
114
    private boolean opened = false;
115

    
116
    protected OGRDataStoreProvider(DataStoreParameters dataParameters,
117
        DataStoreProviderServices storeServices, DynObject metadata) throws InitializeException {
118
        super(dataParameters, storeServices, metadata);
119

    
120
        // Set CRS parameter to metadata
121
        this.setDynValue(DataStore.METADATA_CRS, dataParameters.getDynValue(DataStore.METADATA_CRS));
122

    
123
        getResource().addConsumer(this);
124

    
125
        try {
126
            this.open();
127
        } catch (OpenException e) {
128
            throw new InitializeException(NAME, e);
129
        }
130
    }
131

    
132
    protected OGRDataStoreProvider(DataStoreParameters dataParameters,
133
        DataStoreProviderServices storeServices) throws InitializeException {
134
        this(dataParameters, storeServices, FileHelper
135
            .newMetadataContainer(METADATA_DEFINITION_NAME));
136
    }
137

    
138
    /*
139
     * Lazy initialization of data source
140
     */
141
    protected synchronized DataSource getDataSource() throws OGRUnsupportedFormatException {
142
        if (this.dataSource == null) {
143

    
144
            // Prioritize connection string over file
145
            if (StringUtils.isNotBlank(getOGRParameters().getConnectionString())) {
146

    
147
                // Trying to open in update mode
148
                this.dataSource = ogr.Open(getOGRParameters().getConnectionString(), 1);
149

    
150
                if (this.dataSource == null) {
151
                    this.dataSource = ogr.Open(getOGRParameters().getConnectionString());
152
                    updateSupport = false;
153
                } else {
154
                    updateSupport = true;
155
                }
156

    
157
            } else if (getOGRParameters().getFile() != null
158
                && getOGRParameters().getFile().exists()) {
159

    
160
                // Trying to open in update mode
161
                this.dataSource = ogr.Open(getOGRParameters().getFile().getAbsolutePath(), 1);
162

    
163
                if (this.dataSource == null) {
164
                    this.dataSource = ogr.Open(getOGRParameters().getFile().getAbsolutePath());
165
                    updateSupport = false;
166
                } else {
167
                    updateSupport = true;
168
                }
169

    
170
            } else {
171
                throw new IllegalStateException(
172
                    "Invalid parameters. Connection string must not be blank or file must exists");
173
            }
174
        }
175

    
176
        if (this.dataSource == null) {
177

    
178
            if (StringUtils.isNotBlank(getOGRParameters().getConnectionString())) {
179
                throw new OGRUnsupportedFormatException(getOGRParameters().getConnectionString());
180
            }
181
        }
182

    
183
        return this.dataSource;
184
    }
185

    
186
    /*
187
     * Lazy initialization of update support flag
188
     */
189
    private Boolean hasUpdateSupport() throws OGRUnsupportedFormatException {
190
        if (this.updateSupport == null) {
191
            getDataSource();
192
        }
193
        return this.updateSupport;
194
    }
195

    
196
    /*
197
     * Lazy initialization of layer
198
     */
199
    protected Layer getLayer() throws OGRUnsupportedFormatException {
200
        if (this.newLayer == null) {
201
            String layerName = getOGRParameters().getLayerName();
202
            if(StringUtils.isBlank(layerName)){
203
                this.newLayer = getDataSource().GetLayer(0);
204
                getOGRParameters().setLayerName(this.newLayer.GetName());
205
            } else {
206
                this.newLayer = getDataSource().GetLayer(layerName);
207
                if (this.newLayer == null) {
208
                    LOG.warn("Can not get layer with {} name. Get first layer of data source",
209
                        getOGRParameters().getLayerName());
210
                    this.newLayer = getDataSource().GetLayer(0);
211
                    getOGRParameters().setLayerName(this.newLayer.GetName());
212
                }
213
            }
214
        }
215
        return this.newLayer;
216
    }
217

    
218
    /*
219
     * Lazy envelope initialization
220
     */
221
    @Override
222
    public synchronized Envelope getEnvelope() throws DataException {
223
        open();
224
        if (this.envelope == null) {
225
            this.envelope = (Envelope) getResource().execute(new ResourceAction() {
226

    
227
                @Override
228
                public Object run() throws Exception {
229
                    Layer layer = getLayer();
230
                    double[] extent = layer.GetExtent(true);
231
                    if (extent != null) {
232
                        return GeometryLocator.getGeometryManager().createEnvelope(extent[0],
233
                            extent[2], extent[1], extent[3], SUBTYPES.GEOM2D);
234
                    } else {
235
                        Envelope tmpEnvelope =
236
                            GeometryLocator.getGeometryManager().createEnvelope(SUBTYPES.GEOM2D);
237
                        FeatureType featureType = getStoreServices().getDefaultFeatureType();
238
                        layer.ResetReading();
239
                        Feature feature = layer.GetNextFeature();
240
                        while (feature!=null) {
241
                            double[] envelope = new double[4];
242
                            int geomFieldIndex =
243
                                layer.GetLayerDefn().GetGeomFieldIndex(
244
                                    featureType.getDefaultGeometryAttributeName());
245
                            Geometry ogrGeometry = feature.GetGeomFieldRef(geomFieldIndex);
246
                            ogrGeometry.GetEnvelope(envelope);
247
                            tmpEnvelope.add(GeometryLocator.getGeometryManager()
248
                                .createEnvelope(envelope[0], envelope[2], envelope[1], envelope[3],
249
                                    SUBTYPES.GEOM2D));
250
                            feature = layer.GetNextFeature();
251
                        }
252

    
253
                        return tmpEnvelope;
254
                    }
255
                }
256
            });
257
        }
258
        return this.envelope;
259
    }
260

    
261
    @Override
262
    public String getFullName() {
263

    
264
        StringBuilder stb = new StringBuilder();
265
        stb.append(NAME);
266
        stb.append(":");
267
        if (StringUtils.isBlank(getOGRParameters().getConnectionString())) {
268
            stb.append(getOGRParameters().getFile().getAbsolutePath());
269
            stb.append(":");
270
            stb.append(getOGRParameters().getLayerName());
271
        } else {
272
            stb.append(getOGRParameters().getConnectionString());
273
        }
274
        return stb.toString();
275
    }
276

    
277
    @Override
278
    public String getName() {
279
        return getOGRParameters().getLayerName();
280
    }
281

    
282
    @Override
283
    public String getProviderName() {
284
        return NAME;
285
    }
286

    
287
    @Override
288
    public boolean allowWrite() {
289
        try {
290
            return getLayer().TestCapability(ogrConstants.OLCAlterFieldDefn)
291
                && getLayer().TestCapability(ogrConstants.OLCCreateField)
292
                && getLayer().TestCapability(ogrConstants.OLCDeleteField)
293
                && getLayer().TestCapability(ogrConstants.OLCDeleteFeature) && hasUpdateSupport();
294
        } catch (OGRUnsupportedFormatException e) {
295
            LOG.error("Can not determinate if data source allows write", e);
296
            return false;
297
        }
298
    }
299

    
300
    @Override
301
    public ResourceProvider getResource() {
302

    
303
        if (this.resourceProvider == null) {
304
            if (StringUtils.isBlank(getOGRParameters().getConnectionString())) {
305
                try {
306
                    this.resourceProvider =
307
                        this.createResource(FileResource.NAME, new Object[] { getOGRParameters()
308
                            .getFile().getAbsolutePath() });
309
                } catch (InitializeException e) {
310
                    throw new ReadRuntimeException(String.format(
311
                        "Can not create file resource with %1s path", getOGRParameters().getFile()
312
                            .getAbsolutePath()), e);
313
                }
314
            } else {
315
                try {
316
                    this.resourceProvider =
317
                        this.createResource(OGRResource.NAME, new Object[] { getOGRParameters()
318
                            .getConnectionString() });
319
                } catch (InitializeException e) {
320
                    throw new ReadRuntimeException(String.format(
321
                        "Can not create OGR resource with %1s", getOGRParameters()
322
                            .getConnectionString()), e);
323
                }
324
            }
325
        }
326

    
327
        return resourceProvider;
328
    }
329

    
330
    @Override
331
    public Object getSourceId() {
332
        return this.getOGRParameters().getFile();
333
    }
334

    
335
    @Override
336
    public void open() throws OpenException {
337

    
338
        if (opened == false) {
339
            try {
340
                this.opened = loadFeatureType();
341
            } catch (BaseException e) {
342
                LOG.error("Can not load feature type", e);
343
                throw new OpenException(getFullName(), e);
344
            }
345
        }
346
    }
347

    
348
    protected synchronized boolean loadFeatureType() throws OGRUnsupportedFormatException,
349
        GeometryTypeNotSupportedException, GeometryTypeNotValidException {
350

    
351
        return (boolean) getResource().execute(new ResourceAction() {
352

    
353
            @Override
354
            public Object run() throws Exception {
355
                FeatureDefn featureDefn = getLayer().GetLayerDefn();
356
                OGRConverter converter = new OGRConverter();
357
                String defaultGeometryField = getOGRParameters().getDefaultGeometryField();
358
                FeatureType featureType = converter.convert(featureDefn, defaultGeometryField);
359

    
360
                if (featureType.getDefaultSRS() != null) {
361
                    setDynValue(DataStore.METADATA_CRS, featureType.getDefaultSRS());
362
                }
363

    
364
                List<FeatureType> featureTypes = new ArrayList<FeatureType>();
365
                featureTypes.add(featureType);
366

    
367
                getStoreServices().setFeatureTypes(featureTypes, featureType);
368
                return true;
369
            }
370
        });
371
    }
372

    
373
    @SuppressWarnings("rawtypes")
374
    @Override
375
    public void performChanges(final Iterator deleteds, final Iterator inserteds,
376
        final Iterator updateds, final Iterator featureTypesChanged) throws DataException {
377

    
378
        getResource().execute(new ResourceAction() {
379

    
380
            @Override
381
            public Object run() throws Exception {
382
                OGRConverter converter = new OGRConverter();
383

    
384
                if (getLayer().TestCapability(ogrConstants.OLCTransactions)) {
385
                    getLayer().StartTransaction();
386
                }
387

    
388
                while (featureTypesChanged.hasNext()) {
389
                    FeatureTypeChanged featureTypeChange =
390
                        (FeatureTypeChanged) featureTypesChanged.next();
391
                    FeatureType source = featureTypeChange.getSource();
392
                    FeatureType target = featureTypeChange.getTarget();
393

    
394
                    for (int i = 0; i < source.getAttributeDescriptors().length; i++) {
395
                        EditableFeatureAttributeDescriptor eAttDescriptor =
396
                            source.getEditable().getEditableAttributeDescriptor(i);
397

    
398
                        if (eAttDescriptor.getOriginalName() != null) {
399
                            int index =
400
                                getLayer().GetLayerDefn().GetFieldIndex(
401
                                    eAttDescriptor.getOriginalName());
402

    
403
                            FieldDefn field = converter.convertField(eAttDescriptor);
404
                            getLayer().AlterFieldDefn(index, field, ogrConstants.ALTER_ALL_FLAG);
405
                        } else if (target.getAttributeDescriptor(eAttDescriptor.getName()) == null) {
406
                            int index = getLayer().FindFieldIndex(eAttDescriptor.getName(), 1);
407
                            getLayer().DeleteField(index);
408
                        }
409
                    }
410

    
411
                    List<FieldDefn> fields = converter.convertFields(target);
412
                    for (FieldDefn fieldDefn : fields) {
413
                        int index = getLayer().GetLayerDefn().GetFieldIndex(fieldDefn.GetName());
414
                        if (index == -1) {
415
                            getLayer().CreateField(fieldDefn);
416
                        } else {
417
                            getLayer()
418
                                .AlterFieldDefn(index, fieldDefn, ogrConstants.ALTER_ALL_FLAG);
419
                        }
420
                    }
421

    
422
                    if (getLayer().TestCapability(ogrConstants.OLCCreateGeomField)) {
423
                        List<GeomFieldDefn> geometryFields =
424
                            converter.convertGeometryFields(target, true);
425
                        for (GeomFieldDefn geomFieldDefn : geometryFields) {
426
                            int index =
427
                                getLayer().GetLayerDefn()
428
                                    .GetGeomFieldIndex(geomFieldDefn.GetName());
429
                            if (index == -1) {
430
                                getLayer().CreateGeomField(geomFieldDefn);
431
                            }
432
                        }
433
                    } else {
434
                        StringBuilder stb = new StringBuilder();
435
                        stb.append("Driver '");
436
                        stb.append(getDataSource().GetDriver().GetName());
437
                        stb.append("' does not support create geometry fields");
438
                        LOG.warn(stb.toString());
439
                    }
440
                }
441

    
442
                while (deleteds.hasNext()) {
443
                    FeatureReferenceProviderServices reference =
444
                        (FeatureReferenceProviderServices) deleteds.next();
445
                    getLayer().DeleteFeature((int) reference.getOID());
446
                }
447

    
448
                while (inserteds.hasNext()) {
449
                    FeatureProvider featureProvider = (FeatureProvider) inserteds.next();
450
                    getLayer().CreateFeature(converter.convert(featureProvider));
451
                }
452

    
453
                while (updateds.hasNext()) {
454
                    FeatureProvider featureProvider = (FeatureProvider) updateds.next();
455
                    Feature ogrFeature = converter.convert(featureProvider);
456
                    getLayer().SetFeature(ogrFeature);
457
                }
458

    
459
                if (getLayer().TestCapability(ogrConstants.OLCTransactions)) {
460
                    getLayer().CommitTransaction();
461
                }
462
                getDataSource().SyncToDisk();
463
                repack();
464
                getResource().notifyChanges();
465

    
466
                return null;
467
            }
468
        });
469
    }
470

    
471
    protected void repack() throws OGRUnsupportedFormatException {
472
        LOG.debug("Running SQL: REPACK ".concat(getLayer().GetName()));
473
        getDataSource().ExecuteSQL("REPACK ".concat(getLayer().GetName()));
474
    }
475

    
476
    @Override
477
    public Object createNewOID() {
478
        try {
479
            return getFeatureCount() + 1;
480
        } catch (DataException e) {
481
            LOG.error("Can't get feature count", e);
482
            throw new ReadRuntimeException(getFullName(), e);
483
        }
484
    }
485

    
486
    @Override
487
    public FeatureSetProvider createSet(FeatureQuery query, FeatureType featureType)
488
        throws DataException {
489
        open();
490
        return new OGRFetureSetProvider(this, query, featureType);
491
    }
492

    
493
    @Override
494
    public long getFeatureCount() throws DataException {
495
        open();
496
        return ((Number) getResource().execute(new ResourceAction() {
497

    
498
            @Override
499
            public Object run() throws Exception {
500

    
501
                int featureCount = getLayer().GetFeatureCount(0);
502
                if( featureCount == -1 ) {
503
                    featureCount = getLayer().GetFeatureCount();
504
                }
505
                return featureCount;
506
            }
507
        })).longValue();
508
    }
509

    
510
    @Override
511
    public int getOIDType() {
512
        return DataTypes.LONG;
513
    }
514

    
515
    @Override
516
    protected FeatureProvider internalGetFeatureProviderByReference(
517
        FeatureReferenceProviderServices providerServices, FeatureType featureType)
518
        throws DataException {
519

    
520
        int oid = (int)providerServices.getOID();
521
        // Parece que hay un bug en el proveedor de SQLite para gdal.
522
        // Cuando se lee la capa, el m?todo GetFID est? indexado empezando por 0,
523
        // pero cuando se busca una ogrFeature a partir de dicho FID
524
        // el m?todo GetFeature(fid) est? indexado empezando por 1.
525
        // Esto es para rodear el problema.
526
        if(this.dataSource.GetDriver().getName().equalsIgnoreCase("SQLite")){
527
            oid++;
528
        }
529
        Feature ogrFeature = getLayer().GetFeature(oid);
530
        int fid = ogrFeature.GetFID();
531
        FeatureProvider featureProvider =
532
            new DefaultFeatureProvider(featureType, fid);
533
        OGRConverter converter = new OGRConverter();
534
        featureProvider = converter.convert(featureProvider, featureType, ogrFeature);
535
        return featureProvider;
536
    }
537

    
538
    private OGRDataStoreParameters getOGRParameters() {
539
        return (OGRDataStoreParameters) this.getParameters();
540
    }
541

    
542
    @SuppressWarnings("rawtypes")
543
    protected String compoundSelect(FeatureType type, Evaluator evaluator,
544
        FeatureQueryOrder featureQueryOrder) {
545

    
546
        StringBuilder query = new StringBuilder();
547
        query.append("SELECT ");
548
        FeatureAttributeDescriptor[] attributeDescriptors = type.getAttributeDescriptors();
549
        for (int i = 0; i < attributeDescriptors.length; i++) {
550
            query.append("\"");
551
            query.append(attributeDescriptors[i].getName());
552
            query.append("\"");
553
            // Don't add the last comma
554
            if (i < attributeDescriptors.length - 1) {
555
                query.append(",");
556
            }
557
        }
558

    
559
        query.append(" FROM ");
560
        query.append("\"");
561
        query.append(getOGRParameters().getLayerName());
562
        query.append("\"");
563

    
564
        if (featureQueryOrder != null && featureQueryOrder.iterator().hasNext()) {
565
            query.append(" ORDER BY ");
566
            Iterator iterator = featureQueryOrder.iterator();
567
            while (iterator.hasNext()) {
568
                FeatureQueryOrderMember member = (FeatureQueryOrderMember) iterator.next();
569

    
570
                if (member.hasEvaluator()) {
571
                    // TODO
572
                } else {
573
                    query.append("\"");
574
                    query.append(member.getAttributeName());
575
                    query.append("\"");
576
                }
577
                if (member.getAscending()) {
578
                    query.append(" ASC");
579
                } else {
580
                    query.append(" DESC");
581
                }
582
                if (iterator.hasNext()) {
583
                    query.append(", ");
584
                } else {
585
                    query.append(' ');
586
                    break;
587
                }
588
            }
589
        }
590

    
591
        return query.toString();
592
    }
593

    
594
    @Override
595
    protected void doDispose() throws BaseException {
596
        super.doDispose();
597
        getResource().removeConsumer(this);
598
        this.resourceProvider = null;
599
        getDataSource().delete();
600
        this.envelope = null;
601
        this.newLayer = null;
602
        this.dataSource = null;
603
        this.opened = false;
604
        this.updateSupport = null;
605
    }
606

    
607
    @Override
608
    public boolean closeResourceRequested(ResourceProvider resource) {
609

    
610
        try {
611
            getDataSource().delete();
612
        } catch (OGRUnsupportedFormatException e) {
613
            LOG.warn(String.format("Can not close resource requested %1s", resource), e);
614
        }
615
        this.envelope = null;
616
        this.newLayer = null;
617
        this.dataSource = null;
618
        this.opened = false;
619
        this.updateSupport = null;
620
        return true;
621
    }
622

    
623
    @Override
624
    public void resourceChanged(ResourceProvider resource) {
625

    
626
        try {
627
            getDataSource().delete();
628
        } catch (OGRUnsupportedFormatException e) {
629
            LOG.warn(String.format("Can not close resource requested %1s", resource), e);
630
        }
631
        this.envelope = null;
632
        this.newLayer = null;
633
        this.dataSource = null;
634
        this.opened = false;
635
        this.updateSupport = null;
636
    }
637
}