Ovation Graphics Translation Layer(s) - Pro Analysis
Date: 2025 09-September 17
CB 0: Load OVMAN90 and OVMAN91, request translation layers
Hi
I need you to help create translation layers between modern web graphics (html, javascript, css) to proprietary Ovation source and diagram graphics. A python library for doing the translation would be great, or another language can be recommended.
Let's use this session effectively. Ask clarifying questions before diving in.
GG 0: Initial Analysis and Clarifying Questions
Of course. I can help with that. To ensure I provide the most relevant guidance, I have a few clarifying questions. In the meantime, here is a breakdown of the core concepts of the Ovation Graphics system based on OVMAN90 and OVMAN91.
Understanding Ovation Graphics
Ovation process diagrams are defined using a proprietary language detailed in the "Ovation™™ Graphics Language Reference Manual" (OVMAN91.pdf). Here's a summary of the key concepts:
-
Source and Object Files: Each process diagram consists of two files: a source file (
.src) and an object file (.diag)11.-
The
source file is a human-readable ASCII representation of the diagram, containing commands that define the graphical elements and their behavior22.
-
The
object file is the compiled, "executable" version of the diagram that is displayed on the Ovation Operator Station33.
-
-
Graphics Builder: This is the tool used to create and edit Ovation process diagrams4. It has two modes:
-
An interactive editor for drawing graphical elements.
-
A
source editor for writing and editing the graphics language commands directly55.
-
-
Graphics Language: This is the core of the Ovation graphics system. It consists of commands to:
-
Draw static elements like lines, circles, arcs, and polygons6666666666666666666666.
-
Display dynamic data from the control system using elements like bars, plots, and process points777777777.
-
Handle user interaction through "poke fields," which are clickable areas that can trigger actions8888.
-
Implement logic using conditional statements (
IF/ENDIF,IFELSE/ELSE/ENDIF) 9and loops (LOOP/ENDLOOP)10.
-
Clarifying Questions:
To better assist you, could you please provide more information on the following:
-
What is the primary goal of the translation?
-
Are you aiming to convert existing HTML/JS/CSS graphics into Ovation format?
-
Or are you looking to create a tool that allows for the dynamic generation of Ovation graphics from web-based inputs?
-
-
What is the nature of the web graphics you are working with?
-
Are they static displays, or do they involve complex animations and user interactions?
-
Are they based on a specific framework like React, Angular, or Vue?
-
-
What level of fidelity is required in the translation?
-
Are you looking for a pixel-perfect conversion, or is an approximation of the original graphic acceptable?
-
How important is it to maintain complex animations and interactions?
-
-
What is your development environment?
-
What operating system are you using?
-
Do you have access to an Ovation system for testing and validation?
-
Once I have a better understanding of your requirements, I can provide more specific recommendations for a suitable programming language and a potential library structure.
CB 1: Clarifying Question Answers
I do have access to an Ovation system
I do not like the proprietary graphics editor. I think it is outdated, and it does not enable the user to quickly and elegantly generate beautiful and useful graphics. All the graphics we have appear outdated. I would like to be able to generate beautiful graphics, which can be informed by assets like CAD, P&ID, and GIS - these are abstract and can be first converted to web. I do not have any existing web graphics - rather, I am trying to design a pipeline and graphics creation that exceeds the capacity of the current one. I want to create cool graphics that are immersive, with a key goal being isometric images. I am sure this can be accomplished using modern matric algebra based tools. I am primarily a python developer, i use powershell in windows 11. The OVation system i have is Ovation 3.8 on windows 10.
Pixel perfect is not necessary. What appears on the final Ovation diagram can stylistically be a low fi version of an initial web version (which does not exist yet, and can be formed to be stylistically approximate).
Complex animations and interactions can be determined by the limitations of the Ovation system - there is no need to generate more complex animation than this in the original. Please provide information about these constraints.
I do not yet have a specific web framework in mind. Please recommend.
GG 1: Framework Recommendations
Excellent. Thank you for the detailed clarification. This provides a clear vision of your goal: to create a modern, sophisticated graphics generation pipeline that treats Ovation's proprietary format as a final "compilation target" rather than a primary design environment. This is an ambitious but very achievable project.
Based on your requirements, here is a recommended approach, including a suggested toolchain, a breakdown of Ovation's constraints, and a proposed structure for a Python library.
Recommended Language and Web Framework
Language: Python
Your preference for Python is a perfect fit for this task. It is exceptionally well-suited for building this kind of translation layer due to its:
-
Strong Text Processing: Generating the
.srcfile is fundamentally a text-generation task, which Python excels at. -
Excellent XML/SVG Parsing Libraries: Libraries like
xml.etree.ElementTree(built-in) orlxmlmake it straightforward to parse and extract data from SVG files. -
Mathematical and Geometric Libraries: Libraries like
NumPycan handle the matrix algebra required for isometric projections and coordinate transformations. -
Extensive Ecosystem: You can easily find libraries to read data from your source assets (e.g.,
dxfgrabberfor CAD files,geopandasfor GIS).
Web Format: SVG (Scalable Vector Graphics)
I strongly recommend using SVG as your intermediate web graphics format. Here’s why:
-
Vector-Based: The Ovation Graphics Language is fundamentally vector-based, defining objects through coordinates and commands like
LINE,POLYGON, andARC111111111. SVG's vector nature aligns perfectly with this, making translation far simpler than from a raster format like PNG or JPEG. -
XML Structure: SVG is an XML-based format. This means an SVG file has a structured, hierarchical tree that is easy to parse programmatically. You can read an SVG file and directly map its elements (like
<rect>,<circle>,<path>) to Ovation commands. -
Custom Metadata: You can embed custom data directly into SVG elements using
data-*attributes. This is crucial for storing Ovation-specific information, such as point names or poke field actions, that doesn't have a direct visual representation.
JavaScript Library: D3.js
For the initial step of converting your abstract data (from CAD, P&ID, etc.) into the SVG format, D3.js is an excellent choice. It is a powerful JavaScript library for creating data-driven documents. It would allow you to write scripts that:
-
Take your structured data as input.
-
Generate complex SVG scenes, including applying the matrix transformations needed for the isometric views you want.
-
Bind process point information and interaction metadata to the generated SVG elements.
Proposed Graphics Pipeline
Here is a high-level workflow based on this toolchain:
-
Asset Conversion (Data -> SVG):
-
Write scripts (Python or another language) to parse your source assets (CAD, P&ID, GIS).
-
This script will output a structured data format (like JSON) that describes the shapes, positions, and any associated process data.
-
Use a web page with a D3.js script to read this JSON data and render a dynamic, interactive SVG. This is the stage where you implement your "beautiful" and "immersive" designs, including isometric projections.
-
Crucially, embed Ovation-specific metadata into the SVG elements. For example:
XML
<rect id="pump1" x="100" y="150" width="50" height="70" data-ovation-point="PUMP-01.AV" data-ovation-dynamic="fill-color" /> <g id="valve-control" data-ovation-poke-type="7" data-ovation-program="6" data-ovation-point1="VLV-101.ID" data-ovation-point2="VLV-101.ID"> <path d="..." /> <text>V-101</text> </g>
-
-
Translation (SVG -> Ovation
.srcfile):-
This is the core of your Python library. The library will take the SVG file as input.
-
It will parse the SVG's XML structure.
-
It will iterate through each element and translate it into one or more Ovation Graphics Language commands.
-
It will transform SVG coordinates to fit Ovation's 16,384 x 16,384 virtual coordinate system2.
-
It will read the
data-*attributes to create dynamic elements, poke fields, and conditionals. -
Finally, it will assemble all the commands into a valid
.srcfile, complete withDIAGRAM,BACKGROUND, andFOREGROUNDsections333333333.
-
-
Compilation & Deployment (Ovation System):
-
Use the Ovation Developer Studio to import the generated
.srcfile. -
Compile the graphic, which creates the
.diagfile for use on the Operator Station. -
Download the new graphic to the operator drops.
-
Ovation System Constraints
As you correctly noted, the source web graphic should be designed with Ovation's limitations in mind. Here are the key constraints on animation and interaction:
Animation Constraints
Ovation does not support general keyframe animations like you find in CSS. Instead, it offers a set of specific animation commands that are tied to process data. Your web "source" should model animation as state changes that map to these commands:
-
Visibility/State Change: Instead of a fade-in/out, you can show one of several different shapes or bitmaps based on a condition4444. This is handled by the
COND_DISPLAY_SHAPESorCOND_DISPLAY_BMPScommands. -
Color Change: An object's color can change based on a conditional expression (e.g.,
(A100 AV > 90.0) red)5. This is a discrete change, not a smooth transition. -
Filling: Shapes can be filled dynamically based on an analog point's value. This is useful for tank levels or progress bars666666666666666666666666666666666666. Commands include
VARIABLE_FILL_RECT,BAR,OL_CYLINDER, etc. -
Movement: An object can move along a predefined, static path (
MOVING_SHAPE,MOVING_BMP)7777. The path itself cannot change dynamically. -
Rotation: An object can rotate continuously (
ROTATING_SHAPE,ROTATING_BMP) or be set to a specific angle based on a point's value (BMP_IMAGEwith dynamic rotation)888888888. -
Blinking: Objects can be made to blink (
BLINKING_SHAPE,BLINKING_BMP)9999.
Recommendation: In your SVG/JS environment, avoid CSS transitions or complex timeline animations. Instead, define "states" for your graphical elements. For example, a valve could have an "open" state (green color) and a "closed" state (red color). Your Python translator would then convert these states into Ovation conditional logic.
Interaction Constraints
User interaction is not as freeform as a web page onclick event. All interactions are handled by rectangular, invisible "poke fields" (
POKE_FLD) or stylized BUTTON elements10101010.
-
A poke field is an area that, when clicked, executes a specific
Application Program11. These programs are numbered and perform predefined actions, such as changing a diagram (
CHGDIAG (1)), opening a control faceplate (CNTRL_POKE (6)), or acknowledging an alarm (ALARM_ACKNOWLEDGE (214))121212121212121212. -
The action is hardcoded into the poke field definition. You cannot have arbitrary JavaScript-style logic execute on a click.
Recommendation: In your SVG, any element that should be interactive must be tagged with metadata specifying the Ovation poke_type and the necessary parameters for the corresponding Application Program. Your Python translator will then generate a POKE_FLD command with the correct size and position to overlay that SVG element.
Proposed Python Library Structure
Here is a possible structure for your ovation_translator Python library:
ovation_translator/
├── __init__.py
├── main.py # Command-line interface for the translator
├── svg_parser.py # Logic to read and parse the SVG file
├── ovation_elements.py # Classes for each Ovation graphic element (Line, Rect, PokeField, etc.)
├── ovation_diagram.py # A class to represent the entire diagram and generate the final .src file
└── utils.py # Helper functions for coordinate mapping, color conversion, etc.
ovation_elements.py Example:
Python
# ovation_elements.py
class OvationElement:
def to_src(self):
raise NotImplementedError
class OvationRectangle(OvationElement):
def __init__(self, x, y, w, h, line_width, line_pat, fill_pat):
self.x = x
self.y = y
# ... other properties
def to_src(self):
# Returns the RECTANGLE command string
return f"RECTANGLE {self.x} {self.y} {self.w} {self.h} {self.line_width} {self.line_pat} {self.fill_pat}"
class OvationPokeField(OvationElement):
def __init__(self, x, y, w, h, poke_type, program_id, args):
# ... properties
def to_src(self):
# Returns the POKE_FLD command string with all its arguments
# ...
svg_parser.py Logic:
Python
# svg_parser.py (pseudocode)
import xml.etree.ElementTree as ET
from . import ovation_elements, ovation_diagram
def parse_svg(filepath):
tree = ET.parse(filepath)
root = tree.getroot()
diagram = ovation_diagram.OvationDiagram(name="TranslatedGraphic")
for element in root.iter():
if element.tag.endswith('rect'):
# Extract attributes from SVG <rect>
x = float(element.get('x'))
# ... transform coordinates, map colors, etc.
# Create an OvationRectangle object
rect = ovation_elements.OvationRectangle(...)
diagram.add_foreground_element(rect)
elif element.tag.endswith('g') and 'data-ovation-poke-type' in element.attrib:
# This is an interactive group
# Extract bounding box for poke field dimensions
# Extract data-* attributes for poke parameters
poke_field = ovation_elements.OvationPokeField(...)
diagram.add_keyboard_element(poke_field)
return diagram
This approach provides a robust and extensible framework for your project. You can start by implementing translation for simple shapes and gradually add support for more complex elements, dynamics, and interactions as you refine your pipeline.
CB 2: Design Affect and Pipeline Tip to Tail
Excellent
I would like to generate views of the plant. These can be top-down aerial views of the entire plant, zoomed in to different subsystems, or views can be angled and isometric. Each view should be it's own graphic. These graphics can have navigation buttons that allow movement around the plant, almost like google street view, and could harken back to dungeon CLI games ("Walk north, pick up rock"). As I collect CAD data, GIS, P&ID assets, etc, what would you recommend for organizing those files for successful translation using 3D?
I do not yet have a geotag/coordinate location assigned to every sensor/point in the Ovation DCS system, but this is a goal. In this way, each relevant sensor can be made available in each view, with a distance veil / level of detail (LOD) setting as necessary. COlor coding of sensors can be based on current quality, with blinking LED's representing rate, etc.
Describe more about the level of animation I could possibly achieve.
GG 2: Specific complete architecture
Excellent. Your vision is to essentially create a lightweight "digital twin" interface within the Ovation ecosystem. By using modern 3D/geospatial data as a single source of truth and web tech as a rendering pipeline, you can generate sophisticated, data-rich 2D views that are far beyond what could be created efficiently with the native builder.
Here is a recommended strategy for organizing your files and a deeper dive into the animation and interaction capabilities of the Ovation system.
Strategy for Organizing 3D and Asset Files
The key to success is creating a single source of truth that links your physical assets to their digital representations and their DCS points. I recommend a centralized data hub approach.
Proposed File Structure:
/plant_graphics_project/
|-- /source_assets/
| |-- /cad_models/ # Plant 3D, Revit, STEP files, etc.
| |-- /gis_data/ # Shapefiles, GeoJSON for site layout
| |-- /p&ids/ # Original P&ID drawings (PDF, DWG)
|
|-- /data_hub/
| |-- master_asset_list.csv # The core linking file
| |-- view_definitions.json # "Camera" positions for each graphic
|
|-- /generation_pipeline/
| |-- 1_asset_extractor.py # Python script to parse source assets
| |-- 2_svg_renderer.js # D3.js script to generate SVG views
| |-- 3_ovation_translator.py # Your main Python translation library
|
|-- /output/
| |-- /intermediate_svgs/ # SVG files generated for each view
| |-- /ovation_src/ # Final .src files ready for import
- The Data Hub - Your "Single Source of Truth":
This is the most critical part. Your master_asset_list.csv (or a simple database like SQLite) will be a master table that connects everything. Each row could represent a single piece of equipment or sensor with columns like:
| AssetID | AssetType | P&ID_Tag | Ovation_Point_Name | CAD_Object_ID | GIS_X | GIS_Y | GIS_Z |
|---|---|---|---|---|---|---|---|
| PMP-101A | Pump | P-101A | PUMP-101A-SPEED | uuid_... |
30.45 | -90.01 | 15.2 |
| VLV-205 | Valve | FCV-205 | VLV-205-STATUS | uuid_... |
30.51 | -90.03 | 15.5 |
| TANK-300 | Tank | TK-300 | TANK-300-LEVEL | uuid_... |
31.00 | -89.95 | 18.0 |
- View Definitions:
The view_definitions.json file will define each Ovation graphic you want to create. Each entry will act as a "virtual camera" in your 3D space.
JSON
{
"views": [
{
"name": "Overall_Plant_View",
"type": "top-down",
"camera_pos": [500, 500, 1000],
"zoom_level": 0.1
},
{
"name": "Boiler_Area_Isometric",
"type": "isometric",
"camera_pos": [30, -90, 45],
"camera_angle": [45, 35.264, 0]
},
{
"name": "Boiler_Feedpump_Detail",
"type": "isometric",
"camera_pos": [30.4, -90.0, 16],
"zoom_level": 5.0
}
]
}
Implementing LOD and Geotagging
Your plan to geotag every sensor is the correct approach. This allows for a procedural, data-driven generation of graphics.
Your Python script (1_asset_extractor.py) will perform the following logic for each view defined in view_definitions.json:
-
Set the Camera: Read the camera position, angle, and type for the view.
-
Cull the Assets: Query your
master_asset_list.csvto find all assets within the camera's view frustum or a defined radius. This is your distance veiling. -
Apply Level of Detail (LOD): For each visible asset, calculate its distance from the camera.
-
If the asset is very far, represent it as a simple dot or icon.
-
If it is at a medium distance, use a simplified 3D model or symbol.
-
If it is close, use a detailed 3D model.
-
-
Project to 2D: For each visible asset's geometry, apply a 3D-to-2D projection matrix based on the camera's position and angle (this is where you'd implement the isometric view). The output of this step is a set of 2D coordinates and paths.
-
Generate Data for SVG: Package this 2D information and the associated Ovation point data into a structured format (like JSON) to be consumed by your SVG renderer.
Deep Dive: Ovation Animation Capabilities
This is where you translate the "state" of your web graphic into what Ovation can actually render. Ovation's animation is state-driven, not timeline-based. An action happens because a process point changes its value or quality.
Here are the primary animation types you can achieve, with citations from the provided manuals:
-
Color Change based on Quality/Value: This is the most common and effective way to show status.
-
You can change an object's Foreground (FG), Background (BG), or Erase (ER) color using a
COLORcommand with a conditional expression1111. -
The
(QUALITY)conditional is perfect for your goal of color-coding sensors based on quality222. The graphic can automatically change an object's color to reflectGOOD,FAIR,POOR,BAD, orTIMED-OUTstates. -
You can also use simple expressions like
(point.AV > 95.0) redto change color based on a value threshold3.
-
-
Blinking based on State: To represent a rate or an urgent alarm, blinking is effective.
-
The
BLINKING_SHAPEandBLINKING_BMPcommands make a single shape or bitmap blink at a defined speed444444444444444. -
Crucially, the
speed itself can be dynamic, tied to a process point value (from 1-10)5555. This directly enables your "blinking LED's representing rate" concept. You can have a shape blink faster as a motor's speed increases.
-
The animation can be turned on or off with a conditional runtime state, allowing you to start blinking only when a condition is met (e.g.,
runtime_state OFF { (point.AV > 500) ON })6666.
-
-
Dynamic Filling for Levels and Gauges:
-
You can show tank levels or progress by partially filling a shape based on a point's value.
-
Commands like
BAR,VARIABLE_FILL_RECT,OL_GAUGE, andOL_CYLINDERare designed for this purpose7777777777777777. AVARIABLE_FILL_RECTcan be filled horizontally or vertically to represent a changing value within a defined range8.
-
-
Movement and Rotation:
-
MOVING_SHAPEandMOVING_BMP: These commands move an object along a pre-defined, static path9999. The path itself cannot change at runtime. This could be used to show flow in a pipe or movement on a conveyor belt. -
ROTATING_SHAPEandROTATING_BMP: These provide continuous rotation at a set speed10101010. This is perfect for showing that a pump or fan is running. The speed can be tied to a process point value11111111. -
Dynamic Rotation (Non-Continuous): The
BMP_IMAGEcommand can be dynamically rotated to a specific angle based on a point's value, which is ideal for creating custom analog meters or indicating a valve's position12121212.
-
-
Conditional Display (Swapping Visuals):
-
Instead of animating a single object, you can swap between different objects. The
COND_DISPLAY_SHAPESandCOND_DISPLAY_BMPScommands allow you to display one of up to 10 different shapes/bitmaps based on a conditional131313131313131313. This is a very powerful feature. For example, you could show a stopped motor icon, a running motor icon, and a tripped motor icon, all in the same location, based on the motor's status point.
-
Summary of Animation Feasibility: Your vision of color-coding for quality and blinking for rate is fully achievable with Ovation's graphics language. The key is to map these visual states to the appropriate conditional commands in your Python translator.
Putting It All Together: An Example Workflow for One View
Let's say you want to generate the "Boiler_Area_Isometric" graphic:
-
Define the View: Your Python pipeline reads the "Boiler_Area_Isometric" definition from
view_definitions.json. -
Query Assets: It queries
master_asset_list.csvfor all assets within the boiler area, applying LOD rules. Let's say this includes a feedpumpPMP-101Aand a valveVLV-205. -
Generate SVG: Your D3.js script takes this data and:
-
Draws the static background geometry (pipes, platforms) using isometric projection.
-
Draws a symbol for
PMP-101A. It embeds<g id="PMP-101A" data-ovation-point="PUMP-101A-SPEED" data-ovation-animation="blinking-rate">...</g>. -
Draws a symbol for
VLV-205. It embeds<g id="VLV-205" data-ovation-point="VLV-205-STATUS" data-ovation-states='{"open": "green", "closed": "red"}'>...</g>. -
Draws a "Walk North" button with
<rect id="nav-north" data-ovation-poke-type="1" data-ovation-diag-name="Boiler_Top_View">...</rect>.
-
-
Translate to
.src: Yourovation_translator.pylibrary reads this SVG:-
The static geometry is converted to
POLYGONandLINEcommands in theBACKGROUNDsection. -
The
PMP-101Agroup is translated into aBLINKING_SHAPEcommand inside anIF_DISPLAY (PUMP-101A-SPEED.AV > 0)block. Thespeedparameter of the command is set directly to thePUMP-101A-SPEED.AVpoint. -
The
VLV-205group is translated into aSHAPEcommand with aCOLORcommand before it that uses a conditional:COLOR FG green (VLV-205-STATUS.AV = 1) red. -
The
nav-northrectangle is translated into aPOKE_FLDcommand in theKEYBOARDsection that callsCHGDIAG (1)with the diagram name "Boiler_Top_View".
-
-
Deploy: The final
.srcfile is created, ready for you to compile and use in Ovation.
This pipeline allows you to separate the complex work of geometric projection and visual design (in the web environment) from the rule-based task of generating the proprietary Ovation source code (in Python), giving you the modern workflow and high-quality results you're looking for.
CB 3: Code Request:
excellent
show safe but rigorous and functional initial versions of 1_ovation_translator.py, 2_svg_renderer.js, and 1_asset_extractor.py . Create python classes as necessary. include a __name__ ==__main__ section in each python file at the bottom as a basic demo for your prescribed workflow, assuming the necessary other files exist.
Recommend python tests, without falling into the typical trap of over-definition.
GG: Code, initial versions
Of course. This is an excellent project, and I will provide you with a set of initial, functional scripts that follow your prescribed workflow.
These scripts are designed as a robust starting point. They are safe, using standard libraries and file operations, and demonstrate the core logic of each step in the pipeline. I have structured the Python code with classes to make it rigorous and extensible.
The 3-Step Graphics Pipeline
Here is the code for the three main components of your workflow:
-
1_asset_extractor.py: Reads asset data, applies a 3D isometric projection, and exports the data for web rendering. -
svg_renderer.html: A simple HTML/JavaScript file that ingests the data from step 1 and renders an SVG, embedding Ovation-specific metadata. -
3_ovation_translator.py: The core Python library that parses the SVG from step 2 and translates it into a valid Ovation.srcfile.
File 1: 1_asset_extractor.py
This script simulates reading a master list of assets, defines a "camera" for an isometric view, projects the 3D coordinates into 2D, and saves the result as a JSON file for the next step.
Python
# 1_asset_extractor.py
import csv
import json
import numpy as np
from pathlib import Path
class Asset:
"""A simple data class to hold asset information."""
def __init__(self, asset_id, asset_type, ovation_point, x, y, z):
self.id = asset_id
self.type = asset_type
self.ovation_point = ovation_point
self.coords_3d = np.array([float(x), float(y), float(z)])
class ViewCamera:
"""Represents a virtual camera and handles 3D-to-2D projection."""
def __init__(self, elevation_deg=35.264, azimuth_deg=45):
"""
Initializes the camera with standard isometric projection angles.
"""
# Convert degrees to radians for numpy trigonometric functions
elevation_rad = np.deg2rad(elevation_deg)
azimuth_rad = np.deg2rad(azimuth_deg)
# Create rotation matrices
rot_x = np.array([
[1, 0, 0],
[0, np.cos(elevation_rad), -np.sin(elevation_rad)],
[0, np.sin(elevation_rad), np.cos(elevation_rad)]
])
rot_z = np.array([
[np.cos(azimuth_rad), -np.sin(azimuth_rad), 0],
[np.sin(azimuth_rad), np.cos(azimuth_rad), 0],
[0, 0, 1]
])
# Isometric projection matrix (drop the z-axis for 2D)
projection_2d = np.array([
[1, 0, 0],
[0, 1, 0]
])
# Combined transformation matrix
self.transform_matrix = projection_2d @ rot_x @ rot_z
def project(self, coords_3d):
"""Projects a 3D numpy array to a 2D numpy array."""
return self.transform_matrix @ coords_3d
class AssetExtractor:
"""Loads asset data and generates 2D view data for rendering."""
def __init__(self, asset_csv_path):
self.assets = self._load_assets(asset_csv_path)
def _load_assets(self, path):
assets = []
with open(path, 'r') as f:
reader = csv.DictReader(f)
for row in reader:
assets.append(Asset(
asset_id=row['AssetID'],
asset_type=row['AssetType'],
ovation_point=row['Ovation_Point_Name'],
x=row['X'], y=row['Y'], z=row['Z']
))
return assets
def generate_view_data(self, camera, output_path):
view_data = {'assets': []}
for asset in self.assets:
# Project 3D coordinates to 2D screen coordinates
coords_2d = camera.project(asset.coords_3d)
# Here you would implement Level of Detail (LOD) logic
# For this demo, we'll include all assets
view_data['assets'].append({
'id': asset.id,
'type': asset.type,
'ovation_point': asset.ovation_point,
'screen_x': coords_2d[0],
'screen_y': coords_2d[1]
})
with open(output_path, 'w') as f:
json.dump(view_data, f, indent=2)
print(f"Successfully extracted asset data to '{output_path}'")
# --- Demo Section ---
if __name__ == "__main__":
print("--- Running Asset Extractor Demo ---")
# 1. Create a dummy CSV file for demonstration purposes
demo_csv_content = """AssetID,AssetType,Ovation_Point_Name,X,Y,Z
PMP-101A,Pump,PUMP-101A-SPEED,0,0,0
VLV-205,Valve,VLV-205-STATUS,100,0,0
TANK-300,Tank,TANK-300-LEVEL,0,100,0
NAV-BTN-NORTH,NavButton,Boiler_Top_View,50,200,0
"""
asset_file = Path("master_asset_list.csv")
asset_file.write_text(demo_csv_content)
print(f"Created demo asset file: '{asset_file}'")
# 2. Instantiate the extractor and the camera
extractor = AssetExtractor(asset_file)
isometric_camera = ViewCamera()
# 3. Generate the view data and save to JSON
json_output_path = Path("boiler_area.json")
extractor.generate_view_data(isometric_camera, json_output_path)
# 4. Clean up dummy file
asset_file.unlink()
File 2: svg_renderer.html
This file is opened in a web browser. It will automatically fetch boiler_area.json and render an SVG. A button is provided to download the generated SVG, which becomes the input for the next step.
HTML
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SVG Renderer for Ovation</title>
<style>
body { font-family: sans-serif; margin: 2em; }
svg { border: 1px solid #ccc; background-color: #f0f0f0; }
.pump { fill: #8888dd; }
.valve { fill: #dd8888; }
.tank { fill: #88dd88; }
.nav-button { fill: #dddd88; cursor: pointer; }
</style>
</head>
<body>
<h1>Ovation Graphic Preview (SVG)</h1>
<p>This page reads `boiler_area.json` and generates an SVG. The SVG contains embedded metadata for the Ovation translator.</p>
<svg id="canvas" width="800" height="600" viewBox="-200 -200 400 400"></svg>
<br>
<button id="downloadBtn">Download SVG</button>
<script>
// In a real application, you would use a library like D3.js here for more power.
// This is a simple vanilla JS implementation for clarity.
const svgNS = "http://www.w3.org/2000/svg";
const canvas = document.getElementById('canvas');
// Function to create and add an SVG element
function createSvgElement(asset) {
let elem;
// A simple factory to create different shapes based on asset type
switch (asset.type) {
case 'Pump':
elem = document.createElementNS(svgNS, 'circle');
elem.setAttribute('cx', asset.screen_x);
elem.setAttribute('cy', asset.screen_y);
elem.setAttribute('r', 20);
elem.setAttribute('class', 'pump');
// Embed Ovation metadata
elem.setAttribute('data-ovation-point', asset.ovation_point);
elem.setAttribute('data-ovation-dynamic-type', 'blinking-rate');
break;
case 'Valve':
elem = document.createElementNS(svgNS, 'rect');
elem.setAttribute('x', asset.screen_x - 15);
elem.setAttribute('y', asset.screen_y - 15);
elem.setAttribute('width', 30);
elem.setAttribute('height', 30);
elem.setAttribute('class', 'valve');
elem.setAttribute('data-ovation-point', asset.ovation_point);
elem.setAttribute('data-ovation-dynamic-type', 'conditional-color');
break;
case 'Tank':
elem = document.createElementNS(svgNS, 'rect');
elem.setAttribute('x', asset.screen_x - 25);
elem.setAttribute('y', asset.screen_y - 40);
elem.setAttribute('width', 50);
elem.setAttribute('height', 80);
elem.setAttribute('class', 'tank');
elem.setAttribute('data-ovation-point', asset.ovation_point);
elem.setAttribute('data-ovation-dynamic-type', 'variable-fill');
break;
case 'NavButton':
elem = document.createElementNS(svgNS, 'rect');
elem.setAttribute('x', asset.screen_x - 40);
elem.setAttribute('y', asset.screen_y - 15);
elem.setAttribute('width', 80);
elem.setAttribute('height', 30);
elem.setAttribute('class', 'nav-button');
// Embed poke field metadata
elem.setAttribute('data-ovation-poke-type', '2'); // Diagram & Group
elem.setAttribute('data-ovation-diag-name', asset.ovation_point); // Using point field for diag name
elem.setAttribute('data-ovation-group-num', '0');
break;
}
if (elem) {
canvas.appendChild(elem);
}
}
// Fetch the data and render the SVG
fetch('boiler_area.json')
.then(response => response.json())
.then(data => {
data.assets.forEach(asset => createSvgElement(asset));
})
.catch(error => console.error('Error loading asset data:', error));
// Download handler
document.getElementById('downloadBtn').addEventListener('click', () => {
const svgData = new XMLSerializer().serializeToString(canvas);
const blob = new Blob([svgData], { type: 'image/svg+xml' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = 'boiler_area.svg';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
</script>
</body>
</html>
File 3: 3_ovation_translator.py
This script contains the core translation logic, structured into classes as requested. It parses the SVG, maps elements and metadata to Ovation commands, and generates the final .src file.
Python
# 3_ovation_translator.py
import xml.etree.ElementTree as ET
from pathlib import Path
import abc
# --- Ovation Element Classes ---
class OvationElement(abc.ABC):
"""Abstract base class for all Ovation graphic elements."""
@abc.abstractmethod
def to_src(self) -> str:
pass
class OvationColor(OvationElement):
"""Represents a COLOR command."""
def __init__(self, fg='white', bg='black', er='black', ol=0):
self.fg = fg
self.bg = bg
self.er = er
self.ol = ol
def to_src(self):
return f"COLOR FG {self.fg} BG {self.bg} ER {self.er} OL {self.ol} BLINK FG OFF BG OFF"
class OvationRectangle(OvationElement):
"""Represents a RECTANGLE command."""
def __init__(self, x, y, w, h, line_width=1, line_pat='solid', fill_pat='unfilled'):
self.x, self.y, self.w, self.h = x, y, w, h
self.line_width = line_width
self.line_pat = line_pat
self.fill_pat = fill_pat
def to_src(self):
return f"RECTANGLE {self.x} {self.y} {self.w} {self.h} {self.line_width} {self.line_pat} {self.fill_pat}"
class OvationCircle(OvationElement):
"""Represents a CIRCLE command."""
def __init__(self, cx, cy, r, line_width=1, line_pat='solid', fill_pat='unfilled'):
self.cx, self.cy, self.r = cx, cy, r
self.line_width = line_width
self.line_pat = line_pat
self.fill_pat = fill_pat
def to_src(self):
return f"CIRCLE {self.cx} {self.cy} {self.r} {self.line_width} {self.line_pat} {self.fill_pat}"
class OvationPokeField(OvationElement):
"""Represents a POKE_FLD command."""
def __init__(self, x, y, w, h, poke_type, diag_name, group_num):
self.x, self.y, self.w, self.h = x, y, w, h
self.poke_type = poke_type
self.diag_name = diag_name
self.group_num = group_num
def to_src(self):
# This is a simplified POKE_FLD for poke_type 2
return f"POKE_FLD {self.x} {self.y} {self.w} {self.h} ON {self.poke_type} {self.diag_name} {self.group_num}"
class OvationDiagram:
"""Represents a complete Ovation diagram and generates the final .src file."""
def __init__(self, name="Default", width=16384, height=16384):
self.name = name
self.width = width
self.height = height
self._background_elements = []
self._foreground_elements = []
self._keyboard_elements = []
def add_element(self, element, section='background'):
if section == 'background':
self._background_elements.append(element)
elif section == 'foreground':
self._foreground_elements.append(element)
elif section == 'keyboard':
self._keyboard_elements.append(element)
def to_src(self):
src_lines = []
# Header
src_lines.append(f'DIAGRAM MAIN 0 0 0 1920 1080 gray50 ZOOMABLE 1 0 0 {self.width} {self.height} 1 DEFAULT_POSITION DEFAULT_SIZE "{self.name}"')
# Background Section
if self._background_elements:
src_lines.append("\nBACKGROUND")
for elem in self._background_elements:
src_lines.append(elem.to_src())
# Foreground Section
if self._foreground_elements:
src_lines.append("\nFOREGROUND")
for elem in self._foreground_elements:
src_lines.append(elem.to_src())
# Keyboard Section (for poke fields)
if self._keyboard_elements:
src_lines.append("\nKEYBOARD")
for elem in self._keyboard_elements:
src_lines.append(elem.to_src())
return "\n".join(src_lines)
class SVGTranslator:
"""Translates an SVG file into an OvationDiagram object."""
OVATION_WIDTH = 16384
OVATION_HEIGHT = 16384
def __init__(self, svg_filepath):
self.tree = ET.parse(svg_filepath)
self.root = self.tree.getroot()
self.diagram = OvationDiagram(name=Path(svg_filepath).stem)
def _transform_coords(self, svg_x, svg_y, svg_w, svg_h):
"""Scales SVG coordinates to the Ovation virtual grid. This is a simple linear scale."""
svg_viewbox = [float(v) for v in self.root.get('viewBox').split()]
svg_vb_w = svg_viewbox[2]
svg_vb_h = svg_viewbox[3]
scale_x = self.OVATION_WIDTH / svg_vb_w
scale_y = self.OVATION_HEIGHT / svg_vb_h
ov_x = int((svg_x - svg_viewbox[0]) * scale_x)
ov_y = int((svg_y - svg_viewbox[1]) * scale_y)
ov_w = int(svg_w * scale_x)
ov_h = int(svg_h * scale_y)
return ov_x, ov_y, ov_w, ov_h
def translate(self):
self.diagram.add_element(OvationColor(), 'background')
for elem in self.root.iter():
tag = elem.tag.split('}')[-1] # Remove namespace if present
if tag == 'rect':
svg_x, svg_y = float(elem.get('x', 0)), float(elem.get('y', 0))
svg_w, svg_h = float(elem.get('width', 0)), float(elem.get('height', 0))
ov_x, ov_y, ov_w, ov_h = self._transform_coords(svg_x, svg_y, svg_w, svg_h)
# Check for poke field metadata
if 'data-ovation-poke-type' in elem.attrib:
poke = OvationPokeField(
x=ov_x, y=ov_y, w=ov_w, h=ov_h,
poke_type=elem.get('data-ovation-poke-type'),
diag_name=elem.get('data-ovation-diag-name'),
group_num=elem.get('data-ovation-group-num')
)
self.diagram.add_element(poke, section='keyboard')
else:
# It's a regular or dynamic rectangle
rect = OvationRectangle(x=ov_x, y=ov_y, w=ov_w, h=ov_h, fill_pat='solid')
self.diagram.add_element(rect, section='background')
elif tag == 'circle':
svg_cx, svg_cy = float(elem.get('cx', 0)), float(elem.get('cy', 0))
svg_r = float(elem.get('r', 0))
# For circles, width and height of transform are based on radius
_, _, ov_rx, ov_ry = self._transform_coords(0, 0, svg_r * 2, svg_r * 2)
ov_cx, ov_cy, _, _ = self._transform_coords(svg_cx, svg_cy, 0, 0)
# Ovation uses radius, avg of scaled rx and ry is a good start
ov_r_avg = int((ov_rx + ov_ry) / 4)
circle = OvationCircle(cx=ov_cx, cy=ov_cy, r=ov_r_avg, fill_pat='solid')
self.diagram.add_element(circle, section='background')
return self.diagram
# --- Demo Section ---
if __name__ == "__main__":
print("\n--- Running Ovation Translator Demo ---")
# 1. Create a dummy SVG file for demonstration
demo_svg_content = """<?xml version="1.0" encoding="UTF-8"?>
<svg id="canvas" width="800" height="600" viewBox="-200 -200 400 400" xmlns="http://www.w3.org/2000/svg">
<circle cx="-70.71" cy="70.71" r="20" class="pump" data-ovation-point="PUMP-101A-SPEED" data-ovation-dynamic-type="blinking-rate"/>
<rect x="-15.0" y="-85.71" width="30" height="30" class="valve" data-ovation-point="VLV-205-STATUS" data-ovation-dynamic-type="conditional-color"/>
<rect x="53.03" y="-12.32" width="50" height="80" class="tank" data-ovation-point="TANK-300-LEVEL" data-ovation-dynamic-type="variable-fill"/>
<rect x="-42.68" y="129.9" width="80" height="30" class="nav-button" data-ovation-poke-type="2" data-ovation-diag-name="Boiler_Top_View" data-ovation-group-num="0"/>
</svg>
"""
svg_file = Path("boiler_area.svg")
svg_file.write_text(demo_svg_content)
print(f"Created demo SVG file: '{svg_file}'")
# 2. Instantiate the translator
translator = SVGTranslator(svg_file)
# 3. Translate the SVG to an OvationDiagram object
ovation_diagram_obj = translator.translate()
# 4. Generate the .src file content
src_content = ovation_diagram_obj.to_src()
# 5. Save the final .src file
src_file = Path("BOILER_AREA_ISO.src")
src_file.write_text(src_content)
print(f"Successfully translated SVG to Ovation source file: '{src_file}'")
print("\n--- Generated .src Content ---")
print(src_content)
# 6. Clean up dummy file
svg_file.unlink()
Python Test Recommendations (A Practical Approach)
Over-defining tests can slow down initial development. Here’s a pragmatic strategy using the pytest framework.
- Unit Test Your Ovation Elements:
These are the easiest and most valuable tests. Ensure each element class produces the correct source string.
Python
# In a file like `tests/test_ovation_elements.py`
from ovation_translator.ovation_elements import OvationRectangle
def test_rectangle_to_src():
rect = OvationRectangle(x=100, y=200, w=300, h=400, line_width=2, line_pat='dashed', fill_pat='solid')
expected_src = "RECTANGLE 100 200 300 400 2 dashed solid"
assert rect.to_src() == expected_src
- Unit Test Key Utilities:
Your coordinate transformation logic is critical. Test it with known values.
Python
# In a file like `tests/test_translator.py`
from ovation_translator.ovation_translator import SVGTranslator
def test_coordinate_transform():
# Create a dummy SVG translator instance to access the method
# (Requires a simple dummy SVG file in your test fixtures)
translator = SVGTranslator('tests/fixtures/dummy_800x600.svg')
svg_x, svg_y, svg_w, svg_h = 100, 50, 20, 30
# Ovation grid is 16384x16384, SVG is 800x600 (viewBox)
# Expected ov_x = 100/800 * 16384 = 2048
# Expected ov_y = 50/600 * 16384 = 1365 (approx)
ov_x, ov_y, ov_w, ov_h = translator._transform_coords(svg_x, svg_y, svg_w, svg_h)
assert ov_x == 2048
assert abs(ov_y - 1365) < 2 # Allow for float rounding
- Integration Test the Full Translation:
This test ensures the parser and element generators work together. Instead of comparing the entire output string (which is brittle), check the structure of the result.
Python
# In `tests/test_translator.py`
from ovation_translator.ovation_translator import SVGTranslator
from ovation_translator.ovation_elements import OvationCircle, OvationPokeField
def test_full_translation_structure():
# Use the SVG from the __main__ demo as a test fixture
translator = SVGTranslator('boiler_area.svg')
diagram = translator.translate()
# Assert that the right number of elements were created in the right sections
assert len(diagram._background_elements) == 4 # 1 Color + 3 shapes
assert len(diagram._keyboard_elements) == 1
# Assert that the types are correct
assert isinstance(diagram._background_elements[1], OvationCircle)
assert isinstance(diagram._keyboard_elements[0], OvationPokeField)
# Spot-check a key property
poke_field = diagram._keyboard_elements[0]
assert poke_field.poke_type == '2'
assert poke_field.diag_name == 'Boiler_Top_View'
This testing strategy focuses on validating your core logic without getting bogged down in minute details, allowing you to build and refactor your translation library with confidence.