'how to make gpx file in android
I wanna write gpx file with DOM and Transformer
My code is like that
try {
val document =
DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument()
val trkpt = document.createElement("trkpt")
trkpt.setAttribute("lat", "-33.626932")
trkpt.setAttribute("lon", "-33.626932")
val ele = document.createElement("ele")
ele.appendChild(document.createTextNode("-6"))
trkpt.appendChild(ele)
document.appendChild(trkpt)
val transformer = TransformerFactory.newInstance().newTransformer()
transformer.setOutputProperty(OutputKeys.METHOD,"gpx")
val saveFolder = File(folderPath) // 저장 경로
if (!saveFolder.exists()) { //폴더 없으면 생성
saveFolder.mkdir()
}
val path = "route_${System.currentTimeMillis()}.gpx"
val file = File(saveFolder, path) //로컬에 파일저장
val source = DOMSource(document)
//val result = StreamResult(FileOutputStream(file))
val result = StreamResult(System.out)
transformer.transform(source, result)
return Uri.fromFile(file)
}catch(e:Exception){
e.printStackTrace()
}
the out put is like this
<?xml version="1.0" encoding="UTF-8"?><trkpt lat="-33.626932" lon="-33.626932"><ele>-6</ele></trkpt>
But I wanna change the tag to , like this
<gpx xmlns="http://www.topografix.com/GPX/1/1" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" creator="TraceDeTrail http://www.tracedetrail.fr" version="1.1" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd ">
How can I edit like this, the attributes as well
the reason why I try to change from JPX to myself, is i need to get time from waypoint. enter image description here
Sometimes, I need to get time from WayPoint class, but the time's type is ZonedDateTime. but it's not work on SDK 24... is there any solution get time from waypoint?
I add the ThreeTenABP, but I don't know how I exactly use this. I add library on Gradle and init in app-instance but it still makes an error
wpList.add(
WayPoint.builder()
.lat(currentLatLng.latitude)
.lon(currentLatLng.longitude)
.name("Start")
.desc("Start Description")
.time(System.currentTimeMillis()) <- RunningActivity.kt:107
.type(START_POINT)
.build()
)
2020-05-06 16:46:16.895 8877-8877/com.umpa2020.tracer E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.umpa2020.tracer, PID: 8877
java.lang.NoClassDefFoundError: Failed resolution of: Ljava/time/Instant;
at io.jenetics.jpx.WayPoint$Builder.time(WayPoint.java:767)
at com.umpa2020.tracer.main.start.running.RunningActivity.start(RunningActivity.kt:107)
at com.umpa2020.tracer.main.start.running.RunningActivity.onSingleClick(RunningActivity.kt:175)
at com.umpa2020.tracer.util.OnSingleClickListener$DefaultImpls.onClick(OnSingleClickListener.kt:19)
at com.umpa2020.tracer.main.start.BaseRunningActivity.onClick(BaseRunningActivity.kt:45)
at android.view.View.performClick(View.java:5610)
at android.view.View$PerformClick.run(View.java:22265)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6077)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)
Caused by: java.lang.ClassNotFoundException: Didn't find class "java.time.Instant" on path: DexPathList[[zip file "/data/app/com.umpa2020.tracer-2/base.apk"],nativeLibraryDirectories=[/data/app/com.umpa2020.tracer-2/lib/x86, /system/lib, /vendor/lib]]
at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)
at java.lang.ClassLoader.loadClass(ClassLoader.java:380)
at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
at io.jenetics.jpx.WayPoint$Builder.time(WayPoint.java:767)
at com.umpa2020.tracer.main.start.running.RunningActivity.start(RunningActivity.kt:107)
at com.umpa2020.tracer.main.start.running.RunningActivity.onSingleClick(RunningActivity.kt:175)
at com.umpa2020.tracer.util.OnSingleClickListener$DefaultImpls.onClick(OnSingleClickListener.kt:19)
at com.umpa2020.tracer.main.start.BaseRunningActivity.onClick(BaseRunningActivity.kt:45)
at android.view.View.performClick(View.java:5610)
at android.view.View$PerformClick.run(View.java:22265)
at android.os.Handler.handleCallback(Handler.java:751)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:154)
at android.app.ActivityThread.main(ActivityThread.java:6077)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)
Solution 1:[1]
gpx is a special xml essentially. What you're talking about is the XML Namespace.
How to create it: ho to create XML header
A xml file can only have one root element, but there are so many trkpt
elements in a gpx file. So trkpt
item should not be a root element. You should include all trkpt
elements in a single gpx
element, which is the root element of your gpx file.
To generate a gpx file in Android, the better choice is using a library. Most of them can help you conduct all the above operations.
I'm using io.jenetics.jpx
in my android project and it works fine.
XML doesn't support appending elements. If you want to generate a gpx file while logging, you should do this in an appendable form, such as any Serializable
type. After finishing logging you can create a valid GPX file from the Serialization file.
The WayPoint
in jpx is a Serializable type, I'm using it.
You can also use Android Location
, which implements Parcelable
, the android implementation like Serializable.
my code:
public class TrackService extends Service {
private int notificationId = 142857 ;
NotificationCompat.Builder builder;
NotificationManagerCompat notificationManager;
private Timer timer = new Timer(true);
private static final String CHANNEL_ID = "org.kib.qtp";
private File serializerFile;
private ObjectOutputStream oos;
private Long basetime;
SimpleDateFormat sdf;
@Override
public void onCreate(){
super.onCreate();
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0);
builder = new NotificationCompat.Builder(this, CHANNEL_ID)
.setSmallIcon(R.drawable.ic_map_black_24dp)
.setContentTitle(getString(R.string.track))
.setContentText(getString(R.string.tracktext))
.setPriority(NotificationCompat.PRIORITY_LOW)
.setOngoing(true)
.setOnlyAlertOnce(true)
.setContentIntent(pendingIntent);
notificationManager = NotificationManagerCompat.from(this);
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
startForeground (notificationId, builder.build());
else
notificationManager.notify(notificationId, builder.build());
}
@Override
public void onDestroy(){
try {
endTrack();
} catch (IOException e) {
e.printStackTrace();
}
stopForeground(true);
super.onDestroy();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId){
/**
* @param ele require altitude or not
* @param time update time
* @param fine fine location or not
* @param name the gpx file's name
*/
sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", getResources(). getConfiguration().locale);
int time = 60000;
boolean ele = false;
boolean fine = false;
String name = Calendar.getInstance().getTime().toString().replaceAll(" ", "_");
if (intent.hasExtra("time"))
Objects.requireNonNull(intent.getExtras()).getInt("time");
if (intent.hasExtra("ele"))
Objects.requireNonNull(intent.getExtras()).getBoolean("ele");
if (intent.hasExtra("fine"))
Objects.requireNonNull(intent.getExtras()).getBoolean("fine");
if (intent.hasExtra("name"))
Objects.requireNonNull(intent.getExtras()).getString("name");
File path = new File(getFilesDir().getAbsolutePath() + "/gpx");
while(!path.exists())
path.mkdirs();
try {
serializerFile = new File(getFilesDir().getAbsolutePath() + "/gpx/" + name );
if (serializerFile.exists())
serializerFile.delete();
while(!serializerFile.exists())
serializerFile.createNewFile();
oos = new ObjectOutputStream(new FileOutputStream(serializerFile));
} catch (IOException e) {
e.printStackTrace();
}
basetime = 0L;
Toast.makeText(this, R.string.toast_start_tracking, Toast.LENGTH_SHORT).show();
startTrack(time, ele, fine);
return START_STICKY;
}
private void endTrack() throws IOException {
timer.cancel();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(serializerFile));
TrackSegment.Builder segment = TrackSegment.builder();
while (true) {
try{
WayPoint wp = (WayPoint) ois.readObject();
segment.addPoint(wp);
} catch (EOFException e) {
break;
} catch (ClassNotFoundException ignored){
}
}
final GPX gpx = GPX.builder().addTrack(track -> track.addSegment(segment.build())).build();
GPX.write(gpx, serializerFile.getAbsolutePath() + ".gpx");
Toast.makeText(this, R.string.toast_gpx_write, Toast.LENGTH_SHORT).show();
new File(serializerFile.getAbsolutePath() + ".tobeupload").createNewFile();;
}
private void startTrack(int time, boolean ele, boolean fine) {
TimerTask task = new TimerTask() {
public void run() {
Location location = getLocation(ele, fine);
if (location != null){
try {
if (location.getTime() != basetime){
basetime = location.getTime();
WayPoint point = WayPoint.builder().lat(location.getLatitude()).lon(location.getLongitude()).ele(location.getAltitude()).time(location.getTime()).build();
if (point != null){
oos.writeObject(point);
NotificationCompat.Builder timerBuilder = builder.setContentText(getString(R.string.tracktext) + sdf.format(location.getTime()));
Log.i("track", location.getLatitude() +","+ location.getLongitude() +","+ location.getAltitude());
notificationManager.notify(notificationId,timerBuilder.build());
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
};
timer.schedule(task, 0, time);
}
private Location getLocation(boolean ele, boolean fine) {
//get location here
}
}
jpx includes zonedDateTime class, which works in java8. But it was backported to Java 6 by its creator, Stephen Colebourne, and also backported to early android version as a library.
Library is here: JakeWharton/ThreeTenABP - An adaptation of the JSR-310 backport for Android.
And how to use: How to use ThreeTenABP in Android Project - StackOverFlow
Solution 2:[2]
I have created a bunch of extension functions for the XmlSerializer class to simplify creating GPX files. No need to install 3rd-party libraries.
Example:
import org.xmlpull.v1.XmlSerializer
val xml = Xml.newSerializer()
xml.gpxDocument(stream, context.getString(R.string.app_name))
{
metaData(name = "filename.gpx", time = now())
track("Track Name", "Track Type") {
trackSegment {
extensions {name("Track Name")}
trackPoint(lat, long)
...
trackPoint(lat, lon)
}
}
}
Extension Functions:
val fixType = listOf("none", "2d", "3d", "dgps", "pps")
data class Email(val id: String?, val domain: String?, val email: String?)
data class Link(val href: String, val text: String? = null, val type: String? = null)
data class Person(val name: String? = null, val email: Email? = null, val link: Link? = null)
data class Bounds(val minLat: Double, val minLon: Double, val maxLat: Double, val maxLon: Double)
fun XmlSerializer.document(stream: OutputStream,
encoding: String = "UTF-8",
init: XmlSerializer.() -> Unit)
{
setOutput(stream, encoding)
startDocument(encoding, true)
init()
endDocument()
}
fun XmlSerializer.gpxDocument(stream: OutputStream,
creator: String,
version: String = "1.1",
encoding: String = "UTF-8",
init: XmlSerializer.() -> Unit)
{
document(stream, encoding)
{
element("gpx")
{
attribute("xmlns", "http://www.topografix.com/GPX/1/1")
attribute("xmlns:xml", "http://www.w3.org/XML/1998/namespace")
attribute("xmlns:xsd", "http://www.w3.org/2001/XMLSchema")
attribute("version", version)
attribute("creator", creator)
init()
}
}
}
// element
fun XmlSerializer.element(name: String,
nameSpace: String? = null,
init: XmlSerializer.() -> Unit)
{
startTag(nameSpace, name)
init()
endTag(nameSpace, name)
}
// element with attribute & content
fun XmlSerializer.element(name: String,
content: String,
nameSpace: String? = null,
init: XmlSerializer.() -> Unit)
{
startTag(nameSpace, name)
init()
text(content)
endTag(nameSpace, name)
}
// element with content
fun XmlSerializer.element(name: String,
content: String,
nameSpace: String? = null) =
element(name, nameSpace) {
text(content)
}
fun XmlSerializer.attribute(name: String, value: String): XmlSerializer =
attribute(null, name, value)
fun XmlSerializer.track(name: String? = null,
type: String? = null,
singleSegment: Boolean = false,
init: XmlSerializer.() -> Unit)
{
element("trk")
{
name(name)
type(type)
if (singleSegment)
trackSegment { init() }
else
init()
}
}
fun XmlSerializer.trackSegment(init: XmlSerializer.() -> Unit)
{
element("trkseg")
{
init()
}
}
fun XmlSerializer.route(name: String? = null,
type: String? = null,
init: XmlSerializer.() -> Unit)
{
element("rte")
{
name(name)
type(type)
init()
}
}
fun XmlSerializer.extensions(nameSpace: String? = null, init: XmlSerializer.() -> Unit)
{
element("extensions", nameSpace)
{
init()
}
}
private fun XmlSerializer.point(pointType: String,
lat: Double,
lon: Double,
elevation: Double? = null,
geoidHeight: Double? = null,
time: Instant? = null,
type: String? = null,
bearing: Float? = null,
speed: Float? = null)
{
if (lat < -90.0 || lat > 90.0 || lon < -180.0 || lon > 180.0) return
element(pointType)
{
attribute("lat", "$lat")
attribute("lon", "$lon")
if (elevation != null) element("ele", "$elevation")
if (time != null) element("time", "$time")
if (geoidHeight != null) element("geoidheight", "$geoidHeight")
type(type)
if (bearing != null || speed != null)
extensions {
if (bearing != null) element("bearing", "$bearing")
if (speed != null) element("speed", "$speed")
}
}
}
fun XmlSerializer.routePoint(lat: Double,
lon: Double,
elevation: Double? = null,
geoidHeight: Double? = null,
time: Instant? = null,
type: String? = null,
bearing: Float? = null,
speed: Float? = null) =
point("rtept", lat, lon, elevation, geoidHeight, time, type, bearing, speed)
fun XmlSerializer.trackPoint(lat: Double,
lon: Double,
elevation: Double? = null,
geoidHeight: Double? = null,
time: Instant? = null,
type: String? = null,
bearing: Float? = null,
speed: Float? = null) =
point("trkpt", lat, lon, elevation, geoidHeight, time, type, bearing, speed)
fun XmlSerializer.wayPoint(lat: Double,
lon: Double,
elevation: Double? = null,
geoidHeight: Double? = null,
time: Instant? = null,
type: String? = null,
bearing: Float? = null,
speed: Float? = null) =
point("wpt", lat, lon, elevation, geoidHeight, time, type, bearing, speed)
fun XmlSerializer.metaData(name: String? = null,
description: String? = null,
author: Person? = null,
copyright: String? = null,
link: Link? = null,
time: Instant? = null,
keywords: String? = null,
bounds: Bounds? = null)
{
element("metadata") {
name(name)
description(description)
author(author)
copyright(copyright)
link(link)
time(time)
keywords(keywords)
bounds(bounds)
}
}
fun XmlSerializer.bounds(bounds: Bounds?)
{
if (bounds != null)
element("Bounds") {
attribute("minlat", bounds.minLat.toString())
attribute("minlon", bounds.minLon.toString())
attribute("maxlat", bounds.maxLat.toString())
attribute("maxlon", bounds.maxLon.toString())
}
}
fun XmlSerializer.keywords(keywords: String?)
{
if (keywords != null) element("keywords", "$keywords")
}
fun XmlSerializer.time(time: Instant?)
{
if (time != null) element("time", "$time")
}
fun XmlSerializer.copyright(copyright: String?)
{
if (copyright != null) element("copyright", copyright)
}
fun XmlSerializer.name(name: String?)
{
if (name != null) element("name", name)
}
fun XmlSerializer.type(type: String?)
{
if (type != null) element("type", type)
}
fun XmlSerializer.description(description: String?)
{
if (description != null) element("description", description)
}
fun XmlSerializer.author(author: Person?)
{
if (author != null)
{
name(name)
email(author.email)
link(author.link)
}
}
fun XmlSerializer.link(link: Link?)
{
if (link != null)
{
element("link") {
attribute("href", link.href)
if (link.text != null) element("text", link.text)
type(link.type)
}
}
}
fun XmlSerializer.fix(fix: String?)
{
if (!fixType.contains(fix)) return
if (fix != null) element("fix", fix)
}
fun XmlSerializer.email(email: Email?)
{
if (email != null)
{
var id = email.id ?: ""
var domain = email.domain ?: ""
if (email.id == null && email.email != null) id = email.email.substringBefore("@")
if (email.domain == null && email.email != null) domain = email.email.substringAfter("@")
if (id != "" && domain != "")
element("email") {
attribute("id", id)
attribute("domain", domain)
}
}
}
This work is based on
- www.schibsted.pl/blog/back-end/readable-xml-kotlin-extensions/
- medium.com/android-news/how-to-generate-xml-with-kotlin-extension-functions-and-lambdas-in-android-app-976229f1e4d8
- gist.github.com/Audhil/cf9844def5ad9e4210985d2080a101f0
I haven't implemented all GPX fields, but they can easily be added. Here is a link to the latest GPX schema.
You can find my latest version of these extension functions on GitHub.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | Oliver |