Enable File drop on map

This commit is contained in:
Dave Conway-Jones 2020-12-06 23:53:47 +00:00
parent ab7bbbe34d
commit 821bedca20
No known key found for this signature in database
GPG Key ID: 88BA2B8A411BE9FF
6 changed files with 144 additions and 55 deletions

View File

@ -1,5 +1,7 @@
### Change Log for Node-RED Worldmap ### Change Log for Node-RED Worldmap
- v2.7.0 - Allow track and image files to be dragged onto the map, if enabled
- v2.6.1 - Better fit for worldmap when in ui_template
- v2.6.0 - Add route capability to draw line when online - v2.6.0 - Add route capability to draw line when online
- v2.5.9 - Fix handling of multiple hulls, tidy contextmenu handling - v2.5.9 - Fix handling of multiple hulls, tidy contextmenu handling
- v2.5.8 - Let node name be the full page map title - v2.5.8 - Let node name be the full page map title

View File

@ -11,6 +11,8 @@ map web page for plotting "things" on.
### Updates ### Updates
- v2.7.0 - Allow track and image files to be dragged onto the map, if enabled
- v2.6.1 - Better fit for worldmap when in ui_template
- v2.6.0 - Add route capability to draw line when online - v2.6.0 - Add route capability to draw line when online
- v2.5.9 - Fix handling of multiple hulls, tidy contextmenu handling - v2.5.9 - Fix handling of multiple hulls, tidy contextmenu handling
- v2.5.8 - Let node name be the full page map title - v2.5.8 - Let node name be the full page map title
@ -24,13 +26,8 @@ map web page for plotting "things" on.
- v2.5.0 - Add minimap capability. - v2.5.0 - Add minimap capability.
- v2.4.2 - Fix editing injected shapes. - v2.4.2 - Fix editing injected shapes.
- v2.4.1 - Add convex-hull node for grouping objects. - v2.4.1 - Add convex-hull node for grouping objects.
- v2.3.16 - Add heading to default addMarker, allow custom http icon size.
- v2.3.13 - Fix geoson feature properties fill color, and better marker handling
- v2.3.11 - Better editing of drawing layer, add OpenTopoMap, and better Esri satellite
- v2.3.10 - Improve geojson layer and name handling.
- v2.3.8 - Fix fa-marker offset to improve accuracy.
- see [CHANGELOG](https://github.com/dceejay/RedMap/blob/master/CHANGELOG.md) for full list. - see [CHANGELOG](https://github.com/dceejay/RedMap/blob/master/CHANGELOG.md) for full list of changes.
## Install ## Install
@ -342,10 +339,14 @@ The **worldmap in** node can be used to receive various events from the map. Exa
{ "action": "addlayer", "name": "myLayer" } // when a new map layer is added { "action": "addlayer", "name": "myLayer" } // when a new map layer is added
{ "action": "dellayer", "name": "myLayer" } // when a new map layer is deleted { "action": "dellayer", "name": "myLayer" } // when a new map layer is deleted
{ "action": "file", "name": "filename", "lat":51, "lon":-1, "content":"....."} // when a file is dropped on the map - see below.
{ "action": "button", "name": "My Fancy Button" } // when a user defined button is clicked { "action": "button", "name": "My Fancy Button" } // when a user defined button is clicked
{ "action": "feedback", "name": "some name", "value": "some value", "lat":51, "lon":0, "layer":"unknown" } // when a user calls the feedback function - see below { "action": "feedback", "name": "some name", "value": "some value", "lat":51, "lon":0, "layer":"unknown" } // when a user calls the feedback function - see below
If File Drop is enabled - then the map can accept files of type gpx, kml, nvg, png and jpg. Image contents will be base64 encoded for transfer. The lat, lon of the cursor drop point will be included. Tracks will be locally rendered on the map. The node-red-node-exif node can be used to extract lcoation information from a jpeg image and then geolocate it back on the map.
All actions also include a `msg._sessionid` property that indicates which client session they came from. Any msg sent out that includes this property will ONLY be sent to that session - so you can target map updates to specific sessions if required. All actions also include a `msg._sessionid` property that indicates which client session they came from. Any msg sent out that includes this property will ONLY be sent to that session - so you can target map updates to specific sessions if required.

View File

@ -1,6 +1,6 @@
{ {
"name": "node-red-contrib-web-worldmap", "name": "node-red-contrib-web-worldmap",
"version": "2.6.0", "version": "2.7.0",
"description": "A Node-RED node to provide a web page of a world map for plotting things on.", "description": "A Node-RED node to provide a web page of a world map for plotting things on.",
"dependencies": { "dependencies": {
"cgi": "0.3.1", "cgi": "0.3.1",

View File

@ -103,7 +103,12 @@
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-input-path"><i class="fa fa-globe"></i> Web Path</label> <label for="node-input-path"><i class="fa fa-globe"></i> Web Path</label>
<input type="text" id="node-input-path" placeholder="worldmap"> <input type="text" id="node-input-path" placeholder="worldmap" style="width:30%;">
<i class="fa fa-hand-paper-o" style="margin-left:22px;"></i> File Drop
<select id="node-input-allowFileDrop" style="width:80px;">
<option value="false">Disable</option>
<option value="true">Enable</option>
</select>
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-input-name"><i class="fa fa-file"></i> Name</label> <label for="node-input-name"><i class="fa fa-file"></i> Name</label>
@ -250,7 +255,12 @@ then by default <code>⌘⇧m</code> - <code>ctrl-shift-m</code> will load the m
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-input-path"><i class="fa fa-globe"></i> Web Path</label> <label for="node-input-path"><i class="fa fa-globe"></i> Web Path</label>
<input type="text" id="node-input-path" placeholder="worldmap"> <input type="text" id="node-input-path" placeholder="worldmap" style="width:30%;">
<i class="fa fa-hand-paper-o" style="margin-left:22px;"></i> File Drop
<select id="node-input-allowFileDrop" style="width:80px;">
<option value="false">Disable</option>
<option value="true">Enable</option>
</select>
</div> </div>
<div class="form-row"> <div class="form-row">
<label for="node-input-name"><i class="fa fa-file"></i> Name</label> <label for="node-input-name"><i class="fa fa-file"></i> Name</label>
@ -297,7 +307,6 @@ then by default <code>⌘⇧m</code> - <code>ctrl-shift-m</code> will load the m
to <b>control</b> the map remotely, including how to use a local map server.</p> to <b>control</b> the map remotely, including how to use a local map server.</p>
</script> </script>
<script type="text/javascript"> <script type="text/javascript">
var lnk = document.location.host+RED.settings.httpNodeRoot+"/worldmap"; var lnk = document.location.host+RED.settings.httpNodeRoot+"/worldmap";
lnk = lnk.replace(new RegExp('\/{1,}','g'),'/'); lnk = lnk.replace(new RegExp('\/{1,}','g'),'/');
@ -327,6 +336,7 @@ then by default <code>⌘⇧m</code> - <code>ctrl-shift-m</code> will load the m
hiderightclick: {value:"false"}, hiderightclick: {value:"false"},
coords: {value:"false"}, coords: {value:"false"},
showgrid: {value:"false"}, showgrid: {value:"false"},
allowFileDrop: {value:"false"},
path: {value:"/worldmap"} path: {value:"/worldmap"}
}, },
inputs:1, inputs:1,
@ -395,6 +405,7 @@ then by default <code>⌘⇧m</code> - <code>ctrl-shift-m</code> will load the m
hiderightclick: {value:"true"}, hiderightclick: {value:"true"},
coords: {value:"false"}, coords: {value:"false"},
showgrid: {value:"false"}, showgrid: {value:"false"},
allowFileDrop: {value:"false"},
path: {value:"/worldmap"} path: {value:"/worldmap"}
}, },
inputs:1, inputs:1,
@ -446,7 +457,8 @@ then by default <code>⌘⇧m</code> - <code>ctrl-shift-m</code> will load the m
<label for="node-input-events"><i class="fa fa-sign-out"></i> Output</label> <label for="node-input-events"><i class="fa fa-sign-out"></i> Output</label>
<select id="node-input-events" style="width:70%;"> <select id="node-input-events" style="width:70%;">
<option value="all">All action events</option> <option value="all">All action events</option>
<option value="">Connect event only</option> <option value="connect">Connect events only</option>
<option value="files">File events only</option>
</select> </select>
</div> </div>
<div class="form-row"> <div class="form-row">

View File

@ -42,6 +42,7 @@ module.exports = function(RED) {
node.hiderightclick = n.hiderightclick || "false"; node.hiderightclick = n.hiderightclick || "false";
node.coords = n.coords || "none"; node.coords = n.coords || "none";
node.showgrid = n.showgrid || "false"; node.showgrid = n.showgrid || "false";
node.allowFileDrop = n.allowFileDrop || "false";
node.path = n.path || "/worldmap"; node.path = n.path || "/worldmap";
if (node.path.charAt(0) != "/") { node.path = "/" + node.path; } if (node.path.charAt(0) != "/") { node.path = "/" + node.path; }
if (!sockets[node.path]) { if (!sockets[node.path]) {
@ -78,6 +79,7 @@ module.exports = function(RED) {
c.showlayers = node.layers; c.showlayers = node.layers;
c.grid = {showgrid:node.showgrid}; c.grid = {showgrid:node.showgrid};
c.hiderightclick = node.hiderightclick; c.hiderightclick = node.hiderightclick;
c.allowFileDrop = node.allowFileDrop;
c.coords = node.coords; c.coords = node.coords;
c.toptitle = node.name; c.toptitle = node.name;
client.write(JSON.stringify({command:c})); client.write(JSON.stringify({command:c}));
@ -139,7 +141,7 @@ module.exports = function(RED) {
var frameWidth = (size.sx +size.cx) *width - size.cx - 1; var frameWidth = (size.sx +size.cx) *width - size.cx - 1;
var frameHeight = (size.sy +size.cy) *height - size.cy - 2; var frameHeight = (size.sy +size.cy) *height - size.cy - 2;
var url = encodeURI(config.path); var url = encodeURI(config.path);
var html = `<div style="overflow:hidden;"> var html = `<style>.nr-dashboard-ui_worldmap{padding:0;}</style><div style="overflow:hidden;">
<iframe src="${url}" width="${frameWidth}px" height="${frameHeight}px" style="border:none;"></iframe> <iframe src="${url}" width="${frameWidth}px" height="${frameHeight}px" style="border:none;"></iframe>
</div> </div>
`; `;
@ -224,11 +226,14 @@ module.exports = function(RED) {
node.status({fill:"green",shape:"dot",text:"connected "+Object.keys(clients).length,_sessionid:client.id}); node.status({fill:"green",shape:"dot",text:"connected "+Object.keys(clients).length,_sessionid:client.id});
client.on('data', function(message) { client.on('data', function(message) {
message = JSON.parse(message); message = JSON.parse(message);
if (node.events !== "connect") { if (message.hasOwnProperty("action")) {
setImmediate(function() {node.send({payload:message, topic:node.path.substr(1), _sessionid:client.id})}); if ((node.events === "files") && (message.action === "file")) {
} setImmediate(function() {node.send({payload:message, topic:node.path.substr(1), _sessionid:client.id})});
else { }
if (message.hasOwnProperty("action") && (message.action === "connected")) { else if ((node.events === "connect") && (message.action === "connected")) {
setImmediate(function() {node.send({payload:message, topic:node.path.substr(1), _sessionid:client.id})});
}
else if (node.events === "all") {
setImmediate(function() {node.send({payload:message, topic:node.path.substr(1), _sessionid:client.id})}); setImmediate(function() {node.send({payload:message, topic:node.path.substr(1), _sessionid:client.id})});
} }
} }
@ -236,7 +241,7 @@ module.exports = function(RED) {
client.on('close', function() { client.on('close', function() {
delete clients[client.id]; delete clients[client.id];
node.status({fill:"green",shape:"ring",text:"connected "+Object.keys(clients).length,_sessionid:client.id}); node.status({fill:"green",shape:"ring",text:"connected "+Object.keys(clients).length,_sessionid:client.id});
if (node.events !== "connect") { if (node.events !== "files") {
node.send({payload:{action:"disconnect", clients:Object.keys(clients).length}, topic:node.path.substr(1), _sessionid:client.id}); node.send({payload:{action:"disconnect", clients:Object.keys(clients).length}, topic:node.path.substr(1), _sessionid:client.id});
} }
}); });
@ -367,7 +372,6 @@ module.exports = function(RED) {
} }
RED.nodes.registerType("worldmap-tracks",WorldMapTracks); RED.nodes.registerType("worldmap-tracks",WorldMapTracks);
var WorldMapHull = function(n) { var WorldMapHull = function(n) {
RED.nodes.createNode(this,n); RED.nodes.createNode(this,n);
this.prop = n.prop || "layer"; this.prop = n.prop || "layer";

View File

@ -23,6 +23,7 @@ var inIframe = false;
var showUserMenu = true; var showUserMenu = true;
var showLayerMenu = true; var showLayerMenu = true;
var showMouseCoords = false; var showMouseCoords = false;
var allowFileDrop = false;
var minimap; var minimap;
var sidebyside; var sidebyside;
var layercontrol; var layercontrol;
@ -94,44 +95,50 @@ var connect = function() {
ws.onmessage = function(e) { ws.onmessage = function(e) {
var data = JSON.parse(e.data); var data = JSON.parse(e.data);
// console.log("DATA",typeof data,data); // console.log("DATA",typeof data,data);
if (data) { if (data) { handleData(data); }
if (Array.isArray(data)) {
//console.log("ARRAY");
// map.closePopup();
// var bnds= L.latLngBounds([0,0]);
for (var prop in data) {
if (data[prop].command) { doCommand(data[prop].command); delete data[prop].command; }
if (data[prop].hasOwnProperty("name")) {
setMarker(data[prop]);
// bnds.extend(markers[data[prop].name].getLatLng());
}
else { console.log("SKIP A",data[prop]); }
}
// map.fitBounds(bnds.pad(0.25));
}
else {
if (typeof data === "string" && data.indexOf("<?xml") == 0) {
if (data.indexOf("<nvg") != -1) {
data = {command:{map:{overlay:"NVG", nvg:data}}};
}
else if (data.indexOf("<kml") != -1) {
data = {command:{map:{overlay:"KML", kml:data}}};
}
}
if (data.command) { doCommand(data.command); delete data.command; }
if (data.hasOwnProperty("name")) { setMarker(data); }
else if (data.hasOwnProperty("type")) { doGeojson("geojson",data); }
else {
console.log("SKIP",data);
// if (typeof data === "string") { doDialog(data); }
// else { console.log("SKIP",data); }
}
}
}
}; };
} }
console.log("CONNECT TO",location.pathname + 'socket'); console.log("CONNECT TO",location.pathname + 'socket');
var handleData = function(data) {
if (Array.isArray(data)) {
//console.log("ARRAY");
// map.closePopup();
// var bnds= L.latLngBounds([0,0]);
for (var prop in data) {
if (data[prop].command) { doCommand(data[prop].command); delete data[prop].command; }
if (data[prop].hasOwnProperty("name")) {
setMarker(data[prop]);
// bnds.extend(markers[data[prop].name].getLatLng());
}
else { console.log("SKIP A",data[prop]); }
}
// map.fitBounds(bnds.pad(0.25));
}
else {
if (typeof data === "string" && data.indexOf("<?xml") == 0) {
if (data.indexOf("<nvg") != -1) {
data = {command:{map:{overlay:"NVG", nvg:data}}};
}
else if (data.indexOf("<kml") != -1) {
data = {command:{map:{overlay:"KML", kml:data}}};
}
else if (data.indexOf("<gpx") != -1) {
console.log("GPX")
data = {command:{map:{overlay:"GPX", gpx:data}}};
}
}
if (data.command) { doCommand(data.command); delete data.command; }
if (data.hasOwnProperty("name")) { setMarker(data); }
else if (data.hasOwnProperty("type")) { doGeojson("geojson",data); }
else {
console.log("SKIP",data);
// if (typeof data === "string") { doDialog(data); }
// else { console.log("SKIP",data); }
}
}
}
window.onunload = function() { if (ws) ws.close(); } window.onunload = function() { if (ws) ws.close(); }
var onoffline = function() { if (!navigator.onLine) map.addLayer(layers["_countries"]); } var onoffline = function() { if (!navigator.onLine) map.addLayer(layers["_countries"]); }
@ -153,6 +160,66 @@ document.addEventListener ("keydown", function (ev) {
// Create the Initial Map object. // Create the Initial Map object.
map = new L.map('map').setView(startpos, startzoom); map = new L.map('map').setView(startpos, startzoom);
var droplatlng;
var target = document.getElementById("map")
target.ondragover = function (ev) {
ev.preventDefault()
ev.dataTransfer.dropEffect = "move"
}
target.ondrop = function (ev) {
if (allowFileDrop === true) {
ev.preventDefault();
droplatlng = map.mouseEventToLatLng(ev);
handleFiles(ev.dataTransfer.files);
}
}
var handleFiles = function(files) {
([...files]).forEach(readFile);
}
var readFile = function(file) {
console.log("FILE",file)
// Check if the file is text or kml
if (file.type &&
file.type.indexOf('text') === -1 &&
file.type.indexOf('kml') === -1 &&
file.type.indexOf('jpeg') === -1 &&
file.type.indexOf('png') === -1) {
console.log('File is not text, kml, jpeg or png.', file.type, file);
return;
}
const reader = new FileReader();
reader.addEventListener('load', (event) => {
var content = event.target.result;
var data;
if (content.indexOf("base64") !== -1) {
if (content.indexOf("image") === -1) {
data = atob(content.split("base64,")[1]);
if (data.indexOf("<gpx") !== -1) {
doCommand({map:{overlay:file.name, gpx:data}});
}
else if (data.indexOf("<kml") !== -1) {
doCommand({map:{overlay:file.name, kml:data}});
}
else if (data.indexOf("<nvg") !== -1) {
doCommand({map:{overlay:file.name, nvg:data}});
}
else {
handleData(data);
}
}
ws.send(JSON.stringify({action:"file", name:file.name, content:content, lat:droplatlng.lat, lon:droplatlng.lng}));
}
else {
console.log("NOT SURE WHAT THIS IS?",content)
}
});
reader.readAsDataURL(file);
}
// Create some buttons // Create some buttons
var menuButton = L.easyButton({states:[{icon:'fa-bars fa-lg', onClick:function() { toggleMenu(); }, title:'Toggle menu'}], position:"topright"}); var menuButton = L.easyButton({states:[{icon:'fa-bars fa-lg', onClick:function() { toggleMenu(); }, title:'Toggle menu'}], position:"topright"});
var fullscreenButton = L.control.fullscreen(); var fullscreenButton = L.control.fullscreen();
@ -1600,7 +1667,6 @@ function setMarker(data) {
var b = marker.getPopup().getContent().split("bearing : "); var b = marker.getPopup().getContent().split("bearing : ");
if (b.length === 2) { b = parseFloat(b[1].split("<br")[0]); } if (b.length === 2) { b = parseFloat(b[1].split("<br")[0]); }
else { b = undefined; } else { b = undefined; }
console.log("P",marker.getPopup().getContent())
ws.send(JSON.stringify({action:"move",name:marker.name,layer:marker.lay,icon:marker.icon,iconColor:marker.iconColor,SIDC:marker.SIDC,draggable:true,lat:parseFloat(marker.getLatLng().lat.toFixed(6)),lon:parseFloat(marker.getLatLng().lng.toFixed(6)),bearing:b })); ws.send(JSON.stringify({action:"move",name:marker.name,layer:marker.lay,icon:marker.icon,iconColor:marker.iconColor,SIDC:marker.SIDC,draggable:true,lat:parseFloat(marker.getLatLng().lat.toFixed(6)),lon:parseFloat(marker.getLatLng().lng.toFixed(6)),bearing:b }));
}); });
} }
@ -1850,6 +1916,11 @@ function doCommand(cmd) {
rightmenuMap.setContent(addmenu); rightmenuMap.setContent(addmenu);
} }
} }
if (cmd.hasOwnProperty("allowFileDrop")) {
if (typeof cmd.allowFileDrop === "string") {
allowFileDrop = cmd.allowFileDrop === "false" ? false : true;
}
}
if (cmd.hasOwnProperty("coords")) { if (cmd.hasOwnProperty("coords")) {
try { coords.removeFrom(map); } try { coords.removeFrom(map); }
catch(e) {} catch(e) {}
@ -1985,7 +2056,6 @@ function doCommand(cmd) {
} }
var parser = new NVG(cmd.map.nvg); var parser = new NVG(cmd.map.nvg);
var geoj = parser.toGeoJSON(); var geoj = parser.toGeoJSON();
overlays[cmd.map.overlay] = L.geoJson(geoj,{ overlays[cmd.map.overlay] = L.geoJson(geoj,{
style: function(feature) { style: function(feature) {
var st = { stroke:true, color:"black", weight:2, fill:true }; var st = { stroke:true, color:"black", weight:2, fill:true };