diff --git a/script/gaia_upload.js b/script/gaia_upload.js new file mode 100644 index 0000000..440dbfb --- /dev/null +++ b/script/gaia_upload.js @@ -0,0 +1,545 @@ +// ==UserScript== +// @name GaiaGps Uploader v2 +// @version 2.0 +// @grant none +// @match https://www.gaiagps.com/upload/ +// @require https://ajax.googleapis.com/ajax/libs/jquery/3.5.0/jquery.min.js +// ==/UserScript== + +var gpxParser = function () { + this.xmlSource = ""; + this.metadata = {}; + this.waypoints = []; + this.tracks = []; + this.routes = []; +}; + +gpxParser.prototype.parse = function (gpxstring) { + var keepThis = this; + var domParser = new window.DOMParser(); + this.xmlSource = domParser.parseFromString(gpxstring, 'text/xml'); + + metadata = this.xmlSource.querySelector('metadata'); + if(metadata != null){ + this.metadata.name = this.getElementValue(metadata, "name"); + this.metadata.desc = this.getElementValue(metadata, "desc"); + this.metadata.time = this.getElementValue(metadata, "time"); + + let author = {}; + let authorElem = metadata.querySelector('author'); + if(authorElem != null){ + author.name = this.getElementValue(authorElem, "name"); + author.email = {}; + let emailElem = authorElem.querySelector('email'); + if(emailElem != null){ + author.email.id = emailElem.getAttribute("id"); + author.email.domain = emailElem.getAttribute("domain"); + } + + let link = {}; + let linkElem = authorElem.querySelector('link'); + if(linkElem != null){ + link.href = linkElem.getAttribute('href'); + link.text = this.getElementValue(linkElem, "text"); + link.type = this.getElementValue(linkElem, "type"); + } + author.link = link; + } + this.metadata.author = author; + + let link = {}; + let linkElem = this.queryDirectSelector(metadata, 'link'); + if(linkElem != null){ + link.href = linkElem.getAttribute('href'); + link.text = this.getElementValue(linkElem, "text"); + link.type = this.getElementValue(linkElem, "type"); + this.metadata.link = link; + } + } + + var wpts = [].slice.call(this.xmlSource.querySelectorAll('wpt')); + for (let idx in wpts){ + var wpt = wpts[idx]; + let pt = {}; + pt.name = keepThis.getElementValue(wpt, "name") + pt.lat = parseFloat(wpt.getAttribute("lat")); + pt.lon = parseFloat(wpt.getAttribute("lon")); + pt.ele = parseFloat(keepThis.getElementValue(wpt, "ele")) || null; + pt.cmt = keepThis.getElementValue(wpt, "cmt"); + pt.desc = keepThis.getElementValue(wpt, "desc"); + pt.sym = keepThis.getElementValue(wpt, "sym"); + keepThis.waypoints.push(pt); + } + + var rtes = [].slice.call(this.xmlSource.querySelectorAll('rte')); + for (let idx in rtes){ + var rte = rtes[idx]; + let route = {}; + route.name = keepThis.getElementValue(rte, "name"); + route.cmt = keepThis.getElementValue(rte, "cmt"); + route.desc = keepThis.getElementValue(rte, "desc"); + route.src = keepThis.getElementValue(rte, "src"); + route.number = keepThis.getElementValue(rte, "number"); + + let type = keepThis.queryDirectSelector(rte, "type"); + route.type = type != null ? type.innerHTML : null; + + let link = {}; + let linkElem = rte.querySelector('link'); + if(linkElem != null){ + link.href = linkElem.getAttribute('href'); + link.text = keepThis.getElementValue(linkElem, "text"); + link.type = keepThis.getElementValue(linkElem, "type"); + } + route.link = link; + + let routepoints = []; + var rtepts = [].slice.call(rte.querySelectorAll('rtept')); + + for (let idxIn in rtepts){ + var rtept = rtepts[idxIn]; + let pt = {}; + pt.lat = parseFloat(rtept.getAttribute("lat")); + pt.lon = parseFloat(rtept.getAttribute("lon")); + pt.ele = parseFloat(keepThis.getElementValue(rtept, "ele")); + routepoints.push(pt); + } + + route.points = routepoints; + keepThis.routes.push(route); + } + + var trks = [].slice.call(this.xmlSource.querySelectorAll('trk')); + for (let idx in trks){ + var trk = trks[idx]; + let track = {}; + + track.name = keepThis.getElementValue(trk, "name"); + track.cmt = keepThis.getElementValue(trk, "cmt"); + track.desc = keepThis.getElementValue(trk, "desc"); + track.src = keepThis.getElementValue(trk, "src"); + track.number = keepThis.getElementValue(trk, "number"); + track.color = keepThis.getElementValue(trk, "DisplayColor"); + + let type = keepThis.queryDirectSelector(trk, "type"); + + let link = {}; + let linkElem = trk.querySelector('link'); + if(linkElem != null){ + link.href = linkElem.getAttribute('href'); + link.text = keepThis.getElementValue(linkElem, "text"); + link.type = keepThis.getElementValue(linkElem, "type"); + } + track.link = link; + + let trackpoints = []; + var trkpts = [].slice.call(trk.querySelectorAll('trkpt')); + for (let idxIn in trkpts){ + var trkpt = trkpts[idxIn]; + let pt = {}; + pt.lat = parseFloat(trkpt.getAttribute("lat")); + pt.lon = parseFloat(trkpt.getAttribute("lon")); + pt.ele = parseFloat(keepThis.getElementValue(trkpt, "ele")) || null; + trackpoints.push(pt); + } + track.points = trackpoints; + + keepThis.tracks.push(track); + } +}; + +gpxParser.prototype.getElementValue = function(parent, needle){ + let elem = parent.querySelector(needle); + if(elem != null){ + return elem.innerHTML != undefined ? elem.innerHTML : elem.childNodes[0].data; + } + return elem; +} + +gpxParser.prototype.queryDirectSelector = function(parent, needle) { + + let elements = parent.querySelectorAll(needle); + let finalElem = elements[0]; + + if(elements.length > 1) { + let directChilds = parent.childNodes; + + for(idx in directChilds) { + elem = directChilds[idx]; + if(elem.tagName === needle) { + finalElem = elem; + } + } + } + + return finalElem; +} + +gpxParser.prototype.getGPX = function(sName) { + var sTrack = ''; + + for(var i=0 ; i < this.tracks.length ; i++) { + var sTrkSeg = ''; + for(var j=0 ; j < this.tracks[i].points.length ; j++) { + sTrkSeg += ''+ + ''+(this.tracks[i].points[j].ele || '')+''+ + ''; + } + sTrack += ''+ + ''+(this.tracks[i].name || '')+''+ + ''+(this.tracks[i].desc || '')+''+ + ''+(this.tracks[i].src || '')+''+ + ''+sTrkSeg+''+ + ''; + } + + var sGPX = ''+ + ''+ + ''+sName+''+ + sTrack+ + ''; + + return sGPX; +}; + +class Gaia { + static get URL() { return 'https://www.gaiagps.com'; } + static get API() { return Gaia.URL+'/api/objects'; } + + constructor($Form) { + this.aoWaypoints = []; + this.aoTracks = []; + this.sFolderId = ''; + this.sFolderName = ''; + + this.$Form = $Form; + this.$InputName = $Form.find('input[name=name]'); + this.$InputFile = $Form.find('input[type=file]'); + this.$Feedback = $Form.after($('
')); + this.setLayout(); + } + + feedback(sType, sMsg) { + var sFormattedMsg = sType.charAt(0).toUpperCase()+sType.slice(1)+': '+sMsg+'.'; + console.log(sFormattedMsg); + + let sColor = 'black'; + switch(sType) { + case 'error': sColor = 'red'; break; + case 'warning': sColor = 'orange'; break; + case 'info': sColor = 'green'; break; + } + this.$Feedback.append($('

', {'style': 'color: '+sColor+';'}).text(sFormattedMsg)); + } + + //Modify Gaia DOM Interface + setLayout() { + //Set event on file selection + this.$InputFile + .attr('multiple', 'multiple') + .attr('name', 'files[]') + .change(() => {this.readInputFiles();}); + + //Remove submit button & edit label + this.$Form.find('#fileuploadtext').text('Select files'); + this.$Form.find('button[type=submit]').hide(); + + //Set default Name + this.$InputName.val('PCT'); + + //Clear all upload notifications + this.resetNotif(); + } + + //Parse files from input (multiple) + readInputFiles() { + if (!window.File || !window.FileReader || !window.FileList || !window.Blob) { + this.feedback('error', 'File APIs are not fully supported in this browser'); + return; + } + else this.feedback('info', 'Parsing input files'); + + //Get Folder Name (text input) + this.sFolderName = this.$InputName.val(); + + let aoReaders = []; + let asFiles = this.$InputFile.prop('files'); + let asContents = []; + for(var i=0 ; i < asFiles.length ; i++) { + aoReaders[i] = new FileReader(); + aoReaders[i].onload = ((asResult) => { + asContents.push(asResult.target.result); + this.feedback('info', 'Reading file '+asContents.length+'/'+asFiles.length); + if(asContents.length == asFiles.length) this.parseFile(asContents); + }); + aoReaders[i].readAsText(asFiles[i]); + } + } + + //Parse GPX files to consolidate tracks & waypoints + parseFile(asContents) { + this.feedback('info', 'Merging files'); + for(var i in asContents) { + var oGPX = new gpxParser(); + oGPX.parse(asContents[i]); + + //Waypoints + for(var w in oGPX.waypoints) { + var sWaypointName = oGPX.waypoints[w].name; + if(sWaypointName.indexOf('Milestone - ') != -1 && sWaypointName.substr(-2) != '00') { //1 milestone every 100 miles + this.feedback('info', 'Ignoring milestone waypoint "'+sWaypointName+'"'); + } + else this.aoWaypoints.push(oGPX.waypoints[w]); + } + + //Tracks + for(var t in oGPX.tracks) { + this.aoTracks.push(oGPX.tracks[t]); + } + } + + this.deleteFolder(); + } + + //Delete existing folder with same name + deleteFolder() { + this.feedback('info', 'Looking for existing "'+this.sFolderName+'" folder...'); + + $.get(Gaia.API+'/folder/?routepoints=false&show_archived=true&show_filed=true').done((asFolders) => { + var bCalled = false; + for(var f in asFolders) { + if(asFolders[f].title == this.sFolderName) { + bCalled = true; + this.feedback('info', 'Deleting "'+this.sFolderName+'" folder'); + $.ajax({ + url: Gaia.API+'/folder/'+asFolders[f].id+'/', + type: 'DELETE' + }); + } + } + if(!bCalled) this.feedback('info', 'No folder named "'+this.sFolderName+'" found'); + + this.uploadTrack(); + }); + } + + //Marking all upload notifications as read + resetNotif() { + this.feedback('info', 'Marking all upload notifications as read'); + $.get(Gaia.URL+'/social/notifications/popup/').done((asNotifs) => { + for(var i in asNotifs) { + if(!asNotifs[i].isViewed && asNotifs[i].html.indexOf('has completed') != -1) { + $.post(Gaia.URL+'/social/notifications/'+asNotifs[i].id+'/markviewed/'); + } + } + }); + } + + //Build & Upload Track File + uploadTrack() { + //Build track file + this.feedback('info', 'Building consolidated track'); + var oGPX = new gpxParser(); + oGPX.tracks = this.aoTracks; + var sTrack = oGPX.getGPX(this.sFolderName); + + //Send consolidated Tracks as one file + var oForm = new FormData(); + oForm.append('name', this.sFolderName); + oForm.append('files', new Blob([sTrack]), 'Consolidated.gpx'); + + this.feedback('info', 'Uploading consolidated track'); + $.ajax({ + url: Gaia.URL+'/upload/', + type: 'POST', + data: oForm, + enctype: 'multipart/form-data', + processData: false, + contentType: false, + cache: false + }).done(() => {this.checkNotif();}); + } + + //Wait for file to be processed by Gaia + checkNotif() { + this.feedback('info', 'Waiting for Gaia to process consolidated track'); + $.get(Gaia.URL+'/social/notifications/popup/').done((asNotifs) => { + for(var i in asNotifs) { + if(!asNotifs[i].isViewed && asNotifs[i].html.indexOf('has completed') != -1) { + this.feedback('info', 'Notification '+asNotifs[i].id+' found. Marking as read'); + var $Notif = $('').html(asNotifs[i].html); + this.sFolderId = $Notif.find('a').attr('href').split('/')[3]; + $.post(Gaia.URL+'/social/notifications/'+asNotifs[i].id+'/markviewed/'); + } + } + + if(this.sFolderId != '') { + this.setTracksColor(); + this.uploadWayPoints(); + } + else setTimeout((() => {this.checkNotif();}), 1000); + }); + } + + //Convert QMapshack Track Colors into Gaia Colors + setTracksColor() { + this.feedback('info', 'Track Color - Matching Gaia track IDs with GPX tracks'); + var iCount = 0; + + $.get(Gaia.API+'/folder/'+this.sFolderId+'/').done((asFolder) => { + var self = this; + for(var iGaiaIndex in asFolder.properties.tracks) { + for(var iGpxIndex in this.aoTracks) { + if(asFolder.properties.tracks[iGaiaIndex].title == this.aoTracks[iGpxIndex].name) { + this.aoTracks[iGpxIndex].id = asFolder.properties.tracks[iGaiaIndex].id; + this.feedback('info', 'Track Color - Track "'+this.aoTracks[iGpxIndex].name+'" found a match ('+(++iCount)+'/'+this.aoTracks.length+')'); + + $.ajax({ + url: Gaia.API+'/track/'+this.aoTracks[iGpxIndex].id+'/', + color: this.aoTracks[iGpxIndex].color + }).done(function(asGaiaTrackDisplay) { + var asGaiaTrack = asGaiaTrackDisplay.features[0]; + delete asGaiaTrack.id; + delete asGaiaTrack.style; + + //Set color + var sColor = '#ff0000'; + switch(this.color) { + case 'DarkBlue': sColor = '#2D3FC7'; break; + case 'Magenta': sColor = '#B60DC3'; break; + } + asGaiaTrack.properties.color = sColor; + asGaiaTrack.properties.hexcolor = sColor; + + self.feedback('info', 'Track Color - Setting track color "'+sColor+'" ('+this.color+') to "'+asGaiaTrack.properties.title+'"'); + $.ajax({ + url: Gaia.API+'/track/'+asGaiaTrack.properties.id+'/', + type: 'PUT', + contentType: 'application/json', + data: JSON.stringify(asGaiaTrack) + }); + }); + } + } + } + }); + } + + uploadWayPoints(iIndex, bSecondTry) { + iIndex = iIndex || 0; + bSecondTry = bSecondTry || false; + + //Upload waypoints + if(iIndex < this.aoWaypoints.length) { + var sWaypointName = this.aoWaypoints[iIndex].name; + + this.feedback('info', 'Waypoints Upload - Uploading waypoint '+(iIndex + 1)+'/'+this.aoWaypoints.length+' ('+sWaypointName+')'); + var asPost = { + type: 'Feature', + geometry: { + type: 'Point', + coordinates: [ + this.aoWaypoints[iIndex].lon, + this.aoWaypoints[iIndex].lat, + this.aoWaypoints[iIndex].ele + ] + }, + properties: { + title: sWaypointName, + time_created: "2020-06-07T14:01:03.944Z", + icon: Gaia.getIconName(this.aoWaypoints[iIndex].sym), + writable: true, + localId: iIndex+'', + archived: false, + isTitleLoaded: true, + isLocallyCreated: true, + type: 'waypoint' + } + }; + if(this.aoWaypoints[iIndex].desc) asPost.properties.notes = this.aoWaypoints[iIndex].desc; + + $.ajax({ + url: Gaia.API+'/waypoint/', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify(asPost) + }).done((asWaypoint) => { + //Update local waypoint with all server info (including ID) + this.aoWaypoints[iIndex] = asWaypoint; + + $.get(Gaia.URL+'/public/generate?type=waypoint&objectid='+asWaypoint.properties.id).always(() => { + this.uploadWayPoints(++iIndex); + }); + }).fail(function(){ + this.feedback('error', 'Waypoints Upload - Failed to upload waypoint #'+(iIndex + 1)+' ('+sWaypointName+'). Trying again...'); + this.uploadWayPoints(iIndex, true); + }); + } + + //Done uploading, assigning waypoints to folder + else { + this.feedback('info', 'Waypoints Upload - Getting folders list'); + $.get(Gaia.API+'/folder/').done((asFolders) => { + //Find Folder + for(var f in asFolders) { + if(asFolders[f].id == this.sFolderId) { + var asFolder = asFolders[f]; + } + } + + //Assign waypoints to folder + for(var i in this.aoWaypoints) asFolder.waypoints.push(this.aoWaypoints[i].properties.id); + + this.feedback('info', 'Waypoints Upload - Assigning waypoints to folder '+this.sFolderId); + $.ajax({ + url: Gaia.API+'/folder/'+this.sFolderId+'/', + type: 'PUT', + contentType: 'application/json', + data: JSON.stringify(asFolder) + }).done(() => { + this.feedback('info', 'Waypoints Upload - Finished successfully'); + }).fail(() => { + this.feedback('error', 'Waypoints Upload - Failed to assign waypoints to folder "'+sFolderId+'"'); + }); + }); + } + } + + static getIconName(sGarminName) { + var asMapping = { + 'Trail Head': 'trailhead', + 'Water Source': 'water-24.png', + 'Truck': 'car-24.png', + 'Post Office': 'resupply', + 'Flag, Blue': 'blue-pin-down.png', + 'Car': 'car-24.png', + 'Flag, Red': 'red-pin-down.png', + 'Campground': 'campsite-24.png', + 'Flag, Green': 'green-pin.png', + 'Powerline': 'petroglyph', + 'Shopping Center': 'cafe-24.png', + 'Lodging': 'lodging-24.png', + 'Water Hydrant': 'red-pin-down.png', + 'Drinking Water': 'potable-water', + 'Toll Booth': 'ranger-station', + 'Summit': 'peak', + 'Park': 'park-24.png', + 'Forest': 'forest', + 'Cemetery': 'cemetery-24.png', + 'Bridge': 'dam-24.png', + 'Restaurant': 'restaurant-24.png', + 'Picnic Area': 'picnic', + 'Residence': 'city-24.png', + 'City (Capitol)': 'city-24.png', + 'Ski Resort': 'skiing-24.png', + 'Restroom': 'toilets-24.png', + 'Ground Transportation': 'car-24.png', + 'Church': 'ghost-town' + }; + return (sGarminName in asMapping)?asMapping[sGarminName]:'red-pin-down.png'; + } +} + +console.log('computes'); + +let oGaia = new Gaia($('#uploadForm'));