1

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) 
Yunkwon
  • 21
  • 3

2 Answers2

0

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

Sun Jiaojiao
  • 321
  • 2
  • 10
  • actually i used JPX library. but my app's minimum sdk version is 24. it means I need to use java 7. but the jpx library include zoneddatetime class... it works from java8. so I think I need to create myself... do you know other library?? – Yunkwon May 04 '20 at 02:22
  • jpx works fine in my project, and its min sdk is 21. You can use a Long variable as time. For example location.getTime(). @Yunkwon – Sun Jiaojiao May 04 '20 at 04:45
  • umm.. the reason why I'm changing from jpx to myself is, I need to get time from waypoint... i edit end of my post. plz see and reply.. I need your help... – Yunkwon May 05 '20 at 05:22
  • OK, I have understood. the java.time.* package in java 8 has been backported to Java 6 by its creator Stephen Colebourne, and also backported to early android version. It's here: github.com/JakeWharton/ThreeTenABP – Sun Jiaojiao May 05 '20 at 08:38
  • I have a question again... I add ThreeTenABP to my project. and also init in my App instance. but I think it's not work. well how to use it? I add more information about this on the end of my post – Yunkwon May 06 '20 at 07:45
  • I think you'd better go to github repository of ThreeTenABP and post an issue. – Sun Jiaojiao May 06 '20 at 08:04
0

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.

Oliver
  • 21
  • 3