SVGs on Custom Pages

From MidasWiki
Revision as of 13:38, 12 May 2025 by Rudzki (talk | contribs)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search
Plain SVG based custom page

In this example, an SVG image forms the basis of a custom page. It displays two valves and an expansion volume.

The valves change color depending on their status — open or closed. The expansion volume dynamically changes its height.

All objects within the SVG file are directly modified using JavaScript functions.

Custom Page HTML code

Emptying the expansion volume displayed on an SVG-based custom page

The custom page displays the SVG inside an mtable. The two valves turn green when open and red when closed. This is done by retrieving the SVG element using objVal = svgDoc.getElementById(valveID) and setting its fill color via obj_Valve.style.fill = ...


Sets of modbbutton are used to change the associated ODB boolean variable. A confirmation dialog appears to prevent accidental valve operation.


The expansion volume shows its fill state both via a modbvbar and by adjusting the height of the corresponding SVG object. Depending on how the object is constructed in Inkscape, scaling and translating it can be non-trivial.


The example code below keeps the object's bottom edge fixed while scaling its height based on the fill level. This means that only the y-dimension is scaled. Since scaling also shifts the y-position, the original position and height must be retrieved first. There may also be an additional scaling factor in the SVG, which is why the updateVolume function first compares the viewBox parameters. Then, the position and dimensions are extracted from the transformation matrix defined in the XML.

<!DOCTYPE html>
<html class="mcss">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="midas.css">
    <script src="controls.js"></script>
    <script src="midas.js"></script>
    <script src="mhttpd.js"></script>
    <title>Helium example</title>

    <style>
        .modbbutton {font-size:20px;}
	.text {font-size:20px;}
    </style>

    <script>


        // Valve Coloring  --------------------------------------------------------------

	function updateValveColor(flag, valveID) {
	    console.log(valveID, flag);
       	    const svgDoc = document.getElementById('svgObject').contentDocument;
       	    //The valveID needs to match the ID given in the svg file
       	    const obj_Valve = svgDoc.getElementById(valveID); 

  	    obj_Valve.style.fill = (flag === 1) ? "#00FF00" : "#FF0000";
       	}

        // Confirmation routines --------------------------------------------------------

        function confirmValve(arg, valveAddress) {
	    const txt = arg === "Close" ? "Are you sure to close the valve?" : "Are you sure to open the valve?";
            const val = arg === "Open" ? 1 : 0;
            dlgConfirm(txt, function(confirmed) {
                if (confirmed) {
                    modbset(valveAddress, val);
                }
            });
            return false; // Prevent ODB value change immediately
        }

	// Animation -------------------------------------------------------------------
        
	/**
	 * Update the volume of an SVG element by applying a transformation based on input.
	 * @param {number} elem - The scale factor to modify the volume.
	 * @param {string} svgID - The ID of the SVG element to be transformed.
	 * Comment: This example specifically transform only the ySize of the object,
	 * 	    with the special case that the bottom edge stays in place
	 */
	function updateVolume(elem, svgID) {
	    const svgDoc = document.getElementById('svgObject').contentDocument;
	    const rootSvg = svgDoc.documentElement;

	    // Get the width and height of the root SVG element.
	    const widthAttr = rootSvg.getAttribute("width");
	    const heightAttr = rootSvg.getAttribute("height");

	    // Get and parse the viewBox attribute.
	    const viewBoxAttr = rootSvg.getAttribute("viewBox");                
	    const viewBoxParts = viewBoxAttr.split(" ").map(parseFloat);
	    const viewBoxWidth = viewBoxParts[2];
	    const viewBoxHeight = viewBoxParts[3];

	    // Calculate the scaling factors for x and y axes based on the viewBox and actual width/height.
	    const xScale = widthAttr / viewBoxWidth;
	    const yScale = heightAttr / viewBoxHeight;

	    // Get the element to modify based on the provided svgID.
	    const obj_Vol = svgDoc.getElementById(svgID); 
	    const bbox = obj_Vol.getBBox();

	    // Extract the transformation matrix from the element's transform attribute.
	    const transformAttr = obj_Vol.getAttribute("transform");
	    const matrixMatch = transformAttr.match(/matrix\(([^)]+)\)/);

	    let transformedY, transformedHeight;

	    // Apply matrix transformation if it exists.
	    if (matrixMatch) {
	        const [a, b, c, d, e, f] = matrixMatch[1].split(",").map(parseFloat);

	        // Apply matrix to the top-left corner of the bbox to get transformed Y position and height.
	        transformedY = d * bbox.y + f;
	        transformedHeight = d * bbox.height;
	    }

	    // Scale factor is 0.01 times the provided 'elem' value.
	    const scale = 0.01 * elem;

	    // Calculate the new Y position after applying scaling.
	    const yPosInit = (transformedY / yScale + transformedHeight / yScale) * (1 - scale);

	    // Update the element's transform attribute with the new translation and scaling.
	    // In this example only the height (y-axis) is changed
	    updateTransform(obj_Vol, `0,${yPosInit}`, `1,${scale}`);
	}

	/**
	 * Updates the transform attribute of an SVG element by applying new translation and scale values.
	 * @param {Element} elem - The SVG element whose transform attribute is to be updated.
	 * @param {string} newTranslate - The new translation value as a string (e.g., "0,100").
	 * @param {string} newScale - The new scale value as a string (e.g., "1,0.5").
	 */
	function updateTransform(elem, newTranslate, newScale) {
	    let transform = elem.getAttribute("transform") || "";

	    // Regular expressions to match existing translate and scale transformations.
	    const translateRegex = /translate\(([^)]+)\)/;
	    const scaleRegex = /scale\(([^)]+)\)/;

	    // Remove any existing translate and scale transformations from the transform string.
	    transform = transform
	        .replace(translateRegex, '')
	        .replace(scaleRegex, '')
	        .trim();

	    // Construct the new transform string with updated translation and scale.
	    const newTransform = `translate(${newTranslate}) scale(${newScale}) ${transform}`.trim();

	    // Apply the new transform to the element.
	    elem.setAttribute("transform", newTransform);
	}


    </script>

</head>

<body class="mcss" onload="mhttpd_init('Example');">

<!-- header and side navigation will be filled in mhttpd_init -->
<div id="mheader"></div>
<div id="msidenav"></div>

<div id="mmain">

    <!-- modb values to listen for status changes -->	
    <div id="filling_valve" class="modb" data-odb-path="/Equipment/Example/Filling Valve" onchange="updateValveColor(this.value,'Filling_valve');" onload="updateValveColor(this.value,'Filling_valve');">
    <div id="exhaust_valve" class="modb" data-odb-path="/Equipment/Example/Exhaust Valve" onchange="updateValveColor(this.value,'Exhaust_valve');" onload="updateValveColor(this.value,'Exhaust_valve');">

    <table class="mtable">
        <tr><th class="mtableheader">Helium example</th></tr>
        <tr><td>
            <div style="position:relative;width:600px;margin:auto">
            <!-- Import of a single SVG file, colors and sizes are changed directly for each object --> 
            <object id="svgObject" type="image/svg+xml" data="Midas_helium_example.svg"></object>
	    
	    <!-- Control and monitoring instances -->

            <!-- Filling line -->

            <button class="modbbutton" class="mbutton" data-odb-path="/Equipment/Example/Filling Valve"
                    style="position:absolute; top: 473px;left:125px;"
                    data-validate='confirmValve("Open","/Equipment/Example/Filling Valve")'
                    data-odb-value="1">Open</button>
            <button class="modbbutton" class="mbutton" data-odb-path="/Equipment/Example/Filling Valve" 
                    style="position:absolute; top: 473px;left:195px;"
                    data-validate='confirmValve("Close","/Equipment/Example/Filling Valve")'
                    data-odb-value="0">Close</button>
            <div class="text" style="position:absolute; top: 443px; left:125px; font-weight: bold;">Filling valve:</div>
	   
            <!-- Exhaust line -->

            <button class="modbbutton" class="mbutton" data-odb-path="/Equipment/Example/Exhaust Valve"
                    style="position:absolute; top: 300px;left:352px;"
                    data-validate='confirmValve("Open","/Equipment/Example/Exhaust Valve")'
                    data-odb-value="1">Open</button>
            <button class="modbbutton" class="mbutton" data-odb-path="/Equipment/Example/Exhaust Valve" 
                    style="position:absolute; top: 300px;left:422px;"
                    data-validate='confirmValve("Close","/Equipment/Example/Exhaust Valve")'
                    data-odb-value="0">Close</button>
            <div class="text" style="position:absolute; top: 270px; left:352px; font-weight: bold;">Exhaust valve:</div>

            <!-- Expansion Volume -->
            
	    <div class="modbvbar" data-odb-path="/Equipment/Example/Expansion volume"
                 style="width:20px;height:200px; color:grey; position:absolute; top:56px;left:320px"
                 data-min-value="0" data-max-value="100" data-log="0"
                 onchange="updateVolume(this.value, 'Expansion_volume')"></div>
            <div class="mvaxis" style="width:30px;height:200px; position:absolute; top:56px;left:340px; text-align:left"
                 data-min-value="0" data-max-value="100" data-log="0"></div>


        </td></tr>

    </table>
	 


</div>

</body>
</html>

SVG XML code

This is the XML code of the SVG example used in the custom page. It is important to set the object IDs correctly, as they are used to access and manipulate elements via JavaScript.


<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->

<svg
   width="600"
   height="750"
   viewBox="0 0 600 750.00002"
   version="1.1"
   id="svg1"
   inkscape:version="1.4.1 (unknown)"
   sodipodi:docname="Midas_helium_example.svg"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:svg="http://www.w3.org/2000/svg">
  <sodipodi:namedview
     id="namedview1"
     pagecolor="#ffffff"
     bordercolor="#000000"
     borderopacity="0.25"
     inkscape:showpageshadow="2"
     inkscape:pageopacity="0.0"
     inkscape:pagecheckerboard="0"
     inkscape:deskcolor="#d1d1d1"
     inkscape:document-units="px"
     inkscape:zoom="2.0661644"
     inkscape:cx="423.974"
     inkscape:cy="308.30073"
     inkscape:window-width="1920"
     inkscape:window-height="1009"
     inkscape:window-x="0"
     inkscape:window-y="32"
     inkscape:window-maximized="1"
     inkscape:current-layer="layer1" />
  <defs
     id="defs1">
    <marker
       style="overflow:visible"
       id="marker2"
       refX="0"
       refY="0"
       orient="auto-start-reverse"
       inkscape:stockid="Triangle arrow"
       markerWidth="1"
       markerHeight="1"
       viewBox="0 0 1 1"
       inkscape:isstock="true"
       inkscape:collect="always"
       preserveAspectRatio="xMidYMid">
      <path
         transform="scale(0.5)"
         style="fill:context-stroke;fill-rule:evenodd;stroke:context-stroke;stroke-width:1pt"
         d="M 5.77,0 -2.88,5 V -5 Z"
         id="path2" />
    </marker>
  </defs>
  <g
     inkscape:label="Gas system"
     inkscape:groupmode="layer"
     id="layer1">
    <g
       id="Gas_cabinet"
       inkscape:label="Gas_cabinet"
       style="display:inline"
       transform="matrix(4.056096,0,0,4.056096,-726.2293,-806.75454)">
      <rect
         style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:0.264635;stroke-miterlimit:3.2;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
         id="Gas_cabinet_outline"
         width="45.862354"
         height="58.905613"
         x="267.93396"
         y="308.49619"
         rx="0"
         ry="0"
         inkscape:label="Gas_cabinet_outline" />
      <g
         id="Gas_bottle"
         transform="matrix(3.6447445,0,0,3.4512234,112.28368,-106.71016)"
         style="display:inline;mix-blend-mode:normal;fill:#ffffff;stroke-width:0.118799"
         inkscape:label="Gas_bottle">
        <path
           style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.0746005px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
           d="m 45.466186,125.93812 v -1.31329 c 0,0 0.07542,-0.46399 0.475282,-0.46399 0.43403,0 0.541916,0.46399 0.541916,0.46399 v 1.31329"
           id="path11882"
           sodipodi:nodetypes="ccscc" />
        <path
           style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.0746005px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
           d="m 44.681364,126.73032 c 0,0 0.09055,-0.94582 1.313641,-0.94582 1.120378,0 1.273201,0.94582 1.273201,0.94582 v 9.66595 h -2.586842 z"
           id="path11880"
           sodipodi:nodetypes="cscccc" />
      </g>
      <text
         xml:space="preserve"
         style="font-weight:bold;font-size:13.3333px;font-family:Laksaman;-inkscape-font-specification:'Laksaman Bold';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;display:inline;fill:#00ffff;fill-rule:evenodd;stroke-width:3.77953"
         x="179.48"
         y="151.93512"
         id="Gas_cabinet_title"
         transform="matrix(0.49308498,0,0,0.49308498,179.04638,229.4703)"
         inkscape:label="Gas_cabinet_title"><tspan
           sodipodi:role="line"
           style="font-size:13.3333px;fill:#000000;fill-opacity:1;stroke-width:3.77953"
           x="179.48"
           y="151.93512"
           id="tspan96-8-4-0">Gas cabinet</tspan></text>
    </g>
    <path
       style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2"
       d="M 408.62454,498.40226 V 468.28588 H 288.15902 v 64.98024 H 93.771462 V 255.7319"
       id="Filling_line"
       inkscape:label="Filling_line"
       sodipodi:nodetypes="cccccc" />
    <path
       style="display:inline;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.53449;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
       d="m 220.44846,552.04894 v -37.8841 l -58.46319,37.86918 v -37.8843 z"
       id="Filling_valve"
       sodipodi:nodetypes="ccccc"
       inkscape:label="Filling_valve" />
    <text
       xml:space="preserve"
       style="font-weight:bold;font-size:26.6666px;font-family:Laksaman;-inkscape-font-specification:'Laksaman Bold';text-align:start;writing-mode:lr-tb;direction:ltr;text-anchor:start;display:inline;fill:#00ffff;fill-rule:evenodd;stroke-width:7.55906"
       x="26.068356"
       y="43.542694"
       id="Expansion_volume_title"
       inkscape:label="Expansion_volume_title"><tspan
         sodipodi:role="line"
         style="font-size:26.6666px;fill:#000000;fill-opacity:1;stroke-width:7.55906"
         x="26.068356"
         y="43.542694"
         id="tspan96-8-8">Expansion volume</tspan></text>
    <path
       style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:2;stroke-dasharray:none;marker-end:url(#marker2)"
       d="m 251.67406,254.578 v 104.14268 h 97.7657 181.89378 V 251.47464"
       id="Exhaust_line"
       inkscape:label="Exhaust_line"
       sodipodi:nodetypes="ccccc" />
    <g
       id="Expansion_volume"
       inkscape:label="Expansion_volume"
       style="display:inline"
       transform="matrix(7.5560886,0,0,7.5560886,-161.95634,-143.19437)">
      <path
         style="mix-blend-mode:normal;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.264583;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
         d="m 25.181199,26.590622 h 37.055527 l -2.746143,6.664982 2.885185,6.396771 -2.746137,6.65182 2.919945,6.490565 H 24.903107 l 2.798286,-6.490565 -2.937328,-6.813076 2.885185,-6.235515 z"
         id="path4823"
         sodipodi:nodetypes="ccccccccccc" />
      <path
         style="mix-blend-mode:normal;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.264581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.264581, 0.264581;stroke-dashoffset:0;stroke-opacity:1"
         d="M 27.691557,33.255604 H 59.644908"
         id="path3275"
         sodipodi:nodetypes="cc" />
      <path
         style="mix-blend-mode:normal;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.264581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.264581, 0.264581;stroke-dashoffset:0;stroke-opacity:1"
         d="M 24.974876,39.556896 H 62.361591"
         id="path3277"
         sodipodi:nodetypes="cc" />
      <path
         style="mix-blend-mode:normal;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.264581;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:0.264581, 0.264581;stroke-dashoffset:0;stroke-opacity:1"
         d="m 27.950288,46.304195 h 31.69462"
         id="path3279"
         sodipodi:nodetypes="cc" />
    </g>
    <path
       style="display:inline;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.53449;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
       d="m 449.20856,377.30494 v -37.8841 l -58.4632,37.86918 v -37.8843 z"
       id="Exhaust_valve"
       sodipodi:nodetypes="ccccc"
       inkscape:label="Exhaust_valve" />
  </g>
</svg>