Comparing version 0.4.1 to 0.5.0
{ | ||
"name": "gpxload", | ||
"version": "0.4.1", | ||
"description": "SignalK Server plugin to import / export GPX files and make them available via the SignalK API.", | ||
"main": "src/index.js", | ||
"keywords": [ | ||
"signalk-node-server-plugin", | ||
"signalk-webapp", | ||
"signalk" | ||
], | ||
"repository": "https://bitbucket.org/panaaj/signalk-gpx-plugin", | ||
"scripts": { | ||
"format": "prettier-standard 'src/**/*.js'", | ||
"test": "mocha" | ||
}, | ||
"author": "AdrianP", | ||
"contributors": [ | ||
"name": "gpxload", | ||
"version": "0.5.0", | ||
"description": "GPXLoad: GPX file Import / Export and API plugin for SignalK Server.", | ||
"main": "src/index.js", | ||
"keywords": [ | ||
"signalk-node-server-plugin", | ||
"signalk-webapp", | ||
"signalk" | ||
], | ||
"repository": "https://bitbucket.org/panaaj/signalk-gpx-plugin", | ||
"scripts": { | ||
"format": "prettier-standard 'src/**/*.js'", | ||
"test": "mocha" | ||
}, | ||
"author": "AdrianP", | ||
"contributors": [ | ||
{ | ||
"name": "Adrian Panazzolo" | ||
"name": "Adrian Panazzolo" | ||
} | ||
], | ||
"license": "Apache-20", | ||
"dependencies": { | ||
}, | ||
"devDependencies": { | ||
], | ||
"license": "Apache-20", | ||
"dependencies": { | ||
"geojson-validation": "^0.2.0" | ||
}, | ||
"devDependencies": { | ||
"mocha": "^5.0.5", | ||
"prettier-standard": "^8.0.0" | ||
} | ||
} | ||
} |
@@ -1,13 +0,26 @@ | ||
# signalk-gpx-plugin | ||
# GPXLoad: signalk-server-plugin | ||
SignalK Plugin and WebApp to enable GPX file import / export. | ||
GPX file Import / Export and API plugin for SignalK Server. | ||
Selected resources in GPX files can be loaded using the Web UI that is accessed via the url: *http://signalk-server/gpxload/* | ||
SignalK server plugin and WebApp that: | ||
- Allows the upload of selected selected Routes, Waypoints and Tracks in GPX files to a SignalK server | ||
Once loaded, the imported routes, waypoints and tracks are made available via the SignalK server http api *http://signalk-server/signalk/v1/api/resources/*. | ||
- Makes uploaded resources available via the SignalK API *e.g. http://signalk-server/signalk/v1/api/reources/routes* | ||
- Preserves the SignalK UUIDs of exported resources written to the GPX file to avoid resource duplication. | ||
Use and Operation: | ||
------------------ | ||
**New** | ||
- Added support for SignalK HTTP API PUT actions to enable applications to update / delete resources. | ||
**Breaking Changes:** | ||
- Removed support for resource UUIDs that do not strictly align to the Signal K schema and use the *urn:mrn:signalk:uuid:* prefix. | ||
This will impact resources uploaded from GPX files with versions of GPXLoad prior to 0.4.0. Affected reources will need to be re-imported. | ||
# Use and Operation: | ||
**To Import a GPX file:** | ||
@@ -41,3 +54,3 @@ | ||
*Note: | ||
**Note:** | ||
@@ -48,3 +61,45 @@ - GPXLoad preserves the uuids of resources exported from Signal K server in the GPX file, so if these GPX files are re-imported they will update the corresponding resource on the Signal K server rather than create a new resource. | ||
- Signal K HTTP PUT api can be enabled via Signal K server *Plugin Config* screen | ||
- GPXLoad will only accept HTTP PUT requests for one specified resource id so you will need to provide a **complete**, valid resource record as per the Signal K schema. | ||
*Example: Add / update route* | ||
``` | ||
HTTP PUT http://signalk-server/signalk/v1/api/vessels/self/resources/routes | ||
{ | ||
"value":{ | ||
"urn:mrn:signalk:uuid:a8554512-5d3b-45a1-92e9-eb93da834aaa": { | ||
"name":"KI", | ||
"description":"KI via Edithburgh", | ||
"distance":164006.423, | ||
"start":"urn:mrn:signalk:uuid:e85c96e7-9bd4-4ed9-81aa-d3a350f0de55", | ||
"end":"urn:mrn:signalk:uuid:fab5c914-b49b-4f15-a1b4-e7b90de52cb5", | ||
"feature":{ | ||
"type":"Feature", | ||
"geometry":{ | ||
"type":"LineString", | ||
"coordinates":[ | ||
[137.780507,-35.781544],[137.633972,-35.557088],[137.582846,-35.551319],[137.534531,-35.589552],[137.752977,-35.08999],[138.432849,-34.885938] | ||
] | ||
}, | ||
"properties":{} | ||
} | ||
} | ||
} | ||
}``` | ||
- Supplying a *null* value will DELETE the resource if the configuration is set to allow delete operations. | ||
*Example: Delete route* | ||
``` | ||
HTTP PUT http://signalk-server/signalk/v1/api/vessels/self/resources/routes | ||
{ | ||
"value":{ | ||
"urn:mrn:signalk:uuid:a8554512-5d3b-45a1-92e9-eb93da834aaa": null | ||
} | ||
}``` | ||
279
src/index.js
@@ -19,2 +19,3 @@ /* | ||
const path= require('path') | ||
const geoJSON = require('geojson-validation') | ||
@@ -29,3 +30,8 @@ module.exports= function (app) { | ||
} | ||
let config= {} | ||
let config= { | ||
API: { | ||
put: false, | ||
delete: false | ||
} | ||
} | ||
@@ -53,3 +59,18 @@ const uuidPrefix= 'urn:mrn:signalk:uuid:' | ||
app.debug(`** ${plugin.name} started... ${(res) ? 'OK' : 'with errors!'}`) | ||
}) | ||
}) | ||
if (app.registerActionHandler) { | ||
app.debug('** Registering Action Handler(s) **') | ||
app.registerActionHandler( | ||
'vessels.self', | ||
'resources.routes', | ||
handlePut | ||
) | ||
app.registerActionHandler( | ||
'vessels.self', | ||
'resources.waypoints', | ||
handlePut | ||
) | ||
} | ||
} | ||
@@ -70,5 +91,37 @@ catch (e) { | ||
plugin.schema= { properties: { } } | ||
plugin.schema= { | ||
properties: { | ||
API: { | ||
type: "object", | ||
description: `Use this section to allow | ||
applications to use the Signal K HTTP PUT API to update, | ||
replace and delete resources.`, | ||
properties: { | ||
put: { | ||
type: "boolean", | ||
title: "Enabled" | ||
}, | ||
delete: { | ||
type: "boolean", | ||
title: "Enabled" | ||
} | ||
} | ||
} | ||
} | ||
} | ||
plugin.uiSchema= { } | ||
plugin.uiSchema= { | ||
API: { | ||
put: { | ||
"ui:widget": "checkbox", | ||
"ui:title": "Enable / Disable PUT API", | ||
"ui:help": "Check this box to allow resources to be added / updated via HTTP PUT api" | ||
}, | ||
delete: { | ||
"ui:widget": "checkbox", | ||
"ui:title": "Allow DELETE", | ||
"ui:help": "Check this box to allow resources to be deleted via HTTP PUT api" | ||
} | ||
} | ||
} | ||
@@ -123,6 +176,6 @@ plugin.registerWithRouter= router=> { | ||
}) | ||
router.get(`/resources/waypoints/*-*-*-*-*`, (req, res)=> { | ||
/*router.get(`/resources/waypoints/*-*-*-*-*`, (req, res)=> { | ||
let fname= req.path.substring(req.path.lastIndexOf('/')+1) | ||
res.json( getPersistedResources('waypoint', fname) ) | ||
}) | ||
})*/ | ||
router.get('/resources/routes', (req, res) => { | ||
@@ -135,6 +188,6 @@ res.json( getPersistedResources('route').routes ) | ||
}) | ||
router.get(`/resources/routes/*-*-*-*-*`, (req, res)=> { | ||
/*router.get(`/resources/routes/*-*-*-*-*`, (req, res)=> { | ||
let fname= req.path.substring(req.path.lastIndexOf('/')+1) | ||
res.json( getPersistedResources('route', fname) ) | ||
}) | ||
})*/ | ||
router.get('/resources/tracks', (req, res)=> { | ||
@@ -147,7 +200,7 @@ res.json( getPersistedResources('track').tracks ) | ||
}) | ||
router.get(`/resources/tracks/*-*-*-*-*`, (req, res)=> { | ||
/*router.get(`/resources/tracks/*-*-*-*-*`, (req, res)=> { | ||
let fname= req.path.substring(req.path.lastIndexOf('/')+1) | ||
res.json( getPersistedResources('track', fname) ) | ||
}) | ||
})*/ | ||
return router | ||
@@ -191,29 +244,3 @@ } | ||
// ** process posted data ** | ||
function processUIPost(data) { | ||
app.debug(`** Loading resources.........`) | ||
if(!Array.isArray(data)) { | ||
app.debug(`** ERROR: ** Invalid payload data!!`) | ||
return | ||
} | ||
data.forEach( r=> { persist(r) }) | ||
} | ||
// ** save resource to file | ||
function persist(r) { | ||
let fname= r.id.substring(r.id.lastIndexOf(':')+1) | ||
let p= path.join(resTypes[r.type].path, fname) | ||
app.debug(`****** Saving ${r.type} to : ${fname} ******`) | ||
fs.writeFile(p, JSON.stringify(r.value), (err, res)=> { | ||
if(err) { app.debug(err) } | ||
else { app.debug(`** ${r.type} written to ${fname} **`) } | ||
}) | ||
if(r.type=='route') { | ||
r.waypoints.forEach( w=> { | ||
persist(w) | ||
}) | ||
} | ||
} | ||
//** return persisted resources | ||
//** return persisted resources from storage | ||
function getPersistedResources(type=null, item=null) { | ||
@@ -248,4 +275,180 @@ app.debug('*** Retrieving saved Resource(s) ***') | ||
} | ||
} | ||
// ** process posted data ** | ||
function processUIPost(data) { | ||
app.debug(`** Loading resources.........`) | ||
if(!Array.isArray(data)) { | ||
app.debug(`** ERROR: ** Invalid payload data!!`) | ||
return | ||
} | ||
data.forEach( r=> { persist(r) }) | ||
} | ||
// ** handle PUT via registerActionHandler | ||
function handlePut(context, path, value, cb) { | ||
app.debug(` | ||
${JSON.stringify(path)}, | ||
${JSON.stringify(value)}` | ||
) | ||
// ** test for allowed PUT api use | ||
if(!config.API.put) { | ||
return { | ||
state: 'COMPLETED', | ||
resultStatus: 405, | ||
message: `Method Not Allowed!` | ||
} | ||
} | ||
let p= path.split('.') | ||
if(p.length<2) { | ||
return { | ||
state: 'COMPLETED', | ||
resultStatus: 400, | ||
message: `Invalid path!` | ||
} | ||
} | ||
let r={} | ||
r.type= p[1].slice(0,p[1].length-1) | ||
let v= Object.entries(value) | ||
r.id= v[0][0] | ||
r.value= v[0][1] | ||
app.debug(r) | ||
// ** test for allowed DELETE via PUT api | ||
if(r.value===null && !config.API.delete) { | ||
return { | ||
state: 'COMPLETED', | ||
resultStatus: 405, | ||
message: `Method Not Allowed!` | ||
} | ||
} | ||
// ** test for valid resource identifier | ||
if( !isUUID(r.id) ) { | ||
return { | ||
state: 'COMPLETED', | ||
resultStatus: 400, | ||
message: `Invalid resource id!` | ||
} | ||
} | ||
switch(r.type) { | ||
case 'route': | ||
case 'waypoint': | ||
case 'track': | ||
if(persist(r)) { | ||
return { state: 'COMPLETED' } | ||
} | ||
else { | ||
return { | ||
state: 'COMPLETED', | ||
resultStatus: 502, | ||
message: `Invalid resource data values!` | ||
} | ||
} | ||
break; | ||
default: | ||
return { | ||
state: 'COMPLETED', | ||
resultStatus: 400, | ||
message: `Invalid resource type (${r.type})!` | ||
} | ||
} | ||
} | ||
// ** save / delete (r.value==null) resource file | ||
function persist(r) { | ||
if( !isUUID(r.id) ) { return false } | ||
let fname= r.id.substring(r.id.lastIndexOf(':')+1) | ||
let p= path.join(resTypes[r.type].path, fname) | ||
let action= (r.value===null) ? 'DELETE' : 'SAVE' | ||
app.debug(`****** ${r.type}: ${action} -> ${fname} ******`) | ||
if(r.value===null) { // ** delete file ** | ||
fs.unlink(p, err=> { | ||
if(err) { app.debug('Error deleting file!') } | ||
else { app.debug('File deleted!')} | ||
}) | ||
sendDelta(r) | ||
} | ||
else { | ||
if( !validateData(r) ) { return false } | ||
// ** test for valid SignalK value ** | ||
fs.writeFile(p, JSON.stringify(r.value), (err, res)=> { | ||
if(err) { app.debug(err) } | ||
else { app.debug(`** ${r.type} written to ${fname} **`) } | ||
}) | ||
sendDelta(r) | ||
if(r.type=='route') { | ||
r.waypoints.forEach( w=> { persist(w) }) | ||
} | ||
} | ||
return true; | ||
} | ||
// ** send delta message for resource | ||
function sendDelta(r) { | ||
let key= r.id | ||
let p= `resources.${r.type}s.${key}` | ||
let val= [{path: p, value: r.value}] | ||
app.debug(`****** Send Delta: ******`) | ||
app.debug(JSON.stringify({updates: [ {values: val} ] })) | ||
app.handleMessage(plugin.id, {updates: [ {values: val} ] }) | ||
} | ||
// ** validate provided resource value data | ||
function validateData(r) { | ||
if(!r.type) { return false } | ||
switch(r.type) { | ||
case 'route': | ||
return validateRoute(r.value); | ||
break; | ||
case 'waypoint': | ||
return validateWaypoint(r.value); | ||
break; | ||
case 'track': | ||
return validateTrack(r.value); | ||
break; | ||
default: | ||
return false; | ||
} | ||
} | ||
// ** validate route data | ||
function validateRoute(r) { | ||
if(!r.name) { return false } | ||
if(!r.description) { return false } | ||
if(!r.distance || isNaN(r.distance)) { return false } | ||
if(!r.start) { return false } | ||
if(!r.end) { return false } | ||
if(!r.feature || !geoJSON.valid(r.feature)) { | ||
app.debug(`****** INVALID GeoJSON ******`) | ||
return false | ||
} | ||
app.debug(`****** VALID Route ******`) | ||
return true | ||
} | ||
// ** validate waypoint data | ||
function validateWaypoint(r) { | ||
if(!r.position) { return false } | ||
if(!r.position.latitude || !r.position.longitude) { return false } | ||
if(!r.feature || !geoJSON.valid(r.feature)) { | ||
app.debug(`****** INVALID GeoJSON ******`) | ||
return false | ||
} | ||
app.debug(`****** VALID Waypoint ******`) | ||
return true | ||
} | ||
// ** validate track data | ||
function validateTrack(r) { return true } | ||
// ** returns true if id is a valid UUID ** | ||
function isUUID(id) { | ||
let uuid= RegExp("^urn:mrn:signalk:uuid:[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-4[0-9A-Fa-f]{3}-[89ABab][0-9A-Fa-f]{3}-[0-9A-Fa-f]{12}$") | ||
let result= uuid.test(id) | ||
if(!result) { app.debug(`****** INVALID ID: ${id} ******`) } | ||
return result | ||
} | ||
return plugin | ||
@@ -252,0 +455,0 @@ } |
2426023
1406
104
1
+ Addedgeojson-validation@^0.2.0
+ Addedgeojson-validation@0.2.1(transitive)