svg-processor/config_manager.py
2025-05-16 18:15:31 +04:00

350 lines
16 KiB
Python

import json
import os
import time
# Moved get_application_path here temporarily to avoid circular import
# Ideally, this would be passed in or the config path logic revisited.
def get_application_path():
"""Get the base path for the application, works for both dev and PyInstaller"""
import sys # Local import to avoid being at top level if utils is preferred
if getattr(sys, 'frozen', False):
return os.path.dirname(sys.executable)
else:
# In dev mode, assume config manager is in the same dir as the main script
# This might need adjustment depending on final structure
return os.path.dirname(os.path.abspath(__file__))
# Default configuration values
# Keep default mappings simple here, complex defaults handled in GUI if needed
DEFAULT_CONFIG = {
'file_path': '',
'project_title': 'MyScadaProject',
'parent_project': '', # Often empty for top-level projects
'view_name': 'MyView',
'view_path': '', # Optional path within views, e.g., 'Folder/Subfolder'
'svg_url': '', # Optional background image URL/Path for the view
'image_width': 800,
'image_height': 600,
'default_width': 100, # Default component width
'default_height': 50, # Default component height
'ignition_base_dir': 'C:/Program Files/Inductive Automation/Ignition/data/projects', # Added
'custom_export_dir': '', # Custom directory for SCADA export (empty to use ignition_base_dir)
'element_mappings': [] # List of {svg_id: 'id', scada_type: 'type', scada_path: 'path'}
}
class ConfigManager:
"""
Configuration Manager class for handling configuration persistence.
Loads and saves configuration options to a JSON file.
Auto-creates the configuration file with defaults if it doesn't exist.
Includes logic for backward compatibility with older formats.
"""
def __init__(self, config_file="config.json"):
"""
Initialize the Configuration Manager.
Args:
config_file (str): The name of the configuration file.
It will be stored in the application's directory.
"""
# Store only the filename, calculate full path dynamically
self.config_filename = config_file
self.config_file_path = os.path.join(get_application_path(), self.config_filename)
print(f"DEBUG: Config file path: {self.config_file_path}") # Debug output
self.initialize_config_file() # Ensure file/dir exists
def _get_config_path(self):
"""Returns the full path to the config file."""
# Recalculate in case the application path logic changes
return os.path.join(get_application_path(), self.config_filename)
def initialize_config_file(self):
"""
Create the configuration file with default values if it doesn't exist.
Ensures the containing directory exists.
"""
config_path = self._get_config_path()
config_dir = os.path.dirname(config_path)
try:
# Create directory if it doesn't exist
if config_dir and not os.path.exists(config_dir):
os.makedirs(config_dir)
print(f"Created config directory: {config_dir}")
# Create default config file if it doesn't exist
if not os.path.exists(config_path):
print(f"Config file not found at {config_path}. Creating default config...")
# Use the save method with default config to create the file
self.save_config(DEFAULT_CONFIG)
except Exception as e:
print(f"Error initializing config file/directory: {e}")
def get_config(self):
"""
Load configuration from the file.
Returns:
dict: The loaded (and potentially updated for compatibility) configuration dictionary.
Returns DEFAULT_CONFIG if loading fails.
"""
config_path = self._get_config_path()
try:
if os.path.exists(config_path):
with open(config_path, 'r', encoding='utf-8') as f:
config = json.load(f)
# Ensure compatibility and update format
config = self._ensure_backward_compatibility(config)
config = self._update_config_format(config)
# Merge with defaults to ensure all keys exist
# Loaded values take precedence over defaults
merged_config = DEFAULT_CONFIG.copy()
merged_config.update(config) # Overwrite defaults with loaded values
return merged_config
else:
print(f"Configuration file not found: {config_path}. Returning default config.")
return DEFAULT_CONFIG.copy() # Return a copy of defaults
except json.JSONDecodeError as e:
print(f"Error decoding JSON from config file {config_path}: {e}")
print("Using default configuration.")
# Optionally back up corrupted file
# os.rename(config_path, config_path + ".corrupted")
return DEFAULT_CONFIG.copy()
except Exception as e:
print(f"Error loading configuration from {config_path}: {e}")
return DEFAULT_CONFIG.copy()
def save_config(self, config):
"""
Save configuration to the file.
Args:
config (dict): The configuration dictionary to save.
Returns:
bool: True if saved successfully, False otherwise.
"""
config_path = self._get_config_path()
try:
# Ensure the directory exists before writing
config_dir = os.path.dirname(config_path)
if config_dir and not os.path.exists(config_dir):
os.makedirs(config_dir)
# Write the config file
with open(config_path, 'w', encoding='utf-8') as f:
json.dump(config, f, indent=4)
# print(f"Configuration saved to: {config_path}") # Less verbose logging
return True
except TypeError as e:
print(f"Error saving configuration: Data not JSON serializable - {e}")
print(f"Problematic config data (partial): {str(config)[:500]}")
return False
except Exception as e:
print(f"Error saving configuration to {config_path}: {e}")
return False
def _ensure_backward_compatibility(self, config):
"""Ensure backward compatibility with older config formats."""
# Check if the new 'element_mappings' structure exists and is populated.
# If not, try to convert from older, separate mapping dictionaries.
if 'element_mappings' not in config or not isinstance(config['element_mappings'], list) or not config['element_mappings']:
print("Attempting conversion from older config format...")
element_mappings = []
# Define default values used during conversion
default_type = config.get('type', 'ia.display.view') # Old general type
default_props_path = config.get('path', 'Symbol-Views/Equipment-Views/Status') # Old general path
default_width = config.get('width', 14)
default_height = config.get('height', 14)
# Get old mapping dictionaries safely (default to empty dict)
element_type_mapping = config.get('element_type_mapping', {})
element_props_mapping = config.get('element_props_mapping', {})
element_label_prefix_mapping = config.get('element_label_prefix_mapping', {})
element_size_mapping = config.get('element_size_mapping', {})
# Keep track of SVG types processed to avoid duplicates from different old sources
processed_svg_types = set()
# Combine all known SVG types from the old mappings
all_svg_types = (set(element_type_mapping.keys()) |
set(element_props_mapping.keys()) |
set(element_label_prefix_mapping.keys()) |
set(element_size_mapping.keys()))
for svg_type in all_svg_types:
if not svg_type: continue # Skip empty keys
# Create entries for specific label prefixes if they exist
if svg_type in element_label_prefix_mapping:
prefix = element_label_prefix_mapping[svg_type]
if prefix: # Only if prefix is non-empty
mapping_key = (svg_type, prefix)
if mapping_key not in processed_svg_types:
width, height = self._get_size_from_old_mapping(element_size_mapping, svg_type, default_width, default_height)
mapping = {
'svg_type': svg_type,
'element_type': element_type_mapping.get(svg_type, default_type),
'label_prefix': prefix,
'props_path': element_props_mapping.get(svg_type, default_props_path),
'width': width,
'height': height,
'x_offset': 0, # Assume 0 offset in old format
'y_offset': 0,
'final_prefix': '', # No equivalent in old format
'final_suffix': ''
}
element_mappings.append(mapping)
processed_svg_types.add(mapping_key)
# Create entry for the SVG type without a label prefix (default)
mapping_key = (svg_type, "")
if mapping_key not in processed_svg_types:
width, height = self._get_size_from_old_mapping(element_size_mapping, svg_type, default_width, default_height)
mapping = {
'svg_type': svg_type,
'element_type': element_type_mapping.get(svg_type, default_type),
'label_prefix': "", # Empty prefix for the general case
'props_path': element_props_mapping.get(svg_type, default_props_path),
'width': width,
'height': height,
'x_offset': 0,
'y_offset': 0,
'final_prefix': '',
'final_suffix': ''
}
element_mappings.append(mapping)
processed_svg_types.add(mapping_key)
# Update the config with the new list format
config['element_mappings'] = element_mappings
print(f"Converted {len(element_mappings)} mappings to new format.")
# Optionally remove old keys after conversion (or keep for safety)
# keys_to_remove = ['type', 'path', 'width', 'height', 'element_type_mapping', ...]
# for key in keys_to_remove:
# config.pop(key, None)
return config
def _get_size_from_old_mapping(self, size_mapping, svg_type, default_w, default_h):
"""Helper to extract size from the old element_size_mapping format."""
if svg_type in size_mapping and isinstance(size_mapping[svg_type], dict):
width = size_mapping[svg_type].get('width', default_w)
height = size_mapping[svg_type].get('height', default_h)
# Basic validation
try: width = int(width)
except: width = default_w
try: height = int(height)
except: height = default_h
return width, height
return default_w, default_h
def _update_config_format(self, config):
"""
Update config format to ensure all required fields exist in element mappings.
Also ensures top-level keys from DEFAULT_CONFIG exist.
"""
# Ensure all top-level keys from DEFAULT_CONFIG exist
for key, default_value in DEFAULT_CONFIG.items():
if key not in config:
print(f"Adding missing config key: '{key}'")
config[key] = default_value
# Ensure 'element_mappings' exists and is a list
if 'element_mappings' not in config or not isinstance(config['element_mappings'], list):
print("Reinitializing 'element_mappings' list.")
config['element_mappings'] = DEFAULT_CONFIG['element_mappings'] # Use default
# Check and update each mapping dictionary within the list
updated_mappings = []
default_mapping = DEFAULT_CONFIG['element_mappings'][0] if DEFAULT_CONFIG['element_mappings'] else {}
for i, mapping in enumerate(config['element_mappings']):
if not isinstance(mapping, dict):
print(f"Warning: Skipping non-dictionary item in element_mappings at index {i}")
continue
updated_mapping = mapping.copy() # Work on a copy
missing_fields = False
# Check for essential fields and add defaults if missing
for field, default_val in default_mapping.items():
if field not in updated_mapping:
print(f"Adding missing field '{field}' to mapping for SVG type '{updated_mapping.get('svg_type', 'UNKNOWN')}'")
updated_mapping[field] = default_val
missing_fields = True
# Optionally, perform type validation/conversion here (e.g., ensure width/height are numbers)
for field in ['width', 'height', 'x_offset', 'y_offset']:
try:
if field in updated_mapping:
# Convert to int, handle potential errors
updated_mapping[field] = int(updated_mapping[field])
except (ValueError, TypeError):
print(f"Warning: Invalid value for '{field}' in mapping '{updated_mapping.get('svg_type')}'. Setting default.")
updated_mapping[field] = default_mapping.get(field, 0) # Default numeric to 0 or from DEFAULT_CONFIG
updated_mappings.append(updated_mapping)
# if missing_fields:
# print(f"Updated mapping: {updated_mapping}")
# Replace the old list with the updated one
config['element_mappings'] = updated_mappings
return config
# Example usage (optional, for testing)
# if __name__ == "__main__":
# manager = ConfigManager("test_config.json")
# print("--- Initializing ---")
# initial_config = manager.get_config()
# print("Initial config:", json.dumps(initial_config, indent=2))
#
# # Modify config
# initial_config['project_title'] = "Updated Project Title"
# initial_config['element_mappings'].append({
# "svg_type": "circle",
# "element_type": "ia.display.led_display",
# "label_prefix": "LED_",
# "props_path": "Symbols/LED",
# "width": 20,
# "height": 20,
# "x_offset": -10,
# "y_offset": -10,
# "final_prefix": "PFX_",
# "final_suffix": "_SFX"
# })
#
# print("\n--- Saving Modified Config ---")
# save_success = manager.save_config(initial_config)
# print(f"Save successful: {save_success}")
#
# print("\n--- Reloading Config ---")
# reloaded_config = manager.get_config()
# print("Reloaded config:", json.dumps(reloaded_config, indent=2))
#
# # Test backward compatibility (create a dummy old config)
# old_config_path = manager._get_config_path() + ".old"
# old_config_data = {
# "file_path": "/path/to/old.svg",
# "project_title": "Old Project",
# "element_type_mapping": {"rect": "ia.display.rect", "path": "ia.display.path"},
# "element_label_prefix_mapping": {"rect": "R_"}
# }
# with open(old_config_path, 'w') as f:
# json.dump(old_config_data, f, indent=4)
#
# print("\n--- Testing Backward Compatibility --- ")
# old_manager = ConfigManager(os.path.basename(old_config_path))
# loaded_old_config = old_manager.get_config()
# print("Loaded and converted old config:", json.dumps(loaded_old_config, indent=2))
#
# # Clean up test files
# # os.remove(manager._get_config_path())
# # os.remove(old_config_path)