From 0e7484263cfeda8b918df39d365c026479c001c4 Mon Sep 17 00:00:00 2001 From: Dave Conway-Jones Date: Mon, 22 Oct 2018 21:01:24 +0100 Subject: [PATCH] Add screen split capability --- CHANGELOG.md | 1 + README.md | 2 + package.json | 2 +- worldmap/index.html | 97 ++++-- worldmap/leaflet/Leaflet.Dialog.css | 108 +++++++ worldmap/leaflet/Leaflet.Dialog.js | 374 +++++++++++++++++++++++ worldmap/leaflet/leaflet-side-by-side.js | 5 + worldmap/worldmap.appcache | 4 +- 8 files changed, 562 insertions(+), 31 deletions(-) create mode 100644 worldmap/leaflet/Leaflet.Dialog.css create mode 100644 worldmap/leaflet/Leaflet.Dialog.js diff --git a/CHANGELOG.md b/CHANGELOG.md index bac0db4..8f19585 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ ### Change Log for Node-RED Worldmap + - v1.5.4 - allow remote update of the split position via `msg.command.split` - v1.5.3 - Add side by side mode (via `msg.command` only). - v1.5.2 - Make manually added icons moveable by default. - v1.5.0 - Add multi-map capability - can now have multiple map endpoints. Issue #40 PR #51 diff --git a/README.md b/README.md index fdf5ab3..c127f15 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ map web page for plotting "things" on. ### Updates +- v1.5.4 - allow remote update of the split position via `msg.command.split` - v1.5.3 - Add side by side mode (via `msg.command` only). - v1.5.2 - Make manually added icons moveable by default. - v1.5.0 - Add multi-map capability - can now have multiple map endpoints. @@ -247,6 +248,7 @@ Optional properties include - **showlayer** - show the named overlay - `{command:{showlayer:"foo"}}` - **hidelayer** - hide the named overlay - `{command:{hidelayer:"bar"}}` - **side** - add a second map alongside with slide between them. Use the name of the *baselayer* to add - or "none" to remove the control. - `{command:{side:"Esri Satellite"}}` + - **split** - once you have split the screen - the split value is the % across the screen of the split line. - `{command:{split:50}}` - **map** - Object containing details of a new map layer: - **name** - name of the map base layer OR **overlay** - name of overlay layer - **url** - url of the map layer diff --git a/package.json b/package.json index 71ca64d..9dca051 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "node-red-contrib-web-worldmap", - "version": "1.5.3", + "version": "1.5.4", "description": "A Node-RED node to provide a web page of a world map for plotting things on.", "dependencies": { "cgi": "0.3.1", diff --git a/worldmap/index.html b/worldmap/index.html index a47cfc3..e34cbc1 100644 --- a/worldmap/index.html +++ b/worldmap/index.html @@ -29,6 +29,7 @@ + @@ -45,6 +46,7 @@ + @@ -167,7 +169,7 @@ var connect = function() { }; ws.onmessage = function(e) { var data = JSON.parse(e.data); - //console.log("DATA",data); + //console.log("DATA",typeof data,data); if (Array.isArray(data)) { //console.log("ARRAY"); for (var prop in data) { @@ -179,7 +181,12 @@ var connect = function() { else { if (data.command) { doCommand(data.command); delete data.command; } if (data.hasOwnProperty("name")) { setMarker(data); } - else { console.log("SKIP",data); } + else if (data.hasOwnProperty("type")) { doGeojson(data); } + else { + console.log("SKIP",data); + // if (typeof data === "string") { doDialog(data); } + // else { console.log("SKIP",data); } + } } }; } @@ -223,7 +230,6 @@ var yellowButton = L.easyButton('fa-square wm-yellow', function(btn) { console.l var blackButton = L.easyButton('fa-square wm-black', function(btn) { console.log("BLACK",btn); }) var colorControl = L.easyBar([redButton,blueButton,greenButton,yellowButton,blackButton]); - // Move some bits around if in an iframe if (window.self !== window.top) { console.log("IN an iframe"); @@ -345,6 +351,18 @@ function doTidyUp(l) { } } +// Call tidyup every {maxage} seconds - default 10 mins +var stale = null; +function setMaxAge() { + maxage = document.getElementById('maxage').value; + if (stale) { clearInterval(stale); } + //if (maxage > 0) { + stale = setInterval( function() { doTidyUp() }, 20000); // every 20 secs + //} //every minute + //console.log("Stale time set :",maxage+"s"); +} +setMaxAge(); + // move the daylight / nighttime boundary (if enabled) every minute function moveTerminator() { // if terminator line plotted move it every minute if (layers["_daynight"].getLayers().length > 0) { @@ -352,26 +370,13 @@ function moveTerminator() { // if terminator line plotted move it every minute layers["_daynight"].addLayer(L.terminator()); } } -setInterval( function() {moveTerminator()}, 60000 ); - -// Call tidyup every {maxage} seconds - default 10 mins -var stale = null; -function setMaxAge() { - maxage = document.getElementById('maxage').value; - if (stale) { clearInterval(stale); } - //if (maxage > 0) { - stale = setInterval( function() {doTidyUp()}, 20000); // every 20 secs - //} //every minute - //console.log("Stale time set :",maxage+"s"); -} -setMaxAge(); +setInterval( function() { moveTerminator() }, 60000 ); function setCluster(v) { clusterAt = v || 0; console.log("clusterAt set:",clusterAt); showMapCurrentZoom(); } -//setCluster(); // Search for markers with names of .... function doSearch() { @@ -439,7 +444,6 @@ map.on('overlayadd', function(e) { overlays["satellite"].bringToBack(); } if (e.name == "heatmap") { // show heatmap button when it's layer is added. - //document.getElementById("heat").style.display = 'block'; clrHeat.addTo(map); } if (e.name == "day/night") { @@ -458,7 +462,6 @@ map.on('overlayadd', function(e) { map.on('overlayremove', function(e) { if (e.name == "heatmap") { // hide heatmap button when it's layer is removed. - //document.getElementById("heat").style.display = 'none'; clrHeat.removeFrom(map); } if (e.name == "day/night") { @@ -515,12 +518,12 @@ map.on('zoomend', function() { // ws.send(JSON.stringify({action:"rightclick", lat:e.latlng.lat.toFixed(5), lon:e.latlng.lng.toFixed(5)})); //}); +// single right click to add a marker var rightmenuMap = L.popup({keepInView:true, minWidth:250}).setContent("Add marker
"); var rclk; var addThing = function() { var thing = document.getElementById('rinput').value; - //console.log(thing); map.closePopup(); var bits = thing.split(","); var icon = (bits[1] || "circle").trim(); @@ -528,22 +531,22 @@ var addThing = function() { var colo = (bits[3] || "#910000").trim(); var drag = true; var regi = /^[S,G,E,I,O][A-Z]{4}.*/; // if it looks like a SIDC code - var d; + var d = {action:"point", name:bits[0].trim(), layer:lay, draggable:drag, lat:rclk.lat, lon:rclk.lng}; if (regi.test(icon)) { - icon = (icon+"------------").substr(0,12); - d = {name:bits[0].trim(),layer:lay,SIDC:icon,draggable:drag,lat:rclk.lat,lon:rclk.lng}; + d.SIDC = (icon+"------------").substr(0,12); } else { - d = {name:bits[0].trim(),layer:lay,icon:icon,iconColor:colo,draggable:drag,lat:rclk.lat,lon:rclk.lng}; + d.icon = icon; + d.iconColor = colo; } + ws.send(JSON.stringify(d)); + delete d.action; setMarker(d); map.addLayer(layers[lay]); - d.action = "point"; - ws.send(JSON.stringify(d)); } // allow double right click to zoom out -// single right click opens a message window that sends to the socket. +// single right click opens a message window that adds a marker var rclicked = false; var rtout = null; map.on('contextmenu', function(e) { @@ -769,6 +772,17 @@ layercontrol = L.control.layers(basemaps, overlays); if (!inIframe) { layercontrol.addTo(map); } else { showLayerMenu = false;} +// Add the dialog box for messages +var dialogue = L.control.dialog().addTo(map); +dialogue.freeze(); +dialogue.close(); + +var doDialog = function(d) { + //console.log("DIALOGUE",d); + dialogue.setContent(d); + dialogue.open(); +} + // Delete a marker (and notify websocket) var delMarker = function(dname) { if (typeof polygons[dname] != "undefined") { @@ -1233,6 +1247,7 @@ function setMarker(data) { } } +// handle any incoming COMMANDS to control the map remotely function doCommand(cmd) { // console.log("COMMAND",cmd); // ignore server side initial command if client position already saved. @@ -1478,19 +1493,19 @@ function doCommand(cmd) { basemaps[baselayername].addTo(map); } // Add side by side control - if (cmd.side) { console.log("SIDE",cmd.side); } if (cmd.side && (cmd.side === "none")) { sidebyside.remove(); map.removeLayer(basemaps[sidebyside.lay]); sidebyside = undefined; } if (cmd.side && basemaps.hasOwnProperty(cmd.side)) { - if (sidebyside) { sidebyside.removeFrom(map); } + if (sidebyside) { sidebyside.remove(); map.removeLayer(basemaps[sidebyside.lay]); } basemaps[cmd.side].addTo(map); sidebyside = L.Control.sideBySide(basemaps[baselayername], basemaps[cmd.side]); sidebyside.addTo(map); sidebyside.lay = cmd.side; } + if (cmd.split && sidebyside && (cmd.split <= 100) && (cmd.split >= 0)) { console.log("DING",cmd.split); sidebyside.setSplit(cmd.split); } // Turn on an existing overlay if (cmd.hasOwnProperty("showlayer") && overlays.hasOwnProperty(cmd.showlayer)) { map.addLayer(overlays[cmd.showlayer]); @@ -1520,6 +1535,30 @@ function doCommand(cmd) { } map.setView([clat,clon],czoom); } + +// handle any incoming GEOJSON directly - may style badly +function doGeojson(g) { + console.log("GEOJSON",g); + if (!basemaps["geojson"]) { + var opt = { style: function(feature) { + var st = { stroke:true, color:"#910000", weight:2, fill:true, fillColor:"#910000", fillOpacity:0.3 }; + if (feature.hasOwnProperty("properties")) { + console.log("GPROPS", feature.properties) + } + if (feature.hasOwnProperty("style")) { + console.log("GSTYLE", feature.style) + } + return st; + } + } + opt.onEachFeature = function (f,l) { + if (f.properties) { l.bindPopup('
'+JSON.stringify(f.properties,null,' ').replace(/[\{\}"]/g,'')+'
'); } + } + overlays["geojson"] = L.geoJson(g,opt); + layercontrol.addOverlay(overlays["geojson"],"geojson"); + } + overlays["geojson"].addData(g); +} diff --git a/worldmap/leaflet/Leaflet.Dialog.css b/worldmap/leaflet/Leaflet.Dialog.css new file mode 100644 index 0000000..8ede848 --- /dev/null +++ b/worldmap/leaflet/Leaflet.Dialog.css @@ -0,0 +1,108 @@ +.leaflet-control-dialog { + position: absolute; + background-color: #fff; + padding: 0px; + text-align: left; + border-radius: 4px; + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65); + display: flex; + flex-direction: column; +} + +.leaflet-control-dialog .leaflet-control-dialog-inner { + position: relative; + box-sizing: border-box; + float: left; + width: 100%; + height: 100%; + padding: 16px 0px; +} + +.leaflet-control-dialog + .leaflet-control-dialog-inner + .leaflet-control-dialog-grabber { + position: absolute; + box-sizing: border-box; + width: 20px; + height: 20px; + top: 0px; + left: 0px; + padding: 3px; + font-size: 15px; + line-height: 15px; + color: #ccc; +} + +.leaflet-control-dialog + .leaflet-control-dialog-inner + .leaflet-control-dialog-grabber:hover { + cursor: grab; + cursor: -webkit-grab; + cursor: -moz-grab; +} + +.leaflet-control-dialog + .leaflet-control-dialog-inner + .leaflet-control-dialog-close { + position: absolute; + box-sizing: border-box; + width: 20px; + height: 20px; + top: 0px; + right: 0px; + padding: 2px; + font-size: 16px; + line-height: 16px; + color: #666; +} + +.leaflet-control-dialog + .leaflet-control-dialog-inner + .leaflet-control-dialog-close:hover { + cursor: pointer; +} + +.leaflet-control-dialog + .leaflet-control-dialog-inner + .leaflet-control-dialog-contents { + position: relative; + float: left; + width: 100%; + height: 100%; + margin: 0px; + padding: 0 14px; + min-height: 50px; + overflow: auto; + box-sizing: border-box; +} + +.leaflet-control-dialog + .leaflet-control-dialog-inner + .leaflet-control-dialog-resizer { + position: absolute; + box-sizing: border-box; + width: 20px; + height: 20px; + bottom: 0px; + right: 0px; + padding: 2px; + font-size: 16px; + line-height: 16px; + color: #ccc; +} + +.leaflet-control-dialog + .leaflet-control-dialog-inner + .leaflet-control-dialog-resizer:hover { + cursor: grab; + cursor: -webkit-grab; + cursor: -moz-grab; +} + +.fa-rotate-45 { + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -ms-transform: rotate(45deg); + -o-transform: rotate(45deg); + transform: rotate(45deg); +} diff --git a/worldmap/leaflet/Leaflet.Dialog.js b/worldmap/leaflet/Leaflet.Dialog.js new file mode 100644 index 0000000..691fb19 --- /dev/null +++ b/worldmap/leaflet/Leaflet.Dialog.js @@ -0,0 +1,374 @@ +L.Control.Dialog = L.Control.extend({ + options: { + size: [ 200, 160 ], + minSize: [ 200, 100 ], + maxSize: [ 400, 800 ], + anchor: [ 44, -165 ], + position: "topright", + initOpen: true + }, + + initialize: function(options) { + this.options = JSON.parse(JSON.stringify(this.options)); + L.setOptions(this, options); + this._attributions = {}; + }, + + onAdd: function(map) { + this._initLayout(); + this._map = map; + this.update(); + + if (!this.options.initOpen) { + this.close(); + } + + return this._container; + }, + + open: function() { + if (!this._map) { + return; + } + this._container.style.visibility = ""; + + this._map.fire("dialog:opened", this); + + return this; + }, + + close: function() { + this._container.style.visibility = "hidden"; + + this._map.fire("dialog:closed", this); + return this; + }, + + destroy: function() { + if (!this._map) { + return this; + } + + this._map.fire("dialog:destroyed", this); + this.remove(); + + if (this.onRemove) { + this.onRemove(this._map); + } + + return this; + }, + + setLocation: function(location) { + location = location || [ 250, 250 ]; + + this.options.anchor[0] = 0; + this.options.anchor[1] = 0; + this._oldMousePos.x = 0; + this._oldMousePos.y = 0; + + this._move(location[1], location[0]); + + return this; + }, + + setSize: function(size) { + size = size || [ 300, 300 ]; + + this.options.size[0] = 0; + this.options.size[1] = 0; + this._oldMousePos.x = 0; + this._oldMousePos.y = 0; + + this._resize(size[0], size[1]); + + return this; + }, + + lock: function() { + this._resizerNode.style.visibility = "hidden"; + this._grabberNode.style.visibility = "hidden"; + this._closeNode.style.visibility = "hidden"; + + this._map.fire("dialog:locked", this); + return this; + }, + + unlock: function() { + this._resizerNode.style.visibility = ""; + this._grabberNode.style.visibility = ""; + this._closeNode.style.visibility = ""; + + this._map.fire("dialog:unlocked", this); + return this; + }, + + freeze: function() { + this._resizerNode.style.visibility = "hidden"; + this._grabberNode.style.visibility = "hidden"; + + this._map.fire("dialog:frozen", this); + return this; + }, + + unfreeze: function() { + this._resizerNode.style.visibility = ""; + this._grabberNode.style.visibility = ""; + + this._map.fire("dialog:unfrozen", this); + return this; + }, + + hideClose: function() { + this._closeNode.style.visibility = "hidden"; + + this._map.fire("dialog:closehidden", this); + return this; + }, + + showClose: function() { + this._closeNode.style.visibility = ""; + + this._map.fire("dialog:closeshown", this); + return this; + }, + + hideResize: function() { + this._resizerNode.style.visibility = "hidden"; + + this._map.fire("dialog:resizehidden", this); + return this; + }, + + showResize: function() { + this._resizerNode.style.visibility = ""; + + this._map.fire("dialog:resizeshown", this); + return this; + }, + + setContent: function(content) { + this._content = content; + this.update(); + return this; + }, + + getContent: function() { + return this._content; + }, + + getElement: function() { + return this._container; + }, + + update: function() { + if (!this._map) { + return; + } + + this._container.style.visibility = "hidden"; + + this._updateContent(); + this._updateLayout(); + + this._container.style.visibility = ""; + this._map.fire("dialog:updated", this); + }, + + _initLayout: function() { + var className = "leaflet-control-dialog"; + var container = (this._container = L.DomUtil.create("div", className)); + + container.style.width = this.options.size[0] + "px"; + //container.style.height = this.options.size[1] + "px"; + + container.style.top = this.options.anchor[0] + "px"; + // container.style.left = this.options.anchor[1] + "px"; + container.style.right = "0px"; + // container.style.display = "flex"; + // container.style["flex-direction"] = "column"; + + var stop = L.DomEvent.stopPropagation; + L.DomEvent.on(container, "click", stop) + .on(container, "mousedown", stop) + .on(container, "touchstart", stop) + .on(container, "dblclick", stop) + .on(container, "mousewheel", stop) + .on(container, "contextmenu", stop) + .on(container, "MozMousePixelScroll", stop); + + var innerContainer = (this._innerContainer = L.DomUtil.create( + "div", + className + "-inner" + )); + + var grabberNode = (this._grabberNode = L.DomUtil.create( + "div", + className + "-grabber" + )); + var grabberIcon = L.DomUtil.create("i", "fa fa-arrows"); + grabberNode.appendChild(grabberIcon); + + L.DomEvent.on(grabberNode, "mousedown", this._handleMoveStart, this); + + var closeNode = (this._closeNode = L.DomUtil.create( + "div", + className + "-close" + )); + var closeIcon = L.DomUtil.create("i", "fa fa-times"); + closeNode.appendChild(closeIcon); + L.DomEvent.on(closeNode, "click", this._handleClose, this); + + var resizerNode = (this._resizerNode = L.DomUtil.create( + "div", + className + "-resizer" + )); + var resizeIcon = L.DomUtil.create("i", "fa fa-arrows-h fa-rotate-45"); + resizerNode.appendChild(resizeIcon); + + L.DomEvent.on(resizerNode, "mousedown", this._handleResizeStart, this); + + var contentNode = (this._contentNode = L.DomUtil.create( + "div", + className + "-contents" + )); + + container.appendChild(innerContainer); + + innerContainer.appendChild(contentNode); + innerContainer.appendChild(grabberNode); + innerContainer.appendChild(closeNode); + innerContainer.appendChild(resizerNode); + + this._oldMousePos = { x: 0, y: 0 }; + }, + + _handleClose: function() { + this.close(); + }, + + _handleResizeStart: function(e) { + this._oldMousePos.x = e.clientX; + this._oldMousePos.y = e.clientY; + + L.DomEvent.on(this._map, "mousemove", this._handleMouseMove, this); + L.DomEvent.on(this._map, "mouseup", this._handleMouseUp, this); + + this._map.fire("dialog:resizestart", this); + this._resizing = true; + }, + + _handleMoveStart: function(e) { + this._oldMousePos.x = e.clientX; + this._oldMousePos.y = e.clientY; + + L.DomEvent.on(this._map, "mousemove", this._handleMouseMove, this); + L.DomEvent.on(this._map, "mouseup", this._handleMouseUp, this); + + this._map.fire("dialog:movestart", this); + this._moving = true; + }, + + _handleMouseMove: function(e) { + var diffX = e.originalEvent.clientX - this._oldMousePos.x, + diffY = e.originalEvent.clientY - this._oldMousePos.y; + + // this helps prevent accidental highlighting on drag: + if (e.originalEvent.stopPropagation) { + e.originalEvent.stopPropagation(); + } + if (e.originalEvent.preventDefault) { + e.originalEvent.preventDefault(); + } + + if (this._resizing) { + this._resize(diffX, diffY); + } + + if (this._moving) { + this._move(diffX, diffY); + } + }, + + _handleMouseUp: function() { + L.DomEvent.off(this._map, "mousemove", this._handleMouseMove, this); + L.DomEvent.off(this._map, "mouseup", this._handleMouseUp, this); + + if (this._resizing) { + this._resizing = false; + this._map.fire("dialog:resizeend", this); + } + + if (this._moving) { + this._moving = false; + this._map.fire("dialog:moveend", this); + } + }, + + _move: function(diffX, diffY) { + var newY = this.options.anchor[0] + diffY; + var newX = this.options.anchor[1] + diffX; + + this.options.anchor[0] = newY; + this.options.anchor[1] = newX; + + this._container.style.top = this.options.anchor[0] + "px"; + this._container.style.left = this.options.anchor[1] + "px"; + + this._map.fire("dialog:moving", this); + + this._oldMousePos.y += diffY; + this._oldMousePos.x += diffX; + }, + + _resize: function(diffX, diffY) { + var newX = this.options.size[0] + diffX; + var newY = this.options.size[1] + diffY; + + if (newX <= this.options.maxSize[0] && newX >= this.options.minSize[0]) { + this.options.size[0] = newX; + this._container.style.width = this.options.size[0] + "px"; + this._oldMousePos.x += diffX; + } + + if (newY <= this.options.maxSize[1] && newY >= this.options.minSize[1]) { + this.options.size[1] = newY; + this._container.style.height = this.options.size[1] + "px"; + this._oldMousePos.y += diffY; + } + + this._map.fire("dialog:resizing", this); + }, + + _updateContent: function() { + if (!this._content) { + return; + } + + var node = this._contentNode; + var content = + typeof this._content === "function" ? this._content(this) : this._content; + + if (typeof content === "string") { + node.innerHTML = content; + } else { + while (node.hasChildNodes()) { + node.removeChild(node.firstChild); + } + node.appendChild(content); + } + }, + + _updateLayout: function() { + this._container.style.width = this.options.size[0] + "px"; + //this._container.style.height = this.options.size[1] + "px"; + + this._container.style.top = this.options.anchor[0] + "px"; + //this._container.style.left = this.options.anchor[1] + "px"; + this._container.style.right = "0px"; + } +}); + +L.control.dialog = function(options) { + return new L.Control.Dialog(options); +}; diff --git a/worldmap/leaflet/leaflet-side-by-side.js b/worldmap/leaflet/leaflet-side-by-side.js index e16fea7..18f38a0 100644 --- a/worldmap/leaflet/leaflet-side-by-side.js +++ b/worldmap/leaflet/leaflet-side-by-side.js @@ -78,6 +78,11 @@ L.Control.SideBySide = L.Control.extend({ return this }, + setSplit: function(s) { + this._range.value = s/100; + this._updateClip(); + }, + _updateClip: function () { var map = this._map var rangeValue = this._range.value diff --git a/worldmap/worldmap.appcache b/worldmap/worldmap.appcache index ba7c67b..742c6a6 100644 --- a/worldmap/worldmap.appcache +++ b/worldmap/worldmap.appcache @@ -1,5 +1,5 @@ CACHE MANIFEST -# date: Oct 15th 2018 - v1.5.3 +# date: Oct 17th 2018 - v1.5.4 CACHE: index.html @@ -8,6 +8,8 @@ images/node-red.png images/world-50m-flat.json css/map.css leaflet/L.Terminator.js +leaflet/Leaflet.Dialog.css +leaflet/Leaflet.Dialog.js leaflet/Leaflet.fullscreen.min.js leaflet/Leaflet.vector-markers.css leaflet/Leaflet.vector-markers.min.js