Project

General

Profile

Download (22.4 KB) Statistics
| Branch: | Revision:
1
/**
2
* Copyright (C) 2017 EDIT
3
* European Distributed Institute of Taxonomy
4
* http://www.e-taxonomy.eu
5
*
6
* The contents of this file are subject to the Mozilla Public License Version 1.1
7
* See LICENSE.TXT at the top of this package for the full license terms.
8
*/
9
package eu.etaxonomy.cdm.io.salvador;
10

    
11
import java.text.ParseException;
12
import java.util.HashMap;
13
import java.util.List;
14
import java.util.Map;
15
import java.util.Set;
16
import java.util.UUID;
17
import java.util.regex.Matcher;
18
import java.util.regex.Pattern;
19

    
20
import org.apache.commons.lang3.StringUtils;
21
import org.joda.time.DateTime;
22
import org.joda.time.format.DateTimeFormat;
23
import org.joda.time.format.DateTimeFormatter;
24
import org.springframework.stereotype.Component;
25

    
26
import eu.etaxonomy.cdm.api.facade.DerivedUnitFacade;
27
import eu.etaxonomy.cdm.common.URI;
28
import eu.etaxonomy.cdm.io.common.utils.ImportDeduplicationHelper;
29
import eu.etaxonomy.cdm.io.csv.in.CsvImportBase;
30
import eu.etaxonomy.cdm.model.agent.Person;
31
import eu.etaxonomy.cdm.model.agent.Team;
32
import eu.etaxonomy.cdm.model.agent.TeamOrPersonBase;
33
import eu.etaxonomy.cdm.model.common.Annotation;
34
import eu.etaxonomy.cdm.model.common.CdmBase;
35
import eu.etaxonomy.cdm.model.common.ExtensionType;
36
import eu.etaxonomy.cdm.model.common.Language;
37
import eu.etaxonomy.cdm.model.common.TimePeriod;
38
import eu.etaxonomy.cdm.model.description.CommonTaxonName;
39
import eu.etaxonomy.cdm.model.description.Feature;
40
import eu.etaxonomy.cdm.model.description.IndividualsAssociation;
41
import eu.etaxonomy.cdm.model.description.SpecimenDescription;
42
import eu.etaxonomy.cdm.model.description.TaxonDescription;
43
import eu.etaxonomy.cdm.model.description.TextData;
44
import eu.etaxonomy.cdm.model.location.Country;
45
import eu.etaxonomy.cdm.model.location.NamedArea;
46
import eu.etaxonomy.cdm.model.location.ReferenceSystem;
47
import eu.etaxonomy.cdm.model.occurrence.Collection;
48
import eu.etaxonomy.cdm.model.occurrence.DerivedUnit;
49
import eu.etaxonomy.cdm.model.occurrence.FieldUnit;
50
import eu.etaxonomy.cdm.model.occurrence.SpecimenOrObservationType;
51
import eu.etaxonomy.cdm.model.taxon.Taxon;
52
import eu.etaxonomy.cdm.model.term.TermType;
53
import eu.etaxonomy.cdm.model.term.TermVocabulary;
54

    
55
/**
56
 * @author a.mueller
57
 * @since 08.07.2017
58
 *
59
 */
60
@Component
61
public class SalvadorSpecimenImport
62
            extends CsvImportBase<SalvadorSpecimenImportConfigurator,SalvadorSpecimenImportState, FieldUnit> {
63

    
64
    private static final long serialVersionUID = -2165916187195347780L;
65

    
66
    private ImportDeduplicationHelper<?> dedupHelper;
67

    
68
    /**
69
     * {@inheritDoc}
70
     */
71
    @Override
72
    protected void handleSingleLine(SalvadorSpecimenImportState state) {
73

    
74
        initDedupHelper();
75
        try {
76
            UUID factUuid = UUID.fromString(state.getCurrentRecord().get("specimenFactUuid"));
77

    
78
            if (existingFieldUnits.get(factUuid)== null){
79

    
80
                FieldUnit fieldUnit = makeFieldUnit(state);
81
                DerivedUnitFacade facade = DerivedUnitFacade.NewInstance(
82
                        SpecimenOrObservationType.PreservedSpecimen, fieldUnit);
83
                makeFieldUnitData(state, facade);
84
                makeSpecimen(state, facade);
85
            }else{
86
                FieldUnit fieldUnit = fieldUnitMap.get(factUuid);
87
                if (fieldUnit == null){
88
                    fieldUnit = CdmBase.deproxy(getOccurrenceService().find(existingFieldUnits.get(factUuid)), FieldUnit.class);
89
                    fieldUnitMap.put(factUuid, fieldUnit);
90
                }
91
                DerivedUnitFacade facade = DerivedUnitFacade.NewInstance(
92
                        SpecimenOrObservationType.PreservedSpecimen, fieldUnit);
93
                makeSpecimenDuplicate(state, facade);
94
            }
95

    
96
            return;
97
        } catch (Exception e) {
98
            String message = "Unexpected error in handleSingleLine: " + e.getMessage();
99
            state.getResult().addException(e, message, null, String.valueOf(state.getRow()));
100
            e.printStackTrace();
101
        }
102
    }
103

    
104

    
105
    /**
106
     *
107
     */
108
    private void initDedupHelper() {
109
        if (dedupHelper == null){
110
            dedupHelper = ImportDeduplicationHelper.NewStandaloneInstance();
111
        }
112
    }
113

    
114

    
115
    /**
116
     * @param config
117
     * @param facade
118
     * @param importResult
119
     */
120
    private void makeSpecimenDuplicate(SalvadorSpecimenImportState state,
121
            DerivedUnitFacade facade) {
122

    
123
        Map<String, String> record = state.getCurrentRecord();
124

    
125
        TaxonDescription desc = getTaxonDescription(state, record);
126

    
127
        int row = state.getRow();
128
        String herbariaStr = record.get("Herbaria");
129
        String[] splits = herbariaStr.split(";");
130
        for (String split : splits){
131
            if ("B".equals(split)){
132
                Collection collection = getCollection(split, row);
133
                facade.setCollection(collection);
134
                if (record.get("B-Barcode") != null){
135
                    facade.setBarcode(record.get("B-Barcode"));
136
                }
137
                String uriStr = record.get("B_UUID");
138
                if (uriStr != null){
139
                    URI uri = URI.create(uriStr);
140
                    facade.setPreferredStableUri(uri);
141
                }
142
                IndividualsAssociation assoc = IndividualsAssociation.NewInstance(facade.innerDerivedUnit());
143
                assoc.setFeature(Feature.SPECIMEN());
144
                desc.addElement(assoc);
145
            }
146

    
147
        }
148
    }
149

    
150

    
151
    private Map<UUID, FieldUnit> fieldUnitMap = new HashMap<>();
152
    private Map<UUID, UUID> existingFieldUnits = new HashMap<>();
153
    private FieldUnit makeFieldUnit(SalvadorSpecimenImportState state) {
154

    
155
        Map<String, String> record = state.getCurrentRecord();
156
        UUID factUuid = UUID.fromString(record.get("specimenFactUuid"));
157

    
158
        TextData textSpecimen = (TextData)getDescriptionService().getDescriptionElementByUuid(factUuid);
159
        textSpecimen.setFeature(getTexSpecimenFeature());
160

    
161
        FieldUnit fieldUnit = FieldUnit.NewInstance();
162

    
163
        fieldUnitMap.put(factUuid, fieldUnit);
164
        existingFieldUnits.put(factUuid, fieldUnit.getUuid());
165

    
166
        return fieldUnit;
167
    }
168

    
169
    //taxonUuid, TaxonDescription map
170
    private Map<UUID, TaxonDescription> taxonDescMap = new HashMap<>();
171
    //taxonUuid, TaxonDescription.uuid map
172
    private Map<UUID, UUID> existingTaxonDescs = new HashMap<>();
173

    
174
    private TaxonDescription getTaxonDescription(SalvadorSpecimenImportState state,
175
            Map<String, String> record) {
176

    
177
        int row = state.getRow();
178
        UUID taxonUuid = UUID.fromString(record.get("taxonUuid"));
179
        TaxonDescription taxonDesc = taxonDescMap.get(taxonUuid);
180
        if (taxonDesc == null && existingTaxonDescs.get(taxonUuid) != null){
181
            taxonDesc = (TaxonDescription)getDescriptionService().find(existingTaxonDescs.get(taxonUuid));
182
            taxonDescMap.put(taxonUuid, taxonDesc);
183
        }
184
        if (taxonDesc == null){
185

    
186
            Taxon taxon = (Taxon)getTaxonService().find(taxonUuid);
187

    
188
            taxonDesc = TaxonDescription.NewInstance(taxon);
189
            taxonDesc.setTitleCache("JACQ import for " + taxon.getName().getTitleCache(), true);
190
            taxonDesc.addImportSource(null, null, state.getConfig().getSourceReference(), String.valueOf(row));
191
            taxonDescMap.put(taxonUuid, taxonDesc);
192
            existingTaxonDescs.put(taxonUuid, taxonDesc.getUuid());
193
        }else{
194
            System.out.println("Reuse desc: " + row);
195
        }
196

    
197
        return taxonDesc;
198
    }
199

    
200
    /**
201
     * @param config
202
     * @param facade
203
     * @param importResult
204
     */
205
    private void makeSpecimen(SalvadorSpecimenImportState state, DerivedUnitFacade facade) {
206

    
207
        Map<String, String> record = state.getCurrentRecord();
208

    
209
        TaxonDescription desc = getTaxonDescription(state, record);
210

    
211
        int row = state.getRow();
212
        String herbariaStr = record.get("Herbaria");
213
        String laguUuidStr = record.get("LAGU_UUID");
214
        if (laguUuidStr != null && !herbariaStr.contains("LAGU")){
215
            herbariaStr += ";LAGU";
216
        }
217
        String[] splits = herbariaStr.split(";");
218
        boolean isFirst = true;
219
        for (String split : splits){
220
            Collection collection = getCollection(split, row);
221
            DerivedUnit unit;
222
            if (isFirst){
223
                facade.setCollection(collection);
224
                unit = facade.innerDerivedUnit();
225
            }else{
226
                unit = facade.addDuplicate(collection, null, null, null, null);
227
            }
228
            isFirst = false;
229
            if ("B".equalsIgnoreCase(split)){
230
                unit.setBarcode(record.get("B-Barcode"));
231
                String uriStr = record.get("B_UUID");
232
                if (uriStr != null){
233
                    URI uri = URI.create(uriStr);
234
                    unit.setPreferredStableUri(uri);
235
                }
236
            }else if ("LAGU".equalsIgnoreCase(split)){
237
                String uriStr = record.get("LAGU_UUID");
238
                if (uriStr != null){
239
                    URI uri = URI.create(uriStr);
240
                    unit.setPreferredStableUri(uri);
241
                }
242
            }
243

    
244
            IndividualsAssociation assoc = IndividualsAssociation.NewInstance(unit);
245
            assoc.setFeature(Feature.SPECIMEN());
246
            desc.addElement(assoc);
247
        }
248
    }
249

    
250
    private Map<String, Collection> collectionMap = new HashMap<>();
251

    
252
    private Collection getCollection(String code, int row) {
253

    
254
        if (StringUtils.isBlank(code)){
255
            return null;
256
        }
257
        if (collectionMap.isEmpty()){
258
            List<Collection> collections = getCollectionService().list(null, null, null, null, null);
259
            for (Collection collection :collections){
260
                collectionMap.put(collection.getCode(), collection);
261
            }
262
        }
263
        if (collectionMap.get(code) == null){
264
            Collection collection = Collection.NewInstance();
265
            collection.setCode(code);
266
            collectionMap.put(code, collection);
267
            getCollectionService().save(collection);
268
        }
269
        return collectionMap.get(code);
270
    }
271

    
272
    /**
273
     * @param config
274
     * @param facade
275
     * @param importResult
276
     */
277
    private void makeFieldUnitData(SalvadorSpecimenImportState state, DerivedUnitFacade facade) {
278

    
279
        Map<String, String> record = state.getCurrentRecord();
280
        int row = state.getRow();
281

    
282
        Language spanish = Language.SPANISH_CASTILIAN();
283

    
284
        //idInSource
285
        String idInSource = record.get("IdInSource");
286
        String nameSpace = "http://resolv.jacq.org/";
287
        facade.innerFieldUnit().addImportSource(idInSource, nameSpace,
288
                state.getConfig().getSourceReference(), String.valueOf(row));
289

    
290
        //collector
291
        TeamOrPersonBase<?> collector = makeCollectorTeam(state, record, row);
292
        if (collector != null){
293
            collector = dedupHelper.getExistingAuthor(null, collector);
294
            facade.setCollector(collector);
295
        }
296

    
297
        //collectorsNumber
298
        facade.setFieldNumber(record.get("CollectorsNumber"));
299

    
300
        //CollectionDate
301
        String collectionDate = record.get("CollectionDate");
302
        if (collectionDate != null){
303
            TimePeriod tp;
304
            if (collectionDate.equals("1987")){
305
                tp = TimePeriod.NewInstance(1987);
306
                state.getResult().addWarning("Only year is not correct: " + collectionDate, state.getRow());
307
            }else{
308
                DateTimeFormatter f = DateTimeFormat.forPattern("yyyy-MM-dd");
309
                collectionDate = collectionDate.replace(" 00:00:00", "");
310
                DateTime dateTime = f.parseDateTime(collectionDate);
311
                tp = TimePeriod.NewInstance(dateTime, null);
312
            }
313
            facade.getGatheringEvent(true).setTimeperiod(tp);
314
        }
315
        //Country
316
        Country country = makeCountry(state, record, row);
317
        facade.setCountry(country);
318

    
319
        //Area_Major
320
        NamedArea area = makeMajorArea(state);
321
        if(area != null){
322
            facade.addCollectingArea(area);
323
        }
324

    
325
        //Locality
326
        String locality = record.get("Locality");
327
        if (locality != null){
328
            facade.setLocality(locality, spanish);
329
        }
330

    
331
        //Geo
332
        String latitude = record.get("LatitudeDecimal");
333
        String longitude = record.get("LongitudeDecimal");
334
        longitude = normalizeLongitude(longitude);
335
        if (latitude != null || longitude != null){
336
            if (latitude == null || longitude == null){
337
                state.getResult().addError("Only Lat or Long is null", row);
338
            }
339
            if (!"WGS84".equals(record.get("ReferenceSystem"))){
340
                state.getResult().addWarning("Reference system is not WGS84", row);
341
            }
342
            String errorRadiusStr =record.get("ErrorRadius");
343
            Integer errorRadius = null;
344
            if (errorRadiusStr != null){
345
                errorRadius = Integer.valueOf(errorRadiusStr);
346
            }
347
            try {
348
                facade.setExactLocationByParsing(longitude, latitude, ReferenceSystem.WGS84(), errorRadius);
349
            } catch (ParseException e) {
350
                state.getResult().addError("Error when parsing exact location" + e.getMessage(), row);
351
            }
352
        }
353

    
354
        //Altitude
355
        String altStr = record.get("Altitude");
356
        if (altStr != null){
357
            facade.setAbsoluteElevation(Integer.valueOf(altStr));
358
        }
359
        String altStrMax = record.get("AltitudeMax");
360
        if (altStrMax != null){
361
            facade.setAbsoluteElevationMax(Integer.valueOf(altStrMax));
362
        }
363

    
364
        //habitat
365
        String habitatStr = record.get("habitat");
366
        if (habitatStr != null){
367
            //TODO habitat, not ecology
368
            facade.setEcology(habitatStr, spanish);
369
//            //habitat
370
//            TextData habitat = TextData.NewInstance(Feature.HABITAT(), habitatStr, spanish, null);
371
//            facade.innerFieldUnit().getDescriptions().iterator().next()
372
//                .addElement(habitat);
373
//            facade.removeEcology(spanish);
374
        }
375

    
376
        //plant description
377
        String plantDescription = record.get("PlantDescription");
378
        if (plantDescription != null){
379
            facade.setPlantDescription(plantDescription, spanish);
380
        }
381

    
382
        //note
383
        //TODO is this field unit??
384
        String note = record.get("note");
385
        if (note != null){
386
            facade.innerFieldUnit().addAnnotation(Annotation.NewInstance(note, spanish));
387
        }
388

    
389
        //IdentificationHistory
390
        String identificationHistory = record.get("IdentificationHistory");
391
        if (identificationHistory != null){
392
            ExtensionType type = getExtensionType();
393
            facade.innerFieldUnit().addExtension(identificationHistory, type);
394
        }
395

    
396
        //LocalCommonName
397
        String localCommonName = record.get("LocalCommonName");
398
        if (localCommonName != null){
399
            CommonTaxonName commonName = CommonTaxonName.NewInstance(localCommonName, spanish);
400
            Set<SpecimenDescription> descs = (Set)facade.innerFieldUnit().getDescriptions();
401
            if (descs.isEmpty()){
402
                SpecimenDescription desc = SpecimenDescription.NewInstance(facade.innerFieldUnit());
403
                descs.add(desc);
404
            }
405
            descs.iterator().next().addElement(commonName);
406
        }
407

    
408
    }
409

    
410

    
411
    /**
412
     * @param longitude
413
     * @return
414
     */
415
    private String normalizeLongitude(String longitude) {
416
        if (longitude == null || longitude.startsWith("-")){
417
            return longitude;
418
        }else{
419
            return "-" + longitude;
420
        }
421
    }
422

    
423

    
424
    private ExtensionType identificationHistoryType;
425

    
426
    /**
427
     * @return
428
     */
429
    private ExtensionType getExtensionType() {
430
        if (identificationHistoryType == null){
431
            identificationHistoryType = ExtensionType.NewInstance("Identification History", "Identification History", null);
432
            UUID vocUuid = uuidUserDefinedExtensionTypeVocabulary;
433
            TermVocabulary<ExtensionType> voc = getVocabularyService().find(vocUuid);
434
            if (voc == null){
435
                voc = TermVocabulary.NewInstance(TermType.ExtensionType, ExtensionType.class,
436
                        "User defined extension types", "User defined extension types", null, null);
437
                getVocabularyService().save(voc);
438
            }
439
            voc.addTerm(identificationHistoryType);
440
            getTermService().saveOrUpdate(identificationHistoryType);
441
        }
442
        return identificationHistoryType;
443
    }
444

    
445

    
446

    
447
    private Feature textSpecimenFeature;
448

    
449

    
450
    private Feature getTexSpecimenFeature() {
451
        if (textSpecimenFeature == null){
452
            UUID uuidSpecimenTextOld = SalvadorImportTransformer.uuidSalvadorTextSpecimenOldFeature;
453
            textSpecimenFeature = (Feature)getTermService().find(uuidSpecimenTextOld);
454
        }
455
        if (textSpecimenFeature == null){
456
            String label = "Text Specimen";
457
            textSpecimenFeature = Feature.NewInstance(label, label, null);
458
            UUID vocUuid = SalvadorImportTransformer.uuidSalvadorFeatureVoc;
459
            TermVocabulary<Feature> voc = getVocabularyService().find(vocUuid);
460
            if (voc == null){
461
                voc = TermVocabulary.NewInstance(TermType.Feature, Feature.class,
462
                        "User defined features", "User defined features", null, null);
463
                getVocabularyService().save(voc);
464
            }
465
            textSpecimenFeature.setUuid(SalvadorImportTransformer.uuidSalvadorTextSpecimenOldFeature);
466
            voc.addTerm(textSpecimenFeature);
467
            getTermService().saveOrUpdate(textSpecimenFeature);
468
        }
469
        return textSpecimenFeature;
470
    }
471

    
472

    
473
    private Map<String, NamedArea> majorAreaMap = null;
474

    
475
    /**
476
     * @param state
477
     * @param record
478
     * @param row
479
     * @return
480
     */
481
    private NamedArea makeMajorArea(SalvadorSpecimenImportState state) {
482

    
483
        if (majorAreaMap == null){
484
            majorAreaMap = new HashMap<>();
485
            TermVocabulary<NamedArea> voc = getVocabularyService().find(UUID.fromString("8ef90ca3-77d7-4adc-8bbc-1eb354e61b65"));
486
            for (NamedArea area : voc.getTerms()){
487
                majorAreaMap.put(area.getTitleCache(), area);
488
            }
489
        }
490

    
491
        String areaStr = state.getCurrentRecord().get("Area_Major");
492
        NamedArea area = majorAreaMap.get(areaStr);
493
        if (area == null && areaStr != null){
494
            state.getResult().addError("Major area not found: " + areaStr, state.getRow());
495
        }
496
        return area;
497
    }
498

    
499
    /**
500
     * @param state
501
     * @param record
502
     * @param i
503
     * @return
504
     */
505
    private Country makeCountry(SalvadorSpecimenImportState state, Map<String, String> record, int row) {
506
        String iso = record.get("IsoCountry");
507
        String countryStr = record.get("COUNTRY");
508
        if (iso == null && countryStr == null){
509
            return null;
510
        }else if ("SLV".equals(iso) && "El Salvador".equals(countryStr)){
511
            return Country.ELSALVADORREPUBLICOF();
512
        }else if ("HND".equals(iso) && "Honduras".equals(countryStr)){
513
            return Country.HONDURASREPUBLICOF();
514
        }else if ("GTM".equals(iso) && "Guatemala".equals(countryStr)){
515
            return Country.GUATEMALAREPUBLICOF();
516
        }else{
517
            String message = "Iso-country combination not recognized: " + iso + " - " + countryStr;
518
            state.getResult().addWarning(message, row);
519
            return null;
520
        }
521
    }
522

    
523
    /**
524
     * @param state
525
     * @param record
526
     * @param row
527
     * @param importResult
528
     * @return
529
     */
530
    private TeamOrPersonBase<?> makeCollectorTeam(SalvadorSpecimenImportState state, Map<String, String> record, int row) {
531

    
532
        Team team = Team.NewInstance();
533
        String first = record.get("COLLECTOR_0");
534
        if(first != null && first.startsWith("Grupo Ecológico")){
535
            team.setTitleCache(first, true);
536
            return team;
537
        }else{
538
            makeCollector(state, 0, team, record, row);
539
            makeCollector(state, 1, team, record, row);
540
            makeCollector(state, 2, team, record, row);
541
            makeCollector(state, 3, team, record, row);
542
            makeCollector(state, 4, team, record, row);
543
            if (team.getTeamMembers().size() == 0){
544
                return null;
545
            }else if (team.getTeamMembers().size() == 1){
546
                return team.getTeamMembers().get(0);
547
            }else{
548
                return team;
549
            }
550
        }
551
    }
552

    
553
    private void makeCollector(SalvadorSpecimenImportState state,
554
            int collectorNo, Team team, Map<String, String> record, int row) {
555

    
556
        String str = record.get("COLLECTOR_" + collectorNo);
557
        if (str == null){
558
            return;
559
        }else{
560
            parsePerson(state, str, team, row);
561
        }
562
        return ;
563
    }
564

    
565
    /**
566
     * @param str
567
     * @param team
568
     * @param row
569
     * @param importResult
570
     */
571
    private void parsePerson(SalvadorSpecimenImportState state, String str, Team team, int row) {
572
        Person result = Person.NewInstance();
573
        String regEx = "(.*),(([A-Z]\\.)+(\\sde)?)";
574
        Pattern pattern = Pattern.compile(regEx);
575
        Matcher matcher = pattern.matcher(str);
576

    
577
        String noInitials = "(Campo|Chinchilla|Campos|Claus|Desconocido|Fomtg|Huezo|Martínez|"
578
                + "Quezada|Romero|Ruíz|Sandoval|Serrano|Vásquez|Cabrera|Calderón)";
579

    
580
        if (matcher.matches()){
581
            String familyname = matcher.group(1);
582
            result.setFamilyName(familyname);
583
            String initials = matcher.group(2);
584
            result.setInitials(initials);
585
        }else if (str.matches(noInitials)){
586
            result.setFamilyName(str);
587
        }else if (str.matches("Martínez, F. de M.")){
588
            result.setFamilyName("Martínez");
589
            result.setInitials("F. de M.");
590
        }else if (str.equals("et al.")){
591
            team.setHasMoreMembers(true);
592
            return;
593
        }else if (str.startsWith("Grupo Ecológico")){
594
            result.setFamilyName(str);
595
        }else{
596
            String message = "Collector did not match pattern: " + str;
597
            state.getResult().addWarning(message, row);
598
            result.setTitleCache(str, true);
599
        }
600
        result = dedupHelper.getExistingAuthor(null, result);
601

    
602
        team.addTeamMember(result);
603
        return ;
604
    }
605

    
606

    
607
    @Override
608
    protected void refreshTransactionStatus(SalvadorSpecimenImportState state) {
609
        super.refreshTransactionStatus(state);
610
        collectionMap = new HashMap<>();
611
        fieldUnitMap = new HashMap<>();
612
        taxonDescMap = new HashMap<>();
613
        dedupHelper.restartSession(this, state.getResult());
614
    }
615

    
616

    
617

    
618
}
(2-2/4)