Live data on top of changing picture

OK, so i'm building a compass which gets it data from Openweather API. I'm trying to incorporated that live data (wind direction and speed) into a live compass module. Based on the direction it would choose from eight different pictures (I'm testing only with North) and would lay on top the Wind speed.

I have seen the sample by @hotNipi

This works great as long as your background image is static. But if I change it to be based on the msg.output then its is either image or data. The overlay is not compiling them correctly. What might be the issue.

On sample I'm using only North image and manually injecting 10ms wind speed. I'm using IMAGE-TOOLS to bring up the picture in UI-Template.

[{"id":"1a00ab29e41635cc","type":"ui_template","z":"3d738ea.22f7872","group":"20ad50091d22a9aa","name":"","order":5,"width":"13","height":"10","format":"\n<div id=\"bgr3\">\n    <div class=\"txt\" id=\"txt1\">{{msg.payload.first}}</div>\n</div>\n\n<style>\n\n#bgr3{\n    background-image:url({{msg.payload.dir}});\n    background-size: contain;\n    background-repeat: no-repeat;\n    position:relative;\n    width:100%;\n    height:100%;\n}\n\n.txt{\n    color:white;\n    font-weight:bold;\n    font-size: 30px;\n}\n#txt1{\n    position: absolute;\n    left: 100px;\n    top: 100px;\n}\n\n</style>\n","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":false,"templateScope":"local","className":"","x":1595.4286994934082,"y":1786.0000257492065,"wires":[["0a3588a32451ad3b"]]},{"id":"9a3cc193991def72","type":"inject","z":"3d738ea.22f7872","name":"Wind Speed","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"first\":\"7ms\"}","payloadType":"json","x":1380.428695678711,"y":1754.0000247955322,"wires":[["1a00ab29e41635cc"]]},{"id":"b1dc72b7cbae5527","type":"file in","z":"3d738ea.22f7872","name":"","filename":"/home/security/Pictures/North4.png","filenameType":"str","format":"","chunk":false,"sendError":false,"allProps":false,"x":828.2000465393066,"y":1827.2000980377197,"wires":[["0d3151f0fbfb5a24"]]},{"id":"0d3151f0fbfb5a24","type":"jimp-image","z":"3d738ea.22f7872","name":"","data":"payload","dataType":"msg","ret":"b64","parameter1":"120","parameter1Type":"num","parameter2":"","parameter2Type":"none","parameter3":"","parameter3Type":"msg","parameter4":"","parameter4Type":"msg","parameter5":"","parameter5Type":"msg","parameter6":"","parameter6Type":"msg","parameter7":"","parameter7Type":"msg","parameter8":"","parameter8Type":"msg","sendProperty":"payload","sendPropertyType":"msg","parameterCount":0,"jimpFunction":"none","selectedJimpFunction":{"name":"none","fn":"none","description":"Just loads the image.","parameters":[]},"x":1117.2000579833984,"y":1827.200098991394,"wires":[["8c40f886d841a1ed","a665ba2af61ac5ad"]]},{"id":"8c40f886d841a1ed","type":"image viewer","z":"3d738ea.22f7872","name":"","width":"100","data":"payload","dataType":"msg","active":true,"x":1282.1998138427734,"y":1925.2001008987427,"wires":[[]]},{"id":"e4fb4487e4a258a2","type":"inject","z":"3d738ea.22f7872","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"first\":\"55deg\",\"second\":\"120deg\"}","payloadType":"json","x":531.0001564025879,"y":1829.0000257492065,"wires":[["b1dc72b7cbae5527"]]},{"id":"0a3588a32451ad3b","type":"debug","z":"3d738ea.22f7872","name":"debug 60","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1585.9999885559082,"y":1916.6000022888184,"wires":[]},{"id":"a665ba2af61ac5ad","type":"change","z":"3d738ea.22f7872","name":"","rules":[{"t":"move","p":"payload","pt":"msg","to":"payload.dir","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":1363.999984741211,"y":1826.4000511169434,"wires":[["1a00ab29e41635cc"]]},{"id":"20ad50091d22a9aa","type":"ui_group","name":"Default","tab":"15ab88b14795fa9a","order":1,"disp":true,"width":"15","collapse":false,"className":""},{"id":"15ab88b14795fa9a","type":"ui_tab","name":"X-TEST","icon":"dashboard","disabled":false,"hidden":false}]

Help appriciated as always :slight_smile:

Hi, you don't need to load multiple images to get a wind direction rose.

This is an implementation of the above using uibuilder but you could use other dashboards.

Base HTML:

<div class="compass">
  <div class="direction">
    <p>NE<span>10 mph</span></p>
  </div>
  <div class="arrow ne"></div>
</div>

CSS (I simplified this in my example but it is basically very similar):

@font-face {
  font-family: 'Dosis';
  font-style: normal;
  font-weight: 200;
  src: url(https://fonts.gstatic.com/s/dosis/v27/HhyJU5sn9vOmLxNkIwRSjTVNWLEJt7Ml2xMB.ttf) format('truetype');
}
@font-face {
  font-family: 'Dosis';
  font-style: normal;
  font-weight: 400;
  src: url(https://fonts.gstatic.com/s/dosis/v27/HhyJU5sn9vOmLxNkIwRSjTVNWLEJN7Ml2xMB.ttf) format('truetype');
}
@font-face {
  font-family: 'Dosis';
  font-style: normal;
  font-weight: 500;
  src: url(https://fonts.gstatic.com/s/dosis/v27/HhyJU5sn9vOmLxNkIwRSjTVNWLEJBbMl2xMB.ttf) format('truetype');
}
@font-face {
  font-family: 'Dosis';
  font-style: normal;
  font-weight: 600;
  src: url(https://fonts.gstatic.com/s/dosis/v27/HhyJU5sn9vOmLxNkIwRSjTVNWLEJ6bQl2xMB.ttf) format('truetype');
}
.compass {
  display: block;
  width: 80px;
  height: 80px;
  border-radius: 100%;
  box-shadow: 0 0 10px rgba(0, 0, 0, 0.85);
  position: relative;
  font-family: 'Dosis';
  color: #555;
  text-shadow: 1px 1px 1px white;
}
.compass:before {
  font-weight: bold;
  position: absolute;
  text-align: center;
  width: 100%;
  content: "N";
  font-size: 14px;
  top: -2px;
}
.compass .direction {
  height: 100%;
  width: 100%;
  display: block;
  background: #f2f6f5;
  background: -moz-linear-gradient(top, #f2f6f5 0%, #cbd5d6 100%);
  background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #f2f6f5), color-stop(100%, #cbd5d6));
  background: -webkit-linear-gradient(top, #f2f6f5 0%, #cbd5d6 100%);
  background: -o-linear-gradient(top, #f2f6f5 0%, #cbd5d6 100%);
  border-radius: 100%;
}
.compass .direction p {
  text-align: center;
  margin: 0;
  padding: 0;
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  height: 100%;
  line-height: 80px;
  display: block;
  margin-top: -45px;
  font-size: 28px;
  font-weight: bold;
}
.compass .direction p span {
  display: block;
  line-height: normal;
  margin-top: -24px;
  font-size: 11px;
  text-transform: uppercase;
  font-weight: normal;
}
.compass .arrow {
  width: 100%;
  height: 100%;
  display: block;
  position: absolute;
  top: 0;
}
.compass .arrow:after {
  content: "";
  width: 0;
  height: 0;
  border-left: 5px solid transparent;
  border-right: 5px solid transparent;
  border-bottom: 10px solid red;
  position: absolute;
  top: -6px;
  left: 50%;
  margin-left: -5px;
  z-index: 99;
}
.compass .arrow.nne {
  transform: rotate(22.5deg);
}
.compass .arrow.ne {
  transform: rotate(45deg);
}
.compass .arrow.ene {
  transform: rotate(67.5deg);
}
.compass .arrow.e {
  transform: rotate(90deg);
}
.compass .arrow.ese {
  transform: rotate(112.5deg);
}
.compass .arrow.se {
  transform: rotate(135deg);
}
.compass .arrow.sse {
  transform: rotate(157.5deg);
}
.compass .arrow.s {
  transform: rotate(180deg);
}
.compass .arrow.ssw {
  transform: rotate(202.5deg);
}
.compass .arrow.sw {
  transform: rotate(-135deg);
}
.compass .arrow.wsw {
  transform: rotate(-114.5deg);
}
.compass .arrow.w {
  transform: rotate(-90deg);
}
.compass .arrow.wnw {
  transform: rotate(-69.5deg);
}
.compass .arrow.nw {
  transform: rotate(-45deg);
}
.compass .arrow.nnw {
  transform: rotate(-24.5deg);
}

I use a function in node-red to convert the direction in degrees to the compass positions.

function degToCompass(num) {
    const val = Number((num / 22.5).toFixed())
    const arr = ["n", "nne", "ne", "ene", "e", "ese", "se", "sse", "s", "ssw", "sw", "wsw", "w", "wnw", "nw", "nnw"]
    return arr[(val % 16)]
}

That gets put into the class attribute for the arrow.

1 Like

Thanks, This definitely will help on simple compass, but as always i'm little bit "artsy" so I want to see my own design. And being able to show "live"data in front of static and now also live background is something that i have tried to make for a while now so still looking more solutions.

Nothing wrong with that :grin:

In fact, though, if you think through the shared solution a bit further, you should find that it will also help with your use-case.

At present, that example uses a simple background, but there is nothing stopping you from putting an image into that background instead. If I've understood you right, that would do what you want wouldn't it? In CSS, you can define a background image to a block display element so that should work. You could take out the named compass points if they were already in the image.

That then leaves you with the background and the arrow. I'll admit that the arrow in that example is a bit wimpy (it isn't my example of course). But it should be possible to beef it up.

SOLVED..sort of...

So as pointed the original problem was to create a wind compass that would join live background with live data. Approach originally was to perhaps use the similar approach on other new adventures , but in the end as this solution seemed as dead end with my skill (copy, paste and steal code from others) I needed to look else where. This is where I stumbeled the excellent video of SVG image manipulation
Example of Controllable SVG elements in Node RED Dashboard
Using a online tool BOXY SVG Editor, I could replicate the movements and status update on my own design and integrate it to my Openweather data. So one could say I missed the target , but hitted the point :slight_smile:

Just connect incoming wind direction (0-359) and windspeed to nodes. The wind is from North (0) and wind speed is 4 m/s at the picture. This template is used in tablets around the house.

[{"id":"1ac0ab0f0185d3bc","type":"ui_template","z":"9a28137e.7a067","group":"b1d8674.c132898","name":"SVG Compass","order":7,"width":8,"height":8,"format":"<div id=\"graphics\" ng-init=\"checkSVG()\">\n\n    <!-- SVG FILE CONTENT BELOW -->\n\n<svg viewBox=\"0 0 400 400\" width=\"275px\" height=\"275px\" xmlns=\"http://www.w3.org/2000/svg\"\n    xmlns:bx=\"https://boxy-svg.com\">\n    <defs></defs>\n    <g id=\"compass@cx_move\">\n        <g transform=\"matrix(1, 0, 0, 1, 0.000006, 0)\" bx:origin=\"0.5 0.535722\">\n            <path\n                d=\"M 351.501 200 C 351.501 283.672 283.672 351.501 200 351.501 C 116.328 351.501 48.499 283.672 48.499 200 C 48.499 116.328 116.328 48.499 200 48.499 C 283.672 48.499 351.501 116.328 351.501 200 Z M 200 70.355 C 126.79 70.355 67.441 128.399 67.441 200 C 67.441 271.601 126.79 329.645 200 329.645 C 273.21 329.645 332.559 271.601 332.559 200 C 332.559 128.399 273.21 70.355 200 70.355 Z\"\n                style=\"fill: rgb(255, 255, 255); stroke: rgb(255, 255, 255);\"></path>\n            <path d=\"M 199.737 25.186 L 218.363 82.012 L 181.112 82.012 L 199.737 25.186 Z\"\n                style=\"fill: rgb(255, 0, 0); stroke: rgb(255, 255, 255);\"\n                bx:shape=\"triangle 181.112 25.186 37.251 56.826 0.5 0 1@f0735005\"></path>\n        </g>\n    </g>\n    <text id=\"WS@cx_status\"\n        style=\"fill: rgb(255, 255, 255); font-family: &quot;Agency FB&quot;; font-size: 20px; font-weight: 700; text-anchor: middle; white-space: pre;\"\n        transform=\"matrix(6.652061, 0, 0, 5.619027, -893.371887, -830.482849)\" x=\"164.307\" y=\"190.806\">00</text>\n</svg>\n\n    <!-- SVG FILE CONTENT ABOVE -->\n\n    <script>\n        (function($scope) {\n            const cxMove = \"cx_move\";\n            const cxColor = \"cx_color\";\n            const cxStatus = \"cx_status\";\n            const cxHide = \"cx_hide\";\n            const cxStroke = \"cx_stroke\";\n            const maxStatusLength = 12;\n\n\n            $scope.checkSVG = function() {\n\n                if (!$scope.graphicObjects) {\n                    $scope.graphicObjects = [];\n                }\n                recursiveEach($(\"#graphics\"), $scope.graphicObjects);\n                // console.log($scope.graphicObjects);\n\n                function recursiveEach(element, container){\n                    element.children().each(function () {\n                        const $currentElement = $(this);\n                        if (this.id && this.id.indexOf('@') > 0) {\n                            container.push({\n                                id: this.id,\n                                element: $currentElement,\n                                cxMove: (this.id.indexOf(cxMove) > 0),\n                                cxColor: (this.id.indexOf(cxColor) > 0),\n                                cxStatus: (this.id.indexOf(cxStatus) > 0),\n                                cxHide: (this.id.indexOf(cxHide) > 0),\n                                cxStroke: (this.id.indexOf(cxStroke) > 0),\n                            });\n                        }\n                        recursiveEach($currentElement, container);\n                    });\n                }\n            };\n\n            $scope.$watch('msg', function() {\n                if (!$scope.msg) return;\n\n                if (Array.isArray($scope.msg.payload)) {\n                    for (let aReq of $scope.msg.payload) {\n                        animateObject(aReq.payload, aReq.topic);\n                    }\n                } else {\n                    animateObject($scope.msg.payload, $scope.msg.topic);\n                }\n            });\n\n            function animateObject(payload, topic) {\n\n                if (payload === undefined || payload === null ||\n                    !topic || typeof topic !== 'string' || !topic.includes('@')) return;\n\n                const id = topic.split('@')[0], cxType = topic.split('@')[1];\n\n                for (const graphicObject of $scope.graphicObjects) {\n                    const objId = graphicObject.id.split('@')[0];\n                    if (objId !== id || graphicObject.id.indexOf(cxType) === -1) continue;\n\n                    if (cxType === cxColor && graphicObject.cxColor && typeof payload === 'string') {\n\n                        graphicObject.element.css({fill : payload});\n\n                    } else if (cxType === cxStatus && graphicObject.cxStatus && typeof payload === 'string') {\n                        const statusText = (payload.length > maxStatusLength) ?\n                            payload.substring(0, maxStatusLength - 3) + '...' : payload;\n\n                        graphicObject.element.text(statusText);\n\n                    } else if (cxType === cxHide && graphicObject.cxHide && typeof payload === 'boolean') {\n                        const displayAttr = (payload)? 'none' : '';\n                        graphicObject.element.css({display: displayAttr})\n\n                    } else if (cxType === cxMove && graphicObject.cxMove) {\n                        // box = SVGRect {x: 51.95399475097656, y: 54.69599914550781, width: 94.41000366210938, height: 94.41000366210938}\n                        const box = graphicObject.element[0].getBBox();\n                        const matrix = getMatrix(payload, box);\n                        if (matrix) graphicObject.element.attr('transform', matrix);\n\n                    } else if (cxType === cxStroke && graphicObject.cxStroke) {\n                        // const displayAttr = (payload)? 'none' : '';\n                        if (payload.color != null) graphicObject.element.css({stroke: payload.color});\n                        if (payload.width != null) graphicObject.element.css({[\"stroke-width\"]: payload.width});\n                    }\n                }\n            }\n\n\n            function getMatrix(config, box) {\n                if (!config || !box) return;\n\n                // translation\n                const tx = config.x || 0;\n                const ty = config.y || 0;\n                // angle in radians\n                const rads = config.deg * (Math.PI / 180) || 0;\n                // rotation centre\n                // box = { x: 51.95399475097656, y: 54.69599914550781, width: 94.41000366210938, height: 94.41000366210938 }\n                const pivot_x = (config.pivot && config.pivot[0])? config.pivot[0] : 0;\n                const pivot_y = (config.pivot && config.pivot[1])? config.pivot[1] : 0;\n                const cx = box.x + box.width * ( (pivot_x)? pivot_x : 0.5 );\n                const cy = box.y + box.height * ( (pivot_y)? pivot_y : 0 );\n                // scaling\n                const sx = 1;\n                const sy = 1;\n\n                // matrix(0.594088, -0.516432, 0.62601, 0.720144, -223.586029, 145.949615)\n                return 'matrix(' +\n                    (sx * Math.cos(rads)) + ', ' + (sy * Math.sin(rads)) + ', ' +\n                    (-sx * Math.sin(rads)) + ', ' + (sy * Math.cos(rads)) + ', ' +\n                    ((-Math.cos(rads) * cx + Math.sin(rads) * cy + cx) * sx + tx) + ', ' +\n                    ((-Math.sin(rads) * cx - Math.cos(rads) * cy + cy) * sy + ty) + ')';\n            }\n\n        })(scope);\n    </script>\n</div>","storeOutMessages":true,"fwdInMessages":true,"resendOnRefresh":true,"templateScope":"local","className":"","x":819.533447265625,"y":579.5334854125977,"wires":[[]]},{"id":"4d56ee5f9bf4b9df","type":"function","z":"9a28137e.7a067","name":"Wind D","func":"return {\n    payload: {\n    x: 0,\n    y: 0,\n    deg: msg.payload,\n    pivot: [0.5, 0.53859]\n    },\n    topic: \"compass@cx_move\"\n};\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":646.466682434082,"y":534.0001792907715,"wires":[["1ac0ab0f0185d3bc"]]},{"id":"1ee1f47200d5b210","type":"function","z":"9a28137e.7a067","name":"WS to String","func":"msg.payload = String(msg.payload);\nreturn msg;\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":653.1333236694336,"y":626.6667671203613,"wires":[["1ac0ab0f0185d3bc"]]},{"id":"684cd1e826823cb9","type":"change","z":"9a28137e.7a067","name":"Wind Speed","rules":[{"t":"set","p":"topic","pt":"msg","to":"WS@cx_status","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":477.13333892822266,"y":627.533447265625,"wires":[["1ee1f47200d5b210"]]},{"id":"07ba9b99896c7152","type":"calculator","z":"9a28137e.7a067","name":"","inputMsgField":"payload","outputMsgField":"payload","operation":"round","constant":"","round":false,"decimals":0,"x":459.99999237060547,"y":555.3333492279053,"wires":[["684cd1e826823cb9"]]},{"id":"b1d8674.c132898","type":"ui_group","name":"Default","tab":"2c5a0e64.f3ab02","order":1,"disp":false,"width":28,"collapse":false},{"id":"2c5a0e64.f3ab02","type":"ui_tab","name":"Simple TEMP","icon":"fa-thermometer-half","order":1,"disabled":false,"hidden":false}]

Big thank you to Alex on his work on the controlling the SVG images.
https://github.com/alex-controlx/red-dashboard-svg-control
Hope this will help others with similar issues.

1 Like

Thanks for sharing. Still learning SVG myself and I'm always surprised by what you can do with it with a mix of SVG, HTML and CSS. Been working through an update to one of my old SVG examples (SVG "bulbs" overlaid on an SVG floor plan) to move away from VueJS to vanilla SVG/HTML/CSS so all examples add to the store of knowledge.

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.