/* Copyright (C) 2023-2024 José Rebelo
This file is part of Gadgetbridge.
Gadgetbridge is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Gadgetbridge is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.zeppos.operations;
import android.location.Location;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.model.GPSCoordinate;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.GpxParser;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxFile;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxTrackPoint;
import nodomain.freeyourgadget.gadgetbridge.util.gpx.model.GpxWaypoint;
public class ZeppOsGpxRouteFile {
private static final Logger LOG = LoggerFactory.getLogger(ZeppOsGpxRouteFile.class);
private static final double COORD_MULTIPLIER = 3000000.0;
private final byte[] xmlBytes;
private final long timestamp;
private final GpxFile gpxFile;
public ZeppOsGpxRouteFile(final byte[] xmlBytes) {
this.xmlBytes = xmlBytes;
this.timestamp = System.currentTimeMillis() / 1000;
this.gpxFile = GpxParser.parseGpx(xmlBytes);
}
public boolean isValid() {
return this.gpxFile != null;
}
public long getTimestamp() {
return timestamp;
}
public String getName() {
if (gpxFile == null) {
return "";
}
if (!StringUtils.isNullOrEmpty(gpxFile.getName())) {
return gpxFile.getName();
} else {
return String.valueOf(getTimestamp());
}
}
public byte[] getEncodedBytes() {
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
if (gpxFile == null) {
LOG.error("Failed to read gpx file - this should never happen");
return null;
}
final List trackPoints = gpxFile.getPoints();
final List waypoints = gpxFile.getWaypoints();
double minLatitude = 180;
double maxLatitude = -180;
double minLongitude = 180;
double maxLongitude = -180;
double minAltitude = 10000;
double maxAltitude = -10000;
for (final GPSCoordinate coord : trackPoints) {
minLatitude = Math.min(minLatitude, coord.getLatitude());
maxLatitude = Math.max(maxLatitude, coord.getLatitude());
minLongitude = Math.min(minLongitude, coord.getLongitude());
maxLongitude = Math.max(maxLongitude, coord.getLongitude());
minAltitude = Math.min(minAltitude, coord.getAltitude());
maxAltitude = Math.max(maxAltitude, coord.getAltitude());
}
try {
baos.write(BLETypeConversions.fromUint32(0)); // ?
baos.write(BLETypeConversions.fromUint32(0x54)); // ?
baos.write(BLETypeConversions.fromUint32(0x01)); // ?
baos.write(BLETypeConversions.fromUint32((int) timestamp));
baos.write(BLETypeConversions.fromUint32((int) (minLatitude * COORD_MULTIPLIER)));
baos.write(BLETypeConversions.fromUint32((int) (maxLatitude * COORD_MULTIPLIER)));
baos.write(BLETypeConversions.fromUint32((int) (minLongitude * COORD_MULTIPLIER)));
baos.write(BLETypeConversions.fromUint32((int) (maxLongitude * COORD_MULTIPLIER)));
baos.write(BLETypeConversions.fromUint32((int) minAltitude));
baos.write(BLETypeConversions.fromUint32((int) maxAltitude));
baos.write(truncatePadString(getName()));
baos.write(BLETypeConversions.fromUint32(0)); // ?
if (!waypoints.isEmpty()) {
baos.write(BLETypeConversions.fromUint32(2));
baos.write(BLETypeConversions.fromUint32(waypoints.size() * 68));
for (final GpxWaypoint waypoint : waypoints) {
baos.write(BLETypeConversions.fromUint32(0x1a)); // ?
baos.write(BLETypeConversions.fromUint32((int) (waypoint.getLatitude() * COORD_MULTIPLIER)));
baos.write(BLETypeConversions.fromUint32((int) (waypoint.getLongitude() * COORD_MULTIPLIER)));
baos.write(BLETypeConversions.fromUint32((int) waypoint.getAltitude()));
baos.write(truncatePadString(waypoint.getName()));
baos.write(BLETypeConversions.fromUint32(0)); // ?
}
}
baos.write(BLETypeConversions.fromUint32(1)); // ?
baos.write(BLETypeConversions.fromUint32(trackPoints.size() * 14));
// Keep track of the total distance
double totalDist = 0;
GPSCoordinate prevPoint = trackPoints.isEmpty() ? null : trackPoints.get(0);
for (final GPSCoordinate point : trackPoints) {
totalDist += distanceBetween(prevPoint, point);
baos.write(BLETypeConversions.fromUint32((int) totalDist));
baos.write(BLETypeConversions.fromUint32((int) (point.getLatitude() * COORD_MULTIPLIER)));
baos.write(BLETypeConversions.fromUint32((int) (point.getLongitude() * COORD_MULTIPLIER)));
baos.write(BLETypeConversions.fromUint16((int) point.getAltitude()));
prevPoint = point;
}
} catch (final IOException e) {
LOG.error("Failed to encode gpx file", e);
return null;
}
return baos.toByteArray();
}
public static double distanceBetween(final GPSCoordinate a, final GPSCoordinate b) {
final Location start = new Location("start");
start.setLatitude(a.getLatitude());
start.setLongitude(a.getLongitude());
final Location end = new Location("end");
end.setLatitude(b.getLatitude());
end.setLongitude(b.getLongitude());
return end.distanceTo(start);
}
/**
* Truncates / pads a string to 48 bytes (including null terminator).
*/
public static byte[] truncatePadString(final String s) {
final ByteBuffer buf = ByteBuffer.allocate(48);
buf.put(StringUtils.truncateToBytes(s, 47));
return buf.array();
}
}