"""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. The Operator Interface (desktop) draws arc angles mirrored about the horizontal axis compared with the web client, so only shapes symmetric about that axis look the same on both: 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())