diff --git a/src/androidTest/java/de/dennisguse/opentracks/content/DescriptionGeneratorTest.java b/src/androidTest/java/de/dennisguse/opentracks/content/DescriptionGeneratorTest.java
index 2daf4007d..232b7f620 100644
--- a/src/androidTest/java/de/dennisguse/opentracks/content/DescriptionGeneratorTest.java
+++ b/src/androidTest/java/de/dennisguse/opentracks/content/DescriptionGeneratorTest.java
@@ -88,6 +88,40 @@ public class DescriptionGeneratorTest {
Assert.assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
}
+ @Test
+ public void testGenerateTrackDescriptionWithoutMaxMinElevation() {
+ Track track = new Track();
+ TrackStatistics stats = new TrackStatistics();
+ stats.setTotalDistance(20000);
+ stats.setTotalTime(600000);
+ stats.setMovingTime(300000);
+ stats.setMaxSpeed(100);
+ stats.setMaxElevation(Double.POSITIVE_INFINITY);
+ stats.setMinElevation(Double.NEGATIVE_INFINITY);
+ stats.setTotalElevationGain(6000);
+ stats.setStartTime_ms(START_TIME);
+ track.setTrackStatistics(stats);
+ track.setCategory("hiking");
+ String expected = //"Created by"
+ "OpenTracks
"
+ + "Name: -
"
+ + "Activity type: hiking
"
+ + "Description: -
"
+ + "Total distance: 20.00 km (12.4 mi)
"
+ + "Total time: 10:00
"
+ + "Moving time: 05:00
"
+ + "Average speed: 120.00 km/h (74.6 mi/h)
"
+ + "Average moving speed: 240.00 km/h (149.1 mi/h)
"
+ + "Max speed: 360.00 km/h (223.7 mi/h)
"
+ + "Average pace: 0:30 min/km (0:48 min/mi)
"
+ + "Average moving pace: 0:15 min/km (0:24 min/mi)
"
+ + "Fastest pace: 0:10 min/km (0:16 min/mi)
"
+ + "Elevation gain: 6000 m (19685 ft)
"
+ + "Recorded: " + StringUtils.formatDateTime(context, START_TIME) + "
";
+
+ Assert.assertEquals(expected, descriptionGenerator.generateTrackDescription(track, true));
+ }
+
/**
* Tests {@link DescriptionGenerator#writeDistance(double, StringBuilder, int, String)}.
diff --git a/src/main/java/de/dennisguse/opentracks/content/DescriptionGenerator.java b/src/main/java/de/dennisguse/opentracks/content/DescriptionGenerator.java
index e4933e6f2..be850d530 100644
--- a/src/main/java/de/dennisguse/opentracks/content/DescriptionGenerator.java
+++ b/src/main/java/de/dennisguse/opentracks/content/DescriptionGenerator.java
@@ -126,10 +126,14 @@ public class DescriptionGenerator {
writePace(stats.getMaxSpeed(), builder, R.string.description_fastest_pace_in_minute, lineBreak);
// Max elevation
- writeElevation(stats.getMaxElevation(), builder, R.string.description_max_elevation, lineBreak);
+ if (stats.hasElevationMax()) {
+ writeElevation(stats.getMaxElevation(), builder, R.string.description_max_elevation, lineBreak);
+ }
// Min elevation
- writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak);
+ if (stats.hasElevationMin()) {
+ writeElevation(stats.getMinElevation(), builder, R.string.description_min_elevation, lineBreak);
+ }
// Elevation gain
writeElevation(stats.getTotalElevationGain(), builder, R.string.description_elevation_gain, lineBreak);
diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java
index 393ab19ca..4ed5314f7 100644
--- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java
+++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmlTrackWriter.java
@@ -32,6 +32,7 @@ import de.dennisguse.opentracks.content.data.TrackPoint;
import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.content.sensor.SensorDataSet;
+import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.StringUtils;
/**
@@ -162,9 +163,10 @@ public class KmlTrackWriter implements TrackWriter {
@Override
public void writeWaypoint(Waypoint waypoint) {
if (printWriter != null && exportTrackDetail) {
- if (waypoint.hasPhoto() && exportPhotos) {
+ boolean existsPhoto = FileUtils.getPhotoFileIfExists(context, waypoint.getTrackId(), waypoint.getPhotoURI()) != null;
+ if (waypoint.hasPhoto() && exportPhotos && existsPhoto) {
float heading = getHeading(waypoint.getTrackId(), waypoint.getLocation());
- writePhotoOverlay(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), waypoint.getLocation(), waypoint.getPhotoUrl(), heading);
+ writePhotoOverlay(waypoint, heading);
} else {
writePlacemark(waypoint.getName(), waypoint.getCategory(), waypoint.getDescription(), WAYPOINT_STYLE, waypoint.getLocation());
}
@@ -322,32 +324,29 @@ public class KmlTrackWriter implements TrackWriter {
}
/**
- * Writes a photo overlay.
- * @param name the name
- * @param category the category
- * @param description the description
- * @param location the location
- * @param photoUrl the photo url
- * @param heading the heading
+ * Writes a photo overlay from waypoint.
+ *
+ * @param waypoint Waypoint object.
+ * @param heading the heading.
*/
- private void writePhotoOverlay(String name, String category, String description, Location location, String photoUrl, float heading) {
- if (location != null && exportTrackDetail) {
+ private void writePhotoOverlay(Waypoint waypoint, float heading) {
+ if (exportTrackDetail) {
printWriter.println("");
- printWriter.println("" + StringUtils.formatCData(name) + "");
- printWriter.println("" + StringUtils.formatCData(description) + "");
+ printWriter.println("" + StringUtils.formatCData(waypoint.getName()) + "");
+ printWriter.println("" + StringUtils.formatCData(waypoint.getDescription()) + "");
printWriter.print("");
- printWriter.print("" + location.getLongitude() + "");
- printWriter.print("" + location.getLatitude() + "");
+ printWriter.print("" + waypoint.getLocation().getLongitude() + "");
+ printWriter.print("" + waypoint.getLocation().getLatitude() + "");
printWriter.print("20");
printWriter.print("" + heading + "");
printWriter.print("90");
printWriter.println("");
- printWriter.println("" + getTime(location) + "");
+ printWriter.println("" + getTime(waypoint.getLocation()) + "");
printWriter.println("#" + KmlTrackWriter.WAYPOINT_STYLE + "");
- writeCategory(category);
+ writeCategory(waypoint.getCategory());
if (exportPhotos) {
- printWriter.println("" + KmzTrackExporter.buildKmzImageFilePath(Uri.parse(photoUrl)) + "");
+ printWriter.println("" + KmzTrackExporter.buildKmzImageFilePath(waypoint) + "");
}
printWriter.print("");
@@ -358,7 +357,7 @@ public class KmlTrackWriter implements TrackWriter {
printWriter.print("45");
printWriter.println("");
printWriter.println("");
- printWriter.println("" + getCoordinates(location, ",") + "");
+ printWriter.println("" + getCoordinates(waypoint.getLocation(), ",") + "");
printWriter.println("");
printWriter.println("");
}
diff --git a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmzTrackExporter.java b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmzTrackExporter.java
index ef24556dc..ca4fc59a2 100644
--- a/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmzTrackExporter.java
+++ b/src/main/java/de/dennisguse/opentracks/io/file/exporter/KmzTrackExporter.java
@@ -112,7 +112,11 @@ public class KmzTrackExporter implements TrackExporter {
}
Waypoint waypoint = contentProviderUtils.createWaypoint(cursor);
if (waypoint.hasPhoto()) {
- addImage(context, zipOutputStream, waypoint.getPhotoUrl());
+ Uri uriPhoto = waypoint.getPhotoURI();
+ boolean existsPhoto = FileUtils.getPhotoFileIfExists(context, track.getId(), uriPhoto) != null;
+ if (existsPhoto) {
+ addImage(context, zipOutputStream, uriPhoto, waypoint);
+ }
}
cursor.moveToNext();
@@ -122,11 +126,9 @@ public class KmzTrackExporter implements TrackExporter {
}
}
- private void addImage(Context context, ZipOutputStream zipOutputStream, String photoUrl) throws IOException {
- Uri uri = Uri.parse(photoUrl);
-
+ private void addImage(Context context, ZipOutputStream zipOutputStream, Uri uri, Waypoint waypoint) throws IOException {
try (InputStream inputStream = context.getContentResolver().openInputStream(uri)) {
- ZipEntry zipEntry = new ZipEntry(buildKmzImageFilePath(uri));
+ ZipEntry zipEntry = new ZipEntry(buildKmzImageFilePath(waypoint));
zipOutputStream.putNextEntry(zipEntry);
if (inputStream == null) throw new FileNotFoundException();
@@ -149,11 +151,13 @@ public class KmzTrackExporter implements TrackExporter {
}
/**
- * Builds and returns the path for the file that will be saved inside KMZ_IMAGES_DIR.
+ * Builds and returns the path for the image that will be saved inside KMZ_IMAGES_DIR for the waypoint.
*
- * @param uri URI object.
+ * @param waypoint Waypoint object.
*/
- public static String buildKmzImageFilePath(Uri uri) {
- return KMZ_IMAGES_DIR + File.separatorChar + FileUtils.sanitizeFileName(uri.getLastPathSegment());
+ public static String buildKmzImageFilePath(Waypoint waypoint) {
+ String ext = FileUtils.getExtension(waypoint.getPhotoUrl());
+ ext = ext == null ? "" : "." + ext;
+ return KMZ_IMAGES_DIR + File.separatorChar + FileUtils.sanitizeFileName(waypoint.getId() + ext);
}
}
\ No newline at end of file
diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/AbstractFileTrackImporter.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/AbstractFileTrackImporter.java
index 9c056342e..00dd2393b 100644
--- a/src/main/java/de/dennisguse/opentracks/io/file/importer/AbstractFileTrackImporter.java
+++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/AbstractFileTrackImporter.java
@@ -313,6 +313,9 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
*/
protected TrackPoint getTrackPoint() throws SAXException {
TrackPoint trackPoint = createTrackPoint();
+ if (trackPoint == null) {
+ throw new SAXException(createErrorMessage("Invalid location detected: " + trackPoint));
+ }
// Calculate derived attributes from the previous point
if (trackData.lastLocationInCurrentSegment != null && trackData.lastLocationInCurrentSegment.getTime() != 0) {
@@ -380,23 +383,15 @@ abstract class AbstractFileTrackImporter extends DefaultHandler implements Track
return null;
}
- Uri externalPhotoUri = Uri.parse(externalPhotoUrl);
- if (externalPhotoUri == null) {
- Log.w(TAG, "Could not parse external photo url.");
+ String importFileName = KmzTrackImporter.importNameForFilename(externalPhotoUrl);
+ File file = FileUtils.getPhotoFileIfExists(context, importTrackId, Uri.parse(importFileName));
+ if (file != null) {
+ Uri photoUri = FileUtils.getUriForFile(context, file);
+ return "" + photoUri;
+ }
+ else {
return null;
}
- String filename = externalPhotoUri.getLastPathSegment();
- if (filename == null) {
- Log.w(TAG, "External photo contains no filename.");
- return null;
- }
-
- File dir = FileUtils.getPhotoDir(context, importTrackId);
- File file = new File(dir, filename);
-
- Uri photoUri = FileUtils.getUriForFile(context, file);
-
- return "" + photoUri;
}
/**
diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportAsyncTask.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportAsyncTask.java
index 5c24c0ad2..96a701d9b 100644
--- a/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportAsyncTask.java
+++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/ImportAsyncTask.java
@@ -99,7 +99,7 @@ class ImportAsyncTask extends AsyncTask {
for (DocumentFile candidate : file.listFiles()) {
if (!candidate.isDirectory()) {
String extension = FileUtils.getExtension(candidate.getName());
- if (trackFileFormat.getExtension().equals(extension)) {
+ if (extension != null && trackFileFormat.getExtension().equals(extension)) {
files.add(candidate);
}
} else {
@@ -186,7 +186,7 @@ class ImportAsyncTask extends AsyncTask {
Uri uri = contentProviderUtils.insertTrack(new Track());
long newId = Long.parseLong(uri.getLastPathSegment());
- trackImporter = new KmzTrackImporter(importActivity, newId);
+ trackImporter = new KmzTrackImporter(importActivity, newId, file.getUri());
}
}
diff --git a/src/main/java/de/dennisguse/opentracks/io/file/importer/KmzTrackImporter.java b/src/main/java/de/dennisguse/opentracks/io/file/importer/KmzTrackImporter.java
index 3db706bdd..beda05523 100644
--- a/src/main/java/de/dennisguse/opentracks/io/file/importer/KmzTrackImporter.java
+++ b/src/main/java/de/dennisguse/opentracks/io/file/importer/KmzTrackImporter.java
@@ -17,6 +17,7 @@
package de.dennisguse.opentracks.io.file.importer;
import android.content.Context;
+import android.net.Uri;
import android.util.Log;
import java.io.ByteArrayInputStream;
@@ -25,9 +26,13 @@ import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
+import de.dennisguse.opentracks.content.data.Waypoint;
import de.dennisguse.opentracks.content.provider.ContentProviderUtils;
import de.dennisguse.opentracks.io.file.exporter.KmzTrackExporter;
import de.dennisguse.opentracks.util.FileUtils;
@@ -42,58 +47,190 @@ public class KmzTrackImporter implements TrackImporter {
private static final String TAG = KmzTrackImporter.class.getSimpleName();
+ public static final List KMZ_IMAGES_EXT = Arrays.asList("jpeg", "jpg", "png");
+
private static final int BUFFER_SIZE = 4096;
private final Context context;
private final long importTrackId;
+ private Uri uriKmzFile;
/**
* Constructor.
*
* @param context the context
* @param importTrackId track id to import to. This should not be -1L so that images in the kmz file can be imported.
+ * @param uriFile URI of the kmz file.
*/
- KmzTrackImporter(Context context, long importTrackId) {
+ KmzTrackImporter(Context context, long importTrackId, Uri uriFile) {
this.context = context;
this.importTrackId = importTrackId;
+ this.uriKmzFile = uriFile;
}
@Override
public long importFile(InputStream inputStream) {
- long trackId = importTrackId;
- try (ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
+ long trackId;
+
+ if (!copyKmzImages()) {
+ cleanImport(context, importTrackId);
+ return -1L;
+ }
+
+ trackId = findAndParseKmlFile(inputStream);
+ if (trackId == -1L) {
+ cleanImport(context, importTrackId);
+ return -1L;
+ }
+
+ deleteOrphanImages(context, trackId);
+
+ return trackId;
+ }
+
+ /**
+ * Copies all images that are inside KMZ to OpenTracks external storage.
+ *
+ * @return false if there are errors or true otherwise.
+ */
+ private boolean copyKmzImages() {
+ try (InputStream inputStream = context.getContentResolver().openInputStream(uriKmzFile);
+ ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
ZipEntry zipEntry;
while ((zipEntry = zipInputStream.getNextEntry()) != null) {
if (Thread.interrupted()) {
Log.d(TAG, "Thread interrupted");
- cleanImport(context, trackId);
+ return false;
+ }
+
+ String fileName = zipEntry.getName();
+ if (hasImageExtension(fileName)) {
+ readAndSaveImageFile(zipInputStream, importNameForFilename(fileName));
+ }
+
+ zipInputStream.closeEntry();
+ }
+
+ return true;
+ } catch (IOException e) {
+ Log.e(TAG, "Unable to import file", e);
+ return false;
+ }
+ }
+
+ /**
+ * From path fileName generates an import unique name and returns it.
+ * The name generator is simple: change the path fileName with '-' instead of File.separatorChar.
+ *
+ * @param fileName the file name.
+ */
+ public static String importNameForFilename(String fileName) {
+ // TODO this tricky code for maintain backward compatibility must be deleted some day.
+ /*
+ * In versions before v3.5.0 photo URL in KML files were wrong.
+ * For compatibility reasons it checks if fileName begins with "content://" or "file://".
+ * All fileName begins with "content:/" or "file://" are cooked.
+ * We cannot guess what's the folder name where images are so we use "images" that was the folder name expected in versions before v3.5.0.
+ */
+ if (fileName.startsWith("content://") || fileName.startsWith("file://")) {
+ fileName = "images/" + fileName.substring(fileName.lastIndexOf(File.separatorChar) + 1);
+ }
+
+ return fileName.replace(File.separatorChar, '-');
+ }
+
+ /**
+ * Returns true if fileName ends with some of the KMZ_IMAGES_EXT suffixes.
+ * Otherwise returns false.
+ */
+ private boolean hasImageExtension(String fileName) {
+ if (fileName == null) {
+ return false;
+ }
+
+ String fileExt = FileUtils.getExtension(fileName.toLowerCase());
+ if (fileExt == null) {
+ return false;
+ }
+
+ if (KMZ_IMAGES_EXT.contains(fileExt)) {
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Finds KmzTrackExporter.KMZ_KML_FILE file inside kmz file (inputStream) and it parses it.
+ *
+ * @param inputStream kmz input stream.
+ * @return -1 if error or the id of the track otherwise.
+ */
+ private long findAndParseKmlFile(InputStream inputStream) {
+ try (ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
+ ZipEntry zipEntry;
+ long trackId = -1L;
+
+ while ((zipEntry = zipInputStream.getNextEntry()) != null) {
+ if (Thread.interrupted()) {
+ Log.d(TAG, "Thread interrupted");
return -1L;
}
+
String fileName = zipEntry.getName();
if (KmzTrackExporter.KMZ_KML_FILE.equals(fileName)) {
trackId = parseKml(zipInputStream);
if (trackId == -1L) {
Log.d(TAG, "Unable to parse kml in kmz");
- cleanImport(context, trackId);
return -1L;
}
- } else {
- String prefix = KmzTrackExporter.KMZ_IMAGES_DIR + File.separatorChar;
- if (fileName.startsWith(prefix)) {
- readImageFile(zipInputStream, fileName.substring(prefix.length()));
- }
}
+
zipInputStream.closeEntry();
}
+
return trackId;
} catch (IOException e) {
Log.e(TAG, "Unable to import file", e);
- cleanImport(context, trackId);
return -1L;
}
}
+ /**
+ * Deletes all images that remained in external storage that doesn't have waypoint (marker) associated.
+ *
+ * @param context the Context object.
+ * @param trackId the id of the Track.
+ */
+ private void deleteOrphanImages(Context context, long trackId) {
+ if (trackId != 1L) {
+ // 1.- Gets all photo names in the waypoints of the track identified by id.
+ ContentProviderUtils contentProviderUtils = new ContentProviderUtils(context);
+ List waypoints = contentProviderUtils.getWaypoints(trackId);
+ List photosName = new ArrayList<>();
+ for (Waypoint w : waypoints) {
+ if (w.hasPhoto()) {
+ String photoUrl = Uri.decode(w.getPhotoUrl());
+ photosName.add(photoUrl.substring(photoUrl.lastIndexOf(File.separatorChar) + 1));
+ }
+ }
+
+ // 2.- Deletes all orphan photos from external storage.
+ File dir = FileUtils.getPhotoDir(context, trackId);
+ if (dir.exists() && dir.isDirectory()) {
+ for (File file : dir.listFiles()) {
+ if (!photosName.contains(file.getName())) {
+ file.delete();
+ }
+ }
+ if (dir.listFiles().length == 0) {
+ dir.delete();
+ }
+ }
+ }
+ }
+
/**
* Cleans up import.
*
@@ -147,12 +284,12 @@ public class KmzTrackImporter implements TrackImporter {
}
/**
- * Reads an image file.
+ * Reads an image file (zipInputStream) and save it in a file called fileName inside photo folder.
*
* @param zipInputStream the zip input stream
* @param fileName the file name
*/
- private void readImageFile(ZipInputStream zipInputStream, String fileName) throws IOException {
+ private void readAndSaveImageFile(ZipInputStream zipInputStream, String fileName) throws IOException {
if (importTrackId == -1L || fileName.equals("")) {
return;
}
diff --git a/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java b/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java
index 37cd6b273..fa4fb271d 100644
--- a/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java
+++ b/src/main/java/de/dennisguse/opentracks/stats/TrackStatistics.java
@@ -176,6 +176,10 @@ public class TrackStatistics {
this.maxSpeed_mps = maxSpeed;
}
+ public boolean hasElevationMin() {
+ return !Double.isInfinite(getMinElevation());
+ }
+
/**
* Gets the minimum elevation.
* This is calculated from the smoothed elevation, so this can actually be more than the current elevation.
@@ -193,6 +197,10 @@ public class TrackStatistics {
elevationExtremities.setMin(elevation);
}
+ public boolean hasElevationMax() {
+ return !Double.isInfinite(getMaxElevation());
+ }
+
/**
* Gets the maximum elevation.
* This is calculated from the smoothed elevation, so this can actually be less than the current elevation.
diff --git a/src/main/java/de/dennisguse/opentracks/util/FileUtils.java b/src/main/java/de/dennisguse/opentracks/util/FileUtils.java
index 71368a196..3a609b247 100644
--- a/src/main/java/de/dennisguse/opentracks/util/FileUtils.java
+++ b/src/main/java/de/dennisguse/opentracks/util/FileUtils.java
@@ -95,11 +95,15 @@ public class FileUtils {
/**
* Gets the extension from a file name.
- * Returns null if there is no extension.
*
* @param fileName the file name
+ * @return null if there is no extension or fileName is null.
*/
public static String getExtension(String fileName) {
+ if (fileName == null) {
+ return null;
+ }
+
int index = fileName.lastIndexOf('.');
if (index == -1) {
return null;
@@ -250,4 +254,34 @@ public class FileUtils {
public static Uri getUriForFile(Context context, File file) {
return FileProvider.getUriForFile(context, FileUtils.FILEPROVIDER, file);
}
+
+ /**
+ * Checks that there is a file inside track photo directory whose name is the same that uri file.
+ * If there is a file inside photo directory whose name is the same that uri then returns File. Otherwise returns null.
+ *
+ * @param context the Context.
+ * @param trackId the id of the Track.
+ * @param uri the uri to check.
+ * @return File object or null.
+ */
+ public static File getPhotoFileIfExists(Context context, long trackId, Uri uri) {
+ if (uri == null) {
+ Log.w(TAG, "URI object is null.");
+ return null;
+ }
+
+ String filename = uri.getLastPathSegment();
+ if (filename == null) {
+ Log.w(TAG, "External photo contains no filename.");
+ return null;
+ }
+
+ File dir = FileUtils.getPhotoDir(context, trackId);
+ File file = new File(dir, filename);
+ if (!file.exists()) {
+ return null;
+ }
+
+ return file;
+ }
}