"""Small XML writer for CI Server R1.03 display files (Edit Module format 1.3.0.0). Positions are given as boxes (left, top, width, height) in display pixels; CI Server stores a component as its centre (x, y) plus half sizes (top, bottom, left, right). Element and property names follow the installed component definitions (%TLS_WAP_PATH%\\cfg\\operatorInterfaces\\DEPLOY\\components\\*.xml) and factory displays. """ from xml.sax.saxutils import escape # ---------------------------------------------------------------- theme (dark dashboard) C = { 'bg': (13, 17, 23), 'panel': (22, 27, 34), 'card': (28, 35, 45), 'card2': (36, 45, 57), 'line': (48, 61, 77), 'text': (230, 237, 243), 'muted': (139, 152, 168), 'dim': (88, 100, 116), 'blue': (59, 130, 246), 'teal': (20, 184, 166), 'green': (34, 197, 94), 'amber': (245, 158, 11), 'red': (239, 68, 68), 'violet': (139, 92, 246), 'off': (71, 85, 105), 'white': (255, 255, 255), 'track': (40, 50, 64), } FONT_UI = 'Segoe UI' FONT_NUM = 'Consolas' def rgb(c): c = C.get(c, c) if isinstance(c, str) else c return 'r="%d" g="%d" b="%d"' % tuple(c) def colorset(tag, c): return '<%s type="colorSet" %s/>' % (tag, rgb(c)) def paint(tag, c): return '<%s type="paint">' % (tag, rgb(c), tag) def font(size, bold=False, name=FONT_UI, tag='font'): b = ' bold="true"' if bold else '' return '<%s type="font" name="%s"%s size="%d" underline="false" strikethrough="false"/>' % (tag, name, b, size) def esc(s): return escape(str(s)) def box_attrs(l, t, w, h): return 'x="%.1f" y="%.1f" top="%.1f" bottom="%.1f" left="%.1f" right="%.1f" rotation="0.0" shear="0.0"' % ( l + w / 2.0, t + h / 2.0, h / 2.0, h / 2.0, w / 2.0, w / 2.0) def box_elems(l, t, w, h): return ('%.1f%.1f%.1f%.1f%.1f%.1f' '0.00.0') % (l + w / 2.0, t + h / 2.0, h / 2.0, h / 2.0, w / 2.0, w / 2.0) _FONTS = {} def text_width(s, size, bold=False, name=FONT_UI): """Width in display pixels, measured with the Windows font (matches the HTML export within ~3 %).""" try: from PIL import ImageFont fn = {(FONT_UI, False): 'segoeui.ttf', (FONT_UI, True): 'segoeuib.ttf', (FONT_NUM, False): 'consola.ttf', (FONT_NUM, True): 'consolab.ttf'}[(name, bool(bold))] key = (fn, size) if key not in _FONTS: _FONTS[key] = ImageFont.truetype(r'C:\Windows\Fonts\%s' % fn, size) return _FONTS[key].getlength(s) * 0.975 except Exception: return len(s) * size * 0.55 def fit(l, w, s, size, align, bold=False, name=FONT_UI): """Text and numbers are exported with keepOriginalSize: drawn at natural size, centred on the box. Shrink the box to the measured text width so 'Left' / 'Right' alignment still lands right.""" est = min(w, max(4.0, text_width(s, size, bold, name))) if align == 'Left': return l, est if align == 'Right': return l + w - est, est return l, w # ---------------------------------------------------------------- data connections def tr_linear(i0, i1, o0, o1): return ('java.lang.Double' 'java.lang.Double%s' '%s%s' '%s') % (float(i0), float(i1), float(o0), float(o1)) def tr_threshold(table, inp='Double', out='Paint'): """table: list of (input, output). Output for Paint = colour key/tuple, Boolean = bool.""" itype = {'Double': 'java.lang.Double', 'Boolean': 'java.lang.Boolean'}[inp] otype = {'Paint': 'fasttools.jwui.common.property.paint.PaintSet', 'Boolean': 'java.lang.Boolean', 'Color': 'fasttools.jtools.common.property.color.ColorSet'}[out] rows = [] for i, o in table: iv = ('true' if i else 'false') if inp == 'Boolean' else repr(float(i)) if out == 'Paint': ov = '' % rgb(o) elif out == 'Color': ov = '' % rgb(o) else: ov = '%s' % ('true' if o else 'false') rows.append('%s%s' % (iv, ov)) return ('%s%s' '%s') % (itype, otype, ''.join(rows)) def conn(prop, item, attr='ItemValue', direction=1, transform=''): """Connect component property (e.g. 'number.value') to an item attribute. direction 1 = read, 2 = write, None = read and write (as the factory toggle buttons).""" d = '' if direction is None else '%d' % direction return ('' '0.0%s%s' '%s%s') % ( prop, transform, d, esc(item), attr) def act_set(item, value): return ('' '%s2' '%sItemValue' '') % (float(value), esc(item)) def act_open(display, layout=None, frame=None, event='On action'): lay = '' if layout or frame: lay = ('%s' '%s%s') % (display, layout or '', frame or '') return '%s' % (event, lay) return ('%s' '') % (event, display) def act_close(event='On action'): return '' % event def act_cmd(command, server=True, event='On action'): return ('%s' '%s') % (event, esc(command), 'true' if server else 'false') def data(actions): return '%s' % ''.join(actions) if actions else '' # ---------------------------------------------------------------- display builder class Display: def __init__(self, name, width, height, title, bg='bg', design_h=None): self.name, self.w, self.h, self.title, self.bg = name, width, height, title, bg # Coordinates are written for a display design_h high and stretched vertically to height: # panels, bars and lists grow, text / controls / round shapes keep their size and only move. self.sy = float(height) / design_h if design_h else 1.0 self.parts = [] self.hid = 6 # 1 = layer, 2..6 = visibility groups def _b(self, l, t, w, h, stretch): if self.sy == 1.0: return l, t, w, h if stretch: return l, t * self.sy, w, h * self.sy return l, (t + h / 2.0) * self.sy - h / 2.0, w, h def _id(self): self.hid += 1 return '%d' % self.hid def add(self, s): self.parts.append(s) # ----- shapes def rect(self, l, t, w, h, fill='card', stroke=None, sw=1.0, r=0, actions=(), tip=None, fill_amount=None, fill_dir=None, visible=True): l, t, w, h = self._b(l, t, w, h, h > 40) s = '' + self._id() if tip: s += '%s' % esc(tip) if r: # stored as a fraction (0..1) of the half width / half height (web client # ExtendedRectangle: rx = width / 2 * roundingX); pixel values > 1 draw an ellipse s += '%.4f%.4f' % (min(1.0, 2.0 * r / w), min(1.0, 2.0 * r / h)) s += box_elems(l, t, w, h) s += '' % sw if stroke: s += 'true' + paint('strokePaint', stroke) else: s += 'false' if fill: s += 'true' + paint('fillPaint', fill) else: s += 'false' if fill_amount is not None: s += '%s' % fill_amount if fill_dir: s += '' % fill_dir if not visible: s += 'false' s += data(actions) + '' self.add(s) def ellipse(self, l, t, w, h, fill='green', stroke=None, sw=1.0, actions=(), tip=None): l, t, w, h = self._b(l, t, w, h, False) s = '' + self._id() if tip: s += '%s' % esc(tip) s += box_elems(l, t, w, h) + '' % sw s += ('true' + paint('strokePaint', stroke)) if stroke else 'false' s += ('true' + paint('fillPaint', fill)) if fill else 'false' s += data(actions) + '' self.add(s) def arc(self, cx, cy, rad, start, extent, color='blue', sw=10.0, actions=()): cy = cy * self.sy s = '' + self._id() s += '%.1f%.1ffalse' % (start, extent) s += box_elems(cx - rad, cy - rad, 2 * rad, 2 * rad) s += 'true' % sw s += paint('strokePaint', color) + 'false' s += data(actions) + '' self.add(s) def line(self, x1, y1, x2, y2, color='line', sw=2.0, actions=()): # a line is stored by its bounding box; direction by the sign of the extents is not available, # so only horizontal / vertical lines are used here l, t = min(x1, x2), min(y1, y2) w, h = abs(x2 - x1), abs(y2 - y1) # drawn as a thin filled rectangle: a without curve points is placed wrongly on the web if h < w: self.rect(l, t - sw / 2.0, max(w, 1), sw, fill=color, actions=actions) else: self.rect(l - sw / 2.0, t, sw, max(h, 1), fill=color, actions=actions) # ----- text and numbers def text(self, l, t, w, h, value, size=12, color='text', bold=False, align='Left', actions=(), name=FONT_UI, bgfill=None): l, t, w, h = self._b(l, t, w, h, False) s = '' + self._id() + '%s' % esc(value) s += font(size, bold, name) + paint('fillPaint', color) l, w = fit(l, w, str(value), size, align, bold, name) s += '' if bgfill: s += 'true' + paint('backgroundPaint', bgfill) s += 'true' s += box_elems(l, t, w, h) + data(actions) + '' self.add(s) def number(self, l, t, w, h, item, fmt='999.9', size=22, color='text', bold=True, align='Right', attr='ItemValue', extra=()): l, t, w, h = self._b(l, t, w, h, False) s = '' + self._id() + '0.0%s' % fmt s += font(size, bold, FONT_NUM) + paint('fillPaint', color) l, w = fit(l, w, fmt.replace('9', '0'), size, align, bold, FONT_NUM) s += '' s += 'true' s += box_elems(l, t, w, h) + data([conn('number.value', item, attr)] + list(extra)) + '' self.add(s) def number_field(self, l, t, w, h, item, fmt='9999', size=14, editable=True, attr='ItemValue', as_datetime=False): l, t, w, h = self._b(l, t, w, h, False) s = '' % box_attrs(l, t, w, h) + self._id() s += font(size, True, FONT_NUM) + '%s' % fmt s += '%s' % ('true' if editable else 'false') if as_datetime: s += 'true' s += colorset('foregroundColor', 'text') + colorset('backgroundColor', 'card2') s += '' s += data([conn('numberField.value', item, attr, direction=None if editable else 1)]) + '' self.add(s) def text_field(self, l, t, w, h, item, attr='ItemDescription', size=12): l, t, w, h = self._b(l, t, w, h, False) s = '' % box_attrs(l, t, w, h) + self._id() s += font(size) + 'false' s += colorset('foregroundColor', 'text') + colorset('backgroundColor', 'card2') s += data([conn('textField.value', item, attr)]) + '' self.add(s) def time_text(self, l, t, w, h, item, size=12, color='text', align='Left'): """Last update time of an item: number field with asDateTime (epoch seconds -> local date and time).\n The web dateTimeField only shows a value when editable (white input boxes).""" self.number_field(l, t, w, h, item, '', size, editable=False, attr='ItemUpdTime', as_datetime=True) def datetime_field(self, l, t, w, h, item, attr='ItemUpdTime', size=12, editable=True): l, t, w, h = self._b(l, t, w, h, False) s = '' % box_attrs(l, t, w, h) + self._id() s += font(size) + colorset('foregroundColor', 'text') s += '%strue' % ('true' if editable else 'false') s += colorset('backgroundColor', 'card2') s += data([conn('dateTimeField.value', item, attr)]) + '' self.add(s) # ----- controls def button(self, l, t, w, h, label, actions, fg='text', bg='card2', size=12, bold=True, tip=None, extra=()): l, t, w, h = self._b(l, t, w, h, False) s = '' self.add(s) def toggle(self, l, t, w, h, label, item, size=12): l, t, w, h = self._b(l, t, w, h, False) s = '' % box_attrs(l, t, w, h) + self._id() s += '' % esc(label) + font(size, True) s += colorset('foregroundColor', 'text') + colorset('backgroundColor', 'card2') s += data([conn('toggleButton.value', item, direction=None)]) + '' self.add(s) def checkbox(self, l, t, w, h, label, item, size=12): l, t, w, h = self._b(l, t, w, h, False) s = '' % box_attrs(l, t, w, h) + self._id() s += '' % esc(label) + font(size) s += 'false' + colorset('foregroundColor', 'text') + colorset('backgroundColor', 'card') s += data([conn('checkBox.value', item, direction=None)]) + '' self.add(s) def slider(self, l, t, w, h, item, mn, mx, major, minor=None, size=10): l, t, w, h = self._b(l, t, w, h, False) s = '' % box_attrs(l, t, w, h) + self._id() s += '%s%s%s' % (float(mx), float(mn), float(mn)) s += '%s%s' % ( float(major), float(minor or major)) s += 'truetruefalse' s += font(size) + 'false' + colorset('foregroundColor', 'muted') s += data([conn('slider.value', item, direction=None)]) + '' self.add(s) def spinner(self, l, t, w, h, item, mn, mx, step=1, size=14): l, t, w, h = self._b(l, t, w, h, False) s = '' % box_attrs(l, t, w, h) + self._id() s += 'true%s%s%s' % ( float(step), float(mn), float(mx)) s += font(size, True, FONT_NUM) + colorset('foregroundColor', 'text') + colorset('backgroundColor', 'card2') s += 'false' s += data([conn('spinner.value', item, direction=None)]) + '' self.add(s) def combo(self, l, t, w, h, item, options, size=12): l, t, w, h = self._b(l, t, w, h, False) """options: 'value;Text;value;Text' (web client format).""" s = '' % box_attrs(l, t, w, h) + self._id() s += '' % esc(options) + font(size) + colorset('foregroundColor', 'text') s += data([conn('comboBox.value', item, direction=None)]) + '' self.add(s) # ----- indicators def databar(self, l, t, w, h, item, lo=0, hi=100, fg='blue', bg='track', direction='Up', border=None): l, t, w, h = self._b(l, t, w, h, h > w) s = '' % box_attrs(l, t, w, h) + self._id() s += 'true' + paint('background', bg) + paint('foreground', fg) s += paint('borderColor', border or bg) + paint('foregroundBorderColor', fg) s += '%s%s' % (float(lo), float(hi)) s += '' % direction s += data([conn('dataBar.value', item)]) + '' self.add(s) def rect_scale(self, l, t, w, h, mn, mx, major, minor, vertical=True, fmt='999', size=10): l, t, w, h = self._b(l, t, w, h, h > w) s = '' % box_attrs(l, t, w, h) + self._id() s += '%s%s%s' % ( float(mx), float(mn), float(major)) s += '%s%s%s' % ( float(minor), fmt, 'true' if vertical else 'false') s += colorset('color', 'muted') + font(size) + '' self.add(s) def circ_scale(self, cx, cy, rad, mn, mx, major, minor, start=225, extent=-270, fmt='999', size=9): cy = cy * self.sy s = '' % box_attrs(cx - rad, cy - rad, 2 * rad, 2 * rad) + self._id() s += '%s%s%s' % ( float(mx), float(mn), float(major)) s += '%s%s' % (float(minor), fmt) s += '%s%s' % (float(start), float(extent)) s += colorset('color', 'muted') + font(size) + '' self.add(s) def ring_gauge(self, cx, cy, rad, item, mn, mx, color='teal', sw=12.0, thresholds=None): """270 degree ring with the gap on the right; the value arc grows from 9 o'clock up and down. Arc angles run clockwise from 3 o'clock on both clients (tested 2026-10-06, fixed and connected angles): track 45..315 degrees, value arc centred on 180 degrees (start = 180 - extent / 2).""" self.arc(cx, cy, rad, 45, 270, 'track', sw) acts = [conn('arc.extentAngle', item, transform=tr_linear(mn, mx, 0.5, 270)), conn('arc.startAngle', item, transform=tr_linear(mn, mx, 179.75, 45))] if thresholds: acts.append(conn('arc.strokePaint', item, transform=tr_threshold(thresholds))) self.arc(cx, cy, rad, 179.75, 0.5, color, sw, acts) def lamp(self, l, t, d, item, on='green', off='off', inp='Boolean', table=None): tbl = table or [(False, off), (True, on)] self.ellipse(l, t, d, d, fill=off, actions=[conn('ellipse.fillPaint', item, transform=tr_threshold(tbl, inp))]) # ----- complex components def alarm_overview(self, l, t, w, h, overview='CurrentAlarms', asa=None, size=12, banner=False): l, t, w, h = self._b(l, t, w, h, True) s = '' % box_attrs(l, t, w, h) + self._id() s += 'false' s += '' % overview # dark rows: background, unacknowledged / acknowledged row colour per priority, text colours s += 'true' + colorset('backgroundColor', 'panel') for c, col in (('Alarm', (248, 113, 113)), ('Normal', 'text'), ('Ackn', 'muted'), ('Info', 'muted'), ('Diag', 'amber'), ('Deactive', 'muted'), ('Recover', 'green')): s += colorset('eventType%sAckColor' % c, col) + colorset('eventType%sNackColor' % c, col) for p in range(0, 16): s += ('priorityNackColor[%02d]' 'priorityAckColor[%02d]' ) % (p, rgb((64, 24, 30) if p <= 3 else (36, 45, 57)), p, rgb('card')) s += font(size) + font(size, False, FONT_UI, 'alarmLineFont') + font(size - 1, True, FONT_UI, 'buttonFont') # explicit columns: order and widths spread over the whole component width (autoFit off) cols = [('AlarmTime', 200), ('Priority', 70), ('Section', 300), ('Tag', 170), ('ItemDesciption', 320), ('Text', 300), ('AlarmType', 120), ('Value', 110), ('Acknowledged', 110)] scale = (w - 24.0) / sum(c[1] for c in cols) for i, (c, cw) in enumerate(cols): s += 'true%d' % (c, c, c, i + 1, c) s += '%d' % (c, int(cw * scale), c) hide = ['CEventActor', 'CEventCat', 'CEventCondition', 'CEventSource', 'CEventSubCond', 'EventActor', 'EventCat', 'EventCondition', 'EventSource', 'EventSubCond', 'Installation', 'ItemName', 'OriginalTime', 'Quality', 'Unit', 'User', 'Sub', 'EngUnits', 'Location', 'InstallationDescription', 'UnitDescription', 'SectionDescription', 'AlarmCollection', 'EventID', 'ConditionID', 'BranchID', 'PriorityText'] # AlarmAttribute is an array property for c in hide: s += 'false' % (c, c) if banner: s += ('falsefalsefalse' 'falsefalse' 'false') else: # autoFit in a tab that is hidden while loading squeezes all columns to nothing (web) s += 'false' s += '1-12-2' s += '3-30-0,4-15' if asa: s += ('' '%s' '') % asa s += '' self.add(s) def report(self, l, t, w, h, report=None): l, t, w, h = self._b(l, t, w, h, True) s = '' % box_attrs(l, t, w, h) + self._id() if report: s += '%s' % esc(report) s += '' self.add(s) def micro_trend(self, l, t, w, h, pens, time_range=600, history=True): l, t, w, h = self._b(l, t, w, h, True) """pens: list of (item, min, max, colour).""" s = '' % box_attrs(l, t, w, h) + self._id() s += '%dtrue%s' % (len(pens), 'true' if history else 'false') s += 'truefalse' s += colorset('microTrendBackgroundFillColor', 'panel') + colorset('microTrendBackgroundColor', 'line') acts = [''] for i, (item, mn, mx, col) in enumerate(pens): s += ('itemName[%d]' 'scaleMin[%d]%s' 'scaleMax[%d]%s' 'color[%d]' 'stroke[%d]' ) % (i, i, float(mn), i, float(mx), i, rgb(col), i) acts.append(conn('microTrend.itemName[%d]' % i, item, 'ItemName')) s += data(acts) + '' self.add(s) def trend(self, l, t, w, h, pens, minutes=10, title='', web_spares=True): l, t, w, h = self._b(l, t, w, h, True) """pens: list of (item, colour, min, max, description). Padded to 50 visible pens (ISS-041).""" def pen(item, col, mn, mx, desc, spare=0): if spare: col_x = colorset('color', 'panel') stroke, legend, vis_leg, rng, mn_s, mx_s, typ = '0.0', ' ' * spare, 'false', 1, '1.0E30', '2.0E30', 1 desc = '' else: col_x = colorset('color', col) stroke, legend, vis_leg, rng, mn_s, mx_s, typ = '2.0', '', 'true', 1, repr(float(mn)), repr(float(mx)), 0 return ('%d%s@NULL' 'true%s' '%s0' 'truetruetrue' '@NULL@NULL%d' '%s%sfalse' '<description>%s</description><legendText>%s</legendText><titlePlacement>0</titlePlacement>' '<titleRotation>0</titleRotation><labelRotation>0</labelRotation><majorGridVisible>false</majorGridVisible>' '<minorGridVisible>false</minorGridVisible><lowHighLimitGridVisible>false</lowHighLimitGridVisible>' '<lowHighLimitGridAreaVisible>false</lowHighLimitGridAreaVisible>' '<lowLowHighHighLimitGridVisible>false</lowLowHighHighLimitGridVisible><majorGridColor>@NULL</majorGridColor>' '<minorGridColor>@NULL</minorGridColor><lowHighLimitGridColor>@NULL</lowHighLimitGridColor>' '<lowHighLimitGridAreaColor>@NULL</lowHighLimitGridAreaColor>' '<lowLowHighHighLimiGridColor>@NULL</lowLowHighHighLimiGridColor>' '<majorGridStroke type="stroke" width="0.1"/><minorGridStroke type="stroke" width="0.1"/>' '<lowHighLimitGridStroke type="stroke" width="0.1"/><lowLowHighHighLimiGridStroke type="stroke" width="0.1"/>' '<annotationVisible>false</annotationVisible><timeShift>0</timeShift></pen>') % ( typ, col_x, stroke, vis_leg, esc(item), 'true' if (not spare and pens and item == pens[0][0]) else 'false', rng, mn_s, mx_s, esc(desc), legend) body = ''.join(pen(*p) for p in pens) if web_spares: # the web trend draws only with 50 visible pens (ISS-041); the desktop hairline then lists 50 values body += ''.join(pen(pens[0][0], None, 0, 0, '', i) for i in range(1, 51 - len(pens))) s = ('<chart type="componentData">' + self._id() + '<config type="graphicInfo"><version type="version" value="1.3.0.0"/>' '<backgroundBorder type="border" value="BorderEmpty"/><penPanelVisible>false</penPanelVisible>' '<penPanelHeight>-1</penPanelHeight><toolbarVisible>true</toolbarVisible>' '<toolbarPosition type="borderLayoutPosition" value="PositionNorth"/><shiftOffset>0</shiftOffset>' '<shiftLength>0</shiftLength><enterpriseGroupLocation type="nodeName" node="$display$"/>' '<chartPanel><name/><author/><version type="version" value="1.3.0.0"/><description/>' '<forAllUsers>false</forAllUsers><template>false</template><group>@NULL</group><XyMode>false</XyMode>' '<x>0</x><y>0</y><width>%d</width><heigth>%d</heigth><iconized>false</iconized>' '<backgroundBorder type="border" value="BorderEmpty"/>%s' '<backgroundPaint type="paint"><paint type="colorSet" %s/></backgroundPaint>' '<plotAreaBorder type="border" value="BorderEmpty"/><plotAreaTransparent>false</plotAreaTransparent>' '<plotAreaIcon>@NULL</plotAreaIcon><plotAreaPaint type="paint"><paint type="colorSet" %s/></plotAreaPaint>' '%s<hairlineVisible>false</hairlineVisible><antialiasingText>true</antialiasingText>' '<antialiasingData>true</antialiasingData><renderMode type="chartRendering" value="Rendering2d"/>' '<timeNavigatorVisible>true</timeNavigatorVisible><timeRangeControlVisible>true</timeRangeControlVisible>' '<autoUpdateInterval>false</autoUpdateInterval><legendVisible>true</legendVisible><legendTitle/>' '<legendHeading/>%s%s%s<legendTransparancy>0.0</legendTransparancy>' '<legendBorderType type="border" value="BorderEmpty"/><legendPositionType type="position" value="PositionNorthEast"/>' '<timeSliderVisible>true</timeSliderVisible><timeSliderAbove>false</timeSliderAbove>' '<valueSliderVisible>false</valueSliderVisible><valueSliderLeft>false</valueSliderLeft>' '<xAxisReversed>false</xAxisReversed><yAxisReversed>false</yAxisReversed><projectorReversed>false</projectorReversed>' '<scalingFonts>false</scalingFonts><maximumPens>50</maximumPens><pensPerPack>0</pensPerPack>' '<triggerPen>0</triggerPen><timeAxisVisible>true</timeAxisVisible><timeAxisMajorTickVisible>true</timeAxisMajorTickVisible>' '<timeAxisMinorTickVisible>false</timeAxisMinorTickVisible>%s%s%s' '<timeAxisLabelVisible>true</timeAxisLabelVisible><timeAxisLongLabelVisible>false</timeAxisLongLabelVisible>' '<timeAxisLabelRotation>0</timeAxisLabelRotation><timeAxisTitle/><timeAxisTitleRotation>0</timeAxisTitleRotation>' '<timeAxisTitlePlacement>50</timeAxisTitlePlacement><timeAxisMajorGridVisible>true</timeAxisMajorGridVisible>' '%s<timeAxisMajorGridStroke type="stroke" width="1.0" endCap="butt" lineJoin="bevel"/>' '<timeAxisMinorGridVisible>false</timeAxisMinorGridVisible>%s' '<timeAxisMinorGridStroke type="stroke" width="1.0" endCap="butt" lineJoin="bevel"/>' '<headerText>%s</headerText>%s%s<headerJustification type="jTextJustification" value="TextJustificationLeft"/>' '<footerText/>%s%s<footerJustification type="jTextJustification" value="TextJustificationLeft"/>' '<enterprisePrimarySourceLocation type="nodeName" node="$default$"/>' '<enterpriseSecondarySourceLocation type="nodeName" node="$default$"/>' '<enterprisePrimaryHistoryLocation type="nodeName" node="$default$"/>' '<enterpriseSecondaryHistoryLocation type="nodeName" node="$default$"/>' '<updateFromHistory>true</updateFromHistory><useSampleInterval>true</useSampleInterval>' '<sampleInterval>0</sampleInterval><optimised>true</optimised>%s' '<timeRange type="timeRangeLast" count="%d"><type type="timeUnit" value="TimeUnitMinute"/></timeRange>' '<updateInterval>5</updateInterval><fillHistory>true</fillHistory><updating>true</updating>' '</chartPanel></config>%s<data type="data"><action type="actionInternalServiced"/></data></chart>') % ( w, h, colorset('foregroundColor', 'muted'), rgb('panel'), rgb('panel'), colorset('hairlineColor', 'muted'), colorset('legendColor', 'text'), colorset('legendBackgroundColor', 'panel'), font(11, False, FONT_UI, 'legendFont'), colorset('timeAxisColor', 'muted'), font(11, False, FONT_UI, 'timeAxisFont'), colorset('timeAxisLabelColor', 'muted'), colorset('timeAxisMajorGridColor', 'line'), colorset('timeAxisMinorGridColor', 'line'), esc(title), colorset('headerColor', 'text'), font(12, True, FONT_UI, 'headerFont'), colorset('footerColor', 'muted'), font(11, False, FONT_UI, 'footerFont'), body, minutes, box_elems(l, t, w, h)) self.add(s) # ----- output def xml(self): vg = '' for i, (n, d, z) in enumerate([('Overview', 'Always shown', 10.0), ('Rough', 'Shown when viewing viewing a large area', 25.0), ('Standard', 'Shown when using the default view setting', 100.0), ('Detail', 'Shown only when viewing a small area', 400.0), ('Intricacies', 'Shown only when viewing a very small area', 1000.0)]): vg += ('<visibilityGroup type="componentData"><htmlId>%d</htmlId><name>%s</name><description>%s</description>' '<minimumZoomEnabled>true</minimumZoomEnabled><minimumZoomFactor>%s</minimumZoomFactor>' '</visibilityGroup>\n') % (i + 2, n, d, z) head = ('<?xml version="1.0" encoding="utf-8" ?>\n<visualization protocolVersion="1.3.0.0">\n <globalSection/>\n' ' <coreObjectDefinition type="displayDefinition">\n<version type="version" value="1.3.0.0"/>\n' '<title>%s\ntrue\n%d\n%d\n' '%d\n' '%s\ntrue\n%s\n%s\n%s\n\n' '' '\n' '\n' '500\ntrue\nfalse\n' ) % (esc(self.title), self.w, self.h, self.hid, colorset('backgroundColor', self.bg), colorset('defaultBgColor', self.bg), colorset('defaultFgColor', 'text'), font(12, False, FONT_UI, 'defaultFont')) layer = '1Layer1\n' body = '%d\n' % len(self.parts) # full-size background rectangle first (the web client ignores the display background colour on some frames) return (head + vg + layer + body + '\n'.join(self.parts) + '\n \n\n') def save(self, folder): import os with open(os.path.join(folder, self.name + '.xml'), 'w', encoding='utf-8', newline='\r\n') as f: f.write(self.xml())