Statistics
| Revision:

svn-gvsig-desktop / trunk / org.gvsig.desktop / org.gvsig.desktop.compat.cdc / org.gvsig.fmap.dal / org.gvsig.fmap.dal.impl / src / main / java / org / gvsig / fmap / dal / feature / paging / impl / FeaturePagingHelperImpl.java @ 41819

History | View | Annotate | Download (24.3 KB)

1
/**
2
 * gvSIG. Desktop Geographic Information System.
3
 *
4
 * Copyright (C) 2007-2013 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 3
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.fmap.dal.feature.paging.impl;
25

    
26
import java.util.Collection;
27
import java.util.Iterator;
28
import java.util.List;
29
import java.util.ListIterator;
30
import org.slf4j.Logger;
31
import org.slf4j.LoggerFactory;
32

    
33
import org.gvsig.fmap.dal.exception.DataException;
34
import org.gvsig.fmap.dal.feature.EditableFeature;
35
import org.gvsig.fmap.dal.feature.Feature;
36
import org.gvsig.fmap.dal.feature.FeatureQuery;
37
import org.gvsig.fmap.dal.feature.FeatureSelection;
38
import org.gvsig.fmap.dal.feature.FeatureSet;
39
import org.gvsig.fmap.dal.feature.FeatureStore;
40
import org.gvsig.fmap.dal.feature.FeatureType;
41
import org.gvsig.fmap.dal.feature.exception.ConcurrentDataModificationException;
42
import org.gvsig.fmap.dal.feature.exception.FeatureIndexException;
43
import org.gvsig.fmap.dal.feature.impl.featureset.DynObjectFeatureFacade;
44
import org.gvsig.fmap.dal.feature.paging.FeaturePagingHelper;
45
import org.gvsig.tools.dynobject.DynObject;
46
import org.gvsig.tools.dynobject.DynObjectSet;
47
import org.gvsig.tools.dynobject.impl.DefaultDynObjectPagingHelper;
48
import org.gvsig.tools.exception.BaseException;
49
import org.gvsig.tools.visitor.VisitCanceledException;
50
import org.gvsig.tools.visitor.Visitor;
51

    
52
/**
53
 * Helper class to access the values of a FeatureCollection by position. Handles
54
 * pagination automatically to avoid filling the memory in case of big
55
 * collections.
56
 *
57
 * TODO: evaluate if its more convenient to read values in the background when
58
 * the returned value is near the end of the page, instead of loading a page on
59
 * demand.
60
 *
61
 * @author gvSIG Team
62
 */
63
public class FeaturePagingHelperImpl extends DefaultDynObjectPagingHelper
64
    implements FeaturePagingHelper {
65

    
66
    private static final Logger LOG = LoggerFactory
67
        .getLogger(FeaturePagingHelperImpl.class);
68

    
69
    private FeatureQuery query;
70

    
71
    private FeatureStore featureStore;
72

    
73
    /** If the selected Features must be returned as the first ones. **/
74
    private boolean selectionUp = false;
75

    
76
    private FeatureSet featSet = null;
77
    private FeatureSelection initialSelection = null;
78

    
79
    private Feature[] features = null;
80

    
81
    private boolean initialization_completed = false;
82
    /**
83
     * Constructs a FeaturePagingHelperImpl from data of a FeatureStore.
84
     *
85
     * @param featureStore
86
     *            to extract data from
87
     * @throws DataException
88
     *             if there is an error initializing the helper
89
     */
90
    public FeaturePagingHelperImpl(FeatureStore featureStore)
91
        throws BaseException {
92
        this(featureStore, DEFAULT_PAGE_SIZE);
93
    }
94

    
95
    /**
96
     * Constructs a FeaturePagingHelperImpl from data of a FeatureStore.
97
     *
98
     * @param featureStore
99
     *            to extract data from
100
     * @param pageSize
101
     *            the number of elements per page data
102
     * @throws DataException
103
     *             if there is an error initializing the helper
104
     */
105
    public FeaturePagingHelperImpl(FeatureStore featureStore, int pageSize)
106
        throws BaseException {
107
        this(featureStore, null, pageSize);
108
    }
109

    
110
    /**
111
     * Constructs a FeaturePagingHelperImpl from data of a FeatureStore.
112
     *
113
     * @param featureStore
114
     *            to extract data from
115
     * @throws DataException
116
     *             if there is an error initializing the helper
117
     */
118
    public FeaturePagingHelperImpl(FeatureStore featureStore,
119
        FeatureQuery featureQuery) throws BaseException {
120
        this(featureStore, featureQuery, DEFAULT_PAGE_SIZE);
121
    }
122

    
123
    /**
124
     * Constructs a FeaturePagingHelperImpl from data of a FeatureStore.
125
     *
126
     * @param featureSet
127
     *            to extract data from
128
     * @param pageSize
129
     *            the number of elements per page data
130
     * @throws DataException
131
     *             if there is an error initializing the helper
132
     */
133
    public FeaturePagingHelperImpl(FeatureStore featureStore,
134
        FeatureQuery featureQuery, int pageSize) throws BaseException {
135
        super();
136
        FeatureQuery query = featureQuery;
137
        if (featureQuery == null) {
138
            query = featureStore.createFeatureQuery();
139
            query.setFeatureType(featureStore.getDefaultFeatureType());
140
        }
141

    
142
        this.featureStore = featureStore;
143
        this.query = query;
144
        this.query.setPageSize(pageSize);
145

    
146
        setDefaultCalculator(new Sizeable() {
147
            public long getSize() {
148
                    FeatureSet featureSet = getFeatureSet(false);
149
                try {
150
                                        return featureSet.getSize();
151
                } catch (BaseException e) {
152
                    LOG.error("Error getting the size of the FeatureSet: "
153
                        + featureSet, e);
154
                    return 0l;
155
                }
156
            }
157
        }, pageSize);
158

    
159

    
160
        if (LOG.isDebugEnabled()) {
161

    
162
            LOG.debug("FeaturePagingHelperImpl created with {} pages, "
163
                + "and a page size of {}", new Long(getCalculator()
164
                .getNumPages()), new Integer(pageSize));
165
        }
166
        this.initialization_completed = true;
167
    }
168

    
169
    /**
170
     * @return the selectionUp status
171
     */
172
    public boolean isSelectionUp() {
173
        return selectionUp;
174
    }
175

    
176
    public void setSelectionUp(boolean selectionUp) {
177
        this.selectionUp = selectionUp;
178
        try {
179
            FeatureSelection currentSelection = getFeatureStore().getFeatureSelection();
180
            if (selectionUp && !currentSelection.isEmpty()) {
181
                initialSelection =(FeatureSelection) currentSelection.clone();
182
                setCalculator(new OneSubsetOneSetPagingCalculator(
183
                    new FeatureSetSizeableDelegate(initialSelection),
184
                    new FeatureSetSizeableDelegate(getFeatureSet(false)),
185
                    getMaxPageSize()));
186
            } else {
187
                if (initialSelection != null) {
188
                    initialSelection.dispose();
189
                    initialSelection = null;
190
                }
191
                setDefaultCalculator(new FeatureSetSizeableDelegate(
192
                    getFeatureSet(false)), getMaxPageSize());
193
            }
194
        } catch (BaseException e) {
195
            LOG.error("Error setting the selection up setting to: "
196
                + selectionUp, e);
197
        } catch (CloneNotSupportedException e) {
198
            LOG.error("Error cloning the selection "
199
                + "while setting the selection up", e);
200
        }
201
    }
202

    
203
    public Feature getFeatureAt(long index) throws BaseException {
204
        // Check if we have currently loaded the viewed page data,
205
        // or we need to load a new one
206
        long pageForIndex = (long) Math.floor(index / getMaxPageSize());
207

    
208
        if (pageForIndex != getCurrentPage()) {
209
            setCurrentPage(pageForIndex);
210
        }
211

    
212
        long positionForIndex = index - (getCurrentPage() * getMaxPageSize());
213

    
214
        if (positionForIndex >= getCurrentPageFeatures().length) {
215
            throw new FeatureIndexException(
216
                new IndexOutOfBoundsException("positionForIndex too big: "
217
                    + positionForIndex));
218
        } else {
219
            Feature feature = getCurrentPageFeatures()[(int) positionForIndex];
220
            return feature;
221
        }
222

    
223
    }
224

    
225
    public Feature[] getCurrentPageFeatures() {
226
        if( this.features==null ) {
227
            try {
228
                this.loadCurrentPageData();
229
            } catch (BaseException ex) {
230
                // Do nothing
231
            }
232
            if( this.features == null ) {
233
                String msg = "Can't retrieve the features from current page.";
234
                LOG.warn(msg);
235
                throw new RuntimeException(msg);
236
            }
237
        }
238
        return features;
239
    }
240

    
241
    /**
242
     * Gets the feature set.
243
     * The boolean tells whether we must create the featureset
244
     * again (for example perhaps we need it after a feature
245
     * has been added/removed)
246
     */
247
    private FeatureSet getFeatureSet(boolean reset) {
248

    
249
        if (featSet == null || reset) {
250

    
251
            if (featSet != null) {
252
                try {
253
                    featSet.dispose();
254
                } catch (Exception ex) {
255
                    LOG.info("Error while disposing featset.", ex);
256
                }
257
            }
258

    
259
            try {
260
                FeatureStore featureStore = getFeatureStore();
261
                synchronized (featureStore) {
262
                    featSet = featureStore.getFeatureSet(getFeatureQuery());
263
                }
264
            } catch (DataException e) {
265
                throw new RuntimeException("Error getting a feature set with the query " + getFeatureQuery());
266
            }
267
        }
268
        return featSet;
269
    }
270

    
271
    public DynObjectSet getDynObjectSet() {
272
            return getFeatureSet(false).getDynObjectSet();
273
    }
274

    
275
    public void reloadCurrentPage() throws BaseException {
276

    
277
        boolean sel_up = this.isSelectionUp();
278

    
279
        setSelectionUp(false);
280
        if (getCalculator().getCurrentPage() > -1) {
281
            loadCurrentPageData();
282
        }
283

    
284
        if (sel_up) {
285
            setSelectionUp(true);
286
        }
287
    }
288

    
289
    public void reload() throws BaseException {
290

    
291
        /*
292
         * Force re-creation of feature set
293
         */
294
        this.getFeatureSet(true);
295

    
296

    
297
        setDefaultCalculator(new Sizeable() {
298
            public long getSize() {
299
                    FeatureSet featureSet = getFeatureSet(false);
300
                try {
301
                                        return featureSet.getSize();
302
                } catch (BaseException e) {
303
                    LOG.error("Error getting the size of the FeatureSet: "
304
                        + featureSet, e);
305
                    return 0l;
306
                }
307
            }
308
        }, getCalculator().getMaxPageSize());
309
        reloadCurrentPage();
310
    }
311

    
312
    public FeatureStore getFeatureStore() {
313
        return featureStore;
314
    }
315

    
316
    public FeatureQuery getFeatureQuery() {
317
        return query;
318
    }
319

    
320
    /**
321
     * Loads all the Features of the current page.
322
     */
323
    protected void loadCurrentPageData() throws BaseException {
324
        if( !initialization_completed ) {
325
            return;
326
        }
327
        final int currentPageSize = getCalculator().getCurrentPageSize();
328
        final Feature[] values = new Feature[currentPageSize];
329

    
330
        long t1 = 0;
331
        if (LOG.isTraceEnabled()) {
332
            t1 = System.currentTimeMillis();
333
        }
334

    
335
        if (selectionUp) {
336
            loadCurrentPageDataWithSelectionUp(values);
337
        } else {
338
            loadCurrentPageDataNoSelection(values);
339
        }
340

    
341
        if (LOG.isTraceEnabled()) {
342
            long t2 = System.currentTimeMillis();
343
            LOG.trace("Time to load {} features: {} ms", new Integer(
344
                currentPageSize), new Long(t2 - t1));
345
        }
346

    
347
        this.features = values;
348
    }
349

    
350
    private void loadCurrentPageDataWithSelectionUp(final Feature[] values)
351
            throws BaseException {
352
        FeatureSelection selection = initialSelection;
353
        if (selection == null) {
354
            loadCurrentPageDataNoSelection(values);
355
        } else {
356
            FeatureSet set = getFeatureSet(false);
357
            try {
358
                OneSubsetOneSetPagingCalculator twoSetsCalculator = null;
359
                if (getCalculator() instanceof OneSubsetOneSetPagingCalculator) {
360
                    twoSetsCalculator
361
                            = (OneSubsetOneSetPagingCalculator) getCalculator();
362
                } else {
363
                    twoSetsCalculator
364
                            = new OneSubsetOneSetPagingCalculator(
365
                                    new FeatureSetSizeableDelegate(selection),
366
                                    new FeatureSetSizeableDelegate(set),
367
                                    getMaxPageSize(), getCalculator().getCurrentPage());
368
                    setCalculator(twoSetsCalculator);
369
                }
370

    
371
                // First load values from the selection, if the current page has
372
                // elements from it
373
                if (twoSetsCalculator.hasCurrentPageAnyValuesInFirstSet()) {
374
                    loadDataFromFeatureSet(values, 0, selection,
375
                            twoSetsCalculator.getFirstSetInitialIndex(),
376
                            twoSetsCalculator.getFirstSetHowMany(), null);
377
                }
378
                // Next, load values from the FeatureSet if the current page has values
379
                // from it
380
                if (twoSetsCalculator.hasCurrentPageAnyValuesInSecondSet()) {
381
                    loadDataFromFeatureSet(
382
                            values,
383
                            // The cast will work as that size will be <= maxpagesize,
384
                            // which is an int
385
                            (int) twoSetsCalculator.getFirstSetHowMany(), set,
386
                            twoSetsCalculator.getSecondSetInitialIndex(),
387
                            twoSetsCalculator.getSecondSetHowMany(), selection);
388
                }
389
            } finally {
390
                /*
391
                 * This is the feature set
392
                 * we dont want to lose it
393
                 */
394
                // set.dispose();
395
            }
396
        }
397
    }
398

    
399
    private void loadCurrentPageDataNoSelection(final Feature[] values)
400
        throws BaseException {
401

    
402
        long firstPosition = getCalculator().getInitialIndex();
403

    
404
        if (LOG.isDebugEnabled()) {
405
            LOG.debug("Loading {} Features starting at position {}",
406
                new Integer(getCalculator().getCurrentPageSize()), new Long(
407
                    firstPosition));
408
        }
409

    
410
        FeatureSet featureSet = getFeatureSet(false);
411
        try {
412
                loadDataFromFeatureSet(values, 0, featureSet, firstPosition,
413
                                getCalculator().getCurrentPageSize(), null);
414
        } catch(DataException ex) {
415
            throw ex;
416
            // } finally {
417
                // featureSet.dispose();
418
        }
419

    
420
    }
421

    
422
    private void loadDataFromFeatureSet(final Feature[] values,
423
        final int valuesPosition, FeatureSet set, long initialIndex,
424
        final long howMany, final FeatureSelection selectedFeaturesToSkip)
425
        throws DataException {
426

    
427
        try {
428
            set.accept(new Visitor() {
429

    
430
                private int i = valuesPosition;
431

    
432
                public void visit(Object obj) throws VisitCanceledException,
433
                    BaseException {
434
                    if (i >= valuesPosition + howMany) {
435
                        throw new VisitCanceledException();
436
                    }
437
                    Feature current = (Feature) obj;
438
                    // Add the current Feature only if we don't skip selected
439
                    // features or the feature is not selected
440
                    if (selectedFeaturesToSkip == null
441
                        || !selectedFeaturesToSkip.isSelected(current)) {
442
                        try {
443
                            values[i] = current.getCopy();
444
                            i++;
445
                        } catch(Exception ex) {
446
                            // Aqui no deberia petar, pero...
447
                            // me he encontrado un caso que tenia una referencia a
448
                            // una feature seleccionada que ya no existia. No se como
449
                            // habia pasado, se habia quedado de antes guardada en el
450
                            // proyecto pero la feature ya no existia, y eso hacia que
451
                            // petase al intentar leer de disco la feature a partir
452
                            // de una referencia no valida.
453
                        }
454
                    }
455
                }
456
            }, initialIndex);
457
        } catch (BaseException e) {
458
            if (e instanceof DataException) {
459
                throw ((DataException) e);
460
            } else {
461
                LOG.error("Error loading the data starting at position {}",
462
                    new Long(initialIndex), e);
463
            }
464
        }
465
    }
466

    
467
    public void delete(Feature feature) throws BaseException {
468
        featureStore.delete(feature);
469
        /*
470
         * Force re-creation of feature set
471
         */
472
        this.getFeatureSet(true);
473

    
474
        reloadCurrentPage();
475
    }
476

    
477
    public void insert(EditableFeature feature) throws BaseException {
478
            featureStore.insert(feature);
479
        /*
480
         * Force re-creation of feature set
481
         */
482
        this.getFeatureSet(true);
483

    
484
        reloadCurrentPage();
485
    }
486

    
487
    public void update(EditableFeature feature) throws BaseException {
488
            featureStore.update(feature);
489
        /*
490
         * Force re-creation of feature set
491
         */
492
        this.getFeatureSet(true);
493

    
494
        reloadCurrentPage();
495
    }
496

    
497
    public FeatureType getFeatureType() {
498

    
499
        FeatureType ft = null;
500

    
501
        try {
502
            ft = featureStore.getDefaultFeatureType();
503
        } catch (DataException e) {
504
            LOG.error("Error while getting feature type: " +
505
                e.getMessage(), e);
506
        }
507
        return ft;
508

    
509
        /*
510
         *
511
        FeatureSet featureSet = getFeatureSet();
512
        try {
513
            return featureSet.getDefaultFeatureType();
514
        } finally {
515
            featureSet.dispose();
516
        }
517
        */
518

    
519

    
520
    }
521

    
522
    protected void doDispose() throws BaseException {
523
        initialSelection.dispose();
524
        if (featSet != null) {
525
            try {
526
                featSet.dispose();
527
            } catch (Exception ex) {
528
                LOG.info("Error while disposing featset.", ex);
529
            }
530
        }
531
    }
532

    
533
    public DynObject[] getCurrentPageDynObjects() {
534
        Feature[] features = getCurrentPageFeatures();
535
        DynObject[] dynobjects = new DynObject[features.length];
536
        for (int i = 0; i < dynobjects.length; i++) {
537
            dynobjects[i] = new DynObjectFeatureFacade(features[i]);
538
        }
539
        return dynobjects;
540
    }
541

    
542
    public DynObject getDynObjectAt(long index) throws BaseException {
543
        return new DynObjectFeatureFacade(getFeatureAt(index));
544
    }
545

    
546
    public List asList() {
547
        return new FeaturePagingHelperList();
548
    }
549

    
550
    public List asListOfDynObjects() {
551
        return new DynObjectPagingHelperList();
552
    }
553

    
554
    private class FeaturePagingHelperList extends PagingHelperList {
555
        public Object get(int i) {
556
            try {
557
                return getFeatureAt(i);
558
            } catch (BaseException ex) {
559
                throw  new RuntimeException(ex);
560
            }
561
        }
562
    }
563

    
564
    private class DynObjectPagingHelperList extends PagingHelperList {
565
        public Object get(int i) {
566
            try {
567
                return getDynObjectAt(i);
568
            } catch (BaseException ex) {
569
                throw  new RuntimeException(ex);
570
            }
571
        }
572
    }
573

    
574
    private abstract class PagingHelperList implements List {
575

    
576
        public int size() {
577
            try {
578
                return (int) getFeatureSet(false).getSize();
579
            } catch (DataException ex) {
580
                throw  new RuntimeException(ex);
581
            }
582
        }
583

    
584
        public boolean isEmpty() {
585
            try {
586
                return getFeatureSet(false).isEmpty();
587
            } catch (DataException ex) {
588
                throw  new RuntimeException(ex);
589
            } catch (ConcurrentDataModificationException ex) {
590
                LOG.warn(
591
                    "Error to asking about the emptiness of the store. Retrying reloading data.",
592
                    ex);
593
                try {
594
                    reload();
595
                } catch (BaseException e) {
596
                    LOG.warn("Error reloading data.", e);
597
                    throw new RuntimeException(e);
598
                }
599
                try {
600
                    return getFeatureSet(false).isEmpty();
601
                } catch (DataException e) {
602
                    LOG.warn(
603
                        "Error to asking about the emptiness of the store after reloading data.",
604
                        e);
605
                    throw new RuntimeException(e);
606
                }
607
            }
608
        }
609

    
610
        public Iterator iterator() {
611
            try {
612
                return getFeatureSet(false).fastIterator();
613
            } catch (DataException ex) {
614
                throw  new RuntimeException(ex);
615
            }
616
        }
617

    
618
        public boolean contains(Object o) {
619
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
620
        }
621

    
622
        public Object[] toArray() {
623
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
624
        }
625

    
626
        public Object[] toArray(Object[] ts) {
627
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
628
        }
629

    
630
        public boolean add(Object e) {
631
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
632
        }
633

    
634
        public boolean remove(Object o) {
635
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
636
        }
637

    
638
        public boolean containsAll(Collection clctn) {
639
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
640
        }
641

    
642
        public boolean addAll(Collection clctn) {
643
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
644
        }
645

    
646
        public boolean addAll(int i, Collection clctn) {
647
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
648
        }
649

    
650
        public boolean removeAll(Collection clctn) {
651
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
652
        }
653

    
654
        public boolean retainAll(Collection clctn) {
655
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
656
        }
657

    
658
        public void clear() {
659
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
660
        }
661

    
662
        public Object set(int i, Object e) {
663
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
664
        }
665

    
666
        public void add(int i, Object e) {
667
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
668
        }
669

    
670
        public Object remove(int i) {
671
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
672
        }
673

    
674
        public int indexOf(Object o) {
675
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
676
        }
677

    
678
        public int lastIndexOf(Object o) {
679
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
680
        }
681

    
682
        public ListIterator listIterator() {
683
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
684
        }
685

    
686
        public ListIterator listIterator(int i) {
687
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
688
        }
689

    
690
        public List subList(int i, int i1) {
691
            throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
692
        }
693

    
694
    }
695
}