1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.gallery3d.filtershow.pipeline;
18
19import android.graphics.Bitmap;
20import android.graphics.Rect;
21import android.renderscript.Allocation;
22import android.util.JsonReader;
23import android.util.JsonWriter;
24import android.util.Log;
25
26import com.android.gallery3d.R;
27import com.android.gallery3d.filtershow.cache.BitmapCache;
28import com.android.gallery3d.filtershow.cache.ImageLoader;
29import com.android.gallery3d.filtershow.filters.BaseFiltersManager;
30import com.android.gallery3d.filtershow.filters.FilterCropRepresentation;
31import com.android.gallery3d.filtershow.filters.FilterFxRepresentation;
32import com.android.gallery3d.filtershow.filters.FilterImageBorderRepresentation;
33import com.android.gallery3d.filtershow.filters.FilterMirrorRepresentation;
34import com.android.gallery3d.filtershow.filters.FilterRepresentation;
35import com.android.gallery3d.filtershow.filters.FilterRotateRepresentation;
36import com.android.gallery3d.filtershow.filters.FilterStraightenRepresentation;
37import com.android.gallery3d.filtershow.filters.FilterUserPresetRepresentation;
38import com.android.gallery3d.filtershow.filters.FiltersManager;
39import com.android.gallery3d.filtershow.filters.ImageFilter;
40import com.android.gallery3d.filtershow.imageshow.GeometryMathUtils;
41import com.android.gallery3d.filtershow.imageshow.MasterImage;
42import com.android.gallery3d.filtershow.state.State;
43import com.android.gallery3d.filtershow.state.StateAdapter;
44
45import java.io.IOException;
46import java.io.StringReader;
47import java.io.StringWriter;
48import java.util.ArrayList;
49import java.util.Collection;
50import java.util.Vector;
51
52public class ImagePreset {
53
54    private static final String LOGTAG = "ImagePreset";
55    public static final String JASON_SAVED = "Saved";
56
57    private Vector<FilterRepresentation> mFilters = new Vector<FilterRepresentation>();
58
59    private boolean mDoApplyGeometry = true;
60    private boolean mDoApplyFilters = true;
61
62    private boolean mPartialRendering = false;
63    private Rect mPartialRenderingBounds;
64    private static final boolean DEBUG = false;
65
66    public ImagePreset() {
67    }
68
69    public ImagePreset(ImagePreset source) {
70        for (int i = 0; i < source.mFilters.size(); i++) {
71            FilterRepresentation sourceRepresentation = source.mFilters.elementAt(i);
72            mFilters.add(sourceRepresentation.copy());
73        }
74    }
75
76    public Vector<FilterRepresentation> getFilters() {
77        return mFilters;
78    }
79
80    public FilterRepresentation getFilterRepresentation(int position) {
81        FilterRepresentation representation = null;
82
83        representation = mFilters.elementAt(position).copy();
84
85        return representation;
86    }
87
88    private static boolean sameSerializationName(String a, String b) {
89        if (a != null && b != null) {
90            return a.equals(b);
91        } else {
92            return a == null && b == null;
93        }
94    }
95
96    public static boolean sameSerializationName(FilterRepresentation a, FilterRepresentation b) {
97        if (a == null || b == null) {
98            return false;
99        }
100        return sameSerializationName(a.getSerializationName(), b.getSerializationName());
101    }
102
103    public int getPositionForRepresentation(FilterRepresentation representation) {
104        for (int i = 0; i < mFilters.size(); i++) {
105            if (sameSerializationName(mFilters.elementAt(i), representation)) {
106                return i;
107            }
108        }
109        return -1;
110    }
111
112    private FilterRepresentation getFilterRepresentationForType(int type) {
113        for (int i = 0; i < mFilters.size(); i++) {
114            if (mFilters.elementAt(i).getFilterType() == type) {
115                return mFilters.elementAt(i);
116            }
117        }
118        return null;
119    }
120
121    public int getPositionForType(int type) {
122        for (int i = 0; i < mFilters.size(); i++) {
123            if (mFilters.elementAt(i).getFilterType() == type) {
124                return i;
125            }
126        }
127        return -1;
128    }
129
130    public FilterRepresentation getFilterRepresentationCopyFrom(
131            FilterRepresentation filterRepresentation) {
132        // TODO: add concept of position in the filters (to allow multiple instances)
133        if (filterRepresentation == null) {
134            return null;
135        }
136        int position = getPositionForRepresentation(filterRepresentation);
137        if (position == -1) {
138            return null;
139        }
140        FilterRepresentation representation = mFilters.elementAt(position);
141        if (representation != null) {
142            representation = representation.copy();
143        }
144        return representation;
145    }
146
147    public void updateFilterRepresentations(Collection<FilterRepresentation> reps) {
148        for (FilterRepresentation r : reps) {
149            updateOrAddFilterRepresentation(r);
150        }
151    }
152
153    public void updateOrAddFilterRepresentation(FilterRepresentation rep) {
154        int pos = getPositionForRepresentation(rep);
155        if (pos != -1) {
156            mFilters.elementAt(pos).useParametersFrom(rep);
157        } else {
158            addFilter(rep.copy());
159        }
160    }
161
162    public void setDoApplyGeometry(boolean value) {
163        mDoApplyGeometry = value;
164    }
165
166    public void setDoApplyFilters(boolean value) {
167        mDoApplyFilters = value;
168    }
169
170    public boolean getDoApplyFilters() {
171        return mDoApplyFilters;
172    }
173
174    public boolean hasModifications() {
175        for (int i = 0; i < mFilters.size(); i++) {
176            FilterRepresentation filter = mFilters.elementAt(i);
177            if (!filter.isNil()) {
178                return true;
179            }
180        }
181        return false;
182    }
183
184    public boolean contains(byte type) {
185        for (FilterRepresentation representation : mFilters) {
186            if (representation.getFilterType() == type
187                    && !representation.isNil()) {
188                return true;
189            }
190        }
191        return false;
192    }
193
194    public boolean isPanoramaSafe() {
195        for (FilterRepresentation representation : mFilters) {
196            if (representation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY
197                    && !representation.isNil()) {
198                return false;
199            }
200            if (representation.getFilterType() == FilterRepresentation.TYPE_BORDER
201                    && !representation.isNil()) {
202                return false;
203            }
204            if (representation.getFilterType() == FilterRepresentation.TYPE_VIGNETTE
205                    && !representation.isNil()) {
206                return false;
207            }
208            if (representation.getFilterType() == FilterRepresentation.TYPE_TINYPLANET
209                    && !representation.isNil()) {
210                return false;
211            }
212        }
213        return true;
214    }
215
216    public boolean same(ImagePreset preset) {
217        if (preset == null) {
218            return false;
219        }
220
221        if (preset.mFilters.size() != mFilters.size()) {
222            return false;
223        }
224
225        if (mDoApplyGeometry != preset.mDoApplyGeometry) {
226            return false;
227        }
228
229        if (mDoApplyFilters != preset.mDoApplyFilters) {
230            if (mFilters.size() > 0 || preset.mFilters.size() > 0) {
231                return false;
232            }
233        }
234
235        if (mDoApplyFilters && preset.mDoApplyFilters) {
236            for (int i = 0; i < preset.mFilters.size(); i++) {
237                FilterRepresentation a = preset.mFilters.elementAt(i);
238                FilterRepresentation b = mFilters.elementAt(i);
239
240                if (!a.same(b)) {
241                    return false;
242                }
243            }
244        }
245
246        return true;
247    }
248
249    public boolean equals(ImagePreset preset) {
250        if (preset == null) {
251            return false;
252        }
253
254        if (preset.mFilters.size() != mFilters.size()) {
255            return false;
256        }
257
258        if (mDoApplyGeometry != preset.mDoApplyGeometry) {
259            return false;
260        }
261
262        if (mDoApplyFilters != preset.mDoApplyFilters) {
263            if (mFilters.size() > 0 || preset.mFilters.size() > 0) {
264                return false;
265            }
266        }
267
268        for (int i = 0; i < preset.mFilters.size(); i++) {
269            FilterRepresentation a = preset.mFilters.elementAt(i);
270            FilterRepresentation b = mFilters.elementAt(i);
271            boolean isGeometry = false;
272            if (a instanceof FilterRotateRepresentation
273                    || a instanceof FilterMirrorRepresentation
274                    || a instanceof FilterCropRepresentation
275                    || a instanceof FilterStraightenRepresentation) {
276                isGeometry = true;
277            }
278            boolean evaluate = true;
279            if (!isGeometry && mDoApplyGeometry && !mDoApplyFilters) {
280                evaluate = false;
281            } else if (isGeometry && !mDoApplyGeometry && mDoApplyFilters) {
282                evaluate = false;
283            }
284            if (evaluate && !a.equals(b)) {
285                return false;
286            }
287        }
288
289        return true;
290    }
291
292    public int similarUpTo(ImagePreset preset) {
293        for (int i = 0; i < preset.mFilters.size(); i++) {
294            FilterRepresentation a = preset.mFilters.elementAt(i);
295            if (i < mFilters.size()) {
296                FilterRepresentation b = mFilters.elementAt(i);
297                if (!a.same(b)) {
298                    return i;
299                }
300                if (!a.equals(b)) {
301                    return i;
302                }
303            } else {
304                return i;
305            }
306        }
307        return preset.mFilters.size();
308    }
309
310    public void showFilters() {
311        Log.v(LOGTAG, "\\\\\\ showFilters -- " + mFilters.size() + " filters");
312        int n = 0;
313        for (FilterRepresentation representation : mFilters) {
314            Log.v(LOGTAG, " filter " + n + " : " + representation.toString());
315            n++;
316        }
317        Log.v(LOGTAG, "/// showFilters -- " + mFilters.size() + " filters");
318    }
319
320    public FilterRepresentation getLastRepresentation() {
321        if (mFilters.size() > 0) {
322            return mFilters.lastElement();
323        }
324        return null;
325    }
326
327    public void removeFilter(FilterRepresentation filterRepresentation) {
328        if (filterRepresentation.getFilterType() == FilterRepresentation.TYPE_BORDER) {
329            for (int i = 0; i < mFilters.size(); i++) {
330                if (mFilters.elementAt(i).getFilterType()
331                == filterRepresentation.getFilterType()) {
332                    mFilters.remove(i);
333                    break;
334                }
335            }
336        } else {
337            for (int i = 0; i < mFilters.size(); i++) {
338                if (sameSerializationName(mFilters.elementAt(i), filterRepresentation)) {
339                    mFilters.remove(i);
340                    break;
341                }
342            }
343        }
344    }
345
346    // If the filter is an "None" effect or border, then just don't add this filter.
347    public void addFilter(FilterRepresentation representation) {
348        if (representation instanceof FilterUserPresetRepresentation) {
349            ImagePreset preset = ((FilterUserPresetRepresentation) representation).getImagePreset();
350            if (preset.nbFilters() == 1
351                && preset.contains(FilterRepresentation.TYPE_FX)) {
352                FilterRepresentation rep = preset.getFilterRepresentationForType(
353                        FilterRepresentation.TYPE_FX);
354                addFilter(rep);
355            } else {
356                // user preset replaces everything
357                mFilters.clear();
358                for (int i = 0; i < preset.nbFilters(); i++) {
359                    addFilter(preset.getFilterRepresentation(i));
360                }
361            }
362        } else if (representation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY) {
363            // Add geometry filter, removing duplicates and do-nothing operations.
364            for (int i = 0; i < mFilters.size(); i++) {
365                if (sameSerializationName(representation, mFilters.elementAt(i))) {
366                    mFilters.remove(i);
367                }
368            }
369            int index = 0;
370            for (; index < mFilters.size(); index++) {
371                FilterRepresentation rep = mFilters.elementAt(index);
372                if (rep.getFilterType() != FilterRepresentation.TYPE_GEOMETRY) {
373                    break;
374                }
375            }
376            if (!representation.isNil()) {
377                mFilters.insertElementAt(representation, index);
378            }
379        } else if (representation.getFilterType() == FilterRepresentation.TYPE_BORDER) {
380            removeFilter(representation);
381            if (!isNoneBorderFilter(representation)) {
382                mFilters.add(representation);
383            }
384        } else if (representation.getFilterType() == FilterRepresentation.TYPE_FX) {
385            boolean replaced = false;
386            for (int i = 0; i < mFilters.size(); i++) {
387                FilterRepresentation current = mFilters.elementAt(i);
388                if (current.getFilterType() == FilterRepresentation.TYPE_FX) {
389                    mFilters.remove(i);
390                    replaced = true;
391                    if (!isNoneFxFilter(representation)) {
392                        mFilters.add(i, representation);
393                    }
394                    break;
395                }
396            }
397            if (!replaced && !isNoneFxFilter(representation)) {
398                mFilters.add(0, representation);
399            }
400        } else {
401            mFilters.add(representation);
402        }
403        // Enforces Filter type ordering for borders
404        FilterRepresentation border = null;
405        for (int i = 0; i < mFilters.size();) {
406            FilterRepresentation rep = mFilters.elementAt(i);
407            if (rep.getFilterType() == FilterRepresentation.TYPE_BORDER) {
408                border = rep;
409                mFilters.remove(i);
410                continue;
411            }
412            i++;
413        }
414        if (border != null) {
415            mFilters.add(border);
416        }
417    }
418
419    private boolean isNoneBorderFilter(FilterRepresentation representation) {
420        return representation instanceof FilterImageBorderRepresentation &&
421                ((FilterImageBorderRepresentation) representation).getDrawableResource() == 0;
422    }
423
424    private boolean isNoneFxFilter(FilterRepresentation representation) {
425        return representation instanceof FilterFxRepresentation &&
426                ((FilterFxRepresentation) representation).getNameResource() == R.string.none;
427    }
428
429    public FilterRepresentation getRepresentation(FilterRepresentation filterRepresentation) {
430        for (int i = 0; i < mFilters.size(); i++) {
431            FilterRepresentation representation = mFilters.elementAt(i);
432            if (sameSerializationName(representation, filterRepresentation)) {
433                return representation;
434            }
435        }
436        return null;
437    }
438
439    public Bitmap apply(Bitmap original, FilterEnvironment environment) {
440        Bitmap bitmap = original;
441        bitmap = applyFilters(bitmap, -1, -1, environment);
442        return applyBorder(bitmap, environment);
443    }
444
445    public Collection<FilterRepresentation> getGeometryFilters() {
446        ArrayList<FilterRepresentation> geometry = new ArrayList<FilterRepresentation>();
447        for (FilterRepresentation r : mFilters) {
448            if (r.getFilterType() == FilterRepresentation.TYPE_GEOMETRY) {
449                geometry.add(r);
450            }
451        }
452        return geometry;
453    }
454
455    public FilterRepresentation getFilterWithSerializationName(String serializationName) {
456        for (FilterRepresentation r : mFilters) {
457            if (r != null) {
458                if (sameSerializationName(r.getSerializationName(), serializationName)) {
459                    return r.copy();
460                }
461            }
462        }
463        return null;
464    }
465
466    public Rect finalGeometryRect(int width, int height) {
467        return GeometryMathUtils.finalGeometryRect(width, height, getGeometryFilters());
468    }
469
470    public Bitmap applyGeometry(Bitmap bitmap, FilterEnvironment environment) {
471        // Apply any transform -- 90 rotate, flip, straighten, crop
472        // Returns a new bitmap.
473        if (mDoApplyGeometry) {
474            Bitmap bmp = GeometryMathUtils.applyGeometryRepresentations(
475                    getGeometryFilters(), bitmap);
476            if (bmp != bitmap) {
477                environment.cache(bitmap);
478            }
479            return bmp;
480        }
481        return bitmap;
482    }
483
484    public Bitmap applyBorder(Bitmap bitmap, FilterEnvironment environment) {
485        // get the border from the list of filters.
486        FilterRepresentation border = getFilterRepresentationForType(
487                FilterRepresentation.TYPE_BORDER);
488        if (border != null && mDoApplyGeometry) {
489            bitmap = environment.applyRepresentation(border, bitmap);
490            if (environment.getQuality() == FilterEnvironment.QUALITY_FINAL) {
491            }
492        }
493        return bitmap;
494    }
495
496    public int nbFilters() {
497        return mFilters.size();
498    }
499
500    public Bitmap applyFilters(Bitmap bitmap, int from, int to, FilterEnvironment environment) {
501        if (mDoApplyFilters) {
502            if (from < 0) {
503                from = 0;
504            }
505            if (to == -1) {
506                to = mFilters.size();
507            }
508            for (int i = from; i < to; i++) {
509                FilterRepresentation representation = mFilters.elementAt(i);
510                if (representation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY) {
511                    // skip the geometry as it's already applied.
512                    continue;
513                }
514                if (representation.getFilterType() == FilterRepresentation.TYPE_BORDER) {
515                    // for now, let's skip the border as it will be applied in
516                    // applyBorder()
517                    // TODO: might be worth getting rid of applyBorder.
518                    continue;
519                }
520                Bitmap tmp = bitmap;
521                bitmap = environment.applyRepresentation(representation, bitmap);
522                if (tmp != bitmap) {
523                    environment.cache(tmp);
524                }
525                if (environment.needsStop()) {
526                    return bitmap;
527                }
528            }
529        }
530
531        return bitmap;
532    }
533
534    public void applyBorder(Allocation in, Allocation out,
535            boolean copyOut, FilterEnvironment environment) {
536        FilterRepresentation border = getFilterRepresentationForType(
537                FilterRepresentation.TYPE_BORDER);
538        if (border != null && mDoApplyGeometry) {
539            // TODO: should keep the bitmap around
540            Allocation bitmapIn = in;
541            if (copyOut) {
542                bitmapIn = Allocation.createTyped(
543                        CachingPipeline.getRenderScriptContext(), in.getType());
544                bitmapIn.copyFrom(out);
545            }
546            environment.applyRepresentation(border, bitmapIn, out);
547        }
548    }
549
550    public void applyFilters(int from, int to, Allocation in, Allocation out,
551            FilterEnvironment environment) {
552        if (mDoApplyFilters) {
553            if (from < 0) {
554                from = 0;
555            }
556            if (to == -1) {
557                to = mFilters.size();
558            }
559            for (int i = from; i < to; i++) {
560                FilterRepresentation representation = mFilters.elementAt(i);
561                if (representation.getFilterType() == FilterRepresentation.TYPE_GEOMETRY
562                        || representation.getFilterType() == FilterRepresentation.TYPE_BORDER) {
563                    continue;
564                }
565                if (i > from) {
566                    in.copyFrom(out);
567                }
568                environment.applyRepresentation(representation, in, out);
569            }
570        }
571    }
572
573    public boolean canDoPartialRendering() {
574        if (MasterImage.getImage().getZoomOrientation() != ImageLoader.ORI_NORMAL) {
575            return false;
576        }
577        for (int i = 0; i < mFilters.size(); i++) {
578            FilterRepresentation representation = mFilters.elementAt(i);
579            if (!representation.supportsPartialRendering()) {
580                return false;
581            }
582        }
583        return true;
584    }
585
586    public void fillImageStateAdapter(StateAdapter imageStateAdapter) {
587        if (imageStateAdapter == null) {
588            return;
589        }
590        Vector<State> states = new Vector<State>();
591        for (FilterRepresentation filter : mFilters) {
592            if (filter instanceof FilterUserPresetRepresentation) {
593                // do not show the user preset itself in the state panel
594                continue;
595            }
596            State state = new State(filter.getName());
597            state.setFilterRepresentation(filter);
598            states.add(state);
599        }
600        imageStateAdapter.fill(states);
601    }
602
603    public void setPartialRendering(boolean partialRendering, Rect bounds) {
604        mPartialRendering = partialRendering;
605        mPartialRenderingBounds = bounds;
606    }
607
608    public boolean isPartialRendering() {
609        return mPartialRendering;
610    }
611
612    public Rect getPartialRenderingBounds() {
613        return mPartialRenderingBounds;
614    }
615
616    public Vector<ImageFilter> getUsedFilters(BaseFiltersManager filtersManager) {
617        Vector<ImageFilter> usedFilters = new Vector<ImageFilter>();
618        for (int i = 0; i < mFilters.size(); i++) {
619            FilterRepresentation representation = mFilters.elementAt(i);
620            ImageFilter filter = filtersManager.getFilterForRepresentation(representation);
621            usedFilters.add(filter);
622        }
623        return usedFilters;
624    }
625
626    public String getJsonString(String name) {
627        StringWriter swriter = new StringWriter();
628        try {
629            JsonWriter writer = new JsonWriter(swriter);
630            writeJson(writer, name);
631            writer.close();
632        } catch (IOException e) {
633            return null;
634        }
635        return swriter.toString();
636    }
637
638    public void writeJson(JsonWriter writer, String name) {
639        int numFilters = mFilters.size();
640        try {
641            writer.beginObject();
642            for (int i = 0; i < numFilters; i++) {
643                FilterRepresentation filter = mFilters.get(i);
644                if (filter instanceof FilterUserPresetRepresentation) {
645                    continue;
646                }
647                String sname = filter.getSerializationName();
648                if (DEBUG) {
649                    Log.v(LOGTAG, "Serialization: " + sname);
650                    if (sname == null) {
651                        Log.v(LOGTAG, "Serialization name null for filter: " + filter);
652                    }
653                }
654                writer.name(sname);
655                filter.serializeRepresentation(writer);
656            }
657            writer.endObject();
658
659        } catch (IOException e) {
660           Log.e(LOGTAG,"Error encoding JASON",e);
661        }
662    }
663
664    /**
665     * populates preset from JSON string
666     *
667     * @param filterString a JSON string
668     * @return true on success if false ImagePreset is undefined
669     */
670    public boolean readJsonFromString(String filterString) {
671        if (DEBUG) {
672            Log.v(LOGTAG, "reading preset: \"" + filterString + "\"");
673        }
674        StringReader sreader = new StringReader(filterString);
675        try {
676            JsonReader reader = new JsonReader(sreader);
677            boolean ok = readJson(reader);
678            if (!ok) {
679                reader.close();
680                return false;
681            }
682            reader.close();
683        } catch (Exception e) {
684            Log.e(LOGTAG, "\""+filterString+"\"");
685            Log.e(LOGTAG, "parsing the filter parameters:", e);
686            return false;
687        }
688        return true;
689    }
690
691    /**
692     * populates preset from JSON stream
693     *
694     * @param sreader a JSON string
695     * @return true on success if false ImagePreset is undefined
696     */
697    public boolean readJson(JsonReader sreader) throws IOException {
698        sreader.beginObject();
699
700        while (sreader.hasNext()) {
701            String name = sreader.nextName();
702            FilterRepresentation filter = creatFilterFromName(name);
703            if (filter == null) {
704                Log.w(LOGTAG, "UNKNOWN FILTER! " + name);
705                return false;
706            }
707            filter.deSerializeRepresentation(sreader);
708            addFilter(filter);
709        }
710        sreader.endObject();
711        return true;
712    }
713
714    FilterRepresentation creatFilterFromName(String name) {
715        if (FilterRotateRepresentation.SERIALIZATION_NAME.equals(name)) {
716            return new FilterRotateRepresentation();
717        } else if (FilterMirrorRepresentation.SERIALIZATION_NAME.equals(name)) {
718            return new FilterMirrorRepresentation();
719        } else if (FilterStraightenRepresentation.SERIALIZATION_NAME.equals(name)) {
720            return new FilterStraightenRepresentation();
721        } else if (FilterCropRepresentation.SERIALIZATION_NAME.equals(name)) {
722            return new FilterCropRepresentation();
723        }
724        FiltersManager filtersManager = FiltersManager.getManager();
725        return filtersManager.createFilterFromName(name);
726    }
727
728    public void updateWith(ImagePreset preset) {
729        if (preset.mFilters.size() != mFilters.size()) {
730            Log.e(LOGTAG, "Updating a preset with an incompatible one");
731            return;
732        }
733        for (int i = 0; i < mFilters.size(); i++) {
734            FilterRepresentation destRepresentation = mFilters.elementAt(i);
735            FilterRepresentation sourceRepresentation = preset.mFilters.elementAt(i);
736            destRepresentation.useParametersFrom(sourceRepresentation);
737        }
738    }
739}
740