Update 2016-10-16: Found and error in procedure, see below.

In preparation for a presentation on Oracle Spatial for DBAs I decided to import some GPS-data. I have an app called GPSLogger on my Android mobile that logs GPS data to a file. The format for this is GPS Exchange Format  (GPX).  Since I spent some time understanding the XML side of this I decided to blog about it.

Note, there are two versions of the GPX schema, 1.0 and 1.1. My app uses the version 1.0, if you try to import a GPX file, have a look in it to verify which schema definition it uses (Look for xsi:schemaLocation in the head of the GPX file).

I followed the information from this blog post (that actually uses version 1.1) together with info from Oracle Support, and lots of googling.

The app can upload to Dropbox which makes it easy to transfer the file to the database server. In addition I needed the schema definition, which you can get from Topographix.com, you’ll find the link to the XSD file in the GPX file as mentioned, in my case it was http://www.topografix.com/GPX/1/0/gpx.xsd

However, the file needs some adjustment in order to have Oracle parse values of type xsd:dateTime correctly. Open it in an editor and look for the following block:

<xsd:schema  
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"  
        xmlns:gpx="http://www.topografix.com/GPX/1/0"  
        targetNamespace="http://www.topografix.com/GPX/1/0"  
        elementFormDefault="qualified">  

At the end of this,  add one line so it looks like this:

<xsd:schema  
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"  
        xmlns:gpx="http://www.topografix.com/GPX/1/0"  
        targetNamespace="http://www.topografix.com/GPX/1/0"  
        elementFormDefault="qualified"  
        xmlns:xdb="http://xmlns.oracle.com/xdb>"  

Then you have to search through the file and search for every definition that uses type “xsd:dateTime” and add:

 xdb:sqltype=“TIMESTAMP WITH TIME ZONE”

so it looks like this:

xsd:element minoccurs="0" name="time" type="xsd:dateTime" 
xdb:sqltype="TIMESTAMP WITH TIME ZONE"  
  

(Take away the line break, I added it to make it readable). There are four entries, so this manual job does not take a long time. I transferred this file together with my GPX file to a directory on my server (/u01/app/oracle/load_dir). Here is the first part to get started, the first part is executed as SYSTEM:

create user oyvind identified by oracle   
default tablespace users temporary tablespace temp   
quota unlimited on users;  
  
grant create session, create table, create view, create procedure to oyvind;  
grant alter session to oyvind;  
grant create type to oyvind;  
  
create directory LOAD_DIR as '/u01/app/oracle/load_dir';  
grant read, write on directory load_dir to oyvind;  

As you can see, I did not granted XDBADMIN to this user.  Update 2016-10-16: The use of DBMS_XDB does indeed require being SYS, the owner of the package (XDB), or having one of the roles XDBADMIN / DBA. Since I did not want an ordinary user to have an admin role, I executed the first part that uses the DBMS_XDB package as SYSTEM. The function below was created in the schema of the ordinary user as well. I must have been experimenting with granting and revoking the XDBADMIN without noticing that the first part did nothing since the resource was already there.

The first step is to load the XML schema definition to the database. I copied the function getClobDocument from the mentioned blog, it is used to load files as CLOB:

create or replace function getClobDocument(  
    p_directoryname In varchar2,  
    p_filename      In varchar2,  
    p_charset       In varchar2 default NULL)  
    return          CLOB deterministic  
is  
  v_file          bfile := bfilename(p_directoryname,p_filename);  
  v_charContent   CLOB := ' ';  
  v_targetFile    bfile;  
  v_lang_ctx      number := DBMS_LOB.default_lang_ctx;  
  v_charset_id    number := 0;  
  v_src_offset    number := 1 ;  
  v_dst_offset    number := 1 ;  
  v_warning       number;  
begin  
  if p_charset is not null then  
    v_charset_id := NLS_CHARSET_ID(p_charset);  
  end if;  
  v_targetFile := v_file;  
  DBMS_LOB.fileopen(v_targetFile, DBMS_LOB.file_readonly);  
  DBMS_LOB.LOADCLOBFROMFILE(v_charContent,   
                            v_targetFile,  
                            DBMS_LOB.getLength(v_targetFile),   
                            v_src_offset,   
                            v_dst_offset,  
                            v_charset_id,   
                            v_lang_ctx,  
                            v_warning);  
  DBMS_LOB.fileclose(v_targetFile);  
  return v_charContent;  
end;  
/  

The following code worked for me to create resource:

declare  
  schemaURL varchar2(256) := 'http://www.topografix.com/GPX/1/0/gpx.xsd';  
  schemaDoc varchar2(30) := 'gpx.xsd';  
  xmlSchema xmlType;  
  res       boolean;  
begin  
  xmlSchema := XMLTYPE(getCLOBDocument('LOAD_DIR',schemaDoc,'UTF8'));  
  if (dbms_xdb.existsResource('/home/' ||schemaDoc)) then  
      dbms_xdb.deleteResource('/home/' ||schemaDoc);  
  end if;  
  res := dbms_xdb.createResource('/home/' || schemaDoc,xmlSchema);  
  if res then  
    dbms_output.put_Line('OK');  
  else  
    dbms_output.put_Line('NOK');  
  end if;  
end;  
/  
  

Make sure you turn on server output and get a proper message to indicate if it succeeded or not; no error message was returned when it failed.

The rest of this is executed as user oyvind (Update: also create the getClobDocument from above to this user). Next step is to register the schema, I used the following code:

declare  
  schemaURL varchar2(256) := 'http://www.topografix.com/GPX/1/0/gpx.xsd';  
  schemaDoc varchar2(30) := 'gpx.xsd';  
begin  
  dbms_xmlschema.registerSchema  
  (  
    schemaURL,  
    XMLTYPE(getCLOBDocument('LOAD_DIR',schemaDoc,'AL32UTF8')),  
    local => true,  
    genTypes => true,  
    genTables => false,  
    enableHierarchy => dbms_xmlschema.ENABLE_HIERARCHY_NONE  
  );  
end;  
/  
  

Use the following code to create a table to store the document as XMLType:

CREATE TABLE gpx  
    OF XMLType (CHECK (XMLIsValid(object_value) = 1))   
    XMLSCHEMA "http://www.topografix.com/GPX/1/0/gpx.xsd" ELEMENT "gpx";  
  

Then, finally, you can load the GPX file with this:

INSERT INTO GPX   
VALUES (XMLTYPE(getCLOBDocument('LOAD_DIR','20161001.gpx')));  
commit;   

To see some of the data, you can extract what you find interesting with:

 SELECT   
       to_number(EXTRACTVALUE(VALUE(t), 'trkpt/@lon'))  longitude,  
       to_number(EXTRACTVALUE(VALUE(t), 'trkpt/@lat'))  latitude,  
       TO_NUMBER(EXTRACTVALUE(VALUE(t), 'trkpt/ele'))   Elevation,  
       EXTRACTVALUE(value(t),'trkpt/src') src  
 FROM GPX g,  
      TABLE(XMLSequence(extract(g.OBJECT_VALUE,'/gpx/trk/trkseg/trkpt','xmlns="http://www.topografix.com/GPX/1/0"'))) t;  
  

This is an example of what comes out (it was a short walk):

LONGITUDE

LATITUDE

ELEVATION

SRC

10.79831903

59.83451229

85

gps

10.80009538

59.83263633

170

gps

10.80033655

59.83156553

146

gps

10.8003321

59.8303762

network

In next post I plan to use the Spatial datatype to store these points. I may do another test with GPX version 1.1 in the mean time since that is the version used by my Garmin GPS.

Some tips if you need to clean up in case of errors;  if you have the recyclebin enabled, do a “purge recyclebin” after dropping the table with the XMLType. I used to following command to delete the schema:

purge recyclebin;  
exec dbms_xmlschema.deleteschema(schemaurl => 'http://www.topografix.com/GPX/1/0/gpx.xsd');  
  

Get in touch if you need a copy of the final gpx.xsd file.