'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

  1. www.schibsted.pl/blog/back-end/readable-xml-kotlin-extensions/
  2. medium.com/android-news/how-to-generate-xml-with-kotlin-extension-functions-and-lambdas-in-android-app-976229f1e4d8
  3. 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